Giter Site home page Giter Site logo

genaray / arch Goto Github PK

View Code? Open in Web Editor NEW
769.0 18.0 65.0 952 KB

A high-performance C# based Archetype & Chunks Entity Component System (ECS) with optional multithreading.

License: Apache License 2.0

C# 98.93% Shell 1.07%
csharp dotnet ecs entity-component-system game game-development dotnet-core net6 net7 netstandard21

arch's Introduction

Hi, I'm genaray 💻

I am a 23-year-old computer science student and technology enthusiast living in NRW, Germany.

I recently finished my bachelor thesis at a small local IT company, and I am looking forward to doing my master as well.

I mostly use C# and Java on a daily basis. However, I love learning new languages and technologies. In my spare time, I mostly play with Unity and develop small games.

Hobbies

  • Videogames 🎮
  • Gym 🏋️
  • Photography 📷
  • Learning new languages 🇹🇷
  • Game-Development 🤖

arch's People

Contributors

andreakarasho avatar arhinzi avatar beancheeseburrito avatar clibequilibrium avatar donaut avatar drsmugleaf avatar emelrad12 avatar epicguru avatar genaray avatar hertzole avatar lilithsilver avatar martindevans avatar metalgearsloth avatar mikhail-dcl avatar nathanrw avatar pheubel avatar reeseschultz avatar richdog avatar royconskylands avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

arch's Issues

Null error in QueryArchetypeEnumerator

Hope you're doing well!

I'm not sure how it gets into this state, but occasionally, all of the entries in _archetypes in QueryArchetypeEnumerator become null. The enumerator Current returns null and it throws on archetype.Size. I think it might be related to adding a component to my player entity (which maybe is special in the number of components it has), only seems to happen to that entity.

In my testing, it seems like either all the entries are populated or all null when it encounters the error, so maybe it can break after encountering a null archetype?

Enumerators.cs, QueryArchetypeEnumerator struct:

[SkipLocalsInit]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
    while (_archetypes.MoveNext())
    {
        ref var archetype = ref _archetypes.Current;
        if (archetype.Size > 0 && _query.Valid(archetype.BitSet))
        {
            return true;
        }
    }

    return false;
}

Exception when adding component to entity in query

For reasons I'm not sure of, when adding a component to an entity within a query. It seems to run into this problem (when processing a lot of entities, it doesn't seem to happen when there's not a lot going on):

