From 5e2ee3fed5d6f6c97523e8858e146fa1b71b4d38 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Fri, 31 Mar 2023 15:11:54 -0700 Subject: [PATCH] ecs: begin reworking queries Signed-off-by: Stephen Gutekanst --- libs/ecs/src/entities.zig | 84 +--------------------------- libs/ecs/src/main.zig | 3 + libs/ecs/src/query.zig | 113 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 81 deletions(-) create mode 100644 libs/ecs/src/query.zig diff --git a/libs/ecs/src/entities.zig b/libs/ecs/src/entities.zig index b53ecca9..5da4eadc 100644 --- a/libs/ecs/src/entities.zig +++ b/libs/ecs/src/entities.zig @@ -4,6 +4,7 @@ const Allocator = mem.Allocator; const testing = std.testing; const builtin = @import("builtin"); const assert = std.debug.assert; +const query = @import("query.zig"); const is_debug = builtin.mode == .Debug; @@ -332,87 +333,8 @@ pub fn Entities(comptime all_components: anytype) type { row_index: u32, }; - pub const Query = Query: { - const namespaces = std.meta.fields(@TypeOf(all_components)); - var fields: [namespaces.len]std.builtin.Type.UnionField = undefined; - inline for (namespaces, 0..) |namespace, i| { - const component_enum = std.meta.FieldEnum(namespace.type); - fields[i] = .{ - .name = namespace.name, - .type = component_enum, - .alignment = @alignOf(component_enum), - }; - } - - // need type_info variable (rather than embedding in @Type() call) - // to work around stage 1 bug - const type_info = std.builtin.Type{ - .Union = .{ - .layout = .Auto, - .tag_type = std.meta.FieldEnum(@TypeOf(all_components)), - .fields = &fields, - .decls = &.{}, - }, - }; - break :Query @Type(type_info); - }; - - fn fullComponentName(comptime q: Query) []const u8 { - return @tagName(q) ++ "." ++ @tagName(@field(q, @tagName(std.meta.activeTag(q)))); - } - - pub fn Iter(comptime components: []const Query) type { - return struct { - entities: *Self, - archetype_index: usize = 0, - row_index: u32 = 0, - - const Iterator = @This(); - - 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 (!hasComponents(archetype, components) or iter.row_index >= archetype.len) { - 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.get(iter.entities.allocator, iter.row_index, "id", EntityID).?; - iter.row_index += 1; - return Entry{ .entity = row_entity_id }; - } - }; - } - - fn hasComponents(storage: ArchetypeStorage, comptime components: []const Query) bool { - var archetype = storage; - if (components.len == 0) return false; - inline for (components) |component| { - if (!archetype.hasComponent(fullComponentName(component))) return false; - } - return true; - } - - pub fn query(entities: *Self, comptime components: []const Query) Iter(components) { - return Iter(components){ - .entities = entities, - }; - } + /// A complex query for entities matching a given criteria + pub const Query = query.Query(all_components); pub fn init(allocator: Allocator) !Self { var entities = Self{ .allocator = allocator }; diff --git a/libs/ecs/src/main.zig b/libs/ecs/src/main.zig index bc0df6b0..3bfa33b1 100644 --- a/libs/ecs/src/main.zig +++ b/libs/ecs/src/main.zig @@ -32,6 +32,7 @@ pub const World = @import("systems.zig").World; test "inclusion" { std.testing.refAllDeclsRecursive(@This()); + std.testing.refAllDeclsRecursive(@import("query.zig")); } test "example" { @@ -87,5 +88,7 @@ test "example" { try physics.set(player2, .id, 1234); try physics.set(player3, .id, 1234); + //------------------------------------------------------------------------- + // Send events to modules try world.send(.tick); } diff --git a/libs/ecs/src/query.zig b/libs/ecs/src/query.zig new file mode 100644 index 00000000..3db3a420 --- /dev/null +++ b/libs/ecs/src/query.zig @@ -0,0 +1,113 @@ +const std = @import("std"); +const testing = std.testing; + +pub const QueryType = enum { + any, + all, +}; + +/// A complex query for entities matching a given criteria +pub fn Query(comptime all_components: anytype) type { + return union(QueryType) { + /// Enum matching a namespace. e.g. `.game` or `.physics2d` + pub const Namespace = std.meta.FieldEnum(@TypeOf(all_components)); + + /// Enum matching a component within a namespace + /// e.g. `var a: Component(.physics2d) = .location` + pub fn Component(comptime namespace: Namespace) type { + return std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace)))); + } + + /// Slice of enums matching a component within a namespace + /// e.g. `&.{.location, .rotation}` + pub fn ComponentList(comptime namespace: Namespace) type { + return []const Component(namespace); + } + + /// Tagged union of namespaces matching lists of components + /// e.g. `.physics2d = .{ .location, .rotation }` + pub const NamespaceComponent = T: { + const namespaces = std.meta.fields(Namespace); + var fields: [namespaces.len]std.builtin.Type.UnionField = undefined; + inline for (namespaces, 0..) |namespace, i| { + const ns = std.meta.stringToEnum(Namespace, namespace.name).?; + fields[i] = .{ + .name = namespace.name, + .type = ComponentList(ns), + .alignment = @alignOf(ComponentList(ns)), + }; + } + + break :T @Type(.{ .Union = .{ + .layout = .Auto, + .tag_type = Namespace, + .fields = &fields, + .decls = &.{}, + } }); + }; + + /// Matches entities with any of these components + any: []const NamespaceComponent, + + /// Matches entities with all of these components + all: []const NamespaceComponent, + }; +} + +test "query" { + const Location = struct { + x: f32 = 0, + y: f32 = 0, + z: f32 = 0, + }; + + const Rotation = struct { degrees: f32 }; + + const all_components = .{ + .game = .{ + .name = []const u8, + }, + .physics = .{ + .location = Location, + .rotation = Rotation, + }, + }; + + const Q = Query(all_components); + + // Namespace type lets us select a single namespace. + try testing.expectEqual(@as(Q.Namespace, .game), .game); + try testing.expectEqual(@as(Q.Namespace, .physics), .physics); + + // Component type lets us select a single component within a namespace. + try testing.expectEqual(@as(Q.Component(.physics), .location), .location); + try testing.expectEqual(@as(Q.Component(.game), .name), .name); + + // ComponentList type lets us select multiple components within a namespace. + var x: Q.ComponentList(.physics) = &.{ + .location, + .rotation, + }; + _ = x; + + // NamespaceComponent lets us select multiple components within multiple namespaces. + var y: []const Q.NamespaceComponent = &.{ + .{ .physics = &.{ .location, .rotation } }, + .{ .game = &.{.name} }, + }; + _ = y; + + // Query matching entities with *any* of these components + var z: Q = .{ .any = &.{ + .{ .physics = &.{ .location, .rotation } }, + .{ .game = &.{.name} }, + } }; + _ = z; + + // Query matching entities with *all* of these components. + var w: Q = .{ .all = &.{ + .{ .physics = &.{ .location, .rotation } }, + .{ .game = &.{.name} }, + } }; + _ = w; +}