Skip to content

bugfix(physics): Fix diagonal movement speed discrepancy - #3003

Open
xezon wants to merge 21 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-diagonal-movement-speed
Open

xezon wants to merge 21 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-diagonal-movement-speed

Conversation

@xezon

@xezon xezon commented Jul 22, 2026 •

Copy link
Copy Markdown

This change fixes the diagonal movement speed discrepancy. The new 2d and 3d speeds are scaled by the harmonic mean of the two extremes of the original forward speed discrepancy.

GameDefines gets 3 new defines to control the fix in several ways:

// Whether to preserve the 1.41x speed discrepancy between
// straight and diagonal movements of all objects that move via a Locomotor.
#define PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY (1)

// Whether to preserve the harmonic mean of the straight and diagonal speeds
// of the original forward speed discrepancy bug.
// The locomotor speeds from the INI files are effectively scaled up
// a bit so that on average the world objects travel at comparable speeds.
// Set this to 0 when speeds are set correctly by INI settings (recommended).
#define PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_AVERAGE (1)

// Whether to preserve the 1.41x speed discrepancy between
// straight and diagonal movements of all objects during cinematics.
// Is mostly relevant for the original campaign missions.
#define PRESERVE_RETAIL_PHYSICS_FORWARD_SPEED_DISCREPANCY_IN_CINEMATICS (1)

AI Summary

Retail units moved at 1x their authored speed on axis aligned headings and up to √2x on diagonals. The fix makes the speed uniform, so one constant has to decide where inside that 1 to √2 range the new speed sits.

The candidates.

Option Value Axis leg vs retail Diagonal leg vs retail
Harmonic mean over all headings 1.16299814 +16.30% −17.76%
Harmonic mean of the two extremes 1.17157288 +17.16% −17.16%
Arithmetic mean over all headings 1.18034060 +18.03% −16.54%
Midpoint of the range (original value, already rejected) 1.20710678 +20.71% −14.64%

What the two means over all headings stand for. Both preserve the retail average speed, but under different assumptions:

  • Harmonic assumes every heading gets the same share of the distance travelled. It then preserves average travel time.
  • Arithmetic assumes every heading gets the same share of the time spent moving. It then preserves average distance covered per time.
  • In retail these two assumptions exclude each other, because units spent less time on the headings where they were faster.
  • A path-driven game fits the distance assumption better, since a move order fixes the route and not the duration. Under that assumption the arithmetic mean makes the game 1.49% faster than retail.

Why we picked the harmonic mean of the two extremes.

  • Its claim holds unconditionally. Axis headings get 17.16% faster and diagonal headings 17.16% slower, and no heading changes by more than that. The means over all headings only preserve an average if headings are evenly mixed, which real maps and base layouts do not guarantee.
  • It is the safe middle. It sits about 0.74% from each of the other two, so it is off by at most half of what picking the wrong assumption would cost.
  • It is the simplest. 4 - 2*sqrt(2) needs no elliptic integral and can be explained in one sentence.

What it gives up. It preserves no game-wide total exactly, neither average travel time nor average distance per time. The symmetry is in speed only; in travel time an axis leg is 14.6% shorter and a diagonal leg 20.7% longer.

Scale of the decision. All three candidates are within 1.5% of each other, while any single route changes by up to about 17% depending on its heading. The choice is about which justification to stand behind, not about noticeable gameplay.

TODO

  • Replicate in Generals

@xezon xezon added Bug Something is not working right, typically is user facing Controversial Is controversial Major Severity: Minor < Major < Critical < Blocker Unit AI Is related to unit behavior Gen Relates to Generals ZH Relates to Zero Hour NoRetail This fix or change is not applicable with Retail game compatibility labels Jul 22, 2026
@greptile-apps

greptile-apps Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge.

Summary

This revision completes the cross-variant movement correction and addresses the prior review findings.

  • Replicates corrected 2D and 3D forward-speed calculations and locomotor speed scaling in Generals.
  • Uses runtime cinematic selection instead of an invalid constant-expression branch.
  • Tracks letterbox state and synchronizes both enabled and disabled display state after loading.

Reviews (11) · Last reviewed commit: "Replicate in Generals" · Reviewed by Greptile

Comment thread Core/GameEngine/Include/Common/GameDefines.h Outdated
@gamezerve

Copy link
Copy Markdown

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

xezon commented Jul 22, 2026

Copy link
Copy Markdown
Author

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?

Because then on average the game unit movements will be around 20% slower than originally, noticably making the game play with less pace.

Also, changing movement speeds will inevitably alter the timing of scripted in-game cinematics.

That is a fair point we probably need to think about.

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
@Skyaero42

Skyaero42 commented Jul 25, 2026 •

Copy link
Copy Markdown

Because then on average the game unit movements will be around 20% slower than originally, noticably making the game play with less pace.

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

Copy link
Copy Markdown

Hell yeah, long time due!
Will need to adjust deliveries of transport payloads for OCLs and some other movement related properties, but this must be done regardless of any naysayers.

@xezon

xezon commented Jul 29, 2026

Copy link
Copy Markdown
Author

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.

