Layer Selection
Selecting things through collision layers. A click is a raycast whose mask says which layers it may hit, and the keys let it pick units alone, units and crates, or anything at all; a dragged marquee tests every candidate on the chosen layers through the camera's IsInScreenRectangle. Rings mark the selection, Shift adds to it and a click on nothing clears it.
The Program.cs file shows how to:
- A raycast's CollisionMask as the filter for what a click can select
- Marquee selection with IsInScreenRectangle over the candidates on the chosen layers
- A click and a drag told apart by the distance the mouse moved
- A screen-space rectangle drawn through a ShapeBatch after the post effects
- Using helpers: SceneSetup3D, UseBepu, UseSkybox, CreatePrimitive, CreateMaterial, GetSimulation, ScreenToWorldRay, IsInScreenRectangle, ShapeBatch, TextOverlay

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.Shapes;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Input;
using Stride.Rendering;
// Selecting things through layers. A click is a raycast, and a raycast takes a mask: the layers it may
// hit. Units stand on one layer and crates on another, and the keys choose whether a click can pick
// units alone, units and crates, or anything at all, the ground included. The same mask decides what a
// marquee picks up: drag a rectangle and every candidate on a chosen layer whose position projects
// inside it is selected, through the camera's IsInScreenRectangle.
//
// A ring under each selected thing; Shift adds to the selection, a click on nothing clears it.
// E05_3D_CollisionLayer is the example for the layers and the matrix themselves.
const CollisionLayer UnitLayer = CollisionLayer.Layer1;
const CollisionLayer CrateLayer = CollisionLayer.Layer2;
const float DragThreshold = 0.004f;
(string Name, CollisionMask Mask)[] maskChoices =
[
("units only", CollisionMask.Layer1),
("units and crates", CollisionMask.Layer1 | CollisionMask.Layer2),
("everything, the ground too", CollisionMask.Everything),
];
var maskChoice = 0;
var candidates = new List<(Entity Entity, CollisionMask Layer, float Radius)>();
var selected = new HashSet<Entity>();
var selectedList = new List<Entity>();
var lastHit = "nothing yet";
Vector2? dragStart = null;
BepuSimulation simulation = null!;
CameraComponent camera = null!;
ShapeBatch rings = null!;
ShapeBatch screen = null!;
using var game = new Game();
var sceneSetup = new SceneSetup3D
{
Camera = CameraSetup.Perspective(position: new Vector3(0f, 12f, 16f), rotation: new Vector3(0f, -36f, 0f)).WithController(),
WindowTitle = "Layer selection - Stride Community Toolkit",
Resizable = true,
DpiAware = true,
}.UseBepu().UseSkybox();
game.Run(sceneSetup, start: Start, update: Update);
void Start(Scene scene)
{
camera = game.GetCameraEntity().Get<CameraComponent>();
simulation = game.GetCameraEntity().GetSimulation();
rings = game.AddShapeBatch(depthTest: true);
screen = game.AddShapeBatch(afterPostEffects: true);
var unitMaterial = game.CreateMaterial(new Color(90, 190, 255), metalness: 0f, glossiness: 0.5f);
var crateMaterial = game.CreateMaterial(new Color(190, 140, 80), metalness: 0f, glossiness: 0.3f);
var random = new Random(8);
for (var i = 0; i < 12; i++)
{
Add(scene, $"Unit {i + 1}", Primitive3D.Capsule(0.3f, 0.5f), unitMaterial, UnitLayer, CollisionMask.Layer1, 0.5f,
new Vector3(random.NextSingle() * 12f - 6f, 0.55f, random.NextSingle() * 10f - 5f));
}
for (var i = 0; i < 6; i++)
{
Add(scene, $"Crate {i + 1}", Primitive3D.Cube(1f), crateMaterial, CrateLayer, CollisionMask.Layer2, 0.75f,
new Vector3(random.NextSingle() * 12f - 6f, 0.5f, random.NextSingle() * 10f - 5f));
}
TextOverlay.GetOrCreate(game).AddSection("Layer selection", BuildOverlayLines);
}
void Update(Scene scene, GameTime time)
{
var input = game.Input;
for (var i = 0; i < maskChoices.Length; i++)
{
if (input.IsKeyPressed(Keys.D1 + i) || input.IsKeyPressed(Keys.NumPad1 + i)) maskChoice = i;
}
// Press: remember where. Release: a click where it started, or a marquee from there to here
if (input.IsMouseButtonPressed(MouseButton.Left)) dragStart = input.MousePosition;
if (input.IsMouseButtonReleased(MouseButton.Left) && dragStart is { } start)
{
var additive = input.IsKeyDown(Keys.LeftShift) || input.IsKeyDown(Keys.RightShift);
if (Vector2.Distance(start, input.MousePosition) < DragThreshold) Click(input.MousePosition, additive);
else Marquee(start, input.MousePosition, additive);
dragStart = null;
}
DrawSelection();
if (dragStart is { } from && input.IsMouseButtonDown(MouseButton.Left)) DrawMarquee(from, input.MousePosition);
}
/// <summary>A static body on a layer, remembered as a candidate for the marquee with the layer's mask bit.</summary>
void Add(Scene scene, string name, Primitive3D shape, Material material, CollisionLayer layer, CollisionMask bit, float radius, Vector3 position)
{
var entity = game.CreatePrimitive(shape, new BepuPhysicsOptions
{
EntityName = name,
Body = BodyMode.Static,
Material = material,
ShareModel = true,
Position = position,
});
entity.Get<StaticComponent>().CollisionLayer = layer;
entity.Scene = scene;
candidates.Add((entity, bit, radius));
}
/// <summary>A click is one raycast with the chosen mask: it stops at the first thing on an allowed layer and ignores the rest.</summary>
void Click(Vector2 screenPosition, bool additive)
{
var ray = camera.ScreenToWorldRay(screenPosition);
var mask = maskChoices[maskChoice].Mask;
if (!additive) ClearSelection();
if (simulation.RayCast(ray.Position, ray.Direction, 100f, out var hit, mask) && hit.Collidable?.Entity is { } entity)
{
lastHit = entity.Name;
// The ground is on an allowed layer when the mask says everything, so a click on it is a hit that selects nothing
if (IsCandidate(entity)) Select(entity);
}
else
{
lastHit = "nothing on the chosen layers";
}
}
/// <summary>A marquee tests every candidate on an allowed layer: its position projected to the screen, inside the rectangle or not.</summary>
void Marquee(Vector2 cornerA, Vector2 cornerB, bool additive)
{
var mask = maskChoices[maskChoice].Mask;
var count = 0;
if (!additive) ClearSelection();
foreach (var (entity, bit, _) in candidates)
{
if ((mask & bit) == 0) continue;
if (camera.IsInScreenRectangle(entity.Transform.Position, cornerA, cornerB))
{
Select(entity);
count++;
}
}
lastHit = $"{count} in the rectangle";
}
bool IsCandidate(Entity entity)
{
foreach (var (candidate, _, _) in candidates)
{
if (candidate == entity) return true;
}
return false;
}
void Select(Entity entity)
{
if (selected.Add(entity)) selectedList.Add(entity);
}
void ClearSelection()
{
selected.Clear();
selectedList.Clear();
}
void DrawSelection()
{
rings.StrokeWidth = 2f;
foreach (var (entity, _, radius) in candidates)
{
if (!selected.Contains(entity)) continue;
rings.DrawRing(new Vector3(entity.Transform.Position.X, 0.02f, entity.Transform.Position.Z), Vector3.UnitY, radius, Color.Gold);
}
}
// The rectangle in screen pixels, from the two normalized corners
void DrawMarquee(Vector2 from, Vector2 to)
{
var size = screen.ScreenSize;
var a = from * size;
var b = to * size;
var centre = (a + b) * 0.5f;
var extent = new Vector2(MathF.Abs(b.X - a.X), MathF.Abs(b.Y - a.Y));
screen.Screen = true;
screen.BorderWidth = 1.5f;
screen.Fill.Set(new Color(255, 215, 0), 0.12f);
screen.DrawRectangle(new Vector3(centre, 0f), Vector3.UnitX, Vector3.UnitY, extent, Color.Gold);
screen.Fill.Set(null, 1f);
screen.Screen = false;
}
IReadOnlyList<TextElement> BuildOverlayLines() =>
[
new("Left click", "Select what the ray hits on the chosen layers", Color.Gold),
new("Left drag", "Marquee: select everything inside on the chosen layers", Color.Gold),
new("Shift", "Hold to add to the selection", Color.Gold),
new(["1", "2", "3"], $"The ray and the marquee may pick: {maskChoices[maskChoice].Name}", Color.Gold),
new(""),
new($"Last click hit: {lastHit}", Color.White),
new($"Selected: {selectedList.Count}", Color.White),
];