Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Filtering entities by multiple shared values

Discussion in 'Entity Component System' started by Matsuguma, Jun 26, 2018.

  1. Matsuguma

    Matsuguma

    Joined:
    Aug 7, 2017
    Posts:
    11
    Imagine I am representing hobbies. I have a HobbySharedComponent that has an int value representing a certain hobby (0 is jogging, 1 is swimming, 2 is playing the guitar, etc).
    This makes it really easy to filter for "Everyone who plays the guitar", I can use either SetFilter or CreateForEachFilter on my component group. Great.

    But people can have multiple hobbies! I cannot have two HobbySharedComponents on the same entity, and a SharedComponent can't be a FixedArray.
    To make things worse, I also want to filter people for more than one hobby (who go jogging and swimming, for example). So using tag components seems like a bad option, since I would need to make a component for each hobby.

    The only solution I could think of is hardcoding the number of hobbies, so my SharedComponent has 3 fields: hobbyA, hobbyB and hobbyC. This way I can filter by any of the three fields. But it really looks like a terrible solution.
    Does anybody have a better idea?
     
    Alirux likes this.
  2. recursive

    recursive

    Joined:
    Jul 12, 2012
    Posts:
    669
    You have an Entity that represents a person.

    A person can have multiple hobbies. You then have an entity per hobby, per person.

    One way you could do this.

    Code (CSharp):
    1. public struct Person : IComponentData { } // tag
    2.  
    3. public struct PersonOwner : ISharedComponentData
    4. {
    5.     public Entity Value; // matches the person that has this hobby
    6. }
    7.  
    8. public struct Hobby : ISharedComponentData
    9. {
    10.    public int Id; // matches hobby id, should be stable
    11. }
    You could then create a Person, and create/destroy that person's hobby entities as they start / stop interest in those hobbies.

    Another side benefit is you could track how much skill / how active a Person is and other data relevant to their involvement in a Hobby.

    How do you look up/associate a Hobby with a Person, then? The best way I'd recommend is using a combination of Group filtering for the shared component data, and also a NativeMultiHashMap<Entity,Entity> (getting the owner Entity from the PersonOwner struct) to store the associations while running jobs.
     
    Matsuguma likes this.
  3. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,554
    I think the friction came from the fact that ISharedComponentData is a grouping by any value. But your hobby is a defined and limited like enum. And so ISharedComponenData with value inside is not the right tool for this job.

    It would be suitable if for example you use it to store a number of hobby that a person has. NumberOfHobbyData : ISharedComponentData { public int hobbyCount; } and then you could ask freely how many person has exactly 2 hobbies, 5 hobbies or 10 hobbies. You can also get people with less than 3 hobbies by filtering 0 1 2 hobbies one by one. (it has to be discrete)

    You would have somewhere in your code to map integer 0 to jogging etc. anyways right? So why not use that part of the code to define multiple empty ISharedComponentData for each hobbies instead then you will have the full power of ISharedComponentData. Then you can attach more than one different ISharedComponentData to represent multiple hobbies. If you someday want to get a person with any hobbies, make all of them inherit (implement) interface IHobby. You can then query for people with IHobby instead of any specific ones.

    Edit : and then when you use empty ISharedCompoenentData as a tag it could be replaced by normal IComponentData. Then you replace SetFilter operation with normal injection criteria. It is also more performant if you change data strctures.
     
    Last edited: Jun 27, 2018
  4. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    Why is it that you need a shared component? You could just have one Component per Hobby and filter that way. If you don't need per Entity data on the Component you can even leave them empty and just treat them as tags. In essense this is just a one-liner empty struct for all of them. It's not much more complicated than adding another hobby as an int enum somewhere.
    While I agree it would be nice to have multiple components of the same type with different data on en Entity. This is kind of exactly what components are for: To define abilities, affinities and other traits of an entity.
     
    Last edited: Jun 27, 2018
  5. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    Just adding my cents, I would go for one component per hobby.

    Code (CSharp):
    1. public struct MyHobbyA : IComponentData {}
    2. public struct MyHobbyB : IComponentData {}
    3. public struct MyHobbyC : IComponentData {}
    4.  
    5. [...]
    6.  
    7. public class HobbyBnCSystem : ComponentSystem
    8. {
    9.     struct HobbyData
    10.     {
    11.         public int Length
    12.         public ComponentDataArray<MyHobbyB> HobbyB;
    13.         public ComponentDataArray<MyHobbyC> HobbyC;
    14.     }
    15.     [Inject] private Data m_MyHobbyData;
    16.  
    17.     [...]
    18. }
    The downside is each system must know on compilation time which hobby its gonna use.
     
  6. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,554
    Note for thread owner that for performance Unity ECS is fundamentally based on generic typing so mostly you cannot avoid a concrete type at compilation time. The exception is of course ISharedCompoenentData which turns any value into its own type/group by hashing it.

    If you want to make an interface so that anyone could ask about any hobby instead of declaring [Inject] you might consider this central system approach.

    Code (CSharp):
    1. public interface IHobby : IComponentData { }
    2. public struct MyHobbyA : IHobby { }
    3. public struct MyHobbyB : IHobby { }
    4. public struct MyHobbyC : IHobby { }
    5.  
    6. public class HobbyInjectionSystem : ComponentSystem
    7. {
    8.     protected override void OnUpdate(){}
    9.  
    10.     public EntityArray EntitiesWithHobby<H>() where H : IHobby
    11.     {
    12.         var cg = GetComponentGroup(ComponentType.Create<H>());
    13.         return cg.GetEntityArray();
    14.     }
    15.  
    16.     public ComponentDataArray<T> ComponentDataArrayOnEntitiesWithHobby<H, T>() where H : IHobby where T : struct, IComponentData
    17.     {
    18.         var cg = GetComponentGroup(ComponentType.Create<H>(), ComponentType.Create<T>());
    19.         return cg.GetComponentDataArray<T>();
    20.     }
    21. }
    It is still based on generic typing but might looks better from caller side, depending on your design. Inject this system to other system and use the public method. Also after you call GetComponentGroup for a particular ComponentType[] the first time it permanently add to this system's injection list at OnUpdate. In the end if you call this system a lot it will be equivalent to you declaring all of possible [Inject] permutations you would like to use.
     
    Matsuguma and Afonso-Lage like this.
  7. Matsuguma

    Matsuguma

    Joined:
    Aug 7, 2017
    Posts:
    11
    Wow, loving the responses!

    I don't think creating a component per hobby is a good solution in the long run for two main reasons:
    1. I would need to create a system for each combination I'm interested in (as pointed by @Afonso-Lage)
    2. If I ever add another hobby (which I'll certainly need to), I'll have to review all the systems to add the new combination of hobbies (same for removing hobbies)

    The thing is that I am mainly not interested in how many hobbies the person has, but which hobbies and which combination of hobbies they have. So this approach would only serve as a first pass filter that I would need to refine later somehow. Which is also a valid way to go (multiple filters), but I think there is a better way for my case.

    You are right. But, since I will need iterate through every hobby, it really isn't viable to have specific systems for each combination of hobbies.

    I think I'm leaning towards @recursive's 1-to-many approach here. It adds a little extra work for keeping track and accessing the other linked entities, but it seems to be the most flexible and natural way to go.
     
    recursive likes this.