You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
513 lines
21 KiB
513 lines
21 KiB
const std = @import("std"); |
|
const Allocator = std.mem.Allocator; |
|
|
|
const c = @import("./c.zig"); |
|
const errors = @import("./errors.zig"); |
|
const meta = @import("./meta.zig"); |
|
const Path = @import("./path.zig"); |
|
|
|
const flecs = @import("./builtin/flecs.zig"); |
|
const ChildOf = flecs.core.ChildOf; |
|
|
|
pub const EntityError = error{ |
|
/// Id is `0`. |
|
IsNone, |
|
/// Id is not valid. |
|
IsInvalid, |
|
/// Id is not an `Entity`. |
|
IsNotEntity, |
|
/// Generation doesn't match. |
|
GenMismatch, |
|
/// Entity is not alive. |
|
IsNotAlive, |
|
// TODO: Rename this to `EntityMissing` and similar, use it elsewhere in the code. |
|
}; |
|
|
|
/// An `Entity` is an `Id` which represents a "thing" within the world. This |
|
/// could be a traditional game object, but is also able to represent other |
|
/// concepts such as component types, relationship types, systems, modules, |
|
/// resources, and anything else you can make fit. |
|
/// |
|
/// Each `Entity` can have a number of `Id`s added to it, which could be |
|
/// components (such as `Position`), zero-size tags (such as `Disabled`) or |
|
/// relationship `Pair`s (such as `.{ ChildOf, parent }`). |
|
/// |
|
/// Entities can be created using `World.entity()` and deleted with `delete()`. |
|
/// When an entity is deleted it is no longer considered "alive". A world can |
|
/// contain up to 4 billion alive entities. |
|
/// |
|
/// An `Id` can be converted to an `Entity` using `Id.asEntity()` (fails if |
|
/// the id isn't an entity), and back again with `asId()` (always succeeds). |
|
pub fn Entity(comptime ctx: anytype) type { |
|
return struct { |
|
const Self = @This(); |
|
|
|
const Context = @import("./context.zig").Context(ctx); |
|
const World = Context.World; |
|
const Id = Context.Id; |
|
|
|
world: *World, |
|
raw: c.ecs_entity_t, |
|
|
|
/// Returns an `Entity` for the specified world and raw entity id. |
|
/// |
|
/// No safety checks are done, so if you pass an invalid id, an |
|
/// invalid `Entity` might be returned that could cause panics if |
|
/// passed to other API functions without ensuring their validity. |
|
pub fn fromRaw(world: *World, raw: c.ecs_entity_t) Self { |
|
return .{ .world = world, .raw = raw }; |
|
} |
|
|
|
pub const Config = struct { |
|
// Set to modify an existing entity. |
|
id: ?Self = null, |
|
/// Set to specify the entity's name. |
|
name: ?[:0]const u8 = null, |
|
/// Set to specify the entity's symbol. |
|
symbol: ?[:0]const u8 = null, |
|
/// Whether to use a small id when generating one. |
|
/// These are typically reserved for components. |
|
use_low_id: bool = false, |
|
/// Set to specify the parent to create the entity under. |
|
parent: ?Self = null, |
|
/// Set to specify the `Path` under which to create the entity. |
|
path: ?Path = null, |
|
}; |
|
|
|
/// Creates or modifies an `Entity` in the specified `World`. |
|
/// |
|
/// When the settings in `config` refer to an existing entity, it is |
|
/// modified. When any of these settings conflict, such as when an |
|
/// existing entity `.id` is specified but its `.name` doesn't match, |
|
/// an error is returned. |
|
/// |
|
/// `ids` is a tuple that specifies `Id`s added to the entity. |
|
/// For example: `.{ Position, Velocity, .{ ChildOf, parent } }`. |
|
/// |
|
/// When adding components, they are just default-initialized. |
|
/// To set their values you'll need to use `set(...)` on the |
|
/// `Entity` returned from this function. |
|
pub fn init(world: *World, config: Config, ids: anytype) !Self { |
|
const info = @typeInfo(@TypeOf(ids)); |
|
if (info != .Struct or (info.Struct.is_tuple == false and info.Struct.fields.len > 0)) |
|
@compileError("Expected tuple or empty struct, got '" ++ @typeName(@TypeOf(ids)) ++ "'"); |
|
if (info.Struct.fields.len > c.FLECS_ID_DESC_MAX) |
|
@compileError("Adding more than FLECS_ID_DESC_MAX ids"); |
|
|
|
var id = if (config.id) |i| i.raw else null; |
|
var name_ = config.name; |
|
var scope = if (config.parent) |p| p.raw else null; |
|
|
|
if (config.path) |path_| { |
|
if (path_.absolute) { |
|
// Specifying parent with an absolute path is invalid. |
|
if (config.parent != null) return error.ParentMismatch; |
|
scope = 0; |
|
} |
|
|
|
// Ensure that if `id` or `name` is used alongside path, |
|
// their values do actually match. Also sets these so they |
|
// can be passed to the `ecs_entity_desc_t` struct. |
|
switch (path_.parts[path_.parts.len - 1]) { |
|
.id => |path_id| { |
|
if (path_id == 0) return error.InvalidParameter; |
|
if (id) |i| if (path_id != i) return error.IdMismatch; |
|
id = path_id; |
|
}, |
|
.name => |path_name| { |
|
if (name_) |n| if (!std.mem.eql(u8, path_name, n)) return error.NameMismatch; |
|
name_ = path_name; |
|
}, |
|
} |
|
|
|
// Ensure the parent entities specified in `path` exist, or create them. |
|
for (path_.parts[0..(path_.parts.len - 1)]) |part| { |
|
const parent_ = scope orelse c.ecs_get_scope(world.raw); |
|
switch (part) { |
|
.id => |i| { |
|
const found = c.ecs_get_alive(world.raw, i); |
|
if (found == 0) return error.EntityNotFound; |
|
if (parent_ != c.ecs_get_parent(world.raw, found)) return error.ParentMismatch; |
|
scope = found; |
|
}, |
|
.name => |n| { |
|
const found = c.ecs_lookup_child(world.raw, parent_, n.ptr); |
|
if (found == 0) { |
|
var desc = c.ecs_entity_desc_t{ .sep = "".ptr, .name = n.ptr }; |
|
desc.add[0] = c.ecs_pair(c.EcsChildOf, parent_); |
|
scope = c.ecs_entity_init(world.raw, &desc); |
|
if (scope == 0) return errors.getLastErrorOrUnknown(); |
|
} else scope = found; |
|
}, |
|
} |
|
} |
|
} |
|
|
|
// Set the scope to create the entity under (its parent). |
|
// This avoids using up an id for a `ChildOf` relationship. |
|
const previous = if (scope) |s| world.setScope(s) else null; |
|
defer _ = if (scope != null) world.setScope(previous); |
|
|
|
var desc = c.ecs_entity_desc_t{ |
|
.sep = "".ptr, // Disable tokenization. |
|
.id = if (id) |i| i else 0, |
|
.name = if (name_) |n| n.ptr else null, |
|
.symbol = if (config.symbol) |s| s.ptr else null, |
|
.use_low_id = config.use_low_id, |
|
}; |
|
|
|
inline for (ids, 0..) |a, i| |
|
desc.add[i] = Context.anyToId(a); |
|
|
|
for (desc.add[0..ids.len]) |i| |
|
if (c.ecs_id_is_pair(i) and c.ECS_PAIR_FIRST(i) == c.EcsChildOf) |
|
if (config.parent != null or config.path != null) |
|
return error.FoundChildOf; |
|
|
|
const result = c.ecs_entity_init(world.raw, &desc); |
|
if (result == 0) return errors.getLastErrorOrUnknown(); |
|
return Self.fromRaw(world, result); |
|
} |
|
|
|
/// Ensures this `Entity` is valid, returning an error otherwise. |
|
/// Invalid entities may cause panics when used with API functions. |
|
/// |
|
/// An `Entity` is valid if .. |
|
/// - .. it is not `0`. |
|
/// - .. it does not have dead bits set. |
|
/// - .. it does not have any `Id` flags set. |
|
/// - .. no entity with this id is alive, but this entity's generation is `0`. |
|
/// - .. an entity with this exact id and generation is alive in the world. |
|
pub fn ensureValid(self: Self) !void { |
|
// `0` is not a valid entity id. |
|
if (self.raw == 0) return EntityError.IsNone; |
|
// Entity ids should not contain data in dead zone bits. |
|
const VALID_BITS_MASK: c.ecs_entity_t = 0xFF00FFFFFFFFFFFF; |
|
if ((self.raw & ~VALID_BITS_MASK) != 0) return EntityError.IsInvalid; |
|
// Entity ids should not contain `Id` flag bits. |
|
if ((self.raw & c.ECS_ID_FLAGS_MASK) != 0) return EntityError.IsNotEntity; |
|
|
|
// Get the currently alive entity entity with the same id. |
|
// If entity is alive, the generation has to match. |
|
// If entity is not alive, the generation has to be `0`. |
|
const entity = c.ecs_get_alive(self.world.raw, self.raw); |
|
if (c.ECS_GENERATION(entity) != c.ECS_GENERATION(self.raw)) |
|
return EntityError.GenMismatch; |
|
} |
|
|
|
/// Ensures this `Entity` is alive in this world, returning an error if not. |
|
pub fn ensureAlive(self: Self) !void { |
|
if (!c.ecs_is_alive(self.world.raw, self.raw)) return EntityError.IsNotAlive; |
|
} |
|
|
|
pub fn asId(self: Self) Id { |
|
return .{ .world = self.world, .raw = self.raw }; |
|
} |
|
|
|
pub fn entityId(self: Self) u32 { |
|
return @intCast(self.raw & c.ECS_ENTITY_MASK); |
|
} |
|
|
|
pub fn generation(self: Self) u16 { |
|
return @intCast(c.ECS_GENERATION(self.raw)); |
|
} |
|
|
|
/// Deletes this `Entity` and all of its components. |
|
pub fn delete(self: Self) void { |
|
c.ecs_delete(self.world.raw, self.raw); |
|
} |
|
|
|
/// Returns the full, absolute `Path` of this `Entity`. |
|
/// The entity is assumed to be alive. |
|
/// See also: `Path.fromEntity(...)`. |
|
pub fn path(self: Self, alloc: Allocator) !Path { |
|
return Path.fromEntity(ctx, null, self, alloc); |
|
} |
|
|
|
/// Gets the name of this `Entity`, or `null` if none. |
|
/// |
|
/// The returned string is owned by Flecs and may only be valid while |
|
/// the entity is alive and its name doesn't change. |
|
pub fn name(self: Self) ?[:0]const u8 { |
|
const result = c.ecs_get_name(self.world.raw, self.raw); |
|
return if (result != 0) std.mem.sliceTo(result, 0) else null; |
|
} |
|
|
|
/// Sets the name of this `Entity` to the specified value, if any. |
|
/// |
|
/// Flecs does not take ownership of the provided value. |
|
pub fn setName(self: Self, value: ?[*:0]const u8) void { |
|
_ = c.ecs_set_name(self.world.raw, self.raw, value); |
|
} |
|
|
|
/// Gets the symbol of this `Entity`, or `null` if none. |
|
/// |
|
/// The returned string is owned by Flecs and may only be valid while |
|
/// the entity is alive and its symbol doesn't change. |
|
pub fn symbol(self: Self) ?[:0]const u8 { |
|
const result = c.ecs_get_symbol(self.world.raw, self.raw); |
|
return if (result != 0) std.mem.sliceTo(result, 0) else null; |
|
} |
|
|
|
/// Sets the symbol of this `Entity` to the specified value, if any. |
|
/// |
|
/// Flecs does not take ownership of the provided value. |
|
pub fn setSymbol(self: Self, value: ?[*:0]const u8) void { |
|
_ = c.ecs_set_symbol(self.world.raw, self.raw, value); |
|
} |
|
|
|
/// Gets the parent of this `Entity`, or `null` if it has none. |
|
pub fn parent(self: Self) ?Self { |
|
const result = c.ecs_get_parent(self.world.raw, self.raw); |
|
return if (result != 0) fromRaw(self.world, result) else null; |
|
} |
|
|
|
/// Returns an iterator that yields each of this `Entity`s children. |
|
pub fn children(self: Self) World.TermIterator { |
|
return self.world.term(.{ ChildOf, self }); |
|
} |
|
|
|
/// Returns an iterator that yields each of the targets of the |
|
/// specified `relation` that this `Entity` has, if any. |
|
pub fn targets(self: Self, relation: anytype) TargetIterator { |
|
const rel = Context.anyToEntity(relation); |
|
return .{ .world = self.world, .entity = self.raw, .relation = rel.raw }; |
|
} |
|
|
|
pub const TargetIterator = struct { |
|
world: World, |
|
entity: c.ecs_entity_t, |
|
relation: c.ecs_entity_t, |
|
index: c_int = 0, |
|
|
|
pub fn next(self: *TargetIterator) ?Self { |
|
const result = c.ecs_get_target(self.world.raw, self.entity, self.relation, self.index); |
|
self.index += 1; |
|
return if (result != 0) Self.fromRaw(self.world, result) else null; |
|
} |
|
}; |
|
|
|
/// Returns whether this `Entity` has the specified value. |
|
/// |
|
/// `id` must be convertible to an id. See also: `Context.anyToId(...)`. |
|
pub fn has(self: Self, id: anytype) bool { |
|
const ecs_id = Context.anyToId(id); |
|
return c.ecs_has_id(self.world.raw, self.raw, ecs_id); |
|
} |
|
|
|
pub fn add(self: Self, id: anytype) void { |
|
const ecs_id = Context.anyToId(id); |
|
c.ecs_add_id(self.world.raw, self.raw, ecs_id); |
|
} |
|
|
|
pub fn remove(self: Self, id: anytype) void { |
|
const ecs_id = Context.anyToId(id); |
|
c.ecs_remove_id(self.world.raw, self.raw, ecs_id); |
|
} |
|
|
|
/// Gets a value copy of a component. |
|
/// If the component does not exist, returns null. |
|
pub fn get(self: Self, comptime T: anytype) ?meta.AnyToType(T) { |
|
return if (getRef(self, T)) |p| p.* else null; |
|
} |
|
|
|
/// Gets an immutable pointer to a component. |
|
/// If the component does not exist, returns null. |
|
/// |
|
/// The returned pointer may become invalid after calling other Flecs |
|
/// functions, notably when an `Entity` moves to another table caused |
|
/// by adding or removing other components. |
|
pub fn getRef(self: Self, comptime T: anytype) ?*const meta.AnyToType(T) { |
|
const id = Context.anyToId(T); |
|
const ptr = c.ecs_get_id(self.world.raw, self.raw, id); |
|
return @alignCast(@ptrCast(ptr)); |
|
} |
|
|
|
/// Get a mutable pointer to a component. |
|
/// If the component did not yet exist, it will be added. |
|
/// |
|
/// The returned pointer may become invalid after calling other Flecs |
|
/// functions, notably when an `Entity` moves to another table caused |
|
/// by adding or removing other components. |
|
/// |
|
/// If `getMut` is called when the world is in deferred / readonly |
|
/// mode, and the component does not yet exist, it will return a |
|
/// pointer to a temp storage. |
|
pub fn getMut(self: Self, comptime T: anytype) *meta.AnyToType(T) { |
|
const id = Context.anyToId(T); |
|
const ptr = c.ecs_get_mut_id(self.world.raw, self.raw, id); |
|
return @alignCast(@ptrCast(ptr)); |
|
} |
|
|
|
pub fn set(self: Self, comptime T: anytype, value: meta.AnyToType(T)) void { |
|
const id = Context.anyToId(T); |
|
const size = @sizeOf(@TypeOf(value)); |
|
_ = c.ecs_set_id(self.world.raw, self.raw, id, size, @ptrCast(&value)); |
|
} |
|
|
|
/// The `fmt` parameter can be one of: |
|
/// - `d`, `x` or `X`: Output entity ID as decimal or hexadecimal. |
|
/// - `s`: Output entity name, if any, otherwise falls back to `d`. |
|
/// - `any`: Output full entity path with `Path.FormatOptions.default`. |
|
/// - Other `Path.FormatOptions` are also supported (such as `flecs_c`). |
|
pub fn format( |
|
self: Self, |
|
comptime fmt: []const u8, |
|
options: std.fmt.FormatOptions, |
|
writer: anytype, |
|
) !void { |
|
if (fmt.len == 1 and (fmt[0] == 'd' or fmt[0] == 'x' or fmt[0] == 'X')) { |
|
try std.fmt.formatIntValue(self.entityId(), fmt, options, writer); |
|
} else if (fmt.len == 1 and fmt[0] == 's') { |
|
if (self.name()) |n| |
|
try writer.writeAll(n) |
|
else |
|
try self.format("d", options, writer); |
|
} else { |
|
const opt = Path.fmtToFormatOptions(fmt); |
|
if (self.parent()) |p| { |
|
try p.format(fmt, .{}, writer); |
|
try writer.writeAll(opt.sep); |
|
} else if (opt.root_sep) |sep| |
|
try writer.writeAll(sep); |
|
try self.format("s", .{}, writer); |
|
} |
|
} |
|
}; |
|
} |
|
|
|
const expect = @import("./test/expect.zig"); |
|
const flecszigble = @import("./main.zig"); |
|
|
|
test "Entity with name and symbol" { |
|
flecszigble.init(std.testing.allocator); |
|
var world = try flecszigble.World(void).initMinimal(); |
|
defer world.deinit(); |
|
|
|
const entity = try world.entity(.{ .name = "name", .symbol = "symbol" }, .{}); |
|
|
|
try expect.equalStrings("name", entity.name()); |
|
try expect.equalStrings("symbol", entity.symbol()); |
|
|
|
try expect.equal(entity, world.lookupChild(null, "name")); |
|
try expect.equal(entity, world.lookupSymbol("symbol")); |
|
} |
|
|
|
test "Entity get and set name" { |
|
flecszigble.init(std.testing.allocator); |
|
var world = try flecszigble.World(void).initMinimal(); |
|
defer world.deinit(); |
|
|
|
const entity = try world.entity(.{}, .{}); |
|
try expect.equal(null, entity.name()); |
|
|
|
entity.setName("hello"); |
|
try expect.equalStrings("hello", entity.name()); |
|
|
|
entity.setName(null); |
|
try expect.equal(null, entity.name()); |
|
} |
|
|
|
test "Entity get and set" { |
|
flecszigble.init(std.testing.allocator); |
|
var world = try flecszigble.World(void).initMinimal(); |
|
defer world.deinit(); |
|
|
|
const Position = struct { x: f32, y: f32 }; |
|
const Velocity = struct { x: f32, y: f32 }; |
|
_ = try world.component(Position); |
|
_ = try world.component(Velocity); |
|
|
|
const entity = try world.entity(.{}, .{}); |
|
|
|
entity.set(Position, .{ .x = 10, .y = 20 }); |
|
entity.set(Velocity, .{ .x = 1, .y = 2 }); |
|
|
|
const pos = entity.get(Position).?; |
|
const vel = entity.get(Velocity).?; |
|
|
|
entity.set(Position, .{ .x = pos.x + vel.x, .y = pos.y + vel.y }); |
|
|
|
try expect.equal(.{ .x = 11, .y = 22 }, entity.get(Position).?); |
|
} |
|
|
|
test "Entity getMut" { |
|
flecszigble.init(std.testing.allocator); |
|
var world = try flecszigble.World(void).initMinimal(); |
|
defer world.deinit(); |
|
|
|
const Position = struct { x: f32, y: f32 }; |
|
const Velocity = struct { x: f32, y: f32 }; |
|
_ = try world.component(Position); |
|
_ = try world.component(Velocity); |
|
|
|
const entity = try world.entity(.{}, .{ Position, Velocity }); |
|
|
|
// Position and Velocity need to be present on the entity for component |
|
// pointers to be stable. Otherwise, when `Velocity` is added, the entity |
|
// would move tables and invalidate the `Position` pointer. |
|
|
|
const pos = entity.getMut(Position); |
|
const vel = entity.getMut(Velocity); |
|
|
|
pos.* = .{ .x = 10, .y = 20 }; |
|
vel.* = .{ .x = 1, .y = 2 }; |
|
|
|
pos.* = .{ .x = pos.x + vel.x, .y = pos.y + vel.y }; |
|
|
|
try expect.equal(.{ .x = 11, .y = 22 }, pos.*); |
|
} |
|
|
|
test "Entity set and get with pair type" { |
|
flecszigble.init(std.testing.allocator); |
|
var world = try flecszigble.World(void).initMinimal(); |
|
defer world.deinit(); |
|
|
|
const Position = struct { x: f32, y: f32 }; |
|
const Rank = struct { value: i32 }; // Gives a component a certain "rank". |
|
const Copy = struct {}; // Stores a "copy" of another component. |
|
_ = try world.component(Position); |
|
_ = try world.component(Rank); |
|
_ = try world.tag(Copy); |
|
|
|
const entity = try world.entity(.{}, .{}); |
|
|
|
entity.set(Position, .{ .x = 10, .y = 20 }); |
|
entity.set(.{ Rank, Position }, .{ .value = 9001 }); |
|
entity.set(.{ Copy, Position }, .{ .x = 60, .y = 80 }); |
|
|
|
try expect.equal(.{ .x = 10, .y = 20 }, entity.get(Position)); |
|
try expect.equal(.{ .value = 9001 }, entity.get(.{ Rank, Position })); |
|
try expect.equal(.{ .x = 60, .y = 80 }, entity.get(.{ Copy, Position })); |
|
} |
|
|
|
test "Entity format" { |
|
flecszigble.init(std.testing.allocator); |
|
var world = try flecszigble.World(void).initMinimal(); |
|
defer world.deinit(); |
|
|
|
const grandparent = try world.entity(.{ .name = "grandparent" }, .{}); |
|
const parent = try world.entity(.{ .name = "parent", .parent = grandparent }, .{}); |
|
const child = try world.entity(.{ .name = "child", .parent = parent }, .{}); |
|
|
|
try expect.fmt("410", "{d}", .{grandparent}); |
|
try expect.fmt("411", "{d}", .{parent}); |
|
try expect.fmt("412", "{d}", .{child}); |
|
|
|
try expect.fmt("grandparent", "{s}", .{grandparent}); |
|
try expect.fmt("parent", "{s}", .{parent}); |
|
try expect.fmt("child", "{s}", .{child}); |
|
|
|
try expect.fmt("grandparent", "{}", .{grandparent}); |
|
try expect.fmt("grandparent.parent", "{}", .{parent}); |
|
try expect.fmt("grandparent.parent.child", "{}", .{child}); |
|
|
|
try expect.fmt("/grandparent", "{unix}", .{grandparent}); |
|
try expect.fmt("/grandparent/parent", "{unix}", .{parent}); |
|
try expect.fmt("/grandparent/parent/child", "{unix}", .{child}); |
|
|
|
parent.setName(null); |
|
try expect.fmt("411", "{s}", .{parent}); |
|
try expect.fmt("/grandparent/411", "{unix}", .{parent}); |
|
try expect.fmt("/grandparent/411/child", "{unix}", .{child}); |
|
}
|
|
|