Selection Boxes (SelectionBoxManager)
Selection Boxes (SelectionBoxManager)
Part of the 7DTD Modding Knowledgebase. Drawing colored world-space boxes from mod code using the game's own selection-box system (the one the prefab editor, sleeper volumes and trader teleport volumes use). API reflection-verified against 3.0.x Assembly-CSharp (Elevator mod, 2026-07). In-game rendering confirmed 2026-07: boxes do render on the default camera in normal gameplay, but the solid translucent faces are sized for editor use — up close (e.g. standing at a player-scale box) they flood the view with the face color and hide everything inside/behind the box. For player-facing previews prefer a custom dashed wireframe (below); the Elevator mod dropped SelectionBoxManager entirely for it.
API surface
SelectionBoxManager mgr = SelectionBoxManager.Instance; // static field; scene object — null-check it
// Category = a named group of boxes with shared colors
SelectionCategory cat = mgr.CreateCategory(
"MyCategory",
colSelected, colUnselected, colFaceSelected, // UnityEngine.Color, alpha ~0.2-0.35 for faces
false, // bCollider — false for display-only boxes
"", // collider tag (unused when bCollider false)
0); // layer — 0 renders on the default camera
SelectionBox box = cat.AddBox("myBox", blockPos, sizeInBlocks, false, false); // Vector3i world block coords
box.SetPositionAndSize(blockPos, size); // move/resize an existing box
box.SetVisible(true);
box.ShowThroughWalls(true); // x-ray outline
box.SetFrameColor(Color.red); // edge lines, separate from face color
box.SetCaption("label text"); // floating TextMesh above the box
box.SetSizeVisibility(true); // per-axis dimension labels like the prefab editor
cat.TryGetBox("myBox", out box); // safe lookup — use before AddBox to avoid duplicates
cat.RemoveBox("myBox");
cat.SetVisible(true);
Gotchas
SelectionBoxManager.Instanceis a MonoBehaviour in the game scene: it is destroyed and recreated on world reload. Cache neither categories nor boxes across worlds — compareInstanceto the one you created your category on and rebuild when it changes.mgr.GetCategory(name)is a plain dictionary lookup — prefer tracking your ownSelectionCategoryreference rather than probing (may throw on missing key).- Positions are world block coordinates (
Vector3i); the system handles the floating origin internally (SelectionBoxre-anchors on origin shift). SelectionCategoryface colors come from the category's active/inactive colors; per-boxSetAllFacesColorandSetFrameColoroverride them.SetCaption/SetSizeVisibilitytext is a world-spaceTextMeshusing the GUI text shader: huge (sized for the prefab editor's zoomed-out camera), drawn through all geometry (ZTest Always), and mirrored when viewed from behind (Cull Off). Up close it reads as unusable green garbage. There is no API to shrink it — if you need readable in-world labels, skip captions and render your own text (below).
Rolling your own: dashed wireframe + fitted per-face labels
When SelectionBox's solid faces / giant captions don't fit (verified in-game,
Elevator mod 2026-07, src/ElevatorBoundsRenderer.cs):
- Dashed box outline: one
MeshwithMeshTopology.Lines(pairs of verts per dash), material fromHidden/Internal-Coloredwith_ZTest Always,_ZWrite 0, SrcAlpha/OneMinusSrcAlpha,renderQueue 4000(see [[Runtime Procedural VFX]] for the material recipe). No GL/OnRenderObject needed; rebuild only when the box changes. - Many boxes: batch them all into a single mesh (Elevator's
ElevatorFloorBoxRendererdraws every floor box in one rebuild) and setmesh.indexFormat = IndexFormat.UInt32— lots of tall dashed boxes can pass the 64k default vertex cap. Tint via per-vertex colors so one material serves differently-colored renderers. - Floating origin: write mesh verts in absolute world coords and set
transform.position = -Origin.positioneveryLateUpdate. - Built-in font on Unity 2022 (7DTD 3.0):
Resources.GetBuiltinResource <Font>("LegacyRuntime.ttf")—"Arial.ttf"no longer exists (throws). Try both for forward-compat.TextMeshlives inUnityEngine.TextRenderingModule(add the csproj reference). - Fit text inside a face: set
TextMesh.fontSizehigh (64) for atlas crispness, then measurerenderer.boundsatlocalScale = 1and scale bymin(faceW*margin/w, faceH*margin/h, absoluteCap/h). Works because the faces are axis-aligned: the world AABB cleanly splits width (x or z) vs height (y). - No mirrored text from behind: the font material draws through walls and
is double-sided, so per frame enable only labels whose face normal points at
the camera:
Dot(normal, camPos - labelPos) > 0withEntityPlayerLocal.cameraTransform(reflection-verified field). Standing inside the box hides all labels — acceptable, beats mirrored garble.
Billboarded labels with an outline (readable over bright boxes)
For labels floating inside/over colored boxes (Elevator mod 2026-07,
src/ElevatorFloorLabels.cs), face-fitted text isn't enough — white text on a
bright green face needs an outline, and shaft-style views (looking straight
down) need billboarding instead of per-face culling:
- Screen-aligned billboard: set the label root's
rotation = cameraTransform.rotationeveryLateUpdate. Readable from any angle, never mirrored, never degenerate (unlikeLookRotationwith world-up when looking straight down). - Outline:
TextMeshhas no outline support. Fake it with 8 black copies of the TextMesh offset in a ring (4 cardinal at ±d, 4 diagonal at ±0.7d) behind a white main copy. d ≈ 5% of the line height reads well (line height atfontSize 64/characterSize 0.1is ~0.64 local units → d = 0.03). - Draw order: clone
font.materialtwice and set explicitrenderQueues (e.g. outline 4001, text 4002 — above SelectionBox faces at ~3000 and any custom lines at 4000). Same-queue transparent sorting by distance is a tie at identical positions; explicit queues make outline-under-text deterministic and keep labels on top of the box faces. - Cloned font materials go stale: dynamic fonts regrow their glyph atlas and
may swap in a new texture; clones keep pointing at the old one. Subscribe to
Font.textureRebuiltand re-copyfont.material.mainTextureinto the clones. - Sizing/fitting and the floating-origin trick are the same as the per-face
recipe above (measure
renderer.boundsat scale 1, then uniform-scale tomin(fitWidth*margin/w, maxHeight/h)).