Scenes
Scenes represent a “screen” (or state) in your game: main gameplay, main menu, pause overlay, cutscene, etc.
In Sliver, a Scene is responsible for:
- Holding and updating a list of
GameObjects - Rendering its background + objects + optional overlay
- Dispatching input/events to its objects (with propagation control)
- Running scene physics: body integration, overlap callbacks, and collision solving
- Supporting camera-like movement via a scene
offset(used by transitions too)
Scenes receive a shared GameContext from the engine (via SceneManager), so game objects can access input, audio, scene switching, and the message bus.
Creating scenes and registering them
Create scenes with:
import { Scene, SceneManager } from "sliver-engine";
const main = new Scene("main", "#0b0a18"); // name + optional background color
const pause = new Scene("pause");
const scenes = new SceneManager(
{ main, pause }, // registry (keys are the ids you switch by)
main // initial current scene (optional)
);
Notes:
- The registry key (
"main","pause") is what you’ll use withsetCurrentScene(...)/transitionToScene(...). - The
Sceneconstructor’snameis currently used mainly for logging (setup()prints it). It’s easiest if you keep it the same as the registry key.
Scene lifecycle and context injection
When a scene becomes active, SceneManager will:
- call
scene.setContext(gameContext) - call
scene.setup()once (the first time the scene is activated) - call
scene.onEnter()every time the scene becomes active
This happens when you:
- set a scene as current
- push a scene on the stack
- transition to a scene
When a scene leaves the active stack, scene.onExit() is called.
If a scene becomes active before the game binds a context (for example, when you pass an initial scene to SceneManager), setup() and onEnter() are deferred until the context is available.
If you subclass Scene, use the hooks like this:
import { Scene } from "sliver-engine";
class MainScene extends Scene {
override setup(): void {
super.setup();
// addGameObject(...), setGravity(...), etc.
}
override onEnter(): void {
// reset timers, start music, subscribe to messages, etc.
}
override onExit(): void {
// cleanup subscriptions, pause music, etc.
}
}
Adding and removing game objects
Add one or many objects:
scene.addGameObject(player);
scene.addGameObject([hud, enemy1, enemy2]);
What addGameObject does for you:
- sets
go.scene = scene - injects the scene’s
GameContextinto the object (and its children) sothis.getContext()starts working - triggers
go.onAddedToScene(scene, context)once the context is available
Remove objects either by:
- calling
gameObject.destroy()(recommended; also detaches children and removes from the scene), or scene.removeGameObject(gameObject)if you need manual removal
On removal, go.onRemovedFromScene(scene) runs after onAddedToScene.
Tick: updates, gravity, collisions
Each tick, Scene.tick():
- calls
tick()on everyGameObjectin the scene - steps the scene physics world with the scene gravity and the current
dt
For a deeper dive (hitboxes, restitution/friction/mass, triggers, and collision hooks), see Physics.
Gravity
A scene has a gravity vector:
import { Vector } from "sliver-engine";
scene.setGravity(new Vector(0, 1200));
Gravity is applied by the scene physics world when:
phisics.affectedByGravity === truephisics.immovable === false- the object isn’t being dragged (
beingGrabbed === false)
Gravity is integrated with dt, so treat it as acceleration in world units per second squared. Values in the hundreds or low thousands are a more realistic starting point than tiny fractional values.
Collisions
After objects tick, the scene physics world resolves collisions between hitboxes:
- Only active objects with at least one hitbox participate.
- Each object contributes one physics body, and each of its hitboxes becomes a shape on that body.
- Objects with
phisics.immovable === trueare mapped to static bodies. beforeColision(other)runs on both objects the first time a pair is checked; if either returnsfalse, that pair is ignored for the rest of the tick.onColision(other, penetration)is called once per pair per tick when overlap is detected.- Solid hitboxes enter the solver; non-solid hitboxes still notify overlap hooks but skip physical response.
If you want an object to be physical, you typically:
- give it one or more hitboxes
- set
immovable: false - optionally tweak
restitution,friction,mass, and gravity behavior
See Physics for collision/response details, and Game objects for the hitbox and gameplay patterns.
Render: background, objects, overlay
Each frame, the game renders every active scene (see Game loop).
Scene.render(...) draws, in order:
- background fill (if the scene has a background color)
- all game objects (
obj.render(canvas, scene)) - an optional full-screen overlay color (used by some transitions)
Scenes also have an opacity that multiplies the canvas alpha during render, which is how fade transitions work.
Events and propagation
When a scene receives an input event, Scene.handleEvent(event) forwards it to game objects in reverse order (last added gets first chance to handle it).
If any handler sets event.stopPropagation = true, the scene stops delivering the event to remaining objects.
This works together with the game-level dispatch order (top-most active scene first) to make overlays/menus easy to implement.
Camera offset (scene offset)
Scenes have an offset vector that acts like a simple “camera” shift:
import { Vector } from "sliver-engine";
scene.setOffset(new Vector(-100, 0)); // pan camera right by 100px
Why this works:
GameObject.getPosition()includes the scene offset, so most objects automatically render with the camera shift.- Some input helpers/decorators convert mouse coordinates to “scene space” by subtracting the offset, so hit tests line up while the camera moves.
Slide transitions use a separate internal render offset, so camera movement does not drag the scene background color across the canvas.
Scene stack and scene switching
SceneManager maintains:
- a
currentScene - a list of
activeScenes(a stack)
currentScene is the “main” current scene. Pushing an overlay scene does not necessarily replace the current scene; it just adds a scene on top of the active stack.
Active scenes:
- are all ticked each tick
- are all rendered each frame (in order)
- receive input with the top-most scene first (reverse order), so overlays can consume events
You’ll usually switch scenes through the GameContext helpers:
const ctx = game.getContext();
ctx.setCurrentScene("main"); // replace active stack with [main]
ctx.pushScene("pause"); // overlay pause on top
ctx.popScene(); // remove top-most active scene
Transitions (fade / slide / flash)
Transitions are handled by SceneManager.transitionToScene(...) (also exposed on GameContext).
Built-in transitions are exported from sliver-engine:
import {
fadeTransition,
slideReplace,
slidePush,
slidePop,
colorFlash,
} from "sliver-engine";
await game.getContext().transitionToScene("main", fadeTransition(450), "replace");
await game.getContext().transitionToScene("pause", slidePush("down"), "push");
await game.getContext().transitionToScene("main", slidePop("up"), "replace");
await game.getContext().transitionToScene("main", colorFlash("white", 750), "replace");
Transition details worth knowing:
- Only one transition can run at a time; starting a new one while another is active rejects with an error.
- A transition can control
opacity,offset, andoverlayon the involved scenes. incomingOnTopcontrols whether the incoming scene renders above or below the outgoing scene during the animation (used by pop-like transitions).
Custom transitions
If you want something custom, build one with createSceneTransition:
import { createSceneTransition } from "sliver-engine";
const zoomFade = createSceneTransition(
(t, { from, to }) => {
from?.setOpacity(1 - t);
to.setOpacity(t);
// you can also animate offsets/overlays here
},
{ duration: 600 }
);
Interactive scene transitions
This playground has three scenes:
Scene A: opaque, solid background, buttons to replace withScene Bor pushScene CScene B: opaque, solid background, buttons to replace withScene Aor pushScene CScene C: semitransparent background, button to pop scene
Edit transitions.ts and comment/uncomment transition presets to see how each transition feels.
import { colorFlash, fadeTransition, slidePop, slidePush, slideReplace, type SceneTransition, } from "sliver-engine"; const REPLACE_DURATION = 450; const PUSH_DURATION = 450; const POP_DURATION = 450; // Transition used by "Scene A -> replace Scene B" export const TRANSITION_A_TO_B: SceneTransition = slideReplace( "left", undefined, REPLACE_DURATION, ); // export const TRANSITION_A_TO_B: SceneTransition = fadeTransition(REPLACE_DURATION); // export const TRANSITION_A_TO_B: SceneTransition = colorFlash("#ffffff", 700); // Transition used by "Scene B -> replace Scene A" export const TRANSITION_B_TO_A: SceneTransition = slideReplace( "right", undefined, REPLACE_DURATION, ); // export const TRANSITION_B_TO_A: SceneTransition = fadeTransition(REPLACE_DURATION); // export const TRANSITION_B_TO_A: SceneTransition = colorFlash("#ffffff", 700); // Transition used by "Scene A/B -> push Scene C" export const TRANSITION_PUSH_C: SceneTransition = slidePush( "up", undefined, PUSH_DURATION, ); // export const TRANSITION_PUSH_C: SceneTransition = fadeTransition(PUSH_DURATION); // export const TRANSITION_PUSH_C: SceneTransition = colorFlash("#ffffff", 700); // Transition used by "Scene C -> pop back" export const TRANSITION_POP_C: SceneTransition = slidePop( "down", undefined, POP_DURATION, ); // export const TRANSITION_POP_C: SceneTransition = fadeTransition(POP_DURATION); // export const TRANSITION_POP_C: SceneTransition = colorFlash("#ffffff", 700);