Skip to content

Physics Loop

Björn Aheimer edited this page Oct 23, 2024 · 6 revisions

Purpose

All changes to the state of the game world are performed by the PhysicsEngine, or more precisely, by the "physicists" it employs. These physicists each have their dedicated role in the physics pipeline which we will explain in the following. Furthermore, we will dive into the realm of rigid body physics and cover the theoretical foundations to our physics simulation.

Decoupling Physics and Frame Rate

Not to be confused with the main loop, the physics loop runs "independent" from the main loop, thereby implementing the requirement of the physics simulation not being coupled to the frame rate. Decoupling physics from the frame rate is an important task in our mission to make the physics more realistic and creating a valuable user experience for the player. The usefulness of this requirement becomes particularly evident in two cases:

  • when the physics are compute-heavy and require more time than the renderer
  • when the time step derived from the frame time is too long to achieve accurate physics behaviour

In the first case, the frame rate should not be held back by the physics, while in the second case, the physics should not have to wait for the next frame. The first case also shows the need for interpolation between to physics steps in order to render an accurate depiction of the current state at each frame.

Implementation-wise, multiple options for where to place the physics loop were considered, most notably we discussed putting it on a concurrent thread or "in the main loop". We decided on the latter option, placing it in between receiving the user input and rendering a frame. Using time accumulation, the physics are still independent from the frame rate despite only being called between two frames. The update() method implements this idea.

void PhysicsEngine::update(std::queue<GameEvent> &events) {
    const auto currentTime = static_cast<floatType>(GetTime());
    const floatType frameTime = currentTime - this->timeLastUpdate;
    this->timeLastUpdate = currentTime;

    const auto maxSpfTime =
        static_cast<floatType>(this->physicsConstants.maxNumberOfPhysicsStepsPerFrame) * this->deltaT;

    this->accumulator += std::min(frameTime, maxSpfTime);

    this->eventProcessor.addEvents(events);

    while (this->accumulator >= this->deltaT) {
        this->updateTimeStep();
        this->accumulator -= this->deltaT;
    }

    const floatType alpha = this->accumulator / this->deltaT;
    this->interpolator.interpolate(alpha);
    this->eventProcessor.clearRepeatedEvents();
}

One can imagine the algorithm above as follows:

  • Having rendered the current state, we measure the time between the last frame and the current frame. Time is produced by the renderer
  • If the renderer produced too much time (due to lag), we restrict the number of physics time steps performed, so that the next frame does not take even longer than the current one. This phenomenon of increasing frame times due to too many physics steps in between frames is called the spiral of death. A suitable number for the appropriate maximum number of time steps is found by empirical tests.
  • User inputs, which are received once per frame, are passed to the event processor.
  • If the renderer has accumulated enough time for the physics engine to perform a step, a physics time step is performed and the world state is updated. Time is consumed by the physics engine.
  • Repeat the previous step (perform physics time steps) until there is not enough time left.
  • Interpolate the time that is left in the accumulator. Interpolation is done between the last and the second to last step, making the rendered state exactly one time step behind the actual physics state.

This has some benefits in both the above cases. If the physics are slower than the renderer, the rendered state is still at most one frame behind the actual state. If the physics are faster than the renderer, then the rendered state will at most one time step deltaT behind the actual state.

For more details, see the game engineering evergreen Fix Your Timestep.

What happens in a time step?

In one time step of the physics engine, updates are executed in the following order:

  1. Spawning: Items and rocks are spawned, the terrain generation thread is started or joined if necessary.
  2. Event Processing: User input events are mapped to state changes of the world and the hiker.
  3. Accelerator: (Gravitational) forces are applied to the entities.
  4. Positioner: The positions of entities are updated.
  5. Collision Handling: Collisions are detected and resolved. This step is performed multiple times to ensure the stability of the simulation.
  6. Destructor: An entity will be destroyed if it is out of scope or otherwise undergoes a process of destruction.

loop

Spawner

  • generates new chunks of the terrain
  • spawns rocks and items randomly

Detailed information about the spawning can be found here: Spawning

Event Processor

  • performs game events (that were created based on the user inputs) and adapts the state of the hiker and inventory based on the player's input
  • includes hiker crouching, jumping, picking up/ dropping/ using/ switching items, moving in x-direction

Note: The inputs are gathered once per frame, since the raylib library used for this task synchronizes the input in this way. However, the corresponding game events are still processed every time step. Events that are executed only once (e.g., pick item) are executed in the first step after the frame they were gathered in. Events that are executed repeatedly (e.g., movement in x-direction) are executed at every time step between two frames.

Accelerator

  • updates velocities of hiker and the rocks based on gravitational forces
  • updates direction of the hiker (moving left or right)

Collision Detector

  • checks if hiker is hit by a rock
  • checks if rocks have collided

Collision Handler

  • adapts hiker state when the hiker and a rock collide
  • adapts rock state when rock collides with the terrain (mountain)
  • adapts rock states when rocks collide with each other

Positioner

  • updates position of world border (visible part of the world)
  • updates monster position (continuous movement in x direction)
  • updates rock position based on rock velocity
  • updates hiker position

Destructor

  • destructs items and rocks when they fall behind the world border, so that they are no longer visible
  • destructs mountain that is no longer visible

$\rightarrow$ See how entities are spawned here.

Clone this wiki locally