in Archetype.cs:

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    [Pure]
    internal int Transfer(int index, ref Chunk chunk)
    {
        // Get last entity
        var lastIndex = chunk.Size - 1;
        var lastEntity = chunk.Entities[lastIndex];

chunk.Size is 0, so it tries to access lastIndex = -1.

So I'm not sure if it is because the chunk being transferred into might be empty once in a while? Not sure if it makes sense, but thought I'd post it in case you knew the cause, I can not find a clear way to repro it unfortunately without just throwing a ton of entities and adding components within the query (I use mostly inline non-parallel, but in this case, it happened with both parallel or non-parallel).

IndexOutOfRangeException if LastChunk.Size on Archetype is 0

When removing Entities from an Archetype either directly or via the World it sometimes throws an IndexOutOfRangeException, this is because for some reason the Archetype's LastChunk.Size is 0. An easy reproduction is the following:

Inside EnumeratorTest.cs add a new Test:

[Test]
public void QueryRemove()
{
    var entities = new List<Entity>();
    world.GetEntities(description, entities);

    for (var i = 0; i < entities.Count; i++)
    {
        if (i == 5500)
            ;

        var entity = entities[i];
        world.Destroy(in entity);
    }
}

It will throw the exception when trying to remove the 5500th Entity, hence the if statement to set a breakpoint on the empty statement inside it

CommandBuffer suggestions

I found the commandbuffers implementation not very user-friendly and about this I suggest to merge all the cmdbuffer types into an unique class (not a struct) to emulate the World class.
I see multiple advatanges:

  • a better and tiny API
  • users will work with Entity struct only [all the internal changes should be tracked into Arch]

Ugly pseudo code:

struct Transform { public Vector3 Position; }

var world = World.Create();
var cmd = new CommandBuffer(world);

var entity = cmd.Create<Transform>();
ref var transform = ref cmd.Get<Transform>(in entity);
transform.X = 20;

world.Query(in query, (in Entity e, ref Transform t) => {
   if (t.Position == Vector3.Zero)
      cmd.Destroy(in e); // will mark the entity as destroyed and next queries will not grab this entity until the real destruction
  else
    --t.X;
   
});

Add `World.Create(object[])`

Discussed in #78

Originally posted by Mathetis April 11, 2023
Hi, I'm curious if the following behavior is by design, if I am misusing it, or if this is a bug.

I have a use case where the array of components arrived as an object[], which contains references to struct components (boxing isn't problematic for my case here). I want to create an entity from those components. Here's a simple example:

struct SomeComponent { ... }

object[] components = new object[] { new SomeComponent() { ... } };

Entity entity = world.Create(components);

bool hasComponent = entity.Has<SomeComponent>();  // <-- this will be false.

We should probably add another World.Create overload to create entities by a object[] array :)
This should be done quickly though.

Chunk Lookup-Table instead of Dictionary

After some profiling i noticed that the ComponentIdToArrayIndex Dictionary inside the Chunk is slow... which makes sense since dictionarys are only effective for high amount of items.

Therefore it would make sense to replace the dictionary by a efficient lookup table, that one could be stored in the archetype to minimize memory overhead. Using this lookup table to map the component id to the array inside the chunk would be more efficient.

Alternative to Dictionary<T, int>

In dotnext they created a generic class with a static read-only int field each time somebody accesses the generic class with a new type the static field is incremented. And thus creating a compile-time dictionary. TypeMap There was a discussion about it too somewhere in the c# runtime repository but sadly I lost that.

System API

There should be an additional System API for the world where we can register and process systems. Those systems act as a group of queries and entity/world related operations known from other ECS frameworks.

It probably even could feature code generation...

world.AddSystem<MovementSystem, HealthSystem>();
world.AddSystem(new RangeSystem(10.0f));
world.Update();

public class MovementSystem{

   [Update] 
   [Any<Character, Mob, Entity>]
   [None<Dead, AI>]
   public void Move(ref Transform transform, ref Velocity velocity){
      ....
   }

   [Update] 
   public void CheckCollision(ref Transform transform){
     ...
   }
}

`in Entity` vs `ref Entity` in `Query()` overloads

Hi, I managed to shoot myself in the foot earlier and it took me a little while to work out what I had done wrong.

Bad

            var qd = QueryDescription().WithAll<MyComponent>();
            World.Query(qd, (ref Entity e, ref MyComponent c) =>
            {
                // Do something
            });

This code crashes inside Arch with an access violation.

Good

            var qd = QueryDescription().WithAll<MyComponent>();
            World.Query(qd, (in Entity e, ref MyComponent c) =>
            {
                // Do something
            });

This code works.

This issue is that in the 'bad' example, the lambda is a ForEach and so Entity is actually treated as a component type, and naturally there are no such components.

In the 'good' example, the lambda is a ForEachWithEntity so it works as expected. But the only difference is in Entity vs ref Entity. I wonder if something could be done to catch the problem case?

Entity Templates

Background

Would be pretty neat if we could clone entities and its components based on a template which defines its values.
Basically a way to create entities with set values. Has many advantages, can be serialized easily and is more effective in many ways.

Idea

Either:

var template = new Template(
   new Position(10,10),
   new Mesh(5, Mode.Batched),
   ...
);

var clone = world.Create(template);

Or using source generation:

[Template]
public struct PlayerTemplate{

   public Transform Transform;
   public Velocity Velocity;
   public Model Modell;
   public ...
   ....
}

var template = new PlayerTemplate(whateverData);
var clone = world.Create(template);

When we choose source generators we could even build more cool constructs around this. Like an extremely efficient identifier mechanic to clone certain templates:

[TemplateStorage]
public struct Templates{

   [Identifier("1:1")]  // Or raw ints e.g. for networking? 
   public PlayerTemplate playerTemplate;

   [Identifier("1:2")]
   public MobTemplate mobTemplate;

   [Identifier("2:1")]
   public FireGolemTemplate fireGolemTemplate;
}

var cloneByIdentifier = world.Create("1:1");
var fireGolem = world.Create("2:1");

(new Entity()).IsAlive() is maybe true

This a surprising behavior.

{
   World w = World.Create();
   bool e_alive_1 = (new Entity()).IsAlive(); //Returns false
   w.Create();
   bool e_alive_2 = (new Entity()).IsAlive(); //Returns true
}

this should probably return false in both situations. This leads to surprising results if you are trying to store an entity in a component and want to check if the parameter has been initialized or not.

Faster Lookups

Entity lookups are still pretty "slow" compared to regular iterations... this is because the archetype architecture favors iterations above lookups. However there still some tricks to make lookups optimal.

One idea is a simple lookup array inside the world itself, that one could map a entity index directly to its archetype, version and chunk. This would reduce dictionary lookups and would be optimal.

TryGetArchetype uses reference equality instead of SequenceEquals

In the World method TryGetArchetype(Type[], out Archetype) the backing PooledDictionary uses reference equality, which leads to unexpected behavior such as this:

var archTypes1 = new Type[] { typeof(Transform) };
var archTypes2 = new Type[] { typeof(Transform) };

var entity1 = world.Create(archTypes1);
var entity2 = world.Create(archTypes2);

Creating 2 Archetypes in the World as archTypes1 and archTypes2 don't reference the same instance of Type[].

The fix is quite easy, implement an IEqualityComparer<Type[]> that uses SequenceEquals and pass it to the PooledDictionary to use.

Bulk operations for `QueryDescriptions`

Would probably make sense to add "bulk" operations for Set, Add, Remove components by not targeting a single entity... instead targeting whole chunks and archetypes by queries directly. This looks simplified like this :

world.Set(in queryDescription, new Velocity(1,0));  // Every single entity targeted by the query description gets that transform set.
world.Add(in queryDescription, new AI());  // Every single entity targeted by the query description gets that new component.
world.Remove<Sprite>(in queryDescription);  // You can imagine what this does. 
world.Destroy(in ...);

This would ease the development even more and is extremely usefull for tagging components. Its also way more efficient since it allows us to do bulk copy operations.

Since this is a more advanced feature, its not that highly priorized yet.

Command Buffer

It is not possible to Query and add, destroy or add/remove an entity at the same time. Such a operation breaks the query aswell as the underlying world.

To solve this issue we need to "record" or "buffer" such critical changes during the query to transfer those to the world after the query has finished. This would also allow us to leverage the parallel queries to the next level.

A first API draft would look like this :

var cb = new CommandBuffer(world);
var entityWhichIsBuffered = cb.Create();
cb.Destroy(someExistingEntity);
cb.Set<T>(someEntity, cmp);
cb.Add<T>(someEntity, cmp);
cb.Remove<T>(someEntity);

Questions & Problems

Structs with object references or objects themself can not be written to unmanaged memory... therefore we would probably need to find an efficient solution to write and record such a broad variation of different items and types.

cb.Set<int>(someEntity, 1);
cb.Set<MyStruct>(someEntity, new MyStruct());
cb.Set...

Parallel Queries and multiple threads may write to the command buffer at the same time, this needs to work somehow. We could lock the cb and its internals, however then the parallelqueries will become a bottleneck and pretty much obsolete.

How could we do this ?

Up to 25 or 50 generic overloads ?

Currently there always 10 different generic overloads for all important methods. This could potentially be limiting...

world.Create<T0...T9>(...);

Might be not enough to describe complex entities... Especially players, mobs or similar entities could become very complex in no time. This of course depends on the way the user designs its components.

It would probably make sense to increase the amount of generic overloads to 25 or even 50 for each operation.

world.Create<T0...T25>(...);

Entity pointing to re-used entry

I might be doing this wrong, as I was keeping references to an Entity in a component.

It had an id of 100, then that entity got killed and its entry (100) was reused by some other kind of entity with a new Id (5000). So then it fails hilariously as IsAlive() is now true and has a different set of components.

So I was expecting that entity's IsAlive() to return false since that id no longer exists...

I'm ok with just keeping the entity id, but doesn't seem like there's an easy way to get an entity (or null) by entity Id from World?

Add serialization and deserialization mechanisms

I find myself iterating the entities with an open QueryDescription then serializing all the components on each one, but there should probably be a better (faster?) way. And one that captures only what is needed to rebuild the state. The use-case would be both for syncing around (network or between processes) as well as saving down to disk or database.

Any plans to implement something first class in the near future or any ideas on how to accomplish this that I am missing?

macOS arm64 support

SDK: 7.0.202 arm64
Maybe arm64 is not supported?

Arch/src/Arch.Samples/bin/Debug/net7.0/Arch.Samples
Sample App starts...
Unhandled exception. System.TypeInitializationException: The type initializer for 'Arch.Core.Utils.Group`1' threw an exception.
 ---> System.TypeInitializationException: The type initializer for 'Arch.Core.Utils.Component`1' threw an exception.
 ---> System.ArgumentException: Type 'Arch.Samples.Sprite' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.
   at System.Runtime.InteropServices.Marshal.SizeOfHelper(Type t, Boolean throwIfNotMarshalable)
   at Arch.Core.Utils.Component`1..cctor() in /Users/Kanbaru/GitWorkspace/Arch/src/Arch/Core/Utils/CompileTimeStatics.cs:line 290
   --- End of inner exception stack trace ---
   at Arch.Core.Utils.Group`1..cctor() in /Users/Kanbaru/GitWorkspace/Arch/src/Arch/Arch.SourceGen/Arch.SourceGen.QueryGenerator/CompileTimeStatics.g.cs:line 12
   --- End of inner exception stack trace ---
   at Arch.Samples.ColorSystem..ctor(World world) in /Users/Kanbaru/GitWorkspace/Arch/src/Arch.Samples/Systems.cs:line 162
   at Arch.Samples.Game.BeginRun() in /Users/Kanbaru/GitWorkspace/Arch/src/Arch.Samples/Game.cs:line 67
   at Microsoft.Xna.Framework.Game.Run(GameRunBehavior runBehavior)
   at Microsoft.Xna.Framework.Game.Run()
   at Program.<Main>$(String[] args) in /Users/Kanbaru/GitWorkspace/Arch/src/Arch.Samples/Program.cs:line 15

Process finished with exit code 134.

Reduce shared files for classes

Right now, files such as Arch/Core/World.cs host multiple classes that are not that tightly related. It would be beneficial if in such cases the classes could be broken into their own files so that it becomes easier to maintain and look for.

This is similar to #41 , but tackles a different area in order to improve the code base.

entity.Add(cmp) works differently than World.Add(entity, cmp)

Hey, things have been running pretty good so far with the latest changes. Found a minor one that seems to be reproducible:

entity.Add(new AbcComponent { Field = 1 });

Uses the default uninitialized AbcComponent component instead of the initialized component.

this does seem to work:

World.Add(entity, new AbcComponent { Field = 1 });

Exclusive Queries

Add a way to query for Entities that contain an exact match of components, so say we execute a Query with Exclusive = { Transform, Roation } in a world with an Entity (A) with components Transform and Rotation and another Entity (B) with Transform, Rotation and Ai, only Entity A would be returned.

Initially proposed in a comment on the Share your ideas and wishes discussion

Possible reference issue?

I found what might be another issue due to moving archetypes.

If I make changes using entity.Remove<>(), it seems to change the archetype of the entity immediately, so then the refs become incorrect. The Console.WriteLine("Hello") should never be reached, but I hit the break point locally.

world.Query(in enemyQuery, (in Entity entity, ref EntityStatus entityStatus, ref SpriteSheet spriteSheet, ref Animation animation) =>
            {
                if (entityStatus.State == State.ReadyToDie)
                {
                    entityStatus.State = State.Dying;

                    if (entity.Has<Burn>())
                    {
                        entity.Remove<Burn>();
                    }
                    if (entity.Has<Slow>())
                    {
                        entity.Remove<Slow>();
                    }
                    if (entity.Has<Shock>())
                    {
                        entity.Remove<Shock>();
                    }

                    if (spriteSheet.Width == 16)
                    {
                        if(entity.Get<Health>().Current > 0)
                        {
                            Console.WriteLine("hello");
                        }
                        int bloodToUse = random.Next(1, 9);
                        spriteSheet = new SpriteSheet(textures["MiniBlood" + bloodToUse], "MiniBlood" + bloodToUse, getMiniBloodNumFrames(bloodToUse), 1, 0, .5f);
                        animation = new Animation(0, getMiniBloodNumFrames(bloodToUse) - 1, 1 / 60f, 1, false);
                    }
                    else
                    {
                        int bloodToUse = random.Next(1, 5);
                        spriteSheet = new SpriteSheet(textures["Blood" + bloodToUse], "Blood" + bloodToUse, 30, 1, 0, .5f);
                        animation = new Animation(0, 29, 1 / 60f, 1, false);
                    }

                    if (entityStatus.State == State.Alive)
                    {
                        Console.WriteLine("Hello");
                    }
                }
                else if (entityStatus.State == State.Dying && animation.CurrentFrame == animation.LastFrame)
                {
                    entityStatus.State = State.Dead;
                }
            });

My solution for now will be to move the removal to a separate query because the sprite/animation change later in the loop is affecting enemies that haven't been killed yet. Oddly enough, I already had to do this to fix some issues in removing the physics component.

world.Query(in enemyQuery, (ref EntityStatus entityStatus, ref SpriteSheet spriteSheet, ref Animation animation) =>
            {
                if (entityStatus.State == State.ReadyToDie)
                {
                    entityStatus.State = State.Dying;

                    if (spriteSheet.Width == 16)
                    {
                        int bloodToUse = random.Next(1, 9);
                        spriteSheet = new SpriteSheet(textures["MiniBlood" + bloodToUse], "MiniBlood" + bloodToUse, getMiniBloodNumFrames(bloodToUse), 1, 0, .5f);
                        animation = new Animation(0, getMiniBloodNumFrames(bloodToUse) - 1, 1 / 60f, 1, false);
                    }
                    else
                    {
                        int bloodToUse = random.Next(1, 5);
                        spriteSheet = new SpriteSheet(textures["Blood" + bloodToUse], "Blood" + bloodToUse, 30, 1, 0, .5f);
                        animation = new Animation(0, 29, 1 / 60f, 1, false);
                    }
                }
                else if (entityStatus.State == State.Dying && animation.CurrentFrame == animation.LastFrame)
                {
                    entityStatus.State = State.Dead;
                }
            });

            world.Query(in query, (in Entity entity, ref EntityStatus entityStatus, ref Body body) => 
            { 
                if(entityStatus.State != State.Alive)
                {
                    physicsWorld.DestroyBody(body);
                    entity.RemoveRange(typeof(Body), typeof(Burn), typeof(Slow), typeof(Shock));
                }
            });

I may also just be trying to do too much in my loops, so feel free to tell me I'm doing things a dumb way 😄

Either way, keep of the great work on Arch!

Resource manager: Handle<T>

Implement a Handle<T> to manage managed resources as struct fields as discussed in #44

Prototype

public readonly struct Handle<T>
{
    public readonly int ID;

    internal Handle(int id) => ID = id;
}

public sealed class EcsResourceMap<T>
{
    private readonly List<T> _list = new List<T>();

    public Handle<T> Add(T item)
    {
        var handle = new Handle<T>(_list.Count);
        _list.Add(item);

        return handle;
    }

    public ref T Get(in Handle<T> handle)
    {
        return ref CollectionsMarshal.AsSpan(_list)[handle.ID];
    }
}

Sample

struct Sprite
{
   public Handle<string> Name; // Name.ID as integer
   public Handle<Texture2D> Texture; // Texture.ID as integer
   public Vector2 Position;
   public Vector2 Scale;
}

Make the World class IDisposable

Looks like whenever a new World is created it is added to the World.Worlds static list (among many other things). Then, when calling Destroy(), it is removed from that list. Seems like this should happen with an IDisposable interface as well, so that there is a common way for this to occur (and convenient with using statement). Perhaps Entity should also have this method of disposable as well.

Source generator does not properly build outside windows

C# 11 added raw string literals and although they would be a perfect fit for source generators, it seems that due to source generators needing to be compatible with .net standard 2.0 it causes weird behavior.

When compiling from visual studio on my pc, it works like a charm. however, when building from linux or a windows workflow through github, it makes mistakes in the string literal.

The terminal gets filled with errors that look like:
Error: /home/runner/work/Arch/Arch/src/Arch/Arch.SourceGen/Arch.SourceGen.QueryGenerator/Acessors.g.cs(7974,86): error CS1026: ) expected [/home/runner/work/Arch/Arch/src/Arch/Arch.csproj::TargetFramework=netstandard2.1]

