@vworlds/vecs 1.0.61 → 1.0.62
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 +4 -1
- package/dist/component.d.ts +64 -3
- package/dist/component.js +84 -0
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +21 -1
- package/dist/component_meta.js +34 -1
- package/dist/component_meta.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/reflection/component_members.d.ts +1 -0
- package/dist/reflection/component_members.js +42 -0
- package/dist/reflection/component_members.js.map +1 -0
- package/dist/reflection/decorators.d.ts +31 -0
- package/dist/reflection/decorators.js +129 -0
- package/dist/reflection/decorators.js.map +1 -0
- package/dist/reflection/defaults.d.ts +4 -0
- package/dist/reflection/defaults.js +149 -0
- package/dist/reflection/defaults.js.map +1 -0
- package/dist/reflection/member_types.d.ts +32 -0
- package/dist/reflection/member_types.js +86 -0
- package/dist/reflection/member_types.js.map +1 -0
- package/dist/reflection/vecs_struct.d.ts +31 -0
- package/dist/reflection/vecs_struct.js +259 -0
- package/dist/reflection/vecs_struct.js.map +1 -0
- package/dist/world/world.components.d.ts +52 -1
- package/dist/world/world.components.js +124 -1
- package/dist/world/world.components.js.map +1 -1
- package/docs/README.md +3 -0
- package/docs/components.md +14 -5
- package/docs/diagrams.md +13 -0
- package/docs/reflection.md +251 -0
- package/package.json +1 -1
- package/tsconfig.types.json +2 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Component reflection
|
|
2
|
+
|
|
3
|
+
Declare the types and metadata of a component's members, read them back through `VecsStruct`, and
|
|
4
|
+
check or fix component data with `validate` and `normalize` hooks.
|
|
5
|
+
|
|
6
|
+
## Concept
|
|
7
|
+
|
|
8
|
+
A component class is plain data, so the ECS cannot tell an `i32` from an `f64` or a meter from a
|
|
9
|
+
degree by looking at it. Reflection lets you declare it once and then use it anywhere: an editor or
|
|
10
|
+
inspector, a generated UI, a schema, or a check on data that arrives from outside.
|
|
11
|
+
|
|
12
|
+
- **Member types** name what each field holds: a scalar, a nested component class, or an array.
|
|
13
|
+
- **`VecsStruct`** is a core component holding the declared members. It sits on the class's
|
|
14
|
+
**component entity**, so you query it like any other component.
|
|
15
|
+
- **`validate` and `normalize` hooks** check a component instance or fix it in place. They run only
|
|
16
|
+
when you call `world.validate`, `world.isValid` or `world.normalize`. Nothing runs them on `set`,
|
|
17
|
+
so declaring metadata adds no cost per mutation.
|
|
18
|
+
|
|
19
|
+
A class without declarations registers and behaves exactly as before. Its component entity does not
|
|
20
|
+
carry a `VecsStruct`.
|
|
21
|
+
|
|
22
|
+
## Declaring members
|
|
23
|
+
|
|
24
|
+
You can declare members in three equivalent forms. They all produce the same definition.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { description, range, type, unit, World } from "@vworlds/vecs";
|
|
28
|
+
|
|
29
|
+
// Decorators, read by world.component(Position).
|
|
30
|
+
class Position {
|
|
31
|
+
@type("i32") @range(-100, 100) @unit("m") @description("The X coordinate") x = 0;
|
|
32
|
+
@type("i32") @range(-100, 100) y = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fluent, on the component entity.
|
|
36
|
+
class Velocity {
|
|
37
|
+
dx = 0;
|
|
38
|
+
dy = 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const world = new World();
|
|
42
|
+
world.component(Position);
|
|
43
|
+
world
|
|
44
|
+
.component(Velocity)
|
|
45
|
+
.member("dx", "f32")
|
|
46
|
+
.range(-10, 10)
|
|
47
|
+
.unit("m/s")
|
|
48
|
+
.member("dy", "f32")
|
|
49
|
+
.range(-10, 10)
|
|
50
|
+
.unit("m/s");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The fluent form follows Flecs: `.member(name, type)` returns the `Component` itself, and each
|
|
54
|
+
modifier (`.range`, `.description`, `.unit`, `.values`) applies to the member most recently
|
|
55
|
+
declared with `.member` on that component entity, in that world. Because the chain stays on the
|
|
56
|
+
`Component`, it can carry on with `.on(…)` or any other component method:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
class Score {
|
|
60
|
+
value = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const score = world
|
|
64
|
+
.component(Score)
|
|
65
|
+
.member("value", "u32")
|
|
66
|
+
.description("Points scored")
|
|
67
|
+
.on("set", (entity, s) => console.log(entity.eid, s.value));
|
|
68
|
+
score === world.component(Score); // true
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A modifier called before any `.member` on that component throws, telling you to call
|
|
72
|
+
`.member(name, type)` first.
|
|
73
|
+
|
|
74
|
+
You can also mix the forms. A fluent call that repeats a decorated member with the same type adds
|
|
75
|
+
the fields it is missing:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
class Heading {
|
|
79
|
+
@type("f32") @unit("deg") angle = 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
world.component(Heading).member("angle", "f32").range(0, 360);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Decorators work in both legacy (`experimentalDecorators`) and standard (TC39) mode. In standard mode
|
|
86
|
+
they store their declarations in the class's decorator metadata. Importing `@vworlds/vecs` defines
|
|
87
|
+
`Symbol.metadata` when the runtime lacks it, so `world.component(C)` reads the declarations without
|
|
88
|
+
constructing an instance.
|
|
89
|
+
|
|
90
|
+
Every member that carries metadata needs a type. A `@range` without a `@type` throws when you
|
|
91
|
+
register the class.
|
|
92
|
+
|
|
93
|
+
## Reading the definition
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { VecsStruct } from "@vworlds/vecs";
|
|
97
|
+
|
|
98
|
+
const struct = world.component(Position).get(VecsStruct);
|
|
99
|
+
struct?.members.x; // { type: "i32", range: [-100, 100], unit: "m", description: "The X coordinate" }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Members are inherited along the class chain, however the base class declared them: a subclass of a
|
|
103
|
+
class with fluent declarations gets the same members as a subclass of a decorated one. They are
|
|
104
|
+
listed base class first, then in declaration order. A subclass may repeat an inherited member to add
|
|
105
|
+
fields, such as a `unit`, but not to change one.
|
|
106
|
+
|
|
107
|
+
The definition is **per class, for the whole process**:
|
|
108
|
+
|
|
109
|
+
- The first registration builds it. Later declarations, in any world, merge into it.
|
|
110
|
+
- A declaration that repeats the definition is a no-op. That makes it safe to run the same
|
|
111
|
+
registration code for every world.
|
|
112
|
+
- A declaration that contradicts it (a different type, range, unit, description or set of values)
|
|
113
|
+
throws, naming the class and the member. That includes a base class declaration contradicting a
|
|
114
|
+
subclass that repeated the member.
|
|
115
|
+
- Every world attaches the **same frozen instance**. `getMut(VecsStruct, …)` throws on any write.
|
|
116
|
+
A later declaration, on the class or a base class, is visible through that instance in every
|
|
117
|
+
world. The world that makes the declaration attaches or marks modified the `VecsStruct` of the
|
|
118
|
+
class and of its registered subclasses.
|
|
119
|
+
|
|
120
|
+
`VecsStruct` is registered in a world only when the world first attaches one, so worlds without
|
|
121
|
+
declarations keep their component ids. Look it up with `world.getEntity(VecsStruct)`, because
|
|
122
|
+
`entity.get(VecsStruct)` throws in a world that has never registered it. Like `ComponentDef`, it has
|
|
123
|
+
the `DontSnapshot` policy.
|
|
124
|
+
|
|
125
|
+
## Validating and normalizing
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const position = Object.assign(new Position(), { x: 150.5, y: 3 });
|
|
129
|
+
|
|
130
|
+
world.validate(position); // ["x: 150.5 is not an integer"]
|
|
131
|
+
world.isValid(position); // false
|
|
132
|
+
world.normalize(position); // x becomes 100: truncated, then clamped
|
|
133
|
+
world.isValid(position); // true
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
All three methods resolve the class from `component.constructor`. They throw when that class is not
|
|
137
|
+
registered in the world.
|
|
138
|
+
|
|
139
|
+
### Default hooks
|
|
140
|
+
|
|
141
|
+
When a class declares ranges or integer types and has no explicit hook, the first call to
|
|
142
|
+
`world.validate` or `world.normalize` creates a default hook from the definition:
|
|
143
|
+
|
|
144
|
+
- `validate` checks each such member against its range and its type's bounds, and checks that
|
|
145
|
+
integer values are whole. For example, `u8` is 0–255. An `enum` member with `values` must hold
|
|
146
|
+
one of them. It returns every violation.
|
|
147
|
+
- `normalize` truncates integers, then clamps each such member to its range and its type's bounds.
|
|
148
|
+
`NaN` becomes `0` first. It leaves a value of the wrong JavaScript type, or an `enum` value outside
|
|
149
|
+
its `values`, unchanged, since neither has a sensible fix; `validate` reports both.
|
|
150
|
+
- `u64` and `i64` hold `bigint` values. `normalize` converts a finite `number` to a `bigint`.
|
|
151
|
+
- For an array member, the checks apply to each element.
|
|
152
|
+
- Floats are checked against their range only. A `string`, `bool` or nested member without a range
|
|
153
|
+
is not checked.
|
|
154
|
+
|
|
155
|
+
The default derives its checks from the current definition on every call, so it covers members
|
|
156
|
+
declared later, in any world or on a base class. It stays until you register a hook explicitly, and
|
|
157
|
+
the first explicit hook replaces it.
|
|
158
|
+
|
|
159
|
+
### Explicit hooks
|
|
160
|
+
|
|
161
|
+
Register explicit hooks with `on`, like the lifecycle hooks:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
class Wallet {
|
|
165
|
+
coins = 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
world
|
|
169
|
+
.component(Wallet)
|
|
170
|
+
.on("validate", (wallet) => (wallet.coins >= 0 ? true : "coins must not be negative"))
|
|
171
|
+
.on("normalize", (wallet) => {
|
|
172
|
+
wallet.coins = Math.max(0, Math.floor(wallet.coins));
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
You can also declare them as static methods on the class (see [`vecs_` statics](#vecs_-statics)).
|
|
177
|
+
|
|
178
|
+
Several hooks for the same event run in registration order. `validate` returns the first failure.
|
|
179
|
+
`normalize` hooks chain, each one working on the result of the previous one.
|
|
180
|
+
|
|
181
|
+
## `vecs_` statics
|
|
182
|
+
|
|
183
|
+
`world.component(C)` registers any static `vecs_<event>` method on `C` as a hook for `<event>`. These
|
|
184
|
+
hooks run ahead of the hooks you add with `.on(…)`, and they cover every public hook event:
|
|
185
|
+
|
|
186
|
+
| Static | Event | Signature |
|
|
187
|
+
| ------------------- | -------------- | ------------------------------------------- |
|
|
188
|
+
| `vecs_add` | `add` | `(entity, component) => void` |
|
|
189
|
+
| `vecs_set` | `set` | `(entity, component) => void` |
|
|
190
|
+
| `vecs_remove` | `remove` | `(entity, component) => void` |
|
|
191
|
+
| `vecs_targetAdd` | `targetAdd` | `(source, relationship, target) => void` |
|
|
192
|
+
| `vecs_targetRemove` | `targetRemove` | `(source, relationship, target) => void` |
|
|
193
|
+
| `vecs_validate` | `validate` | `(component) => true \| string \| string[]` |
|
|
194
|
+
| `vecs_normalize` | `normalize` | `(component) => void` |
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
class Health {
|
|
198
|
+
@type("u16") hp = 100;
|
|
199
|
+
|
|
200
|
+
static vecs_normalize(health: Health): void {
|
|
201
|
+
health.hp = Math.min(Math.max(Math.round(health.hp), 0), 1000);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Statics are called with the class as `this`, and component instances never store closures.
|
|
207
|
+
`vecs_targetAdd` and `vecs_targetRemove` require a [relationship](./relationships.md) class.
|
|
208
|
+
Registering one on any other class throws. Like any explicit hook, `vecs_validate` and
|
|
209
|
+
`vecs_normalize` replace the default.
|
|
210
|
+
|
|
211
|
+
## Reference
|
|
212
|
+
|
|
213
|
+
### Member types (`lib/vecs/src/reflection/member_types.ts`)
|
|
214
|
+
|
|
215
|
+
| Type | Values |
|
|
216
|
+
| -------------------------------------- | ---------------------------------------------------- |
|
|
217
|
+
| `bool` | `boolean` |
|
|
218
|
+
| `u8`, `i8`, `u16`, `i16`, `u32`, `i32` | Integer `number` within the type's bounds |
|
|
219
|
+
| `u64`, `i64` | `bigint` within the type's bounds |
|
|
220
|
+
| `enum` | `i32`, optionally restricted with `values([...])` |
|
|
221
|
+
| `f32`, `f64` | `number` |
|
|
222
|
+
| `string`, `bytes`, `any`, `entity` | String, `Uint8Array`, any value, an entity reference |
|
|
223
|
+
| A component class | A nested instance of that class |
|
|
224
|
+
| `[type]` | An array of the element type |
|
|
225
|
+
|
|
226
|
+
The names match the `@wireType` vocabulary of `@vworlds/vecs-wire`.
|
|
227
|
+
|
|
228
|
+
### Declarations
|
|
229
|
+
|
|
230
|
+
| Decorator | Fluent | Member field |
|
|
231
|
+
| -------------------- | ------------------------------------ | ------------- |
|
|
232
|
+
| `@type(memberType)` | `component.member(name, memberType)` | `type` |
|
|
233
|
+
| `@range(min, max)` | `.range(min, max)` | `range` |
|
|
234
|
+
| `@description(text)` | `.description(text)` | `description` |
|
|
235
|
+
| `@unit(text)` | `.unit(text)` | `unit` |
|
|
236
|
+
| `@values([...])` | `.values([...])` | `values` |
|
|
237
|
+
|
|
238
|
+
Each fluent call returns the `Component`, and each modifier applies to the member last declared
|
|
239
|
+
with `.member` on it. A `range` needs a numeric type and is inclusive. For `u64` and `i64` it is stored as `bigint`.
|
|
240
|
+
`values` needs an `enum` type.
|
|
241
|
+
|
|
242
|
+
### `World` reflection methods (`lib/vecs/src/world/world.components.ts`)
|
|
243
|
+
|
|
244
|
+
| Method | Returns |
|
|
245
|
+
| ---------------------- | -------------------------------------------------------- |
|
|
246
|
+
| `validate(component)` | `true`, or the violations: a message or a list of them. |
|
|
247
|
+
| `isValid(component)` | `validate(component) === true`. |
|
|
248
|
+
| `normalize(component)` | `component`, modified in place by the `normalize` hooks. |
|
|
249
|
+
|
|
250
|
+
See [Components — hooks](./components.md#hooks-component-and-relationship-edges) for the lifecycle
|
|
251
|
+
events and [ECS design diagrams](./diagrams.md#member-types) for drawing member types.
|
package/package.json
CHANGED