From 5659dd6266ccef3ffcfdf417a59b60b972ab0c5b Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sat, 6 Apr 2024 12:09:29 -0700 Subject: [PATCH] examples/custom-renderer: cleanup docs Signed-off-by: Stephen Gutekanst --- examples/custom-renderer/Game.zig | 131 ++++++++++++++++---------- examples/custom-renderer/Renderer.zig | 13 +-- examples/custom-renderer/main.zig | 4 +- examples/custom-renderer/shader.wgsl | 1 + 4 files changed, 91 insertions(+), 58 deletions(-) diff --git a/examples/custom-renderer/Game.zig b/examples/custom-renderer/Game.zig index a5487990..d0179746 100644 --- a/examples/custom-renderer/Game.zig +++ b/examples/custom-renderer/Game.zig @@ -10,51 +10,65 @@ const vec2 = math.vec2; const Vec2 = math.Vec2; const Vec3 = math.Vec3; +// Global state for our game module. timer: mach.Timer, player: ecs.EntityID, direction: Vec2 = vec2(0, 0), spawning: bool = false, spawn_timer: mach.Timer, +// Components our game module defines. pub const components = .{ + // Whether an entity is a "follower" of our player entity or not. The type is void because we + // don't need any information, this is just a tag we assign to an entity with no data. .follower = .{ .type = void }, }; +// Global events that we will listen for. pub const global_events = .{ - .init = .{ .handler = init }, - .tick = .{ .handler = tick }, + .init = .{ .handler = init }, // Event sent on app startup + .tick = .{ .handler = tick }, // Event sent on each frame }; -// Each module must have a globally unique name declared, it is impossible to use two modules with -// the same name in a program. To avoid name conflicts, we follow naming conventions: -// -// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it -// provides. -// 2. Single-word names like `.renderer`, `.game`, etc. are reserved for the application itself. -// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` -// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally -// unique. -// +// Define the globally unique name of our module. You can use any name here, but keep in mind no +// two modules in the program can have the same name. pub const name = .game; + +// The mach.Mod type corresponding to our module struct (this file.) This provides methods for +// working with this module (e.g. sending events, working with its components, etc.) +// +// Note that Mod.state() returns an instance of our module struct. pub const Mod = mach.Mod(@This()); -// TODO(engine): remove need for returning an error here +// TODO(important): remove need for returning an error here 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: engine: *mach.Engine.Mod, renderer: *Renderer.Mod, game: *Mod, ) !void { // The Mach .core is where we set window options, etc. + // TODO(important): replace this API with something better core.setTitle("Hello, ECS!"); - // We can create entities, and set components on them. Note that components live in a module - // namespace, e.g. the `.renderer` module could have a 3D `.location` component with a different - // type than the `.physics2d` module's `.location` component if you desire. - + // Create our player entity. const player = try engine.newEntity(); - try renderer.set(player, .location, vec3(0, 0, 0)); + + // 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 + // them. + // + // Components live in a module's namespace, so e.g. a physics2d module and renderer3d module could + // both define a .position component with a different data type, and both could be added to the + // same entity. + try renderer.set(player, .position, vec3(0, 0, 0)); try renderer.set(player, .scale, 1.0); + // Initialize our game module's state - these are the struct fields defined at the top of this + // file. If this is not done, then game.state() will panic indicating the state was never + // initialized. game.init(.{ .timer = try mach.Timer.start(), .spawn_timer = try mach.Timer.start(), @@ -62,13 +76,13 @@ fn init( }); } -// TODO(engine): remove need for returning an error here +// TODO(important): remove need for returning an error here fn tick( engine: *mach.Engine.Mod, renderer: *Renderer.Mod, game: *Mod, ) !void { - // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. + // TODO(important): event polling should occur in mach.Engine module and get fired as ECS event. var iter = core.pollEvents(); var direction = game.state().direction; var spawning = game.state().spawning; @@ -98,33 +112,56 @@ fn tick( else => {}, } } + + // Keep track of which direction we want the player to move based on input, and whether we want + // to be spawning entities. + // + // Note that game.state() simply returns a pointer to a global singleton of the struct defined + // by this file, so we can access fields defined at the top of this file. game.state().direction = direction; game.state().spawning = spawning; - var player_pos = renderer.get(game.state().player, .location).?; + // Get the current player position + var player_pos = renderer.get(game.state().player, .position).?; + + // If we want to spawn new entities, then spawn them now. The timer just makes spawning rate + // independent of frame rate. if (spawning and game.state().spawn_timer.read() > 1.0 / 60.0) { - for (0..10) |_| { - // Spawn a new follower entity - _ = game.state().spawn_timer.lap(); + _ = 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 engine.newEntity(); - try game.set(new_entity, .follower, {}); - try renderer.set(new_entity, .location, player_pos); + try renderer.set(new_entity, .position, player_pos); try renderer.set(new_entity, .scale, 1.0 / 6.0); + + // Tag the entity as one that follows the player + try game.set(new_entity, .follower, {}); } } // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. const delta_time = game.state().timer.lap(); - // Move following entities closer to us. + // Calculate the player position, by moving in the direction the player wants to go + // by the speed amount. + const speed = 1.0; + player_pos.v[0] += direction.x() * speed * delta_time; + player_pos.v[1] += direction.y() * speed * delta_time; + try renderer.set(game.state().player, .position, player_pos); + + // Query all the entities that have the .follower tag indicating they should follow the player. + // TODO(important): better querying API var archetypes_iter = engine.entities.query(.{ .all = &.{ .{ .game = &.{.follower} }, } }); while (archetypes_iter.next()) |archetype| { + // Iterate the ID and position of each entity const ids = archetype.slice(.entity, .id); - const locations = archetype.slice(.renderer, .location); - for (ids, locations) |id, location| { - // Avoid other follower entities by moving away from them if they are close to us. + const positions = archetype.slice(.renderer, .position); + for (ids, positions) |id, position| { + // Nested query to find all the other follower entities that we should move away from. + // We will avoid all other follower entities if we're too close to them. + // This is not very efficient, but it works! const close_dist = 1.0 / 15.0; var avoidance = Vec3.splat(0); var avoidance_div: f32 = 1.0; @@ -133,37 +170,33 @@ fn tick( } }); while (archetypes_iter_2.next()) |archetype_2| { const other_ids = archetype_2.slice(.entity, .id); - const other_locations = archetype_2.slice(.renderer, .location); - for (other_ids, other_locations) |other_id, other_location| { + const other_positions = archetype_2.slice(.renderer, .position); + for (other_ids, other_positions) |other_id, other_position| { if (id == other_id) continue; - if (location.dist(&other_location) < close_dist) { - avoidance = avoidance.sub(&location.dir(&other_location, 0.0000001)); + if (position.dist(&other_position) < close_dist) { + avoidance = avoidance.sub(&position.dir(&other_position, 0.0000001)); avoidance_div += 1.0; } } } - // Avoid the player + + // Avoid the player if we're too close to it var avoid_player_multiplier: f32 = 1.0; - if (location.dist(&player_pos) < close_dist * 6.0) { - avoidance = avoidance.sub(&location.dir(&player_pos, 0.0000001)); + if (position.dist(&player_pos) < close_dist * 6.0) { + avoidance = avoidance.sub(&position.dir(&player_pos, 0.0000001)); avoidance_div += 1.0; avoid_player_multiplier = 4.0; } - // Move away from things we want to avoid + // Determine our new position, taking into account things we want to avoid const move_speed = 1.0 * delta_time; - var new_location = location.add(&avoidance.divScalar(avoidance_div).mulScalar(move_speed * avoid_player_multiplier)); + var new_position = position.add(&avoidance.divScalar(avoidance_div).mulScalar(move_speed * avoid_player_multiplier)); - // Move towards the center - new_location = new_location.lerp(&vec3(0, 0, 0), move_speed / avoidance_div); - try renderer.set(id, .location, new_location); + // Try to move towards the center of the world if we don't need to avoid something else + new_position = new_position.lerp(&vec3(0, 0, 0), move_speed / avoidance_div); + + // Finally, update our entity position. + try renderer.set(id, .position, new_position); } } - - // Calculate the player position, by moving in the direction the player wants to go - // by the speed amount. - const speed = 1.0; - player_pos.v[0] += direction.x() * speed * delta_time; - player_pos.v[1] += direction.y() * speed * delta_time; - try renderer.set(game.state().player, .location, player_pos); } diff --git a/examples/custom-renderer/Renderer.zig b/examples/custom-renderer/Renderer.zig index 36759ed4..de5f7caf 100644 --- a/examples/custom-renderer/Renderer.zig +++ b/examples/custom-renderer/Renderer.zig @@ -1,3 +1,4 @@ +// TODO(important): docs const std = @import("std"); const mach = @import("mach"); @@ -21,7 +22,7 @@ pub const name = .renderer; pub const Mod = mach.Mod(@This()); pub const components = .{ - .location = .{ .type = Vec3 }, + .position = .{ .type = Vec3 }, .rotation = .{ .type = Vec3 }, .scale = .{ .type = f32 }, }; @@ -32,7 +33,7 @@ pub const global_events = .{ .tick = .{ .handler = tick }, }; -// TODO: this shouldn't be a packed struct, it should be extern. +// TODO(important): this shouldn't be a packed struct, it should be extern. const UniformBufferObject = packed struct { offset: Vec3.Vector, scale: f32, @@ -134,18 +135,18 @@ fn tick( // Update uniform buffer var archetypes_iter = engine.entities.query(.{ .all = &.{ - .{ .renderer = &.{ .location, .scale } }, + .{ .renderer = &.{ .position, .scale } }, } }); var num_entities: usize = 0; while (archetypes_iter.next()) |archetype| { const ids = archetype.slice(.entity, .id); - const locations = archetype.slice(.renderer, .location); + const positions = archetype.slice(.renderer, .position); const scales = archetype.slice(.renderer, .scale); - for (ids, locations, scales) |id, location, scale| { + for (ids, positions, scales) |id, position, scale| { _ = id; const ubo = UniformBufferObject{ - .offset = location.v, + .offset = position.v, .scale = scale, }; encoder.writeBuffer(renderer.state().uniform_buffer, uniform_offset * num_entities, &[_]UniformBufferObject{ubo}); diff --git a/examples/custom-renderer/main.zig b/examples/custom-renderer/main.zig index 74c2a44f..d378073a 100644 --- a/examples/custom-renderer/main.zig +++ b/examples/custom-renderer/main.zig @@ -1,11 +1,9 @@ -// Experimental ECS app example. Not yet ready for actual use. const mach = @import("mach"); const Renderer = @import("Renderer.zig"); const Game = @import("Game.zig"); -// The list of modules to be used in our application. Our game itself is implemented in our own -// module called Game. +// The global list of Mach modules registered for use in our application. pub const modules = .{ mach.Engine, Renderer, diff --git a/examples/custom-renderer/shader.wgsl b/examples/custom-renderer/shader.wgsl index 20e08214..1ee56549 100644 --- a/examples/custom-renderer/shader.wgsl +++ b/examples/custom-renderer/shader.wgsl @@ -1,3 +1,4 @@ +// TODO(important): docs struct Uniform { pos: vec3, scale: f32,