//! This module provides an implementation of an Entity Component System const std = @import("std"); const component = @import("component.zig"); const ComponentType = component.ComponentType; /// EntityComponentSystem keeps track of components in a MultiArrayList of Components. /// Each Entity is simply an index into that MultiArrayList of Components. /// To save on memory and avoid resizing, ids are recycled upon Entities being deleted. pub const EntityComponentSystem = struct { /// An allocator for use by the EntityComponentSystem to manage memory allocator: std.mem.Allocator, /// The mapping of Components to Entities is in this MultiArrayList components: std.MultiArrayList(component.Components), /// Keeps track of ids available for reuse available_ids: std.ArrayList(usize), fn nullEntity(self: *EntityComponentSystem, entity: usize) void { inline for (std.enums.valuesFromFields(ComponentType, std.meta.fields(ComponentType))) |comp| { self.componentRemoveEntity(comp, entity); } } pub fn init(allocator: std.mem.Allocator) EntityComponentSystem { return EntityComponentSystem{ .allocator = allocator, .components = .{}, .available_ids = std.ArrayList(usize).init(allocator), }; } pub fn deinit(self: *EntityComponentSystem) void { self.available_ids.deinit(); self.components.deinit(self.allocator); } /// Create a new entity (or recycle an old one) and return the id. /// May error if the Allocator runs out of memory. pub fn createEntity(self: *EntityComponentSystem) !usize { const id = self.available_ids.popOrNull() orelse noroom: { const id = self.components.len; try self.components.append(self.allocator, undefined); break :noroom id; }; self.nullEntity(id); return id; } /// Add an entity to a component. /// Takes a ComponentType and an entity id along with any arguments needed to initialize the component. pub fn componentAddEntity( self: *EntityComponentSystem, comp: component.ComponentType, entity: usize, args: anytype, ) void { switch (comp) { ComponentType.component_stub => self.components.items(.component_stub)[entity] = component.ComponentStub.init(args), } } /// Delete an entity and make it ready for recycling by nulling all components. /// Takes an entity id. /// May error if the Allocator runs out of memory. pub fn deleteEntity(self: *EntityComponentSystem, entity: usize) !void { self.nullEntity(entity); try self.available_ids.append(entity); } fn componentRemoveEntityGeneric(comp: anytype, entity: usize) void { var fixed_comp = comp[entity] orelse return; fixed_comp.deinit(); comp[entity] = null; } /// Removes an entity from a component. /// Takes a ComponentType and an entity id. pub fn componentRemoveEntity( self: *EntityComponentSystem, comp: component.ComponentType, entity: usize, ) void { var components = self.components; switch (comp) { ComponentType.component_stub => componentRemoveEntityGeneric(components.items(.component_stub), entity), } } }; test "add entities to components" { var ecs = EntityComponentSystem.init(std.testing.allocator); defer ecs.deinit(); for (0..100) |i| { const entity = try ecs.createEntity(); try std.testing.expectEqual(i, entity); ecs.componentAddEntity(ComponentType.component_stub, entity, .{}); try std.testing.expect(ecs.components.items(.component_stub)[entity] != null); } } test "remove entities from components" { var ecs = EntityComponentSystem.init(std.testing.allocator); defer ecs.deinit(); const entity = try ecs.createEntity(); ecs.componentAddEntity(ComponentType.component_stub, entity, .{}); const entity2 = try ecs.createEntity(); ecs.componentAddEntity(ComponentType.component_stub, entity2, .{}); try std.testing.expect(ecs.components.items(.component_stub)[entity2] != null); ecs.componentRemoveEntity(ComponentType.component_stub, entity2); try std.testing.expect(ecs.components.items(.component_stub)[entity2] == null); } test "delete entities" { var ecs = EntityComponentSystem.init(std.testing.allocator); defer ecs.deinit(); const entity = try ecs.createEntity(); ecs.componentAddEntity(ComponentType.component_stub, entity, .{}); const entity2 = try ecs.createEntity(); ecs.componentAddEntity(ComponentType.component_stub, entity2, .{}); try std.testing.expect(ecs.components.items(.component_stub)[entity2] != null); try ecs.deleteEntity(entity2); try std.testing.expect(ecs.components.items(.component_stub)[entity2] == null); try std.testing.expectEqual(1, ecs.available_ids.items.len); } test "recycle entities" { var ecs = EntityComponentSystem.init(std.testing.allocator); defer ecs.deinit(); const entity = try ecs.createEntity(); ecs.componentAddEntity(ComponentType.component_stub, entity, .{}); const entity2 = try ecs.createEntity(); ecs.componentAddEntity(ComponentType.component_stub, entity2, .{}); try ecs.deleteEntity(entity2); try std.testing.expectEqual(1, ecs.available_ids.items.len); const entity3 = try ecs.createEntity(); try std.testing.expectEqual(1, entity3); try std.testing.expectEqual(0, ecs.available_ids.items.len); const entity4 = try ecs.createEntity(); try std.testing.expectEqual(2, entity4); try ecs.deleteEntity(entity); try std.testing.expectEqual(1, ecs.available_ids.items.len); const entity5 = try ecs.createEntity(); try std.testing.expectEqual(0, entity5); try std.testing.expectEqual(0, ecs.available_ids.items.len); try ecs.deleteEntity(entity5); try ecs.deleteEntity(entity4); try ecs.deleteEntity(entity3); try std.testing.expectEqual(3, ecs.available_ids.items.len); }