Compare commits

..

14 Commits

  1. 57
      README.md
  2. 2
      src/flecs-cs
  3. 7
      src/gaemstone.ECS/Entity.cs
  4. 8
      src/gaemstone.ECS/EntityBuilder.cs
  5. 2
      src/gaemstone.ECS/EntityPath.cs
  6. 2
      src/gaemstone.ECS/Filter.cs
  7. 3
      src/gaemstone.ECS/Internal/Iterator.cs
  8. 2
      src/gaemstone.ECS/Iterator.cs
  9. 4
      src/gaemstone.ECS/Rule.cs
  10. 4
      src/gaemstone.ECS/Term.cs
  11. 1
      src/gaemstone.ECS/Utility/CStringExtensions.cs
  12. 6
      src/gaemstone.ECS/World+Bare.cs
  13. 13
      src/gaemstone.ECS/World.cs

@ -1,10 +1,8 @@
# gaemstone.ECS
.. is a medium-level managed wrapper library around the [flecs-cs] bindings
for the amazing [Entity Component System (ECS)][ECS] framework [Flecs]. It is
used as part of the [gæmstone] game engine, but may be used in other projects
as well. To efficiently use this library, a thorough understanding of [Flecs]
is required.
.. is a medium-level managed wrapper library around the [flecs-cs] bindings for the amazing [Entity Component System (ECS)][ECS] framework [Flecs]. It is used as part of the [gæmstone] game engine, but may be used in other projects as well. To efficiently use this library, a thorough understanding of [Flecs] is required.
These classes have been split from the main [gæmstone] project. It is still a little unclear what functionality belongs where, and structural changes may occur. In its current state, I recommend to use this repository simply as a reference for building similar projects.
[ECS]: https://en.wikipedia.org/wiki/Entity_component_system
[Flecs]: https://github.com/SanderMertens/flecs
@ -13,20 +11,18 @@ is required.
## Features
These classes have only recently been split from the main **gæmstone** project. It is currently unclear what functionality belongs where, and structural changes are still very likely. In its current state, feel free to use **gæmstone.ECS** as a reference for building similar projects, or be aware that things are in flux, and in general nowhere near stable.
- Simple wrapper structs such as [Entity] and [Identifier].
- Classes with convenience functions like [EntityRef] and [EntityType].
- Convenient wrapper types such as [Id], [Entity] and [EntityType].
- [EntityPath] uses a unix-like path, for example `/Game/Players/copygirl`.
- Fast type-to-entity lookup using generic context on [World]. (See below.)
- Define your own [Components] as both value or reference types.
- Query the ECS with [Iterators], [Filters], [Queries] and [Rules].
- Create [Systems] for game logic and [Observers] to act on changes.
[Id]: ./src/gaemstone.ECS/Id.cs
[Entity]: ./src/gaemstone.ECS/Entity.cs
[Identifier]: ./src/gaemstone.ECS/Identifier.cs
[EntityRef]: ./src/gaemstone.ECS/EntityRef.cs
[EntityType]: ./src/gaemstone.ECS/EntityType.cs
[EntityPath]: ./src/gaemstone.ECS/EntityPath.cs
[World]: ./src/gaemstone.ECS/World.cs
[Components]: ./src/gaemstone.ECS/Component.cs
[Iterators]: ./src/gaemstone.ECS/Iterator.cs
[Filters]: ./src/gaemstone.ECS/Filter.cs
@ -38,7 +34,7 @@ These classes have only recently been split from the main **gæmstone** project.
## Example
```cs
var world = new World();
var world = new World<Program>();
var position = world
.New("Position") // Create a new EntityBuilder, and set its name.
@ -55,26 +51,31 @@ entities.NewChild("Two").Set(new Position(10, 20)).Build();
// Changed my mind: Let's multiply each entity's position by 10.
foreach (var child in entities.GetChildren()) {
ref var pos = ref child.GetRefOrThrow<Position>();
pos = new(pos.X * 10, pos.Y * 10);
ref var pos = ref child.GetRefOrThrow<Position>();
pos = new(pos.X * 10, pos.Y * 10);
}
var onUpdate = world.LookupByPathOrThrow("/flecs/pipeline/OnUpdate");
// The following systems run in the "OnUpdate"
// phase of the default pipeline provided by Flecs.
var dependsOn = world.LookupPathOrThrow("/flecs/core/DependsOn");
var onUpdate = world.LookupPathOrThrow("/flecs/pipeline/OnUpdate");
// Create a system that will move all entities with
// the "Position" component downwards by 2 every frame.
world.New("FallSystem").Build()
.InitSystem(onUpdate, new("Position"), iter => {
world.New("FallSystem")
.Add(dependsOn, onUpdate)
.Build().InitSystem(new("Position"), iter => {
var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) {
ref var pos = ref posColumn[i];
pos = new(pos.X, pos.Y + 2);
pos = new(pos.X, pos.Y - 2);
}
});
// Create a system that will print out entities' positions.
world.New("PrintPositionSystem").Build()
.InitSystem(onUpdate, new("[in] Position"), iter => {
.Add(dependsOn, onUpdate)
.InitSystem(new("[in] Position"), iter => {
var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) {
var entity = iter.Entity(i);
@ -109,3 +110,21 @@ dotnet add reference gaemstone.ECS/gaemstone.ECS.csproj
# To generate flecs-cs' bindings:
./gaemstone.ECS/src/flecs-cs/library.sh
```
## On the `TContext` type parameter
Entities may be looked up simply by their type, once they're registered with `CreateLookup` or `InitComponent`. Under the hood, this is made possible using a nested static generic class that hold onto an `Entity` field. Theoretically this could be compiled into a simple field lookup and therefore be faster than dictionary lookups.
To support scenarios where multiple worlds may be used simultaneously, each with their own unique type lookups, we specify a generic type as that context.
In cases where only a single world is used, the amount of typing can be reduced by including a file similar to the following, defining global aliases:
```cs
global using Entity = gaemstone.ECS.Entity<Context>;
global using Id = gaemstone.ECS.Id<Context>;
global using Iterator = gaemstone.ECS.Iterator<Context>;
global using World = gaemstone.ECS.World<Context>;
// Add more aliases as you feel they are needed.
public struct Context { }
```

