Search Unity

[Suggestion] Delay IConvertGameObjectToEntity.Convert to after Monobehaviour.Start

Discussion in 'Entity Component System' started by Abbrew, May 25, 2019.

  1. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    417
    Right now the lifecycle of a Monobehaviour that implements IConvertGameObjectToEntity with Convert And Destroy is Awake -> Convert -> (All Monobehaviours are initialized with Awake) -> OnDestroy -> (Monobehaviour no longer exists) -> Start. Say we want to include extra initialization game logic that depends on other GOs. If we place it inside Convert, then there is a change that the other Monobehaviours that the game logic depends on have not been initialized yet. If we place it inside start, then the game logic is never executed since it is executed after OnDestroy. We can't use Convert And Inject GameObject because that doesn't convert child GOs. The only place to make this work is in OnDestroy, which while does execute the initialization game logic, seems a little unintuitive.

    Use Cases:
    Having an entity available to both ECS and Monobehaviour, especially if using an old API such as Monobehaviour-based Behaviour Tree APIs
    Code (CSharp):
    1. public class MessagePulser : MonoBehaviour
    2. {
    3.     private static MessagePulser instance;
    4.  
    5.     private Entity recipient;
    6.     private bool visualized;
    7.  
    8.     public static void SetRecipient(Entity entity)
    9.     {
    10.         instance.recipient = entity;
    11.     }
    12.  
    13.     private void Update()
    14.     {
    15.         if (Input.GetMouseButtonDown(0) )
    16.         {
    17.             //if (!visualized)
    18.             //{
    19.             //    visualized = true;
    20.             //    MovementScheduler.ScheduleFlankRight(recipient);
    21.             //}
    22.             //else
    23.             //{
    24.             //    visualized = false;
    25.             //    DestinationVisualizer.Clear();
    26.             //}
    27.             MovementScheduler.ScheduleFlankRight(recipient);
    28.  
    29.         }
    30.     }
    31.  
    32.  
    33.     private void Awake()
    34.     {
    35.         instance = this;
    36.     }
    37. }
    38.  
    Code (CSharp):
    1. [RequiresEntityConversion]
    2. [RequireComponent(typeof(FlankComponent))]
    3. public class MessagePulserUserTagComponent : MonoBehaviour, IConvertGameObjectToEntity
    4. {
    5.     private Entity entity;
    6.     public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    7.     {
    8.         this.entity = entity;
    9.     }
    10.  
    11.     private void Start()
    12.     {
    13.  
    14.     }
    15.  
    16.     private void OnDestroy()
    17.     {
    18.         MessagePulser.SetRecipient(entity);
    19.     }
    20. }
    21. [Serializable]
    22. public struct MessagePulserUserTag : IComponentData
    23. {
    24.  
    25. }
     
  2. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Personally I find it quite useful that entities are generated before start because then I can reference them in start for (debug) UI etc.
     
  3. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    417
    Adding a new method to the IConvertGameObjectToEntity would make everyone happy. A method like ConvertInitialize that is called after Start, and OnDestroy is delayed till after ConvertInitialize
     
  4. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Why would you couple your logic like this by doing it on IConvertGameObjectToEntity

    Why not have a system for pulsing new entities.
     
  5. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    417
    I want to be able to access the entity from a MonoBehaviour script. There's a behavior tree implementation that requires MonoBehaviour, and I need some scripts to add, remove, and check for components entities. I've whipped up a working solution that's pretty decoupled from other code.

    The below script can be attached to any GameObject
    Code (CSharp):
    1. [RequiresEntityConversion]
    2. public class EntitySharer : MonoBehaviour, IConvertGameObjectToEntity
    3. {
    4.     [SerializeField]
    5.     private EntityStore store;
    6.  
    7.     public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    8.     {
    9.         store.entity = entity;
    10.     }
    11. }
    12.  
    This script can be attached to any separate GameObject (GO with EntitySharer will be destroyed). Drag this GO into the store field of EntitySharer
    Code (CSharp):
    1. public class EntityStore : MonoBehaviour
    2. {
    3.     public Entity entity
    4.     {
    5.         get;
    6.         set;
    7.     }
    8. }
    9.  
    Here's a script that needs an entity to run. Drag the EntityStore GO into the entityStore field. The script provides methods for a behavior tree implementation, so Monobehaviours are required
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using Unity.Entities;
    5. using Panda;
    6.  
    7. public class FlankAILinker : MonoBehaviour
    8. {
    9.     [SerializeField]
    10.     private EntityStore entityStore;
    11.  
    12.     private EntityManager em;
    13.     private Entity user;
    14.  
    15.     void Start()
    16.     {
    17.         em = World.Active.EntityManager;
    18.         user = entityStore.entity;
    19.     }
    20.  
    21.     [Task]
    22.     private bool Begin()
    23.     {
    24.         em.AddComponentData(user, new FlankMessage());
    25.         return true;
    26.     }
    27.  
    28.     [Task]
    29.     private bool IsCompleted()
    30.     {
    31.         return em.HasComponent(user, typeof(TraverseSuccessMessage));
    32.     }
    33. }
    34.  
    35.  
     
  6. JohanF_TF

    JohanF_TF

    Joined:
    May 20, 2015
    Posts:
    75
    One way to handle it is to trigger the conversion manually.

    This is the Awake method of ConvertToEntity that is run when attaching the component to a GameObject and inserting the object into the conversion flow

    Code (CSharp):
    1.         void Awake()
    2.         {
    3.             if (World.Active != null)
    4.             {
    5.                 // Root ConvertToEntity is responsible for converting the whole hierarchy
    6.                 if (transform.parent != null && transform.parent.GetComponentInParent<ConvertToEntity>() != null)
    7.                     return;
    8.                
    9.                 if (ConversionMode == Mode.ConvertAndDestroy)
    10.                     ConvertHierarchy(gameObject);
    11.                 else
    12.                     ConvertAndInjectOriginal(gameObject);
    13.             }
    14.             else
    15.             {
    16.                 UnityEngine.Debug.LogWarning("ConvertEntity failed because there was no Active World", this);
    17.             }
    18.         }
    That's all it does.
    The ConvertHierarchy and ConvertAndInjectOriginal are public static methods and can be called wherever you like. You probably want to trigger it in bulk, in the same way they're now triggered in Awake. In your example you could just add a line in your Start method.

    Code (CSharp):
    1. void Start()
    2. {
    3.    // Your stuff
    4.    // ...
    5.    ConvertToEntity.ConvertHierarchy(this.gameObject);
    6. }
     
    eterlan and Abbrew like this.