The World
is the data structure holding all entities, as well as all the systems. In order to create entities and add components to them, you will need to create a world first.
import { World } from 'ecstra';
const world = new World();
You can also change the configuration of the world:
const world = new World({
systems, // List of default systems
maxComponentType, // Maximum number of component registered
useManualPooling, // If `true`, user need to manage memory pools
EntityClass // Entity class to instanciate on entity creation
);
You can read the API section to get all the information about the world.
You must register every system you want to run:
world.register(MySystemClass, {
... // System options
});
It's possible to register a system at any time. However, registering a system comes at the cost of pre-computing static queries. Doing the registration early can be benefitial.
Entities should be created using a world:
const entity = world.create('nameOfTheEntity');
Entities belong to a given world and can't be exchange / shared between two world instances.
Destroying an entity at the opposite can be done directly on the entity itself:
entity.destroy();
You must add component to entity in order to query them in systems:
entity.addComponent(MyComponent, {
... // Component options
});
If you plan on reading a component, please use
import { ComponentData, StringProp } from 'ecstra';
class MyComponent extends ComponentData {}
MyComponent.Properties = {
myString: StringProp('Hello World!')
};
const component = entity.read(MyComponent);
console.log(component.myString); // 'Hello World!'
You can also retrieve a component in read-write mode
const component = entity.write(MyComponent);
component.myString = 'Hello Ecstra!';
console.log(component.myString); // 'Hello Ecstra!'
NOTE: Right now, reading a component as read-only or as read-write doesn't d anything. The idea is to introduce soon a way to improve query with this information.
When you are done with a component, you can get rid of it by using:
entity.remove(MyComponent);
If you often create and destroy components, consider using pooling to increase performance. For more information, please have a look at the 'Pooling' section.
Components contain data attached to entity and used by systems to apply logic on those entities. Ecstra exposes several type of components that will serve different purposes.
Components deriving ComponentData
can use the automatic initialization and copy of components. ComponentData
can declare a property schema and the component will be initialized automatically.
import { ComponentData, NumberProp } from 'ecstra';
class HealthComponent extends ComponentData {}
HealthComponent.Properties = {
value: NumberProp(100) // 100 is a default value
};
const component = new HealthComponent();
console.log(component.value); // '100'
NOTE: TypeScript users can declare propertiess with decorators.
The DataComponent
class exposes a simple interface:
export class Component {
init(source) {}
copy(source) {}
clone() {}
}
init()
⟶ initialize the component with an object containing the same properties. Missing properties will default to the default value set in theProperties
schemacopy(source)
⟶ Copies the data from the source object, i.e: the same kind of component or an object with the same propertiesclone()
⟶ Returns a new instance of the object with the same properties
Thankfully, you will not need to override those methods if you use the Properties
schema. Those methods will automatically use the properties definition in order to know how to initialize and copy the component.
Ecstra already comes with a few basic property types:
BooleanProp
⟶ Defaults tofalse
NumberProp
⟶ Defaults to0
StringProp
⟶ Defaults to''
ArrayProp
⟶ Defaults to[]
RefProp
- Use it to store reference to object
- Defaults to
null
CopyableProp
- Use it on types implementing
copy()
andclone()
- Defaults to
new type()
, withtype
a given class
- Use it on types implementing
For more information about how to create a custom property, please have a look at the 'Custom Properties' section.
Tags are special kind of components. They hold no data and are used to select entity in queries.
class PlayerTagComponent extends TagComponent {}
You can then attach this component to a "player" entity. This tag component will allow you to select the player in systems and performs custom logic.
Coming soon.
Systems are run when the world ticks. They are scheduled to run one after the other, one group at a time. Systems can query entities based on the components they hold.
import { System } from 'ecstra';
class PhysicsSystem extends System {
init() {
// Triggered on initialization. Note: you can also use the
// constructor for that.
}
execute(delta) {
// Performs update logic here.
}
dispose() {
// Triggered when system is removed from the world.
}
}
Systems have the following lifecycle:
init()
→ Triggered upon system instanciation in the worldexecute()
→ Triggered when the world executedispose()
→ Triggered when system is destroyed by the world
It's possible to declare relation between system of a same group. Then, the group will be sorted based on those relations. Currently, it's possible to define hierarchies using:
UpdateBefore(list)
⟶ the system will run before all other systems listedUpdateAfter(list)
⟶ the system will run after all other systems listed
import { System } from 'ecstra';
class SystemA extends System {
execute() {}
}
SystemA.UpdateAfter = [ SystemC, SystemB ];
class SystemB extends System {
execute() {}
}
class SystemC extends System {
execute() {}
}
SystemC.UpdateBefore = [ SystemA ];
SystemC.UpdateAfter = [ SystemB ];
The group will automatically be sorted using those relations and will the final
group will be [ SystemB, SystemC, SystemA ]
.
Sorting topologically is nice, but you may want to change ordering after the world is setup with a simple priority system.
Systems can be registered with an order that define the position of execution:
world.register(SystemB, { order: 0 });
world.register(SystemC, { order: 1 });
world.register(SystemA, { order: 2 });
At any time, you can change the ordering of systems either by modifying the
order
attribute, or even by modifying the static UpdateBefore
and
UpdateAfter
properties (not recommended).
You will simply need to retrieve the group and call the sort()
method to
ask for a refresh order of the list:
const system = world.system(SystemC);
system.order = 10;
system.group.sort();
System may have a Queries
static properties that list all the queries you want to
cache. Queries are created upon system instanciation, and are
cached until the system is unregistered.
import { NumberProp, System } from 'ecstra';
class TransformComponent extends ComponentData { }
TransformComponent.Properties = {
x: NumberProp(), // Defaults to 0.
y: NumberProp(), // Defaults to 0.
};
class SpeedComponent extends ComponentData { }
SpeedComponent.Properties = {
value: NumberProp(150.0)
};
class PhysicsSystem extends System {
execute(delta) {
// `query` contains **every** entity that has at least the
// components `SpeedComponent` and `TransformComponent`.
const query = this.queries.entitiesWithBox;
// Loops over every entity.
query.execute((entity) => {
const transform = entity.write(TransformComponent);
const speed = entity.read(SpeedComponent);
transform.y = Math.max(0.0, transform.y - speed.value * delta);
});
}
}
// The static property `Queries` list the query you want to automatically
// create with the system.
PhysicsSystem.Queries = {
// The `entitiesWithBox` matches every entity with the `SpeedComponent` and
// `TransformComponent` components.
entitiesWithBox: [ SpeedComponent, TransformComponent ]
};
Queries can also specify that they want to deal with entities that do not have a given component:
import { Not } from 'ecstra';
...
PhysicsSystem.Queries = {
// Matches entities with `SpeedComponent` and `TransformComponent but
// without `PlayerComponent`.
entitiesWithBoxThatArentPlayers: [
SpeedComponent,
TransformComponent,
Not(PlayerComponent)
]
};
A query will notifiy when a new entity is matching its component layout:
class MySystem extends System {
init() {
this.queries.myQuery.onEntityAdded = () => {
// Triggered when a new entity matches the component layout of the
// query `myQuery`.
};
this.queries.myQuery.onEntityRemoved = () => {
// Triggered when an entity that was previously matching query isn't
// matching anymore.
};
}
}
MySystem.Queries = {
myQuery: [ ... ]
}
You can use those two events to perform initialization and disposal of resources.
Those events are synchronous and can be called in any order. If you
have two queries, never assumes the onEntityAdded
and onEntityRemoved
events
of one will be triggered before the other.
NOTE: the current behaviour could be later changed in the library if events must be based on the systems execution order.
For TypeScript users, it's possible to use decorators to declare the properties:
class TestComponentDecorator extends ComponentData {
@boolean
myBoolean: boolean = true;
@number
myNumber: number = 10;
@string
myString: string = 'my string!';
@array
myStringArray: string[] = [];
@reference
myRef: Object | null = null;
}
The first version of Ecstra had pooling disabled by default. However, when I started to benchmark the library I quickly realized that pooling was a must have by default.
By default, every component type and entities have associated pools. If you have 50 different components, Ecstra will then allocates 50 component pools and one extra pool for entities. This may seem like a waste of memory, but will bring by ~50% the cost of creating components and entities.
You can derive your own pool implementation by creating a class matching this interface:
export interface ObjectPool<T> {
destroy?: () => void;
acquire(): T;
release(value: T): void;
expand(count: number): void;
}
You can then use your own default pool for entities / components:
const world = new World({
ComponentPoolClass: MySuperFastPoolClass,
EntityPoolClass: MySuperFastPoolClass
});
Alternatively, you can change the pool on a per-component basis using:
world.registerComponent(MyComponentClass, { pool: MySuperFastPoolClass });
or
world.setComponentPool(MyComponentClass, MySuperFastPoolClass);
If you don't want any default pooling, you can create your World
using:
const world = new World({
useManualPooling: true
})
When the automatic pooling is disabled, ComponentPoolClass
and
EntityPoolClass
are unused. However, manually assigning pool using
world.setComponentPool
is still a possibility.
Pooling can significantly improve performance, especially if you often add or remove components. The default pooling scheme should be enough in most cases, but creating custom pool systems can also help.
Even if pooling is used, adding / deleting components always comes at a cost. The components list is hashed into a string, used to find the new archetype of the entity.
You can probably enabled / disable some components by using a custom field.
You can create your own properties by extending the Property
class:
import { Property } from 'property';
class MyProperty extends Property {
copy(dest, src) {
// Optional method to implement.
// `dest` should receive the value (for reference type).
// `src` is the input.
return dest;
}
}
You can also create a function that setup your property:
function MyProp(options) {
// Process the `options` argument and create the property.
return new MyProperty(...);
}
Please have a look at the generated API (coming soon).