Yes. But the total average of all movements will be right between former min and max speeds.

IF this solution is considered, it will need extensive testing among the community on different maps..

I would like to have this run as a trial.

Also, as Pathfinding uses a grid based algorithm, therefore most movements are either horizontal/vertical or diagonal, but rarely any other angle.

I do not understand this statement. Movements are free into any direction, for both ground and air units.

@xezon

xezon commented Jul 29, 2026

Copy link
Copy Markdown
Author

Also, changing movement speeds will inevitably alter the timing of scripted in-game cinematics.

I have addressed this and confirmed that it works correctly in mission cinematics.

From my POV this change is final right now.

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
Comment thread Core/GameEngine/Include/Common/GameDefines.h Outdated
@xezon
xezon requested review from Mauller and Skyaero42 August 1, 2026 08:17
@xezon

xezon commented Aug 1, 2026

Copy link
Copy Markdown
Author

Needs review.

@penfriendz

Copy link
Copy Markdown

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

xezon commented Aug 1, 2026

Copy link
Copy Markdown
Author

Here it is (1 + sqrt(2)) / 2

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
@bobtista

bobtista commented Aug 8, 2026

Copy link
Copy Markdown

On the constant, for reference: the true per-heading factor is 1/sqrt(cos^4(t) + sin^4(t)), equal to 1.0 at 0 degrees and sqrt(2) at 45 degrees. Averaged over uniform headings that is 1.1803 (1.1630 harmonic, if the goal is preserving traversal time).

Answering penfriendz: 4/pi averages cos(t)+sin(t), which matches the true curve only at the endpoints. At 30 degrees it gives 1.3660 versus 1.2649 actual, about 8% high, and it sits 5.5% above 1.2071.

(1+sqrt(2))/2 is the mean of the two extremes, which is exact for a distribution concentrated at 0 and 45 degrees. Ground pathfinding expands eight neighbours (AIPathfind.cpp:6007), which supports Skyaero42's point, though it does not make runtime headings exactly bimodal. Aircraft paths are destination-based, so the same reasoning does not carry over. Worth stating in the comment which distribution the constants target.

@Mauller

Mauller commented Aug 10, 2026

Copy link
Copy Markdown

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.

