module: injected mach.Entity.Mod for global entity operations

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-05-07 14:14:31 -07:00 committed by Stephen Gutekanst
parent cb6bdd7eca
commit 65e2168b9f
11 changed files with 94 additions and 56 deletions

View file

@ -48,6 +48,7 @@ fn init(
// These are injected dependencies - as long as these modules were registered in the top-level
// of the program we can have these types injected here, letting us work with other modules in
// our program seamlessly and with a type-safe API:
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
renderer: *Renderer.Mod,
game: *Mod,
@ -55,7 +56,7 @@ fn init(
renderer.send(.init, .{});
// Create our player entity.
const player = try core.newEntity();
const player = try entity.new();
// Give our player entity a .renderer.position and .renderer.scale component. Note that these
// are defined by the Renderer module, so we use `renderer: *Renderer.Mod` to interact with
@ -81,6 +82,7 @@ fn init(
// TODO(important): remove need for returning an error here
fn tick(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
renderer: *Renderer.Mod,
game: *Mod,
@ -134,7 +136,7 @@ fn tick(
_ = game.state().spawn_timer.lap(); // Reset the timer
for (0..5) |_| {
// Spawn a new entity at the same position as the player, but smaller in scale.
const new_entity = try core.newEntity();
const new_entity = try entity.new();
try renderer.set(new_entity, .position, player_pos);
try renderer.set(new_entity, .scale, 1.0 / 6.0);

View file

@ -60,6 +60,7 @@ fn init(core: *mach.Core.Mod, sprite_pipeline: *gfx.SpritePipeline.Mod, glyphs:
}
fn afterInit(
entity: *mach.Entity.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
glyphs: *Glyphs.Mod,
@ -67,7 +68,7 @@ fn afterInit(
) !void {
// Create a sprite rendering pipeline
const texture = glyphs.state().texture;
const pipeline = try sprite_pipeline.newEntity();
const pipeline = try entity.new();
try sprite_pipeline.set(pipeline, .texture, texture);
sprite_pipeline.send(.update, .{});
@ -76,7 +77,7 @@ fn afterInit(
// type than the `.physics2d` module's `.location` component if you desire.
const r = glyphs.state().regions.get('?').?;
const player = try sprite.newEntity();
const player = try entity.new();
try sprite.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0)));
try sprite.set(player, .pipeline, pipeline);
try sprite.set(player, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height)));
@ -97,6 +98,7 @@ fn afterInit(
}
fn tick(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
@ -150,7 +152,7 @@ fn tick(
const rand_index = game.state().rand.random().intRangeAtMost(usize, 0, glyphs.state().regions.count() - 1);
const r = glyphs.state().regions.entries.get(rand_index).value;
const new_entity = try core.newEntity();
const new_entity = try entity.new();
try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3)));
try sprite.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height)));
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y))));

View file

