Doraku / DefaultEcs

Entity Component System framework aiming for syntax and usage simplicity with maximum performance for game development.
MIT No Attribution
655 stars 62 forks source link

add hierarchy logic between entities #97

Closed Doraku closed 3 years ago

Doraku commented 4 years ago

ex: need some easy way to state that a child entity should be rendered at a relative position to its parent entity. This logic is highly specific to the used engine but DefaultEcs should provide some assistance in such endeavor.

Doraku commented 4 years ago

pseudo idea of how to achieve this:

public readonly struct Parent<T>
{
    public readonly Entity Value;
}

public readonly struct Level<T>
{
    public readonly int Value;
}

Probably need a buffered system implementation? find a way to pass the parent T value for all its children, maybe a hidden component type to easily share it?

Doraku commented 4 years ago

Current working prototype

    // component used to set parent/child relation
    public readonly struct Parent : IEquatable<Parent>
    {
        public readonly Entity Value;

        public Parent(Entity value)
        {
            Value = value;
        }

        #region IEquatable

        public bool Equals(Parent other) => Value == other.Value;

        #endregion

        #region Object

        public override bool Equals(object obj) => obj is Parent other && Equals(other);

        public override int GetHashCode() => Value.GetHashCode();

        #endregion
    }

    // component used to process each generation one after another to ensure parents has been processed before their child
    public readonly struct Generation
    {
        public readonly int Value;

        public Generation(int value)
        {
            Value = value;
        }

        #region IEquatable

        public bool Equals(Generation other) => Value == other.Value;

        #endregion

        #region Object

        public override bool Equals(object obj) => obj is Generation other && Equals(other);

        public override int GetHashCode() => Value;

        #endregion
    }

    // type to automatically set the generation based on the parent graph
    public sealed class GenerationSetter : IDisposable
    {
        private readonly EntitiesMap<Parent> _map;
        private readonly IDisposable _addedSubscription;
        private readonly IDisposable _changedSubscription;
        private readonly IDisposable _removedSubscription;

        public GenerationSetter(World world)
        {
            _map = world.GetEntities().AsMultiMap<Parent>();

            _addedSubscription = world.SubscribeComponentAdded<Parent>(OnAdded);
            _changedSubscription = world.SubscribeComponentChanged<Parent>(OnChanged);
            _removedSubscription = world.SubscribeComponentRemoved<Parent>(OnRemoved);

            using EntitySet entities = world.GetEntities().With<Parent>().AsSet();

            foreach (ref readonly Entity entity in entities.GetEntities())
            {
                OnAdded(entity, entity.Get<Parent>());
            }
        }

        private void OnAdded(in Entity entity, in Parent value)
        {
            int generation = 1;
            if (value.Value.Has<Generation>())
            {
                generation += value.Value.Get<Generation>().Value;
            }

            entity.Set(new Generation(generation));

            SetChildrenGeneration(entity, generation + 1);
        }

        private void OnChanged(in Entity entity, in Parent oldValue, in Parent newValue) => OnAdded(entity, newValue);

        private void OnRemoved(in Entity entity, in Parent value)
        {
            entity.Remove<Generation>();

            SetChildrenGeneration(entity, 1);
        }

        private void SetChildrenGeneration(in Entity entity, int generation)
        {
            if (_map.TryGetEntities(new Parent(entity), out ReadOnlySpan<Entity> children))
            {
                foreach (ref readonly Entity child in children)
                {
                    if (generation > 0)
                    {
                        child.Set(new Generation(generation));
                    }
                    else
                    {
                        child.Remove<Generation>();
                    }

                    SetChildrenGeneration(child, generation + 1);
                }
            }
        }

        #region IDisposable

        public void Dispose()
        {
            _map.Dispose();
            _addedSubscription.Dispose();
            _changedSubscription.Dispose();
            _removedSubscription.Dispose();
        }

        #endregion
    }

    // base class to create systems based on the generation
    public abstract class AHierarchyEntitiesSystem<T> : ISystem<T>
    {
        #region Types

        private sealed class NoGenerationSystem : AEntitySystem<T>
        {
            private readonly AHierarchyEntitiesSystem<T> _mainSystem;

            public NoGenerationSystem(EntitySet set, IParallelRunner runner, int minEntityCountByRunnerIndex, AHierarchyEntitiesSystem<T> mainSystem)
                : base(set, runner, minEntityCountByRunnerIndex)
            {
                _mainSystem = mainSystem;
            }

            protected override void Update(T state, ReadOnlySpan<Entity> entities) => _mainSystem.Update(state, entities);
        }

        private sealed class Runnable : IParallelRunnable
        {
            private readonly AHierarchyEntitiesSystem<T> _system;

            public T CurrentState;
            public int EntitiesPerIndex;
            public Generation Generation;

            public Runnable(AHierarchyEntitiesSystem<T> system)
            {
                _system = system;
            }

            public void Run(int index, int maxIndex)
            {
                int start = index * EntitiesPerIndex;

                _system.Update(CurrentState, _system._map[Generation].Slice(start, index == maxIndex ? _system._map.Count(Generation) - start : EntitiesPerIndex));
            }
        }

        #endregion

        #region Fields

        private readonly IParallelRunner _runner;
        private readonly Runnable _runnable;
        private readonly int _minEntityCountByRunnerIndex;
        private readonly NoGenerationSystem _noGenerationSystem;
        private readonly EntitiesMap<Generation> _map;

        #endregion

        #region Initialisation

        private AHierarchyEntitiesSystem(IParallelRunner runner, int minEntityCountByRunnerIndex)
        {
            _runner = runner ?? new DefaultParallelRunner(1);
            _runnable = new Runnable(this);
            _minEntityCountByRunnerIndex = minEntityCountByRunnerIndex;
        }

        protected AHierarchyEntitiesSystem(EntityRuleBuilder builder, IParallelRunner runner, int minEntityCountByRunnerIndex)
            : this(runner, minEntityCountByRunnerIndex)
        {
            _noGenerationSystem = new NoGenerationSystem(builder.Copy().Without<Generation>().AsSet(), runner, minEntityCountByRunnerIndex, this);
            _map = builder.AsMultiMap<Generation>();
        }

        protected AHierarchyEntitiesSystem(EntityRuleBuilder builder, IParallelRunner runner)
            : this(builder, runner, 0)
        { }

        protected AHierarchyEntitiesSystem(EntityRuleBuilder builder)
            : this(builder, null)
        { }

        #endregion

        #region Methods

        protected virtual void PreUpdate(T state) { }

        protected virtual void PostUpdate(T state) { }

        protected virtual void Update(T state, in Entity entity) { }

        protected virtual void Update(T state, ReadOnlySpan<Entity> entities)
        {
            foreach (ref readonly Entity entity in entities)
            {
                Update(state, entity);
            }
        }

        #endregion

        #region ISystem

        public bool IsEnabled { get; set; } = true;

        public void Update(T state)
        {
            if (IsEnabled)
            {
                PreUpdate(state);

                _noGenerationSystem.Update(state);

                _runnable.CurrentState = state;

                foreach (Generation generation in _map.Keys)
                {
                    _runnable.EntitiesPerIndex = _map.Count(generation) / _runner.DegreeOfParallelism;
                    _runnable.Generation = generation;

                    if (_runnable.EntitiesPerIndex < _minEntityCountByRunnerIndex)
                    {
                        Update(state, _map[generation]);
                    }
                    else
                    {
                        _runner.Run(_runnable);
                    }
                }
                _map.Complete();

                PostUpdate(state);
            }
        }

        #endregion

        #region IDisposable

        public virtual void Dispose()
        {
            _map.Dispose();
            _noGenerationSystem.Dispose();
        }

        #endregion
    }

