Search Unity

Interface base queries

Discussion in 'Entity Component System' started by illinar, May 17, 2019.

  1. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Although polymorphism can often be replaced by separating components. It often seems like it would be very convinient to have interface based polymorphism.

    Just for example there are Components that implement IDisposableComponent and there is a system that can query for all disposable components and delete them at the end of the frame.

    Or there are many different components wit ITimeCountdown with Time property. And there is a system that performs Time -= deltaTime on all of them.

    Or there are many types of tweeners VelocityTweener, PositionTweener, RotationTweener etc. All they implement ITweenerComponent, and you can query them by that interface. That way you can have any number of different tweeners on an Entity but then TweeningSystem can work with them through a common interface.

    The only workaround I see in this case is to have each Tweener as a separate entity which seems much more complicated.

    TLDR: Can querying entities by custom interfaces be implemented? (e.g.
    Entities.WithInterface<ITweener>()
    ) It would be very useful, I think.
     
    Last edited: May 17, 2019
    NotaNaN likes this.
  2. BrendonSmuts

    BrendonSmuts

    Joined:
    Jun 12, 2017
    Posts:
    86
    You can get something close to this by creating generic systems that operate on interface types and creating systems for the types you care about. You can refer to this post for an example.
     
  3. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Yes, I've done this before. It's not the best way this could work also requires a custom Bootstrap which is currently not very well supported (in terms of debugging). And this requires a new system per each interface implementation, might get hard to keep track of and might have some overhead.

    Thank you though. I might also try a generic jobs. Less overhead in some cases, but I really don't like the idea of coming back and editing the system each time I add a new component type that implements the interface.
     
  4. SergeyRomanko

    SergeyRomanko

    Joined:
    Oct 18, 2014
    Posts:
    47
    I think, using interfaces like ITimeCountdown implies that at some point you will access structs (value types) by interface. This will cause boxing and creation of garbage.

    If this is true, Unity team will not do this. They have put too much effort to eliminate garbage creation.
     
  5. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Right, yes this is what I was asking. I imagine boxing means not just garbage for the collector but also usage of non-vectorized managed memory which goes against data oriented design. (Actually I'm not sure how it works)
     
  6. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    There should be a TimeCountdown component only having Time that always counts down, or a Time component only having a Value and a separate tag component Countdown.

    All of these tweeners have different data and different operations need to be performed with them. You should have three separate component, you can have them all on one entity.

    Not without boxing as this only makes sense when you actually use them through the interface. + Burst is gone.

    Usecases that actually don't use the component's data, eg your IDisposableComponent sample, could be implemented efficiently although I don't think you will use them very often. You could do that yourself using reflection where all the overhead is in OnCreate of your system.
     
  7. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    My specific case was cooldowns. One entity could have multiple cooldown compoents, so I can't just use a common component for TimeCountdown, but a generic system or interface would work great, and of course an Interface would be much better.

    They have more than half of coommon data and common operations (delay, duration, value [float 0-1], curve type, curve lookup) and specific tweeners like RotationTweener only translate the animated float into rotation or whatever they are animating. So currently I have to have two tweener components. So I can't have more than one on the same entity without a separate entity.

    Yes, thanks, we've discussed it.
     
  8. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    Here is a way one could implement something like this. For Jobs and Burst compatibility it will get way more complex.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Reflection;
    5. using Unity.Collections;
    6. using Unity.Collections.LowLevel.Unsafe;
    7. using Unity.Entities;
    8.  
    9. class EmbeddedCooldownAttribute : Attribute
    10. {
    11.     public string fieldName;
    12. }
    13.  
    14. [EmbeddedCooldown]
    15. public struct Cooldown : IComponentData
    16. {
    17.     public float Time;
    18.     public float Speed;
    19. }
    20.  
    21. [EmbeddedCooldown(fieldName = nameof(Cooldown))]
    22. public struct HasEmbedded1 : IComponentData
    23. {
    24.     public int Data0;
    25.     public Cooldown Cooldown;
    26.     public int Data2;
    27. }
    28.  
    29. [EmbeddedCooldown(fieldName = nameof(Cooldown))]
    30. public struct HasEmbedded2 : IComponentData
    31. {
    32.     public int Data0;
    33.     public int Data1;
    34.     public Cooldown Cooldown;
    35. }
    36.  
    37. public class ExperimentalGenericSystem : ComponentSystem
    38. {
    39.     Helper[] helpers;
    40.     EntityQuery[] queries;
    41.  
    42.     protected override void OnCreate()
    43.     {
    44.         var types = new List<Type>();
    45.         GetComponentsWithAttribute(typeof(EmbeddedCooldownAttribute), types);
    46.  
    47.         helpers = new Helper[types.Count];
    48.         queries = new EntityQuery[helpers.Length];
    49.         for (var i = 0; i < types.Count; i++)
    50.         {
    51.             var wrapper = typeof(HelperTyped<>).MakeGenericType(types[i]);
    52.             var constructor = wrapper.GetConstructor(Array.Empty<Type>());
    53.             helpers[i] = constructor.Invoke(Array.Empty<object>()) as Helper;
    54.             queries[i] = GetEntityQuery(helpers[i].readWrite /*, additional, components */);
    55.         }
    56.  
    57.         base.OnCreate();
    58.     }
    59.  
    60.     protected override void OnUpdate()
    61.     {
    62.         for (var i = 0; i < queries.Length; i++)
    63.         {
    64.             var query = queries[i];
    65.             using (var chunks = query.CreateArchetypeChunkArray(Allocator.TempJob))
    66.             {
    67.                 helpers[i].InitFrame(this);
    68.                 for (var chunkIndex = 0; chunkIndex < chunks.Length; chunkIndex++)
    69.                 {
    70.                     var chunk = chunks[chunkIndex];
    71.                     var cooldown = helpers[i].GetCooldownArray(chunk);
    72.                     for (var j = 0; j < cooldown.Length; j++)
    73.                     {
    74.                         var cd = cooldown[j];
    75.                         cd.Time += cd.Speed;
    76.                         cooldown[j] = cd;
    77.                     }
    78.                 }
    79.             }
    80.         }
    81.     }
    82.  
    83.     abstract class Helper
    84.     {
    85.         public ComponentType readWrite;
    86.  
    87.         public abstract void InitFrame(ComponentSystemBase system);
    88.         public abstract NativeSlice<Cooldown> GetCooldownArray(ArchetypeChunk chunk);
    89.     }
    90.  
    91.     class HelperTyped<T> : Helper
    92.         where T : unmanaged, IComponentData
    93.     {
    94.         ArchetypeChunkComponentType<T> type;
    95.         int offset;
    96.  
    97.         public HelperTyped()
    98.         {
    99.             readWrite = ComponentType.ReadWrite<T>();
    100.             var fieldName = typeof(T).GetCustomAttribute<EmbeddedCooldownAttribute>()?.fieldName;
    101.             if (fieldName == null)
    102.             {
    103.                 offset = 0;
    104.             }
    105.             else
    106.             {
    107.                 var field = typeof(T).GetField(fieldName);
    108.                 offset = UnsafeUtility.GetFieldOffset(field);
    109.             }
    110.         }
    111.  
    112.         public override void InitFrame(ComponentSystemBase system)
    113.         {
    114.             type = system.GetArchetypeChunkComponentType<T>();
    115.         }
    116.  
    117.         public override NativeSlice<Cooldown> GetCooldownArray(ArchetypeChunk chunk)
    118.         {
    119.             var components = new NativeSlice<T>(chunk.GetNativeArray(type));
    120.             return components.SliceWithStride<Cooldown>(offset);
    121.         }
    122.     }
    123.  
    124.     static List<Type> allComponents;
    125.  
    126.     static void GetComponentsWithAttribute(Type attribute, List<Type> result)
    127.     {
    128.         if (allComponents == null)
    129.         {
    130.             allComponents = new List<Type>();
    131.             foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    132.             {
    133.                 if (!TypeManager.IsAssemblyReferencingEntities(assembly))
    134.                     continue;
    135.  
    136.                 IEnumerable<Type> allTypes;
    137.                 try
    138.                 {
    139.                     allTypes = assembly.GetTypes();
    140.                 }
    141.                 catch (ReflectionTypeLoadException e)
    142.                 {
    143.                     allTypes = e.Types.Where(t => t != null);
    144.                 }
    145.                 allComponents.AddRange(allTypes.Where(t => typeof(IComponentData).IsAssignableFrom(t)));
    146.             }
    147.         }
    148.  
    149.         for (var i = 0; i < allComponents.Count; i++)
    150.         {
    151.             var c = allComponents[i];
    152.             if (c.GetCustomAttribute(attribute) != null)
    153.                 result.Add(c);
    154.         }
    155.     }
    156. }
    157.  
    Example usage:
    Code (CSharp):
    1.  
    2.         public void DoesItWork()
    3.         {
    4.             var entities = new List<Entity>();
    5.             manager.CreateEntity(typeof(Translation));
    6.             entities.Add(manager.CreateEntity(typeof(Translation), typeof(Cooldown)));
    7.             entities.Add(manager.CreateEntity(typeof(Translation), typeof(HasEmbedded1)));
    8.             entities.Add(manager.CreateEntity(typeof(Translation), typeof(HasEmbedded2)));
    9.             entities.Add(manager.CreateEntity(typeof(Translation), typeof(HasEmbedded1), typeof(HasEmbedded2)));
    10.             entities.Add(manager.CreateEntity(typeof(Translation), typeof(Cooldown), typeof(HasEmbedded1), typeof(HasEmbedded2)));
    11.  
    12.             for (var i = 0; i < entities.Count; i++)
    13.             {
    14.                 var e = entities[i];
    15.                 if (manager.HasComponent<Cooldown>(e))
    16.                 {
    17.                     manager.SetComponentData(e, new Cooldown
    18.                     {
    19.                         Time = 1.0f,
    20.                         Speed = 0.5f
    21.                     });
    22.                 }
    23.                 if (manager.HasComponent<HasEmbedded1>(e))
    24.                 {
    25.                     manager.SetComponentData(e, new HasEmbedded1
    26.                     {
    27.                         Cooldown = new Cooldown
    28.                         {
    29.                             Time = 1.0f,
    30.                             Speed = 0.5f
    31.                         }
    32.                     });
    33.                 }
    34.                 if (manager.HasComponent<HasEmbedded2>(e))
    35.                 {
    36.                     manager.SetComponentData(e, new HasEmbedded2
    37.                     {
    38.                         Cooldown = new Cooldown
    39.                         {
    40.                             Time = 1.0f,
    41.                             Speed = 0.5f
    42.                         }
    43.                     });
    44.                 }
    45.             }
    46.  
    47.             Update();
    48.  
    49.             for (var i = 0; i < entities.Count; i++)
    50.             {
    51.                 var e = entities[i];
    52.                 if (manager.HasComponent<Cooldown>(e))
    53.                 {
    54.                     var cd = manager.GetComponentData<Cooldown>(e);
    55.                     Debug.Log($"{i} - {cd.Time} - {cd.Speed}");
    56.                 }
    57.                 if (manager.HasComponent<HasEmbedded1>(e))
    58.                 {
    59.                     var cd = manager.GetComponentData<HasEmbedded1>(e);
    60.                     Debug.Log($"{i} - {cd.Cooldown.Time} - {cd.Cooldown.Speed}");
    61.                 }
    62.                 if (manager.HasComponent<HasEmbedded2>(e))
    63.                 {
    64.                     var cd = manager.GetComponentData<HasEmbedded2>(e);
    65.                     Debug.Log($"{i} - {cd.Cooldown.Time} - {cd.Cooldown.Speed}");
    66.                 }
    67.             }
    68.         }
    After executing the system all Cooldowns, including the embedded ones, got executed once.