Appearance
Code:
Entities
An entity is an object that has a unique ID, very much like a JavaScript object. Its purpose is to group components together; it may have up to one component of each type.
Creating entities
You can create entities by invoking createEntity
on your world or on a system. You pass in the types of the components that you want the entity to start out with, each optionally followed by initial values to assign to the component's fields.
js
world.createEntity(ComponentFoo, {foo: 'bar', baz: 42}, ComponentBar);
ts
world.createEntity(ComponentFoo, {foo: 'bar', baz: 42}, ComponentBar);
It's also fine (if unusual) to create an entity with no components as a kind of placeholder.
Adding components
Once an entity has been created, it is possible to add components to it at any time:
ts
@component class ComponentA {
@field.int32 declare value: number;
}
@component class ComponentB {
@field.dynamicString(20) declare message: string;
}
// in a system, given an entity:
entity.add(ComponentA, {value: 10});
// or add multiple components at once:
entity.addAll(ComponentA, {value: 10}, ComponentB, {message: 'hello'});
js
class ComponentA {
static schema = {
value: Type.int32
};
}
class ComponentB {
static schema = {
message: Type.dynamicString(20)
};
}
// in a system, given an entity:
entity.add(ComponentA, {value: 10});
// or add multiple components at once:
entity.addAll(ComponentA, {value: 10}, ComponentB, {message: 'hello'});
The arguments to add
and addAll
are the same as those to createEntity
above.
Trying to add the same component type to an entity more than once will result in an error. Adding an enum component type will automatically remove any other component from the same enum.
Accessing and modifying components
Components can be accessed from an entity in two ways:
read(Component)
: get the component for read-only operations. (Attempts to set field values will throw an error unless you're running in performance mode.)write(Component)
: get the component to modify its field values.
ts
@component class ComponentA {
@field.int32 declare value: number;
}
@component class ComponentB {
@field.int32 declare value: number;
}
// in a system, given an entity:
entity.write(ComponentA).value += entity.read(ComponentB).value;
js
class ComponentA {
static schema = {
value: Type.int32
};
}
class ComponentB {
static schema = {
value: Type.int32
};
}
// in a system, given an entity:
entity.write(ComponentA).value += entity.read(ComponentB).value;
DANGER
You must not hang on to the component handles returned by read
and write
, as they'll be invalidated by the next call to read
or write
on the same component type.
These two access modes help to implement reactive queries with minimal overhead, allowing your systems to easily get lists of entities whose components have been mutated. Note that the component will get marked as changed even if you don't change any fields, so try to use write
only when you know you will actually modify the component and use read
otherwise.
Keeping these two modes distinct also makes it clear how a system is acting on components, and allows Becsy's scheduler to automatically parallelize system execution without needing to use expensive and error-prone locks.
Removing components
Another common operation on entities is to remove components:
ts
entity.remove(ComponentA);
entity.removeAll(ComponentA, ComponentB);
js
entity.remove(ComponentA);
entity.removeAll(ComponentA, ComponentB);
Removing an enum from an entity will instead remove the entity's current enum component. Trying to remove a component that an entity doesn't have will result in an error.
Removing a component makes it disappear from the entity immediately, but Becsy actually keeps it around until the end of the next frame. This is done so that every system that needs to react to the removal gets a chance to access the data of removed components. You can access recently removed components like this:
ts
world.build(sys => {
const entity = sys.createEntity(ComponentA, {value: 10});
entity.read(ComponentA).value; // 10
entity.remove(ComponentA);
// entity.read(ComponentA).value; // error!
sys.accessRecentlyDeletedData();
entity.read(ComponentA).value; // 10
})
js
world.build(sys => {
const entity = sys.createEntity(ComponentA, {value: 10});
entity.read(ComponentA).value; // 10
entity.remove(ComponentA);
// entity.read(ComponentA).value; // error!
sys.accessRecentlyDeletedData();
entity.read(ComponentA).value; // 10
})
However you cannot write to recently deleted components.
Checking for components
While normally you'll use queries to select entities with the desired combination of components, sometimes you'll need to check explicitly whether an entity has a component or not. This is useful when writing validators but can also be used to check whether a component needs to be added or removed.
There are a few methods available for these checks:
ts
entity.has(ComponentA);
entity.hasSomeOf(ComponentA, ComponentB);
entity hasAllOf(ComponentA, ComponentB);
entity.hasAnyOtherThan(ComponentA, ComponentB);
entity.countHas(ComponentA, ComponentB, ComponentC);
js
entity.has(ComponentA);
entity.hasSomeOf(ComponentA, ComponentB);
entity hasAllOf(ComponentA, ComponentB);
entity.hasAnyOtherThan(ComponentA, ComponentB);
entity.countHas(ComponentA, ComponentB, ComponentC);
All these methods respect System.accessRecentlyDeletedData()
, in case you need to check whether a component was recently removed, but reactive queries are usually better for this.
All of the above methods (except hasAllOf
) will accept an enum to stand in for all its member component types. There's also an extra method for efficiently figuring out which component of an enum is currently present on the entity, if any:
ts
entity.hasWhich(enumA); // returns a component type or undefined
js
entity.hasWhich(enumA); // returns a component type or undefined
Deleting entities
Unlike JavaScript objects, which are automatically disposed of when they're no longer referenced, entities must be explicitly deleted like so:
ts
entity.delete();
js
entity.delete();
Doing so will remove all components from the entity (triggering relevant reactive queries) then delete the entity itself. The system deleting an entity will need to hold write
entitlements for all components on the entity. If it's hard to predict the set of possible component types a common pattern is to delegate the deletion to a dedicated system:
ts
@component class ToBeDeleted {}
@system class SystemA extends System {
execute() {
// Instead of entity.delete(), just tag it:
entity.add(ToBeDeleted);
}
}
@system class Deleter extends System {
// Note the usingAll.write below, which grants write entitlements on all component types.
entities = this.query(q => q.current.with(ToBeDeleted).usingAll.write);
execute() {
for (const entity of this.entities.current) entity.delete();
}
}
js
class ToBeDeleted {}
class SystemA extends System {
execute() {
// Instead of entity.delete(), just tag it:
entity.add(ToBeDeleted);
}
}
class Deleter extends System {
constructor() {
// Note the usingAll.write below, which grants write entitlements on all component types.
this.entities = this.query(q => q.current.with(ToBeDeleted).usingAll.write);
}
execute() {
for (const entity of this.entities.current) entity.delete();
}
}
Deleting an entity that has already been deleted will result in an error.
Holding on to entity objects
The entity objects returned from createEntity
or obtained from queries are ephemeral: they are only guaranteed to remain valid until the system finishes executing. Afterwards, they may be invalidated at any time even if the entity has not yet been deleted. (It's fine to assign these ephemeral entities to a ref
field, though, as it keeps track of the underlying entity directly.)
To keep an entity object for longer you need to "hold" it:
ts
@system class MySystem extends System {
private myImportantEntity: Entity;
initialize(): void {
const newEntity = this.createEntity(Foo, Bar);
this.myImportantEntity = newEntity.hold();
}
execute(): void {
this.myImportantEntity.read(Foo); // OK!
}
}
js
class MySystem extends System {
initialize(): void {
const newEntity = this.createEntity(Foo, Bar);
this.myImportantEntity = newEntity.hold();
}
execute(): void {
this.myImportantEntity.read(Foo); // OK!
}
}
A held entity handle becomes invalid shortly after the underlying entity has been deleted, at which point trying to call any method on it will result in an error. If the lifecycle of an entity held by a system is outside its control then you should check entity.alive
every frame and stop referencing the entity once it becomes false
. You're guaranteed at least one frame where entity.alive
is false
and the handle is still valid, but if you miss the opportunity you're out of luck.