Skip to main content

Behaviors

Behaviors are TypeScript classes that bring logic to an Entity.
If you know Unity, think MonoBehaviours; in Godot, they're Scripts.

A Behavior can

  • react to lifecycle callbacks (onInitialize, onTick, …)
  • read global helpers like this.game / this.entity
  • declare @value() / @syncedValue() properties that show up in the editor and replicate over the network

1. Defining a Behavior

Place the file inside your project's src/ folder and export a class that extends Behavior:

import { Behavior, value } from "@dreamlab/engine";

export default class Spinner extends Behavior {
/** degrees per second (auto-saved & editor-visible) */
@value()
speed = 90;

onTick() {
this.entity.transform.rotation += this.speed * this.time.delta;
}
}

Marking a behavior property as a @value() or @syncedValue() is similar to the [SerializeField] annotation in Unity or the @export annotation in Godot, with the added benefit of enabling automatic network syncing.


2. Handy built-ins

Property / HelperWhat you get
this.gameCurrent Game object (network, renderer, …)
this.entityThe Entity this script sits on
this.timeTime helper (ticks, delta, frame counts)
this.inputsInput manager (keys, mouse, virtual joysticks)
this.listen()Sugar for other.on(Event, handler) + auto-cleanup
this.hasAuthority()true if this peer controls the entity

3. Lifecycle callbacks

MethodRuns when…Typical use
setup()Immediately after constructor—before hierarchyRare edge-case initialisation
onInitialize()First frame the entity is aliveAllocate resources, attach listeners
onTick()Every simulation tick (60 TPS by default)Gameplay logic, physics, AI
onFrame() (client)Every render frameExtra visuals that don't need to sync
onPreTick()First phase of the tick (before physics)Input sampling, clear one-frame buffers
onPostTick()Last phase of the tick (after physics)Camera follow, cleanup

Platform-specific hooks

Add Client or Server to restrict a hook:

  • onInitializeClient, onInitializeServer
  • onTickClient, onTickServer

TypeScript can't yet narrow this.game automatically; guard with this.game.isClient() when you need the specialised type.


4. Typed access to other entities

The dot-path proxy (root._.<Name>) returns a generic Entity.
Call .cast(Type) for full API & IntelliSense:

const cam = this.game.local._.Camera.cast(Camera);
cam.zoom = 2;

Bracket syntax works for names with spaces:

const boss = game.world._["Final Boss"].cast(Rigidbody);

5. Listening to signals

Dreamlab uses an event-bus called Signals.
Subscribe with this.listen() (auto-unsubscribed on destroy):

onInitialize() {
// Stop the player when hitting this zone’s collider
this.listen(this.entity, EntityCollision, e => {
if (e.started) {
const player = e.other.getBehavior(Player);
player.speed = 0;
}
});
}

Common Signals:

SignalFires when…
EntitySpawnedEntity constructed & ready
EntityDestroyedEntity about to disappear
EntityCollisionTwo colliders begin / end overlap
Value.onChangedA @value() property changes

6. Example - interactive zone

import { Behavior, EntityCollision, value } from "@dreamlab/engine";

export default class Zone extends Behavior {
@value() zone = "shop";

onInitialize() {
if (!this.game.isClient()) return;

// Run when the zone collides with the player's hitbox
this.listen(this.entity, EntityCollision, e => {
if (!e.started && e.other.name !== 'Player') return;

this.game.fire(EnterZoneEvent, this.entity.id, e.other.id, this.zone);
});
}
}

Happy scripting!