Search Unity

ECS way of interaction

Discussion in 'Entity Component System' started by manpower13, Jan 31, 2020.

  1. manpower13

    manpower13

    Joined:
    Dec 22, 2013
    Posts:
    140
    Hello everyone!

    After many years of using Unity in the 'regular' way, I wanted to give dots a try.

    I understand the basics of ecs and jobs. We have a data struct, with some data. We have a system that finds all the specific (has specific types of data) entities it needs and computes results for the entities.
    The main example being a lot of objects all moving at the same time, but with different speed or direction.

    Now I am stuck with a more specific problem. I am trying to build an interaction system.
    Concept: A player, with data 'Interactor', should see a visual in the screen (a sprite/quad coming up showing that something is interactable) whenever it is close to an 'Interaction'. Close means, within range, where the range is specified as a value of 'Interaction'.

    That is the first step. I know how to loop over each interactor and then have a loop over all interactables, similar to what is done here:
    . This way I could find whether any interactor is close enough.

    However, I have two questions.
    1. What would be the right approach to add a visual effect? Like a sprite coming up? Would I instantiate a new entity, with a renderer, and have a component that makes it follow its 'parent' (e.g. an npc or chest, if it moves the icon should still be above the object). Or should I draw it every time this is computed using DrawInstanced?
    2. What should be the next step to allow actual interaction? If the user presses a button, a chest should open if it's close enough, or a NPC should start talking. In the 'old' way of doing this, I would create a method that each object can implement. The chest would open in the method, the npc would start talking in that method. What would be the correct approach for dots?

    Is there any example that might look like my problem? Thank you very much!
     
  2. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    This talk sounds similar to your question. The pattern is to turn interaction into relationship. Interactor create data, interacted object updates and see data, then it is being interacted. If an interaction is on an instant or 1 frame (e.g. method call -like) then you remove/destroy the data.

     
    bb8_1, xVergilx and diesoftgames like this.
  3. diesoftgames

    diesoftgames

    Joined:
    Nov 27, 2018
    Posts:
    122
    Thank you for this! I somehow missed this video and I didn't know about ComponentDataFromEntity so I've been pulling data from a query and injecting it into Jobs with native containers to form relationships, which... yeah I figured a better way would come along and it looks like exactly what I wanted. :)
     
  4. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Hey, I like your tutorials, which is very fun and helpful!
    Hope my English doesn't make you crazy:rolleyes:.
    To handle Polymorphism in ECS way, I think using different systems to handle one component plus another component is a good way.
    In your case, NPC & chest are all interactable, so they both have the Interactable component. You can then make a system handles all the entities which have INTERACTABLE & ACTIVATED & CHEST component, while another system handles all the entities which have INTERACTABLE & ACTIVATED & NPC component.
    The ACTIVATED component is added and removed after the overlap detection, I guess you know that clearly, mention it just in case.

    BTW, this question let me think another interesting question, how to handle the NPC that has a second dialogue?
    I think to add it as another component (DIALOGUE2) might be better than leaving another slot in component1. In this way, I don't need to check null or waste much memory for those NPC which only have one sentence.

    I don't really know much about another question, but your solution sounds ok for me.
     
  5. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    How about using IBufferElements for dialogue?
     
  6. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Yes that's better. Further more, if it's not gonne change, use blob asset might even better.
     
    WAYNGames likes this.
  7. manpower13

    manpower13

    Joined:
    Dec 22, 2013
    Posts:
    140
    Great help everyone!
    The video (thanks @5argon, I missed it!) really helped me out and made me jump to the same conclusion as @eterlan :)

    I'm starting to really like this whole pattern of coding!
     
    eterlan likes this.
  8. manpower13

    manpower13

    Joined:
    Dec 22, 2013
    Posts:
    140
    So, the interaction system itself works. It detects when a player can interact, and I have an idea of how to respond to that. Now I am wondering, how would I go with an icon showing up whenever it's interactable and the player is close by?

    I would start with creating a system that iterates over all entities that have the TagInteraction (set by the system that detects distance to player whenever an object is close enough) and have a InfoPopup component (contains information about what should be shown to the user, e.g. what icon/texture).

    Now we get to the actual rendering of the popup icon. I could create a new entity, but that entity should 'know' who the 'owner' (the chest), is. Then another system could make sure that these 'icon' entities are following their 'owners'. This entity could simply have a renderer attached.

    Or, I use DrawMesh and draw an icon for each entity.

    To me, the first one sounds like a little bit more work, but more customizable in the end. Both will work in the end.
    I'm wondering what most of you would do! Still trying to learn the dots way ^^
    Thanks!
     
  9. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    You may want to use the same thing as entity conversion + Unity's Transform systems for hierarchy. That is, add LinkedEntityGroup on the chest then add to the buffer any child you want. Then LocalToWorld of that child would be updated based on its Translation/Rotation/Scale and also its parent. (If Translation is 0 then there are systems that make sure LTW is exactly at parent instead of at 0.) Then use LTW to render or just use Hybrid Renderer. (RenderMesh + LTW)
     
  10. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Wait, I think LinkedEntityGroup only deal with it instantiate / destroy together. Rather its a component named Parent that cause the transform based on parent. The parent need not to know about any of its child.
     
  11. manpower13

    manpower13

    Joined:
    Dec 22, 2013
    Posts:
    140
    @5argon Thank you for your reaction.

    LinkedEntityGroup only seems to deal with enabled/disabled, instantiate/destroy.

    I tried the following, but burst does not like a component that refers to a component with a RenderMesh. If I remove the 'AddSharedComponent(index, popupEntity, new RenderMesh...', it compiles in burst. Seems like an equality method is called somewhere.

    Code (CSharp):
    1. [RequireComponentTag(typeof(Tag_Interactable), typeof(InfoPopup))]
    2.     [BurstCompile]
    3.     private struct CreatePopupJob : IJobForEachWithEntity<Translation>
    4.     {
    5.         public EntityCommandBuffer.Concurrent entityCommandBuffer;
    6.        
    7.         public void Execute(Entity entity, int index, ref Translation translation)
    8.         {
    9.             Entity popupEntity = entityCommandBuffer.CreateEntity(index);
    10.             entityCommandBuffer.AddSharedComponent(index, popupEntity, new RenderMesh{mesh = MeshHelper.QuadMesh});
    11.             entityCommandBuffer.AddComponent(index, popupEntity, new RelationWithEntity{Entity = entity});
    12.         }
    13.     }
    RelationWithEntity looks like this:
    Code (CSharp):
    1. public struct RelationWithEntity : IComponentData
    2. {
    3.     public Entity Entity;
    4. }
     
  12. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    I think you can only used SharedComponents inside Burst/Job code when they only contain blittable types (even then I'm not sure). Eiether way because RenderMesh contains a Mesh which is a reference type it won't let you use it.

    Since presumably all the entities would be sharing the same mesh you could do it on the main thread (using a non-concurrent EntityCommandBuffer to avoid blocking). You can pass in an entityQuery to commandBuffer.AddComponent/AddSharedComponent and it will add it to all the entities of the query. This is also significantly faster than doing it one at a time since it avoids the repeated chunk changes.
     
  13. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Shared component commands will never work with Burst even with ECB and even if the type contains purely blittable types since it would use managed array to store commands. (And this RenderMesh contains a reference type Material and Mesh)
     
  14. manpower13

    manpower13

    Joined:
    Dec 22, 2013
    Posts:
    140
    Hmm, alright thank you! That makes sense.
    I have the creation of the 'child' entities working! That's done on a ComponentSystem now, on the main thread. They all have a component that refers to their 'parent', an Entity.
    Now I need a system to update the 'children's translations' to their 'parent's translations'. At first I thought this was possible in a JobComponentSystem, but since I am not completely sure whether a parent might be a child of another Entity again, I don't think that's possible.

    However, I am not completely sure how I get the Translation component if I only have a reference to the parent Entity?
    Code (CSharp):
    1. protected override void OnUpdate()
    2.     {
    3.         Entities.WithAll<Tag_FollowRelation>().ForEach((ref Translation translation, in RelationWithEntity relationWithEntity) =>
    4.         {
    5.             Translation.Value = relationWithEntity.Entity[.Translation.Value ?]
    6.         });
    7.     }
    I know how to do it in a JobComponentSystem:
    Code (CSharp):
    1. [RequireComponentTag(typeof(Tag_FollowRelation))]
    2.     [BurstCompile]
    3.     private struct UpdatePositionJob : IJobForEachWithEntity<RelationWithEntity, Translation>
    4.     {
    5.         [ReadOnly]
    6.         public ComponentDataFromEntity<Translation> translationData;
    7.        
    8.         public void Execute(Entity entity, int index, [ReadOnly] ref RelationWithEntity relationWithEntity, ref Translation translation)
    9.         {
    10.             if (!translationData.Exists(relationWithEntity.Entity))
    11.             {
    12.                 return;
    13.             }
    14.            
    15.             translation.Value = translationData[relationWithEntity.Entity].Value;
    16.         }
    17.     }
    18.  
    19.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    20.     {
    21.         UpdatePositionJob updatePositionJob = new UpdatePositionJob
    22.         {
    23.             translationData = GetComponentDataFromEntity<Translation>(true)
    24.         };
    25.  
    26.         return updatePositionJob.Schedule(this, inputDeps);
    27.     }
    but that is not allowed because of the nested possibilities.

    What would be a solution here? Thank you very much! While the system is completely different from what I am familiar with, I like it.

    Sincerely,
    Floris Weers
     
  15. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Just get the component you need with EntityManager.GetComponentData and use it, sounds simple, what's the problem?
     
  16. manpower13

    manpower13

    Joined:
    Dec 22, 2013
    Posts:
    140
    Oh, thanks! I actually missed there was a simple method like that. I only knew the ComponentDataFromEntity, my bad.

    It's working!