-
-
Notifications
You must be signed in to change notification settings - Fork 165
/
Copy pathentities.zig
692 lines (596 loc) · 30.9 KB
/
entities.zig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
const std = @import("std");
const mem = std.mem;
const Allocator = mem.Allocator;
const testing = std.testing;
const builtin = @import("builtin");
const assert = std.debug.assert;
/// An entity ID uniquely identifies an entity globally within an Entities set.
pub const EntityID = u64;
/// Represents the storage for a single type of component within a single type of entity.
///
/// Database equivalent: a column within a table.
pub fn ComponentStorage(comptime Component: type) type {
return struct {
/// A reference to the total number of entities with the same type as is being stored here.
total_rows: *usize,
/// The actual component data. This starts as empty, and then based on the first call to
/// .set() or .setDense() is initialized as dense storage (an array) or sparse storage (a
/// hashmap.)
///
/// Sparse storage may turn to dense storage if someone later calls .set(), see that method
/// for details.
data: std.ArrayListUnmanaged(Component) = .{},
const Self = @This();
pub fn deinit(storage: *Self, allocator: Allocator) void {
storage.data.deinit(allocator);
}
// If the storage of this component is sparse, it is turned dense as calling this method
// indicates that the caller expects to set this component for most entities rather than
// sparsely.
pub fn set(storage: *Self, allocator: Allocator, row_index: u32, component: Component) !void {
if (storage.data.items.len <= row_index) try storage.data.appendNTimes(allocator, undefined, storage.data.items.len + 1 - row_index);
storage.data.items[row_index] = component;
}
/// Removes the given row index.
pub fn remove(storage: *Self, row_index: u32) void {
if (storage.data.items.len > row_index) {
_ = storage.data.swapRemove(row_index);
}
}
/// Gets the component value for the given entity ID.
pub inline fn get(storage: Self, row_index: u32) Component {
return storage.data.items[row_index];
}
pub inline fn copy(dst: *Self, allocator: Allocator, src_row: u32, dst_row: u32, src: *Self) !void {
try dst.set(allocator, dst_row, src.get(src_row));
}
pub inline fn copySparse(dst: *Self, allocator: Allocator, src_row: u32, dst_row: u32, src: *Self) !void {
// TODO: setSparse!
try dst.set(allocator, dst_row, src.get(src_row));
}
};
}
/// A type-erased representation of ComponentStorage(T) (where T is unknown).
///
/// This is useful as it allows us to store all of the typed ComponentStorage as values in a hashmap
/// despite having different types, and allows us to still deinitialize them without knowing the
/// underlying type.
pub const ErasedComponentStorage = struct {
ptr: *anyopaque,
deinit: fn (erased: *anyopaque, allocator: Allocator) void,
remove: fn (erased: *anyopaque, row: u32) void,
cloneType: fn (erased: ErasedComponentStorage, total_entities: *usize, allocator: Allocator, retval: *ErasedComponentStorage) error{OutOfMemory}!void,
copy: fn (dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) error{OutOfMemory}!void,
copySparse: fn (dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) error{OutOfMemory}!void,
pub fn cast(ptr: *anyopaque, comptime Component: type) *ComponentStorage(Component) {
var aligned = @alignCast(@alignOf(*ComponentStorage(Component)), ptr);
return @ptrCast(*ComponentStorage(Component), aligned);
}
};
/// Represents a single archetype, that is, entities which have the same exact set of component
/// types. When a component is added or removed from an entity, it's archetype changes.
///
/// Database equivalent: a table where rows are entities and columns are components (dense storage).
pub const ArchetypeStorage = struct {
allocator: Allocator,
/// The hash of every component name in this archetype, i.e. the name of this archetype.
hash: u64,
/// A mapping of rows in the table to entity IDs.
///
/// Doubles as the counter of total number of rows that have been reserved within this
/// archetype table.
entity_ids: std.ArrayListUnmanaged(EntityID) = .{},
/// A string hashmap of component_name -> type-erased *ComponentStorage(Component)
components: std.StringArrayHashMapUnmanaged(ErasedComponentStorage),
/// Calculates the storage.hash value. This is a hash of all the component names, and can
/// effectively be used to uniquely identify this table within the database.
pub fn calculateHash(storage: *ArchetypeStorage) void {
storage.hash = 0;
var iter = storage.components.iterator();
while (iter.next()) |entry| {
const component_name = entry.key_ptr.*;
storage.hash ^= std.hash_map.hashString(component_name);
}
}
pub fn deinit(storage: *ArchetypeStorage) void {
for (storage.components.values()) |erased| {
erased.deinit(erased.ptr, storage.allocator);
}
storage.entity_ids.deinit(storage.allocator);
storage.components.deinit(storage.allocator);
}
/// New reserves a row for storing an entity within this archetype table.
pub fn new(storage: *ArchetypeStorage, entity: EntityID) !u32 {
// Return a new row index
const new_row_index = storage.entity_ids.items.len;
try storage.entity_ids.append(storage.allocator, entity);
return @intCast(u32, new_row_index);
}
/// Undoes the last call to the new() operation, effectively unreserving the row that was last
/// reserved.
pub fn undoNew(storage: *ArchetypeStorage) void {
_ = storage.entity_ids.pop();
}
/// Sets the value of the named component (column) for the given row in the table. Realizes the
/// deferred allocation of column storage for N entities (storage.counter) if it is not already.
pub fn set(storage: *ArchetypeStorage, row_index: u32, name: []const u8, component: anytype) !void {
var component_storage_erased = storage.components.get(name).?;
var component_storage = ErasedComponentStorage.cast(component_storage_erased.ptr, @TypeOf(component));
try component_storage.set(storage.allocator, row_index, component);
}
/// Removes the specified row. See also the `Entity.delete()` helper.
///
/// This merely marks the row as removed, the same row index will be recycled the next time a
/// new row is requested via `new()`.
pub fn remove(storage: *ArchetypeStorage, row_index: u32) !void {
_ = storage.entity_ids.swapRemove(row_index);
for (storage.components.values()) |component_storage| {
component_storage.remove(component_storage.ptr, row_index);
}
}
/// The number of entities actively stored in this table (not counting entities which are
/// allocated in this table but have been removed)
pub fn count(storage: *ArchetypeStorage) usize {
return storage.entity_ids.items.len;
}
/// Tells if this archetype has every one of the given components.
pub fn hasComponents(storage: *ArchetypeStorage, components: []const []const u8) bool {
for (components) |component_name| {
if (!storage.components.contains(component_name)) return false;
}
return true;
}
};
pub const void_archetype_hash = std.math.maxInt(u64);
/// A database of entities. For example, all player, monster, etc. entities in a game world.
///
/// ```
/// const world = Entities.init(allocator); // all entities in our world.
/// defer world.deinit();
///
/// const player1 = world.new(); // our first "player" entity
/// const player2 = world.new(); // our second "player" entity
/// ```
///
/// Entities are divided into archetypes for optimal, CPU cache efficient storage. For example, all
/// entities with two components `Location` and `Name` are stored in the same table dedicated to
/// densely storing `(Location, Name)` rows in contiguous memory. This not only ensures CPU cache
/// efficiency (leveraging data oriented design) which improves iteration speed over entities for
/// example, but makes queries like "find all entities with a Location component" ridiculously fast
/// because one need only find the tables which have a column for storing Location components and it
/// is then guaranteed every entity in the table has that component (entities do not need to be
/// checked one by one to determine if they have a Location component.)
///
/// Components can be added and removed to entities at runtime as you please:
///
/// ```
/// try player1.set("rotation", Rotation{ .degrees = 90 });
/// try player1.remove("rotation");
/// ```
///
/// When getting a component value, you must know it's type or undefined behavior will occur:
/// TODO: improve this!
///
/// ```
/// if (player1.get("rotation", Rotation)) |rotation| {
/// // player1 had a rotation component!
/// }
/// ```
///
/// When a component is added or removed from an entity, it's archetype is said to change. For
/// example player1 may have had the archetype `(Location, Name)` before, and after adding the
/// rotation component has the archetype `(Location, Name, Rotation)`. It will be automagically
/// "moved" from the table that stores entities with `(Location, Name)` components to the table that
/// stores `(Location, Name, Rotation)` components for you.
///
/// You can have 65,535 archetypes in total, and 4,294,967,295 entities total. Entities which are
/// deleted are merely marked as "unused" and recycled
///
/// Database equivalents:
/// * Entities is a database of tables, where each table represents a single archetype.
/// * ArchetypeStorage is a table, whose rows are entities and columns are components.
/// * EntityID is a mere 32-bit array index, pointing to a 16-bit archetype table index and 32-bit
/// row index, enabling entities to "move" from one archetype table to another seamlessly and
/// making lookup by entity ID a few cheap array indexing operations.
/// * ComponentStorage(T) is a column of data within a table for a single type of component `T`.
pub const Entities = struct {
allocator: Allocator,
/// TODO!
counter: EntityID = 0,
/// A mapping of entity IDs (array indices) to where an entity's component values are actually
/// stored.
entities: std.AutoHashMapUnmanaged(EntityID, Pointer) = .{},
/// A mapping of archetype hash to their storage.
///
/// Database equivalent: table name -> tables representing entities.
archetypes: std.AutoArrayHashMapUnmanaged(u64, ArchetypeStorage) = .{},
/// Points to where an entity is stored, specifically in which archetype table and in which row
/// of that table. That is, the entity's component values are stored at:
///
/// ```
/// Entities.archetypes[ptr.archetype_index].rows[ptr.row_index]
/// ```
///
pub const Pointer = struct {
archetype_index: u16,
row_index: u32,
};
pub const Iterator = struct {
entities: *Entities,
components: []const []const u8,
archetype_index: usize = 0,
row_index: usize = 0,
pub const Entry = struct {
entity: EntityID,
pub fn unlock(e: Entry) void {
_ = e;
}
};
pub fn next(iter: *Iterator) ?Entry {
const entities = iter.entities;
// If the archetype table we're looking at does not contain the components we're
// querying for, keep searching through tables until we find one that does.
var archetype = entities.archetypes.entries.get(iter.archetype_index).value;
while (!archetype.hasComponents(iter.components) or iter.row_index >= archetype.count()) {
iter.archetype_index += 1;
iter.row_index = 0;
if (iter.archetype_index >= entities.archetypes.count()) {
return null;
}
archetype = entities.archetypes.entries.get(iter.archetype_index).value;
}
const row_entity_id = archetype.entity_ids.items[iter.row_index];
iter.row_index += 1;
return Entry{ .entity = row_entity_id };
}
};
pub fn query(entities: *Entities, components: []const []const u8) Iterator {
return Iterator{
.entities = entities,
.components = components,
};
}
pub fn init(allocator: Allocator) !Entities {
var entities = Entities{ .allocator = allocator };
try entities.archetypes.put(allocator, void_archetype_hash, ArchetypeStorage{
.allocator = allocator,
.components = .{},
.hash = void_archetype_hash,
});
return entities;
}
pub fn deinit(entities: *Entities) void {
entities.entities.deinit(entities.allocator);
var iter = entities.archetypes.iterator();
while (iter.next()) |entry| {
entry.value_ptr.deinit();
}
entities.archetypes.deinit(entities.allocator);
}
/// Returns a new entity.
pub fn new(entities: *Entities) !EntityID {
const new_id = entities.counter;
entities.counter += 1;
var void_archetype = entities.archetypes.getPtr(void_archetype_hash).?;
const new_row = try void_archetype.new(new_id);
const void_pointer = Pointer{
.archetype_index = 0, // void archetype is guaranteed to be first index
.row_index = new_row,
};
entities.entities.put(entities.allocator, new_id, void_pointer) catch |err| {
void_archetype.undoNew();
return err;
};
return new_id;
}
/// Removes an entity.
pub fn remove(entities: *Entities, entity: EntityID) !void {
var archetype = entities.archetypeByID(entity);
const ptr = entities.entities.get(entity).?;
// A swap removal will be performed, update the entity stored in the last row of the
// archetype table to point to the row the entity we are removing is currently located.
const last_row_entity_id = archetype.entity_ids.items[archetype.entity_ids.items.len - 1];
try entities.entities.put(entities.allocator, last_row_entity_id, Pointer{
.archetype_index = ptr.archetype_index,
.row_index = ptr.row_index,
});
// Perform a swap removal to remove our entity from the archetype table.
try archetype.remove(ptr.row_index);
_ = entities.entities.remove(entity);
}
/// Returns the archetype storage for the given entity.
pub inline fn archetypeByID(entities: *Entities, entity: EntityID) *ArchetypeStorage {
const ptr = entities.entities.get(entity).?;
return &entities.archetypes.values()[ptr.archetype_index];
}
/// Sets the named component to the specified value for the given entity,
/// moving the entity from it's current archetype table to the new archetype
/// table if required.
pub fn setComponent(entities: *Entities, entity: EntityID, name: []const u8, component: anytype) !void {
var archetype = entities.archetypeByID(entity);
// Determine the old hash for the archetype.
const old_hash = archetype.hash;
// Determine the new hash for the archetype + new component
var have_already = archetype.components.contains(name);
const new_hash = if (have_already) old_hash else old_hash ^ std.hash_map.hashString(name);
// Find the archetype storage for this entity. Could be a new archetype storage table (if a
// new component was added), or the same archetype storage table (if just updating the
// value of a component.)
var archetype_entry = try entities.archetypes.getOrPut(entities.allocator, new_hash);
if (!archetype_entry.found_existing) {
archetype_entry.value_ptr.* = ArchetypeStorage{
.allocator = entities.allocator,
.components = .{},
.hash = 0,
};
var new_archetype = archetype_entry.value_ptr;
// Create storage/columns for all of the existing components on the entity.
var column_iter = archetype.components.iterator();
while (column_iter.next()) |entry| {
var erased: ErasedComponentStorage = undefined;
entry.value_ptr.cloneType(entry.value_ptr.*, &new_archetype.entity_ids.items.len, entities.allocator, &erased) catch |err| {
assert(entities.archetypes.swapRemove(new_hash));
return err;
};
new_archetype.components.put(entities.allocator, entry.key_ptr.*, erased) catch |err| {
assert(entities.archetypes.swapRemove(new_hash));
return err;
};
}
// Create storage/column for the new component.
const erased = entities.initErasedStorage(&new_archetype.entity_ids.items.len, @TypeOf(component)) catch |err| {
assert(entities.archetypes.swapRemove(new_hash));
return err;
};
new_archetype.components.put(entities.allocator, name, erased) catch |err| {
assert(entities.archetypes.swapRemove(new_hash));
return err;
};
new_archetype.calculateHash();
}
// Either new storage (if the entity moved between storage tables due to having a new
// component) or the prior storage (if the entity already had the component and it's value
// is merely being updated.)
var current_archetype_storage = archetype_entry.value_ptr;
if (new_hash == old_hash) {
// Update the value of the existing component of the entity.
const ptr = entities.entities.get(entity).?;
try current_archetype_storage.set(ptr.row_index, name, component);
return;
}
// Copy to all component values for our entity from the old archetype storage
// (archetype) to the new one (current_archetype_storage).
const new_row = try current_archetype_storage.new(entity);
const old_ptr = entities.entities.get(entity).?;
// Update the storage/columns for all of the existing components on the entity.
var column_iter = archetype.components.iterator();
while (column_iter.next()) |entry| {
var old_component_storage = entry.value_ptr;
var new_component_storage = current_archetype_storage.components.get(entry.key_ptr.*).?;
new_component_storage.copy(new_component_storage.ptr, entities.allocator, new_row, old_ptr.row_index, old_component_storage.ptr) catch |err| {
current_archetype_storage.undoNew();
return err;
};
}
current_archetype_storage.entity_ids.items[new_row] = entity;
// Update the storage/column for the new component.
current_archetype_storage.set(new_row, name, component) catch |err| {
current_archetype_storage.undoNew();
return err;
};
var swapped_entity_id = archetype.entity_ids.items[archetype.entity_ids.items.len - 1];
archetype.remove(old_ptr.row_index) catch |err| {
current_archetype_storage.undoNew();
return err;
};
try entities.entities.put(entities.allocator, swapped_entity_id, old_ptr);
try entities.entities.put(entities.allocator, entity, Pointer{
.archetype_index = @intCast(u16, archetype_entry.index),
.row_index = new_row,
});
return;
}
/// gets the named component of the given type (which must be correct, otherwise undefined
/// behavior will occur). Returns null if the component does not exist on the entity.
pub fn getComponent(entities: *Entities, entity: EntityID, name: []const u8, comptime Component: type) ?Component {
var archetype = entities.archetypeByID(entity);
var component_storage_erased = archetype.components.get(name) orelse return null;
const ptr = entities.entities.get(entity).?;
var component_storage = ErasedComponentStorage.cast(component_storage_erased.ptr, Component);
return component_storage.get(ptr.row_index);
}
/// Removes the named component from the entity, or noop if it doesn't have such a component.
pub fn removeComponent(entities: *Entities, entity: EntityID, name: []const u8) !void {
var archetype = entities.archetypeByID(entity);
if (!archetype.components.contains(name)) return;
// Determine the old hash for the archetype.
const old_hash = archetype.hash;
// Determine the new hash for the archetype with the component removed
var new_hash: u64 = 0;
var iter = archetype.components.iterator();
while (iter.next()) |entry| {
const component_name = entry.key_ptr.*;
if (!std.mem.eql(u8, component_name, name)) new_hash ^= std.hash_map.hashString(component_name);
}
assert(new_hash != old_hash);
// Find the archetype storage for this entity. Could be a new archetype storage table (if a
// new component was added), or the same archetype storage table (if just updating the
// value of a component.)
var archetype_entry = try entities.archetypes.getOrPut(entities.allocator, new_hash);
if (!archetype_entry.found_existing) {
archetype_entry.value_ptr.* = ArchetypeStorage{
.allocator = entities.allocator,
.components = .{},
.hash = 0,
};
var new_archetype = archetype_entry.value_ptr;
// Create storage/columns for all of the existing components on the entity.
var column_iter = archetype.components.iterator();
while (column_iter.next()) |entry| {
if (std.mem.eql(u8, entry.key_ptr.*, name)) continue;
var erased: ErasedComponentStorage = undefined;
entry.value_ptr.cloneType(entry.value_ptr.*, &new_archetype.entity_ids.items.len, entities.allocator, &erased) catch |err| {
assert(entities.archetypes.swapRemove(new_hash));
return err;
};
new_archetype.components.put(entities.allocator, entry.key_ptr.*, erased) catch |err| {
assert(entities.archetypes.swapRemove(new_hash));
return err;
};
}
new_archetype.calculateHash();
}
// Either new storage (if the entity moved between storage tables due to having a new
// component) or the prior storage (if the entity already had the component and it's value
// is merely being updated.)
var current_archetype_storage = archetype_entry.value_ptr;
// Copy to all component values for our entity from the old archetype storage
// (archetype) to the new one (current_archetype_storage).
const new_row = try current_archetype_storage.new(entity);
const old_ptr = entities.entities.get(entity).?;
// Update the storage/columns for all of the existing components on the entity.
var column_iter = current_archetype_storage.components.iterator();
while (column_iter.next()) |entry| {
var src_component_storage = archetype.components.get(entry.key_ptr.*).?;
var dst_component_storage = entry.value_ptr;
dst_component_storage.copy(dst_component_storage.ptr, entities.allocator, new_row, old_ptr.row_index, src_component_storage.ptr) catch |err| {
current_archetype_storage.undoNew();
return err;
};
}
current_archetype_storage.entity_ids.items[new_row] = entity;
var swapped_entity_id = archetype.entity_ids.items[archetype.entity_ids.items.len - 1];
archetype.remove(old_ptr.row_index) catch |err| {
current_archetype_storage.undoNew();
return err;
};
try entities.entities.put(entities.allocator, swapped_entity_id, old_ptr);
try entities.entities.put(entities.allocator, entity, Pointer{
.archetype_index = @intCast(u16, archetype_entry.index),
.row_index = new_row,
});
return;
}
// TODO: iteration over all entities
// TODO: iteration over all entities with components (U, V, ...)
// TODO: iteration over all entities with type T
// TODO: iteration over all entities with type T and components (U, V, ...)
// TODO: "indexes" - a few ideas we could express:
//
// * Graph relations index: e.g. parent-child entity relations for a DOM / UI / scene graph.
// * Spatial index: "give me all entities within 5 units distance from (x, y, z)"
// * Generic index: "give me all entities where arbitraryFunction(e) returns true"
//
pub fn initErasedStorage(entities: *const Entities, total_rows: *usize, comptime Component: type) !ErasedComponentStorage {
var new_ptr = try entities.allocator.create(ComponentStorage(Component));
new_ptr.* = ComponentStorage(Component){ .total_rows = total_rows };
return ErasedComponentStorage{
.ptr = new_ptr,
.deinit = (struct {
pub fn deinit(erased: *anyopaque, allocator: Allocator) void {
var ptr = ErasedComponentStorage.cast(erased, Component);
ptr.deinit(allocator);
allocator.destroy(ptr);
}
}).deinit,
.remove = (struct {
pub fn remove(erased: *anyopaque, row: u32) void {
var ptr = ErasedComponentStorage.cast(erased, Component);
ptr.remove(row);
}
}).remove,
.cloneType = (struct {
pub fn cloneType(erased: ErasedComponentStorage, _total_rows: *usize, allocator: Allocator, retval: *ErasedComponentStorage) !void {
var new_clone = try allocator.create(ComponentStorage(Component));
new_clone.* = ComponentStorage(Component){ .total_rows = _total_rows };
var tmp = erased;
tmp.ptr = new_clone;
retval.* = tmp;
}
}).cloneType,
.copy = (struct {
pub fn copy(dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) !void {
var dst = ErasedComponentStorage.cast(dst_erased, Component);
var src = ErasedComponentStorage.cast(src_erased, Component);
return dst.copy(allocator, src_row, dst_row, src);
}
}).copy,
.copySparse = (struct {
pub fn copySparse(dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) !void {
var dst = ErasedComponentStorage.cast(dst_erased, Component);
var src = ErasedComponentStorage.cast(src_erased, Component);
return dst.copySparse(allocator, src_row, dst_row, src);
}
}).copySparse,
};
}
// TODO: ability to remove archetype entirely, deleting all entities in it
// TODO: ability to remove archetypes with no entities (garbage collection)
};
test "entity ID size" {
try testing.expectEqual(8, @sizeOf(EntityID));
}
test "example" {
const allocator = testing.allocator;
//-------------------------------------------------------------------------
// Create a world.
var world = try Entities.init(allocator);
defer world.deinit();
//-------------------------------------------------------------------------
// Define component types, any Zig type will do!
// A location component.
const Location = struct {
x: f32 = 0,
y: f32 = 0,
z: f32 = 0,
};
//-------------------------------------------------------------------------
// Create first player entity.
var player1 = try world.new();
try world.setComponent(player1, "name", "jane"); // add Name component
try world.setComponent(player1, "location", Location{}); // add Location component
// Create second player entity.
var player2 = try world.new();
try testing.expect(world.getComponent(player2, "location", Location) == null);
try testing.expect(world.getComponent(player2, "name", []const u8) == null);
//-------------------------------------------------------------------------
// We can add new components at will.
const Rotation = struct { degrees: f32 };
try world.setComponent(player2, "rotation", Rotation{ .degrees = 90 });
try testing.expect(world.getComponent(player1, "rotation", Rotation) == null); // player1 has no rotation
//-------------------------------------------------------------------------
// Remove a component from any entity at will.
// TODO: add a way to "cleanup" truly unused archetypes
try world.removeComponent(player1, "name");
try world.removeComponent(player1, "location");
try world.removeComponent(player1, "location"); // doesn't exist? no problem.
//-------------------------------------------------------------------------
// Introspect things.
//
// Archetype IDs, these are our "table names" - they're just hashes of all the component names
// within the archetype table.
var archetypes = world.archetypes.keys();
try testing.expectEqual(@as(usize, 6), archetypes.len);
try testing.expectEqual(@as(u64, 18446744073709551615), archetypes[0]);
try testing.expectEqual(@as(u64, 6893717443977936573), archetypes[1]);
try testing.expectEqual(@as(u64, 7008573051677164842), archetypes[2]);
try testing.expectEqual(@as(u64, 14420739110802803032), archetypes[3]);
try testing.expectEqual(@as(u64, 13913849663823266920), archetypes[4]);
try testing.expectEqual(@as(u64, 0), archetypes[5]);
// Number of (living) entities stored in an archetype table.
try testing.expectEqual(@as(usize, 0), world.archetypes.get(archetypes[2]).?.count());
// Component names for a given archetype.
var component_names = world.archetypes.get(archetypes[2]).?.components.keys();
try testing.expectEqual(@as(usize, 2), component_names.len);
try testing.expectEqualStrings("name", component_names[0]);
try testing.expectEqualStrings("location", component_names[1]);
// Component names for a given entity
var player2_archetype = world.archetypeByID(player2);
component_names = player2_archetype.components.keys();
try testing.expectEqual(@as(usize, 1), component_names.len);
try testing.expectEqualStrings("rotation", component_names[0]);
// TODO: iterating components an entity has not currently supported.
//-------------------------------------------------------------------------
// Remove an entity whenever you wish. Just be sure not to try and use it later!
try world.remove(player1);
}