Not sure if this should be directly in DefaultEcs, as we can't enforce the usage of GenerationSetter, this seems like a job for the engine using DefaultEcs. Still I will see if the api can be improved to reduce this bolder plate.

Doraku commented 4 years ago

4efce50e31f49bab9fc70525a58a3efda3109a65 a lot of the bolder plate code has been reduced Example of usage

        // apply the parent position to their child
        [With(typeof(DrawInfo), typeof(Parent))]
        private sealed class ParentSystem : AEntitiesSystem<float, Generation>
        {
            public ParentSystem(World world, IParallelRunner runner)
                : base(world, runner)
            { }

            protected override void Update(float state, in Generation key, ReadOnlySpan<Entity> entities)
            {
                foreach (ref readonly Entity entity in entities)
                {
                    ref DrawInfo drawInfo = ref entity.Get<DrawInfo>();
                    ref DrawInfo parent = ref entity.Get<Parent>().Value.Get<DrawInfo>();

                    drawInfo.Position = parent.Position + Vector2.Transform(drawInfo.Position, Matrix.CreateRotationZ(parent.Rotation));
                    drawInfo.Rotation += parent.Rotation;
                }
            }
        }

and it can be used in some interesting way

        // render all entities in a specific layer order
        [With(typeof(DrawInfo))]
        private sealed class LayerSystem : AEntitiesSystem<float, Layer>
        {
            private readonly SpriteBatch _batch;
            private readonly Layer[] _layers;

            public LayerSystem(World world, SpriteBatch batch)
                : base(world)
            {
                _batch = batch;
                _layers = new[]
                {
                    Layer.Background,
                    Layer.Unit,
                    Layer.Particle,
                    Layer.Ui
                };
            }

            protected override Span<Layer> GetKeys() => _layers.AsSpan();

            protected override void Update(float state, in Layer key, ReadOnlySpan<Entity> entities)
            {
                foreach (ref readonly Entity entity in entities)
                {
                    ref DrawInfo drawInfo = ref entity.Get<DrawInfo>();

                    _batch.Draw(drawInfo.Texture, drawInfo.Position, null, drawInfo.Color, drawInfo.Rotation, drawInfo.Origin, drawInfo.Size, SpriteEffects.None, 0f);
                }
            }
        }

