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, …) - Tick-based key handling (
@onKeyPressed,@onKeyComboPressed) - 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>(() => {
this.getContext()?.setCurrentScene("main");
})
@onHover<StartButton>(() => {
this.setOpacity(0.8);
})
@onStopHovering<StartButton>(() => {
this.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.
@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.
Keyboard decorators (tick-based)
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)
Runs the handler every tick while key is held.
import { onKeyPressed } from "sliver-engine";
import { Vector } from "sliver-engine";
@onKeyPressed("ArrowLeft", (obj) => {
obj.speed = new Vector(-2, 0);
})
@onKeyPressed("ArrowRight", (obj) => {
obj.speed = new Vector(2, 0);
})
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
@onKeyComboPressed(keys, handler)
Runs the handler every tick while all keys in keys are held.
import { onKeyComboPressed } from "sliver-engine";
@onKeyComboPressed(["Shift", "ArrowRight"], (obj) => {
obj.speed.x = 5; // “dash”
})
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.
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.