As a test i decided to change StructuralChangesExtensions.AppendWorldAdd(this StringBuilder, int)

var template =
    $$"""
    [SkipLocalsInit]
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Add<{{generics}}>(in Entity entity, {{parameters}})
    {
        var oldArchetype = EntityInfo[entity.Id].Archetype;
        // Create a stack array with all component we now search an archetype for.
        Span<int> ids = stackalloc int[oldArchetype.Types.Length + {{amount + 1}}];
        oldArchetype.Types.WriteComponentIds(ids);
        {{setIds}}
        if (!TryGetArchetype(ids, out var newArchetype))
            newArchetype = GetOrCreate(oldArchetype.Types.Add({{types}}));
        Move(in entity, oldArchetype, newArchetype, out var newSlot);
        newArchetype.Set<{{generics}}>(ref newSlot, {{inParameters}});
    }
    """;

to

string template = string.Format(
@"[SkipLocalsInit]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add<{0}>(in Entity entity, {1})
{
    var oldArchetype = EntityInfo[entity.Id].Archetype;
    // Create a stack array with all component we now search an archetype for.
    Span<int> ids = stackalloc int[oldArchetype.Types.Length + {2}];
    oldArchetype.Types.WriteComponentIds(ids);
    {3}

    if (!TryGetArchetype(ids, out var newArchetype))
        newArchetype = GetOrCreate(oldArchetype.Types.Add({4}));
    Move(in entity, oldArchetype, newArchetype, out var newSlot);
    newArchetype.Set<{0}>(ref newSlot, {5});
}", generics, parameters, amount + 1, setIds, types, inParameters);

As a result the errors that looked like the one i described earlier went away.

Edit

the actual issue was due to the stringbuilder being trimmed too far on Linux due to a different newline standard.

For any future souls getting haunted by source generators, temporarily adding

<PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

to your project might help with debugging the code that gets generated. that way you can a view into what actually gets generated.

Arch.Extended `ParallelQuery`?

Discussed in #62

Originally posted by stgeorge February 4, 2023
This is more for the Arch.Extended project but it didn't have a Discussions section.

I noticed there's not an easy way to change a [Query] to parallel other than kind of pulling it out of the source generator and using the Arch scheme. While this is OK, it would be pretty cool to just be able to say [ParallelQuery] and have it work without much fuss. I am liking the source generator approach in terms of less boilerplate.

Add Query Unique

We need a method to query singleton components. We will use this method in many scenarios, such as Player Input, making the code look more concise and readable.

    private QueryDescription mDescription = new QueryDescription{Any = new ComponentType[]{typeof(C_PlayerInput)}};
    public void Query()
    {
        world.Query(mDescription, (ref C_PlayerInput input) =>
        {
            if (input.IsClicked)
            {
                //Query another entity....
            }
        });
    }
    public void QueryUnique()
    {
        var playerInput = world.QueryUnique<C_PlayerInput>();
        //Query another entity...
    }

Sorting entities

Great library! I ditched my naive one and used this and tripled the raw frame rate.

I'd like to get a set of entities sorted by their Y position to draw them back to front, is there a performant way to do this?

Replace Dictionary EntityInfo with a simple array.

Currently, in the code, you have a EntityInfo property which is a dictionary but you can make it into an array because all the entities ids just increasing numbers starting from 0. So if you have an entity with an id 5 it can be inside the array at index 5. I'm planning to make a pull request about this, but the array resize logic needs to live somewhere should I place an EnsureCapacity method inside the World class or have an Array Wrapper?

16KB Chunks should scale up when entities are too big in size...

When entities are getting too big in size ( in terms of components and the sum of their total bytes ), the archetypes should increase their total chunk capacity to like 32,64 or 128 KB. This would improve performance for huge entities and datasets.

E.g :

var archetype = new Type[]{
   typeof(Transform),
   typeof(Rotation),
   typeof(Stats),         // Like 10 floats or whatever
   typeof(Health),
   typeof(Inventory),
   typeof(Equipment),
  .... // More components
};

Important entities like players are mostly getting pretty big... there always ways to reduce the size of components, however its not alwayss possible. In such a case above only a handfull of entities fit into each chunk... which means more chunk switches and therefore more cache misses.

By increasing the chunk capacity for bigger entities, this could be avoided and still provide the best possible performance.

Key not found when calling world.Destroy

         struct T { };
         struct K { };

          var arch = new Type[] { typeof(T), typeof(K) };

            for (int i = 0; i< 100; ++i)
                _world.Create(arch);

            var rng = new Random();
            while (true)
            {
                _world.Query(new QueryDescription() { All = arch }, (in Entity e, ref T t, ref K k) =>
                {
                    if (rng.Next() % 2 == 0)
                    {
                        _world.Destroy(in e);
                    }
                });
            }

At the exception throwing the _world.Size is > 0

GetEntites throws a NullReferenceException if the world doesn't contain any entities that match the query

Repro/ UnitTest for it:

[Test]
public void GetEntitesTest()
{
	world = World.Create();

	var archTypes = new Type[] { typeof(Transform) };
	var query = new QueryDescription { All = archTypes };

	var entity = world.Create(archTypes);

	var entites = new List<Entity>();
	world.GetEntities(query, entites);

	Assert.That(entites.Count, Is.EqualTo(1));

	entites.Clear();
	world.Destroy(entity);
	world.GetEntities(query, entites);

	Assert.That(entites.Count, Is.EqualTo(0));
}

`WithNone<Component>()` doesn't remove those entities from the query

I'm guessing that something is making it so that the WithNone and WithAll are conflicting with each other.

Here's the example code:

QueryDescription groundSpritesQuery = new QueryDescription()
                                                .WithNone<Aura, CanFly>()
                                                .WithAll<EntityStatus, Position, SpriteSheet, Animation>();

world.Query(in groundSpritesQuery, (in Entity entity, ref EntityStatus entityStatus, ref Position pos, ref Animation anim, ref SpriteSheet sprite) =>
                {
                    if (entity.Has<Aura>())
                    {
                        Console.WriteLine("hello");
                    }
                    if (entityStatus.State != State.Dead)
                    {
                        Vector2 position = pos.XY - playerPosition;
                        renderEntity(spriteBatch, textures, sprite, anim, position, offset);
                    }
                });

Set and enforce a consistent code style

Arch is currently using unconventional code styles in a lot of places. I would like to propose some more consistent styles, as well as some other things to potentially look at. Enforcing a consistent code style allows for a better overall coding and reading experience.

Major things this project needs to tackle:

  • Misspellings in both documentation and code.
  • Using one file for multiple types.
  • Misuse of the in modifier.
  • Overuse of the MethodImpl attribute.
  • Difficult readability because of single-line statements.

Below, I've listed some conventions this project can enforce. These are up for discussion though. I'm happy to help implementing this.

The below can also be found here as an .editorconfig file.


Naming Conventions and Accessibility Modifiers

  • private, protected, private protected, and protected internal fields should be _camelCase. The same applies when they're static.
  • internal and public properties should be PascalCase. The same applies when they're static.
  • const locals and fields should be PascalCase.
  • Parameters should be camelCase.
  • Methods, enums, structs and classes (and their record equivalents), and namespaces should be PascalCase.
  • Type parameters should be TPascalCase.
  • Interfaces should be IPascalCase.

Fields should never be internal or public. Properties should never be private, protected, private protected, or protected internal.
Accessibility modifiers should be required at all times.
readonly should be added whenever possible.

var Preferences

  • Do not use var anywhere.

.NET Conventions

  • System using directives should always be sorted first.
  • .this qualification should never be used.
  • Predefined types (int) should always be used over framework types (Int32).
  • Parentheses should be used for clarity:
    • In arithmetic operators: a + (b * c)
    • In other binary operators: a || (b && c)
    • In relational operators: (a < b) == (c > d)

Expressions

  • Should prefer index and range operators.
  • Should prefer simple default expression.
  • Should prefer object and collection initializers.
  • Should prefer conditional expression (?:) over if with assignments and returns.
  • Should prefer local function over anonymous function (lambda).
  • Should prefer implicit object creation when type is apparent (target-typed new).
  • Do not use inferred tuple element and anonymous type member names.

  • Do not use expression body for constructors.
  • Do not use expression body for local functions.
  • Do not use expression body for methods.
  • Do not use expression body for operators.
  • Do not use expression body for indexers.
  • Do not use expression body for properties.
  • Should prefer expression body for accessors when on single line.
  • Should prefer expression body for lambdas when on single line.

Pattern Matching and null Checking

  • Should prefer pattern matching when possible.
  • Do not use throw-expression (obj ?? throw new ArgumentNullException(nameof(obj));).
  • Should prefer conditional delegate call.
  • Should prefer coalesce expression.
  • Should prefer null propagation.
  • Should prefer is null over ReferenceEquals or == null.

New Line and Braces Preferences

  • Do not allow multiple blank lines.
  • Do not allow embedded statements on same line.
  • Do not allow blank lines between consecutive braces.
  • Do not allow statements immediately after block.
  • Do not allow blank line after colon in constructor initializer.
  • Should prefer opening brace after if, else, case blocks.

Empty structs / Tag components should not consume any memory

Tag components ( Empty structs ) do actually consume 1 byte per instance and are actually being allocated in each chunk.
Instead we could just ignore them, however this means that that some query like this...

world.Query(in desc, (ref Transform t, ref SomeTag tag) => {});

Would throw an exception since the entity is tagged with it, but since its a non allocated tag component it can not be acessed.
Debug.Assert could probably help here to avoid such errors.

More generic overloads for `world.` and `entity.` methods

Would be probably a cool addition to the existing API to allow some generic methods to have more generic overloads...

var entity = world.Create<Health,Transform>();
var entity = world.Create(in Health(), in Transform(), ...);

entity.Set(new Health(), new Transform(),...);
entity.Get<Health, Transform>(out var health, out var transform);
entity.Add<Stats,AI>();
entity.Remove<Stats,AI>();

// The same required for world. since entity. relies on them
world.Set(new Health(), new Transform(),...);
world.Get<Health, Transform>(out var health, out var transform);
world.Add<Stats,AI>();
world.Remove<Stats,AI>();

This would reduce boilerplate code and would also speed up some stuff :)
Probably #19 needed, since the generic overloads would likely need to target the fitting archetype ( atleast for creating, adding and removing ).

