Table of Contents

Give Me a Cube

Add behaviour to an entity with a SyncScript component instead of the update callback of game.Run. The script steers a cube around a circle each frame by setting the linear velocity of a kinematic Bepu body, so it really sweeps through the scene and knocks a ring of smaller dynamic cubes out of the way. Writing Transform.Position instead would move only the mesh and collide with nothing.

The Program.cs file shows how to:

  • Adding behaviour with a SyncScript component
  • The Start and Update lifecycle methods
  • Configuring a script through public properties
  • Moving a kinematic body by setting its linear velocity
  • Waking a body after changing its velocity
  • Why writing Transform.Position does not move a physics body
  • Sizing a primitive and its collider together
  • Using helpers: SceneSetup3D, UseSkybox, CreatePrimitive

Give Me a Cube

View on GitHub.

using E02_3D_GiveMeACube;
using Stride.CommunityToolkit.Bepu;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;

// A cube orbits the scene and knocks a ring of smaller cubes out of its way.
// The orbit is driven by OrbitScript, a SyncScript attached to the entity, rather than by the update
// callback of game.Run(...) - the point of the example.

// Half of a 1x1x1 cube, so it sits exactly on the ground
const float CubeRestingHeight = 0.5f;

const float OrbitRadius = 3f;
const float OrbitSpeed = 1f;

const float LooseCubeSize = 0.4f;
const int LooseCubeCount = 8;

using var game = new Game();

game.Run(new SceneSetup3D().UseBepu().UseSkybox(), start: Start);

void Start(Scene rootScene)
{
    CreateCentreCube(rootScene);
    CreateLooseCubes(rootScene);
    CreateOrbitingCube(rootScene);
}

/// <summary>
/// Creates the cube the orbit is centred on, using the toolkit's default Bepu physics: a dynamic body
/// with a matching box collider. It rests on the ground and the orbit never reaches it.
/// </summary>
void CreateCentreCube(Scene rootScene)
{
    var cube = game.CreatePrimitive(Primitive3D.Cube());

    cube.Transform.Position = new Vector3(0, CubeRestingHeight, 0);
    cube.Scene = rootScene;
}

/// <summary>
/// Scatters small dynamic cubes evenly around the orbit path, so there is something for the orbiting
/// cube to collide with.
/// </summary>
void CreateLooseCubes(Scene rootScene)
{
    for (var i = 0; i < LooseCubeCount; i++)
    {
        // The half step keeps the first cube clear of the orbiting cube's spawn point
        var angle = MathF.Tau * (i + 0.5f) / LooseCubeCount;

        // Size drives both the generated model and the generated collider
        var cube = game.CreatePrimitive(Primitive3D.Cube(LooseCubeSize));

        cube.Transform.Position = new Vector3(MathF.Sin(angle) * OrbitRadius, LooseCubeSize / 2, MathF.Cos(angle) * OrbitRadius);
        cube.Scene = rootScene;
    }
}

/// <summary>
/// Creates the cube that <see cref="OrbitScript"/> moves.
/// </summary>
/// <remarks>
/// The body is kinematic, so it follows the scripted path without being affected by gravity or by the
/// cubes it hits, while still pushing them aside. A dynamic body would fall and be knocked off course;
/// an entity with no body at all could be moved by writing its transform, but would pass straight
/// through everything.
/// </remarks>
void CreateOrbitingCube(Scene rootScene)
{
    var cube = game.CreatePrimitive(Primitive3D.Cube(), new BepuPhysicsOptions { Body = BodyMode.Kinematic });

    // Spawn where the orbit starts: on the circle, along +Z from its centre
    cube.Transform.Position = new Vector3(0, CubeRestingHeight, OrbitRadius);

    // Adding the script is what gives the entity its behaviour
    cube.Add(new OrbitScript
    {
        Centre = new Vector3(0, CubeRestingHeight, 0),
        Radius = OrbitRadius,
        AngularSpeed = OrbitSpeed
    });

    cube.Scene = rootScene;
}

OrbitScript.cs

using Stride.BepuPhysics;
using Stride.CommunityToolkit.Engine;
using Stride.Core.Mathematics;
using Stride.Engine;

namespace E02_3D_GiveMeACube;

/// <summary>
/// Drives its entity around a horizontal circle about <see cref="Centre"/>.
/// </summary>
/// <remarks>
/// This is the point of the example: behaviour lives in a <see cref="SyncScript"/> attached to the
/// entity, instead of in the update callback passed to <c>game.Run(update: ...)</c>. Stride calls
/// <see cref="Start"/> once when the entity enters the scene and <see cref="Update"/> once per frame.
/// <para>
/// The entity is expected to carry a kinematic <see cref="BodyComponent"/>. The script moves that body
/// rather than the transform, which is what lets the orbiting cube shove the loose cubes out of its way.
/// </para>
/// <para>
/// It sets <see cref="BodyComponent.LinearVelocity"/> rather than calling <c>SetTargetPose</c>, whose
/// velocity, <c>(target - position) / FixedTimeStep</c>, assumes one physics tick per call. Below the
/// physics rate two ticks run on it, the body overshoots, and each correction overshoots further
/// until it diverges. A known velocity has no such feedback loop.
/// </para>
/// </remarks>
public class OrbitScript : SyncScript
{
    private BodyComponent? _body;
    private float _angle;

    /// <summary>
    /// Point the entity travels around. The entity should be spawned on the circle itself, at
    /// <see cref="Centre"/> plus <see cref="Radius"/> along +Z.
    /// </summary>
    public Vector3 Centre { get; set; }

    /// <summary>
    /// Distance from <see cref="Centre"/>, in world units.
    /// </summary>
    public float Radius { get; set; } = 3f;

    /// <summary>
    /// How fast the entity travels around the circle, in radians per second.
    /// </summary>
    public float AngularSpeed { get; set; } = 1f;

    /// <summary>
    /// How strongly the body is steered back onto the circle, per second. Higher values track the
    /// path more tightly; very high values reintroduce the overshoot this is meant to avoid.
    /// </summary>
    public float CorrectionStrength { get; set; } = 2f;

    /// <summary>
    /// Called once when the entity is added to the scene.
    /// </summary>
    public override void Start()
    {
        _body = Entity.Get<BodyComponent>();

        if (_body is null)
        {
            Log.Error($"{nameof(OrbitScript)} expects a kinematic {nameof(BodyComponent)} on the same entity.");
        }
    }

    /// <summary>
    /// Called once per frame.
    /// </summary>
    public override void Update()
    {
        // DeltaTime keeps the motion frame rate independent
        _angle += AngularSpeed * Game.DeltaTime();

        if (_body is null) return;

        // Move the body with a velocity, never Entity.Transform.Position: the sync runs physics to
        // transform only, so a transform write moves the mesh, leaves the collider and pushes nothing.

        // Velocity of a point travelling around a circle is the derivative of its position
        var tangent = new Vector3(MathF.Cos(_angle), 0, -MathF.Sin(_angle)) * (AngularSpeed * Radius);

        // Steer gently back onto the circle so small integration errors cannot accumulate
        var idealPosition = Centre + new Vector3(MathF.Sin(_angle), 0, MathF.Cos(_angle)) * Radius;
        var correction = (idealPosition - _body.Position) * CorrectionStrength;

        _body.LinearVelocity = tangent + correction;

        // Setting a velocity does not wake a sleeping body, so do it explicitly
        _body.Awake = true;
    }
}