murow 0.0.52 → 0.0.53
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/dist/ecs/component-store.d.ts +20 -35
- package/dist/ecs/component-store.js +78 -75
- package/dist/ecs/world.d.ts +5 -0
- package/dist/ecs/world.js +1 -1
- package/package.json +1 -1
- package/src/ecs/component-store.ts +81 -107
- package/src/ecs/world.ts +7 -1
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { Component } from "./component";
|
|
2
2
|
/**
|
|
3
|
-
* Stores component data
|
|
4
|
-
*
|
|
3
|
+
* Stores component data using separate TypedArrays per field (SoA - Structure of Arrays).
|
|
4
|
+
* Alternative to DataView-based storage for comparison.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
6
|
+
* Tradeoffs:
|
|
7
|
+
* + Faster individual field access (native typed array operations)
|
|
8
|
+
* + Better for column-major access patterns
|
|
9
|
+
* + SIMD-friendly (can vectorize single-field operations)
|
|
10
|
+
* - Worse cache locality for row-major access (whole component reads)
|
|
11
|
+
* - More memory fragmentation (separate arrays)
|
|
12
|
+
* - Slightly higher memory overhead (each TypedArray has its own header)
|
|
11
13
|
*/
|
|
12
14
|
export declare class ComponentStore<T extends object> {
|
|
13
|
-
private
|
|
14
|
-
private view;
|
|
15
|
+
private arrays;
|
|
15
16
|
private stride;
|
|
16
17
|
private component;
|
|
17
18
|
private maxEntities;
|
|
18
19
|
private reusableObject;
|
|
19
|
-
private fieldOffsets;
|
|
20
20
|
private fields;
|
|
21
21
|
private fieldKeys;
|
|
22
22
|
private fieldIndexMap;
|
|
@@ -25,42 +25,22 @@ export declare class ComponentStore<T extends object> {
|
|
|
25
25
|
* Get component data for an entity.
|
|
26
26
|
*
|
|
27
27
|
* ⚠️ IMPORTANT: Returns a REUSED object that is overwritten on the next get() call.
|
|
28
|
-
* Use immediately or copy the data. For safe access, use getMutable() or copyTo().
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* // ✅ CORRECT: Use immediately
|
|
32
|
-
* const t = store.get(entity);
|
|
33
|
-
* console.log(t.x, t.y);
|
|
34
|
-
*
|
|
35
|
-
* // ❌ WRONG: Storing reference
|
|
36
|
-
* const t1 = store.get(entity1);
|
|
37
|
-
* const t2 = store.get(entity2); // t1 is now corrupted!
|
|
38
|
-
*
|
|
39
|
-
* // ✅ CORRECT: Copy if you need multiple
|
|
40
|
-
* const t1 = { ...store.get(entity1) };
|
|
41
|
-
* const t2 = { ...store.get(entity2) };
|
|
42
28
|
*/
|
|
43
29
|
get(entityId: number): Readonly<T>;
|
|
44
30
|
/**
|
|
45
31
|
* Get a mutable copy of component data.
|
|
46
|
-
* Use this when you need to modify and keep the data.
|
|
47
|
-
*
|
|
48
|
-
* Note: This allocates a new object. Use sparingly in hot paths.
|
|
49
32
|
*/
|
|
50
33
|
getMutable(entityId: number): T;
|
|
51
34
|
/**
|
|
52
35
|
* Copy component data into a provided object.
|
|
53
|
-
* Use this when you need to keep multiple components at once.
|
|
54
36
|
*/
|
|
55
37
|
copyTo(entityId: number, target: T): void;
|
|
56
38
|
/**
|
|
57
39
|
* Set component data for an entity.
|
|
58
|
-
* Writes the data directly into the typed array.
|
|
59
40
|
*/
|
|
60
41
|
set(entityId: number, data: T): void;
|
|
61
42
|
/**
|
|
62
|
-
* Update specific fields of a component
|
|
63
|
-
* Optimized to only iterate over the fields being updated.
|
|
43
|
+
* Update specific fields of a component.
|
|
64
44
|
*/
|
|
65
45
|
update(entityId: number, partial: Partial<T>): void;
|
|
66
46
|
/**
|
|
@@ -68,12 +48,17 @@ export declare class ComponentStore<T extends object> {
|
|
|
68
48
|
*/
|
|
69
49
|
clear(entityId: number): void;
|
|
70
50
|
/**
|
|
71
|
-
* Get direct access to the underlying
|
|
72
|
-
* Advanced use only - for SIMD operations,
|
|
51
|
+
* Get direct access to the underlying arrays.
|
|
52
|
+
* Advanced use only - for SIMD operations, batch processing, etc.
|
|
53
|
+
*/
|
|
54
|
+
getRawArrays(): readonly (Float32Array | Int32Array | Uint32Array | Uint16Array | Uint8Array)[];
|
|
55
|
+
/**
|
|
56
|
+
* Get a specific field's array directly.
|
|
57
|
+
* Useful for vectorized operations on a single field across all entities.
|
|
73
58
|
*/
|
|
74
|
-
|
|
59
|
+
getFieldArray(fieldName: keyof T): Float32Array | Int32Array | Uint32Array | Uint16Array | Uint8Array;
|
|
75
60
|
/**
|
|
76
|
-
* Get the stride in bytes.
|
|
61
|
+
* Get the stride in bytes (for compatibility with DataView version).
|
|
77
62
|
*/
|
|
78
63
|
getStride(): number;
|
|
79
64
|
}
|
|
@@ -1,35 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Stores component data
|
|
3
|
-
*
|
|
2
|
+
* Stores component data using separate TypedArrays per field (SoA - Structure of Arrays).
|
|
3
|
+
* Alternative to DataView-based storage for comparison.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
5
|
+
* Tradeoffs:
|
|
6
|
+
* + Faster individual field access (native typed array operations)
|
|
7
|
+
* + Better for column-major access patterns
|
|
8
|
+
* + SIMD-friendly (can vectorize single-field operations)
|
|
9
|
+
* - Worse cache locality for row-major access (whole component reads)
|
|
10
|
+
* - More memory fragmentation (separate arrays)
|
|
11
|
+
* - Slightly higher memory overhead (each TypedArray has its own header)
|
|
10
12
|
*/
|
|
11
13
|
export class ComponentStore {
|
|
12
14
|
constructor(component, maxEntities) {
|
|
13
15
|
this.component = component;
|
|
14
16
|
this.maxEntities = maxEntities;
|
|
15
|
-
// Use exact byte size (no waste like Float32Array)
|
|
16
17
|
this.stride = component.size;
|
|
17
|
-
//
|
|
18
|
-
this.buffer = new ArrayBuffer(maxEntities * this.stride);
|
|
19
|
-
this.view = new DataView(this.buffer);
|
|
20
|
-
// Pre-compute field metadata once
|
|
18
|
+
// Pre-compute field metadata
|
|
21
19
|
this.fieldKeys = component.fieldNames;
|
|
22
|
-
this.fieldOffsets = [];
|
|
23
20
|
this.fields = [];
|
|
24
21
|
this.fieldIndexMap = {};
|
|
25
|
-
|
|
22
|
+
this.arrays = [];
|
|
23
|
+
// Create separate typed array for each field
|
|
26
24
|
for (let i = 0; i < this.fieldKeys.length; i++) {
|
|
27
25
|
const key = this.fieldKeys[i];
|
|
28
26
|
const field = component.schema[key];
|
|
29
|
-
this.fieldOffsets.push(offset);
|
|
30
27
|
this.fields.push(field);
|
|
31
28
|
this.fieldIndexMap[key] = i;
|
|
32
|
-
|
|
29
|
+
// Allocate appropriate typed array based on field type
|
|
30
|
+
switch (field.size) {
|
|
31
|
+
case 4:
|
|
32
|
+
// Could be f32, i32, or u32 - check field type
|
|
33
|
+
if (field.read.toString().includes("getFloat32")) {
|
|
34
|
+
this.arrays.push(new Float32Array(maxEntities));
|
|
35
|
+
}
|
|
36
|
+
else if (field.read.toString().includes("getInt32")) {
|
|
37
|
+
this.arrays.push(new Int32Array(maxEntities));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.arrays.push(new Uint32Array(maxEntities));
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
case 2:
|
|
44
|
+
this.arrays.push(new Uint16Array(maxEntities));
|
|
45
|
+
break;
|
|
46
|
+
case 1:
|
|
47
|
+
this.arrays.push(new Uint8Array(maxEntities));
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
// Fallback to Uint8Array with multiple elements
|
|
51
|
+
this.arrays.push(new Uint8Array(maxEntities * field.size));
|
|
52
|
+
}
|
|
33
53
|
}
|
|
34
54
|
// Create single reusable object
|
|
35
55
|
this.reusableObject = {};
|
|
@@ -41,53 +61,35 @@ export class ComponentStore {
|
|
|
41
61
|
* Get component data for an entity.
|
|
42
62
|
*
|
|
43
63
|
* ⚠️ IMPORTANT: Returns a REUSED object that is overwritten on the next get() call.
|
|
44
|
-
* Use immediately or copy the data. For safe access, use getMutable() or copyTo().
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* // ✅ CORRECT: Use immediately
|
|
48
|
-
* const t = store.get(entity);
|
|
49
|
-
* console.log(t.x, t.y);
|
|
50
|
-
*
|
|
51
|
-
* // ❌ WRONG: Storing reference
|
|
52
|
-
* const t1 = store.get(entity1);
|
|
53
|
-
* const t2 = store.get(entity2); // t1 is now corrupted!
|
|
54
|
-
*
|
|
55
|
-
* // ✅ CORRECT: Copy if you need multiple
|
|
56
|
-
* const t1 = { ...store.get(entity1) };
|
|
57
|
-
* const t2 = { ...store.get(entity2) };
|
|
58
64
|
*/
|
|
59
65
|
get(entityId) {
|
|
60
|
-
const baseOffset = entityId * this.stride;
|
|
61
66
|
const length = this.fields.length;
|
|
62
67
|
// Unrolled loop for common cases
|
|
63
68
|
if (length === 2) {
|
|
64
|
-
this.reusableObject[this.fieldKeys[0]] = this.
|
|
65
|
-
this.reusableObject[this.fieldKeys[1]] = this.
|
|
69
|
+
this.reusableObject[this.fieldKeys[0]] = this.arrays[0][entityId];
|
|
70
|
+
this.reusableObject[this.fieldKeys[1]] = this.arrays[1][entityId];
|
|
66
71
|
}
|
|
67
72
|
else if (length === 3) {
|
|
68
|
-
this.reusableObject[this.fieldKeys[0]] = this.
|
|
69
|
-
this.reusableObject[this.fieldKeys[1]] = this.
|
|
70
|
-
this.reusableObject[this.fieldKeys[2]] = this.
|
|
73
|
+
this.reusableObject[this.fieldKeys[0]] = this.arrays[0][entityId];
|
|
74
|
+
this.reusableObject[this.fieldKeys[1]] = this.arrays[1][entityId];
|
|
75
|
+
this.reusableObject[this.fieldKeys[2]] = this.arrays[2][entityId];
|
|
71
76
|
}
|
|
72
77
|
else if (length === 4) {
|
|
73
|
-
this.reusableObject[this.fieldKeys[0]] = this.
|
|
74
|
-
this.reusableObject[this.fieldKeys[1]] = this.
|
|
75
|
-
this.reusableObject[this.fieldKeys[2]] = this.
|
|
76
|
-
this.reusableObject[this.fieldKeys[3]] = this.
|
|
78
|
+
this.reusableObject[this.fieldKeys[0]] = this.arrays[0][entityId];
|
|
79
|
+
this.reusableObject[this.fieldKeys[1]] = this.arrays[1][entityId];
|
|
80
|
+
this.reusableObject[this.fieldKeys[2]] = this.arrays[2][entityId];
|
|
81
|
+
this.reusableObject[this.fieldKeys[3]] = this.arrays[3][entityId];
|
|
77
82
|
}
|
|
78
83
|
else {
|
|
79
84
|
// Generic loop for other sizes
|
|
80
85
|
for (let i = 0; i < length; i++) {
|
|
81
|
-
this.reusableObject[this.fieldKeys[i]] = this.
|
|
86
|
+
this.reusableObject[this.fieldKeys[i]] = this.arrays[i][entityId];
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
return this.reusableObject;
|
|
85
90
|
}
|
|
86
91
|
/**
|
|
87
92
|
* Get a mutable copy of component data.
|
|
88
|
-
* Use this when you need to modify and keep the data.
|
|
89
|
-
*
|
|
90
|
-
* Note: This allocates a new object. Use sparingly in hot paths.
|
|
91
93
|
*/
|
|
92
94
|
getMutable(entityId) {
|
|
93
95
|
const copy = {};
|
|
@@ -96,94 +98,95 @@ export class ComponentStore {
|
|
|
96
98
|
}
|
|
97
99
|
/**
|
|
98
100
|
* Copy component data into a provided object.
|
|
99
|
-
* Use this when you need to keep multiple components at once.
|
|
100
101
|
*/
|
|
101
102
|
copyTo(entityId, target) {
|
|
102
|
-
const baseOffset = entityId * this.stride;
|
|
103
103
|
for (let i = 0; i < this.fields.length; i++) {
|
|
104
|
-
target[this.fieldKeys[i]] = this.
|
|
104
|
+
target[this.fieldKeys[i]] = this.arrays[i][entityId];
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
108
|
* Set component data for an entity.
|
|
109
|
-
* Writes the data directly into the typed array.
|
|
110
109
|
*/
|
|
111
110
|
set(entityId, data) {
|
|
112
|
-
const baseOffset = entityId * this.stride;
|
|
113
111
|
const length = this.fields.length;
|
|
114
112
|
// Unrolled loop for common cases
|
|
115
113
|
if (length === 2) {
|
|
116
|
-
this.
|
|
117
|
-
this.
|
|
114
|
+
this.arrays[0][entityId] = data[this.fieldKeys[0]];
|
|
115
|
+
this.arrays[1][entityId] = data[this.fieldKeys[1]];
|
|
118
116
|
}
|
|
119
117
|
else if (length === 3) {
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
122
|
-
this.
|
|
118
|
+
this.arrays[0][entityId] = data[this.fieldKeys[0]];
|
|
119
|
+
this.arrays[1][entityId] = data[this.fieldKeys[1]];
|
|
120
|
+
this.arrays[2][entityId] = data[this.fieldKeys[2]];
|
|
123
121
|
}
|
|
124
122
|
else if (length === 4) {
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
123
|
+
this.arrays[0][entityId] = data[this.fieldKeys[0]];
|
|
124
|
+
this.arrays[1][entityId] = data[this.fieldKeys[1]];
|
|
125
|
+
this.arrays[2][entityId] = data[this.fieldKeys[2]];
|
|
126
|
+
this.arrays[3][entityId] = data[this.fieldKeys[3]];
|
|
129
127
|
}
|
|
130
128
|
else {
|
|
131
129
|
// Generic loop for other sizes
|
|
132
130
|
for (let i = 0; i < length; i++) {
|
|
133
|
-
this.
|
|
131
|
+
this.arrays[i][entityId] = data[this.fieldKeys[i]];
|
|
134
132
|
}
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
135
|
/**
|
|
138
|
-
* Update specific fields of a component
|
|
139
|
-
* Optimized to only iterate over the fields being updated.
|
|
136
|
+
* Update specific fields of a component.
|
|
140
137
|
*/
|
|
141
138
|
update(entityId, partial) {
|
|
142
|
-
|
|
143
|
-
// Fast path for single field update (90% of cases) - avoids Object.keys allocation
|
|
139
|
+
// Fast path for single field update
|
|
144
140
|
const keys = Object.keys(partial);
|
|
145
141
|
const keyCount = keys.length;
|
|
146
142
|
if (keyCount === 1) {
|
|
147
143
|
const key = keys[0];
|
|
148
144
|
const i = this.fieldIndexMap[key];
|
|
149
|
-
this.
|
|
145
|
+
this.arrays[i][entityId] = partial[key];
|
|
150
146
|
return;
|
|
151
147
|
}
|
|
152
|
-
// Fast path for two field update
|
|
148
|
+
// Fast path for two field update
|
|
153
149
|
if (keyCount === 2) {
|
|
154
150
|
const key0 = keys[0];
|
|
155
151
|
const key1 = keys[1];
|
|
156
152
|
const i0 = this.fieldIndexMap[key0];
|
|
157
153
|
const i1 = this.fieldIndexMap[key1];
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
154
|
+
this.arrays[i0][entityId] = partial[key0];
|
|
155
|
+
this.arrays[i1][entityId] = partial[key1];
|
|
160
156
|
return;
|
|
161
157
|
}
|
|
162
158
|
// Generic path for multiple fields
|
|
163
159
|
for (let j = 0; j < keyCount; j++) {
|
|
164
160
|
const key = keys[j];
|
|
165
161
|
const i = this.fieldIndexMap[key];
|
|
166
|
-
this.
|
|
162
|
+
this.arrays[i][entityId] = partial[key];
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
165
|
/**
|
|
170
166
|
* Clear component data for an entity (set to default values)
|
|
171
167
|
*/
|
|
172
168
|
clear(entityId) {
|
|
173
|
-
const baseOffset = entityId * this.stride;
|
|
174
169
|
for (let i = 0; i < this.fields.length; i++) {
|
|
175
|
-
this.
|
|
170
|
+
this.arrays[i][entityId] = this.fields[i].toNil();
|
|
176
171
|
}
|
|
177
172
|
}
|
|
178
173
|
/**
|
|
179
|
-
* Get direct access to the underlying
|
|
180
|
-
* Advanced use only - for SIMD operations,
|
|
174
|
+
* Get direct access to the underlying arrays.
|
|
175
|
+
* Advanced use only - for SIMD operations, batch processing, etc.
|
|
176
|
+
*/
|
|
177
|
+
getRawArrays() {
|
|
178
|
+
return this.arrays;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get a specific field's array directly.
|
|
182
|
+
* Useful for vectorized operations on a single field across all entities.
|
|
181
183
|
*/
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
getFieldArray(fieldName) {
|
|
185
|
+
const index = this.fieldIndexMap[fieldName];
|
|
186
|
+
return this.arrays[index];
|
|
184
187
|
}
|
|
185
188
|
/**
|
|
186
|
-
* Get the stride in bytes.
|
|
189
|
+
* Get the stride in bytes (for compatibility with DataView version).
|
|
187
190
|
*/
|
|
188
191
|
getStride() {
|
|
189
192
|
return this.stride;
|
package/dist/ecs/world.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Component } from "./component";
|
|
2
2
|
import { EntityHandle } from "./entity-handle";
|
|
3
|
+
/**
|
|
4
|
+
* Storage backend type for component data
|
|
5
|
+
*/
|
|
6
|
+
export type StorageBackend = "dataview" | "typedarrays";
|
|
3
7
|
/**
|
|
4
8
|
* Configuration for creating a World
|
|
5
9
|
*/
|
|
@@ -61,6 +65,7 @@ export declare class World {
|
|
|
61
65
|
private componentMasks;
|
|
62
66
|
private componentMasks0;
|
|
63
67
|
private numMaskWords;
|
|
68
|
+
private storageBackend;
|
|
64
69
|
private componentMap;
|
|
65
70
|
private components;
|
|
66
71
|
private queryResultBuffers;
|
package/dist/ecs/world.js
CHANGED
|
@@ -82,7 +82,7 @@ export class World {
|
|
|
82
82
|
config.components.forEach((component, index) => {
|
|
83
83
|
this.components.push(component);
|
|
84
84
|
this.componentMap.set(component, index);
|
|
85
|
-
// Create component store with
|
|
85
|
+
// Create component store with selected backend
|
|
86
86
|
const store = new ComponentStore(component, this.maxEntities);
|
|
87
87
|
this.componentStoresArray[index] = store;
|
|
88
88
|
});
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { Component } from "./component";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Stores component data
|
|
5
|
-
*
|
|
4
|
+
* Stores component data using separate TypedArrays per field (SoA - Structure of Arrays).
|
|
5
|
+
* Alternative to DataView-based storage for comparison.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
7
|
+
* Tradeoffs:
|
|
8
|
+
* + Faster individual field access (native typed array operations)
|
|
9
|
+
* + Better for column-major access patterns
|
|
10
|
+
* + SIMD-friendly (can vectorize single-field operations)
|
|
11
|
+
* - Worse cache locality for row-major access (whole component reads)
|
|
12
|
+
* - More memory fragmentation (separate arrays)
|
|
13
|
+
* - Slightly higher memory overhead (each TypedArray has its own header)
|
|
12
14
|
*/
|
|
13
15
|
export class ComponentStore<T extends object> {
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private stride: number; // Component size in bytes
|
|
16
|
+
private arrays: (Float32Array | Int32Array | Uint32Array | Uint16Array | Uint8Array)[];
|
|
17
|
+
private stride: number; // Component size in bytes (for compatibility)
|
|
17
18
|
private component: Component<T>;
|
|
18
19
|
private maxEntities: number;
|
|
19
20
|
|
|
@@ -21,36 +22,50 @@ export class ComponentStore<T extends object> {
|
|
|
21
22
|
private reusableObject: T;
|
|
22
23
|
|
|
23
24
|
// Pre-computed field metadata for fast access
|
|
24
|
-
private fieldOffsets: number[];
|
|
25
25
|
private fields: any[];
|
|
26
26
|
private fieldKeys: (keyof T)[];
|
|
27
|
-
private fieldIndexMap: Record<string, number>;
|
|
27
|
+
private fieldIndexMap: Record<string, number>;
|
|
28
28
|
|
|
29
29
|
constructor(component: Component<T>, maxEntities: number) {
|
|
30
30
|
this.component = component;
|
|
31
31
|
this.maxEntities = maxEntities;
|
|
32
|
-
|
|
33
|
-
// Use exact byte size (no waste like Float32Array)
|
|
34
32
|
this.stride = component.size;
|
|
35
33
|
|
|
36
|
-
//
|
|
37
|
-
this.buffer = new ArrayBuffer(maxEntities * this.stride);
|
|
38
|
-
this.view = new DataView(this.buffer);
|
|
39
|
-
|
|
40
|
-
// Pre-compute field metadata once
|
|
34
|
+
// Pre-compute field metadata
|
|
41
35
|
this.fieldKeys = component.fieldNames;
|
|
42
|
-
this.fieldOffsets = [];
|
|
43
36
|
this.fields = [];
|
|
44
37
|
this.fieldIndexMap = {};
|
|
38
|
+
this.arrays = [];
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
// Create separate typed array for each field
|
|
47
41
|
for (let i = 0; i < this.fieldKeys.length; i++) {
|
|
48
42
|
const key = this.fieldKeys[i];
|
|
49
43
|
const field = component.schema[key];
|
|
50
|
-
this.fieldOffsets.push(offset);
|
|
51
44
|
this.fields.push(field);
|
|
52
45
|
this.fieldIndexMap[key as string] = i;
|
|
53
|
-
|
|
46
|
+
|
|
47
|
+
// Allocate appropriate typed array based on field type
|
|
48
|
+
switch (field.size) {
|
|
49
|
+
case 4:
|
|
50
|
+
// Could be f32, i32, or u32 - check field type
|
|
51
|
+
if (field.read.toString().includes("getFloat32")) {
|
|
52
|
+
this.arrays.push(new Float32Array(maxEntities));
|
|
53
|
+
} else if (field.read.toString().includes("getInt32")) {
|
|
54
|
+
this.arrays.push(new Int32Array(maxEntities));
|
|
55
|
+
} else {
|
|
56
|
+
this.arrays.push(new Uint32Array(maxEntities));
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case 2:
|
|
60
|
+
this.arrays.push(new Uint16Array(maxEntities));
|
|
61
|
+
break;
|
|
62
|
+
case 1:
|
|
63
|
+
this.arrays.push(new Uint8Array(maxEntities));
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
// Fallback to Uint8Array with multiple elements
|
|
67
|
+
this.arrays.push(new Uint8Array(maxEntities * field.size));
|
|
68
|
+
}
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
// Create single reusable object
|
|
@@ -64,45 +79,27 @@ export class ComponentStore<T extends object> {
|
|
|
64
79
|
* Get component data for an entity.
|
|
65
80
|
*
|
|
66
81
|
* ⚠️ IMPORTANT: Returns a REUSED object that is overwritten on the next get() call.
|
|
67
|
-
* Use immediately or copy the data. For safe access, use getMutable() or copyTo().
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* // ✅ CORRECT: Use immediately
|
|
71
|
-
* const t = store.get(entity);
|
|
72
|
-
* console.log(t.x, t.y);
|
|
73
|
-
*
|
|
74
|
-
* // ❌ WRONG: Storing reference
|
|
75
|
-
* const t1 = store.get(entity1);
|
|
76
|
-
* const t2 = store.get(entity2); // t1 is now corrupted!
|
|
77
|
-
*
|
|
78
|
-
* // ✅ CORRECT: Copy if you need multiple
|
|
79
|
-
* const t1 = { ...store.get(entity1) };
|
|
80
|
-
* const t2 = { ...store.get(entity2) };
|
|
81
82
|
*/
|
|
82
83
|
get(entityId: number): Readonly<T> {
|
|
83
|
-
const baseOffset = entityId * this.stride;
|
|
84
84
|
const length = this.fields.length;
|
|
85
85
|
|
|
86
86
|
// Unrolled loop for common cases
|
|
87
87
|
if (length === 2) {
|
|
88
|
-
this.reusableObject[this.fieldKeys[0]] = this.
|
|
89
|
-
this.reusableObject[this.fieldKeys[1]] = this.
|
|
88
|
+
this.reusableObject[this.fieldKeys[0]] = this.arrays[0][entityId] as any;
|
|
89
|
+
this.reusableObject[this.fieldKeys[1]] = this.arrays[1][entityId] as any;
|
|
90
90
|
} else if (length === 3) {
|
|
91
|
-
this.reusableObject[this.fieldKeys[0]] = this.
|
|
92
|
-
this.reusableObject[this.fieldKeys[1]] = this.
|
|
93
|
-
this.reusableObject[this.fieldKeys[2]] = this.
|
|
91
|
+
this.reusableObject[this.fieldKeys[0]] = this.arrays[0][entityId] as any;
|
|
92
|
+
this.reusableObject[this.fieldKeys[1]] = this.arrays[1][entityId] as any;
|
|
93
|
+
this.reusableObject[this.fieldKeys[2]] = this.arrays[2][entityId] as any;
|
|
94
94
|
} else if (length === 4) {
|
|
95
|
-
this.reusableObject[this.fieldKeys[0]] = this.
|
|
96
|
-
this.reusableObject[this.fieldKeys[1]] = this.
|
|
97
|
-
this.reusableObject[this.fieldKeys[2]] = this.
|
|
98
|
-
this.reusableObject[this.fieldKeys[3]] = this.
|
|
95
|
+
this.reusableObject[this.fieldKeys[0]] = this.arrays[0][entityId] as any;
|
|
96
|
+
this.reusableObject[this.fieldKeys[1]] = this.arrays[1][entityId] as any;
|
|
97
|
+
this.reusableObject[this.fieldKeys[2]] = this.arrays[2][entityId] as any;
|
|
98
|
+
this.reusableObject[this.fieldKeys[3]] = this.arrays[3][entityId] as any;
|
|
99
99
|
} else {
|
|
100
100
|
// Generic loop for other sizes
|
|
101
101
|
for (let i = 0; i < length; i++) {
|
|
102
|
-
this.reusableObject[this.fieldKeys[i]] = this.
|
|
103
|
-
this.view,
|
|
104
|
-
baseOffset + this.fieldOffsets[i]
|
|
105
|
-
);
|
|
102
|
+
this.reusableObject[this.fieldKeys[i]] = this.arrays[i][entityId] as any;
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
|
|
@@ -111,9 +108,6 @@ export class ComponentStore<T extends object> {
|
|
|
111
108
|
|
|
112
109
|
/**
|
|
113
110
|
* Get a mutable copy of component data.
|
|
114
|
-
* Use this when you need to modify and keep the data.
|
|
115
|
-
*
|
|
116
|
-
* Note: This allocates a new object. Use sparingly in hot paths.
|
|
117
111
|
*/
|
|
118
112
|
getMutable(entityId: number): T {
|
|
119
113
|
const copy = {} as T;
|
|
@@ -123,82 +117,63 @@ export class ComponentStore<T extends object> {
|
|
|
123
117
|
|
|
124
118
|
/**
|
|
125
119
|
* Copy component data into a provided object.
|
|
126
|
-
* Use this when you need to keep multiple components at once.
|
|
127
120
|
*/
|
|
128
121
|
copyTo(entityId: number, target: T): void {
|
|
129
|
-
const baseOffset = entityId * this.stride;
|
|
130
|
-
|
|
131
122
|
for (let i = 0; i < this.fields.length; i++) {
|
|
132
|
-
target[this.fieldKeys[i]] = this.
|
|
133
|
-
this.view,
|
|
134
|
-
baseOffset + this.fieldOffsets[i]
|
|
135
|
-
);
|
|
123
|
+
target[this.fieldKeys[i]] = this.arrays[i][entityId] as any;
|
|
136
124
|
}
|
|
137
125
|
}
|
|
138
126
|
|
|
139
127
|
/**
|
|
140
128
|
* Set component data for an entity.
|
|
141
|
-
* Writes the data directly into the typed array.
|
|
142
129
|
*/
|
|
143
130
|
set(entityId: number, data: T): void {
|
|
144
|
-
const baseOffset = entityId * this.stride;
|
|
145
131
|
const length = this.fields.length;
|
|
146
132
|
|
|
147
133
|
// Unrolled loop for common cases
|
|
148
134
|
if (length === 2) {
|
|
149
|
-
this.
|
|
150
|
-
this.
|
|
135
|
+
this.arrays[0][entityId] = data[this.fieldKeys[0]] as any;
|
|
136
|
+
this.arrays[1][entityId] = data[this.fieldKeys[1]] as any;
|
|
151
137
|
} else if (length === 3) {
|
|
152
|
-
this.
|
|
153
|
-
this.
|
|
154
|
-
this.
|
|
138
|
+
this.arrays[0][entityId] = data[this.fieldKeys[0]] as any;
|
|
139
|
+
this.arrays[1][entityId] = data[this.fieldKeys[1]] as any;
|
|
140
|
+
this.arrays[2][entityId] = data[this.fieldKeys[2]] as any;
|
|
155
141
|
} else if (length === 4) {
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
142
|
+
this.arrays[0][entityId] = data[this.fieldKeys[0]] as any;
|
|
143
|
+
this.arrays[1][entityId] = data[this.fieldKeys[1]] as any;
|
|
144
|
+
this.arrays[2][entityId] = data[this.fieldKeys[2]] as any;
|
|
145
|
+
this.arrays[3][entityId] = data[this.fieldKeys[3]] as any;
|
|
160
146
|
} else {
|
|
161
147
|
// Generic loop for other sizes
|
|
162
148
|
for (let i = 0; i < length; i++) {
|
|
163
|
-
this.
|
|
164
|
-
this.view,
|
|
165
|
-
baseOffset + this.fieldOffsets[i],
|
|
166
|
-
data[this.fieldKeys[i]]
|
|
167
|
-
);
|
|
149
|
+
this.arrays[i][entityId] = data[this.fieldKeys[i]] as any;
|
|
168
150
|
}
|
|
169
151
|
}
|
|
170
152
|
}
|
|
171
153
|
|
|
172
154
|
/**
|
|
173
|
-
* Update specific fields of a component
|
|
174
|
-
* Optimized to only iterate over the fields being updated.
|
|
155
|
+
* Update specific fields of a component.
|
|
175
156
|
*/
|
|
176
157
|
update(entityId: number, partial: Partial<T>): void {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// Fast path for single field update (90% of cases) - avoids Object.keys allocation
|
|
158
|
+
// Fast path for single field update
|
|
180
159
|
const keys = Object.keys(partial) as (keyof T)[];
|
|
181
160
|
const keyCount = keys.length;
|
|
182
161
|
|
|
183
162
|
if (keyCount === 1) {
|
|
184
163
|
const key = keys[0];
|
|
185
164
|
const i = this.fieldIndexMap[key as string];
|
|
186
|
-
this.
|
|
187
|
-
this.view,
|
|
188
|
-
baseOffset + this.fieldOffsets[i],
|
|
189
|
-
partial[key]!
|
|
190
|
-
);
|
|
165
|
+
this.arrays[i][entityId] = partial[key] as any;
|
|
191
166
|
return;
|
|
192
167
|
}
|
|
193
168
|
|
|
194
|
-
// Fast path for two field update
|
|
169
|
+
// Fast path for two field update
|
|
195
170
|
if (keyCount === 2) {
|
|
196
171
|
const key0 = keys[0];
|
|
197
172
|
const key1 = keys[1];
|
|
198
173
|
const i0 = this.fieldIndexMap[key0 as string];
|
|
199
174
|
const i1 = this.fieldIndexMap[key1 as string];
|
|
200
|
-
this.
|
|
201
|
-
this.
|
|
175
|
+
this.arrays[i0][entityId] = partial[key0] as any;
|
|
176
|
+
this.arrays[i1][entityId] = partial[key1] as any;
|
|
202
177
|
return;
|
|
203
178
|
}
|
|
204
179
|
|
|
@@ -206,11 +181,7 @@ export class ComponentStore<T extends object> {
|
|
|
206
181
|
for (let j = 0; j < keyCount; j++) {
|
|
207
182
|
const key = keys[j];
|
|
208
183
|
const i = this.fieldIndexMap[key as string];
|
|
209
|
-
this.
|
|
210
|
-
this.view,
|
|
211
|
-
baseOffset + this.fieldOffsets[i],
|
|
212
|
-
partial[key]!
|
|
213
|
-
);
|
|
184
|
+
this.arrays[i][entityId] = partial[key] as any;
|
|
214
185
|
}
|
|
215
186
|
}
|
|
216
187
|
|
|
@@ -218,27 +189,30 @@ export class ComponentStore<T extends object> {
|
|
|
218
189
|
* Clear component data for an entity (set to default values)
|
|
219
190
|
*/
|
|
220
191
|
clear(entityId: number): void {
|
|
221
|
-
const baseOffset = entityId * this.stride;
|
|
222
|
-
|
|
223
192
|
for (let i = 0; i < this.fields.length; i++) {
|
|
224
|
-
this.fields[i].
|
|
225
|
-
this.view,
|
|
226
|
-
baseOffset + this.fieldOffsets[i],
|
|
227
|
-
this.fields[i].toNil()
|
|
228
|
-
);
|
|
193
|
+
this.arrays[i][entityId] = this.fields[i].toNil();
|
|
229
194
|
}
|
|
230
195
|
}
|
|
231
196
|
|
|
232
197
|
/**
|
|
233
|
-
* Get direct access to the underlying
|
|
234
|
-
* Advanced use only - for SIMD operations,
|
|
198
|
+
* Get direct access to the underlying arrays.
|
|
199
|
+
* Advanced use only - for SIMD operations, batch processing, etc.
|
|
200
|
+
*/
|
|
201
|
+
getRawArrays(): readonly (Float32Array | Int32Array | Uint32Array | Uint16Array | Uint8Array)[] {
|
|
202
|
+
return this.arrays;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get a specific field's array directly.
|
|
207
|
+
* Useful for vectorized operations on a single field across all entities.
|
|
235
208
|
*/
|
|
236
|
-
|
|
237
|
-
|
|
209
|
+
getFieldArray(fieldName: keyof T): Float32Array | Int32Array | Uint32Array | Uint16Array | Uint8Array {
|
|
210
|
+
const index = this.fieldIndexMap[fieldName as string];
|
|
211
|
+
return this.arrays[index];
|
|
238
212
|
}
|
|
239
213
|
|
|
240
214
|
/**
|
|
241
|
-
* Get the stride in bytes.
|
|
215
|
+
* Get the stride in bytes (for compatibility with DataView version).
|
|
242
216
|
*/
|
|
243
217
|
getStride(): number {
|
|
244
218
|
return this.stride;
|
package/src/ecs/world.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { Component } from "./component";
|
|
|
3
3
|
import { ComponentStore } from "./component-store";
|
|
4
4
|
import { EntityHandle } from "./entity-handle";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Storage backend type for component data
|
|
8
|
+
*/
|
|
9
|
+
export type StorageBackend = "dataview" | "typedarrays";
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* Configuration for creating a World
|
|
8
13
|
*/
|
|
@@ -73,6 +78,7 @@ export class World {
|
|
|
73
78
|
private componentMasks: Uint32Array[]; // Dynamic array of bitmask words (32 components per word)
|
|
74
79
|
private componentMasks0!: Uint32Array; // Fast path: cached reference to first word (most common case)
|
|
75
80
|
private numMaskWords: number = 0; // Number of allocated mask words
|
|
81
|
+
private storageBackend: StorageBackend;
|
|
76
82
|
|
|
77
83
|
// Component registry (Map only for initial lookup)
|
|
78
84
|
private componentMap: Map<Component<any>, number> = new Map();
|
|
@@ -127,7 +133,7 @@ export class World {
|
|
|
127
133
|
this.components.push(component);
|
|
128
134
|
this.componentMap.set(component, index);
|
|
129
135
|
|
|
130
|
-
// Create component store with
|
|
136
|
+
// Create component store with selected backend
|
|
131
137
|
const store = new ComponentStore(component, this.maxEntities);
|
|
132
138
|
this.componentStoresArray[index] = store;
|
|
133
139
|
});
|