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.
@@ -1,22 +1,22 @@
1
1
  import { Component } from "./component";
2
2
  /**
3
- * Stores component data for entities using typed arrays.
4
- * Provides efficient packed storage with O(1) access by entity ID.
3
+ * Stores component data using separate TypedArrays per field (SoA - Structure of Arrays).
4
+ * Alternative to DataView-based storage for comparison.
5
5
  *
6
- * Key optimizations:
7
- * - ArrayBuffer storage (exact byte size, no waste)
8
- * - Single reusable DataView (no allocations)
9
- * - Single reusable object for get() (zero allocations)
10
- * - Pre-computed field offsets (no runtime calculations)
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 buffer;
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 without reading the whole component first.
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 buffer.
72
- * Advanced use only - for SIMD operations, GPU uploads, zero-copy networking, etc.
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
- getRawBuffer(): ArrayBuffer;
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 for entities using typed arrays.
3
- * Provides efficient packed storage with O(1) access by entity ID.
2
+ * Stores component data using separate TypedArrays per field (SoA - Structure of Arrays).
3
+ * Alternative to DataView-based storage for comparison.
4
4
  *
5
- * Key optimizations:
6
- * - ArrayBuffer storage (exact byte size, no waste)
7
- * - Single reusable DataView (no allocations)
8
- * - Single reusable object for get() (zero allocations)
9
- * - Pre-computed field offsets (no runtime calculations)
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
- // Allocate exact memory needed
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
- let offset = 0;
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
- offset += field.size;
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.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
65
- this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
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.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
69
- this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
70
- this.reusableObject[this.fieldKeys[2]] = this.fields[2].read(this.view, baseOffset + this.fieldOffsets[2]);
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.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
74
- this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
75
- this.reusableObject[this.fieldKeys[2]] = this.fields[2].read(this.view, baseOffset + this.fieldOffsets[2]);
76
- this.reusableObject[this.fieldKeys[3]] = this.fields[3].read(this.view, baseOffset + this.fieldOffsets[3]);
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.fields[i].read(this.view, baseOffset + this.fieldOffsets[i]);
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.fields[i].read(this.view, baseOffset + this.fieldOffsets[i]);
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.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
117
- this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
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.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
121
- this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
122
- this.fields[2].write(this.view, baseOffset + this.fieldOffsets[2], data[this.fieldKeys[2]]);
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.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
126
- this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
127
- this.fields[2].write(this.view, baseOffset + this.fieldOffsets[2], data[this.fieldKeys[2]]);
128
- this.fields[3].write(this.view, baseOffset + this.fieldOffsets[3], data[this.fieldKeys[3]]);
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.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], data[this.fieldKeys[i]]);
131
+ this.arrays[i][entityId] = data[this.fieldKeys[i]];
134
132
  }
135
133
  }
136
134
  }
137
135
  /**
138
- * Update specific fields of a component without reading the whole component first.
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
- const baseOffset = entityId * this.stride;
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.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], partial[key]);
145
+ this.arrays[i][entityId] = partial[key];
150
146
  return;
151
147
  }
152
- // Fast path for two field update (common for 2D positions)
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.fields[i0].write(this.view, baseOffset + this.fieldOffsets[i0], partial[key0]);
159
- this.fields[i1].write(this.view, baseOffset + this.fieldOffsets[i1], partial[key1]);
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.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], partial[key]);
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.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], this.fields[i].toNil());
170
+ this.arrays[i][entityId] = this.fields[i].toNil();
176
171
  }
177
172
  }
178
173
  /**
179
- * Get direct access to the underlying buffer.
180
- * Advanced use only - for SIMD operations, GPU uploads, zero-copy networking, etc.
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
- getRawBuffer() {
183
- return this.buffer;
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;
@@ -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 typed arrays
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,6 +1,6 @@
1
1
  {
2
2
  "name": "murow",
3
- "version": "0.0.52",
3
+ "version": "0.0.53",
4
4
  "description": "A lightweight TypeScript game engine for server-authoritative multiplayer games",
5
5
  "main": "dist/core.js",
6
6
  "module": "dist/core.esm.js",
@@ -1,19 +1,20 @@
1
1
  import { Component } from "./component";
2
2
 
3
3
  /**
4
- * Stores component data for entities using typed arrays.
5
- * Provides efficient packed storage with O(1) access by entity ID.
4
+ * Stores component data using separate TypedArrays per field (SoA - Structure of Arrays).
5
+ * Alternative to DataView-based storage for comparison.
6
6
  *
7
- * Key optimizations:
8
- * - ArrayBuffer storage (exact byte size, no waste)
9
- * - Single reusable DataView (no allocations)
10
- * - Single reusable object for get() (zero allocations)
11
- * - Pre-computed field offsets (no runtime calculations)
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 buffer: ArrayBuffer;
15
- private view: DataView;
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>; // For O(1) field lookup in update()
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
- // Allocate exact memory needed
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
- let offset = 0;
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
- offset += field.size;
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.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
89
- this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
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.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
92
- this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
93
- this.reusableObject[this.fieldKeys[2]] = this.fields[2].read(this.view, baseOffset + this.fieldOffsets[2]);
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.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
96
- this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
97
- this.reusableObject[this.fieldKeys[2]] = this.fields[2].read(this.view, baseOffset + this.fieldOffsets[2]);
98
- this.reusableObject[this.fieldKeys[3]] = this.fields[3].read(this.view, baseOffset + this.fieldOffsets[3]);
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.fields[i].read(
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.fields[i].read(
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.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
150
- this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
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.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
153
- this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
154
- this.fields[2].write(this.view, baseOffset + this.fieldOffsets[2], data[this.fieldKeys[2]]);
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.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
157
- this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
158
- this.fields[2].write(this.view, baseOffset + this.fieldOffsets[2], data[this.fieldKeys[2]]);
159
- this.fields[3].write(this.view, baseOffset + this.fieldOffsets[3], data[this.fieldKeys[3]]);
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.fields[i].write(
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 without reading the whole component first.
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
- const baseOffset = entityId * this.stride;
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.fields[i].write(
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 (common for 2D positions)
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.fields[i0].write(this.view, baseOffset + this.fieldOffsets[i0], partial[key0]!);
201
- this.fields[i1].write(this.view, baseOffset + this.fieldOffsets[i1], partial[key1]!);
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.fields[i].write(
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].write(
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 buffer.
234
- * Advanced use only - for SIMD operations, GPU uploads, zero-copy networking, etc.
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
- getRawBuffer(): ArrayBuffer {
237
- return this.buffer;
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 typed arrays
136
+ // Create component store with selected backend
131
137
  const store = new ComponentStore(component, this.maxEntities);
132
138
  this.componentStoresArray[index] = store;
133
139
  });