@ -65,6 +65,7 @@ fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void {
}
fn audioStateChange(
entity: *mach.Entity.Mod,
audio: *mach.Audio.Mod,
app: *Mod,
) !void {
@ -81,20 +82,21 @@ fn audioStateChange(
if (app.get(id, .play_after)) |frequency| {
// Play a new sound
const entity = try audio.newEntity();
try audio.set(entity, .samples, try fillTone(audio, frequency));
try audio.set(entity, .channels, @intCast(audio.state().player.channels().len));
try audio.set(entity, .playing, true);
try audio.set(entity, .index, 0);
const e = try entity.new();
try audio.set(e, .samples, try fillTone(audio, frequency));
try audio.set(e, .channels, @intCast(audio.state().player.channels().len));
try audio.set(e, .playing, true);
try audio.set(e, .index, 0);
}
// Remove the entity for the old sound
try audio.removeEntity(id);
try entity.remove(id);
}
}
}
fn tick(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
audio: *mach.Audio.Mod,
app: *Mod,
@ -121,16 +123,16 @@ fn tick(
// Piano keys
else => {
// Play a new sound
const entity = try audio.newEntity();
try audio.set(entity, .samples, try fillTone(audio, keyToFrequency(ev.key)));
try audio.set(entity, .channels, @intCast(audio.state().player.channels().len));
try audio.set(entity, .playing, true);
try audio.set(entity, .index, 0);
const e = try entity.new();
try audio.set(e, .samples, try fillTone(audio, keyToFrequency(ev.key)));
try audio.set(e, .channels, @intCast(audio.state().player.channels().len));
try audio.set(e, .playing, true);
try audio.set(e, .index, 0);
if (app.state().ghost_key_mode) {
// After that sound plays, we'll chain on another sound that is one semi-tone higher.
const one_semi_tone_higher = keyToFrequency(ev.key) * math.pow(f32, 2.0, (1.0 / 12.0));
try app.set(entity, .play_after, one_semi_tone_higher);
try app.set(e, .play_after, one_semi_tone_higher);
}
},
}

View file

@ -32,7 +32,12 @@ pub const components = .{
sfx: Opus,
fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) !void {
fn init(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
audio: *mach.Audio.Mod,
app: *Mod,
) !void {
audio.send(.init, .{});
app.send(.after_init, .{});
@ -48,7 +53,7 @@ fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) !void {
// Initialize module state
app.init(.{ .sfx = sfx });
const bgm_entity = try audio.newEntity();
const bgm_entity = try entity.new();
try app.set(bgm_entity, .is_bgm, {});
try audio.set(bgm_entity, .samples, bgm.samples);
try audio.set(bgm_entity, .channels, bgm.channels);
@ -75,6 +80,7 @@ fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void {
}
fn audioStateChange(
entity: *mach.Entity.Mod,
audio: *mach.Audio.Mod,
app: *Mod,
) !void {
@ -93,13 +99,14 @@ fn audioStateChange(
try audio.set(id, .playing, true);
} else {
// Remove the entity for the old sound
try audio.removeEntity(id);
try entity.remove(id);
}
}
}
}
fn tick(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
audio: *mach.Audio.Mod,
app: *Mod,
@ -121,11 +128,11 @@ fn tick(
},
else => {
// Play a new SFX
const entity = try audio.newEntity();
try audio.set(entity, .samples, app.state().sfx.samples);
try audio.set(entity, .channels, app.state().sfx.channels);
try audio.set(entity, .index, 0);
try audio.set(entity, .playing, true);
const e = try entity.new();
try audio.set(e, .samples, app.state().sfx.samples);
try audio.set(e, .channels, app.state().sfx.channels);
try audio.set(e, .index, 0);
try audio.set(e, .playing, true);
},
},
.close => core.send(.exit, .{}),

View file

@ -52,6 +52,7 @@ fn deinit(
}
fn init(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
@ -65,12 +66,12 @@ fn init(
// Create a sprite rendering pipeline
const allocator = gpa.allocator();
const pipeline = try core.newEntity();
const pipeline = try entity.new();
try sprite_pipeline.set(pipeline, .texture, try loadTexture(core, allocator));
sprite_pipeline.send(.update, .{});
// Create our player sprite
const player = try core.newEntity();
const player = try entity.new();
try sprite.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0)));
try sprite.set(player, .size, vec2(32, 32));
try sprite.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0)));
@ -94,6 +95,7 @@ fn init(
}
fn tick(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
@ -143,7 +145,7 @@ fn tick(
new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 25;
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 25;
const new_entity = try core.newEntity();
const new_entity = try entity.new();
try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3))));
try sprite.set(new_entity, .size, vec2(32, 32));
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0)));

View file

@ -63,6 +63,7 @@ fn deinit(
}
fn init(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
@ -73,21 +74,21 @@ fn init(
// TODO: a better way to initialize entities with default values
// TODO(text): most of these style options are not respected yet.
const style1 = try core.newEntity();
const style1 = try entity.new();
try text_style.set(style1, .font_name, "Roboto Medium"); // TODO
try text_style.set(style1, .font_size, 48 * gfx.px_per_pt); // 48pt
try text_style.set(style1, .font_weight, gfx.font_weight_normal);
try text_style.set(style1, .italic, false);
try text_style.set(style1, .color, vec4(0.6, 1.0, 0.6, 1.0));
const style2 = try core.newEntity();
const style2 = try entity.new();
try text_style.set(style2, .font_name, "Roboto Medium"); // TODO
try text_style.set(style2, .font_size, 48 * gfx.px_per_pt); // 48pt
try text_style.set(style2, .font_weight, gfx.font_weight_normal);
try text_style.set(style2, .italic, true);
try text_style.set(style2, .color, vec4(0.6, 1.0, 0.6, 1.0));
const style3 = try core.newEntity();
const style3 = try entity.new();
try text_style.set(style3, .font_name, "Roboto Medium"); // TODO
try text_style.set(style3, .font_size, 48 * gfx.px_per_pt); // 48pt
try text_style.set(style3, .font_weight, gfx.font_weight_bold);
@ -95,12 +96,12 @@ fn init(
try text_style.set(style3, .color, vec4(0.6, 1.0, 0.6, 1.0));
// Create a text rendering pipeline
const pipeline = try core.newEntity();
const pipeline = try entity.new();
try text_pipeline.set(pipeline, .is_pipeline, {});
text_pipeline.send(.update, .{});
// Create some text
const player = try core.newEntity();
const player = try entity.new();
try text.set(player, .pipeline, pipeline);
try text.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0))));
@ -132,6 +133,7 @@ fn init(
}
fn tick(
entity: *mach.Entity.Mod,
core: *mach.Core.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
@ -181,7 +183,7 @@ fn tick(
new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 50;
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 50;
const new_entity = try core.newEntity();
const new_entity = try entity.new();
try text.set(new_entity, .pipeline, game.state().pipeline);
try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));

View file