Godot engine is not supported by this repo?

Hi, I'm a Godot and C# beginner, In the README.md file, it says as follows:

  • 🚢 SUPPORT > Supports .NetStandard 2.1, .Net Core 6 and 7 and therefore you may use it with Unity or Godot!

However, I found there are some C# 11 syntaxes, like this (using the scoped keyword):

Arch/Arch/Core/Archetype.cs

Lines 200 to 204 in b08cc53

internal unsafe ref T Get<T>(scoped ref Slot slot)
{
ref var chunk = ref GetChunk(slot.ChunkIndex);
return ref chunk.Get<T>(in slot.Index);
}

As far as I know, the newest Godot 4.0 support only .Net 6.0 and C# 10:

With the move to .NET 6, users can now target a newer framework that brings optimizations and new APIs. With .NET 6, projects use C# 10 by default and all features are available.

Does it mean the Godot engine is not supported by this project?

Query BitSet comparison issue

Found an issue when my component count increased past 32. It started failing on components being "found" in Chunks not containing them causing memory access errors.

In BitSet All method, when the bitset for the new component extends to 2 uint's, but it checks against a bitset with 1 uint, there is an extra check to handle bitset's 2nd uint:

    var extra = _bits.Length - count;
    for (var i = count; i < extra; i++)

