SceneManager
SceneManager organizes your game into discrete scenes (menus, levels, HUDs) with lifecycle management, stack-based navigation, and visual transitions. Place it inside Game and register scenes with Scene.
import { SceneManager } from "@carverjs/core/components";Quick Start #
import { Game, SceneManager, Scene, World, Actor } from "@carverjs/core/components";
import { useScene } from "@carverjs/core/hooks";
function MenuScene() {
const { go } = useScene();
return (
<World>
<Actor
type="primitive" shape="box" color="#facc15"
onClick={() => go("gameplay", { level: 1 }, { type: "fade", duration: 0.8 })}
/>
</World>
);
}
function GameplayScene({ data }: { data?: { level: number } }) {
return (
<World>
<Actor type="primitive" shape="sphere" color="#22c55e" />
</World>
);
}
function App() {
return (
<Game mode="2d">
<SceneManager initial="menu">
<Scene name="menu" component={MenuScene} />
<Scene name="gameplay" component={GameplayScene} />
</SceneManager>
</Game>
);
}Props Reference #
| Prop | Type | Default | Description |
|---|---|---|---|
initial | string | — | Required. Name of the scene to start on mount |
loadingFallback | ReactNode | null | Fallback shown while lazy-loaded scenes are loading (must be a Three.js element) |
persistent | ReactNode | — | Elements rendered above all scenes, surviving transitions (e.g., global audio wrapper) |
children | ReactNode | — | <Scene> components for registration |
Navigation #
Scene navigation is stack-based with four operations:
| Method | Behavior | Previous Scene |
|---|---|---|
go(name) | Clear the stack, navigate to scene | Destroyed (or sleeping if persistent) |
push(name) | Push scene on top of stack | Sleeping |
pop() | Remove top scene, return to previous | Destroyed (popped scene) |
replace(name) | Swap top of stack | Destroyed (or sleeping if persistent) |
Use the useScene hook or the imperative getSceneManager() API.
Example: Pause menu with push/pop #
function GameplayScene() {
const { push } = useScene();
const { isActionJustPressed } = useInput({ actions: { pause: ["Escape"] } });
useGameLoop(() => {
if (isActionJustPressed("pause")) push("pause");
});
return <World>{/* game content */}</World>;
}
function PauseScene() {
const { pop } = useScene();
return (
<group>
<Actor type="primitive" shape="plane" color="rgba(0,0,0,0.5)" />
<Actor type="primitive" shape="box" color="green" onClick={() => pop()} />
</group>
);
}Transitions #
Pass a TransitionConfig as the third argument to any navigation method:
go("gameplay", { level: 1 }, { type: "fade", duration: 1, color: "#000000" });| Type | Behavior |
|---|---|
"none" | Instant swap (default) |
"fade" | Fade to color at midpoint, swap scenes, fade from color |
"custom" | User-provided GLSL fragment shader with uProgress uniform (0-1) |
Default transition per scene #
Set a default transition on the <Scene> config. It applies when navigating to that scene without an explicit transition:
<Scene
name="gameplay"
component={GameplayScene}
transition={{ type: "fade", duration: 0.5 }}
/>Custom shader transition #
go("credits", undefined, {
type: "custom",
duration: 1.5,
fragmentShader: `
uniform float uProgress;
void main() {
float circle = length(gl_FragCoord.xy / vec2(800.0, 600.0) - 0.5);
float alpha = step(circle, uProgress);
gl_FragColor = vec4(0.0, 0.0, 0.0, alpha);
}
`,
});Scene Lifecycle #
| Status | Mounted | Visible | Updates | Description |
|---|---|---|---|---|
created | No | No | No | Registered, not started |
preloading | Yes | No | No | Lazy loading via Suspense |
running | Yes | Yes | Yes | Active scene |
paused | Yes | Yes | No | Visible but updates disabled |
sleeping | Yes | No | No | Hidden, preserves React tree + GPU resources |
shutting_down | Yes | Yes | No | Transitioning out |
destroyed | No | No | No | Unmounted |
Sleep vs Destroy #
Non-persistent scenes are unmounted (destroyed) when navigated away from
Persistent scenes (
persistent={true}) are hidden (sleeping) — their React tree, Three.js objects, and GPU resources are preserved for fast re-entry
<Scene name="hud" component={HUDScene} persistent />Shared Data #
Pass data between scenes without prop drilling:
// In scene A
const { setShared } = useSceneData();
setShared("playerName", "Hero");
setShared("score", 100);
// In scene B
const { getShared } = useSceneData();
const name = getShared<string>("playerName"); // "Hero"Lazy Loading #
Code-split scenes with the loader prop:
<Scene
name="gameplay"
loader={() => import("./scenes/GameplayScene")}
transition={{ type: "fade", duration: 0.5 }}
/>The scene's JavaScript bundle loads on-demand when navigated to. A loadingFallback renders during loading:
<SceneManager initial="menu" loadingFallback={<LoadingSpinner />}>
{/* scenes */}
</SceneManager>Error Isolation #
Each scene is wrapped in its own error boundary. If a scene crashes, other scenes and the transition system continue running. The crashed scene is marked as destroyed.
Architecture #
SceneManager
├── Scene (registration only, renders null)
├── SceneTransitionFlush — advances transition at priority -15
├── SceneRenderer[] — one per active/sleeping scene
│ ├── <group visible={...}>
│ ├── SceneErrorBoundary
│ ├── Suspense
│ ├── SceneContent — resolves component/loader
│ └── SceneUpdateRunner — wires onUpdate to useGameLoop
├── TransitionOverlay — fullscreen fade quad (renderOrder 9999)
└── persistent — user-provided persistent elementsType Definitions #
See Types for SceneManagerProps, SceneConfig, SceneStatus, TransitionConfig, TransitionType, SceneStoreState, SceneEntry, and TransitionState.