Skip to main content
Version: Next

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 / maxHp directly 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 +1 button to recover HP.
  • Move with WASD to see the health bar keep its relative offset.
  • The health bar reads hp/maxHp from getMotherShip().
  • Edit Player.ts to 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);
	}
}