Since extra is subtracting "count", and starting the loop from "count", the loop won't run and always returns true. It can use the bits.Length to loop through the extra uint.

    for (var i = count; i < _bits.Length; i++)

A unit test for it:

[Test]
public void AllWithDifferentLengthBitSet()
{
    var bitSet1 = new BitSet();
    bitSet1.SetBit(5);
    var bitSet2 = new BitSet();
    bitSet2.SetBit(33);

    bool result = bitSet2.All(bitSet1);

    That(result, Is.EqualTo(false));
}

Ref iterators for `Query`

Would be neat to have one more alternative to the existing query APIs. A iterator directly received from the query, replacing world.Query(in, (...) => {});

Like this :

var query = world.Query(in desc); 
foreach(var refs in query.GetIterator<Position, Velocity,...>()){
   refs.t0.x += refs.t1.x;
   refs.t0.y += refs.t1.y;
}

foreach(var refs in query.GetEntityIterator<Position, Velocity,...>()){
   Console.WriteLine(refs.entity);
   refs.t0.x += refs.t1.x;
   refs.t0.y += refs.t1.y;
}

Those could probably even replace world.Query(in, (...) => {}) calls completly, since they offer more advantages. They will not closure copy, offer greater controll over the iteration flow and are probably easier to maintain.