@ -106,7 +106,7 @@ fn start(core: *Mod) !void {
core.state().run_state = .running;
}
fn init(core: *Mod) !void {
fn init(entity: *mach.Entity.Mod, core: *Mod) !void {
mach.core.allocator = gpa.allocator(); // TODO: banish this global allocator
// Initialize GPU implementation
@ -116,7 +116,7 @@ fn init(core: *Mod) !void {
try mach.core.init(.{});
// TODO(important): update this information upon framebuffer resize events
const main_window = try core.newEntity();
const main_window = try entity.new();
try core.set(main_window, .framebuffer_format, mach.core.descriptor.format);
try core.set(main_window, .framebuffer_width, mach.core.descriptor.width);
try core.set(main_window, .framebuffer_height, mach.core.descriptor.height);

View file

@ -40,7 +40,7 @@ pub const EventID = @import("module/main.zig").EventID;
pub const AnyEvent = @import("module/main.zig").AnyEvent;
pub const merge = @import("module/main.zig").merge;
pub const builtin_modules = @import("module/main.zig").builtin_modules;
pub const EntityModule = @import("module/main.zig").EntityModule;
pub const Entity = @import("module/main.zig").Entity;
/// To use experimental sysgpu graphics API, you can write this in your main.zig:
///

View file

@ -9,7 +9,7 @@ const StringTable = @import("StringTable.zig");
const ComponentTypesByName = @import("module.zig").ComponentTypesByName;
const merge = @import("main.zig").merge;
const builtin_modules = @import("main.zig").builtin_modules;
const EntityModule = @import("main.zig").EntityModule;
const Entity = @import("main.zig").Entity;
const ModuleName = @import("module.zig").ModuleName;
const ComponentNameM = @import("module.zig").ComponentNameM;
const ComponentName = @import("module.zig").ComponentName;
@ -130,7 +130,7 @@ pub fn Entities(comptime modules: anytype) type {
.component_names = component_names,
.buckets = buckets,
};
entities.id_name = entities.componentName(EntityModule.name, .id);
entities.id_name = entities.componentName(Entity.name, .id);
const columns = try allocator.alloc(Archetype.Column, 1);
columns[0] = .{
@ -1076,7 +1076,7 @@ test "example" {
// Query for all entities that have all of the given components
const W = @TypeOf(world);
var q = try world.query(.{
.ids = W.ComponentQuery{ .read = W.ModuleComponentName{ .module = EntityModule.name, .component = .id } },
.ids = W.ComponentQuery{ .read = W.ModuleComponentName{ .module = Entity.name, .component = .id } },
.rotations = W.ComponentQuery{ .write = W.ModuleComponentName{ .module = Game.name, .component = .rotation } },
});
while (q.next()) |v| {
@ -1090,7 +1090,7 @@ test "example" {
// Dynamic queries (e.g. issued from another programming language without comptime)
var q2 = try world.queryDynamic(.{
.op_and = &.{
.{ .read = world.componentName(EntityModule.name, .id) },
.{ .read = world.componentName(Entity.name, .id) },
.{ .read = world.componentName(Game.name, .rotation) },
},
});

View file

@ -13,10 +13,10 @@ pub const AnyEvent = @import("module.zig").AnyEvent;
pub const Merge = @import("module.zig").Merge;
pub const merge = @import("module.zig").merge;
pub const builtin_modules = .{EntityModule};
pub const builtin_modules = .{Entity};
/// Builtin .entity module
pub const EntityModule = struct {
pub const Entity = struct {
pub const name = .entity;
pub const Mod = mach.Mod(@This());
@ -82,14 +82,15 @@ test "entities DB" {
defer world.deinit(allocator);
// Initialize module state.
var entity = &world.mod.entity;
var physics = &world.mod.physics;
var renderer = &world.mod.renderer;
physics.init(.{ .pointer = 123 });
_ = physics.state().pointer; // == 123
const player1 = try physics.newEntity();
const player2 = try physics.newEntity();
const player3 = try physics.newEntity();
const player1 = try entity.new();
const player2 = try entity.new();
const player3 = try entity.new();
try physics.set(player1, .id, 1001);
try renderer.set(player1, .id, 1001);

View file

@ -574,6 +574,36 @@ pub fn ModSet(comptime modules: anytype) type {
pub fn Mod(comptime M: anytype) type {
const module_tag = M.name;
const components = ComponentTypesM(M){};
if (M.name == .entity) {
// The .entity module is a special builtin module, with its own unique API.
return struct {
/// Private/internal fields
__entities: *Entities(modules),
__is_initialized: bool,
__state: void,
pub const IsInjectedArgument = void;
pub inline fn read(comptime component_name: ComponentNameM(M)) Entities(modules).ComponentQuery {
return .{ .read = .{
.module = M.name,
.component = comptime stringToEnum(ComponentName(modules), @tagName(component_name)).?,
} };
}
/// Returns a new entity.
pub inline fn new(m: *@This()) !EntityID {
return m.__entities.new();
}
/// Removes an entity.
pub inline fn remove(m: *@This(), entity: EntityID) !void {
try m.__entities.remove(entity);
}
};
}
return struct {
/// Private/internal fields
__entities: *Entities(modules),
@ -622,16 +652,6 @@ pub fn ModSet(comptime modules: anytype) type {
return &m.__state;
}
/// Returns a new entity.
pub inline fn newEntity(m: *@This()) !EntityID {
return m.__entities.new();
}
/// Removes an entity.
pub inline fn removeEntity(m: *@This(), entity: EntityID) !void {
try m.__entities.remove(entity);
}
/// 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.