Prerequisites
Game Version
Bug Description
rotateCamera, zoomCamera and pitchCamera convert their authored duration into a count of 33.33 ms frames, then advance exactly one step per render frame. A scripted camera move therefore completes renderFps / logicFps times faster than the mission scripter intended.
m_rcInfo.numFrames = milliseconds/TheW3DFrameLengthInMsec; // W3DView.cpp:2721
...
void W3DView::rotateCameraOneFrame()
{
m_rcInfo.curFrame++; // W3DView.cpp:3273
TheW3DFrameLengthInMsec is a fixed 33.33 ms (W3DView.cpp:101), and rotateCameraOneFrame is reached from updateCameraMovements() inside W3DView::update(). That sits on the render path: GameEngine::update() calls TheGameClient->UPDATE() unconditionally on every render frame, which reaches W3DDisplay::draw() and its updateViews(), while TheGameLogic->UPDATE() is gated by canUpdateGameLogic(). Nothing scales the camera step by TheFramePacer. The same applies to zoomCameraOneFrame (:3344) and pitchCameraOneFrame (:3370).
Network games hit this without any special setup, because FramePacer::getActualLogicTimeScaleFps() returns TheNetwork->getFrameRate() whenever TheNetwork is set. The logic rate is then pinned near 30 while the render rate is whatever the player's limit allows, so scripted camera moves in a multiplayer or co-op mission run several times too fast.
Measured with a "1 rotation over 2.00 seconds" camera move, logic time scale pinned at 30 FPS, varying only the max render FPS:
| max render FPS |
wall time |
vs authored |
| 30 |
1960 ms |
correct |
| 60 |
986 ms |
2.0x too fast |
| 120 |
525 ms |
3.8x too fast |
numFrames was 60 in every run, so completion time is numFrames / renderFps — linear in the render rate and independent of the logic rate.
Game logic also stays frozen for as long as the render-paced camera takes: GameEngine::isTimeFrozen() waits on isCameraMovementFinished(), whose flags are cleared only by those render-paced functions, and that gates GameLogic::update(). At high frame rates the mission resumes proportionally early.
Camera translation along a waypoint path is already correct, and the fix for it is in the same file: #1451 converted moveAlongWaypointPath to accumulate elapsed milliseconds (m_mcwpInfo.elapsedTimeMilliseconds += milliseconds) fed by TheFramePacer->getLogicTimeStepMilliseconds(FramePacer::IgnoreFrozenTime), and compares against a total duration rather than a frame count. Rotate, zoom and pitch were left on the old frame-counting form.
Reproduction Steps
- Make a map script with
Rotate Camera Around The Current View Center: 1 rotation over 10.00 seconds, with no freeze-time modifier.
- In game, set the max render FPS to 30 with
Ctrl + Keypad Plus / Ctrl + Keypad Minus, then set the logic time scale to 30 with Shift + Ctrl + Keypad Plus / Shift + Ctrl + Keypad Minus. Both print their current value on screen.
- Trigger the rotation and time it. It takes about 10 seconds.
- Raise the max render FPS to 120 with
Ctrl + Keypad Plus, leaving the logic time scale at 30, and trigger the rotation again.
- The rotation completes in roughly a quarter of the time.
The logic time scale has to stay below the max render FPS for the two rates to differ at all — changeLogicTimeScale disables the time scale as soon as the requested value reaches the render limit, which restores the render-aligned behaviour and hides the bug.
Additional Context
Do not use Camera Modifier: Freeze Time for the comparison. In that path W3DDisplay::draw() runs the entire camera move to completion inside a single call, and TheFramePacer->update() lives outside that loop, so the configured FPS limit does not apply at all and both runs bypass the cap.
Camera Modifier: Set Final Zoom / Set Final Pitch derive their duration from the already-inflated remaining frame count and inherit the speedup.
Possibly related: #1995 (lock up during scripted camera intro/outro) and #2647 (Fast Forward broken in replay during scripted camera).
Prerequisites
Game Version
Bug Description
rotateCamera,zoomCameraandpitchCameraconvert their authored duration into a count of 33.33 ms frames, then advance exactly one step per render frame. A scripted camera move therefore completesrenderFps / logicFpstimes faster than the mission scripter intended.TheW3DFrameLengthInMsecis a fixed 33.33 ms (W3DView.cpp:101), androtateCameraOneFrameis reached fromupdateCameraMovements()insideW3DView::update(). That sits on the render path:GameEngine::update()callsTheGameClient->UPDATE()unconditionally on every render frame, which reachesW3DDisplay::draw()and itsupdateViews(), whileTheGameLogic->UPDATE()is gated bycanUpdateGameLogic(). Nothing scales the camera step byTheFramePacer. The same applies tozoomCameraOneFrame(:3344) andpitchCameraOneFrame(:3370).Network games hit this without any special setup, because
FramePacer::getActualLogicTimeScaleFps()returnsTheNetwork->getFrameRate()wheneverTheNetworkis set. The logic rate is then pinned near 30 while the render rate is whatever the player's limit allows, so scripted camera moves in a multiplayer or co-op mission run several times too fast.Measured with a "1 rotation over 2.00 seconds" camera move, logic time scale pinned at 30 FPS, varying only the max render FPS:
numFrameswas 60 in every run, so completion time isnumFrames / renderFps— linear in the render rate and independent of the logic rate.Game logic also stays frozen for as long as the render-paced camera takes:
GameEngine::isTimeFrozen()waits onisCameraMovementFinished(), whose flags are cleared only by those render-paced functions, and that gatesGameLogic::update(). At high frame rates the mission resumes proportionally early.Camera translation along a waypoint path is already correct, and the fix for it is in the same file: #1451 converted
moveAlongWaypointPathto accumulate elapsed milliseconds (m_mcwpInfo.elapsedTimeMilliseconds += milliseconds) fed byTheFramePacer->getLogicTimeStepMilliseconds(FramePacer::IgnoreFrozenTime), and compares against a total duration rather than a frame count. Rotate, zoom and pitch were left on the old frame-counting form.Reproduction Steps
Rotate Camera Around The Current View Center: 1 rotation over 10.00 seconds, with no freeze-time modifier.Ctrl + Keypad Plus/Ctrl + Keypad Minus, then set the logic time scale to 30 withShift + Ctrl + Keypad Plus/Shift + Ctrl + Keypad Minus. Both print their current value on screen.Ctrl + Keypad Plus, leaving the logic time scale at 30, and trigger the rotation again.The logic time scale has to stay below the max render FPS for the two rates to differ at all —
changeLogicTimeScaledisables the time scale as soon as the requested value reaches the render limit, which restores the render-aligned behaviour and hides the bug.Additional Context
Do not use
Camera Modifier: Freeze Timefor the comparison. In that pathW3DDisplay::draw()runs the entire camera move to completion inside a single call, andTheFramePacer->update()lives outside that loop, so the configured FPS limit does not apply at all and both runs bypass the cap.Camera Modifier: Set Final Zoom/Set Final Pitchderive their duration from the already-inflated remaining frame count and inherit the speedup.Possibly related: #1995 (lock up during scripted camera intro/outro) and #2647 (Fast Forward broken in replay during scripted camera).