Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Querying Components on an Entity via Interface?

Discussion in 'Entity Component System' started by TheGabelle, Apr 17, 2019.

  1. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Is it possible to group various components? Maybe with an interface?

    public struct Health : IComponentData, IStat
    public struct Energy : IComponentData, IStat


    How would I get an entity's components that are IStat? Could I do this within a job?
     
  2. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    417
    Perhaps you can have a separate entity that only has IStat components, and reference this entity from the main entity. You could do this recursively for other "interfaces" like:
    Character
    - Stats
    -- Offense
    -- Defense
    - Items
    -- Key
    -- Potions
    -- Weapons
    - Quests

    Use ComponentDataFromEntity<YourComponent>(Entity entity) to fetch components inside of a job.
    Code (CSharp):
    1. var offenseEntity = characterEntity.statsEntity.offenseEntity;
    2. var fireballComponent = getFireballFromEntity[offenseEntity];
    3. var lightningComponent = getLightningFromEntity[offenseEntity];
    4. var iceballComponent = getIceballFromEntity[offenseEntity];
    5. // where FireBall, Lightning, and IceBall would have the IOffense interface following your example
    If you're looking for another way, EntityManager has

    public NativeArray<ComponentType> GetComponentTypes(Entity entity, Allocator allocator = null)

    but it cannot be used inside of a job.
    EntityCommandBuffer can be used in a job but it cannot be used to fetch components - it's used to schedule modification of components to a later time.
     
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,753
    While you can't query the interface you can still use them to do some cool things and save yourself writing a bunch of duplicate code.

    For example I used reusable jobs for an AI solution.

    (Please note I'm pulling this from an older repo as an example, it's using a previous version of entities so still uses IJobProcessComponentDataWithEntity)

    The interface looked like this

    Code (CSharp):
    1.     public interface ITargetOrder : IComponentData
    2.     {
    3.         /// <summary>
    4.         /// Gets or sets the target of the order.
    5.         /// </summary>
    6.         Entity Target { get; set; }
    7.  
    8.         /// <summary>
    9.         /// Gets or sets if the order is processing? Usually happens after being in range of target.
    10.         /// </summary>
    11.         bool IsProcessing { get; set; }
    12.  
    13.         /// <summary>
    14.         /// Gets or sets the last time the order started processing.
    15.         /// </summary>
    16.         float StartTime { get; set; }
    17.     }
    Implemented like this for example

    Code (CSharp):
    1. /// <summary>
    2.     /// The deposit order which tells an entity to empty their inventory into a storage.
    3.     /// </summary>
    4.     public struct DepositOrder : ITargetOrder
    5.     {
    6.         /// <summary>
    7.         /// Gets the target storage.
    8.         /// </summary>
    9.         public Entity Storage;
    10.  
    11.         /// <summary>
    12.         /// Gets if we are currently depositing.
    13.         /// </summary>
    14.         public bool IsDepositing;
    15.  
    16.         /// <summary>
    17.         /// Gets the start time of the depositing routine.
    18.         /// </summary>
    19.         public float StartTime;
    20.  
    21.         /// <inheritdoc />
    22.         Entity ITargetOrder.Target
    23.         {
    24.             get => this.Storage;
    25.             set => this.Storage = value;
    26.         }
    27.  
    28.         /// <inheritdoc />
    29.         bool ITargetOrder.IsProcessing
    30.         {
    31.             get => this.IsDepositing;
    32.             set => this.IsDepositing = value;
    33.         }
    34.  
    35.         /// <inheritdoc />
    36.         float ITargetOrder.StartTime
    37.         {
    38.             get => this.StartTime;
    39.             set => this.StartTime = value;
    40.         }
    41.     }
    And the job looks like this

    Code (CSharp):
    1. [BurstCompile]
    2.     public struct MoveToTargetJob<TO> : IJobProcessComponentDataWithEntity<TO, Translation>
    3.         where TO : struct, ITargetOrder
    4.     {
    5.  
    6.         public void Execute(Entity entity, int index, ref TO order, [ReadOnly] ref Translation position)
    7.         {
    8.             var target = order.Target;
    9.  
    10.             if (target == Entity.Null)
    11.             {
    12.                 return;
    13.             }
    14.  
    15.             // ... etc
    So all my orders that needed to move to a target implemented the ITargetOrder interface then I could just re-use this job in multiple different systems that handled different logic routines.
     
  4. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Is it possible to loop over an entity's components, or get the full archetype of the entity and loop over the respective component types? If so I might be able to pull out the ones that implement iStat.
     
  5. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    I hacked together a solution, but I'm sure there's a better way.
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Reflection;
    5. using Unity.Entities;
    6. using UnityEngine;
    7.  
    8.  
    9. public interface IStat
    10. {
    11.  
    12. }
    13.  
    14. [Serializable]
    15. public struct Hunger : IComponentData, IStat
    16. {
    17.     public float Value;
    18. }
    19.  
    20.  
    21. [Serializable]
    22. public struct Health : IComponentData, IStat
    23. {
    24.     public float Value;
    25. }
    26.  
    27.  
    28. public class TestSystem : ComponentSystem
    29. {
    30.  
    31.     EntityQuery IStatComponents;
    32.  
    33.     protected override void OnCreate()
    34.     {
    35.         StatComponentsQueryViaInterface();
    36.     }
    37.  
    38.  
    39.     protected override void OnUpdate()
    40.     {
    41.  
    42.     }
    43.  
    44.  
    45.     void StatComponentsQueryViaInterface()
    46.     {
    47.         Type istat = typeof(IStat);
    48.         List<Type> statTypes = new List<Type>();
    49.  
    50.         foreach (Type t in Assembly.GetExecutingAssembly().GetTypes())
    51.         {          
    52.             foreach(Type i in t.GetInterfaces())
    53.             {
    54.                 if (i == istat)
    55.                 {
    56.                     statTypes.Add(t);
    57.                 }
    58.             }      
    59.         }
    60.  
    61.         TypeListToEntityQuery(statTypes);
    62.     }
    63.  
    64.  
    65.     void TypeListToEntityQuery(List<Type> types)
    66.     {
    67.         List<ComponentType> cts = new List<ComponentType>();
    68.  
    69.         foreach(Type t in types)
    70.         {
    71.             cts.Add( new ComponentType(t, ComponentType.AccessMode.ReadWrite) );
    72.         }
    73.  
    74.         IStatComponents = GetEntityQuery(new EntityQueryDesc
    75.         {
    76.             Any = cts.ToArray()
    77.         });
    78.     }
    79. }

    edit: cleaned up code a bit.
     
    Last edited: Apr 18, 2019
  6. NoDumbQuestion

    NoDumbQuestion

    Joined:
    Nov 10, 2017
    Posts:
    186
    I am sure interface with struct component data should be avoid if possible since the idea of grouping unknown data (same as polymorphism in OOP but in Data type) to query system is not very nice. Because the system still have to know what kind of Data that is to process while you still have to setup Data structure manually not dynamically in Editor.

    I would write [HeaderAttribute] instead of interface to group stuff with reflection during run time.
     
  7. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Fair point. Here's the attribute based implementation.
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Reflection;
    5. using Unity.Entities;
    6. using UnityEngine;
    7.  
    8.  
    9. public class Stat : Attribute
    10. {
    11.  
    12. }
    13.  
    14.  
    15. [Serializable] [Stat]
    16. public struct Hunger : IComponentData
    17. {
    18.     public float Value;
    19. }
    20.  
    21.  
    22. [Serializable] [Stat]
    23. public struct Health : IComponentData
    24. {
    25.     public float Value;
    26. }
    27.  
    28.  
    29. public class TestSystem : ComponentSystem
    30. {
    31.  
    32.     EntityQuery IStatComponents;
    33.  
    34.     protected override void OnCreate()
    35.     {
    36.         StatComponentsQueryViaAttribute();
    37.     }
    38.  
    39.     protected override void OnUpdate()
    40.     {
    41.  
    42.     }
    43.  
    44.  
    45.     void StatComponentsQueryViaAttribute()
    46.     {
    47.         Type stat = typeof(Stat);
    48.         List<Type> statTypes = new List<Type>();
    49.  
    50.         foreach(Type t in Assembly.GetExecutingAssembly().GetTypes()) {
    51.             if (t.GetCustomAttributes(stat, true).Length > 0) {
    52.                 statTypes.Add(t);
    53.             }
    54.         }
    55.  
    56.         TypeListToEntityQuery(statTypes);
    57.     }
    58.  
    59.  
    60.     void TypeListToEntityQuery(List<Type> types)
    61.     {
    62.         List<ComponentType> cts = new List<ComponentType>();
    63.  
    64.         foreach(Type t in types)
    65.         {
    66.             cts.Add( new ComponentType(t, ComponentType.AccessMode.ReadWrite) );
    67.         }
    68.  
    69.         IStatComponents = GetEntityQuery(new EntityQueryDesc
    70.         {
    71.             Any = cts.ToArray()
    72.         });
    73.     }
    74. }
     
    Last edited: Apr 18, 2019
  8. Rennan24

    Rennan24

    Joined:
    Jul 13, 2014
    Posts:
    38
    Did you guys miss tertle's solution?
    This is 1,000,000 times better than the reflection you guys are doing, please anyone else that sees this thread, please please please do not use reflection during the runtime of your game. I would like the games that you guys create in the future to not be laggy games because of reflection, if you need an example using ComponentSystem instead of JobComponentSystem here it is:
    Code (CSharp):
    1. public interface IStat : IComponentData
    2. {
    3.     float Value { get; }
    4. }
    5.  
    6. public struct StrengthData : IStat
    7. {
    8.     public float Value;
    9.     float IStat.Value => Value;
    10. }
    11.  
    12. public struct IntelligenceData : IStat
    13. {
    14.     public float Value;
    15.     float IStat.Value => Value;
    16. }
    17.  
    18. public struct AgilityData : IStat
    19. {
    20.     public float Value;
    21.     float IStat.Value => Value;
    22. }
    23.  
    24. public class StatSystem : ComponentSystem
    25. {
    26.     protected override void OnCreate()
    27.     {
    28.         // If you need to do a query here's how it would work
    29.         GetEntityQuery(new EntityQueryDesc {
    30.             Any = new [] {
    31.                 ComponentType.ReadWrite<StrengthData>(),
    32.                 ComponentType.ReadWrite<AgilityData>(),
    33.                 ComponentType.ReadWrite<IntelligenceData>(),
    34.             }
    35.         });
    36.     }
    37.  
    38.     // If you want to use Entities.ForEach I would recommend doing this way to reuse code!
    39.     protected override void OnUpdate()
    40.     {
    41.         ForEachStat<StrengthData>();
    42.         ForEachStat<AgilityData>();
    43.         ForEachStat<IntelligenceData>();
    44.     }
    45.  
    46.     protected void ForEachStat<TStat>() where TStat : struct, IStat
    47.     {
    48.         Entities.ForEach((ref TStat stat) => {
    49.             Debug.Log($"Stat {typeof(TStat)}: {stat.Value}");
    50.         });
    51.     }
    52. }
    53. }
    But as you can see its much cleaner than the hack that was being presented up above and is much more performant as well. Also thank you tertle for showing me this, I was also wondering how this would be done which is how I stumbled upon this thread :D
     
    recursive likes this.
  9. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Tertle's solution is excellent if you know which component types implement IStat. I'm not sure how the solution works for an unknown number of IStat components (mods). I haven't seem much on the forums about how modding will work with ecs, so I've been writing little projects that answer "Can I do this?" just to see where some limitations are.
     
  10. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    Code (CSharp):
    1. var chunk = EntityManager.GetChunk(entity);
    2. var compTypes = chunk.Archetype.GetComponentTypes(Allocator.TempJob);
     
    TheGabelle likes this.
  11. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    Also a few words on Reflection. Right now the ECS api is so young, you cannot do anything custom without reflection. You need to do custom/multiple worlds? You need reflection. You want to do Netcode that sync components in a generic way? You need reflection.

    I just finished my own Netcode. When it comes to reflection, key is to do as much as possible as one-off operations such as in OnCreate(). For performance-critical code in OnUpdate(), you need to convert all Method.Invoke() or equivalents to delegates. You can expect 20x to 40x performance gain this way (very close to calling the method directly). When dealing with EntityManager.AddComponent / GetComponent(), you also need to take care of boxing.

    I don't see anything wrong in the Reflection code in this thread. They are in OnCreate, so needn't be performance-critical. And they can potentially serve very different purposes than just utilizing generics (tertle's code).
     
    Last edited: Apr 19, 2019
  12. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
  13. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    As the API stands right now, if you are doing multiple worlds in any non-trivial manner, you will need to roll your own World Initialization code instead of using DefaultWorldInitialization as it. This is mostly due to 2 reasons:

    1) Both GameObjectEntity and the new Conversion API depends on World.Active. And ICustomBootstrap doesn't have a world passed in. So if you have multiple worlds and want to use ICustomBootstrap, you are forced to check World.Active. And if you're doing multiple worlds, you'll soon realize that having code dependent on checking a global static makes things error-prone and harder to test.

    2) ScriptBehaviourUpdateOrder.UpdatePlayerLoop() doesn't support multiple worlds now. Good news is they are getting rid of it completely in the future. But as it stands right now, a lot of things still depend on it (eg. Entity Debugger).

    More context:
    https://forum.unity.com/threads/icustombootstrap-feedback.639052/
    https://forum.unity.com/threads/scr...ger-support-multiple-worlds-in-0-0-26.640279/
     
  14. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    I know API, how it work and evolve from beginning. I use multiple wolds expensively, for different parts of logic, for asynch entities creation by ExclusiveEntityTransaction, for dividing presentation and simulation, etc.
    You shouldn’t touch
    ScriptBehaviourUpdateOrder manually as before, it’s wrong way in current API state. And this no need multiple worlds argument, coz how player loop works now, and how we build player loop now by filling update order of groups. Read manual which I linked, especially multiple world part, where described how properly work with multiple worlds.
     
  15. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    ScriptBehaviourUpdateOrder.UpdatePlayerLoop() has the 3 top-level SystemGroups hardcoded in. So if you want to do anything custom (and have things show up in Entity Debugger), you need to resort to reflection or changing the source code.

    I'm aware of the current "recommended" way of creating multiple worlds. But it's limited. I don't want to strictly adhere to the 3 set-in-stone SystemGroups for all my worlds.

    My original reflection comment was really more about customized worlds than just multiple worlds.
     
  16. Piefayth

    Piefayth

    Joined:
    Feb 7, 2017
    Posts:
    61
    Hah, I was about to chime in on the above until I saw the Entity Debugger comment. I've definitely just been living with systems not showing up there.

    That said, I have had good success using the conversion APIs directly. For example, loading the same scene into two separate Worlds:

    Code (CSharp):
    1.  
    2. if (Settings.client) { // true
    3.     DefaultWorldInitialization.Initialize(WorldKey.CLIENT_WORLD.ToString(), false);
    4.     Worlds.clientWorld = World.Active;
    5.  
    6.     GameObjectConversionUtility.ConvertScene(scene, default, Worlds.clientWorld);
    7. }
    8.  
    9. if (Settings.server) { // true
    10.     DefaultWorldInitialization.Initialize(WorldKey.SERVER_WORLD.ToString(), false);
    11.     Worlds.serverWorld = World.Active;
    12.  
    13.     GameObjectConversionUtility.ConvertScene(scene, default, Worlds.serverWorld);
    14. }
    15.  
     
  17. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    Yep I agree with you in this part, if you need some “custom” toppest level groups. But what your case for root groups, if you always can put some other your own groups inside and onther inside them etc. :p
     
  18. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    Yeah, in my case, I needed to use custom UpdateLoops for Netcode and Physics. The current way the root SystemGroups are set up doesn't allow that. But I'm hopeful they'll make it better in the next couple versions.

    @Piefayth I think I did something similar, sort of bootstrapping client and server separately so that the conversion utility can be aware of which world to act on in Editor mode.