sqrt(sqr(dir->x) + sqr(dir->y) == 1 etc, except it will need to take the float inaccuracy into account to do it properly.

If the dir vector is not normalised then the expectation that we are getting the magnitude of the velocity breaks.

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
@xezon
xezon force-pushed the xezon/fix-diagonal-movement-speed branch from 19ada89 to 1b00149 Compare August 23, 2026 11:51
@xezon

xezon commented Aug 23, 2026

Copy link
Copy Markdown
Author

I have completely reworked this change after several review rounds with Claude Opus.

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: fa6fbb0e-b436-4a43-b174-6baa125115d0

📥 Commits

Reviewing files that changed from the base of the PR and between 3d75bed and 72ffa99.

📒 Files selected for processing (11)
  • Core/GameEngine/Include/Common/GameDefines.h
  • Core/GameEngine/Include/Common/GameUtility.h
  • Core/GameEngine/Source/Common/GameUtility.cpp
  • GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h
  • GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h
  • GeneralsMD/Code/GameEngine/Include/GameLogic/ScriptEngine.h
  • GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Locomotor.cpp
  • GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JetAIUpdate.cpp
  • GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp
  • GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp
  • GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp Outdated
@xezon

xezon commented Sep 20, 2026

Copy link
Copy Markdown
Author

All review comments have been worked on. Ready for Review.

@stephanmeesters stephanmeesters left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good to me.

@Skyaero42 there's requested changes from you open

@Skyaero42

Skyaero42 commented Sep 26, 2026 •

Copy link
Copy Markdown

I am not confident about this PR.

I have expressed my concerns earlier about the correction factor and its impact on the game play.

I see potential issues for cross-maps vs opposite player maps, as timings - and therefore strategies will change. A diagonal attack takes roughly 22% longer, while a straight attack is like 15% slower. This may confuse the pro-players.

I also am concerned about modded games like AOD, where timings will be different than before - which could have a great impact on the difficulty of the game.

Additionally, I think we need the harmonic mean here instead of the arithmetic - as the goal is the similar times going across a path. You would have to calculate 1 / avg(1/ v(theta)), which according to Claude is 1.16300031 - slightly lower than the arithmetic one.

I also don't like that the letterbox - a script action for a UI effect - is now used as a logic trigger. It also doesn't fully resolve the issue. Anything in game that is speed related, like a convoy or escort type of mission - will have its timings off. I'm not sure if the single player games do have any of these types, but there are a lot custom-made missions that may not be so strict on using letterbox for scripted movements.

I understand this is seen as a long-awaited bug fix in the game - rightfully so. But I believe the potential consequences on (custom) maps are being underestimated.

I'm dismissing my review - because I don't want to be a blocker in this. But I can't approve the PR in good faith.

@xezon

xezon commented Sep 27, 2026

Copy link
Copy Markdown
Author

I am not confident about this PR.

I have expressed my concerns earlier about the correction factor and its impact on the game play.

I see potential issues for cross-maps vs opposite player maps, as timings - and therefore strategies will change. A diagonal attack takes roughly 22% longer, while a straight attack is like 15% slower. This may confuse the pro-players.

I also am concerned about modded games like AOD, where timings will be different than before - which could have a great impact on the difficulty of the game.

The fix is opt-in. Default disabled. We cannot modify all maps now to accomodate this code fix. It's just the way it is. Map creators can create new maps for fixed movement speeds.

There is an argument to make that maybe this could be a GameData.ini opt-in, rather than compile opt-in. But making this a per map.ini setting would be silly, because then maps can play differently under the same Mod.

Additionally, I think we need the harmonic mean here instead of the arithmetic - as the goal is the similar times going across a path. You would have to calculate 1 / avg(1/ v(theta)), which according to Claude is 1.16300031 - slightly lower than the arithmetic one.

Why? What are the benefits? I remember I chatted with Claude about this time ago and we settled on arithmetic mean as the logical choice for preserving the former average speed for heading into all directions with equal probability.

I also don't like that the letterbox - a script action for a UI effect - is now used as a logic trigger. It also doesn't fully resolve the issue. Anything in game that is speed related, like a convoy or escort type of mission - will have its timings off. I'm not sure if the single player games do have any of these types, but there are a lot custom-made missions that may not be so strict on using letterbox for scripted movements.

Please suggest an alternative if there is a better way. The letter-box was picked because every cinematic in the campaign is shown with the letter box, it is determistic for logic and therefore works for this use case. It can be opted out of with a compile option.

I understand this is seen as a long-awaited bug fix in the game - rightfully so. But I believe the potential consequences on (custom) maps are being underestimated.

They are not underestimated. Everyone knows this is highly controversial which is why this is default disabled.

@xezon
xezon force-pushed the xezon/fix-diagonal-movement-speed branch from 30a0132 to d30582a Compare October 11, 2026 09:37
@xezon

xezon commented Oct 11, 2026

Copy link
Copy Markdown
Author

Additionally, I think we need the harmonic mean here instead of the arithmetic - as the goal is the similar times going across a path. You would have to calculate 1 / avg(1/ v(theta)), which according to Claude is 1.16300031 - slightly lower than the arithmetic one.

Ok. I have discussed this with Claude as well again, and we have settled on the harmonic mean of the two extremes, which sits exactly between the harmonic mean and arithmetic mean.

The switch is done: DiagonalCompensation2D is now 1.17157288f (4 - 2*sqrt(2)), and the comment block in Locomotor.cpp:82 plus the macro comment in GameDefines.h:105 are updated to match. I did not build it (the code change is one literal) and nothing is committed.

Summary

The question. Retail units moved at 1x their authored speed on axis aligned headings and up to √2x on diagonals. The fix makes the speed uniform, so one constant has to decide where inside that 1 to √2 range the new speed sits. The branch used the arithmetic mean over all headings, and the question was whether the harmonic mean would be better.

The candidates.

Option Value Axis leg vs retail Diagonal leg vs retail
Harmonic mean over all headings 1.16299814 +16.30% −17.76%
Harmonic mean of the two extremes 1.17157288 +17.16% −17.16%
Arithmetic mean over all headings 1.18034060 +18.03% −16.54%
Midpoint of the range (original value, already rejected) 1.20710678 +20.71% −14.64%

What the two means over all headings stand for. Both preserve the retail average speed, but under different assumptions:

  • Harmonic assumes every heading gets the same share of the distance travelled. It then preserves average travel time.
  • Arithmetic assumes every heading gets the same share of the time spent moving. It then preserves average distance covered per time.
  • In retail these two assumptions exclude each other, because units spent less time on the headings where they were faster.
  • A path-driven game fits the distance assumption better, since a move order fixes the route and not the duration. Under that assumption the arithmetic mean makes the game 1.49% faster than retail.

Why we picked the harmonic mean of the two extremes.

  • Its claim holds unconditionally. Axis headings get 17.16% faster and diagonal headings 17.16% slower, and no heading changes by more than that. The means over all headings only preserve an average if headings are evenly mixed, which real maps and base layouts do not guarantee.
  • It is the safe middle. It sits about 0.74% from each of the other two, so it is off by at most half of what picking the wrong assumption would cost.
  • It is the simplest. 4 - 2*sqrt(2) needs no elliptic integral and can be explained in one sentence.

What it gives up. It preserves no game-wide total exactly, neither average travel time nor average distance per time. The symmetry is in speed only; in travel time an axis leg is 14.6% shorter and a diagonal leg 20.7% longer.

Scale of the decision. All three candidates are within 1.5% of each other, while any single route changes by up to about 17% depending on its heading. The choice is about which justification to stand behind, not about noticeable gameplay.

@xezon

xezon commented Oct 11, 2026

Copy link
Copy Markdown
Author

Replicated in Generals without conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Controversial Is controversial Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker NoRetail This fix or change is not applicable with Retail game compatibility Unit AI Is related to unit behavior ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Locomotor makes units move faster diagonally

9 participants