Components are the datatypes we use in the Entity-Component-System paradigm (ECS). In Ape ECS, Components are single shallow Objects managed by JS classes. Those properties can be JavaScript types and special Entity Reference values.
class EquipmentSlot extends ApeECS.Component {
init(values) {
}
preDestroy() {
}
}
// we could assign parameters and other static properties
// directly onto EquipmentSlot and it would be equivalant
EquipmentSlot.parameters = {
name: 'Right Hand',
slotType: 'wieldable',
slot: ApeECS.EntityRef,
holding: false
};
//default
EquipmentSlot.serialize = true;
// defaults to null, only serialize fields listed
EquipmentSlot.serializeFields = ['name', 'slotType', 'slot'];
// defaults to null, don't serialize these fieldsk
EquipmentSlot.skipSerializeFields = ['holding'];
// defaults to false, when true it sends change events
// on property changes through component.update()
EquipmentSlot.changeEvents = false;
// set typeName if your build system renames functions
EquipmentSlot.typeName = 'EquipmentSlot';
👆 You could use static
parameters instead of assigning these values to the class, but as of this writing is still a stage-3 proposal for ECMAScript.
👀 See the World registerComponent documentation for information on how to define a new Component type.
👀 See the Entity Ref Docs for more on Entity Reference properties.
Components can only exist when they're part of an Entity. You can have any number of the same Component type within an Entity. You create instances of a Component either by defining them within world.createEnitity, world.createEntities, or entity.addComponent. Component instances are destroyed through component.destroy or entity.removeComponent.
Component property values can be accessed and changed directly on the Component instance like any other object.
Component
, or update those properties by passing an object with your new values to component.update()
. Currently, the only consequences for not calling this method are:
component.updated
won't be updated to the current tickentity.updatedValues
won't be updated to the current tick- Query filters by
lastUpdated
won't be accurate. - Change events won't be produced.
In future versions, there may be more features tied to this functionality.
- Creating Component Instances
- id
- properties
- key
- init
- preInit
- update
- entity
- getObject
- destroy
- Setters and Getters
There are a few factory functions for creating Components. When you create Entities
you can include the initial components and their values.
Or you can add them on to an existing Entity
.
Regardless of the method you use to create a Component
, the format is as follows:
{
type: 'Position', // Component Type
id: 'hi-there', // optional, unique id, auto generated if not provided
key: 'position', // optional, if set you can access the component by this value from the Entity instance
// see below for more on keys
x: 34, // set the initial value of any property defined when registered
y: 3 // another property that you previously defined
// you don't have to assign values to all of your properties
}
📑 Related, you can always add Tags
, which are similar to Components, except they're just the name/type. entity.addTag
☝️ Components
can't exist without being part of an Entity
. A future release may add more ways to use Components.
💭 Components
are pooled by their type. When you world.registerComponent, set a pool size close to the number of a given type you expect to exist at a time. The Component
pools add some efficiency, and lowers the amount of CPU time the JavaScript garbage collector needs.
The id property of a Component
is a String
that has generally been auto-generated.
Entity
or Component
after they have been created. Here there be dragons. 🐉
You can access any property directly on a Component
instance that you have registered with the component.
See the example at the top of this document.
Component
that you didn't register, they won't behave properly.
Component
method, be wary of inserting game logic to Components as it quickly turns your game into an EC
game rather than an ECS. You'll lose some of the benefits of this approach. You might also make logic that limits you in the future, like the ability to run the same Components on a server and a client. Keep your game logic in your Systems
and override Component
methods only for data formatting and access.
For example, if you have a game engine sprite, you might have a Component that contains all of the necessary information to stand up a sprite, and a reference to the game engine sprite instance itself. When you store the sprite you may need to remove the sprite instance from the Component, because it can't be serialized. You can do that by overriding getObject
. But resist the tempation to re-create the game engine sprite in init
. That won't work on your server. Instead, tag any new Entitys that have an unititialized Sprite Component
with "New" or "NewSprite". You can then have a System
Query
that checks for Query.fromAll(['Sprite', 'New'])
to initialize it for you.
Setting the key property will map the Component
within its entity.c
by that value for convenience. As always, that same Component
will still be accessible within entity.types
.
const entity = world.createEntity({
components: [
{
type: 'Position',
key: 'position',
x: 3,
y: 10
}
]
});
console.log(entity.c.position.x); // 3
for (const position of entity.types.Position) {
console.log(position.x, position.y);
}
// 3 10
👆 Setting the key
property is completely optional.
If a Component
instance doesn't have a key, it doesn't show up in the entity.c
Object, but it will still be retrievable with entity.getComponents(type) and the entity.types[type]
. Each return Sets
of Components on the Entity of a given type.
For example, an Entity
representing a character may have many Component instances of the type "Buff", but only one "Inventory" Component. In that case, it doesn't make sense to have a key
on any of the Buff instances, but it does on the "Inventory" component for convenience.
👆 You can also set the key in an Entity
creation factory on the c
property.
This is equivalant to the above:
const entity = world.createEntity({
c: {
position: {
type: 'Position',
x: 3,
y: 10
}
}
});
You can override the init
method of your Component
.
It's ran after the Component
has been created through any of the factory methods.
Arguments:
- initialValues:
Object
values that were passed as the initial property values. Does not include the results of anysetters
or defaults.
You can override the preInit
method of your Component
.
It's ran before the Component
has assigned the initial values.
You must return an object of initial values to get assigned.
Arguments:
- initialValues:
Object
values that were passed as the initial property values. Does not include the results of anysetters
or defaults.
Returns Initial values to assign.
👆 This is a good place to set up any properties on the _meta
object that you may need for your getters/setters.
Method to mark Component as updated, optionally update properties, optionally send system.subscribe() change event.
Arguments:
- values:
Object
, optional, properties and values of component to update
You can update the properties on a Component directly, but query.execute() will not be able to filter by updatedValues if you don't. Additionally, if you system.subscribe() to a Component type, you won't get change events unless you use this method to update values.
👆 You'll also need to set the static property changeEvents = true
if you want change subscription events.
The entity property gives you access to the parent Entity
instance.
Get a serializable version of the Component.
const obj = component.getObject();
console.log(obj);
{
type: 'Point', // component type name
id: 'aabbbcc-37', // generated or assigned component id
entityId: 'kjasdlf-03', // generated or assigned entity id
key: 'point', // assigned key, if set
x: 38, // any properties that you registered
y: 44
}
☝️ If you have a staticarray for serializeFields
, then only those fields will be in the resulting object.
skipSerializeFields
is the opposite approach, listing fields that you do not wish to serialize.
☝️ You can use the results of component.getObject
as the component in entity.addComponent and as a component part in world.createEntity and world.createEntities.
💭 getObject
will grab meta.values
for a given property, if available.
💭 component.getObject
is called by entity.getObject and world.getObject unless you specify the static property serialize = false
on the Component class.
Destroy the component.
someComponent.destroy();
Before any other actions are taken, the preDestroy
function is run.
💭 This has the same effect as entity.removeComponent. This clears all the data in the component and releases it back to the Component
pool of its type.
You can add setters and getters for any property that you defined.
class Position extends ApeECS.Component {
get x() {
return this._meta.values.x;
}
set x(value) {
this._meta.values.x = value;
this.coord = `${this.x}x${this.y}`;
}
get y() {
return this._meta.values.y;
}
set y(value) {
this._meta.values.y = value;
this.coord = `${this.x}x${this.y}`;
}
}
Position.properties = {
x: 0,
y: 0,
coord: '0x0'
};
If you define a setter for a property, be sure to store it in the _meta.values
Object
since you won't be able to store it under the same name.
getObject
assumes that if a _meta.values
property is set, that it's the serializable version. You may need to override getObject
or store your value elsewhere in _meta
if this assumption is incorrect.
You can override preInit if you want to setup a separate place in _meta
for your values.
⭐️ Some of you may have noticed that we could have done the above example more efficiently. You get a gold star, but I needed a more complete example. Here's the simpler version:
class Position extends ApeECS.Component {
get coord() {
return `${this.x}x${this.y}`;
}
}
Position.properties = {
x: 0,
y: 0,
coord: '0x0'
};