see https://github.com/Doraku/DefaultEcs/blob/4efce50e31f49bab9fc70525a58a3efda3109a65/source/Sample/DefaultBoids/System/SetBehaviorSystem.cs

Missing stuff:

MarkWilds commented 4 years ago

Personally I think defaultecs should stay lean and believe this is not the responsibility of the ecs system to handle. (I also believe this way for entity relations)

Doraku commented 4 years ago

I am leaning more and more toward this conclusion too. The horrible parent/child implementation has been an eyesore to me for a long time, even if it is not supported directly in DefaultEcs I still want to give a safe way to do it in engine. Supporting a link to an other entity component (not the same a shared component value) is probably the only thing remaining to do which has its place here.

georg-eckert-zeiss commented 4 years ago

This is the one and only problem for me with ECS - I need hierarchies, it would be awesome if this was included : )

georg-eckert-zeiss commented 4 years ago

Since almost every game relies on some sort of scene graph / child-parent relations, I think this should be built into DefaultEcs. Otherwise this will stop many people (especially when developing in 3D) from using it. Right now it is very hard to apply relative movements with DefaultEcs - think of a scene like this:

ezgif com-optimize

In my old system I did it with Transform components. Calculating World Matrices relies on accessing parent entities. I could not yet figure out how to implement it with DefaultEcs : (

Looking forward for future updates. <3

Doraku commented 4 years ago

Indeed, I too need this which is what prompt me to create this issue. Currently this is handled in my engine (and editor) image Internally the editor set a special component:

    public readonly struct Parent : IEquatable<Parent>
    {
        public readonly Entity Value;

        public Parent(Entity value)
        {
            Value = value;
        }

        #region IEquatable

        public bool Equals(Parent other) => Value == other.Value;

        #endregion

        #region Object

        public override bool Equals(object obj) => obj is Parent other && Equals(other);

        public override int GetHashCode() => Value.GetHashCode();

        #endregion
    }

    // this one is what is serialized, each entity has a string component which is just a guid really
    public readonly struct ParentReference
    {
        public readonly string Value;

        public ParentReference(string value)
        {
            Value = value;
        }
    }

The engine then uses its own special component and systems:

            // this is what is done on a world load to set the Parent component
            using (EntityMap<string> map = _world.GetEntities().AsMap<string>())
            using (EntitySet entities = _world.GetEntities().With<ParentReference>().AsSet())
            {
                foreach (ref readonly Entity entity in entities.GetEntities())
                {
                    entity.Set(new Parent(map[entity.Get<ParentReference>().Value]));
                }
            }

    // used to create the order of the update
    public readonly struct Generation : IComparable<Generation>
    {
        public readonly int Value;

        public Generation(int value)
        {
            Value = value;
        }

        #region IComparable

        public int CompareTo(Generation other) => Value.CompareTo(other.Value);

        #endregion

        #region IEquatable

        public bool Equals(Generation other) => Value == other.Value;

        #endregion

        #region Object

        public override bool Equals(object obj) => obj is Generation other && Equals(other);

        public override int GetHashCode() => Value;

        #endregion
    }

    // and the system responsible for setting the correct generation
    public sealed class GenerationSetter : IDisposable
    {
        private readonly EntitiesMap<Parent> _map;
        private readonly IDisposable _addedSubscription;
        private readonly IDisposable _changedSubscription;
        private readonly IDisposable _removedSubscription;

        public GenerationSetter(World world)
        {
            _map = world.GetEntities().AsMultiMap<Parent>();

            _addedSubscription = world.SubscribeComponentAdded<Parent>(OnAdded);
            _changedSubscription = world.SubscribeComponentChanged<Parent>(OnChanged);
            _removedSubscription = world.SubscribeComponentRemoved<Parent>(OnRemoved);

            using EntitySet entities = world.GetEntities().With<Parent>().AsSet();

            foreach (ref readonly Entity entity in entities.GetEntities())
            {
                OnAdded(entity, entity.Get<Parent>());
            }
        }

        private void OnAdded(in Entity entity, in Parent value)
        {
            int generation = 1;
            if (value.Value.Has<Generation>())
            {
                generation += value.Value.Get<Generation>().Value;
            }

            entity.Set(new Generation(generation));

            SetChildrenGeneration(entity, generation + 1);
        }

        private void OnChanged(in Entity entity, in Parent oldValue, in Parent newValue) => OnAdded(entity, newValue);

        private void OnRemoved(in Entity entity, in Parent value)
        {
            entity.Remove<Generation>();

            SetChildrenGeneration(entity, 1);
        }

        private void SetChildrenGeneration(in Entity entity, int generation)
        {
            if (_map.TryGetEntities(new Parent(entity), out ReadOnlySpan<Entity> children))
            {
                foreach (ref readonly Entity child in children)
                {
                    if (generation > 0)
                    {
                        child.Set(new Generation(generation));
                    }
                    else
                    {
                        child.Remove<Generation>();
                    }

                    SetChildrenGeneration(child, generation + 1);
                }
            }
        }

        #region IDisposable

        public void Dispose()
        {
            _map.Dispose();
            _addedSubscription.Dispose();
            _changedSubscription.Dispose();
            _removedSubscription.Dispose();
        }

        #endregion
    }

        // and finally the system which set the correct potition/rotation relative to the parent, it use Generation as a key so it is automatically sorted and updated by it
        [With(typeof(DrawInfo), typeof(Parent))]
        private sealed class ParentSystem : AEntitiesSystem<float, Generation>
        {
            public ParentSystem(World world, IParallelRunner runner)
                : base(world, runner)
            { }

            protected override void Update(float state, in Generation key, ReadOnlySpan<Entity> entities)
            {
                foreach (ref readonly Entity entity in entities)
                {
                    ref DrawInfo drawInfo = ref entity.Get<DrawInfo>();
                    ref DrawInfo parent = ref entity.Get<Parent>().Value.Get<DrawInfo>();

                    drawInfo.Position = parent.Position + Vector2.Transform(drawInfo.Position, Matrix.CreateRotationZ(parent.Rotation));
                    drawInfo.Rotation += parent.Rotation;
                }
            }
        }

It works great but it's far from satisfactory, still working on what could be moved into DefaultEcs to reduce the bloat needed for users to have such feature :/

georg-eckert-zeiss commented 4 years ago

Ah. I understand. Thanks for explaining : )

