From 1ae119adf3456972c85ebe5fc6a9b0a941bae338 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:46:36 -0800 Subject: [PATCH 01/21] Decouple pitch/roll rate damping from render update --- .../GameEngine/Source/GameClient/Drawable.cpp | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index c8f9b0f56ac..a89eb9f6094 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1786,11 +1786,17 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // the ground can only push back if we're touching it if (overlapped || m_locoInfo->m_overlapZ <= 0.0f) { - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + + m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) - m_locoInfo->m_pitchRate *= 0.5f; + { + const Real pitchDamp = 1.0f - (1.0f - 0.5f) * timeScale; + m_locoInfo->m_pitchRate *= pitchDamp; + } - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; @@ -2013,11 +2019,17 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // the ground can only push back if we're touching it if (!airborne) { - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + + m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) - m_locoInfo->m_pitchRate *= 0.5f; + { + const Real pitchDamp = 1.0f - (1.0f - 0.5f) * timeScale; + m_locoInfo->m_pitchRate *= pitchDamp; + } - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; From 0a2205f3ab2d8a45c34bba01cacea1d927a65f82 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:46:36 -0800 Subject: [PATCH 02/21] Replicate to generals --- .../GameEngine/Source/GameClient/Drawable.cpp | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index dcb9fc153f7..7b4040e6dec 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1640,11 +1640,17 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // the ground can only push back if we're touching it if (overlapped || m_locoInfo->m_overlapZ <= 0.0f) { - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + + m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) - m_locoInfo->m_pitchRate *= 0.5f; + { + const Real pitchDamp = 1.0f - (1.0f - 0.5f) * timeScale; + m_locoInfo->m_pitchRate *= pitchDamp; + } - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; @@ -1866,11 +1872,17 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // the ground can only push back if we're touching it if (!airborne) { - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + + m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) - m_locoInfo->m_pitchRate *= 0.5f; + { + const Real pitchDamp = 1.0f - (1.0f - 0.5f) * timeScale; + m_locoInfo->m_pitchRate *= pitchDamp; + } - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; From 2fdfe628693951fba0663a915fc5f06f88920e76 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 17:23:59 -0800 Subject: [PATCH 03/21] Decouple wheel suspension offset from render update --- .../GameEngine/Source/GameClient/Drawable.cpp | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index a89eb9f6094..0373872f2d6 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1960,15 +1960,20 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; m_locoInfo->m_wheelInfo.m_framesAirborneCounter++; + + // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real suspensionFactor = 0.5f * timeScale; + if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) { - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * suspensionFactor; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) * suspensionFactor; } else { - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * suspensionFactor; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) * suspensionFactor; } } // Calculate suspension info. @@ -2130,27 +2135,31 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI newInfo.m_rearLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); newInfo.m_frontLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); } + // TheSuperHackers @tweak Wheel compression dampening is now decoupled from the render update. + const Real compressionTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real compressionFactor = 0.5f * compressionTimeScale; + if (newInfo.m_frontLeftHeightOffset < m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset += (newInfo.m_frontLeftHeightOffset - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset += (newInfo.m_frontLeftHeightOffset - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset = newInfo.m_frontLeftHeightOffset; } if (newInfo.m_frontRightHeightOffset < m_locoInfo->m_wheelInfo.m_frontRightHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset += (newInfo.m_frontRightHeightOffset - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_frontRightHeightOffset += (newInfo.m_frontRightHeightOffset - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_frontRightHeightOffset = newInfo.m_frontRightHeightOffset; } if (newInfo.m_rearLeftHeightOffset < m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (newInfo.m_rearLeftHeightOffset - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (newInfo.m_rearLeftHeightOffset - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset = newInfo.m_rearLeftHeightOffset; } if (newInfo.m_rearRightHeightOffset < m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (newInfo.m_rearRightHeightOffset - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (newInfo.m_rearRightHeightOffset - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = newInfo.m_rearRightHeightOffset; } @@ -2261,14 +2270,19 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; m_locoInfo->m_wheelInfo.m_framesAirborneCounter++; + + // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real suspensionFactor = 0.5f * timeScale; + if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) { - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * suspensionFactor; m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset; } else { - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * suspensionFactor; m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset; } } @@ -2440,10 +2454,14 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf newInfo.m_rearLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); } */ + // TheSuperHackers @tweak Wheel compression dampening is now decoupled from the render update. + const Real compressionTimeScale2 = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real compressionFactor2 = 0.5f * compressionTimeScale2; + if (newInfo.m_frontLeftHeightOffset < m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset += (newInfo.m_frontLeftHeightOffset - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset += (newInfo.m_frontLeftHeightOffset - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) * compressionFactor2; m_locoInfo->m_wheelInfo.m_frontRightHeightOffset = m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset; } else @@ -2454,7 +2472,7 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf if (newInfo.m_rearLeftHeightOffset < m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (newInfo.m_rearLeftHeightOffset - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (newInfo.m_rearLeftHeightOffset - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * compressionFactor2; m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset; } else From aba9ba8f3cf8c19a8e29f651749793ffc35b2583 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 18:47:56 -0800 Subject: [PATCH 04/21] Replicate to generals --- .../GameEngine/Source/GameClient/Drawable.cpp | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 7b4040e6dec..ecdd46a5fe0 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1813,15 +1813,20 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; m_locoInfo->m_wheelInfo.m_framesAirborneCounter++; + + // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real suspensionFactor = 0.5f * timeScale; + if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) { - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * suspensionFactor; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (MAX_SUSPENSION_EXTENSION - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) * suspensionFactor; } else { - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * suspensionFactor; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (0 - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) * suspensionFactor; } } // Calculate suspension info. @@ -1983,27 +1988,31 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI newInfo.m_rearLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); newInfo.m_frontLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); } + // TheSuperHackers @tweak Wheel compression dampening is now decoupled from the render update. + const Real compressionTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real compressionFactor = 0.5f * compressionTimeScale; + if (newInfo.m_frontLeftHeightOffset < m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset += (newInfo.m_frontLeftHeightOffset - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset += (newInfo.m_frontLeftHeightOffset - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset = newInfo.m_frontLeftHeightOffset; } if (newInfo.m_frontRightHeightOffset < m_locoInfo->m_wheelInfo.m_frontRightHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset += (newInfo.m_frontRightHeightOffset - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_frontRightHeightOffset += (newInfo.m_frontRightHeightOffset - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_frontRightHeightOffset = newInfo.m_frontRightHeightOffset; } if (newInfo.m_rearLeftHeightOffset < m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (newInfo.m_rearLeftHeightOffset - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (newInfo.m_rearLeftHeightOffset - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset = newInfo.m_rearLeftHeightOffset; } if (newInfo.m_rearRightHeightOffset < m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) { // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (newInfo.m_rearRightHeightOffset - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset)/2.0f; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (newInfo.m_rearRightHeightOffset - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) * compressionFactor; } else { m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = newInfo.m_rearRightHeightOffset; } From 84410e19730384f22273da722c65b4a941bdc317 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 19:28:32 -0800 Subject: [PATCH 05/21] Decouple missile wobble and thrust roll from render update --- .../GameEngine/Source/GameClient/Drawable.cpp | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 0373872f2d6..dc327b81695 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1449,6 +1449,11 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI Real MAX_WOBBLE = locomotor->getMaxWobble(); Real MIN_WOBBLE = locomotor->getMinWobble(); + // TheSuperHackers @tweak Wobble and thrust roll rates are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real scaledWobbleRate = WOBBLE_RATE * timeScale; + const Real scaledThrustRoll = THRUST_ROLL * timeScale; + // // this is a kind of quick thrust implementation cause we need scud missiles to wobble *now*, // we deal with just adjusting pitch, yaw, and roll just a little bit @@ -1463,15 +1468,15 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( m_locoInfo->m_pitch < MAX_WOBBLE - WOBBLE_RATE * 2 ) { - m_locoInfo->m_pitch += WOBBLE_RATE; - m_locoInfo->m_yaw += WOBBLE_RATE; + m_locoInfo->m_pitch += scaledWobbleRate; + m_locoInfo->m_yaw += scaledWobbleRate; } else { - m_locoInfo->m_pitch += (WOBBLE_RATE / 2.0f); - m_locoInfo->m_yaw += (WOBBLE_RATE / 2.0f); + m_locoInfo->m_pitch += (scaledWobbleRate / 2.0f); + m_locoInfo->m_yaw += (scaledWobbleRate / 2.0f); } @@ -1485,15 +1490,15 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( m_locoInfo->m_pitch >= MIN_WOBBLE + WOBBLE_RATE * 2.0f ) { - m_locoInfo->m_pitch -= WOBBLE_RATE; - m_locoInfo->m_yaw -= WOBBLE_RATE; + m_locoInfo->m_pitch -= scaledWobbleRate; + m_locoInfo->m_yaw -= scaledWobbleRate; } else { - m_locoInfo->m_pitch -= (WOBBLE_RATE / 2.0f); - m_locoInfo->m_yaw -= (WOBBLE_RATE / 2.0f); + m_locoInfo->m_pitch -= (scaledWobbleRate / 2.0f); + m_locoInfo->m_yaw -= (scaledWobbleRate / 2.0f); } if( m_locoInfo->m_pitch <= MIN_WOBBLE ) @@ -1509,7 +1514,7 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( THRUST_ROLL ) { - m_locoInfo->m_roll += THRUST_ROLL; + m_locoInfo->m_roll += scaledThrustRoll; info.m_totalRoll = m_locoInfo->m_roll; } From 1d93d7a55f14164933c12168cd6a46e989a7f2f3 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 19:34:05 -0800 Subject: [PATCH 06/21] Replicate to generals --- .../GameEngine/Source/GameClient/Drawable.cpp | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index ecdd46a5fe0..31acf421b13 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1317,6 +1317,11 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI Real MAX_WOBBLE = locomotor->getMaxWobble(); Real MIN_WOBBLE = locomotor->getMinWobble(); + // TheSuperHackers @tweak Wobble and thrust roll rates are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + const Real scaledWobbleRate = WOBBLE_RATE * timeScale; + const Real scaledThrustRoll = THRUST_ROLL * timeScale; + // // this is a kind of quick thrust implementation cause we need scud missiles to wobble *now*, // we deal with just adjusting pitch, yaw, and roll just a little bit @@ -1331,15 +1336,15 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( m_locoInfo->m_pitch < MAX_WOBBLE - WOBBLE_RATE * 2 ) { - m_locoInfo->m_pitch += WOBBLE_RATE; - m_locoInfo->m_yaw += WOBBLE_RATE; + m_locoInfo->m_pitch += scaledWobbleRate; + m_locoInfo->m_yaw += scaledWobbleRate; } else { - m_locoInfo->m_pitch += (WOBBLE_RATE / 2.0f); - m_locoInfo->m_yaw += (WOBBLE_RATE / 2.0f); + m_locoInfo->m_pitch += (scaledWobbleRate / 2.0f); + m_locoInfo->m_yaw += (scaledWobbleRate / 2.0f); } @@ -1353,15 +1358,15 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( m_locoInfo->m_pitch >= MIN_WOBBLE + WOBBLE_RATE * 2.0f ) { - m_locoInfo->m_pitch -= WOBBLE_RATE; - m_locoInfo->m_yaw -= WOBBLE_RATE; + m_locoInfo->m_pitch -= scaledWobbleRate; + m_locoInfo->m_yaw -= scaledWobbleRate; } else { - m_locoInfo->m_pitch -= (WOBBLE_RATE / 2.0f); - m_locoInfo->m_yaw -= (WOBBLE_RATE / 2.0f); + m_locoInfo->m_pitch -= (scaledWobbleRate / 2.0f); + m_locoInfo->m_yaw -= (scaledWobbleRate / 2.0f); } if( m_locoInfo->m_pitch <= MIN_WOBBLE ) @@ -1377,7 +1382,7 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( THRUST_ROLL ) { - m_locoInfo->m_roll += THRUST_ROLL; + m_locoInfo->m_roll += scaledThrustRoll; info.m_totalRoll = m_locoInfo->m_roll; } From a67529ae69bc848cbdede2eac51a296400f66ebd Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 19:47:04 -0800 Subject: [PATCH 07/21] Decouple hover/wings spring-damper physics from render update --- .../GameEngine/Source/GameClient/Drawable.cpp | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index dc327b81695..4984814add8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1562,19 +1562,22 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics const Coord3D* accel = physics->getAcceleration(); const Coord3D* vel = physics->getVelocity(); - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + // TheSuperHackers @tweak Spring-damper physics are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)) * timeScale; // spring/damper + m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)) * timeScale; // spring/damper + + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -1588,23 +1591,23 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics if (fabs(vel->z) > TINY_DZ) { Real pitch = atan2(vel->z, sqrt(sqr(vel->x)+sqr(vel->y))); - m_locoInfo->m_pitch -= Z_VEL_PITCH_COEFF * pitch; + m_locoInfo->m_pitch -= Z_VEL_PITCH_COEFF * pitch * timeScale; } } // cause the chassis to pitch & roll in reaction to current speed Real forwardVel = dir->x * vel->x + dir->y * vel->y; - m_locoInfo->m_pitch += -(FORWARD_VEL_COEFF * forwardVel); + m_locoInfo->m_pitch += -(FORWARD_VEL_COEFF * forwardVel) * timeScale; Real lateralVel = -dir->y * vel->x + dir->x * vel->y; - m_locoInfo->m_roll += -(LATERAL_VEL_COEFF * lateralVel); + m_locoInfo->m_roll += -(LATERAL_VEL_COEFF * lateralVel) * timeScale; // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; } // limit acceleration pitch and roll From b60629da239f668dba74b5b594db2ad93a0b9ee8 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 19:50:29 -0800 Subject: [PATCH 08/21] Replicate to generals --- .../GameEngine/Source/GameClient/Drawable.cpp | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 31acf421b13..9d5600f029b 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1428,19 +1428,22 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics const Coord3D* accel = physics->getAcceleration(); const Coord3D* vel = physics->getVelocity(); - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + // TheSuperHackers @tweak Spring-damper physics are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)) * timeScale; // spring/damper + m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)) * timeScale; // spring/damper + + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -1454,23 +1457,23 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics if (fabs(vel->z) > TINY_DZ) { Real pitch = atan2(vel->z, sqrt(sqr(vel->x)+sqr(vel->y))); - m_locoInfo->m_pitch -= Z_VEL_PITCH_COEFF * pitch; + m_locoInfo->m_pitch -= Z_VEL_PITCH_COEFF * pitch * timeScale; } } // cause the chassis to pitch & roll in reaction to current speed Real forwardVel = dir->x * vel->x + dir->y * vel->y; - m_locoInfo->m_pitch += -(FORWARD_VEL_COEFF * forwardVel); + m_locoInfo->m_pitch += -(FORWARD_VEL_COEFF * forwardVel) * timeScale; Real lateralVel = -dir->y * vel->x + dir->x * vel->y; - m_locoInfo->m_roll += -(LATERAL_VEL_COEFF * lateralVel); + m_locoInfo->m_roll += -(LATERAL_VEL_COEFF * lateralVel) * timeScale; // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; } // limit acceleration pitch and roll From 2a9bc04267ce6c2e1e1a78a2f020f103574eacec Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:08:43 -0800 Subject: [PATCH 09/21] Decouple remaining Drawable physics from render update --- .../GameEngine/Source/GameClient/Drawable.cpp | 83 +++++++++++-------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 4984814add8..1e0e236c7f0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1778,7 +1778,11 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // if we had an overlap last frame, and we're now in the air, give a // kick to the pitch for effect if (physics->getPreviousOverlap() != INVALID_ID && m_locoInfo->m_overlapZ > 0.0f) - m_locoInfo->m_pitchRate += LEAVE_OVERLAP_PITCH_KICK; + { + // TheSuperHackers @tweak Leave overlap pitch kick is now decoupled from the render update. + const Real overlapTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_pitchRate += LEAVE_OVERLAP_PITCH_KICK * overlapTimeScale; + } } @@ -1853,8 +1857,10 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI Real recoil = PI/16.0f * GameClientRandomValueReal( 0.5f, 1.0f ); - m_locoInfo->m_accelerationPitchRate -= recoil * forward; - m_locoInfo->m_accelerationRollRate -= recoil * lateral; + // TheSuperHackers @tweak Hit recoil is now decoupled from the render update. + const Real hitRecoilTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_accelerationPitchRate -= recoil * forward * hitRecoilTimeScale; + m_locoInfo->m_accelerationRollRate -= recoil * lateral * hitRecoilTimeScale; } m_lastDamageTimestamp = obj->getBodyModule()->getLastDamageTimestamp(); @@ -2002,24 +2008,27 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI Real factor = curSpeed/maxSpeed; if (fabs(m_locoInfo->m_pitchRate)m_rollRate)getActualLogicTimeScaleOverFpsRatio(); + const Real scaledBounceKick = BOUNCE_ANGLE_KICK * factor * bounceTimeScale; // do the bouncy. switch (GameClientRandomValue(0,3)) { case 0: - m_locoInfo->m_pitchRate -= BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate -= BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate -= scaledBounceKick; + m_locoInfo->m_rollRate -= scaledBounceKick/2; break; case 1: - m_locoInfo->m_pitchRate += BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate -= BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate += scaledBounceKick; + m_locoInfo->m_rollRate -= scaledBounceKick/2; break; case 2: - m_locoInfo->m_pitchRate -= BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate += BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate -= scaledBounceKick; + m_locoInfo->m_rollRate += scaledBounceKick/2; break; case 3: - m_locoInfo->m_pitchRate += BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate += BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate += scaledBounceKick; + m_locoInfo->m_rollRate += scaledBounceKick/2; break; } } @@ -2312,24 +2321,27 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf Real factor = curSpeed/maxSpeed; if (fabs(m_locoInfo->m_pitchRate)m_rollRate)getActualLogicTimeScaleOverFpsRatio(); + const Real scaledBounceKick = BOUNCE_ANGLE_KICK * factor * bounceTimeScale; // do the bouncy. switch (GameClientRandomValue(0,3)) { case 0: - m_locoInfo->m_pitchRate -= BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate -= BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate -= scaledBounceKick; + m_locoInfo->m_rollRate -= scaledBounceKick/2; break; case 1: - m_locoInfo->m_pitchRate += BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate -= BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate += scaledBounceKick; + m_locoInfo->m_rollRate -= scaledBounceKick/2; break; case 2: - m_locoInfo->m_pitchRate -= BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate += BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate -= scaledBounceKick; + m_locoInfo->m_rollRate += scaledBounceKick/2; break; case 3: - m_locoInfo->m_pitchRate += BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate += BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate += scaledBounceKick; + m_locoInfo->m_rollRate += scaledBounceKick/2; break; } } @@ -2339,29 +2351,32 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf // process chassis suspension dynamics - damp back towards groundPitch + // TheSuperHackers @tweak Spring-damper physics are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + // the ground can only push back if we're touching it if (!airborne) { - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)) * timeScale; // spring/damper + m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)) * timeScale; // spring/damper } else { //Autolevel - m_locoInfo->m_pitchRate += ( (-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate) ); // spring/damper - m_locoInfo->m_rollRate += ( (-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate) ); // spring/damper + m_locoInfo->m_pitchRate += ( (-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate) ) * timeScale; // spring/damper + m_locoInfo->m_rollRate += ( (-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate) ) * timeScale; // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -2380,10 +2395,10 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; } // limit acceleration pitch and roll @@ -4285,8 +4300,10 @@ Bool Drawable::handleWeaponFireFX(WeaponSlotType wslot, Int specificBarrelToUse, recoilAngle += PI; if (m_locoInfo) { - m_locoInfo->m_accelerationPitchRate += recoilAmount * Cos(recoilAngle); - m_locoInfo->m_accelerationRollRate += recoilAmount * Sin(recoilAngle); + // TheSuperHackers @tweak Weapon recoil is now decoupled from the render update. + const Real recoilTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_accelerationPitchRate += recoilAmount * Cos(recoilAngle) * recoilTimeScale; + m_locoInfo->m_accelerationRollRate += recoilAmount * Sin(recoilAngle) * recoilTimeScale; } } From a48541877accc85b4a46a77a52d07ffdb88792cb Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:10:55 -0800 Subject: [PATCH 10/21] Replicate to generals --- .../GameEngine/Source/GameClient/Drawable.cpp | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 9d5600f029b..9f31fd0946a 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1632,7 +1632,11 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // if we had an overlap last frame, and we're now in the air, give a // kick to the pitch for effect if (physics->getPreviousOverlap() != INVALID_ID && m_locoInfo->m_overlapZ > 0.0f) - m_locoInfo->m_pitchRate += LEAVE_OVERLAP_PITCH_KICK; + { + // TheSuperHackers @tweak Leave overlap pitch kick is now decoupled from the render update. + const Real overlapTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_pitchRate += LEAVE_OVERLAP_PITCH_KICK * overlapTimeScale; + } } @@ -1707,8 +1711,10 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI Real recoil = PI/16.0f * GameClientRandomValueReal( 0.5f, 1.0f ); - m_locoInfo->m_accelerationPitchRate -= recoil * forward; - m_locoInfo->m_accelerationRollRate -= recoil * lateral; + // TheSuperHackers @tweak Hit recoil is now decoupled from the render update. + const Real hitRecoilTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_accelerationPitchRate -= recoil * forward * hitRecoilTimeScale; + m_locoInfo->m_accelerationRollRate -= recoil * lateral * hitRecoilTimeScale; } m_lastDamageTimestamp = obj->getBodyModule()->getLastDamageTimestamp(); @@ -1855,24 +1861,27 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI Real factor = curSpeed/maxSpeed; if (fabs(m_locoInfo->m_pitchRate)m_rollRate)getActualLogicTimeScaleOverFpsRatio(); + const Real scaledBounceKick = BOUNCE_ANGLE_KICK * factor * bounceTimeScale; // do the bouncy. switch (GameClientRandomValue(0,3)) { case 0: - m_locoInfo->m_pitchRate -= BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate -= BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate -= scaledBounceKick; + m_locoInfo->m_rollRate -= scaledBounceKick/2; break; case 1: - m_locoInfo->m_pitchRate += BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate -= BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate += scaledBounceKick; + m_locoInfo->m_rollRate -= scaledBounceKick/2; break; case 2: - m_locoInfo->m_pitchRate -= BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate += BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate -= scaledBounceKick; + m_locoInfo->m_rollRate += scaledBounceKick/2; break; case 3: - m_locoInfo->m_pitchRate += BOUNCE_ANGLE_KICK*factor; - m_locoInfo->m_rollRate += BOUNCE_ANGLE_KICK*factor/2; + m_locoInfo->m_pitchRate += scaledBounceKick; + m_locoInfo->m_rollRate += scaledBounceKick/2; break; } } @@ -3750,8 +3759,10 @@ Bool Drawable::handleWeaponFireFX(WeaponSlotType wslot, Int specificBarrelToUse, recoilAngle += PI; if (m_locoInfo) { - m_locoInfo->m_accelerationPitchRate += recoilAmount * Cos(recoilAngle); - m_locoInfo->m_accelerationRollRate += recoilAmount * Sin(recoilAngle); + // TheSuperHackers @tweak Weapon recoil is now decoupled from the render update. + const Real recoilTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_accelerationPitchRate += recoilAmount * Cos(recoilAngle) * recoilTimeScale; + m_locoInfo->m_accelerationRollRate += recoilAmount * Sin(recoilAngle) * recoilTimeScale; } } From 815eeafb26f6cef926385ecf6ea2bc1f771e0c23 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:37:07 -0800 Subject: [PATCH 11/21] Fix additional missed physics timing issues --- .../GameEngine/Source/GameClient/Drawable.cpp | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 1e0e236c7f0..8a1a4b6cee2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1629,8 +1629,10 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics const Real ELEVATOR_CORRECTION_DEGREE = locomotor->getElevatorCorrectionDegree(); const Real ELEVATOR_CORRECTION_RATE = locomotor->getElevatorCorrectionRate(); - info.m_totalYaw = RUDDER_CORRECTION_DEGREE * sin( m_locoInfo->m_yawModulator += RUDDER_CORRECTION_RATE ); - info.m_totalPitch += ELEVATOR_CORRECTION_DEGREE * cos( m_locoInfo->m_pitchModulator += ELEVATOR_CORRECTION_RATE ); + // TheSuperHackers @tweak Rudder/elevator correction rates are now decoupled from the render update. + const Real correctionTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + info.m_totalYaw = RUDDER_CORRECTION_DEGREE * sin( m_locoInfo->m_yawModulator += RUDDER_CORRECTION_RATE * correctionTimeScale ); + info.m_totalPitch += ELEVATOR_CORRECTION_DEGREE * cos( m_locoInfo->m_pitchModulator += ELEVATOR_CORRECTION_RATE * correctionTimeScale ); info.m_totalZ = 0.0f; @@ -1795,12 +1797,12 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // process chassis suspension dynamics - damp back towards groundPitch + // TheSuperHackers @tweak The physics are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + // the ground can only push back if we're touching it if (overlapped || m_locoInfo->m_overlapZ <= 0.0f) { - // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) { @@ -1811,16 +1813,16 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; // process chassis recoil dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -1830,10 +1832,10 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; } #ifdef RECOIL_FROM_BEING_DAMAGED @@ -1892,10 +1894,12 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI Real ztmp = m_locoInfo->m_overlapZ/2.0f; // do fake Z physics + // TheSuperHackers @tweak Overlap Z physics is now decoupled from the render update. + const Real overlapTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); if (m_locoInfo->m_overlapZ > 0.0f) { - m_locoInfo->m_overlapZVel -= 0.2f; - m_locoInfo->m_overlapZ += m_locoInfo->m_overlapZVel; + m_locoInfo->m_overlapZVel -= 0.2f * overlapTimeScale; + m_locoInfo->m_overlapZ += m_locoInfo->m_overlapZVel * overlapTimeScale; } if (m_locoInfo->m_overlapZ <= 0.0f) @@ -2038,12 +2042,12 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // process chassis suspension dynamics - damp back towards groundPitch + // TheSuperHackers @tweak The physics are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + // the ground can only push back if we're touching it if (!airborne) { - // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) { @@ -2054,16 +2058,16 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -2073,10 +2077,10 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; } // limit acceleration pitch and roll @@ -2126,8 +2130,10 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // etc, this smaller angle we'll be adding covers the constant wheel shifting // left and right when moving in a relatively straight line // + // TheSuperHackers @tweak Wheel angle smoothing is now decoupled from the render update. #define WHEEL_SMOOTHNESS 10.0f // higher numbers add smaller angles, make it more "smooth" - m_locoInfo->m_wheelInfo.m_wheelAngle += (newInfo.m_wheelAngle - m_locoInfo->m_wheelInfo.m_wheelAngle)/WHEEL_SMOOTHNESS; + const Real wheelAngleTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_wheelInfo.m_wheelAngle += (newInfo.m_wheelAngle - m_locoInfo->m_wheelInfo.m_wheelAngle)/WHEEL_SMOOTHNESS * wheelAngleTimeScale; const Real SPRING_FACTOR = 0.9f; if (pitchHeight<0) { // Front raising up @@ -2448,8 +2454,10 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf // etc, this smaller angle we'll be adding covers the constant wheel shifting // left and right when moving in a relatively straight line // + // TheSuperHackers @tweak Wheel angle smoothing is now decoupled from the render update. #define WHEEL_SMOOTHNESS 10.0f // higher numbers add smaller angles, make it more "smooth" - m_locoInfo->m_wheelInfo.m_wheelAngle += (newInfo.m_wheelAngle - m_locoInfo->m_wheelInfo.m_wheelAngle)/WHEEL_SMOOTHNESS; + const Real wheelAngleTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_wheelInfo.m_wheelAngle += (newInfo.m_wheelAngle - m_locoInfo->m_wheelInfo.m_wheelAngle)/WHEEL_SMOOTHNESS * wheelAngleTimeScale; const Real SPRING_FACTOR = 0.9f; if (pitchHeight<0) From cfcf22159a23db32526d057caadc7af141c3854d Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:39:09 -0800 Subject: [PATCH 12/21] Replicate to generals --- .../GameEngine/Source/GameClient/Drawable.cpp | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 9f31fd0946a..f3b0f365432 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1649,12 +1649,12 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // process chassis suspension dynamics - damp back towards groundPitch + // TheSuperHackers @tweak The physics are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + // the ground can only push back if we're touching it if (overlapped || m_locoInfo->m_overlapZ <= 0.0f) { - // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) { @@ -1665,16 +1665,16 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; // process chassis recoil dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -1684,10 +1684,10 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; } #ifdef RECOIL_FROM_BEING_DAMAGED @@ -1746,10 +1746,12 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI Real ztmp = m_locoInfo->m_overlapZ/2.0f; // do fake Z physics + // TheSuperHackers @tweak Overlap Z physics is now decoupled from the render update. + const Real overlapTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); if (m_locoInfo->m_overlapZ > 0.0f) { - m_locoInfo->m_overlapZVel -= 0.2f; - m_locoInfo->m_overlapZ += m_locoInfo->m_overlapZVel; + m_locoInfo->m_overlapZVel -= 0.2f * overlapTimeScale; + m_locoInfo->m_overlapZ += m_locoInfo->m_overlapZVel * overlapTimeScale; } if (m_locoInfo->m_overlapZ <= 0.0f) @@ -1891,12 +1893,12 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // process chassis suspension dynamics - damp back towards groundPitch + // TheSuperHackers @tweak The physics are now decoupled from the render update. + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + // the ground can only push back if we're touching it if (!airborne) { - // TheSuperHackers @tweak The pitch/roll rate damping is now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) { @@ -1907,16 +1909,16 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -1926,10 +1928,10 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; } // limit acceleration pitch and roll @@ -1979,8 +1981,10 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // etc, this smaller angle we'll be adding covers the constant wheel shifting // left and right when moving in a relatively straight line // + // TheSuperHackers @tweak Wheel angle smoothing is now decoupled from the render update. #define WHEEL_SMOOTHNESS 10.0f // higher numbers add smaller angles, make it more "smooth" - m_locoInfo->m_wheelInfo.m_wheelAngle += (newInfo.m_wheelAngle - m_locoInfo->m_wheelInfo.m_wheelAngle)/WHEEL_SMOOTHNESS; + const Real wheelAngleTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_wheelInfo.m_wheelAngle += (newInfo.m_wheelAngle - m_locoInfo->m_wheelInfo.m_wheelAngle)/WHEEL_SMOOTHNESS * wheelAngleTimeScale; const Real SPRING_FACTOR = 0.9f; if (pitchHeight<0) { // Front raising up From ac60e33831801b006864271c3b1a14add25f2bbb Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:51:51 -0800 Subject: [PATCH 13/21] Decouple decal opacity fade from render update --- GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 8a1a4b6cee2..bd27aeee471 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1188,7 +1188,9 @@ void Drawable::updateDrawable( void ) { //LERP (*dm)->setTerrainDecalOpacity(m_decalOpacity); - m_decalOpacity += m_decalOpacityFadeRate; + // TheSuperHackers @tweak Decal opacity fade is now decoupled from the render update. + const Real decalFadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_decalOpacity += m_decalOpacityFadeRate * decalFadeTimeScale; } //--------------- From 8dc97d0ba26a30505cd7329ecbb3ef4a906030a6 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 20:52:12 -0800 Subject: [PATCH 14/21] Replicate to generals --- Generals/Code/GameEngine/Source/GameClient/Drawable.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index f3b0f365432..5fd7bdc8690 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1113,7 +1113,9 @@ void Drawable::updateDrawable( void ) { //LERP (*dm)->setTerrainDecalOpacity(m_decalOpacity); - m_decalOpacity += m_decalOpacityFadeRate; + // TheSuperHackers @tweak Decal opacity fade is now decoupled from the render update. + const Real decalFadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_decalOpacity += m_decalOpacityFadeRate * decalFadeTimeScale; } //--------------- From 328fdc1042b8d68b882a13de5507b91be5b3383b Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 21:10:54 -0800 Subject: [PATCH 15/21] Decouple drawable fade timing from render update --- .../GameEngine/Include/GameClient/Drawable.h | 2 +- .../GameEngine/Source/GameClient/Drawable.cpp | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index b66eb7dc96a..65bdbfb6ea1 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -691,7 +691,7 @@ class Drawable : public Thing, FADING_OUT }; FadingMode m_fadeMode; - UnsignedInt m_timeElapsedFade; ///< for how many frames have i been fading + Real m_timeElapsedFade; ///< for how long have i been fading (in 30fps-equivalent frames) UnsignedInt m_timeToFade; ///< how slowly am I fading UnsignedInt m_shroudClearFrame; ///< Last frame the local player saw this drawable "OBJECTSHROUD_CLEAR" diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index bd27aeee471..c7bdfbf6c1b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1170,7 +1170,9 @@ void Drawable::updateDrawable( void ) Real numer = (m_fadeMode == FADING_IN) ? (m_timeElapsedFade) : (m_timeToFade-m_timeElapsedFade); setDrawableOpacity(numer/(Real)m_timeToFade); - ++m_timeElapsedFade; + // TheSuperHackers @tweak Drawable fade is now decoupled from the render update. + const Real fadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_timeElapsedFade += fadeTimeScale; if (m_timeElapsedFade > m_timeToFade) m_fadeMode = FADING_NONE; @@ -4993,7 +4995,7 @@ void Drawable::xfer( Xfer *xfer ) #if RETAIL_COMPATIBLE_XFER_SAVE const XferVersion currentVersion = 7; #else - const XferVersion currentVersion = 8; + const XferVersion currentVersion = 9; #endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -5168,7 +5170,17 @@ void Drawable::xfer( Xfer *xfer ) xfer->xferUser( &m_fadeMode, sizeof( FadingMode ) ); // time elapsed fade - xfer->xferUnsignedInt( &m_timeElapsedFade ); + // TheSuperHackers @tweak Changed from UnsignedInt to Real for frame-rate independent fading. + if (version >= 9) + { + xfer->xferReal( &m_timeElapsedFade ); + } + else + { + UnsignedInt timeElapsedFadeFrames = static_cast(m_timeElapsedFade); + xfer->xferUnsignedInt( &timeElapsedFadeFrames ); + m_timeElapsedFade = static_cast(timeElapsedFadeFrames); + } // time to fade xfer->xferUnsignedInt( &m_timeToFade ); From 0ba63b2a469510c9a51af3569688a582a8ac4add Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 21:11:03 -0800 Subject: [PATCH 16/21] Replicate to generals --- .../GameEngine/Include/GameClient/Drawable.h | 2 +- .../GameEngine/Source/GameClient/Drawable.cpp | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameClient/Drawable.h b/Generals/Code/GameEngine/Include/GameClient/Drawable.h index 8d4c158d7c4..b23bb044911 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Drawable.h +++ b/Generals/Code/GameEngine/Include/GameClient/Drawable.h @@ -653,7 +653,7 @@ class Drawable : public Thing, FADING_OUT }; FadingMode m_fadeMode; - UnsignedInt m_timeElapsedFade; ///< for how many frames have i been fading + Real m_timeElapsedFade; ///< for how long have i been fading (in 30fps-equivalent frames) UnsignedInt m_timeToFade; ///< how slowly am I fading UnsignedInt m_shroudClearFrame; ///< Last frame the local player saw this drawable "OBJECTSHROUD_CLEAR" diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 5fd7bdc8690..924e98f58cb 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1095,7 +1095,9 @@ void Drawable::updateDrawable( void ) Real numer = (m_fadeMode == FADING_IN) ? (m_timeElapsedFade) : (m_timeToFade-m_timeElapsedFade); setDrawableOpacity(numer/(Real)m_timeToFade); - ++m_timeElapsedFade; + // TheSuperHackers @tweak Drawable fade is now decoupled from the render update. + const Real fadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_timeElapsedFade += fadeTimeScale; if (m_timeElapsedFade > m_timeToFade) m_fadeMode = FADING_NONE; @@ -4314,7 +4316,7 @@ void Drawable::xfer( Xfer *xfer ) #if RETAIL_COMPATIBLE_XFER_SAVE const XferVersion currentVersion = 5; #else - const XferVersion currentVersion = 6; + const XferVersion currentVersion = 7; #endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -4489,7 +4491,17 @@ void Drawable::xfer( Xfer *xfer ) xfer->xferUser( &m_fadeMode, sizeof( FadingMode ) ); // time elapsed fade - xfer->xferUnsignedInt( &m_timeElapsedFade ); + // TheSuperHackers @tweak Changed from UnsignedInt to Real for frame-rate independent fading. + if (version >= 7) + { + xfer->xferReal( &m_timeElapsedFade ); + } + else + { + UnsignedInt timeElapsedFadeFrames = static_cast(m_timeElapsedFade); + xfer->xferUnsignedInt( &timeElapsedFadeFrames ); + m_timeElapsedFade = static_cast(timeElapsedFadeFrames); + } // time to fade xfer->xferUnsignedInt( &m_timeToFade ); From 7d084f2227439d90ab85fafa6978c03562e0655d Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 21:36:29 -0800 Subject: [PATCH 17/21] Decouple airborne frame counter from render update --- .../GameEngine/Include/GameClient/Drawable.h | 4 ++-- .../GameEngine/Source/GameClient/Drawable.cpp | 23 ++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index 65bdbfb6ea1..0eae61a9130 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -128,8 +128,8 @@ struct TWheelInfo Real m_rearLeftHeightOffset; Real m_rearRightHeightOffset; Real m_wheelAngle; ///< Wheel angle. 0 = straight, >0 left, <0 right. - Int m_framesAirborneCounter; ///< Counter. - Int m_framesAirborne; ///< How many frames it was in the air. + Real m_framesAirborneCounter; ///< Counter (in 30fps-equivalent frames). + Real m_framesAirborne; ///< How long it was in the air (in 30fps-equivalent frames). }; //----------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index c7bdfbf6c1b..f5ddd7ca48a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1981,10 +1981,9 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI { // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; - m_locoInfo->m_wheelInfo.m_framesAirborneCounter++; - // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_wheelInfo.m_framesAirborneCounter += timeScale; const Real suspensionFactor = 0.5f * timeScale; if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) @@ -2296,10 +2295,9 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf { // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; - m_locoInfo->m_wheelInfo.m_framesAirborneCounter++; - // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_wheelInfo.m_framesAirborneCounter += timeScale; const Real suspensionFactor = 0.5f * timeScale; if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) @@ -5234,8 +5232,21 @@ void Drawable::xfer( Xfer *xfer ) xfer->xferReal( &m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset ); xfer->xferReal( &m_locoInfo->m_wheelInfo.m_rearRightHeightOffset ); xfer->xferReal( &m_locoInfo->m_wheelInfo.m_wheelAngle ); - xfer->xferInt( &m_locoInfo->m_wheelInfo.m_framesAirborneCounter ); - xfer->xferInt( &m_locoInfo->m_wheelInfo.m_framesAirborne ); + // TheSuperHackers @tweak Changed from Int to Real for frame-rate independent airborne tracking. + if (version >= 9) + { + xfer->xferReal( &m_locoInfo->m_wheelInfo.m_framesAirborneCounter ); + xfer->xferReal( &m_locoInfo->m_wheelInfo.m_framesAirborne ); + } + else + { + Int framesAirborneCounter = static_cast(m_locoInfo->m_wheelInfo.m_framesAirborneCounter); + Int framesAirborne = static_cast(m_locoInfo->m_wheelInfo.m_framesAirborne); + xfer->xferInt( &framesAirborneCounter ); + xfer->xferInt( &framesAirborne ); + m_locoInfo->m_wheelInfo.m_framesAirborneCounter = static_cast(framesAirborneCounter); + m_locoInfo->m_wheelInfo.m_framesAirborne = static_cast(framesAirborne); + } } // modules From c06aa3186faf4fddc1a96f4f6b3cb8695f1c04cd Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 3 Jan 2026 21:37:31 -0800 Subject: [PATCH 18/21] Replicate to generals --- .../GameEngine/Include/GameClient/Drawable.h | 4 ++-- .../GameEngine/Source/GameClient/Drawable.cpp | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameClient/Drawable.h b/Generals/Code/GameEngine/Include/GameClient/Drawable.h index b23bb044911..1f15eeede68 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Drawable.h +++ b/Generals/Code/GameEngine/Include/GameClient/Drawable.h @@ -127,8 +127,8 @@ struct TWheelInfo Real m_rearLeftHeightOffset; Real m_rearRightHeightOffset; Real m_wheelAngle; ///< Wheel angle. 0 = straight, >0 left, <0 right. - Int m_framesAirborneCounter; ///< Counter. - Int m_framesAirborne; ///< How many frames it was in the air. + Real m_framesAirborneCounter; ///< Counter (in 30fps-equivalent frames). + Real m_framesAirborne; ///< How long it was in the air (in 30fps-equivalent frames). }; //----------------------------------------------------------------------------- diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 924e98f58cb..a38af1199b4 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1832,10 +1832,9 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI { // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; - m_locoInfo->m_wheelInfo.m_framesAirborneCounter++; - // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_locoInfo->m_wheelInfo.m_framesAirborneCounter += timeScale; const Real suspensionFactor = 0.5f * timeScale; if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) @@ -4555,8 +4554,21 @@ void Drawable::xfer( Xfer *xfer ) xfer->xferReal( &m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset ); xfer->xferReal( &m_locoInfo->m_wheelInfo.m_rearRightHeightOffset ); xfer->xferReal( &m_locoInfo->m_wheelInfo.m_wheelAngle ); - xfer->xferInt( &m_locoInfo->m_wheelInfo.m_framesAirborneCounter ); - xfer->xferInt( &m_locoInfo->m_wheelInfo.m_framesAirborne ); + // TheSuperHackers @tweak Changed from Int to Real for frame-rate independent airborne tracking. + if (version >= 7) + { + xfer->xferReal( &m_locoInfo->m_wheelInfo.m_framesAirborneCounter ); + xfer->xferReal( &m_locoInfo->m_wheelInfo.m_framesAirborne ); + } + else + { + Int framesAirborneCounter = static_cast(m_locoInfo->m_wheelInfo.m_framesAirborneCounter); + Int framesAirborne = static_cast(m_locoInfo->m_wheelInfo.m_framesAirborne); + xfer->xferInt( &framesAirborneCounter ); + xfer->xferInt( &framesAirborne ); + m_locoInfo->m_wheelInfo.m_framesAirborneCounter = static_cast(framesAirborneCounter); + m_locoInfo->m_wheelInfo.m_framesAirborne = static_cast(framesAirborne); + } } // modules From 3364794e49bc9d46015021a4f263bc680ae322ff Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 5 Jan 2026 16:59:35 -0800 Subject: [PATCH 19/21] Calculate physics at fixed logic rate with interpolation to restore wobble --- .../GameEngine/Include/GameClient/Drawable.h | 8 +- .../GameEngine/Source/GameClient/Drawable.cpp | 202 ++++++++---------- 2 files changed, 99 insertions(+), 111 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index 0eae61a9130..17d179074a0 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -626,7 +626,13 @@ class Drawable : public Thing, Real m_totalYaw; ///< Current total yaw for this frame Real m_totalZ; - PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0) { } + Real m_prevTotalPitch; + Real m_prevTotalRoll; + Real m_prevTotalYaw; + Real m_prevTotalZ; + + PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0), + m_prevTotalPitch(0), m_prevTotalRoll(0), m_prevTotalYaw(0), m_prevTotalZ(0) { } }; Bool calcPhysicsXform(PhysicsXformInfo& info); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index f5ddd7ca48a..baf84db0a61 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1376,17 +1376,34 @@ void Drawable::applyPhysicsXform(Matrix3D* mtx) { if (m_physicsXform != NULL) { - // TheSuperHackers @tweak Update the physics transform on every WW Sync only. - // All calculations are originally catered to a 30 fps logic step. + // TheSuperHackers @tweak Run physics on logic frames only, interpolate for rendering. + // This provides stable physics at any framerate without numerical integration issues. if (WW3D::Get_Sync_Frame_Time() != 0) { + // Save previous state before calculating new physics + m_physicsXform->m_prevTotalPitch = m_physicsXform->m_totalPitch; + m_physicsXform->m_prevTotalRoll = m_physicsXform->m_totalRoll; + m_physicsXform->m_prevTotalYaw = m_physicsXform->m_totalYaw; + m_physicsXform->m_prevTotalZ = m_physicsXform->m_totalZ; + calcPhysicsXform(*m_physicsXform); } - mtx->Translate(0.0f, 0.0f, m_physicsXform->m_totalZ); - mtx->Rotate_Y( m_physicsXform->m_totalPitch ); - mtx->Rotate_X( -m_physicsXform->m_totalRoll ); - mtx->Rotate_Z( m_physicsXform->m_totalYaw ); + // Interpolate between previous and current state based on fractional sync time. + // Logic runs at ~33.3ms per frame (30fps), fractional time accumulates between logic frames. + const Real LOGIC_FRAME_MS = TheFramePacer->getLogicTimeStepMilliseconds(); + const Real fractionalMs = (Real)WW3D::Get_Fractional_Sync_Milliseconds(); + const Real t = (LOGIC_FRAME_MS > 0.0f) ? clamp(0.0f, fractionalMs / LOGIC_FRAME_MS, 1.0f) : 0.0f; + + const Real interpPitch = m_physicsXform->m_prevTotalPitch + t * (m_physicsXform->m_totalPitch - m_physicsXform->m_prevTotalPitch); + const Real interpRoll = m_physicsXform->m_prevTotalRoll + t * (m_physicsXform->m_totalRoll - m_physicsXform->m_prevTotalRoll); + const Real interpYaw = m_physicsXform->m_prevTotalYaw + t * (m_physicsXform->m_totalYaw - m_physicsXform->m_prevTotalYaw); + const Real interpZ = m_physicsXform->m_prevTotalZ + t * (m_physicsXform->m_totalZ - m_physicsXform->m_prevTotalZ); + + mtx->Translate(0.0f, 0.0f, interpZ); + mtx->Rotate_Y( interpPitch ); + mtx->Rotate_X( -interpRoll ); + mtx->Rotate_Z( interpYaw ); } } @@ -1453,11 +1470,6 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI Real MAX_WOBBLE = locomotor->getMaxWobble(); Real MIN_WOBBLE = locomotor->getMinWobble(); - // TheSuperHackers @tweak Wobble and thrust roll rates are now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - const Real scaledWobbleRate = WOBBLE_RATE * timeScale; - const Real scaledThrustRoll = THRUST_ROLL * timeScale; - // // this is a kind of quick thrust implementation cause we need scud missiles to wobble *now*, // we deal with just adjusting pitch, yaw, and roll just a little bit @@ -1472,15 +1484,15 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( m_locoInfo->m_pitch < MAX_WOBBLE - WOBBLE_RATE * 2 ) { - m_locoInfo->m_pitch += scaledWobbleRate; - m_locoInfo->m_yaw += scaledWobbleRate; + m_locoInfo->m_pitch += WOBBLE_RATE; + m_locoInfo->m_yaw += WOBBLE_RATE; } else { - m_locoInfo->m_pitch += (scaledWobbleRate / 2.0f); - m_locoInfo->m_yaw += (scaledWobbleRate / 2.0f); + m_locoInfo->m_pitch += (WOBBLE_RATE / 2.0f); + m_locoInfo->m_yaw += (WOBBLE_RATE / 2.0f); } @@ -1494,15 +1506,15 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( m_locoInfo->m_pitch >= MIN_WOBBLE + WOBBLE_RATE * 2.0f ) { - m_locoInfo->m_pitch -= scaledWobbleRate; - m_locoInfo->m_yaw -= scaledWobbleRate; + m_locoInfo->m_pitch -= WOBBLE_RATE; + m_locoInfo->m_yaw -= WOBBLE_RATE; } else { - m_locoInfo->m_pitch -= (scaledWobbleRate / 2.0f); - m_locoInfo->m_yaw -= (scaledWobbleRate / 2.0f); + m_locoInfo->m_pitch -= (WOBBLE_RATE / 2.0f); + m_locoInfo->m_yaw -= (WOBBLE_RATE / 2.0f); } if( m_locoInfo->m_pitch <= MIN_WOBBLE ) @@ -1518,7 +1530,7 @@ void Drawable::calcPhysicsXformThrust( const Locomotor *locomotor, PhysicsXformI if( THRUST_ROLL ) { - m_locoInfo->m_roll += scaledThrustRoll; + m_locoInfo->m_roll += THRUST_ROLL; info.m_totalRoll = m_locoInfo->m_roll; } @@ -1566,22 +1578,19 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics const Coord3D* accel = physics->getAcceleration(); const Coord3D* vel = physics->getVelocity(); - // TheSuperHackers @tweak Spring-damper physics are now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)) * timeScale; // spring/damper - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)) * timeScale; // spring/damper + m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -1595,23 +1604,23 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics if (fabs(vel->z) > TINY_DZ) { Real pitch = atan2(vel->z, sqrt(sqr(vel->x)+sqr(vel->y))); - m_locoInfo->m_pitch -= Z_VEL_PITCH_COEFF * pitch * timeScale; + m_locoInfo->m_pitch -= Z_VEL_PITCH_COEFF * pitch; } } // cause the chassis to pitch & roll in reaction to current speed Real forwardVel = dir->x * vel->x + dir->y * vel->y; - m_locoInfo->m_pitch += -(FORWARD_VEL_COEFF * forwardVel) * timeScale; + m_locoInfo->m_pitch += -(FORWARD_VEL_COEFF * forwardVel); Real lateralVel = -dir->y * vel->x + dir->x * vel->y; - m_locoInfo->m_roll += -(LATERAL_VEL_COEFF * lateralVel) * timeScale; + m_locoInfo->m_roll += -(LATERAL_VEL_COEFF * lateralVel); // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); } // limit acceleration pitch and roll @@ -1633,10 +1642,8 @@ void Drawable::calcPhysicsXformHoverOrWings( const Locomotor *locomotor, Physics const Real ELEVATOR_CORRECTION_DEGREE = locomotor->getElevatorCorrectionDegree(); const Real ELEVATOR_CORRECTION_RATE = locomotor->getElevatorCorrectionRate(); - // TheSuperHackers @tweak Rudder/elevator correction rates are now decoupled from the render update. - const Real correctionTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - info.m_totalYaw = RUDDER_CORRECTION_DEGREE * sin( m_locoInfo->m_yawModulator += RUDDER_CORRECTION_RATE * correctionTimeScale ); - info.m_totalPitch += ELEVATOR_CORRECTION_DEGREE * cos( m_locoInfo->m_pitchModulator += ELEVATOR_CORRECTION_RATE * correctionTimeScale ); + info.m_totalYaw = RUDDER_CORRECTION_DEGREE * sin( m_locoInfo->m_yawModulator += RUDDER_CORRECTION_RATE ); + info.m_totalPitch += ELEVATOR_CORRECTION_DEGREE * cos( m_locoInfo->m_pitchModulator += ELEVATOR_CORRECTION_RATE ); info.m_totalZ = 0.0f; @@ -1785,9 +1792,7 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // kick to the pitch for effect if (physics->getPreviousOverlap() != INVALID_ID && m_locoInfo->m_overlapZ > 0.0f) { - // TheSuperHackers @tweak Leave overlap pitch kick is now decoupled from the render update. - const Real overlapTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_pitchRate += LEAVE_OVERLAP_PITCH_KICK * overlapTimeScale; + m_locoInfo->m_pitchRate += LEAVE_OVERLAP_PITCH_KICK; } } @@ -1801,32 +1806,28 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI // process chassis suspension dynamics - damp back towards groundPitch - // TheSuperHackers @tweak The physics are now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - // the ground can only push back if we're touching it if (overlapped || m_locoInfo->m_overlapZ <= 0.0f) { - m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) { - const Real pitchDamp = 1.0f - (1.0f - 0.5f) * timeScale; - m_locoInfo->m_pitchRate *= pitchDamp; + m_locoInfo->m_pitchRate *= 0.5f; } - m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; // process chassis recoil dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -1836,10 +1837,10 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); } #ifdef RECOIL_FROM_BEING_DAMAGED @@ -1863,10 +1864,8 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI Real recoil = PI/16.0f * GameClientRandomValueReal( 0.5f, 1.0f ); - // TheSuperHackers @tweak Hit recoil is now decoupled from the render update. - const Real hitRecoilTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_accelerationPitchRate -= recoil * forward * hitRecoilTimeScale; - m_locoInfo->m_accelerationRollRate -= recoil * lateral * hitRecoilTimeScale; + m_locoInfo->m_accelerationPitchRate -= recoil * forward; + m_locoInfo->m_accelerationRollRate -= recoil * lateral; } m_lastDamageTimestamp = obj->getBodyModule()->getLastDamageTimestamp(); @@ -1898,12 +1897,10 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI Real ztmp = m_locoInfo->m_overlapZ/2.0f; // do fake Z physics - // TheSuperHackers @tweak Overlap Z physics is now decoupled from the render update. - const Real overlapTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); if (m_locoInfo->m_overlapZ > 0.0f) { - m_locoInfo->m_overlapZVel -= 0.2f * overlapTimeScale; - m_locoInfo->m_overlapZ += m_locoInfo->m_overlapZVel * overlapTimeScale; + m_locoInfo->m_overlapZVel -= 0.2f; + m_locoInfo->m_overlapZ += m_locoInfo->m_overlapZVel; } if (m_locoInfo->m_overlapZ <= 0.0f) @@ -1981,10 +1978,8 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI { // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; - // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_wheelInfo.m_framesAirborneCounter += timeScale; - const Real suspensionFactor = 0.5f * timeScale; + m_locoInfo->m_wheelInfo.m_framesAirborneCounter += 1.0f; + const Real suspensionFactor = 0.5f; if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) { @@ -2015,9 +2010,7 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI Real factor = curSpeed/maxSpeed; if (fabs(m_locoInfo->m_pitchRate)m_rollRate)getActualLogicTimeScaleOverFpsRatio(); - const Real scaledBounceKick = BOUNCE_ANGLE_KICK * factor * bounceTimeScale; + const Real scaledBounceKick = BOUNCE_ANGLE_KICK * factor; // do the bouncy. switch (GameClientRandomValue(0,3)) { @@ -2045,32 +2038,28 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // process chassis suspension dynamics - damp back towards groundPitch - // TheSuperHackers @tweak The physics are now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - // the ground can only push back if we're touching it if (!airborne) { - m_locoInfo->m_pitchRate += timeScale * ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper if (m_locoInfo->m_pitchRate > 0.0f) { - const Real pitchDamp = 1.0f - (1.0f - 0.5f) * timeScale; - m_locoInfo->m_pitchRate *= pitchDamp; + m_locoInfo->m_pitchRate *= 0.5f; } - m_locoInfo->m_rollRate += timeScale * ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper + m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -2080,10 +2069,10 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); } // limit acceleration pitch and roll @@ -2295,10 +2284,8 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf { // Wheels extend when airborne. m_locoInfo->m_wheelInfo.m_framesAirborne = 0; - // TheSuperHackers @tweak Wheel suspension offset is now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - m_locoInfo->m_wheelInfo.m_framesAirborneCounter += timeScale; - const Real suspensionFactor = 0.5f * timeScale; + m_locoInfo->m_wheelInfo.m_framesAirborneCounter += 1.0f; + const Real suspensionFactor = 0.5f; if (pos->z - hheight > -MAX_SUSPENSION_EXTENSION) { @@ -2329,9 +2316,7 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf Real factor = curSpeed/maxSpeed; if (fabs(m_locoInfo->m_pitchRate)m_rollRate)getActualLogicTimeScaleOverFpsRatio(); - const Real scaledBounceKick = BOUNCE_ANGLE_KICK * factor * bounceTimeScale; + const Real scaledBounceKick = BOUNCE_ANGLE_KICK * factor; // do the bouncy. switch (GameClientRandomValue(0,3)) { @@ -2359,32 +2344,29 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf // process chassis suspension dynamics - damp back towards groundPitch - // TheSuperHackers @tweak Spring-damper physics are now decoupled from the render update. - const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); - // the ground can only push back if we're touching it if (!airborne) { - m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)) * timeScale; // spring/damper - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)) * timeScale; // spring/damper + m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper + m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } else { //Autolevel - m_locoInfo->m_pitchRate += ( (-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate) ) * timeScale; // spring/damper - m_locoInfo->m_rollRate += ( (-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate) ) * timeScale; // spring/damper + m_locoInfo->m_pitchRate += ( (-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate) ); // spring/damper + m_locoInfo->m_rollRate += ( (-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate) ); // spring/damper } - m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING * timeScale; - m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING * timeScale; + m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; + m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; // process chassis acceleration dynamics - damp back towards zero - m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate * timeScale; + m_locoInfo->m_accelerationPitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_accelerationPitch)) + (-PITCH_DAMPING * m_locoInfo->m_accelerationPitchRate)); // spring/damper + m_locoInfo->m_accelerationPitch += m_locoInfo->m_accelerationPitchRate; - m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)) * timeScale; // spring/damper - m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate * timeScale; + m_locoInfo->m_accelerationRollRate += ((-ROLL_STIFFNESS * m_locoInfo->m_accelerationRoll) + (-ROLL_DAMPING * m_locoInfo->m_accelerationRollRate)); // spring/damper + m_locoInfo->m_accelerationRoll += m_locoInfo->m_accelerationRollRate; // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; @@ -2403,10 +2385,10 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf { // cause the chassis to pitch & roll in reaction to acceleration/deceleration Real forwardAccel = dir->x * accel->x + dir->y * accel->y; - m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel) * timeScale; + m_locoInfo->m_accelerationPitchRate += -(FORWARD_ACCEL_COEFF * forwardAccel); Real lateralAccel = -dir->y * accel->x + dir->x * accel->y; - m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel) * timeScale; + m_locoInfo->m_accelerationRollRate += -(LATERAL_ACCEL_COEFF * lateralAccel); } // limit acceleration pitch and roll From e5b12c828f8e5caa894d3f2249c3c5facf567af1 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 29 Jun 2026 13:21:27 -0400 Subject: [PATCH 20/21] docs(drawable): Document xfer version 9 in version history --- GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index baf84db0a61..337650b7a24 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -4966,6 +4966,7 @@ void Drawable::xferDrawableModules( Xfer *xfer ) * 6: Added m_ambientSoundEnabledFromScript flag * 7: Save the customize ambient sound info * 8: TheSuperHackers @bugfix Removed m_prevTintStatus because loading its value is unnecessary and undesirable + * 9: TheSuperHackers @tweak Changed m_timeElapsedFade and wheel airborne timers from integer to Real for frame-rate independent timing */ // ------------------------------------------------------------------------------------------------ void Drawable::xfer( Xfer *xfer ) From 77e6dadca3b80e82d7cc67d9916bb5cbd959084a Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 29 Jun 2026 13:21:27 -0400 Subject: [PATCH 21/21] Replicate to generals --- Generals/Code/GameEngine/Source/GameClient/Drawable.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index a38af1199b4..e7ffd568e1b 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -4306,6 +4306,7 @@ void Drawable::xferDrawableModules( Xfer *xfer ) * 4: Added m_ambientSoundEnabled flag * 5: save full mtx, not pos+orient. * 6: TheSuperHackers @bugfix Removed m_prevTintStatus because loading its value is unnecessary and undesirable + * 7: TheSuperHackers @tweak Changed m_timeElapsedFade and wheel airborne timers from integer to Real for frame-rate independent timing */ // ------------------------------------------------------------------------------------------------ void Drawable::xfer( Xfer *xfer )