@ -1 +1 @@
Subproject commit fcce5620d21fcd14c79d73a4406db8ce85f53dad
Subproject commit 1ae8ffade56a279d450dbf42545afbf873bdbafe

@ -21,7 +21,7 @@ public unsafe readonly partial struct Entity<TContext>
public bool IsValid => EntityAccess.IsValid(World, this);
public bool IsAlive => IsSome && EntityAccess.IsAlive(World, this);
public string? Name { get => EntityAccess.GetName(World, this); set => EntityAccess.SetName(World, this, value); }
public string? Name { get => EntityAccess.GetName(World, this); set => EntityAccess.SetName(World, this, value); }
public string? Symbol { get => EntityAccess.GetSymbol(World, this); set => EntityAccess.SetSymbol(World, this, value); }
public EntityPath Path => EntityPath.From(World, this);
@ -30,7 +30,7 @@ public unsafe readonly partial struct Entity<TContext>
public Entity<TContext>? Parent
=> GetOrNull(World, GetTargets(FlecsBuiltIn.ChildOf).FirstOrDefault());
public IEnumerable<Entity<TContext>> Children
=> Iterator<TContext>.FromTerm(World, new(FlecsBuiltIn.ChildOf, this)).GetAllEntities();
=> World.Term(new(FlecsBuiltIn.ChildOf, this)).GetAllEntities();
private Entity(World<TContext> world, Entity value)
@ -76,7 +76,8 @@ public unsafe readonly partial struct Entity<TContext>
public Entity<TContext> ChildOf(Entity parent)
=> Add(FlecsBuiltIn.ChildOf, parent);
public bool IsDisabled => Has(FlecsBuiltIn.Disabled);
public bool IsDisabled => Has(FlecsBuiltIn.Disabled);
public bool IsEnabled => !Has(FlecsBuiltIn.Disabled);
public Entity<TContext> Disable() => Add(FlecsBuiltIn.Disabled);
public Entity<TContext> Enable() => Remove(FlecsBuiltIn.Disabled);

@ -109,11 +109,11 @@ public class EntityBuilder<TContext>
using var alloc = TempAllocator.Use();
var desc = new ecs_entity_desc_t {
id = Id,
name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default,
symbol = alloc.AllocateCString(_symbol),
add_expr = alloc.AllocateCString(Expression),
_name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default,
_symbol = alloc.AllocateCString(_symbol),
_add_expr = alloc.AllocateCString(Expression),
use_low_id = UseLowId,
sep = CStringExtensions.ETX,
_sep = CStringExtensions.Empty,
};
var add = desc.add; var index = 0;

