Skip to content

Latest commit

 

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MiGu

中文说明

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.

Install

Fetch MiGu in your project:

zig fetch --save=migu git+https://github.com/jiangbo/MiGu.git

Then 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");

Basic Example

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);
}

Components

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);

Queries

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{});
        }
    }
}

Identity

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.

Handle

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{});
}

Resource

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

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);

Notes

  • Entity is u16.
  • createEntity, add, and addEvent panic on allocation failure.
  • Use tryCreateEntity, tryAdd, and tryAddEvent when 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.

Acknowledgements

MiGu is heavily inspired by EnTT and zig-ecs. Thanks to both projects.

License

MIT

About

MiGu is a tiny ECS for Zig.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages