module: components are written in the same style as events

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-03-26 13:19:58 -07:00 committed by Stephen Gutekanst
parent f50a27b83d
commit 7e0b9dde68
10 changed files with 265 additions and 123 deletions

View file

@ -16,8 +16,8 @@ direction: Vec2 = vec2(0, 0),
spawning: bool = false,
spawn_timer: mach.Timer,
pub const components = struct {
pub const follower = void;
pub const components = .{
.{ .name = .follower, .type = void },
};
pub const events = .{

View file

@ -20,10 +20,10 @@ uniform_buffer: *gpu.Buffer,
pub const name = .renderer;
pub const Mod = mach.Mod(@This());
pub const components = struct {
pub const location = Vec3;
pub const rotation = Vec3;
pub const scale = f32;
pub const components = .{
.{ .name = .location, .type = Vec3 },
.{ .name = .rotation, .type = Vec3 },
.{ .name = .scale, .type = f32 },
};
pub const events = .{

View file

@ -42,16 +42,18 @@ pub fn ArchetypeSlicer(comptime all_components: anytype) type {
pub fn slice(
slicer: @This(),
// TODO: cleanup comptime
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))),
) []@field(
@field(all_components, @tagName(namespace_name)),
@tagName(component_name),
) {
).type {
// TODO: cleanup comptime
const Type = @field(
@field(all_components, @tagName(namespace_name)),
@tagName(component_name),
);
).type;
if (namespace_name == .entity and component_name == .id) {
const name_id = slicer.archetype.component_names.index("id").?;
return slicer.archetype.getColumnValues(name_id, Type).?[0..slicer.archetype.len];

View file

@ -281,12 +281,13 @@ pub fn Entities(comptime all_components: anytype) type {
pub fn setComponent(
entities: *Self,
entity: EntityID,
// TODO: cleanup comptime
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))),
component: @field(
@field(all_components, @tagName(namespace_name)),
@tagName(component_name),
),
).type,
) !void {
const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
const name_id = try entities.component_names.indexOrPut(entities.allocator, name_str);
@ -483,16 +484,19 @@ pub fn Entities(comptime all_components: anytype) type {
pub fn getComponent(
entities: *Self,
entity: EntityID,
// TODO: cleanup comptime
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))),
) ?@field(
@field(all_components, @tagName(namespace_name)),
@tagName(component_name),
) {
).type {
// TODO: cleanup comptime
const Component = comptime @field(
@field(all_components, @tagName(namespace_name)),
@tagName(component_name),
);
).type;
const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
const name_id = entities.component_names.index(name_str) orelse return null;
@ -523,8 +527,9 @@ pub fn Entities(comptime all_components: anytype) type {
pub fn removeComponent(
entities: *Self,
entity: EntityID,
// TODO: cleanup comptime
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))),
) !void {
const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
const name_id = try entities.component_names.indexOrPut(entities.allocator, name_str);

View file

@ -47,8 +47,8 @@ test "example" {
pointer: u8,
pub const name = .physics;
pub const components = struct {
pub const id = u32;
pub const components = .{
.{ .name = .id, .type = u32 },
};
pub const events = .{
.{ .global = .tick, .handler = tick },
@ -61,8 +61,8 @@ test "example" {
const Renderer = struct {
pub const name = .renderer;
pub const components = struct {
pub const id = u16;
pub const components = .{
.{ .name = .ud, .type = u16 },
};
pub const events = .{
.{ .global = .tick, .handler = tick },

View file

@ -9,23 +9,27 @@ pub const QueryTag = enum {
/// A complex query for entities matching a given criteria
pub fn Query(comptime all_components: anytype) type {
return union(QueryTag) {
// TODO: cleanup comptime
/// Enum matching a namespace. e.g. `.game` or `.physics2d`
pub const Namespace = std.meta.FieldEnum(@TypeOf(all_components));
// TODO: cleanup comptime
/// Enum matching a component within a namespace
/// e.g. `var a: Component(.physics2d) = .location`
pub fn Component(comptime namespace: Namespace) type {
const components = @field(all_components, @tagName(namespace));
if (@typeInfo(components).Struct.decls.len == 0) return enum {};
return std.meta.DeclEnum(components);
if (@typeInfo(@TypeOf(components)).Struct.fields.len == 0) return enum {};
return std.meta.FieldEnum(@TypeOf(components));
}
// TODO: cleanup comptime
/// Slice of enums matching a component within a namespace
/// e.g. `&.{.location, .rotation}`
pub fn ComponentList(comptime namespace: Namespace) type {
return []const Component(namespace);
}
// TODO: cleanup comptime
/// Tagged union of namespaces matching lists of components
/// e.g. `.physics2d = &.{ .location, .rotation }`
pub const NamespaceComponent = T: {

View file

@ -7,6 +7,7 @@ const Entities = @import("entities.zig").Entities;
const EntityID = @import("entities.zig").EntityID;
const comp = @import("comptime.zig");
const Module = @import("../module.zig").Module;
const NamespacedComponents = @import("../module.zig").NamespacedComponents;
pub fn World(comptime mods: anytype) type {
const StateT = NamespacedState(mods);
@ -37,8 +38,9 @@ pub fn World(comptime mods: anytype) type {
pub inline fn set(
m: *@This(),
entity: EntityID,
comptime component_name: std.meta.DeclEnum(components),
component: @field(components, @tagName(component_name)),
// TODO: cleanup comptime
comptime component_name: std.meta.FieldEnum(@TypeOf(components)),
component: @field(components, @tagName(component_name)).type,
) !void {
const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m));
const world = @fieldParentPtr(WorldT, "mod", mod_ptr);
@ -50,8 +52,9 @@ pub fn World(comptime mods: anytype) type {
pub inline fn get(
m: *@This(),
entity: EntityID,
comptime component_name: std.meta.DeclEnum(components),
) ?@field(components, @tagName(component_name)) {
// TODO: cleanup comptime
comptime component_name: std.meta.FieldEnum(@TypeOf(components)),
) ?@field(components, @tagName(component_name)).type {
const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m));
const world = @fieldParentPtr(WorldT, "mod", mod_ptr);
return world.entities.getComponent(entity, module_tag, component_name);
@ -61,7 +64,8 @@ pub fn World(comptime mods: anytype) type {
pub inline fn remove(
m: *@This(),
entity: EntityID,
comptime component_name: std.meta.DeclEnum(components),
// TODO: cleanup comptime
comptime component_name: std.meta.FieldEnum(@TypeOf(components)),
) !void {
const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m));
const world = @fieldParentPtr(WorldT, "mod", mod_ptr);
@ -80,6 +84,7 @@ pub fn World(comptime mods: anytype) type {
world.modules.sendGlobal(module_tag, event_name, args);
}
// TODO: eliminate this
pub fn dispatchNoError(m: *@This()) void {
const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m));
const world = @fieldParentPtr(WorldT, "mod", mod_ptr);
@ -173,42 +178,6 @@ pub fn World(comptime mods: anytype) type {
};
}
// TODO: reconsider components concept
fn NamespacedComponents(comptime modules: anytype) type {
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
inline for (modules) |M| {
const components = if (@hasDecl(M, "components")) M.components else struct {};
fields = fields ++ [_]std.builtin.Type.StructField{.{
.name = @tagName(M.name),
.type = type,
.default_value = &components,
.is_comptime = true,
.alignment = @alignOf(@TypeOf(components)),
}};
}
// Builtin components
const entity_components = struct {
pub const id = EntityID;
};
fields = fields ++ [_]std.builtin.Type.StructField{.{
.name = "entity",
.type = type,
.default_value = &entity_components,
.is_comptime = true,
.alignment = @alignOf(@TypeOf(entity_components)),
}};
return @Type(.{
.Struct = .{
.layout = .Auto,
.is_tuple = false,
.fields = fields,
.decls = &[_]std.builtin.Type.Declaration{},
},
});
}
// TODO: reconsider state concept
fn NamespacedState(comptime modules: anytype) type {
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};

View file

@ -18,25 +18,29 @@ pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline),
pub const name = .mach_gfx_sprite;
pub const Mod = mach.Mod(@This());
pub const components = struct {
/// The ID of the pipeline this sprite belongs to. By default, zero.
///
/// This determines which shader, textures, etc. are used for rendering the sprite.
pub const pipeline = u8;
pub const components = .{
.{ .name = .pipeline, .type = u8, .description =
\\ The ID of the pipeline this sprite belongs to. By default, zero.
\\
\\ This determines which shader, textures, etc. are used for rendering the sprite.
},
/// The sprite model transformation matrix. A sprite is measured in pixel units, starting from
/// (0, 0) at the top-left corner and extending to the size of the sprite. By default, the world
/// origin (0, 0) lives at the center of the window.
///
/// Example: in a 500px by 500px window, a sprite located at (0, 0) with size (250, 250) will
/// cover the top-right hand corner of the window.
pub const transform = Mat4x4;
.{ .name = .transform, .type = Mat4x4, .description =
\\ The sprite model transformation matrix. A sprite is measured in pixel units, starting from
\\ (0, 0) at the top-left corner and extending to the size of the sprite. By default, the world
\\ origin (0, 0) lives at the center of the window.
\\
\\ Example: in a 500px by 500px window, a sprite located at (0, 0) with size (250, 250) will
\\ cover the top-right hand corner of the window.
},
/// UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels.
pub const uv_transform = Mat3x3;
.{ .name = .uv_transform, .type = Mat3x3, .description =
\\ UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels.
},
/// The size of the sprite, in pixels.
pub const size = Vec2;
.{ .name = .size, .type = Vec2, .description =
\\ The size of the sprite, in pixels.
},
};
pub const events = .{

View file

@ -27,41 +27,55 @@ pub const Mod = mach.Mod(@This());
//
// TODO: allow user to specify projection matrix (3d-space flat text etc.)
pub const components = struct {
/// The ID of the pipeline this text belongs to. By default, zero.
///
/// This determines which shader, textures, etc. are used for rendering the text.
pub const pipeline = u8;
pub const components = .{
.{ .name = .pipeline, .type = u8, .description =
\\ The ID of the pipeline this text belongs to. By default, zero.
\\
\\ This determines which shader, textures, etc. are used for rendering the text.
},
/// The text model transformation matrix. Text is measured in pixel units, starting from
/// (0, 0) at the top-left corner and extending to the size of the text. By default, the world
/// origin (0, 0) lives at the center of the window.
pub const transform = Mat4x4;
.{ .name = .transform, .type = Mat4x4, .description =
\\ The text model transformation matrix. Text is measured in pixel units, starting from
\\ (0, 0) at the top-left corner and extending to the size of the text. By default, the world
\\ origin (0, 0) lives at the center of the window.
},
/// String segments of UTF-8 encoded text to render.
///
/// Expected to match the length of the style component.
pub const text = []const []const u8;
.{ .name = .text, .type = []const []const u8, .description =
\\ String segments of UTF-8 encoded text to render.
\\
\\ Expected to match the length of the style component.
},
/// The style to apply to each segment of text.
///
/// Expected to match the length of the text component.
pub const style = []const mach.ecs.EntityID;
.{ .name = .style, .type = []const mach.ecs.EntityID, .description =
\\ The style to apply to each segment of text.
\\
\\ Expected to match the length of the text component.
},
/// Style component: desired font to render text with.
pub const font_name = []const u8; // TODO: ship a default font
// TODO: ship a default font
.{ .name = .font_name, .type = []const u8, .description =
\\ Style component: desired font to render text with.
},
/// Style component: font size in pixels
pub const font_size = f32; // e.g. 12 * mach.gfx.px_per_pt // 12pt
// e.g. 12 * mach.gfx.px_per_pt // 12pt
.{ .name = .font_size, .type = f32, .description =
\\ Style component: font size in pixels
},
/// Style component: font weight
pub const font_weight = u16; // e.g. mach.gfx.font_weight_normal
// e.g. mach.gfx.font_weight_normal
.{ .name = .font_weight, .type = u16, .description =
\\ Style component: font weight
},
/// Style component: italic text
pub const italic = bool; // e.g. false
// e.g. false
.{ .name = .italic, .type = bool, .description =
\\ Style component: italic text
},
/// Style component: fill color
pub const color = Vec4; // e.g. vec4(0, 0, 0, 1.0),
// e.g. vec4(0, 0, 0, 1.0)
.{ .name = .color, .type = Vec4, .description =
\\ Style component: fill color
},
};
pub const events = .{

View file

@ -2,6 +2,8 @@ const builtin = @import("builtin");
const std = @import("std");
const testing = @import("testing.zig");
const EntityID = @import("./ecs/entities.zig").EntityID;
/// Verifies that M matches the basic layout of a Mach module
pub fn Module(comptime M: type) type {
if (@typeInfo(M) != .Struct) @compileError("mach: expected module struct, found: " ++ @typeName(M));
@ -11,11 +13,7 @@ pub fn Module(comptime M: type) type {
const prefix = "mach: module ." ++ @tagName(M.name) ++ " ";
if (!@hasDecl(M, "events")) @compileError(prefix ++ "must have `pub const events = .{};`");
validateEvents("mach: module ." ++ @tagName(M.name) ++ " ", M.events);
// TODO: move this to ecs
if (@hasDecl(M, "components")) {
if (@typeInfo(M.components) != .Struct) @compileError("Module.components must be `pub const components = struct { ... };`, found type:" ++ @typeName(M.components));
}
_ = MComponents(M);
return M;
}
@ -461,6 +459,7 @@ fn ModuleName(comptime mods: anytype) type {
});
}
// TODO: tests
/// Struct like .{.foo = FooMod, .bar = BarMod}
fn NamespacedModules(comptime modules: anytype) type {
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
@ -483,6 +482,7 @@ fn NamespacedModules(comptime modules: anytype) type {
});
}
// TODO: tests
fn validateEvents(comptime error_prefix: anytype, comptime events: anytype) void {
if (@typeInfo(@TypeOf(events)) != .Struct or !@typeInfo(@TypeOf(events)).Struct.is_tuple) {
@compileError(error_prefix ++ "expected a tuple of structs, found: " ++ @typeName(@TypeOf(events)));
@ -525,6 +525,152 @@ fn validateEvents(comptime error_prefix: anytype, comptime events: anytype) void
}
}
// TODO: tests
/// Returns a struct type defining all module's components by module name, e.g.:
///
/// ```
/// struct {
/// builtin: struct {
/// id: @TypeOf() = .{ .type = EntityID, .description = "Entity ID" },
/// },
/// physics: struct {
/// location: @TypeOf() = .{ .type = Vec3, .description = null },
/// rotation: @TypeOf() = .{ .type = Vec2, .description = "rotation component" },
/// },
/// renderer: struct {
/// location: @TypeOf() = .{ .type = Vec2, .description = null },
/// },
/// }
/// ```
pub fn NamespacedComponents(comptime modules: anytype) type {
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
inline for (modules) |M| {
const MC = MComponents(M);
fields = fields ++ [_]std.builtin.Type.StructField{.{
.name = @tagName(M.name),
.type = MC,
.default_value = &MC{},
.is_comptime = true,
.alignment = @alignOf(MC),
}};
}
// Builtin components
// TODO: better method of injecting builtin module?
const BuiltinMC = MComponents(struct {
pub const name = .builtin;
pub const components = .{
.{ .name = .id, .type = EntityID, .description = "Entity ID" },
};
});
fields = fields ++ [_]std.builtin.Type.StructField{.{
.name = "entity",
.type = BuiltinMC,
.default_value = &BuiltinMC{},
.is_comptime = true,
.alignment = @alignOf(BuiltinMC),
}};
return @Type(.{
.Struct = .{
.layout = .Auto,
.is_tuple = false,
.fields = fields,
.decls = &[_]std.builtin.Type.Declaration{},
},
});
}
// TODO: tests
/// Returns a struct type defining the module's components, e.g.:
///
/// ```
/// struct {
/// location: @TypeOf() = .{ .type = Vec3, .description = null },
/// rotation: @TypeOf() = .{ .type = Vec2, .description = "rotation component" },
/// }
/// ```
fn MComponents(comptime M: anytype) type {
const error_prefix = "mach: module ." ++ @tagName(M.name) ++ " .components ";
if (!@hasDecl(M, "components")) {
return struct {};
}
if (@typeInfo(@TypeOf(M.components)) != .Struct or !@typeInfo(@TypeOf(M.components)).Struct.is_tuple) {
@compileError(error_prefix ++ "expected a tuple of structs, found: " ++ @typeName(@TypeOf(M.components)));
}
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
inline for (M.components, 0..) |component, i| {
const Component = @TypeOf(component);
if (@typeInfo(Component) != .Struct) @compileError(std.fmt.comptimePrint(
error_prefix ++ "expected a tuple of structs, found tuple element ({}): {s}",
.{ i, @typeName(Component) },
));
// Verify .name = .foo component name field
const name_tag = if (@hasField(Component, "name")) component.name else @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) missing field `.name = .foo` (component name)",
.{i},
));
if (@typeInfo(@TypeOf(name_tag)) != .EnumLiteral) @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) expected field `.name = .foo`, found: {s}",
.{ i, @typeName(@TypeOf(name_tag)) },
));
// Verify .type = Foo, field
if (!@hasField(Component, "type")) @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) missing field `.type = Foo`",
.{i},
));
if (@typeInfo(@TypeOf(component.type)) != .Type) @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) expected field `.type = Foo`, found: {s}",
.{ i, @typeName(@TypeOf(component.type)) },
));
const description = blk: {
if (@hasField(Component, "description")) {
if (!isString(@TypeOf(component.description))) @compileError(std.fmt.comptimePrint(
error_prefix ++ "tuple element ({}) expected (optional) field `.description = \"foo\"`, found: {s}",
.{ i, @typeName(@TypeOf(component.description)) },
));
break :blk component.description;
} else break :blk null;
};
const NSComponent = struct {
type: type,
description: ?[]const u8,
};
const ns_component = NSComponent{ .type = component.type, .description = description };
fields = fields ++ [_]std.builtin.Type.StructField{.{
.name = @tagName(name_tag),
.type = NSComponent,
.default_value = &ns_component,
.is_comptime = true,
.alignment = @alignOf(NSComponent),
}};
}
return @Type(.{
.Struct = .{
.layout = .Auto,
.is_tuple = false,
.fields = fields,
.decls = &[_]std.builtin.Type.Declaration{},
},
});
}
// TODO: tests
// TODO: stricter enforcement
fn isString(comptime S: type) bool {
return switch (@typeInfo(S)) {
.Pointer => |p| switch (@typeInfo(p.child)) {
.Array => |a| a.child == u8,
else => false,
},
else => false,
};
}
test {
testing.refAllDeclsRecursive(@This());
}
@ -538,9 +684,8 @@ test Module {
pub const name = .engine_physics;
/// Physics module components
pub const components = struct {
/// A location component
pub const location = @Vector(3, f32);
pub const components = .{
.{ .name = .location, .type = @Vector(3, f32), .description = "A location component" },
};
pub const events = .{
@ -560,9 +705,8 @@ test Modules {
pub const name = .engine_physics;
/// Physics module components
pub const components = struct {
/// A location component
pub const location = @Vector(3, f32);
pub const components = .{
.{ .name = .location, .type = @Vector(3, f32), .description = "A location component" },
};
pub const events = .{
@ -579,7 +723,7 @@ test Modules {
};
/// Renderer module components
pub const components = struct {};
pub const components = .{};
fn tick() !void {}
});
@ -604,7 +748,7 @@ test Modules {
test "event name" {
const Physics = Module(struct {
pub const name = .engine_physics;
pub const components = struct {};
pub const components = .{};
pub const events = .{
.{ .global = .foo, .handler = foo },
.{ .global = .bar, .handler = bar },
@ -620,7 +764,7 @@ test "event name" {
const Renderer = Module(struct {
pub const name = .engine_renderer;
pub const components = struct {};
pub const components = .{};
pub const events = .{
.{ .global = .foo_unused, .handler = fn (f32, i32) void },
.{ .global = .bar_unused, .handler = fn (i32, f32) void },
@ -817,7 +961,7 @@ test "event name calling" {
};
const Physics = Module(struct {
pub const name = .engine_physics;
pub const components = struct {};
pub const components = .{};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .local = .update, .handler = update },
@ -838,7 +982,7 @@ test "event name calling" {
});
const Renderer = Module(struct {
pub const name = .engine_renderer;
pub const components = struct {};
pub const components = .{};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .local = .update, .handler = update },
@ -925,7 +1069,7 @@ test "dispatch" {
});
const Physics = Module(struct {
pub const name = .engine_physics;
pub const components = struct {};
pub const components = .{};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .local = .update, .handler = update },
@ -946,7 +1090,7 @@ test "dispatch" {
});
const Renderer = Module(struct {
pub const name = .engine_renderer;
pub const components = struct {};
pub const components = .{};
pub const events = .{
.{ .global = .tick, .handler = tick },
.{ .global = .frame_done, .handler = fn (i32) void },