bugfix(drawable): Decouple visual render from projectile's logic position#2598
Conversation
|
| Filename | Overview |
|---|---|
| GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h | Adds m_visualExtrapolationMtx, m_useExtrapolation, m_logicVelocityPtr members and setLogicVelocity/clearLogicVelocity methods; the new public: section accidentally changes the access level of m_isModelDirty in DIRTY_CONDITION_FLAGS builds. |
| GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp | Core extrapolation logic: applySubFrameExtrapolation (correct alpha = accumulator x logicFPS), getTransformMatrix now returns the visual matrix when extrapolating; setLogicVelocity(nullptr) omits resetting m_useExtrapolation, leaving getTransformMatrix returning a stale matrix after cleanup. |
| GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp | Adds m_logicStepVelocity tracking (zeroed each update, computed as next-step minus current position), wires setLogicVelocity on fire, and cleans up via onDelete; logic is correct. |
| GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp | Captures m_logicStepVelocity = m_vel at the end of doLaunch and doAttack, zeros it in DEAD state, and properly cleans up via onDelete; previously-empty onDelete now correctly chains to UpdateModule::onDelete. |
| GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h | Exposes getLogicTimeAccumulator() accessor for the already-existing m_logicTimeAccumulator; straightforward and correct. |
| GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h | Adds getProjectileLogicVelocity() virtual method with a default nullptr return to ProjectileUpdateInterface; non-breaking addition. |
| GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h | Adds m_logicStepVelocity member, onDelete override, and getProjectileLogicVelocity override; clean declarations. |
| GeneralsMD/Code/GameEngine/Include/GameLogic/Module/NeutronMissileUpdate.h | Adds m_logicStepVelocity member and getProjectileLogicVelocity override; clean declarations. |
Sequence Diagram
sequenceDiagram
participant Logic as Game Logic (Fixed-rate)
participant Proj as DumbProjectile/NeutronMissile
participant Draw as Drawable
participant Eng as GameEngine (accumulator)
participant Rend as Renderer
Logic->>Proj: update()
Proj->>Proj: advance position to flightPath[K]
Proj->>Proj: m_logicStepVelocity = flightPath[K+1] - flightPath[K]
Proj->>Draw: setLogicVelocity at launch time
loop Each render frame (high FPS)
Rend->>Eng: getLogicTimeAccumulator()
Eng-->>Rend: elapsed time since last logic frame
Rend->>Draw: updateDrawable()
Draw->>Draw: alpha = accumulator x logicFPS (0 to 1)
Draw->>Draw: visualMtx = logicPos + velocity x alpha
Draw->>Draw: m_useExtrapolation = TRUE
Rend->>Draw: getTransformMatrix()
Draw-->>Rend: m_visualExtrapolationMtx (smoothed)
end
Logic->>Proj: update() next logic frame
Proj->>Proj: Position snaps to flightPath[K+1], new velocity set
Prompt To Fix All With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h
Line: 743-749
Comment:
**`public:` section accidentally exposes `m_isModelDirty`**
Inserting the new `public:` specifier before `#ifdef DIRTY_CONDITION_FLAGS` silently changes `m_isModelDirty` from private to public in any build that defines `DIRTY_CONDITION_FLAGS`. A `private:` re-specifier is needed before the `#ifdef` block.
```suggestion
public:
void setLogicVelocity(const Coord3D* velocity);
static void clearLogicVelocity(Object* obj);
private:
#ifdef DIRTY_CONDITION_FLAGS
Bool m_isModelDirty; ///< if true, must call replaceModelConditionState() before drawing or accessing drawmodule info
#endif
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp
Line: 5657-5660
Comment:
**`setLogicVelocity(nullptr)` leaves `m_useExtrapolation` stale**
When `clearLogicVelocity` is called (e.g. in `onDelete`), `m_logicVelocityPtr` is set to `nullptr` but `m_useExtrapolation` is not reset. Until the next `updateDrawable()` executes (which would skip `applySubFrameExtrapolation` and never clear the flag), any caller of `getTransformMatrix()` — audio positioning, collision proximity checks, etc. — will receive the old extrapolated matrix rather than the current logic position.
```suggestion
void Drawable::setLogicVelocity(const Coord3D* velocity)
{
m_logicVelocityPtr = velocity;
if (velocity == nullptr)
m_useExtrapolation = FALSE;
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (6): Last reviewed commit: "added caching" | Re-trigger Greptile
| { | ||
|
|
||
| } | ||
| void Drawable::setLogicVelocity(const Coord3D* velocity) |
There was a problem hiding this comment.
I think this is going into the right direction and this ideally becomes applicable to ALL drawable, not just projectiles. I am not a fan of this hand holding of requiring external users to set velocities. It makes caller code more complicated.
I suggest to try implement this more generic so that any movements are interpolated between real logic frames. To do that, try to have a recycled history of 3 to 5 transforms and then extrapolate them. This could work alright even with accelleration and braking. Maybe make the history length configurable per drawable, because for objects that have no accelleration, 2 points would be enough for velocity.
This PR fixes the visual "stutter" or "jitter" seen on fast-moving projectiles when playing at high framerates. While the game's internal logic updates at a fixed frequency, rendering often happens much faster, causing projectiles to appear to "step" across the screen rather than move smoothly.
Implemented a system that fills in the gaps between logic steps, allowing projectiles to match the user's framerate.
Created a unified way for the game's rendering system to communicate with various projectile types.
This applies to dumb and precalculated projectiles. Such as Nuke superweapon and Inferno Cannon. Particles that are attached to the projectiles and smart projectiles are outside of the scope of this PR.
Video is of 30 logic and 60 visual, notice the projectile moving with every step:
https://github.com/user-attachments/assets/fcdefce9-723d-4d3b-aeda-d60d1fcce5de
Todo:
replicate to generals