This document tells an automated agent (LLM) everything it needs to add a new example to the Sandbox project, and to modify an existing one.
It is the authoritative recipe for this project. Read it fully before creating files.
PhysicsCore2D rules still apply. This project is PhysicsCore2D only (
Unity.U2D.Physics). Never use the legacyPhysics2Dcomponent system (Rigidbody2D,Collider2D, …). Verify every PhysicsCore2D API against the bundledunity-physicscore2d-*skills — never guess a signature, never use an[Obsolete]member. See the repo-rootCLAUDE.md.
Architecture. Every example derives from
SandboxExampleBehaviourand is tagged with[ExampleScene], with the repeated menu chrome factored into shared infrastructure. All examples use this pattern; the old inlineMonoBehaviourboilerplate is fully retired. Author all new and modified examples with the new pattern below.
An example is a single .cs file inside Assets/Examples/. There are no subfolders and no
scene files. An example's category comes solely from its [ExampleScene("<Category>", …)]
attribute:
Assets/Examples/<Name>.cs ← a SandboxExampleBehaviour subclass tagged [ExampleScene]
Assets/Examples/<Name>.cs.meta ← asset GUID (Unity auto-generates this on import)
If the example needs serialised assets (sprites, materials, etc.), create a companion
<Name>Data : ExampleSceneData ScriptableObject class and a matching .asset instance in
Assets/Examples/Assets/ — see §5.
There is no per-example .uxml. Option controls are built in code (see §6); the
UIDocument renders the single shared ExampleChrome.uxml.
Examples are loaded at runtime by instantiating a new GameObject and calling
AddComponent<T>() — no scene loading, no build settings management.
| Component / type | Role |
|---|---|
SandboxManager |
Global controls (pause/step/reset/colors/debug), world draw options, shared deterministic Random, per-example reset hook. |
SceneManifest |
ScriptableObject registry of all examples. Switches examples by destroying the old GameObject and adding the new component. |
CameraManipulator |
Drives the runtime camera (pan/zoom/drag/explode). Each example sets initial framing via overrides. |
SandboxExampleBehaviour |
The base class every example derives from. Resolves the infra, finds the chrome regions, titles/describes the panel, wires Reset, exposes the AddSlider/AddToggle/AddEnum/… control-builder helpers, and clears overrides — so the example file is just its physics + options. |
Rendering: examples do not need a SpriteRenderer/GameObject per body. The
PhysicsCore2D debug renderer draws all shapes/joints automatically. Use a Unity
Transform (via body.transformObject) only when you want a sprite/visual to track a body.
Step 1 — Create Assets/Examples/<Name>.cs from the §7 template. Set the class name,
[ExampleScene] category and description, camera framing, options, and physics content.
Unity auto-generates the .meta on import.
Step 2 — Register. Run Tools > 2D > Physics > Rebuild Sandbox Registry.
That's it. The tool scans every [ExampleScene]-tagged type that derives from
SandboxExampleBehaviour, writes the assembly-qualified TypeName into
Assets/Framework/SceneManifest.asset, and removes entries for types that no longer exist.
No GUIDs to generate, no scene files to author, no build settings to edit.
Step 3 — (Optional) Iterate. Set StartScene: on the SandboxManager in Sandbox.unity
to your example's display name to boot straight into it (revert before committing if needed).
ExampleRegistryBuilder (Assets/Editor/ExampleRegistryBuilder.cs) runs on:
- the explicit
Tools > 2D > Physics > Rebuild Sandbox Registrymenu item, and - any script import inside
Assets/Examples/that introduces a new[ExampleScene]type.
It:
- Calls
TypeCache.GetTypesWithAttribute<ExampleSceneAttribute>()— no file scanning. - Validates that every found type derives from
SandboxExampleBehaviour(logs a warning otherwise). - Upserts
SceneItementries byTypeName(assembly-qualified class name). - Auto-discovers companion
ExampleSceneDataassets by searching for a ScriptableObject whose asset name matches{TypeName}Data(e.g.SpriteDestructionData.assetforSpriteDestruction). - Ensures only
Assets/Sandbox.unityis inEditorBuildSettings.scenes; removes any staleAssets/Scenes/entries left over from older project layouts.
SceneManifest is a ScriptableObject (Assets/Framework/SceneManifest.asset). SandboxManager
holds a direct serialised reference to it.
When switching to example name:
1. SetActive(false) on the current example GameObject → OnDisable fires immediately.
2. Destroy the current example GameObject.
3. Invoke the reset callback (clears physics, debug draw, RNG seed).
4. m_CurrentExampleGO = new GameObject(item.Name) ← SetActive(false) first
example = m_CurrentExampleGO.AddComponent(Type.GetType(item.TypeName))
example.ExampleData = item.Data ← inject before OnEnable
LoadedSceneName = item.Name
LoadedSceneDescription = item.Description
m_CurrentExampleGO.SetActive(true) ← OnEnable fires here with all state set
The switch is synchronous — no coroutines, no async scene loading.
Most examples are fully procedural. If an example needs a serialised asset (a Sprite, a
Material, a PhysicsMaterial2D, etc.), use the companion data pattern:
Step A — Define a data class (in Assets/Examples/ or inline):
using UnityEngine;
[CreateAssetMenu(fileName = "MyExampleData", menuName = "2D Physics/Example Data/My Example")]
public class MyExampleData : ExampleSceneData
{
public Sprite MySprite;
public Material MyMaterial;
}Step B — Read from ExampleData in OnExampleEnable():
protected override void OnExampleEnable()
{
var data = (MyExampleData)ExampleData;
m_Sprite = data.MySprite;
m_Material = data.MyMaterial;
}Step C — Create the .asset instance inside Unity: right-click in Assets/Examples/Assets/,
choose Create > 2D Physics > Example Data > My Example, name it MyExampleData, then assign
the sprite/material references in the Inspector.
Step D — Register. Run Tools > 2D > Physics > Rebuild Sandbox Registry. The tool finds
MyExampleData.asset by convention (asset name matches {TypeName}Data) and wires it into the
SceneManifest entry automatically.
All example assets live in the single flat Assets/Examples/Assets/ folder — no per-example
subfolders. This keeps the folder hierarchy flat and makes asset discovery trivial.
Derive from SandboxExampleBehaviour and implement the hooks. The base class resolves the
infra, sets SceneOptionsUI, applies your camera framing, registers Reset, finds the chrome
regions (title/description/options-content), and clears overrides on disable — so your file is
just physics + options.
[ExampleScene("Shapes", "Demonstrating X.")]
public sealed class MyExample : SandboxExampleBehaviour
{
protected override float CameraSize => 12f; // optional framing overrides
protected override Vector2 CameraPosition => Vector2.zero;
protected override void OnExampleEnable() { /* state, world overrides, event subscribe, native alloc */ }
protected override void OnExampleDisable() { /* event unsubscribe, native dispose, world restore */ }
protected override void SetupOptions() { /* build controls with AddSlider/AddToggle/… */ }
protected override void SetupScene() { var world = World; /* create physics */ }
}Base-class call order on enable: OnExampleEnable() → build chrome + SetupOptions()
→ RebuildScene() (which calls ResetSceneState() then your SetupScene()).
Key rules:
SetupScene()is called repeatedly — first load, every Reset (R), and whenever you callRebuildScene()from an option callback. The world is already reset when it runs, so begin directly with physics. To rebuild after a structural option change, callRebuildScene()(notSetupScene()directly — that would skip the world reset).- The
Worldaccessor is for reads and method calls (World.gravity,World.CreateBody(...)). To set a world property, copy to a local first —PhysicsWorldis a handle struct and C# forbids assigning a property on the by-value accessor result (compiler error CS1612):var world = World; world.gravity = m_OldGravity * scale; // ✅ (World.gravity = … ❌ CS1612)
- Determinism: use
Random(the base'srefto the sharedUnity.Mathematics.Random, re-seeded byResetSceneState()), notUnityEngine.Random. - Shape colours: set
shapeDef.surfaceMaterial.customColor = ShapeColor;so the global "Colors" toggle works. - Overrides auto-reset. If you call
SandboxManager.SetOverrideDrawOptions(...)/SetOverrideColorShapeState(...)inOnExampleEnable, you do not need to reset them — the baseOnDisabledoes it. Only undo things the base doesn't know about (e.g.CameraManipulator.DisableManipulators, world contact params,PhysicsEventssubs). - Do not create/destroy bodies during the simulation step (WORM rule).
- You can still add
Update()/FixedUpdate()and implement callback interfaces (PhysicsCallbacks.IContactCallback, etc.) directly on your subclass.
| Member | Purpose |
|---|---|
World |
PhysicsWorld.defaultWorld (reads/methods; local-copy to set properties). |
ref Random Random |
Shared deterministic RNG. |
Color ShapeColor |
Per-shape colour honouring the global Colors toggle. |
ExampleData |
Optional ExampleSceneData asset injected before OnEnable (null for most examples). |
SandboxManager / SceneManifest / CameraManipulator / UIDocument |
The resolved infra. |
RebuildScene() |
ResetSceneState() + SetupScene(); call from option callbacks. |
CameraSize / CameraPosition (override) |
Initial framing. |
OnExampleEnable / OnExampleDisable / SetupOptions / SetupScene (override) |
Your hooks. |
SetOverrideDrawOptions(...) / SetOverrideColorShapeState(bool) (auto-reset by the base),
ShowFPS() / HideFPS(), WorldPaused, ControlsMenu[i] (touch buttons), WorldSleeping.
There is no per-example uxml. Override SetupOptions() and add controls with the base-class
helpers; each constructs the control, sets the shared conventions (focusable = false,
input-field shown for sliders), registers a value-changed callback, adds it to the panel
(OptionsContent, which is PickingMode.Ignore so empty space passes camera input through), and
returns it.
protected Slider AddSlider(string label, float value, float low, float high, Action<float> onChanged, bool rebuild = false);
protected SliderInt AddSliderInt(string label, int value, int low, int high, Action<int> onChanged, bool rebuild = false);
protected Toggle AddToggle(string label, bool value, Action<bool> onChanged, bool rebuild = false);
protected EnumField AddEnum<TEnum>(string label, TEnum value, Action<TEnum> onChanged, bool rebuild = false) where TEnum : Enum;
protected T AddElement<T>(T element) where T : VisualElement; // display/custom widgets; returns itvalueis the control's initial value — pass your backing field (m_Count), so the field is the single source of truth for defaults.- The
onChangedlambda receives the new value; do your field assignment / live update there. Passrebuild: truewhen the change should rebuild the scene (the helper callsRebuildScene()after your lambda) — don't callRebuildScene()yourself in that case. AddEnuminfersTEnumfrom the value, so a private nested enum (or a PhysicsCore2D enum likePhysicsBody.BodyType) works directly.- For widgets the typed helpers don't cover (
ProgressBar,MinMaxSlider, read-onlyFloatField,Label), construct them and pass toAddElement(...); cache the return for later updates. Add thehash-labelUSS class for a bordered display box (seeSandboxStyleOverrides.uss).
Controls appear in the order you add them.
using UnityEngine;
using Unity.U2D.Physics;
using UnityEngine.UIElements;
[ExampleScene("<Category>", "<One-sentence description shown in the options panel.>")]
public sealed class <Name> : SandboxExampleBehaviour
{
private int m_Count = 10;
protected override float CameraSize => 12f;
protected override Vector2 CameraPosition => Vector2.zero;
protected override void SetupOptions()
{
AddSliderInt("Count", m_Count, 1, 100, v => m_Count = v, rebuild: true);
}
protected override void SetupScene()
{
var world = World;
// Ground.
var groundBody = world.CreateBody();
groundBody.CreateShape(
new SegmentGeometry { point1 = new Vector2(-100f, 0f), point2 = new Vector2(100f, 0f) },
PhysicsShapeDefinition.defaultDefinition);
// Content.
var bodyDef = PhysicsBodyDefinition.defaultDefinition;
bodyDef.type = PhysicsBody.BodyType.Dynamic;
var shapeDef = PhysicsShapeDefinition.defaultDefinition;
for (var i = 0; i < m_Count; ++i)
{
bodyDef.position = new Vector2(0f, 1f + i * 1.2f);
var body = world.CreateBody(bodyDef);
shapeDef.surfaceMaterial.customColor = ShapeColor;
body.CreateShape(new CircleGeometry { center = Vector2.zero, radius = 0.5f }, shapeDef);
}
}
}Add OnExampleEnable/OnExampleDisable overrides only if you need per-example setup/teardown
(world-property save/restore, PhysicsEvents subscribe/unsubscribe, NativeArray/NativeList
alloc+dispose, DisableManipulators, ControlsMenu buttons, SetOverride*).
No scene file is required. No GUIDs to generate. No build settings to edit.
- Touch only
Assets/Examples/<Name>.cs— both behaviour and controls live there. - Add a control: add an
AddSlider/AddToggle/AddEnum/… call (orAddElement(...)) inSetupOptions(). - Change a default value: edit the
m_Fieldinitialiser in.cs(it's passed straight to the helper as the control's initial value). Edit the helper args for ranges/limits and label. - Change the simulation: edit
SetupScene(). It begins after a world reset; rebuild from option callbacks viaRebuildScene()(orrebuild: trueon the helper). - Set a world property: copy
Worldto a local first (var world = World; world.gravity = …) — see the CS1612 note in §6. Cache the old value inOnExampleEnableand restore it inOnExampleDisable; override draw/colour state is auto-reset by the base. - Every example already uses the
SandboxExampleBehaviourpattern with code-built options. In the unlikely event you meet a plainMonoBehaviourwith aFindAnyObjectByTypepreamble or a per-example*Menu.uxml(e.g. restored from old history), migrate it: derive from the base, delete the infra fields +OnEnablepreamble, move setup intoOnExampleEnable, rebuild the options inSetupOptions()with theAddXhelpers, makeSetupScenestart atvar world = World;, move teardown intoOnExampleDisable(dropResetOverride*— auto), add[ExampleScene], and run the registry tool. - After editing PhysicsCore2D calls, re-verify any unfamiliar member against the
unity-physicscore2d-*skills.
-
Assets/Examples/<Name>.csexists (Unity will auto-generate.metaon import). - Class derives from
SandboxExampleBehaviour, tagged[ExampleScene("<Category>", "…")], implementsSetupScene(), overridesSetupOptions/camera/OnExample*as needed. - No
World.<prop> = …(use a local); usesRandom/ShapeColor; rebuilds viaRebuildScene(). -
SetupOptions()builds controls with theAddXhelpers (no per-example uxml). - If asset dependencies are needed: companion
<Name>Data : ExampleSceneDataclass defined,<Name>Data.assetcreated inAssets/Examples/Assets/, assets assigned in Inspector. - Ran
Tools > 2D > Physics > Rebuild Sandbox Registry— example appears in the menu. - No legacy
Physics2Dtypes; no[Obsolete]PhysicsCore2D members.