Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions assets/shaders/vulkan/terrain.vert
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ layout(set = 0, binding = 0) uniform GlobalUniforms {

layout(push_constant) uniform ModelUniforms {
mat4 model;
vec3 color_override;
float mask_radius;
float _pad0;
float _pad1;
float _pad2;
} model_data;

void main() {
Expand All @@ -53,7 +51,7 @@ void main() {
gl_Position = clipPos;
gl_Position.y = -gl_Position.y;

vColor = aColor;
vColor = aColor * model_data.color_override;
vNormal = aNormal;
vTexCoord = aTexCoord;
vTileID = int(aTileID);
Expand Down
93 changes: 93 additions & 0 deletions src/ecs_tests.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const std = @import("std");
const testing = std.testing;
const ecs_manager = @import("engine/ecs/manager.zig");
const ECSRegistry = ecs_manager.Registry;
const ecs_components = @import("engine/ecs/components.zig");
const Vec3 = @import("zig-math").Vec3;

test "ECS registry basic operations" {
const allocator = testing.allocator;
var registry = ECSRegistry.init(allocator);
defer registry.deinit();

const e1 = registry.create();
const e2 = registry.create();

try registry.transforms.set(e1, .{ .position = Vec3.init(1, 2, 3) });
try registry.physics.set(e1, .{ .aabb_size = Vec3.init(1, 1, 1) });
try registry.transforms.set(e2, .{ .position = Vec3.init(4, 5, 6) });

try testing.expect(registry.transforms.has(e1));
try testing.expect(registry.physics.has(e1));
try testing.expect(registry.transforms.has(e2));
try testing.expect(!registry.physics.has(e2));

const t1 = registry.transforms.get(e1).?;
try testing.expectEqual(@as(f32, 1), t1.position.x);
}

test "ECS Query API" {
const allocator = testing.allocator;
var registry = ECSRegistry.init(allocator);
defer registry.deinit();

const e1 = registry.create();
const e2 = registry.create();
const e3 = registry.create();

try registry.transforms.set(e1, .{ .position = Vec3.init(1, 0, 0) });
try registry.physics.set(e1, .{ .aabb_size = Vec3.one });

try registry.transforms.set(e2, .{ .position = Vec3.init(2, 0, 0) });
// e2 has no Physics

try registry.transforms.set(e3, .{ .position = Vec3.init(3, 0, 0) });
try registry.physics.set(e3, .{ .aabb_size = Vec3.one });

var count: usize = 0;
var query = registry.query(.{ ecs_components.Transform, ecs_components.Physics });
while (query.next()) |row| {
count += 1;
// Check if components are correct
try testing.expect(row.components[0].position.x == 1.0 or row.components[0].position.x == 3.0);
}

try testing.expectEqual(@as(usize, 2), count);
}

test "ECS Serialization" {
const allocator = testing.allocator;
var registry = ECSRegistry.init(allocator);
defer registry.deinit();

const e1 = registry.create();
try registry.transforms.set(e1, .{ .position = Vec3.init(10, 20, 30) });
try registry.meshes.set(e1, .{ .color = Vec3.init(1, 0, 0) });

// Take snapshot
const snapshot = try registry.takeSnapshot(allocator);
defer snapshot.deinit(allocator);

try testing.expectEqual(@as(usize, 1), snapshot.entities.len);
try testing.expectEqual(e1, snapshot.entities[0]);

// Load into new registry
var registry2 = ECSRegistry.init(allocator);
defer registry2.deinit();
try registry2.loadSnapshot(snapshot);

try testing.expect(registry2.transforms.has(e1));
try testing.expect(registry2.meshes.has(e1));
try testing.expectEqual(@as(f32, 10), registry2.transforms.get(e1).?.position.x);

// Test JSON
const json_str = try std.json.Stringify.valueAlloc(allocator, snapshot, .{});
defer allocator.free(json_str);

var registry3 = ECSRegistry.init(allocator);
defer registry3.deinit();
try registry3.loadFromJson(allocator, json_str);

try testing.expect(registry3.transforms.has(e1));
try testing.expectEqual(@as(f32, 10), registry3.transforms.get(e1).?.position.x);
}
27 changes: 27 additions & 0 deletions src/engine/ecs/components.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Component definitions.

const std = @import("std");
const math = @import("zig-math");
const Vec3 = math.Vec3;
const AABB = math.AABB;

pub const Transform = struct {
/// World-space position; rendering converts to camera-relative for floating origin.
position: Vec3,
rotation: Vec3 = Vec3.zero, // Euler angles (pitch, yaw, roll)
scale: Vec3 = Vec3.one,
};

pub const Physics = struct {
velocity: Vec3 = Vec3.zero,
acceleration: Vec3 = Vec3.zero,
aabb_size: Vec3, // Width, Height, Depth relative to position
grounded: bool = false,
use_gravity: bool = true,
};

pub const Mesh = struct {
/// For now, just a color for debug rendering
color: Vec3 = Vec3.init(1.0, 0.0, 1.0), // Magenta by default
visible: bool = true,
};
5 changes: 5 additions & 0 deletions src/engine/ecs/entity.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Entity definition and ID generation.
//
// EntityId lives here to avoid circular imports between storage and registry.

pub const EntityId = u64;
217 changes: 217 additions & 0 deletions src/engine/ecs/manager.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//! ECS Registry/Manager.

const std = @import("std");
const EntityId = @import("entity.zig").EntityId;
const ComponentStorage = @import("storage.zig").ComponentStorage;
const components = @import("components.zig");

pub const Registry = struct {
allocator: std.mem.Allocator,
next_entity_id: EntityId = 1,

// Component Storages
transforms: ComponentStorage(components.Transform),
physics: ComponentStorage(components.Physics),
meshes: ComponentStorage(components.Mesh),

pub fn init(allocator: std.mem.Allocator) Registry {
return .{
.allocator = allocator,
.transforms = ComponentStorage(components.Transform).init(allocator),
.physics = ComponentStorage(components.Physics).init(allocator),
.meshes = ComponentStorage(components.Mesh).init(allocator),
};
}

pub fn deinit(self: *Registry) void {
self.transforms.deinit();
self.physics.deinit();
self.meshes.deinit();
}

pub fn create(self: *Registry) EntityId {
const id = self.next_entity_id;
// Check for overflow (extremely unlikely but good practice)
if (self.next_entity_id == std.math.maxInt(EntityId)) {
@panic("Entity ID overflow");
}
self.next_entity_id += 1;
return id;
}

pub fn destroy(self: *Registry, entity: EntityId) void {
_ = self.transforms.remove(entity);
_ = self.physics.remove(entity);
_ = self.meshes.remove(entity);
}

pub fn clear(self: *Registry) void {
self.transforms.clear();
self.physics.clear();
self.meshes.clear();
self.next_entity_id = 1;
}

/// Returns a query iterator for the given component types.
/// Example: registry.query(.{ Transform, Physics })
pub fn query(self: *Registry, comptime component_types: anytype) Query(component_types) {
return Query(component_types).init(self);
}

pub fn Query(comptime component_types: anytype) type {
return struct {
const Self = @This();
registry: *Registry,
index: usize = 0,

pub fn init(registry: *Registry) Self {
return .{ .registry = registry };
}

pub const Row = struct {
entity: EntityId,
components: ComponentsTuple,
};

const ComponentsTuple = blk: {
var fields: [component_types.len]std.builtin.Type.StructField = undefined;
for (component_types, 0..) |T, i| {
const field_name = std.fmt.comptimePrint("{}", .{i});
fields[i] = .{
.name = (field_name ++ "\x00")[0..field_name.len :0],
.type = *T,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(*T),
};
}
break :blk @Type(.{
.@"struct" = .{
.layout = .auto,
.backing_integer = null,
.fields = &fields,
.decls = &.{},
.is_tuple = true,
},
});
};

pub fn next(self: *Self) ?Row {
// Use the first component storage as the primary source of entities.
const PrimaryType = component_types[0];
const primary_storage = self.registry.getStorage(PrimaryType);

while (self.index < primary_storage.entities.items.len) {
const entity = primary_storage.entities.items[self.index];
self.index += 1;

var comp_tuple: ComponentsTuple = undefined;
var all_present = true;

inline for (component_types, 0..) |T, i| {
if (self.registry.getStorage(T).getPtr(entity)) |ptr| {
comp_tuple[i] = ptr;
} else {
all_present = false;
break;
}
}

if (all_present) {
return Row{
.entity = entity,
.components = comp_tuple,
};
}
}

return null;
}
};
}

/// Internal helper to get storage by type
fn getStorage(self: *Registry, comptime T: type) *ComponentStorage(T) {
if (T == components.Transform) return &self.transforms;
if (T == components.Physics) return &self.physics;
if (T == components.Mesh) return &self.meshes;
@compileError("Unsupported component type for query: " ++ @typeName(T));
}

pub const Snapshot = struct {
next_entity_id: EntityId,
entities: []const EntityId,
transforms: []const ?components.Transform,
physics: []const ?components.Physics,
meshes: []const ?components.Mesh,

pub fn deinit(self: Snapshot, allocator: std.mem.Allocator) void {
allocator.free(self.entities);
allocator.free(self.transforms);
allocator.free(self.physics);
allocator.free(self.meshes);
}
};

pub fn takeSnapshot(self: *Registry, allocator: std.mem.Allocator) !Snapshot {
// Collect all unique entities
var entities_map = std.AutoHashMap(EntityId, void).init(allocator);
defer entities_map.deinit();

for (self.transforms.entities.items) |id| try entities_map.put(id, {});
for (self.physics.entities.items) |id| try entities_map.put(id, {});
for (self.meshes.entities.items) |id| try entities_map.put(id, {});

const entities = try allocator.alloc(EntityId, entities_map.count());
var it = entities_map.keyIterator();
var i: usize = 0;
while (it.next()) |id| {
entities[i] = id.*;
i += 1;
}

const transforms = try allocator.alloc(?components.Transform, entities.len);
const physics = try allocator.alloc(?components.Physics, entities.len);
const meshes = try allocator.alloc(?components.Mesh, entities.len);

for (entities, 0..) |id, idx| {
transforms[idx] = self.transforms.get(id);
physics[idx] = self.physics.get(id);
meshes[idx] = self.meshes.get(id);
}

return Snapshot{
.next_entity_id = self.next_entity_id,
.entities = entities,
.transforms = transforms,
.physics = physics,
.meshes = meshes,
};
}

pub fn loadSnapshot(self: *Registry, snapshot: Snapshot) !void {
self.clear();
self.next_entity_id = snapshot.next_entity_id;

for (snapshot.entities, 0..) |id, idx| {
if (snapshot.transforms[idx]) |val| try self.transforms.set(id, val);
if (snapshot.physics[idx]) |val| try self.physics.set(id, val);
if (snapshot.meshes[idx]) |val| try self.meshes.set(id, val);
}
}

pub fn saveToJson(self: *Registry, allocator: std.mem.Allocator, writer: anytype) !void {
const snapshot = try self.takeSnapshot(allocator);
defer snapshot.deinit(allocator);
try std.json.stringify(snapshot, .{}, writer);
}

pub fn loadFromJson(self: *Registry, allocator: std.mem.Allocator, content: []const u8) !void {
const parsed = try std.json.parseFromSlice(Snapshot, allocator, content, .{
.ignore_unknown_fields = true,
});
defer parsed.deinit();

try self.loadSnapshot(parsed.value);
}
};
Loading
Loading