Skip to main content
Version: Next

Game objects

GameObject is the base building block in Sliver. Most of your gameplay code lives in objects: players, enemies, UI widgets, triggers, effects, etc.

At runtime, a GameObject can:

  • Update on every tick (tick())
  • Draw on every frame (render(...))
  • React to input via decorated handleEvent(event) (see Input & events)
  • Participate in collisions via hitboxes (see Physics)
  • Communicate through the GameContext message bus
  • Own children (a parent/“mothership” relationship)

Creating a GameObject

The base constructor is:

new GameObject(name, position, visible?, active?, hitboxes?, scene?, children?, showOriginDebug?)

Most of the time you either:

  • subclass GameObject, or
  • create one and assign runtime behavior with setTickFunction / setRenderFunction.

Tick vs render

Sliver’s loops are split (see Game loop):

  • tick() runs at the game tick rate (default 60 TPS) and is where you update state.
  • render(canvas, scene) runs every animation frame and is where you draw based on current state.

tickFn and renderFn (runtime-swappable behavior)

Each GameObject has a tickFn and renderFn that are called from tick() / render(). You can replace them at runtime:

gameObject.setTickFunction((obj) => {
// logic for the current “state”
});

gameObject.setRenderFunction((obj, canvas) => {
// draw for the current “state”
});

This is a simple way to implement state machines without subclassing:

import { GameObject, Vector } from "sliver-engine";

const npc = new GameObject("npc", new Vector(200, 200));

const idle = () => npc.setTickFunction(() => {
npc.speed = Vector.zero();
});

const flee = () => npc.setTickFunction(() => {
npc.speed = new Vector(-2, 0);
});

idle();

// later, switch behavior instantly
npc.onMessage("npc:flee", () => flee());

Why it works well in Sliver:

  • tickFn/renderFn are called after key-tick decorators and before physics integration, so they’re a natural place to update speed/rotation/state.
  • Swapping functions is cheap and keeps call sites explicit (“set the object into flee mode now”).

Visibility and activity

  • visible: if false, the object won’t render (children won’t render either).
  • active: if false, the object won’t tick (children won’t tick either).

destroy() is the common way to remove an object:

  • marks it inactive and invisible
  • detaches it from its parent (if any)
  • destroys children
  • removes it from the scene’s object list

Position, scene space, and camera offset

There are two important coordinate spaces:

  • scene/world space (your “real” coordinates)
  • canvas space (scene space plus the scene’s camera offset)

Use:

  • getScenePosition() for world logic (pathing, physics reasoning, storing positions)
  • getPosition() for rendering and pointer hit-testing (it includes the scene offset)

getPosition() adds the scene’s offset automatically, so when you pan the camera (or run a slide transition), objects render and interact correctly.

Children and the “mothership” relationship

A GameObject can own children:

parent.addChild(child);

What this does:

  • sets the child’s “mothership” (child.getMotherShip())
  • propagates scene and GameContext into the child
  • ensures child ticks and renders with the parent

Relative positioning

Children are not automatically positioned relative to their parent. If you want a child’s position to be relative to the parent:

child.setPositionRelativeToMotherShip(true);

When enabled, the child’s world position becomes:

motherShip.absolutePosition + child.localPosition

This is useful for things like a health bar attached to a character, or a weapon attached to a player.

Input and event handling (decorators)

handleEvent(event) is where objects react to input, and the default pattern is to use decorators on handleEvent.

The base GameObject.handleEvent already includes @onHover / @onStopHovering to maintain gameObject.hovering, so your override should usually call super.handleEvent(event).

See Input & events for:

  • click/hover/wheel decorators
  • key-held tick decorators (@onKeyPressed, combos)
  • composition (stacking multiple decorators)

Messaging between GameObjects

Objects communicate through the GameContext message bus (publish/subscribe). GameObject exposes small wrappers:

Send

this.sendMessage("ui:clicked", { id: "start" });

That publishes on the global message bus and sets sender to the current object.

Listen

const unsubscribe = this.onMessage<{ id: string }>("ui:clicked", (payload, sender) => {
if (payload.id === "start") {
// react
}
});

// call unsubscribe() when you no longer need it

Use messages when you want loose coupling (UI talks to gameplay without direct references, enemies broadcast “died”, etc.).

Collisions, hitboxes, and physics hooks

To make an object participate in collisions, give it one or more hitboxes and set physics flags (immovable, mass, restitution, etc.).

Collision flow is:

  • beforeColision(other) (return false to ignore)
  • onColision(other, penetration) (react to overlap)

See Physics for hitbox creation, triggers (solid: false), and how impulses/translation are applied.

Scene control from a GameObject

Since a GameObject has access to the GameContext, you can switch scenes from inside an object:

const ctx = this.getContext();
if (!ctx) return;

ctx.setCurrentScene("main");
ctx.pushScene("pause");
await ctx.transitionToScene("level2", transition, "replace");

Walker (pathing/movement helper)

Sliver includes a Walker helper you can attach to an object via setWalker(...). It can move an object along waypoints and optionally avoid obstacles.

See Walker for usage and configuration.