@andreakarasho Probably already had a first draft of this according to #35 ( if i understood that correctly ) ^^

Entity Events

Being able to listen for entities event is also probably a nice feature. Many ECS frameworks actually do this.
However, this should be an optional feature since it would heavily conflict with the "performance first" approach of this project.

world.OnEntityAdded += (in Entity en) => {};
world.OnEntityRemoved += (in Entity en) => {};
world.OnAdded<T> += (in Entity en, ref T cmp) => {};
...

Issue with .dll hot-reloading and Type change

Hello,

I am using Arch in my game engine but facing an issue when reloading a .dll. My dll contains a Component struct and Arch would fail to recognize that the component from a new reloaded .dll is the same as from the old one. It wouldn't match any queries and I would be able to assign duplicate components of the "same" type to an entity.

I see that you use direct matching of Types in a dictionary to match the components, do you think it's possible to change it to a more robust approach, or do you happen to have tips on how to make Arch compatible with hot-reloading ?

 /// <summary>
    ///     Trys to get a component if it is registered.
    /// </summary>
    /// <param name="type">Its <see cref="Type"/>.</param>
    /// <param name="componentType">Its <see cref="ComponentType"/>, if it is registered.</param>
    /// <returns>True if it registered, otherwhise false.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryGet(Type type, out ComponentType componentType)
    {
        return _types.TryGetValue(type, out componentType);
    }

