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

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