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 / Helper | What you get |
---|---|
this.game | Current Game object (network, renderer, …) |
this.entity | The Entity this script sits on |
this.time | Time helper (ticks, delta, frame counts) |
this.inputs | Input 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
Method | Runs when… | Typical use |
---|---|---|
setup() | Immediately after constructor—before hierarchy | Rare edge-case initialisation |
onInitialize() | First frame the entity is alive | Allocate resources, attach listeners |
onTick() | Every simulation tick (60 TPS by default) | Gameplay logic, physics, AI |
onFrame() (client) | Every render frame | Extra 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 withthis.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:
Signal | Fires when… |
---|---|
EntitySpawned | Entity constructed & ready |
EntityDestroyed | Entity about to disappear |
EntityCollision | Two colliders begin / end overlap |
Value.onChanged | A @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!