@ -197,7 +197,7 @@ public class EntityPath
foreach (var part in path)
fixed (byte* ptr = part.AsSpan())
if (skipLookup || (parent = new(ecs_lookup_child(world, parent, ptr))).IsNone) {
var desc = new ecs_entity_desc_t { name = ptr, sep = CStringExtensions.ETX };
var desc = new ecs_entity_desc_t { _name = ptr, _sep = CStringExtensions.Empty };
if (parent.IsSome) desc.add[0] = Id.Pair(FlecsBuiltIn.ChildOf, parent);
parent = new(ecs_entity_init(world, &desc));
skipLookup = true;

@ -77,7 +77,7 @@ public class FilterDesc
public unsafe ecs_filter_desc_t ToFlecs(IAllocator allocator)
{
var desc = new ecs_filter_desc_t {
expr = allocator.AllocateCString(Expression),
_expr = allocator.AllocateCString(Expression),
instanced = Instanced,
entity = Entity,
};

@ -58,6 +58,9 @@ public unsafe class Iterator
public Entity First()
=> new(ecs_iter_first(Handle));
public bool Any()
=> ecs_iter_is_true(Handle);
public IEnumerable<Entity> GetAllEntities()
{
while (Next())

@ -29,6 +29,8 @@ public unsafe class Iterator<TContext> : Iterator
public new Iterator<TContext> SetVar(Variable var, Entity entity)
=> (Iterator<TContext>)base.SetVar(var, entity);
public new Entity<TContext>? First()
=> Entity<TContext>.GetOrNull(World, base.First());
public new IEnumerable<Entity<TContext>> GetAllEntities()
=> base.GetAllEntities().Select(e => Entity<TContext>.GetOrInvalid(World, e));

@ -52,9 +52,9 @@ public unsafe class Rule<TContext>
internal VariableCollection(ecs_rule_t* handle)
{
// Find the $This variable, if the rule has one.
// Find the $this variable, if the rule has one.
var thisIndex = ecs_filter_find_this_var(ecs_rule_get_filter(handle));
if (thisIndex >= 0) _variables.Add(new(thisIndex, "This"));
if (thisIndex >= 0) _variables.Add(new(thisIndex, "this"));
// Find all the other "accessible" variables.
var count = ecs_rule_var_count(handle);

@ -61,7 +61,7 @@ public enum TermOperKind
public class TermId
{
public static TermId This { get; } = new("$This");
public static TermId This { get; } = new("$this");
public Entity Id { get; }
public string? Name { get; }
@ -80,7 +80,7 @@ public class TermId
public ecs_term_id_t ToFlecs(IAllocator allocator) => new() {
id = Id,
name = allocator.AllocateCString(Name),
_name = allocator.AllocateCString(Name),
trav = Traverse,
flags = (ecs_flags32_t)(uint)Flags
};

@ -8,7 +8,6 @@ namespace gaemstone.ECS.Utility;
public unsafe static class CStringExtensions
{
public static CString Empty { get; } = (CString)"";
public static CString ETX { get; } = (CString)"\x3"; // TODO: Temporary, until flecs supports Empty.
public static unsafe byte[]? FlecsToBytes(this CString str)
{

@ -17,7 +17,9 @@ public unsafe struct World
public World(ecs_world_t* handle)
=> Handle = handle;
public World(params string[] args)
/// <summary> Initializes a new Flecs world. </summary>
/// <param name="minimal"> If true, doesn't automatically import built-in modules. </param>
public World(bool minimal = false)
{
[UnmanagedCallersOnly]
static void Abort() => throw new FlecsAbortException();
@ -27,7 +29,7 @@ public unsafe struct World
api.abort_ = new FnPtr_Void { Pointer = &Abort };
ecs_os_set_api(&api);
Handle = ecs_init_w_args(args.Length, null);
Handle = minimal ? ecs_mini() : ecs_init();
}
public bool Progress(TimeSpan delta)

@ -15,8 +15,11 @@ public unsafe struct World<TContext>
public World(World value)
=> Value = value;
public World(params string[] args)
: this(new World(args)) { }
/// <summary> Initializes a new Flecs world. </summary>
/// <param name="minimal"> If true, doesn't automatically import built-in modules. </param>
public World(bool minimal = false)
: this(new World(minimal)) { }
public bool Progress(TimeSpan delta) => Value.Progress(delta);
public void Quit() => Value.Quit();
@ -57,6 +60,12 @@ public unsafe struct World<TContext>
=> Pair(Entity<TRelation>(), Entity<TTarget>());
public Iterator<TContext> Term(Term term) => Iterator<TContext>.FromTerm(this, term);
public Filter<TContext> Filter(FilterDesc desc) => new(this, desc);
public Query<TContext> Query(QueryDesc desc) => new(this, desc);
public Rule<TContext> Rule(FilterDesc desc) => new(this, desc);
public bool Equals(World<TContext> other) => Value == other.Value;
public override bool Equals(object? obj) => (obj is World<TContext> other) && Equals(other);
public override int GetHashCode() => Value.GetHashCode();

Loading…
Cancel
Save