Skip to content

Components

A component is state attached to an actor. You declare one by extending Component and describing it with a decorator.

import { Component, DefineComponent, DefineField } from 'dacha';
@DefineComponent({
name: 'Health',
})
export default class Health extends Component {
@DefineField({
initialValue: 100,
})
points!: number;
maxPoints = this.points;
}

That is the whole thing. Nothing else has to be registered anywhere.

Two rules are not visible in the code. The class has to be the default export, because that is what the glob collects. And the name in @DefineComponent is the name the configuration uses, so with the decorator you never assign componentName yourself.

The decorator is optional. A class can set static componentName = 'Health' instead. The game then finds it, but the editor does not show it. The editor lists only classes with a decorator.

The engine fills every field that has @DefineField. It takes the value from the configuration. When the key is missing or the value is null, it uses the field’s initializer, and then initialValue. A field without either stays undefined.

The ! after points tells TypeScript that the field gets a value without a constructor.

Fields fill in the order you declare them. That is why maxPoints = this.points sees the value of points.

Write a constructor when a field needs additional initialization beyond its configured value. Pass the configuration to super. Without it, the decorated fields keep their initial values:

constructor(config: object) {
super(config);
this.maxPoints = this.points * 2;
}

The constructor body runs after the fields are filled, so it can read and change them.

points and maxPoints are both plain properties, and they behave completely differently. The decorator is the whole difference.

A field marked with DefineField is saved into data.json and passed back to the component constructor. An undecorated property is not saved, and the constructor computes it again each time the component is built. your classdata/data.jsonthe live component@DefineField()points"points": 100points = 100the inspectorsaves itpassed tothe constructormaxPointsno decoratornot savedmaxPoints = 100computed on the spotthe constructor rebuilds it every time the component is built
A decorated field travels through the configuration. An undecorated property never leaves your code, and the constructor rebuilds it on every load.

points carries the decorator. You edit it in the inspector, and the editor writes it into data/data.json. The engine copies the saved value into points when it builds the actor. The value in the inspector is the value the component starts with.

maxPoints carries no decorator. It is not in the inspector, not in the file, and nothing restores it.

So decorate what you want to change from the editor, and leave the rest as ordinary run-time state.

Adding a field to a component that actors already use is safe. When the editor loads a project it compares the saved configuration against the current schemas, and it writes the field’s initial value wherever the value is missing. Existing actors pick up the new field with no work from you.

Renaming a field is not the same operation. The editor sees one name disappear and another appear. It fills the new name with its initial value and leaves the old key in the file, where nothing reads it. Values do not travel across a rename, so treat a rename as a delete plus an add.

The engine never calls into a component. There is no update, and nothing in the loop reaches a component except through the system or behavior that reads it.

Methods are fine. A helper that keeps two fields consistent, or a getter that derives a value from them, is still state with a convenient surface on it. What a component cannot be is a place for work that has to run on its own.

A component can look around from where it sits. this.actor is the actor the component is attached to, and the engine sets it on attachment. getParentComponent() returns the same component class from the parent actor, or undefined when the parent has none. Both are for reading the hierarchy, not for running work of your own.

A component with no fields at all is still useful. It marks an actor, and a query finds the actor by that mark. The engine’s own Behaviors is close to this, and a Player marker is the kind you might write.

The work itself goes in a system when it is about a class of actors, or in a behavior when it is about one.

Once the file exists, the component is available in the editor’s inspector for any actor, and in code by class:

import Health from '../components/health/health.component';
const health = actor.getComponent(Health);
if (health) {
health.points -= 10;
}

The editor can write this file for you, with the decorator and the class already in place. See generating scripts.