Table of Contents

Jitter2 Physics - Constraining to 2D

Demonstrates restricting a Jitter2 3D physics simulation to 2D-style behaviour using axis locking. Each falling cube uses AllowedMotion = MotionAxes.PlaneXY to allow translation in X/Y and rotation about Z while locking the other world-space axes, without additional constraints. Builds on E06_Jitter2 with falling cubes spread across a grid so they cascade and pile up sideways.

The Program.cs file shows how to:

  • Constraining a 3D physics engine to 2D motion
  • Configuring RigidBody.AllowedMotion with MotionAxes.PlaneXY
  • Locking world-space translation and rotation axes without additional constraints
  • Synchronising physics bodies with visual entities
  • Fixed-timestep physics update loop, decoupled from the render frame rate

Jitter2 Physics - Constraining to 2D

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 HorizontalSpacing = 0.6f;
const float VerticalSpacing = 0.6f;
const int Columns = 10;

// 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, 1f, 2f);

// Four substeps per step, for accuracy
var world = new World { SubstepCount = 4 };

List<CubeInstance> cubes = [];

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

using var game = new Game();

game.Run(new SceneSetup3D
{
    Ground = null,
    Profiler = true,
    WindowTitle = "Jitter 2 Physics Axis Locking Example - Stride Community Toolkit",
}.UseSkybox(), start: Start, update: Update);

void Start(Scene rootScene)
{
    CreateGround(rootScene);
    CreateCubes(rootScene, count: 350);
}

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++;
    }

    // Clamped to one frame's step budget, so a long stretch below the physics rate slows the
    // simulation down instead of growing the backlog without bound
    accumulatedTime = MathF.Min(accumulatedTime, FixedTimeStep * MaxStepsPerFrame);

    SyncPhysicsToEntities();
}

void CreateGround(Scene rootScene)
{
    var groundEntity = game.CreatePrimitive(Primitive3D.Plane(groundSize.X, groundSize.Z));

    groundEntity.Scene = rootScene;

    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++)
    {
        // A grid of columns rather than one stack, so the cubes cascade sideways and the planar motion shows.
        var column = i % Columns;
        var row = i / Columns;
        var cubePosition = new Vector3((column - Columns / 2f) * HorizontalSpacing, 10 + row * VerticalSpacing, 0);

        var cubeEntity = game.CreatePrimitive(Primitive3D.Cube(CubeSize), new()
        {
            Material = game.CreateMaterial(Color.Red),
            Position = cubePosition,
        });

        cubeEntity.Scene = rootScene;

        var cubeBody = world.CreateRigidBody();
        cubeBody.AddShape(new BoxShape(CubeSize));
        cubeBody.SetMassInertia(1f);
        cubeBody.Position = new JVector(cubePosition.X, cubePosition.Y, cubePosition.Z);

        // Allow translation in X/Y and rotation about Z; lock the other world-space axes.
        cubeBody.AllowedMotion = MotionAxes.PlaneXY;
        cubeBody.AngularVelocity = new JVector(0f, 0f, 0.01f);

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

void SyncPhysicsToEntities()
{
    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);