1 | # super-ecs
|
2 | Entity Component System library for JavaScript/TypeScript games.
|
3 |
|
4 | - **API Docs:** https://goldenratio.github.io/super-ecs/
|
5 | - **Example Repo:** https://github.com/goldenratio/super-ecs-example
|
6 | - **Example Repo (Vanilla JS):** https://github.com/goldenratio/super-ecs-vanilla-js-example
|
7 | - **CDN:** https://unpkg.com/browse/super-ecs/dist/
|
8 |
|
9 | > npm install --save super-ecs
|
10 |
|
11 | https://www.npmjs.com/package/super-ecs
|
12 |
|
13 | ## Basic Usage
|
14 | To define a new component, simply implement `Component`.
|
15 | Note that each component should have a unique `name` property.
|
16 |
|
17 | ```js
|
18 | const COMPONENT_NAMES = {
|
19 | Position: Symbol('Position'),
|
20 | Velocity: Symbol('Velocity'),
|
21 | Health: Symbol('Health')
|
22 | };
|
23 |
|
24 | class Position {
|
25 | name = COMPONENT_NAMES.Position;
|
26 | constructor({ x = 0, y = 0 }) {
|
27 | this.x = x;
|
28 | this.y = y;
|
29 | }
|
30 | }
|
31 |
|
32 | class Velocity {
|
33 | name = COMPONENT_NAMES.Velocity;
|
34 | constructor({ x = 0, y = 0 }) {
|
35 | this.x = x;
|
36 | this.y = y;
|
37 | }
|
38 | }
|
39 |
|
40 | class Health {
|
41 | name = COMPONENT_NAMES.Health;
|
42 | constructor({ maxHealth = 100 }) {
|
43 | this.health = maxHealth;
|
44 | this.maxHealth = maxHealth;
|
45 | }
|
46 |
|
47 | isDead() {
|
48 | return this.health <= 0;
|
49 | }
|
50 |
|
51 | receiveDamage(damage) {
|
52 | this.health -= damage;
|
53 | }
|
54 | }
|
55 | ```
|
56 |
|
57 | An entity is essentially a container of one or more components.
|
58 |
|
59 | ```js
|
60 | const hero = new Entity();
|
61 | hero.addComponent(new Position({ x: 0, y: 0 }));
|
62 | hero.addComponent(new Velocity({ x: 0, y: 0 }));
|
63 | hero.addComponent(new Health({ maxHealth: 50 }));
|
64 | ```
|
65 |
|
66 | The system is responsible for updating the entities.
|
67 | In a real game there may be a lot of systems, like `CollisionSystem`,
|
68 | `RenderSystem`, `ControlSystem` etc.
|
69 |
|
70 | ```js
|
71 | class PhysicSystem extends System {
|
72 |
|
73 | update(delta) {
|
74 |
|
75 | const entities = this.world.getEntities([COMPONENT_NAMES.Position, COMPONENT_NAMES.Velocity]);
|
76 | entities.forEach(entity => {
|
77 | const position = entity.getComponent(COMPONENT_NAMES.Position);
|
78 | const velocity = entity.getComponent(COMPONENT_NAMES.Velocity);
|
79 | if (position && velocity) {
|
80 | position.x += velocity.x * delta;
|
81 | position.y += velocity.y * delta;
|
82 | }
|
83 | });
|
84 | }
|
85 | }
|
86 | ```
|
87 |
|
88 | The world is the container of all the entities and systems.
|
89 | Calling the `update` method will *sequentially* update all the systems,
|
90 | in the order they were added.
|
91 |
|
92 | ```js
|
93 | const world = new World();
|
94 |
|
95 | world.addEntity(hero);
|
96 | // ... add other entities
|
97 |
|
98 | world.addSystem(new PhysicSystem());
|
99 | // ... add other systems
|
100 |
|
101 | requestAnimationFrame(function () {
|
102 | world.update(/* interval */);
|
103 | })
|
104 | ```
|
105 |
|
106 | A system is notified when it is added or removed from the world:
|
107 |
|
108 | ```js
|
109 | class MySystem extends System {
|
110 |
|
111 | addedToWorld(world) {
|
112 | super.addedToWorld(world);
|
113 | // Code to handle being added to world. Remember to call `super`.
|
114 | }
|
115 |
|
116 | removedFromWorld(world) {
|
117 | super.removedFromWorld(world);
|
118 | // Code to handle being removed from world.. Remember to call `super`.
|
119 | }
|
120 | }
|
121 | ```
|
122 |
|
123 | The world emits Observable when entities are added or removed. You can listen for
|
124 | specific entities and handle the Observable accordingly:
|
125 |
|
126 | ```js
|
127 | class MySystem extends System {
|
128 |
|
129 | removedFromWorld(world) {
|
130 | super.removedFromWorld(world);
|
131 | if (this._disposeBag) {
|
132 | this._disposeBag.dispose();
|
133 | }
|
134 | }
|
135 |
|
136 | addedToWorld(world) {
|
137 | super.addedToWorld(world);
|
138 | // Code to handle being added to world. Remember to call `super`.
|
139 |
|
140 | this._disposeBag = new DisposeBag();
|
141 |
|
142 | this._disposeBag.completable$(world.entityAdded$([COMPONENT_NAMES.Position, COMPONENT_NAMES.Velocity]))
|
143 | .subscribe(entity => {
|
144 | // This function is called whenever an entity with both 'position' and
|
145 | // 'velocity' components is added to the world. It can also be called when
|
146 | // a component is added to an entity; for example, when an entity with
|
147 | // only 'position' has 'velocity' added to it.
|
148 | });
|
149 |
|
150 | this._disposeBag.completable$(world.entityRemoved$([COMPONENT_NAMES.Position, COMPONENT_NAMES.Velocity]))
|
151 | .subscribe(entity => {
|
152 | // This function is called whenever an entity with both 'position' and
|
153 | // 'velocity' components is removed from the world. It can also be called
|
154 | // when a component is removed from an entity; for example, when an entity
|
155 | // with both 'position' and 'velocity' has 'velocity' removed from it.
|
156 | });
|
157 | }
|
158 | }
|
159 | ```
|
160 |
|
161 | Port of [CES.js](https://github.com/qiao/ces.js) with some changes,
|
162 | - instead of `signal`, we use `rxjs`
|
163 | - component names should be of type `symbol`
|
164 |
|
165 | ## Release
|
166 | ```
|
167 | npm version {major | minor | patch}
|
168 | npm publish
|
169 | ```
|