georg-eckert-zeiss commented 4 years ago

I am a little bit confused by the Generation struct though. Does this represent some kind of hierarchy level? ( I know its used to process entities in the correct order - the name is somewhat confusing ^^")

Doraku commented 4 years ago

Yes, in my editor, the black brick are entities and are organized in a hierarchy (white brick are template which are an easy way for me to apply a collection of components to an entity and puzzle piece are component but I digress). The generation is basically the depth of a child entity. The AEntitiesSystem will use this to group all entities at the same depth/generation so it can safely process in parallel all the entities of a single group (a child and a parent can't be of the same generation), each group being processed sequentially in order of the generation (because the component implement IComparable<Generation>) to ensure that when your process a child, its parent has already been processed and has the correct final position/rotation (in my usage anyway).

georg-eckert-zeiss commented 4 years ago

Yeah. Cool stuff : ) Thanks for explaining. Hope to see something like it in the final DefaultEcs : )

georg-eckert-zeiss commented 4 years ago

I modified this a little bit, since my transform component is a little bit more complex. I wanted all hierarchy levels to have the Generation component so I have one system for all entities that have DrawInfo It might be of some help :):

In my case your DrawInfo is similar to my Transform and your Generation is renamed to HierarchyLevel.

using Microsoft.Xna.Framework;

public struct Transform : IComponent
{
  public Vector3 LocalPosition;
  public Quaternion LocalRotation;
  public Vector3 LocalScale;

  public Vector3 Forward { get; internal set; }
  public Vector3 Backward { get; internal set; }
  public Vector3 Right { get; internal set; }
  public Vector3 Left { get; internal set; }
  public Vector3 Up { get; internal set; }
  public Vector3 Down { get; internal set; }

  public Vector3 Position { get; internal set; }
  public Quaternion Rotation { get; internal set; }
  public Vector3 Scale { get; internal set; }

  public Matrix LocalToGlobalTransformation { get; internal set; }
  public Matrix GlobalToLocalTransformation { get; internal set; }
  public Matrix LocalToParentTransformation { get; internal set; }
  public Matrix ParentToLocalTransformation { get; internal set; }
}

The generation setter is now:

using System;
using DefaultEcs;

public sealed class HierarchyLevelSetter : IDisposable
{
  private readonly EntitiesMap<Parent> _Map;
  private readonly IDisposable _AddedSubscription;
  private readonly IDisposable _ChangedSubscription;
  private readonly IDisposable _RemovedSubscription;

  public HierarchyLevelSetter( World world )
  {
    _Map = world.GetEntities().AsMultiMap<Parent>();

    _AddedSubscription = world.SubscribeComponentAdded<Parent>( OnAdded );
    _ChangedSubscription = world.SubscribeComponentChanged<Parent>( OnChanged );
    _RemovedSubscription = world.SubscribeComponentRemoved<Parent>( OnRemoved );

    using EntitySet entities = world.GetEntities().With<Parent>().AsSet();
    foreach( ref readonly var entity in entities.GetEntities() )
    {
      OnAdded( entity, entity.Get<Parent>() );
    }
  }

  private void OnAdded( in Entity entity, in Parent parent )
  {
    if(!parent.Value.Has<HierarchyLevel>())
    {
      parent.Value.Set(new HierarchyLevel( 0 ));
    }

    var level = 1 + parent.Value.Get<HierarchyLevel>().Value;

    entity.Set( new HierarchyLevel( level ) );

    SetChildrenHierarchyLevel( entity, level + 1 );
  }

  private void OnChanged( in Entity entity, in Parent oldValue, in Parent newValue )
  {
    OnAdded( entity, newValue );
  }

  private void OnRemoved( in Entity entity, in Parent value )
  {
    var oldLevel = entity.Get<HierarchyLevel>().Value;
    entity.Set( new HierarchyLevel(oldLevel - 1) );

    SetChildrenHierarchyLevel( entity, 1 );
  }

  private void SetChildrenHierarchyLevel( in Entity entity, int generation )
  {
    if( !_Map.TryGetEntities( new Parent( entity ), out var children ) ) 
      return;

    foreach( ref readonly var child in children )
    {
      child.Set( new HierarchyLevel( generation ) );

      SetChildrenHierarchyLevel( child, generation + 1 );
    }
  }

  public void Dispose()
  {
    _Map.Dispose();
    _AddedSubscription.Dispose();
    _ChangedSubscription.Dispose();
    _RemovedSubscription.Dispose();
  }
}

And my transform updating system:

[With( typeof( Transform ), typeof( HierarchyLevel ) )]
public class TransformSystem : AEntitiesSystem<float, HierarchyLevel>
{ 
  private readonly World _World;
  private readonly EntityCommandRecorder _Recorder;

  public TransformSystem( World world, IParallelRunner runner )
    : base( world, runner )
  {
    _World = world;
    _Recorder = new EntityCommandRecorder();
  }

  protected override void Update( float state, in HierarchyLevel key, ReadOnlySpan<Entity> entities )
  {
    foreach( ref readonly var entity in entities )
    {
      ref var transform = ref entity.Get<Transform>();

      Update( entity, ref transform );
    }
  }

  public static void Initialize( in Entity entity, ref Transform transform )
  {
    transform.LocalRotation = Quaternion.Identity;
    transform.LocalPosition = Vector3.Zero;
    transform.LocalScale = Vector3.One;

    Update( entity, ref transform );
  }

  public static void Update( in Entity entity, ref Transform transform )
  {
    // Calculate global transforms for all entities.
    transform.Position = transform.LocalPosition;
    transform.Scale = transform.LocalScale;
    transform.Rotation = transform.LocalRotation;

    transform.LocalToParentTransformation
      = Matrix.CreateScale( transform.LocalScale )
         * Matrix.CreateFromQuaternion( transform.LocalRotation )
         * Matrix.CreateTranslation( transform.LocalPosition );
    transform.ParentToLocalTransformation
      = Matrix.Invert( transform.LocalToParentTransformation );
    transform.LocalToGlobalTransformation
      = transform.LocalToParentTransformation;

    // Add parent transforms for entities of deeper hierarchy levels.
    if( entity.Has<Parent>() )
    {
      var parentTransform = entity.Get<Parent>().Value.Get<Transform>();
      transform.Rotation *= parentTransform.Rotation;
      transform.Scale *= parentTransform.Scale;
      transform.Position *= parentTransform.Position;
      transform.LocalToGlobalTransformation *= parentTransform.LocalToGlobalTransformation;
    }

    // Calculate derived transforms.
    transform.GlobalToLocalTransformation
      = Matrix.Invert( transform.LocalToGlobalTransformation );

    transform.Forward = Vector3.Transform( Vector3.Forward, transform.Rotation );
    transform.Backward = Vector3.Transform( Vector3.Backward, transform.Rotation );
    transform.Up = Vector3.Transform( Vector3.Up, transform.Rotation );
    transform.Down = Vector3.Transform( Vector3.Down, transform.Rotation );
    transform.Right = Vector3.Transform( Vector3.Right, transform.Rotation );
    transform.Left = Vector3.Transform( Vector3.Left, transform.Rotation );
  }

  protected override void PostUpdate( float state )
  {
    _Recorder.Execute( _World );
  }

  public override void Dispose()
  {
    _Recorder.Dispose();

    base.Dispose();
  }
}
Doraku commented 4 years ago

Nice! But see I feel this really has its place in the engine. I feel only the parent/hierarchy level could be generic enough to be part of DefaultEcs in some form.

The only thing that I am not perfectly happy with AEntitiesSystem is that each entity has to request the parent component. For maximum performance we should group by parent and pass its value to the Update of each child (instead of each child having to get the parent value on its own) but I fear groups would be too small and hard to run in parallel, making it actually less performant. What do you think?

Anyway glad to see you could include this in your project without too much hassle (I hope!), until it get easier :D.

MarkWilds commented 4 years ago

Nice! But see I feel this really has its place in the engine. I feel only the parent/hierarchy level could be generic enough to be part of DefaultEcs in some form.

The only thing that I am not perfectly happy with AEntitiesSystem is that each entity has to request the parent component. For maximum performance we should group by parent and pass its value to the Update of each child (instead of each child having to get the parent value on its own) but I fear groups would be too small and hard to run in parallel, making it actually less performant. What do you think?

Anyway glad to see you could include this in your project without too much hassle (I hope!), until it get easier :D.

I agree, like I said before. I dont think this belongs inside defaultECS at all. Maybe you can start creating a expansion project that builds on top of the core library. DefaultECS stays lean and if you want extra features you include the expansion lib and use the newly defined systems. Everything is potentionaly losely coupled because DefaultEcs uses an event system at its core.

onehundredfeet commented 4 years ago

I agree with Mark. There are many ways to implement hierarchy and I’d rather an ECS not prescribe it.

I like the suggestion of creating a new project that implements what you are describing as an extension.

On Sep 15, 2020, at 04:49, Mark van der Wal notifications@github.com wrote:

 Nice! But see I feel this really has its place in the engine. I feel only the parent/hierarchy level could be generic enough to be part of DefaultEcs in some form.

The only thing that I am not perfectly happy with AEntitiesSystem is that each entity has to request the parent component. For maximum performance we should group by parent and pass its value to the Update of each child (instead of each child having to get the parent value on its own) but I fear groups would be too small and hard to run in parallel, making it actually less performant. What do you think?

Anyway glad to see you could include this in your project without too much hassle (I hope!), until it get easier :D.

I agree, like I said before. I dont think this belongs inside defaultECS at all. Maybe you can start creating a expansion project that builds on top of the core library. DefaultECS stays lean and if you want extra features you include the expansion lib and use the newly defined systems. Everything is potentionaly losely coupled because DefaultEcs uses an event system at its core.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or unsubscribe.

Doraku commented 3 years ago

I went ahead and created a DefaultEcs.Extension project here. Its purpose is to provide example of features built upon the base framework that are too specific or not good enough yet in their implementation to be part of DefaultEcs. While I don't intend to release this as its own package, I see it as a good way to experiment with new functionalities and sharing a code base to help users do certain things. I think what is there should you need some kind of hierarchy is enough to integrate in your engine and start from there :) Maybe one day it will end up in DefaultEcs in a different form but for now I consider this issue closed.

georg-eckert-zeiss commented 3 years ago

That's a great idea. Thanks for sharing : )