Thanks

Array.CreateInstance

Today a chunk needs to allocate an array of array of struct. This is achieved using the API Array.CreateInstance which creates an opaque managed array of objects[].
I see some downside:

  • the compiler cannot predict the bytes size in memory, this is done during the runtime. So we could have a waste of resources here
  • accessing to a component might be slower when using the Unsafe.As API for the reason above (things get done runtime side)
  • the app cannot use the nativeaot optimizations (disable reflection) if the users wants (need to try out tbh)

I was wondering if it's worth to replace the managed Array.CreateInstance with an opaque unmanaged pointers pool.

Compile time static Queries, GroupDescriptions and more ?

Theres still a lot of mapping under the hood, often used with Dictionarys... and we all know that Dictionarys are kinda slow. You may ask why ?

Since we often need to find the certain archetype, query or bitset for a Type[]. So we need to map it, which is hella expensive.

world.TryGetArchetype();
world.Query();
world.Move();
entity.Add/Remove

Make heavily use of that mapping.
An alternative to this approach is to somehow use compile time static magic to prevent additional dictionary lookups. Which also increases the performance a lot.

public static class Archetype{
	
	public static int Id;
}

public static class Archetype<T>{
	
	public static readonly int Id;
	public static readonly Type[] BitSet; // BitSet würde sich durch ComponentMeta von T erstellen lassen durch code generierung. 
	static Archetype(){	
		Id = ++Archetype.Id; 
		BitSet = new Type[]{ typeof(T) };					  
	}

