MiGu is a tiny ECS for Zig.
It is designed for small single-threaded games and tools. Entity ids are
u16, so the maximum entity count is limited.
The name comes from MiGu (迷毂), a plant from ShanHaiJing, also known as Classic of Mountains and Rivers.
Fetch MiGu in your project:
zig fetch --save=migu git+https://github.com/jiangbo/MiGu.gitThen import the module in build.zig:
const migu = b.dependency("migu", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("ecs", migu.module("ecs"));Then import it in code:
const ecs = @import("ecs");const std = @import("std");
const ecs = @import("ecs");
const Position = struct { x: f32 = 0, y: f32 = 0 };
const Velocity = struct { x: f32 = 0, y: f32 = 0 };
fn move(world: *ecs.World, delta: f32) void {
var query = world.query(.{ Position, Velocity });
while (query.next()) |entity| {
const velocity = query.get(entity, Velocity);
const position = query.getPtr(entity, Position);
position.x += velocity.x * delta;
position.y += velocity.y * delta;
}
}
test "move entity" {
var world = ecs.World.init(std.testing.allocator);
defer world.deinit();
const entity = world.createEntity();
world.add(entity, Position{ .x = 10, .y = 20 });
world.add(entity, Velocity{ .x = 5, .y = -2 });
move(&world, 2);
const position = world.get(entity, Position).?;
try std.testing.expectEqual(20, position.x);
try std.testing.expectEqual(16, position.y);
}const Position = struct { x: f32, y: f32 };
const Player = struct {};Add components:
world.add(entity, Position{ .x = 1, .y = 2 });
world.add(entity, Player{});Read components:
const position = world.get(entity, Position).?;
const position_ptr = world.getPtr(entity, Position).?;Remove components:
world.remove(entity, Player);Use query for entities that have all requested components.
var query = world.query(.{ Position, Velocity });
while (query.next()) |entity| {
const position = query.getPtr(entity, Position);
const velocity = query.get(entity, Velocity);
_ = .{ position, velocity };
}Use queryNot to exclude components.
var query = world.queryNot(.{ Position, Sprite }, .{Hidden});Use queryBy when iteration order must follow a specific component store.
world.sort(Render, lessThanRender);
var query = world.queryBy(Render, .{ Position, Sprite }, .{Hidden});Use reverse for destructive passes.
var query = world.query(.{ Dead }).reverse();
while (query.next()) |entity| {
world.destroyEntity(entity);
}Use query.add to safely add components while iterating. It asserts if the
new component is part of the current query.
fn markIdle(world: *ecs.World) void {
var query = world.query(.{ Position, Velocity });
while (query.next()) |entity| {
if (query.get(entity, Velocity).x == 0) {
query.add(world, entity, Idle{});
}
}
}An identity stores one entity for a type, such as the player.
const Player = struct {};
const Position = struct { x: f32 = 0, y: f32 = 0 };
const Camera = struct { x: f32 = 0, y: f32 = 0 };
fn followPlayer(world: *ecs.World, camera: *Camera) void {
const player = world.getIdentity(Player) orelse return;
const position = world.get(player, Position) orelse return;
camera.x = position.x;
camera.y = position.y;
}
const player = world.createIdentity(Player);
world.add(player, Position{ .x = 10, .y = 20 });It does not create a component automatically. It only records the entity id.
Use Entity directly by default. Use Handle only when an entity may be
destroyed and its id may be reused later.
const enemy = world.createEntity();
const handle = world.entities.to(enemy).?;
world.destroyEntity(enemy);
if (world.entities.get(handle)) |alive| {
world.add(alive, Target{});
}world.entity is an optional entity slot. One simple resource pattern is to
create one entity for global components.
If you use this pattern, create it right after World.init.
const Clock = struct { hour: u8 = 6 };
const Inventory = struct { gold: u32 = 0 };
var world = ecs.World.init(allocator);
defer world.deinit();
world.entity = world.createEntity();
world.add(world.entity, Clock{});
world.add(world.entity, Inventory{});
const clock = world.getPtr(world.entity, Clock).?;
clock.hour += 1;When resetting a world, resetKeep can keep selected component stores.
world.resetKeep(.{ Clock, Inventory });
world.entity = world.createEntity();Events are typed queues. They are not cleared automatically.
const SoundPlay = struct { id: u8 };
world.addEvent(SoundPlay{ .id = 1 });
for (world.getEvent(SoundPlay)) |event| {
playSound(event.id);
}
world.clearEvent(SoundPlay);Entityisu16.createEntity,add, andaddEventpanic on allocation failure.- Use
tryCreateEntity,tryAdd, andtryAddEventwhen you need errors. - Component ids are based on Zig types. A type alias is not a new component type.
const Name = struct {};
const PlayerName = Name;
// Name and PlayerName are the same component type.MiGu is heavily inspired by EnTT and zig-ecs. Thanks to both projects.
MIT