Health bar HUD
This example shows a health bar as a GameObject that is a child of the player:
- it follows the player via the parent/“mothership” relationship
- it reads
hp/maxHpdirectly from its mothership (no messages needed)
Health bar child object
import { GameObject, Vector } from "sliver-engine";
import type { CanvasController, Scene } from "sliver-engine";
class HealthBar extends GameObject {
constructor() {
// Local position (relative to the player when enabled below).
super("ui:health", new Vector(-8, -14));
// Make this object's position relative to its mothership.
this.setPositionRelativeToMotherShip(true);
}
override render(canvas: CanvasController, _scene: Scene): void {
type PlayerWithHealth = GameObject & { hp: number; maxHp: number };
const player = this.getMotherShip<PlayerWithHealth>();
if (!player) return;
const draw = canvas.getShapeDrawer();
const pos = this.getPosition();
const pct = player.maxHp > 0 ? player.hp / player.maxHp : 0;
const width = 32;
const height = 6;
draw.drawRectangle(pos.x, pos.y, width, height, "#333", true);
draw.drawRectangle(pos.x, pos.y, width * pct, height, "#3fb950", true);
}
}
Player owns the health bar
import {
GameObject,
SquareHitbox,
Vector,
onKeyHold,
type GameEvent,
} from "sliver-engine";
class Player extends GameObject {
public hp = 10;
public maxHp = 10;
private static readonly SPEED = 120;
constructor(position: Vector) {
super("player", position);
this.addHitbox(new SquareHitbox(Vector.zero(), new Vector(16, 16), this));
this.setPhisics({
immovable: false,
affectedByGravity: false,
friction: 0,
restitution: 0,
});
// Attach UI as a child so it follows the player automatically.
const healthBar = new HealthBar();
this.addChild(healthBar);
}
damage(amount: number): void {
this.hp = Math.max(0, this.hp - amount);
}
override tick(): void {
this.speed = Vector.zero();
super.tick();
}
@onKeyHold<Player>("w", (obj) => obj.speed = new Vector(0, -Player.SPEED))
@onKeyHold<Player>("a", (obj) => obj.speed = new Vector(-Player.SPEED, 0))
@onKeyHold<Player>("s", (obj) => obj.speed = new Vector(0, Player.SPEED))
@onKeyHold<Player>("d", (obj) => obj.speed = new Vector(Player.SPEED, 0))
override handleEvent(event: GameEvent): void {
super.handleEvent(event);
}
}
Why this pattern is nice:
- The UI doesn’t need to find the player globally or subscribe to messages.
- The bar automatically inherits scene/context from the parent via
addChild(...).
Interactive example
This sandbox shows a health bar as a child object attached to the player.
- Click the player square to damage it (
-1 HP). - Click the
Heal +1button to recover HP. - Move with
WASDto see the health bar keep its relative offset. - The health bar reads
hp/maxHpfromgetMotherShip(). - Edit
Player.tsto tweak player/health behavior.
import { GameObject, SquareHitbox, Vector, onClick, onKeyHold, type GameEvent, } from "sliver-engine"; import { HealthBar } from "./HealthBar"; const PLAYER_SIZE = 40; const PLAYER_SPEED = 120; const CANVAS_WIDTH = 956; const CANVAS_HEIGHT = 380; const MIN_X = 0; const MAX_X = CANVAS_WIDTH - PLAYER_SIZE; const MIN_Y = 0; const MAX_Y = CANVAS_HEIGHT - PLAYER_SIZE; export class Player extends GameObject { public hp = 10; public maxHp = 10; private pendingVelocity = Vector.zero(); constructor(position: Vector) { super("player", position.clone()); this.addHitbox( new SquareHitbox(Vector.zero(), new Vector(PLAYER_SIZE, PLAYER_SIZE), this, { solid: false, debug: false, }), ); this.setPhisics({ immovable: false, affectedByGravity: false, friction: 0, restitution: 0, }); this.setRenderFunction((obj, canvas) => { const pos = obj.getPosition(); canvas .getShapeDrawer() .drawRectangle( pos.x, pos.y, PLAYER_SIZE, PLAYER_SIZE, this.hp <= 3 ? "#ef4444" : "#38bdf8", true, ); }); this.addChild(new HealthBar()); } damage(amount: number): void { this.hp = Math.max(0, this.hp - amount); } heal(amount: number): void { this.hp = Math.min(this.maxHp, this.hp + amount); } override tick(): void { const pos = this.getPosition(); this.setPosition( new Vector( Math.max(MIN_X, Math.min(MAX_X, pos.x)), Math.max(MIN_Y, Math.min(MAX_Y, pos.y)), ), ); this.pendingVelocity = Vector.zero(); super.tick(); this.speed = this.pendingVelocity.clone(); } @onKeyHold<Player>("w", (obj) => obj.queueVelocity(new Vector(0, -PLAYER_SPEED))) @onKeyHold<Player>("a", (obj) => obj.queueVelocity(new Vector(-PLAYER_SPEED, 0))) @onKeyHold<Player>("s", (obj) => obj.queueVelocity(new Vector(0, PLAYER_SPEED))) @onKeyHold<Player>("d", (obj) => obj.queueVelocity(new Vector(PLAYER_SPEED, 0))) @onClick<Player>((obj) => { obj.damage(1); }) override handleEvent(event: GameEvent): void { super.handleEvent(event); } private queueVelocity(delta: Vector): void { this.pendingVelocity.add(delta); } }