	public static class Add<A>{
		
		public static readonly int InnerId;
		public static readonly Type[] AddedBitSet;
		
		static Add(){ 
			InnerId = Archetype<T,A>.Id;
			AddedBitSet = Archetype<T,A>.BitSet;
		}
	}
}

public static class Archetype<T,TT>{
	
	public static readonly int Id;
	public static readonly Type[] BitSet; 
	static Archetype(){	
		Id = ++Archetype.Id;
		BitSet = new Type[]{ typeof(T), typeof(TT)};
	}
	
	public static class Add<A>{
		
		public static readonly int InnerId;
		public static readonly Type[] AddedBitSet; 
		static Add(){ 
			InnerId = Id;
			AddedBitSet = new Type[]{ typeof(T), typeof(TT), typeof(A) };
		}
	}
	
	public static class Remove<T>{
		
		public static readonly int InnerId;
		public static readonly Type[] RemovedBitSet;
		
		static Remove(){ 
			InnerId = Archetype<TT>.Id;
			RemovedBitSet = Archetype<TT>.BitSet;
		}
	}
}

This is compile time static and could be used to receive bitsets quickly. The queryDesc could additional receive those ids or the bitset directly to avoid that query dictionary lookup. The old queryDesc would stay since its more readable.

var bitSet = Archetype<Health, Transform>.BitSet; // Compile time static, instant, no lookups required. 
var queryDesc = new QueryDescription{
  All = Archetype<Health, Transform>.Bitset,
  None = Archetype<AI>.Bitset
};

// Or 
var queryDesc = new QueryDescription().All<Health, Transform>().None<AI>();  // Acesses Archetype<T,...> under the hood

// No lookups needed since BitSets are compiletime static generated and our query would know instantly everything it needs. 
world.Query(in queryDesc,....);

Would also be great for structural changes... since we can easily calculate the archetype an entity would be moved to.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.