Table of Contents

Jitter2 Physics Integration

Demonstrates integrating Jitter2 physics engine with Stride. Shows how to create a physics world, synchronize physics bodies with visual entities, and simulate dynamic rigid body interactions. Features 150 falling cubes with proper collision detection and a static ground plane.

The Program.cs file shows how to:

  • Integrating external physics engine (Jitter2)
  • Creating and managing physics world
  • Synchronizing physics bodies with visual entities
  • Dynamic rigid body simulation
  • Static vs dynamic physics bodies
  • Fixed-timestep physics update loop, decoupled from the render frame rate

Jitter2 Physics Integration

View on GitHub.

using Jitter2;
using Jitter2.Collision.Shapes;
using Jitter2.Dynamics;
using Jitter2.LinearMath;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;

const float CubeSize = 0.5f;
const float VerticalSpacing = 2f;

// Physics runs at a fixed rate, decoupled from the render frame rate (see Update)
const float FixedTimeStep = 1f / 100f;
const int MaxStepsPerFrame = 5;

var groundSize = new Vector3(15f, 1, 15f);

// Initialize Jitter2 physics world with 4 substeps for better accuracy
var world = new World()
{
    SubstepCount = 4,
};

// Each cube's visual entity and physics body, kept together so they can never drift out of sync
var cubes = new List<CubeInstance>();

// Accumulates real elapsed time between fixed physics steps (see Update)
var accumulatedTime = 0f;

using var game = new Game();

game.Run(start: Start, update: Update);

void Start(Scene rootScene)
{
    game.Window.Title = "Jitter 2 Physics Example - Stride Community Toolkit";

    game.SetupBase3D();
    game.AddSkybox();
    game.Add3DCameraController();
    game.AddProfiler();

    CreateGround(rootScene);
    CreateCubes(rootScene, count: 150);
}

void Update(Scene scene, GameTime time)
{
    // Accumulate real elapsed time and step the simulation in fixed increments, so it advances at
    // the correct speed even when the render frame rate drifts away from FixedTimeStep. Capping the
    // number of steps per frame avoids a "spiral of death" if a frame hitches badly.
    accumulatedTime += (float)time.Elapsed.TotalSeconds;

    var steps = 0;

    while (accumulatedTime >= FixedTimeStep && steps < MaxStepsPerFrame)
    {
        world.Step(FixedTimeStep, true);

        accumulatedTime -= FixedTimeStep;
        steps++;
    }

    // If the frame rate stays below the physics rate for a sustained period rather than a single
    // hitch, the loop above can never fully drain accumulatedTime and the backlog would otherwise
    // keep growing forever. Clamping it bounds that backlog to one frame's worth of catch-up,
    // trading permanently-delayed simulation time for a value that can't grow without bound (which
    // would eventually lose precision as a float in a long-running session).
    accumulatedTime = MathF.Min(accumulatedTime, FixedTimeStep * MaxStepsPerFrame);

    // Update visual entities to match their physics body positions
    SyncPhysicsToEntities();
}

void CreateGround(Scene rootScene)
{
    // Create visual ground plane
    var groundEntity = game.Create3DPrimitive(PrimitiveModelType.Plane, new()
    {
        Size = groundSize,
    });
    groundEntity.Scene = rootScene;

    // Create physics body for the ground (static, won't move)
    var groundBody = world.CreateRigidBody();
    groundBody.MotionType = MotionType.Static;
    groundBody.AddShape(new BoxShape(groundSize.X, groundSize.Y, groundSize.Z));
    groundBody.Position = new JVector(0, -0.5f, 0);
}

void CreateCubes(Scene rootScene, int count)
{
    for (int i = 0; i < count; i++)
    {
        var cubePosition = new Vector3(0, 10 + i * VerticalSpacing, 0);

        // Create visual cube entity
        var cubeEntity = game.Create3DPrimitive(PrimitiveModelType.Cube, new()
        {
            Material = game.CreateMaterial(Color.Red),
            Size = new Vector3(CubeSize),
        });

        cubeEntity.Transform.Position = cubePosition;
        cubeEntity.Scene = rootScene;

        // Create physics body for the cube (dynamic, affected by forces)
        var cubeBody = world.CreateRigidBody();
        cubeBody.AddShape(new BoxShape(CubeSize));
        cubeBody.SetMassInertia(1f);
        cubeBody.Position = new JVector(cubePosition.X, cubePosition.Y, cubePosition.Z);

        cubes.Add(new CubeInstance(cubeEntity, cubeBody));
    }
}

void SyncPhysicsToEntities()
{
    // Copy physics body transforms to visual entities each frame
    foreach (var cube in cubes)
    {
        var position = cube.Body.Position;
        var orientation = cube.Body.Orientation;

        cube.Entity.Transform.Position = new Vector3(position.X, position.Y, position.Z);
        cube.Entity.Transform.Rotation = new Quaternion(orientation.X, orientation.Y, orientation.Z, orientation.W);
    }
}

/// <summary>
/// Pairs a cube's visual entity with its physics body, so the two can never drift out of sync.
/// </summary>
sealed record CubeInstance(Entity Entity, RigidBody Body);