Input & events
Sliver’s default way to handle input is through decorators applied to a GameObject’s handleEvent(event) method.
This gives you:
- Hitbox-aware mouse events (
@onClick,@onHover, …) - Screen-wide mouse events (
@onClickAnywhere,@onMouseWheel) - Key handling (
@onKeyPressedfor one-shot,@onKeyHoldfor continuous; combos too) - Event propagation control (
event.stopPropagation) - Composition: stack multiple decorators on the same method
Enable decorators in TypeScript
Decorators are a TypeScript feature. Enable them in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
The event model (what actually happens)
Event sources
The engine produces these event types:
keyPressed/keyReleased(fromwindow)mouseMoved,mouseButtonPressed,mouseButtonReleased,mouseWheelScrolled(from the game canvas element)
Mouse events include x/y in canvas coordinates (top-left is 0,0).
Dispatch order (who gets the event first)
When an event occurs:
Gamedispatches it to active scenes in reverse order (top-most scene first).- Each
Scenedispatches it to game objects in reverse order (last added object first).
Stopping propagation
Any handler can set:
event.stopPropagation = true;
Once that happens, remaining objects won’t receive the event, and underlying scenes will effectively stop receiving it too (they’ll early-out because stopPropagation is already true).
This is what makes overlays work: your pause menu scene (on top) can consume input so the gameplay scene underneath doesn’t react.
The recommended pattern: decorate handleEvent
Most of the time, you don’t manually switch on event.type. You decorate handleEvent and keep it tiny:
import { GameObject } from "sliver-engine";
import type { GameEvent } from "sliver-engine";
import { onClick, onHover, onStopHovering } from "sliver-engine";
class StartButton extends GameObject {
@onClick<StartButton>((obj) => {
obj.getContext()?.setCurrentScene("main");
})
@onHover<StartButton>((obj) => {
obj.setOpacity(0.8);
})
@onStopHovering<StartButton>((obj) => {
obj.setOpacity(1);
})
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
}
Why call super.handleEvent(event):
- The base
GameObject.handleEventincludes built-in hover state management. - If you skip the
supercall, you opt out of that base behavior.
Mouse decorators
Mouse decorators are usually hitbox-based, meaning they check if the mouse point is inside any of the object’s hitboxes.
Prerequisite: your object needs at least one hitbox for hitbox-based decorators to ever fire.
@onClick(handler)
Runs on mouseButtonPressed if the pointer is inside the object’s hitboxes.
@onClickAnywhere(handler)
Runs on mouseButtonPressed anywhere on the canvas (not hitbox-based).
@onMouseRelease(handler)
Runs on mouseButtonReleased if the pointer is inside the object’s hitboxes.
@onHover(handler) / @onStopHovering(handler)
Runs on hover enter / hover exit (based on mouseMoved).
The engine keeps a gameObject.hovering boolean for you; these decorators flip it and run only on transitions (enter/exit), not every move.
@onMouseMoved(handler)
Runs on every mouseMoved while the pointer is inside the hitboxes (continuous hover).
@onMouseWheel(handler)
Runs on every mouseWheelScrolled anywhere on the canvas (not hitbox-based).
@onMouseWheelOverHitbox(handler)
Runs on mouseWheelScrolled only if the pointer is inside the hitboxes.
Live example: all mouse decorators stacked
This sandbox uses all mouse decorators on one object:
@onClick@onClickAnywhere@onMouseRelease@onHover@onStopHovering@onMouseMoved@onMouseWheel@onMouseWheelOverHitbox
Only MouseDecoratorsObject.ts is editable. The base class (MouseDecoratorsObject.base.ts) contains the box behavior/state, and MouseDecoratorsHud.ts renders the text/counters.
import { onClick, onClickAnywhere, onHover, onMouseMoved, onMouseRelease, onMouseWheel, onMouseWheelOverHitbox, onStopHovering, Vector, type GameEvent, } from "sliver-engine"; import { MouseDecoratorsObjectBase } from "./MouseDecoratorsObject.base"; const BOX_STEP_X = 8; const BOX_MIN_X = 40; const BOX_MAX_X = 520 - 64 - 40; export class MouseDecoratorsObject extends MouseDecoratorsObjectBase { constructor() { super("mouse-decorators-demo"); } @onClick<MouseDecoratorsObject>((obj) => { obj.decoratorCounters.onClick += 1; obj.fillColor = "#f97316"; const pos = obj.getPosition(); obj.setPosition( new Vector(Math.max(BOX_MIN_X, Math.min(BOX_MAX_X, pos.x + BOX_STEP_X)), pos.y), ); }) @onClickAnywhere<MouseDecoratorsObject>((obj) => { obj.decoratorCounters.onClickAnywhere += 1; obj.outlineColor = obj.outlineColor === "#e2e8f0" ? "#22d3ee" : "#e2e8f0"; }) @onMouseRelease<MouseDecoratorsObject>((obj) => { obj.decoratorCounters.onMouseRelease += 1; obj.fillColor = "#f59e0b"; }) @onHover<MouseDecoratorsObject>((obj) => { obj.decoratorCounters.onHover += 1; obj.setOpacity(0.84); }) @onStopHovering<MouseDecoratorsObject>((obj) => { obj.decoratorCounters.onStopHovering += 1; obj.setOpacity(1); }) @onMouseMoved<MouseDecoratorsObject>((obj, event) => { obj.decoratorCounters.onMouseMoved += 1; obj.lastMousePosition = new Vector(event.x, event.y); }) @onMouseWheel<MouseDecoratorsObject>((obj, event) => { obj.decoratorCounters.onMouseWheel += 1; obj.lastWheelDeltaY = event.deltaY; }) @onMouseWheelOverHitbox<MouseDecoratorsObject>((obj, event) => { obj.decoratorCounters.onMouseWheelOverHitbox += 1; const direction = event.deltaY === 0 ? 0 : Math.sign(event.deltaY); obj.rotation += direction * 0.08; }) override handleEvent(event: GameEvent): void { super.handleEvent(event); } }
Keyboard decorators
Keyboard events update an internal KeyAccumulator. The key decorators use the current key state from the GameContext, which makes “hold to move” logic easy.
@onKeyPressed(key, handler) (one-shot)
Runs the handler once when key becomes pressed.
import { onKeyPressed } from "sliver-engine";
import { Vector } from "sliver-engine";
@onKeyPressed("ArrowLeft", (obj) => {
obj.sendMessage("ui:back", null);
})
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
@onKeyHold(key, handler) (continuous)
Runs the handler every tick while key is held (good for movement).
import { onKeyHold } from "sliver-engine";
import { Vector } from "sliver-engine";
override tick(): void {
this.speed = Vector.zero();
super.tick();
}
@onKeyHold("ArrowLeft", (obj) => {
obj.speed = new Vector(-120, 0);
})
@onKeyHold("ArrowRight", (obj) => {
obj.speed = new Vector(120, 0);
})
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
@onKeyComboPressed(keys, handler) (one-shot)
Runs the handler once when all keys in keys become pressed.
import { onKeyComboPressed } from "sliver-engine";
@onKeyComboPressed(["Shift", "ArrowRight"], (obj) => {
obj.sendMessage("player:dash", { dir: "right" });
})
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
Notes:
- Keys are compared against
KeyboardEvent.key(e.g."ArrowLeft","a","Shift"). - These are tick handlers; they don’t require you to react to raw
keyPressedevents.
@onKeyComboHold(keys, handler) (continuous)
Runs the handler every tick while all keys in keys are held.
import { onKeyComboHold } from "sliver-engine";
@onKeyComboHold(["Shift", "ArrowRight"], (obj) => {
obj.speed.x = 5; // hold-to-dash
})
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
Live example: all key decorators stacked
This sandbox uses all key decorators on one object:
@onKeyPressed@onKeyHold@onKeyComboPressed@onKeyComboHold
Try these key inputs:
- Hold
W/A/S/Dto move the box - Hold
Shift + AorShift + Dto rotate the box - Tap
Shift + Spaceto restore its rotation - Tap
Spaceto make it have a random opacity
Only KeyboardDecoratorsObject.ts is editable. The base class (KeyboardDecoratorsObject.base.ts) contains the box state and rendering boilerplate.
import { onKeyComboHold, onKeyComboPressed, onKeyHold, onKeyPressed, Vector, type GameEvent, } from "sliver-engine"; import { KeyboardDecoratorsObjectBase } from "./KeyboardDecoratorsObject.base"; const HOLD_SPEED = 120; const SPACE_KEY = " "; export class KeyboardDecoratorsObject extends KeyboardDecoratorsObjectBase { private pendingVelocity = Vector.zero(); constructor() { super("keyboard-decorators-demo"); } override tick(): void { this.pendingVelocity = Vector.zero(); super.tick(); this.speed = this.pendingVelocity.clone(); } @onKeyComboHold<KeyboardDecoratorsObject>(["Shift", "A"], (obj) => { obj.rotation -= 0.08; }) @onKeyComboHold<KeyboardDecoratorsObject>(["Shift", "D"], (obj) => { obj.rotation += 0.08; }) @onKeyHold<KeyboardDecoratorsObject>("w", (obj) => { obj.queueVelocity(new Vector(0, -HOLD_SPEED)); }) @onKeyHold<KeyboardDecoratorsObject>("a", (obj) => { obj.queueVelocity(new Vector(-HOLD_SPEED, 0)); }) @onKeyHold<KeyboardDecoratorsObject>("s", (obj) => { obj.queueVelocity(new Vector(0, HOLD_SPEED)); }) @onKeyHold<KeyboardDecoratorsObject>("d", (obj) => { obj.queueVelocity(new Vector(HOLD_SPEED, 0)); }) @onKeyComboPressed<KeyboardDecoratorsObject>(["Shift", SPACE_KEY], (obj) => { obj.rotation = 0; }) @onKeyPressed<KeyboardDecoratorsObject>(SPACE_KEY, (obj) => { obj.setOpacity(Math.random()); }) override handleEvent(event: GameEvent): void { super.handleEvent(event); } private queueVelocity(delta: Vector): void { this.pendingVelocity.add(delta); } }
Composition: stacking multiple decorators
You can stack decorators freely. This is the normal way to “compose” behavior in Sliver.
Important detail: when you stack multiple decorators, the top-most decorator runs first at runtime (because TypeScript applies method decorators bottom-up).
Example (order matters):
@onClick((obj, event) => {
event.stopPropagation = true;
obj.sendMessage("ui:clicked", obj.name);
})
@onMouseRelease((obj) => {
// Note: stopPropagation affects dispatch to other objects/scenes,
// not other decorators on the same object.
})
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
Dragging: @grabbable()
@grabbable() makes an object draggable with the mouse:
- On press inside hitboxes: sets
beingGrabbed = true, zeroes velocity, and stops propagation - On move while grabbed: moves the object to follow the mouse (in scene/world space) and stops propagation
- On release: ends the grab and optionally “throws” the object based on recent movement (sets
speed)
import { grabbable } from "sliver-engine";
@grabbable()
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
Because grabbing sets beingGrabbed, it interacts with physics: gravity is not applied while the object is being dragged. See Physics.
Forwarding events to children: @onChildrenEvents()
If your object is a container (has children) and you want children to also receive events, use:
import { onChildrenEvents } from "sliver-engine";
@onChildrenEvents()
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
This decorator calls your method first, then forwards the same event instance to every child’s handleEvent.
Tip: since the same event object is forwarded, a child can see event.stopPropagation === true if a parent already consumed it.
Collision-related decorator: @solidTo(predicate)
@solidTo is a convenience decorator for collision filtering on beforeColision(other).
It lives in the same decorators module, but it’s part of the physics/collision story. See Physics for recommended usage patterns.