@draug/engine 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +273 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1 +1,273 @@
|
|
|
1
|
-
# engine
|
|
1
|
+
# @draug/engine
|
|
2
|
+
|
|
3
|
+
Small ECS-first game skeleton for TypeScript: a `World` holds entities, components (plain data), systems (per-frame logic), double-buffered events, typed resources, and optional plugins. A thin `Runtime` plus `GameLoop` / `Clock` help you step simulation on a steady timer instead of growing everything inside one giant class.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **ECS core** — register component storages, attach data to entities, query by `include` / `exclude` / `anyOf`, run systems in dependency order (DAG over `@System` metadata).
|
|
8
|
+
- **Deferred commands** — queue structural changes (e.g. `commands.createEntity`) and apply them after systems run, so you do not mutate archetypes mid-iteration.
|
|
9
|
+
- **Events** — `EventBus` + `EventBuffer` with a per-frame `swap` (done for you inside `world.update` before systems).
|
|
10
|
+
- **Resources** — singleton-style services keyed by constructor (`insert` / `get` / `getOrInsert`).
|
|
11
|
+
- **Plugins** — install decorated `PluginBase` classes, resolve dependency order, then `world.build()` instantiates them.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @draug/engine
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The package is ESM-first (`"type": "module"`) and ships a CommonJS build as well (`exports` in `package.json`).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
Enable legacy decorators in `tsconfig.json` (the library uses `experimentalDecorators` today):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"compilerOptions": {
|
|
28
|
+
"experimentalDecorators": true,
|
|
29
|
+
"target": "ES2022",
|
|
30
|
+
"module": "ESNext",
|
|
31
|
+
"moduleResolution": "Bundler"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Minimal world: register every component type once, register systems, call `systems.build()`, spawn entities, then step with `world.update(dt)`.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import {
|
|
40
|
+
World,
|
|
41
|
+
Component,
|
|
42
|
+
System,
|
|
43
|
+
SystemBase,
|
|
44
|
+
entry,
|
|
45
|
+
type SystemComputeContext,
|
|
46
|
+
} from '@draug/engine';
|
|
47
|
+
|
|
48
|
+
@Component()
|
|
49
|
+
class Position {
|
|
50
|
+
x = 0;
|
|
51
|
+
y = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@System({ query: { include: [Position] } })
|
|
55
|
+
class GravitySystem extends SystemBase {
|
|
56
|
+
compute({ entities, world, dt }: SystemComputeContext): void {
|
|
57
|
+
const positions = world.components.getStorage(Position);
|
|
58
|
+
for (const id of entities) {
|
|
59
|
+
const p = positions.get(id);
|
|
60
|
+
if (p) p.y += 20 * dt;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const world = new World();
|
|
66
|
+
|
|
67
|
+
world.components.register(Position);
|
|
68
|
+
world.systems.register(new GravitySystem());
|
|
69
|
+
world.systems.build();
|
|
70
|
+
|
|
71
|
+
// Same-frame visibility: create entity, then attach components immediately.
|
|
72
|
+
const player = world.entities.create();
|
|
73
|
+
world.addComponent(player, Position, (p) => {
|
|
74
|
+
p.x = 0;
|
|
75
|
+
p.y = 0;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Or defer to end-of-frame (handy when spawning from inside a system):
|
|
79
|
+
world.commands.createEntity(
|
|
80
|
+
entry(Position, (p) => {
|
|
81
|
+
p.x = 100;
|
|
82
|
+
p.y = 0;
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
world.update(1 / 60);
|
|
87
|
+
world.update(1 / 60);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Note:** `world.update` runs systems first, then flushes the command queue. Anything created with `commands.createEntity` only gets components after that flush, so it will not appear in queries until the *next* `update` unless you attach components synchronously.
|
|
91
|
+
|
|
92
|
+
## Usage / API
|
|
93
|
+
|
|
94
|
+
### World
|
|
95
|
+
|
|
96
|
+
`World` is the façade: `entities`, `components`, `systems`, `events`, `resources`, `commands`, `queries`, `plugins`.
|
|
97
|
+
|
|
98
|
+
- **`world.query(params)`** — bitmask-backed query; supports `include`, `exclude`, `anyOf`, `excludeEntitiesIds`, and `filter`.
|
|
99
|
+
- **`world.addComponent(id, ComponentClass, init?)` / `removeComponent`** — structural changes; queries are invalidated for you.
|
|
100
|
+
- **`world.update(dt)`** — `events.swapAll()`, run systems in order, then `commands.flush`.
|
|
101
|
+
|
|
102
|
+
### Components
|
|
103
|
+
|
|
104
|
+
Mark data classes with `@Component()` so the ECS can assign stable type ids:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { Component, ComponentStorageType } from '@draug/engine';
|
|
108
|
+
|
|
109
|
+
// `world` is your World instance from the quick start.
|
|
110
|
+
|
|
111
|
+
@Component()
|
|
112
|
+
class Health {
|
|
113
|
+
current = 100;
|
|
114
|
+
max = 100;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
world.components.register(Health);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Optional **singleton** storage (one instance, not per-entity):
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { Component, ComponentStorageType } from '@draug/engine';
|
|
124
|
+
|
|
125
|
+
@Component()
|
|
126
|
+
class GlobalRNG {
|
|
127
|
+
next() {
|
|
128
|
+
return Math.random();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
world.components.register(GlobalRNG, {
|
|
133
|
+
storageType: ComponentStorageType.SINGLETON_STORAGE,
|
|
134
|
+
factory: () => new GlobalRNG(),
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Systems
|
|
139
|
+
|
|
140
|
+
Extend `SystemBase`, implement `compute`, and decorate with `@System`:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { System, SystemBase, type SystemComputeContext } from '@draug/engine';
|
|
144
|
+
|
|
145
|
+
// `Position`, `Health`, `SomeTag`, `PhysicsSystem` are your own component/system types.
|
|
146
|
+
|
|
147
|
+
@System({
|
|
148
|
+
query: { include: [Position, Health] },
|
|
149
|
+
requiredComponents: [SomeTag],
|
|
150
|
+
computeAfter: [PhysicsSystem],
|
|
151
|
+
})
|
|
152
|
+
class ApplyDamageSystem extends SystemBase {
|
|
153
|
+
compute(ctx: SystemComputeContext): void {
|
|
154
|
+
const { entities, world, dt } = ctx;
|
|
155
|
+
// ...
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- **`query`** — drives which entity ids are passed into `compute`.
|
|
161
|
+
- **`requiredComponents`** — ensures storages exist and documents extra reads that are not part of the query mask.
|
|
162
|
+
- **`computeAfter`** — ordering edges between system classes (both must be registered).
|
|
163
|
+
|
|
164
|
+
Call **`world.systems.build()`** after you finish registering systems so `onInit` hooks run and execution order is computed.
|
|
165
|
+
|
|
166
|
+
### Commands
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { entry } from '@draug/engine';
|
|
170
|
+
|
|
171
|
+
world.commands.add((w) => {
|
|
172
|
+
const id = w.entities.create();
|
|
173
|
+
w.addComponent(id, Health, (h) => {
|
|
174
|
+
h.current = 50;
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const id = world.commands.createEntity(
|
|
179
|
+
entry(Position, (p) => {
|
|
180
|
+
p.x = 0;
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Events
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { createEventKey } from '@draug/engine';
|
|
189
|
+
|
|
190
|
+
const DamageDealt = createEventKey<{ target: number; amount: number }>();
|
|
191
|
+
|
|
192
|
+
const incoming = world.events.getBuffer(DamageDealt);
|
|
193
|
+
incoming.write({ target: 1, amount: 7 });
|
|
194
|
+
|
|
195
|
+
// Normally you only read after the bus swaps at the start of `world.update`.
|
|
196
|
+
world.events.swapAll();
|
|
197
|
+
for (const e of incoming.read()) {
|
|
198
|
+
console.log(e.amount);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Plugins
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { Plugin, PluginBase } from '@draug/engine';
|
|
206
|
+
|
|
207
|
+
@Plugin({
|
|
208
|
+
id: 'audio',
|
|
209
|
+
version: '1.0.0',
|
|
210
|
+
name: 'Audio bootstrap',
|
|
211
|
+
})
|
|
212
|
+
class AudioPlugin extends PluginBase {}
|
|
213
|
+
|
|
214
|
+
world.plugins.install(AudioPlugin /*, ...ctor args if any */);
|
|
215
|
+
world.build(); // builds plugins, then call your own game bootstrap as needed
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
After `world.build()`, resolve instances with `world.plugins.getPluginInstance(AudioPlugin)` (or by string id). Lifecycle hooks (`onPluginLoad`, etc.) live on `PluginBase` for you to call from your game code if you want explicit phases—the engine focuses on construction order and DAG validation.
|
|
219
|
+
|
|
220
|
+
### Game loop
|
|
221
|
+
|
|
222
|
+
`Clock` measures delta time from a `TimeSource`; `GameLoop` invokes your step function and asks the host for the next frame (`requestAnimationFrame`, `queueMicrotask` in tests, etc.).
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { Clock, GameLoop, World } from '@draug/engine';
|
|
226
|
+
|
|
227
|
+
const world = new World();
|
|
228
|
+
const clock = new Clock({ now: () => performance.now() });
|
|
229
|
+
|
|
230
|
+
const loop = new GameLoop(clock, (dt) => {
|
|
231
|
+
world.update(dt);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
loop.start((cb) => {
|
|
235
|
+
requestAnimationFrame(cb);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// loop.stop() when shutting down
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
In Node, `performance.now()` is available on modern versions; otherwise pass `{ now: () => Date.now() }` (millisecond resolution).
|
|
242
|
+
|
|
243
|
+
### Runtime (optional)
|
|
244
|
+
|
|
245
|
+
`Runtime` is a tiny wrapper: `update(dt)` forwards to `world.update(dt)`. In the Amber workspace it is usually constructed together with `@draug/assets`’s `AssetsManager` for loading textures and similar; for a headless server or a toy sim you can ignore `Runtime` and call `world.update` directly from your own driver.
|
|
246
|
+
|
|
247
|
+
## Configuration
|
|
248
|
+
|
|
249
|
+
| Area | What you can tune |
|
|
250
|
+
|------|---------------------|
|
|
251
|
+
| **World** | `new World(maxEntityCount?)` — caps sparse sets / bitmaps (default is large; lower for fixed small games if you care about memory). |
|
|
252
|
+
| **Components** | `components.register(Type, { factory })` for pooled defaults; `ComponentStorageType.SINGLETON_STORAGE` + `factory` for global state. |
|
|
253
|
+
| **Systems** | `@System({ query, requiredComponents, computeAfter })` for selection, extra storages, and ordering. |
|
|
254
|
+
| **Clock / loop** | Inject any `TimeSource`; drive the loop with the scheduling primitive your platform gives you. |
|
|
255
|
+
|
|
256
|
+
## Best practices
|
|
257
|
+
|
|
258
|
+
1. **Register components before first `getStorage`** — registration is explicit; the world will throw if a system touches an unknown component type.
|
|
259
|
+
2. **Treat commands as “end of frame”** — if something must exist in the same system pass, use `addComponent` / `entities.create` directly (or split into a later system).
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
Issues and PRs are welcome in the [GitHub repository](https://github.com/yazmeyaa). If you change public API or query behaviour, add or update a small reproduction so we can turn it into a regression test later (the package is still light on automated tests).
|
|
264
|
+
|
|
265
|
+
## Author & support
|
|
266
|
+
|
|
267
|
+
- **Author:** future_undefined — [GitHub @yazmeyaa](https://github.com/yazmeyaa) · [evgenijantonenkov456@gmail.com](mailto:evgenijantonenkov456@gmail.com)
|
|
268
|
+
|
|
269
|
+
Related workspace packages: `@draug/core` (DAG sort, object pools, etc.) and `@draug/types` (shared `ClassType` helpers). Everything this library exposes to apps is listed in `src/index.ts` in the repository.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
GPL-3.0-only — see [LICENSE](https://www.gnu.org/licenses/gpl-3.0.html) for the full text.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@draug/engine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "future_undefined",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"license": "GPL-3.0-only",
|
|
12
12
|
"readme": "README.md",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@draug/core": "^1.0.
|
|
14
|
+
"@draug/core": "^1.0.2",
|
|
15
15
|
"bitmap-index": "^1.0.9",
|
|
16
16
|
"ts-sparse-set": "^1.0.5"
|
|
17
17
|
},
|