Browse Source

Use helpers when calculating missile velocity

eventually maybe we can move to fixed point approximations instead of using hypot and casts to double/float? probably way slower than using the FPU on modern systems though :D
pull/4661/head
ephphatha 4 years ago committed by Anders Jenbo
parent
commit
606cc162a1
  1. 70
      Source/engine/displacement.hpp
  2. 16
      Source/missiles.cpp
  3. 33
      test/math_test.cpp

70
Source/engine/displacement.hpp

@ -78,6 +78,20 @@ struct Displacement {
return *this; return *this;
} }
constexpr Displacement &operator/=(const int factor)
{
deltaX /= factor;
deltaY /= factor;
return *this;
}
constexpr Displacement &operator/=(const float factor)
{
deltaX = static_cast<int>(deltaX / factor);
deltaY = static_cast<int>(deltaY / factor);
return *this;
}
constexpr friend Displacement operator+(Displacement a, Displacement b) constexpr friend Displacement operator+(Displacement a, Displacement b)
{ {
a += b; a += b;
@ -102,16 +116,43 @@ struct Displacement {
return a; return a;
} }
constexpr friend Displacement operator/(Displacement a, const int factor)
{
a /= factor;
return a;
}
constexpr friend Displacement operator/(Displacement a, const float factor)
{
a /= factor;
return a;
}
constexpr friend Displacement operator-(const Displacement &a) constexpr friend Displacement operator-(const Displacement &a)
{ {
return { -a.deltaX, -a.deltaY }; return { -a.deltaX, -a.deltaY };
} }
constexpr friend Displacement operator<<(Displacement a, unsigned factor)
{
return { a.deltaX << factor, a.deltaY << factor };
}
constexpr friend Displacement operator>>(Displacement a, unsigned factor)
{
return { a.deltaX >> factor, a.deltaY >> factor };
}
constexpr friend Displacement abs(Displacement a) constexpr friend Displacement abs(Displacement a)
{ {
return { abs(a.deltaX), abs(a.deltaY) }; return { abs(a.deltaX), abs(a.deltaY) };
} }
float magnitude() const
{
return static_cast<float>(hypot(deltaX, deltaY));
}
/** /**
* @brief Returns a new Displacement object in screen coordinates. * @brief Returns a new Displacement object in screen coordinates.
* *
@ -154,9 +195,34 @@ struct Displacement {
return { (2 * deltaY + deltaX) / 8, (2 * deltaY - deltaX) / 8 }; return { (2 * deltaY + deltaX) / 8, (2 * deltaY - deltaX) / 8 };
} }
constexpr Displacement operator>>(size_t factor) /**
* @brief Returns a 16 bit fixed point normalised displacement in isometric projection
*
* This will return a displacement of the form (-1.0 to 1.0, -0.5 to 0.5), to get a full tile offset you can multiply by 16
*/
Displacement worldToNormalScreen() const
{
// Most transformations between world and screen space take shortcuts when scaling to simplify the math. This
// routine is typically used with missiles where we want a normal vector that can be multiplied with a target
// velocity (given in pixels). We could normalize the vector first but then we'd need to scale it during
// rotation from world to screen space. To save performing unnecessary divisions we rotate first without
// correcting the scaling. This gives a vector in elevation projection aligned with screen space.
Displacement rotated { (deltaY - deltaX), -(deltaY + deltaX) };
// then normalize this vector
Displacement rotatedAndNormalized = rotated.normalized();
// and finally scale the y axis to bring it to isometric projection
return { rotatedAndNormalized.deltaX, rotatedAndNormalized.deltaY / 2 };
}
/**
* @brief Calculates a 16 bit fixed point normalized displacement (having magnitude of ~1.0) from the current Displacement
*/
Displacement normalized() const
{ {
return Displacement(deltaX >> factor, deltaY >> factor); float magnitude = this->magnitude();
Displacement normalDisplacement = *this << 16;
normalDisplacement /= magnitude;
return normalDisplacement;
} }
constexpr Displacement Rotate(int quadrants) constexpr Displacement Rotate(int quadrants)

16
Source/missiles.cpp

@ -100,22 +100,18 @@ constexpr Direction16 Direction16Flip(Direction16 x, Direction16 pivot)
return static_cast<Direction16>(ret); return static_cast<Direction16>(ret);
} }
void UpdateMissileVelocity(Missile &missile, Point destination, int v) void UpdateMissileVelocity(Missile &missile, Point destination, int velocityInPixels)
{ {
missile.position.velocity = { 0, 0 }; missile.position.velocity = { 0, 0 };
if (missile.position.tile == destination) if (missile.position.tile == destination)
return; return;
// rotate so that +x leads to the right of screen and +y leads to the bottom (screen space but without the scaling factor) // Get the normalized vector in isometric projection
double dxp = (destination.x + missile.position.tile.y - missile.position.tile.x - destination.y); Displacement fixed16NormalVector = (missile.position.tile - destination).worldToNormalScreen();
double dyp = (destination.y + destination.x - missile.position.tile.x - missile.position.tile.y);
double dr = sqrt(dxp * dxp + dyp * dyp); // Multiplying by the target velocity gives us a scaled velocity vector.
// velocity is stored in screen coordinates so apply the scaling factor to the y axis while normalizing. missile.position.velocity = fixed16NormalVector * velocityInPixels;
double normalizedX = dxp / dr;
double normalizedY = dyp / dr / 2;
missile.position.velocity.deltaX = static_cast<int>(normalizedX * (v << 16));
missile.position.velocity.deltaY = static_cast<int>(normalizedY * (v << 16));
} }
/** /**

33
test/math_test.cpp

@ -6,7 +6,7 @@ namespace devilution {
TEST(MathTest, WorldScreenTransformation) TEST(MathTest, WorldScreenTransformation)
{ {
Displacement offset = { 5, 2 }; Displacement offset { 5, 2 };
// Diablo renders tiles with the world origin translated to the top left of the screen, while the normal convention // Diablo renders tiles with the world origin translated to the top left of the screen, while the normal convention
// has the screen origin at the bottom left. This means that we end up with negative offsets in screen space for // has the screen origin at the bottom left. This means that we end up with negative offsets in screen space for
// tiles in world space where x > y // tiles in world space where x > y
@ -28,4 +28,35 @@ TEST(MathTest, WorldScreenTransformation)
EXPECT_EQ(cursorPosition.screenToWorld().worldToScreen(), Displacement(320, -160)); EXPECT_EQ(cursorPosition.screenToWorld().worldToScreen(), Displacement(320, -160));
} }
TEST(MathTest, NormalizeDisplacement)
{
// Normalizing displacements transforms the value into 16 bit fixed point representations
Displacement vector { 5, 0 };
EXPECT_FLOAT_EQ(vector.magnitude(), 5);
EXPECT_EQ(vector.normalized(), Displacement(1 << 16, 0)); // (1.0, 0.0)
vector = { 3, 4 };
EXPECT_FLOAT_EQ(vector.magnitude(), 5);
EXPECT_EQ(vector.normalized(), Displacement(39321, 52428)); // ~(0.6, 0.8)
vector = { -5, 2 };
EXPECT_FLOAT_EQ(vector.magnitude(), 5.3851647f);
EXPECT_EQ(vector.normalized(), Displacement(-60848, 24339)); // ~(-0.92, 0.37)
}
TEST(MathTest, MissileTransformation)
{
// starting with a Displacement 2 world units West results in a vector pointing left of screen
EXPECT_EQ(Displacement(2, -2).worldToNormalScreen(), Displacement(-65536, 0));
// if it's not normalizing the vector then it's a problem :D
EXPECT_EQ(Displacement(4, -4).worldToNormalScreen(), Displacement(-65536, 0));
// Because of the isometric projection the y axis gets squashed
EXPECT_EQ(Displacement(8, 1).worldToNormalScreen(), Displacement(-40235, -25865)); // ~(0.6, 0.8/2)
// in elevation projection this would be a vector with x == y, isometric projection means y == x/2
EXPECT_EQ(Displacement(8, 0).worldToNormalScreen(), Displacement(-46340, -23170)); // ~(0.7, 0.7/2)
}
} // namespace devilution } // namespace devilution

Loading…
Cancel
Save