Table of Contents

Reset Out of Bounds

A row of balls drops onto a tilted plank with no ground under it, rolls off its low end and falls. The update reads every ball's position once a frame, and a ball below the red line goes back above the high end of the plank with TeleportAtRest, still and awake, so it rolls again; the overlay counts the trips. Teleport alone would keep the speed and the spin the ball arrived with.

The Program.cs file shows how to:

  • A threshold under the scene, read from each body's entity position once a frame
  • TeleportAtRest puts a body back still and awake, where Teleport alone keeps its speed and spin
  • A static body tilted through its transform before it joins the scene
  • A model with no body as a marker, with Body = BodyMode.None
  • Using helpers: SceneSetup3D, UseBepu, UseSkybox, CreatePrimitive, CreateMaterial, TeleportAtRest, TextOverlay

Reset Out of Bounds

View on GitHub.

using Stride.BepuPhysics;
using Stride.CommunityToolkit.Bepu;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.Overlay;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Rendering.Text;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Input;

// A body that rolls off the edge of the world falls for as long as the game runs: nothing stops it and
// nothing brings it back, so the scene is one ball poorer. Here a row of balls drops onto a tilted plank
// with no ground under it, rolls off its low end and falls. The update looks at every ball once a frame,
// and a ball below the red line is put back above the high end of the plank with TeleportAtRest, still
// and awake, so it rolls again. The overlay counts the trips.
//
// Teleport alone would move the ball and nothing else: it keeps the speed it fell with and the spin it
// picked up on the plank, and arrives at the top already moving. TeleportAtRest zeroes both and wakes the
// body from a fresh sleep, so every trip starts the same way.

const float FloorLine = -4f;
const float PlankTilt = 12f;
const float PlankLength = 8f;
const float PlankWidth = 6f;
const float BallRadius = 0.3f;
const int BallCount = 5;

var balls = new BodyComponent[BallCount];
var trips = new int[BallCount];

using var game = new Game();

var sceneSetup = new SceneSetup3D
{
    // No ground: a ball that leaves the plank has nothing to land on, which is the point
    Ground = null,
    Camera = CameraSetup.Perspective(position: new Vector3(0f, 2.5f, 13f), rotation: new Vector3(0f, -12f, 0f)).WithController(),
    WindowTitle = "Reset out of bounds - Stride Community Toolkit",
    Resizable = true,
    DpiAware = true,
}.UseBepu().UseSkybox();

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

void Start(Scene scene)
{
    // The plank: a static box, tilted before it joins the scene so a ball set down on its left end rolls off the right
    var plank = game.CreatePrimitive(Primitive3D.Box(new Vector3(PlankLength, 0.2f, PlankWidth)), new BepuPhysicsOptions
    {
        EntityName = "Plank",
        Body = BodyMode.Static,
        Material = game.CreateMaterial(new Color(150, 110, 70), metalness: 0f, glossiness: 0.3f),
    });

    plank.Transform.Rotation = Quaternion.RotationZ(MathUtil.DegreesToRadians(-PlankTilt));
    plank.Scene = scene;

    // The line: a thin slab with no body, so it marks the threshold and stops nothing
    var line = game.CreatePrimitive(Primitive3D.Box(new Vector3(18f, 0.02f, 8f)), new BepuPhysicsOptions
    {
        EntityName = "Floor line",
        Body = BodyMode.None,
        Material = game.CreateMaterial(new Color(220, 60, 60), metalness: 0f, glossiness: 0.2f),
        Position = new Vector3(0f, FloorLine, 0f),
    });

    line.Scene = scene;

    var ballMaterial = game.CreateMaterial(new Color(80, 170, 255), metalness: 0f, glossiness: 0.6f);

    for (var i = 0; i < BallCount; i++)
    {
        var ball = game.CreatePrimitive(Primitive3D.Sphere(BallRadius), new BepuPhysicsOptions
        {
            EntityName = $"Ball {i + 1}",
            Material = ballMaterial,
            ShareModel = true,
            Position = StartPosition(i),
        });

        ball.Scene = scene;
        balls[i] = ball.Get<BodyComponent>();
    }

    TextOverlay.GetOrCreate(game).AddSection("Reset out of bounds", BuildOverlayLines);
}

void Update(Scene scene, GameTime time)
{
    if (game.Input.IsKeyPressed(Keys.R))
    {
        for (var i = 0; i < balls.Length; i++)
        {
            Reset(i);
        }
    }

    // Below the line a ball is falling for good, so it goes back to the start
    for (var i = 0; i < balls.Length; i++)
    {
        if (balls[i].Entity.Transform.Position.Y < FloorLine)
        {
            trips[i]++;
            Reset(i);
        }
    }
}

// The rotation defaults to identity, so a ball that tumbled off the plank comes back the way up it started
void Reset(int index) => balls[index].TeleportAtRest(StartPosition(index));

// Above the high end of the plank, the row centred across its width, each ball a little further along the slope
Vector3 StartPosition(int index) => new(-PlankLength / 2f + 0.8f + index * 0.5f, 2.2f, (index - (BallCount - 1) / 2f) * 0.7f);

IReadOnlyList<TextElement> BuildOverlayLines() =>
[
    new("R", "Put every ball back on the plank now", Color.Gold),
    new(""),
    new($"Trips so far: {string.Join(", ", trips)}", Color.White),
];