object: add ability to tag arbitrary objects with arbitrary tags and values
Modules define lists of objects, e.g. a `SpriteRenderer` module may define
```zig
sprites: mach.Objects(struct {
// ...
}),
```
Previously, the only way for another Mach module to 'attach data to a sprite object' or 'tag a sprite' would be to (ab)use the graph relations system, creating their own object and using parent/child relations to express that the sprite has some tag/data associated with it. For example:
```zig
// Game.zig
is_monster: mach.Object(struct{}), // empty object just to indicate 'some other object is a monster'
// ...
// Create a 'tag object'
const is_monster_tag_obj_id = game.is_monster.new(.{});
// Add the 'tag object' as a child of our sprite
sprite_renderer.sprites.addChild(my_sprite_id, is_monster_tag_obj_id);
// ...
```
This usage of the API was quite ugly/usage, and importantly eliminated your ability to use the parent/child relations for _other_ things where they are more appropriate. However, it did mean that you didn't have to go and fork+modify the `SpriteRenderer` module that you e.g. imported as a reusable package.
With this change, we add object _tags_ and _tags with values_. Any module can add their own tags or tags with values to any object, whether it is from their module or not. For example, the `is_monster` example above could now be written as:
```zig
// Game.zig
pub const mach_tags = .{ .is_monster };
// ...
try sprite_renderer.sprites.setTag(sprite_id, Game, .is_monster, null);
const is_monster: bool = sprite_renderer.sprites.hasTag(sprite_id, Game, .is_monster);
// is_monster == true!
// No longer a monster
try sprite_renderer.sprites.removeTag(sprite_id, Game, .is_monster);
```
This allows for effectively tagging objects as distinct kinds, states, etc. even though they aren't our object and we can't modify their `struct {}` type to include an `is_monster: bool` field of our own.
Internally, the implementation stores tags using a hashmap under the assumption that not all objects in a list will have a tag.
Tags with values work almost identically, the only difference is that the last parameter to `setTag` is set to another `mach.ObjectID` which points to whatever arbitrary data you'd like to attach to the object, and `getTag` returns it. For example:
```zig
// Game.zig
pub const mach_tags = .{
/// Whether a sprite is a monster
.is_monster,
/// Whether a sprite has a friendly sprite attached to it
.{ .friend, Sprite, .sprites },
};
// ...
try sprite_renderer.sprites.setTag(sprite_id, Game, .friend, friendly_sprite_id);
const has_friend: bool = sprite_renderer.sprites.hasTag(sprite_id, Game, .friend);
// has_friend == true!
// Get our friend
const friend_id: mach.ObjectID = sprite_renderer.sprites.getTag(sprite_id, Game, .friend);
// friend_id == friendly_sprite_id
// Delete our friend
try sprite_renderer.sprites.removeTag(sprite_id, Game, .friend);
```
Signed-off-by: Emi Gutekanst <emi@hexops.com>
This commit is contained in:
parent
9749cd9a65
commit
17a830f857
1 changed files with 94 additions and 0 deletions
|
|
@ -70,6 +70,9 @@ pub fn Objects(options: ObjectsOptions, comptime T: type) type {
|
|||
|
||||
/// A bitset used to track per-field changes. Only used if options.track_fields == true.
|
||||
updated: ?std.bit_set.DynamicBitSetUnmanaged = if (options.track_fields) .{} else null,
|
||||
|
||||
/// Tags storage
|
||||
tags: std.AutoHashMapUnmanaged(TaggedObject, ?ObjectID) = .{},
|
||||
},
|
||||
|
||||
pub const IsMachObjects = void;
|
||||
|
|
@ -77,6 +80,11 @@ pub fn Objects(options: ObjectsOptions, comptime T: type) type {
|
|||
const Generation = u16;
|
||||
const Index = u32;
|
||||
|
||||
const TaggedObject = struct {
|
||||
object_id: ObjectID,
|
||||
tag_hash: u64,
|
||||
};
|
||||
|
||||
const PackedID = packed struct(u64) {
|
||||
type_id: ObjectTypeID,
|
||||
generation: Generation,
|
||||
|
|
@ -275,6 +283,52 @@ pub fn Objects(options: ObjectsOptions, comptime T: type) type {
|
|||
if (mach.is_debug) data.set(unpacked.index, undefined);
|
||||
}
|
||||
|
||||
// TODO(objects): evaluate whether tag operations should ever return an error
|
||||
|
||||
/// Sets a tag on an object
|
||||
pub fn setTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M), value_id: ?ObjectID) !void {
|
||||
_ = objs.validateAndUnpack(id, "setTag");
|
||||
|
||||
// TODO: validate that value_id is an object coming from the mach.Objects(T) list indicated by the tag value in M.mach_tags.
|
||||
//const value_mach_objects = moduleTagValueObjects(M, tag);
|
||||
|
||||
const tagged = TaggedObject{
|
||||
.object_id = id,
|
||||
.tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)),
|
||||
};
|
||||
try objs.internal.tags.put(objs.internal.allocator, tagged, value_id);
|
||||
}
|
||||
|
||||
/// Removes a tag on an object
|
||||
pub fn removeTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M)) void {
|
||||
_ = objs.validateAndUnpack(id, "setTag");
|
||||
const tagged = TaggedObject{
|
||||
.object_id = id,
|
||||
.tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)),
|
||||
};
|
||||
_ = objs.internal.tags.remove(tagged);
|
||||
}
|
||||
|
||||
/// Whether an object has a tag
|
||||
pub fn hasTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M)) bool {
|
||||
_ = objs.validateAndUnpack(id, "hasTag");
|
||||
const tagged = TaggedObject{
|
||||
.object_id = id,
|
||||
.tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)),
|
||||
};
|
||||
return objs.internal.tags.contains(tagged);
|
||||
}
|
||||
|
||||
/// Get an object's tag value, or null.
|
||||
pub fn getTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M)) ?mach.ObjectID {
|
||||
_ = objs.validateAndUnpack(id, "hasTag");
|
||||
const tagged = TaggedObject{
|
||||
.object_id = id,
|
||||
.tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)),
|
||||
};
|
||||
return objs.internal.tags.get(tagged) orelse null;
|
||||
}
|
||||
|
||||
pub fn slice(objs: *@This()) Slice {
|
||||
return Slice{
|
||||
.index = 0,
|
||||
|
|
@ -497,6 +551,46 @@ fn ModuleFunctionName2(comptime M: type) type {
|
|||
});
|
||||
}
|
||||
|
||||
/// Enum describing all mach_tags for a given comptime-known module.
|
||||
fn ModuleTagEnum(comptime M: type) type {
|
||||
// TODO(object): handle duplicate enum field case in mach_tags with a more clear error?
|
||||
// TODO(object): improve validation error messages here
|
||||
validate(M);
|
||||
if (@typeInfo(@TypeOf(M.mach_tags)) != .@"struct") {
|
||||
@compileError("mach: invalid module, `pub const mach_tags must be `.{ .is_monster, .{ .renderer, mach.Renderer.objects } }`, found: " ++ @typeName(@TypeOf(M.mach_tags)));
|
||||
}
|
||||
var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{};
|
||||
var i: u32 = 0;
|
||||
inline for (@typeInfo(@TypeOf(M.mach_tags)).@"struct".fields, 0..) |field, field_index| {
|
||||
const f = M.mach_tags[field_index];
|
||||
if (@typeInfo(field.type) == .enum_literal) {
|
||||
enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(f), .value = i }};
|
||||
i += 1;
|
||||
} else {
|
||||
if (@typeInfo(field.type) != .@"struct") {
|
||||
@compileError("mach: invalid module, mach_tags entry is not an enum literal or struct, found: " ++ @typeName(field.type));
|
||||
}
|
||||
// TODO(objects): validate length of struct
|
||||
const tag = f.@"0";
|
||||
const M2 = f.@"1";
|
||||
const object_list_tag = f.@"2";
|
||||
_ = object_list_tag; // autofix
|
||||
validate(M2);
|
||||
// TODO: validate that M2.object_list_tag is a mach objects list
|
||||
enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(tag), .value = i }};
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return @Type(.{
|
||||
.@"enum" = .{
|
||||
.tag_type = if (enum_fields.len > 0) std.math.IntFittingRange(0, enum_fields.len - 1) else u0,
|
||||
.fields = enum_fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
.is_exhaustive = true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn Modules(module_lists: anytype) type {
|
||||
inline for (moduleTuple(module_lists)) |module| {
|
||||
validate(module);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue