Closed Doraku closed 3 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;
}
EntitiesMap<Level<T>>
so we can process sequentially each level in parallelWorld.SubscribeComponentAdded/Changed/Removed<Parent<T>>
to update Level<T>
valueWorld.SubscribeComponentRemoved<T>
to remove child Parent<T>
Level<T>
need to be updatedProbably 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?
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
}
Generation
component should be hidden from userParent
component is global, can't set different hierarchy for different usecase (seems like a nightmare to make something more generic...)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.
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);
}
}
}
Missing stuff:
DrawInfo
component of the parent is removed?)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)
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.
This is the one and only problem for me with ECS - I need hierarchies, it would be awesome if this was included : )
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:
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
Indeed, I too need this which is what prompt me to create this issue. Currently this is handled in my engine (and editor) 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 :/
Ah. I understand. Thanks for explaining : )
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 ^^")
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).
Yeah. Cool stuff : ) Thanks for explaining. Hope to see something like it in the final DefaultEcs : )
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();
}
}
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.
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.
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.
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.
That's a great idea. Thanks for sharing : )
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.