Entities
Entities are the building blocks of the world. Any "thing" that exists inside your world is an entity (player, wall, lights, etc.). They hold data in the form of components, which can hold values, and also represent relationships and tags.
Entities are actually just number
s, which is one of the reasons BagelECS is so fast. So while to you it looks like you are calling ent.add()
, you are actually calling (0).add()
. This means serializing entities is very easy, and you can pass them around without almost any performance hit.
BagelECS adds some methods to the Number
prototype, which is generally frowned upon. However, it is not an issue unless you are using other monkey patching libraries that modify the same entity methods.
That being said, most entity methods require that it's this
value is a valid entity in the global World
, so please don't use (3.14).add(new Pos())
in your code. Use the provided method World.prototype.spawn()
to get an entity reference
Creating
To create an entity and get a reference to it, use World.prototype.spawn
. This automatically adds the entity to the world as well.
const world = new World(100);
const ent = world.spawn();
Removing
To remove an entity from a world, use World.prototype.destroy
:
world.destory(ent);
Adding Components
To add a component, use Entity.prototype.add
:
ent.add(new Pos({ x: 1, y: 2 }));
If you want to use a custom component ID (To allow multiple components of the same type), supply an intoId
as the second argument.
ent.add("Player 1", "name");
ent.add("Red", "team");
Built-in components (created with Component()
) can not use custom component ID's
Removing Components
To remove a given component from an entity, use Entity.prototype.remove
, with an intoId
pointing to your component:
ent.remove(Pos);
ent.remove("name");
ent.remove("team");
Checking for Components
To find if a given entity has a given component, use Entity.prototype.has
, which also takes an single intoId
:
ent.has(Pos);
ent.has("name");
If you need to get all the components an entity has, use Entity.prototype.components
:
const componentIds = ent.components();
if(componentIds.has(Pos.getId())) {
...
}
Note: This returns a ReadonlySet<number>
containing all the component ID's an entity has. It does not include the actual component data associated with this entity.
Getting Data
To get a component or a components data from an entity, use Entity.prototype.get
. There are a few different signatures, all meant to provide the most type inference with the least boilerplate, however, they all require a typeId
.
When using a base typeId
literal (most of the time a number
or string
), BagelECS doesn't have enough information to know what kind of data is stored there at compile time. Because of this, you have to specify it:
ent.get("name"); // Return type of any
ent.get<string>("team"); // string
However, if you are getting a whole instance of a class (from a library or somewhere else), BagelECS fills in the type for you when you supply the class constructor:
import { Object3D } from "three";
ent.get(Object3D); // Object3D
If you are using built-in components, it also remembers what your schema looks like and uses that to find a return type:
const Rect = Component({
pos: {
x: Type.number,
y: Type.number,
},
col: Type.string,
});
ent.get(Rect.pos.x); // Infered return type of number
ent.get(Rect.pos.y); // number
ent.get(Rect.col); // string
You may have noticed how built-in components differ from external components when getting data:
// External (Built without Component())
ent.get(Object3D).position.x;
// Built-in (With Component())
ent.get(Rect.pos.x);
Notice how you specify the property you want as part of the argument when using built-in components. This is because BagelECS doesn't actually hold onto the whole Rect
object, only the properties it needs, which is the reason it is so fast. However, this means that if you try to get the plain Rect
object, things will break.
ent.get(Rect).pos.x++; // Error: ent.get(...).pos is undefined
For a longer explanation on why this is the case, see this.
However, this means that you have to get the most specific property you can while using built-in components, or it will break:
const Deep = Component({
a: {
b: {
c: {
prop: Type.number,
},
},
},
});
ent.get(Deep).a.b.c.prop; // ❌ Breaks
ent.get(Deep.a).b.c.prop; // ❌ Breaks
ent.get(Deep.a.b.c).prop; // ❌ Breaks
ent.get(Deep.a.b.c.prop); // ✅ Works
As of right now, BagelECS is not smart enough to emit a compile time error for these kind of mistakes, however it will mark the return type as any
, so if you are expecting ts to infer something and it doesn't it could be a clue you messed up somewhere
Note that this obviously does not work with external components
ent.get(Object3D.position.x); // Object3D does not have property "position"
Updating Data
It can be tempting to do something like this:
ent.get(Rect.pos.x)++;
However, this will not effect the data that is held inside the entity, because the call to ent.get
returns a number
, which is passed by value in js, so any modifications will effect the "new" number
, not the one that is stored.
If you are using external components, or have a non-primitive type in your schema, you can update sub-properties of those components using the assignment operator
There are 2 ways of updating component data that already exists on an entity: Entity.prototype.update
and Entity.prototype.getSlowRef
:
update
can update one property at a time and works very similar to add, except you have to specify the full "path" to your data when using built-in components:
// External components
ent.update(new Object3D(..)); // Overwrite the object I already have stored here with something new
// Built-in components
ent.update(Rect.pos.x, 10);
Notice how you still have to follow the same rules that Entity.prototype.get
sets when updating data.
However, using update can get cumbersome fairly often:
// Move the entity 1 unit to the right
ent.update(Rect.pos.x, ent.get(Rect.pos.x) + 1);
So for cases where performance is not a concern, BagelECS provides getSlowRef
. As it's name implies, it is much slower than other methods, but it provides a much better developer experience:
const ref = ent.getSlowRef();
ref[Rect.pos.x]++;
It returns a proxy, which allows you to get and set any component values using bracket syntax instead of .get
and .update
. However, the property keys (what you put inside the brackets) follows the same rules as .get
, so the following would still not work:
ref[Rect].pos.x++; // ❌ Fails again
Extras
There are also a few more entity methods that you should know about:
tag()
and removeTag()
Attach a tag component (a component without any associated properties/data) to an entity:
ent.tag("enemy");
ent.removeTag("alive");
These interact the same way as other components when using .has
or world queries:
// All entities that have been taged as alive
world.query("alive").forEach(..)
// See if it has a tag
ent.has("enemy");
Hierarchy Methods
One of BagelECS's secondary goals is to provide an easy to use, fast hierarchy API. More information can be found on the hierarchy page
Relationships
BagelECS also supports flecs style relationships, documentation and related methods are on the relationships page