1. Observed Physics Issues in Painkiller
Painkiller’s physics and animations demonstrably break at high frame rates. Community sources repeatedly note that capping to 60 FPS is “necessary” because above that “the physics are broken”. In practice, players see ragdolls “fly” strangely when killed with the Painkiller beam, and some scripted jumps or drops fail (e.g. enemies meant to drop through ceilings get stuck). One mod patch for Painkiller: Overdose explicitly fixes “Meteorite/Grenade rains” and “hanging corpses” that were originally “FPS based” and would “fling [corpses] like a feather”. Similarly, special weapon mechanics can misbehave: for example the HellBlade’s ammo consumption originally depended on the frame rate.
Other symptoms include input and loading glitches tied to vsync (the NVidia forum notes a “60 Hz fullscreen lock” bug when MSAA is enabled) and even system issues on multi-core CPUs. A user on the Widescreen Gaming Forum warned that Painkiller has “serious issues with multicore CPUs” – the only reliable fix was to force the game onto a single core or enable vsync. In short, anything that changes the timing of frame updates (higher FPS, multi-core scheduling, no vsync) can break game logic.
2. Technical Causes
The root cause is that Painkiller’s update loop ties physics/game logic directly to each frame rather than using a fixed time step. In developer interviews, Painkiller is noted to use Havok physics, but there is no evidence the engine implemented a proper fixed-step integrator for Havok. Instead, the game likely uses each frame’s time delta (or even an implicit unit of “one frame”) as the physics step. According to physics programming principles, this approach makes simulation results depend on the frame rate. As Glenn Fiedler’s “Fix Your Timestep!” explains, using a variable delta time each frame can cause physics to speed up, explode, or tunnel inconsistently when the frame rate changes. Painkiller appears to exhibit exactly these symptoms.
Moreover, many in-game systems likely schedule events by frame count or frame-based delays. For example, scripted triggers or enemy AI routines might assume a constant number of frames per second. When actual FPS deviates, those triggers fire too early or too late (or not at all). The mod patch notes suggest things like grenade spawn rates were per frame, hence fixed once they were made “no longer FPS based”. In combination, the engine’s lack of a fixed-tick accumulator, and coupling of physics to rendering, plus possible use of integer timers or frame-based loops, causes the frame-rate-dependent bugs.
3. Consequences for Gameplay and Modding
Frame-tied physics has serious gameplay consequences. Players on modern hardware either must cap FPS to 60 (losing smoothness) or suffer glitches. Critical gameplay elements (jumping puzzles, enemy hits) may fail unpredictably, making some parts of the game unplayable at higher FPS. Multiplayer (and any deterministic mechanics) also suffer: if client and host run at different FPS, their game states diverge, leading to desyncs or incorrect predictions. Modders likewise face challenges: any mod that adds fast-moving physics objects or scripts might work at 60 Hz but break at higher rates. Reproducing bugs is hard when simulation depends on frame timing. Even tool enhancements like the FPS limiter mod had to manually throttle the game (to 120 FPS) to restore “playable” physics.
In summary, the game is essentially non-deterministic across hardware, and any attempt to speed it up causes “broken” physics. This undermines reproducibility (critical for debugging and mods) and spoils the intended gameplay feel.
4. Proposed Solutions (Prioritised)
We recommend these fixes, in order of importance:
-
Implement a Fixed Timestep Loop: Use a constant physics update interval (e.g. 60 Hz or 30 Hz) with an accumulator. Each frame, accumulate real elapsed time, and step the physics simulation in fixed-size chunks. This guarantees consistent simulation regardless of rendering FPS.
-
Clamp or Sub-step Excess Time: To prevent instabilities if a frame takes too long, cap each physics step to the fixed interval. If frameTime > fixedStep, run multiple sub-steps within a loop (a “semi-fixed timestep”). This ensures no single step is too large (avoiding explosions or tunneling).
-
Interpolate for Smooth Rendering: With physics stepping decoupled, use interpolation to smoothly render frames between physics updates. Store the previous and current physics state, compute
alpha = accumulator/fixedStep, and interpolate positions for rendering. This allows rendering at high FPS while physics runs at fixed rate. -
Decouple Networking/Logic Tick: Separate any network or game-logic tick from rendering. For example, keep a game simulation tick (e.g. 60 Hz) that advances game logic, while rendering and input processing can run independently. This prevents network updates from speeding up with graphics.
-
Rework Timing/Event System: Convert any frame-count-based timers to real-time or fixed-step timers. For example, instead of “wait 30 frames” use “wait 0.5 seconds” in scripts. Ensure that triggers and scripts use the fixed timestep rather than raw frame events.
-
Determinism and Compatibility: Make the fixed-step simulation deterministic (given same inputs it produces the same output). For mods, offer a “legacy mode” fallback that emulates the old timing (e.g. by limiting FPS to 60 under the hood) for backwards compatibility.
-
User-Configurable Tick Rate: Expose a configuration for target physics tick rate (like
SetMaxFPSor a newMaxTickRate). The engine already supports a max FPS setting (community mod usesWORLD.SetMaxFPS); we would embed an official config. This allows modders or players to tune if needed.
Each fix should be carefully tested. The highest priority is a correct fixed timestep (guaranteed consistent physics) over e.g. just limiting FPS by other means. The table below summarises key symptoms and solutions:
| Symptom / Issue | Likely Cause | Proposed Fix | Risk/Impact |
|---|---|---|---|
| Ragdolls/enemies misbehave at >60 FPS | Physics and forces updated per render frame | Use fixed-update loop (e.g. 60 Hz tick) with accumulator | Major: Changes pacing of physics. Need tuning, but fixes accuracy. |
| Scripted events fail (e.g. drop triggers) | Frame-based event counters / timers tied to FPS | Use time-based triggers or fixed-tick counters | Medium: Requires rewriting scripts/triggers, thorough testing. |
| Multi-core/unlimited FPS crashes | Engine not thread-safe / no FPS cap | Force single-thread or implement thread safety; set fixed max FPS | Low: Workaround vsvsync may reduce performance. Full thread fix complex. |
| Network inconsistencies (multiplayer) | Server/client tick tied to render loop | Separate network tick, or enforce fixed server tick (e.g. 60 Hz) | Medium: May need new network code; improves sync. |
5. Migration Plan (Legacy Engine)
For the legacy Painkiller engine (no source code currently available publicly), we outline an implementation approach:
- Engine Code Changes: Identify the main game loop (in the engine or DLL). Insert a timing accumulator: measure real time each frame, add to
accumulator. In a loop, whileaccumulator ≥ fixedStep, call the existing physics/logic update function (ideally extracted) with a constantdt = fixedStep, then subtract fromaccumulator. If source code is not available, one could inject code via a DLL patch or use the existing LScripts mechanism to call a new update loop, if feasible. - Config Options: Introduce new settings (e.g. in
Config.ini) forPhysicsTickRateor adapt the existingMaxFPSto mean physics tick. Ensure there’s a default that replicates old behaviour (possibly “60 Hz fixed” or even an “unlimited” legacy mode) for compatibility. Provide a console command (as the mod does) to adjust it. - Script Integration: Review any engine scripts (Lua/LScripts) for frame-based logic. Replace frame-dependent waits (
Wait:N) with time-based or fixed-step waits if possible. If LScript exposes time, use that; otherwise use the new tick rate to calculate frames. - Testing Strategy: Create automated tests or manual checklists (below) running the game at various simulated FPS (30, 60, 120, 240) and verifying identical outcomes. Compare with baseline (unmodded) at 60 FPS to ensure fixes do not break intended gameplay. Focus on boss fights, physics puzzles, and ragdoll interactions.
- Rollback/Compatibility: If some mods expect the old fast-FPS behaviour (unlikely), provide a “legacy mode” toggle. Consider a rollback/resume system only if the engine ever supported save-scumming; more importantly ensure new code does not conflict with saved game states or cheats.
- Performance Trade-offs: A fixed timestep loop may require multiple physics steps per frame on fast machines, costing CPU. However, Painkiller’s physics are relatively simple (2004 era). We recommend targeting 60 Hz; if performance suffers, consider “semi-fixed” where a maximum sub-step count is allowed (as in Gaffer’s article). Use interpolation to allow rendering at higher rates without additional simulation cost.
6. Example Pseudocode and Flowchart
Fixed-timestep loop (pseudocode):
c
const double fixedStep = 1.0 / 60.0; // 60 Hz physics tick
double accumulator = 0.0;
double prevTime = getTime();
GameState prevState, currState;
while (gameRunning) {
double now = getTime();
double frameTime = now - prevTime;
prevTime = now;
accumulator += frameTime;
// Perform fixed-timestep updates
while (accumulator >= fixedStep) {
prevState = currState;
updatePhysics(currState, fixedStep); // advance physics/logic
accumulator -= fixedStep;
}
// Interpolation factor
double alpha = accumulator / fixedStep;
// Interpolate state for smooth render
GameState renderState = interpolateState(prevState, currState, alpha);
render(renderState);
}
This uses the accumulator pattern. It ensures physics runs at a constant rate (60 Hz) no matter how fast getTime() advances. The renderer interpolates between prevState and currState based on alpha for smooth visuals.
mermaid
flowchart TD
A[Start Frame] --> B{Get current time, compute Δt}
B --> C[accumulator += Δt]
C --> D{accumulator >= fixedStep?}
D -- Yes --> E[prevState = currState]
E --> F[updatePhysics(currState, fixedStep)]
F --> G[accumulator -= fixedStep]
G --> D
D -- No --> H[alpha = accumulator / fixedStep]
H --> I[interpolate(prevState, currState, alpha)]
I --> J[Render interpolated state]
J --> A
This flowchart illustrates how physics updates (yellow) run in discrete steps of fixedStep time, while rendering (blue) uses interpolation between physics states.
7. Testing Checklist & Metrics
To verify the fixes, we propose this checklist and metrics:
- Multi-FPS Consistency: Run key gameplay segments (start of Level 1, boss fights, puzzles) at different frame caps (30, 60, 120, 240 FPS) and ensure identical outcomes. No ragdoll anomalies or missed triggers should occur.
- Physics Stability: Dropping or throwing objects should behave the same at all frame rates. Ragdoll trajectories from the Painkiller beam should land consistently (no “flying away”).
- Performance: Measure CPU usage and frame time. Ensure physics steps per second ~= tick rate, and that the simulation “keeps up” without spiral-of-death. If not, consider lowering tick rate or capping steps per frame as per semi-fixed advice.
- Networking: If multiplayer is used, check that lag compensation or prediction is unaffected. Host/client should not desync due to frame differences.
- User Experience: Confirm that input handling (movement, shooting) feels correct with interpolation; no input lag introduced. Check mouse/keyboard responsiveness.
- Regression Tests: For mod compatibility, load popular mods (e.g. co-op, new levels) at uncapped FPS and verify they don’t break scripts. Compare with stock behaviour at 60 Hz.
- Tools/Automation: Use a script or tool to force frame rates and capture logs. A simple metric is to log the number of physics steps per second (should be constant, e.g. 60).
If any test fails, investigate whether physics or logic code still depends on frame counts, and adjust accordingly. For performance, the key metric is that simulation time per real second ≈ physicsTickRate (e.g. 60 updates per second on average), and that rendering can exceed it without issues.
Sources
The above analysis draws on both official and community information about Painkiller’s engine and physics:
- Steam Community Discussion – “What is the optimal framerate for this game?” (Painkiller: Black Edition). Reports from players confirming that physics glitches occur above ~60 FPS.
- Widescreen Gaming Forum – “Painkiller: Detailed Report” (2007). Notes on engine issues with modern multi-core CPUs and the need to cap FPS or use one core.
- ModDB Patch Notes – Painkiller: Overdose 86.2u Patch (community mod). Lists specific fixes of frame-rate-dependent bugs (e.g. Hellblade ammo, meteor rains, ragdoll flinging).
- Steam Community Guide – “Painkiller FPS limiter” by Peppins (2015, updated 2023). Demonstrates that by default the engine had no FPS cap (unlimited mode) and community mods set it to 120 FPS for stability.
- Gaffer on Games – “Fix Your Timestep!” (Glenn Fiedler, 2004). Explains why physics must not be tied to variable frame Δ-time and provides fixed-step and accumulator techniques.
- Jakub’s Tech Blog – “Reliable fixed timestep & inputs” (2024). Describes a fixed-update game loop with interpolation and the pitfalls of separating physics and rendering updates.
Each source is cited with numbered references. Community sources are explicitly flagged and developer guidance on fixed timesteps provides the technical basis for the proposed solution.