Uh oh!
There was an error while loading. Please reload this page.
bugfix(physics): Fix diagonal movement speed discrepancy - #3003
Conversation
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Include/Common/GameDefines.h | Adds shared compatibility flags, but the Generals target has no implementation that consumes them. |
| GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp | Adds compatibility-gated diagonal compensation to the Zero Hour 2D and 3D forward-speed calculations. |
Prompt To Fix All With AI
### Issue 1
Core/GameEngine/Include/Common/GameDefines.h:91-98
**Generals ignores correction flags**
When the compatibility flags are disabled to enable the diagonal-speed correction, Generals sees these shared definitions but compiles its separate, unchanged `PhysicsUpdate.cpp`, whose 2D and 3D functions always use the retail square-root calculations. The Generals build therefore retains the diagonal movement speed discrepancy regardless of these flag values, while only Zero Hour receives the corrected branches.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (3): Last reviewed commit: "Polish comments" | Re-trigger Greptile
Uh oh!
There was an error while loading. Please reload this page.
gamezerve
commented
Jul 22, 2026
Why is the horizontal movement speed being increased? In retail, units already move at their locomotor-defined speed when traveling horizontally or vertically. Wouldn't slowing down diagonal movement be the more appropriate solution? Also, changing movement speeds will inevitably alter the timing of scripted in-game cinematics. |
xezon
commented
Jul 22, 2026
Because then on average the game unit movements will be around 20% slower than originally, noticably making the game play with less pace.
That is a fair point we probably need to think about. |
| // The inverse looks intuitively wrong, but it is correct, because the value returned by this function is | ||
| // used to determine the additional velocity needed to reach the target speed. | ||
| constexpr const Real DiagonalCompensation = 1.0f / 1.20710678f; | ||
| dot *= DiagonalCompensation; |
There was a problem hiding this comment.
This is wrong, the speed is not the dot product, the dot product tells you the difference in direction between the two vectors. If it goes negative then it means your vectors are going in opposite directions.
The speed of a vector is the magnitude of the vector.
The speed for 2D is:speed = sqrtf( sqr(m_vel.x) + sqr(m_vel.y) ); which you can then scale with a constant
The speed for 3D is:speed = sqrtf( sqr(m_vel.x) + sqr(m_vel.y) + sqr(m_vel.z) ); then the same can be scaled with a constant.
you still need to check the dot product and negate the speed if the dot product is negative.
There was a problem hiding this comment.
dot is correct.
Chat Gippy
Here's a side-by-side comparison using a true velocity magnitude of 100 in each case.
| Facing | Moving | dir | vel | Original Function | Dot Product |
|---|---|---|---|---|---|
| East | East | (1.000, 0.000) | (100.00, 0.00) | 100.00 | 100.00 |
| North | North | (0.000, 1.000) | (0.00, 100.00) | 100.00 | 100.00 |
| 45° | 45° | (0.707, 0.707) | (70.71, 70.71) | 70.71 | 100.00 |
| East | Northeast | (1.000, 0.000) | (70.71, 70.71) | 70.71 | 70.71 |
| 45° | East | (0.707, 0.707) | (100.00, 0.00) | 70.71 | 70.71 |
| 30° | 30° | (0.866, 0.500) | (86.60, 50.00) | 79.06 | 100.00 |
| 60° | 60° | (0.500, 0.866) | (50.00, 86.60) | 79.06 | 100.00 |
| 15° | 15° | (0.966, 0.259) | (96.59, 25.88) | 93.54 | 100.00 |
| 75° | 75° | (0.259, 0.966) | (25.88, 96.59) | 93.54 | 100.00 |
This reveals that the original function is effectively applying a heading-dependent scale factor:
0° / 90°: ×1.000
15° / 75°: ×0.935
30° / 60°: ×0.791
45°: ×0.707
So if getForwardSpeed2D() is used in movement logic rather than just for display, the old code was inherently reducing the reported speed whenever the unit faced away from the world axes. That could explain why replacing it with the mathematically correct dot product changed the feel of movement.
There was a problem hiding this comment.
The dot product is not correct for calculating the speed. The only relation the dot product has with the speed is the direction of the movement in relation to the orientation of the model/hull. So whether it is forwards or backwards etc.
in the code, Dir is the direction vector for the objects model/hull to tell which way it is facing and m_vel is the motion vector for the movement of the objects.
the dot product is used to tell the difference in angle between Dir and m_vel so we can determine if the object is moving backwards in relation to the direction it is facing. If the dot product is negative then the two vectors are facing in opposite directions and the speed will be negative relative to the objects orientation.
You have to workout the magnitude of m_vel to determine the speed of the object, which is the equivalent to using Pythagoras theorem to workout the hypotenuse of a triangle.
The flaw in the original code is that they used vy and vx which are intermediate products of calculating the dot product between Dir and m_vel. These should never be used outside of that calculation as they are meaningless outside of that context.
Since these intermediate products are not unit scaled they give the faster motion in the diagonal direction, but they also don't give the true speed either.
There was a problem hiding this comment.
Problem is speed = sqrtf( sqr(m_vel.x) + sqr(m_vel.y) ); does not account for object direction. If the object is facing sideways then it will not produce the same direction drag (or lack thereof). What the proposed solution does is eliminate the diagonal speed variance, but otherwise preserve the original average speed.
I tested it in game and it looked right, but I did not do a lab test. Maybe it needs a lab test.
There was a problem hiding this comment.
The m_vel value is already normalised, which means it correctly scales in all directions without the diagonal calculated vector magnitudes being larger than expected. The flaw in the original code is that they don't correctly calculate the speed since they use the intermediate dot product values which are not normalised values.
This function is just returning speed which is only a scalar value, it has no direction information within it apart from forwards and backwards.
Both calculations need to be done, speed = sqrtf( sqr(m_vel.x) + sqr(m_vel.y) ); and the dot product is used to determine if the speed is positive or negative.
Beyond this, to make the speeds, on average, closer to the original flawed speeds you can then scale the calculated speed just by multiplying it with a constant. This will scale in all directions due to m_vel already being normalised.
so finalSpeed = scalingValue * speed * dotProductDirection the dot product direction just being if it's positive or negative.
There was a problem hiding this comment.
i think it needs a test on helicopters to see what happened
hover locomotor is the only exception that oriention might be different with movement direction.
eg. 1 comanche and 1 battlemaster which has same speed moves parallelly towards same direction and BM is in comanche range.
There was a problem hiding this comment.
I just tested a Comanche Helicopter and it looked ok. What is the problem with Helicopters?
There was a problem hiding this comment.
i mean, if comanche not facing the moving direction, but attacking(therefore facing) another direction, in special case like orientation perpendicular to the movement direction, speed vector's projection on orientation vector could go deeply wrong, which is not likely to happen cuz there is nothing obviously wrong with the game.
the fact that nothing obviously wrong probably means orientation on hover units doesn't act like what we thought.
i'm not sure.
as for your previous jets circling test, shouldn't it be like jet's speed always have same direction as its orientation? why projection speed is different from real speed?
There was a problem hiding this comment.
ok it's a case that never happen in vanilla game.
in mod when helicopters turns to attack another unit the speed looks pretty normal, which means no issue
im not testing with dot product version because i knew even with EA version it's gonna be wrong somewhere
like if orientation vector is (0, 1) speed vector (10, 0) both dot and EA version will got 0 as an answer
still i don't know how exactly it works maybe need more investigation but it looks fine now.
another case which is reverse move that also have orientation doesn't match with speed direction but i saw they use negative speed eventually so it's two different stories
The amount of diagonal and straight movements is highly map dependent. A 2-player game where players start in the corners will have relatively more diagonal movements - therefore the game will be slower than before - than on a map where players start in the (middle) top and bottom - and increases the game speed compared to before. I feel like this solution is too crude. It is such a major hack that has significant impact on how the game feels. IF this solution is considered, it will need extensive testing among the community on different maps.. Also, as Pathfinding uses a grid based algorithm, therefore most movements are either horizontal/vertical or diagonal, but rarely any other angle. |
Float1ngFree
commented
Jul 26, 2026
Hell yeah, long time due! |
xezon
commented
Jul 29, 2026
Yes. But the total average of all movements will be right between former min and max speeds.
I would like to have this run as a trial.
I do not understand this statement. Movements are free into any direction, for both ground and air units. |
xezon
commented
Jul 29, 2026
I have addressed this and confirmed that it works correctly in mission cinematics. From my POV this change is final right now. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
xezon
commented
Aug 1, 2026
Needs review. |
penfriendz
commented
Aug 1, 2026
Is the scaling factor 1.207 (v(x) = 1+4/pi*(sqrt(2)-1)*x) or 4/pi ~= 1.27 (v(x) = cos(x) + sin(x) if x is positive)? |
xezon
commented
Aug 1, 2026
Here it is (1 + sqrt(2)) / 2 |
| // Inverse scales len by (1 + sqrt(3)) / 2 to adjust to the average of the former min/max movement speed. | ||
| // The inverse looks intuitively wrong, but it is correct, because the value returned by this function is | ||
| // used to determine the additional velocity needed to reach the target speed. | ||
| constexpr const Real DiagonalCompensation = 1.0f / 1.36602540f; |
There was a problem hiding this comment.
getForwardSpeed3D has a single consumer, moveTowardsPositionThrust (Locomotor.cpp:1911). (1+sqrt(3))/2 assumes the worst case dir = (1,1,1)/sqrt(3), i.e. 35.3 degrees pitch and 45 degrees yaw. For near-level motion dir.z is approximately 0 and the range collapses to [1, sqrt(2)], the same as 2D.
So LOCO_THRUST objects settle at 1.366x their INI speed while every 2D consumer settles at 1.207x, a 13.2% relative shift with no retail equivalent. Note LOCO_WINGS routes through moveTowardsPositionOther (Locomotor.cpp:1873) and uses the 2D getter, so this also splits thrust objects from ordinary aircraft.
What pitch distribution do THRUST paths actually have? If they are mostly level, the 2D constant is the consistent choice.
There was a problem hiding this comment.
THRUST is used by missiles, including the Dragon Tank flame thrower.
It is a fair suspicion that missile movements are in 2D space, for example from Rocket Buggies, Rocket Men, Patriots, Scud Storm...
There are missiles that will fly in 3D space, such as missiles from planes & helicopters to ground, Tomahawk and Scud Missile, Spectre Howitzer, Nuclear Cannon Shell.
It should probably scale closer to the 2D scale. Maybe as a compromise we just make it a bit faster than the 2D scale? Or we can try to use different scale depending on the Z velocity. If no or little Z, then 2D scale, otherwise 3D scale.
There was a problem hiding this comment.
I have reworked the compensation values after chatting with Mr Claude Opus.
| #if PRESERVE_RETAIL_SCRIPTED_PHYSICS_FORWARD_SPEED | ||
| if (const AIUpdateInterface *ai = getObject()->getAIUpdateInterface()) | ||
| { | ||
| if (ai->getLastCommandSource() == CMD_FROM_SCRIPT) |
There was a problem hiding this comment.
This gate breaks inside a single scripted command. AIStates.cpp:3651 calls ai->friend_setLastCommandSource(CMD_FROM_AI) when a scripted attack-move auto-acquires a target, and JetAIUpdate.cpp:1999 / :2560 do the same on takeoff and return-for-landing. A unit issued CMD_FROM_SCRIPT silently switches to the new math partway through the move.
m_lastCommandSource is documented at AIUpdate.h:693 as provenance for the immediately following SetState, not as durable policy. It is never cleared, and it is serialized (AIUpdate.cpp:5080), so saves carry whatever value was last written.
A flag owned by the script action would survive these transitions. Separately, objects with no AIUpdateInterface get no preservation at all. Same applies to the duplicate at line 1021.
There was a problem hiding this comment.
I have reworked this. Now uses Letter Box to determine cinematic sequences, which is good enough for the Campaign Missions.
bobtista
commented
Aug 8, 2026
On the constant, for reference: the true per-heading factor is Answering penfriendz:
|
Mauller
commented
Aug 10, 2026
If the dot product is to be used to determine the velocity in the direction the object is facing then there should be a debug assert added to make sure that the dir vector is always normalised.
If the dir vector is not normalised then the expectation that we are getting the magnitude of the velocity breaks. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| Real PhysicsBehavior::getForwardSpeed3D() const | ||
| { | ||
| Vector3 dir = getObject()->getTransformMatrix()->Get_X_Vector(); | ||
There was a problem hiding this comment.
per @Mauller suggestion an assert to ensure dir is normalized would be preferable here.
float dir_length = dir.Length();
`DEBUG_ASSERT(dir_length > 0.999f && dir_length < 1.001f, "Directional vector is not normalized");There was a problem hiding this comment.
We implicitly expect that a rotation matrix has normalized column vectors. Why do we need to assert it? In what event do we expect the transform matrix to not supply normalized directions?
Chat Gippy: https://chatgpt.com/share/6a85ea13-640c-83eb-84c1-3de128c8e4a4
19ada89 to
1b00149Comparexezon
commented
Aug 23, 2026
I have completely reworked this change after several review rounds with Claude Opus. |
| protected: | ||
| private: | ||
| /// TheSuperHackers @bugfix Speeds authored in INI are understated by the forward speed the Locomotor measures |
| Real getActualMinTurnSpeed() const; | ||
| /// Scale a speed that has no stored counterpart because it was not authored on this template. | ||
| Real scaleSpeed(Real speed) const; |
This change fixes the diagonal movement speed discrepancy.
The new 2d and 3d speeds are scaled to the average of the former min and max speeds.
TODO