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

Discussion Syncing ECS gameplay with Unity UI

Discussion in 'Entity Component System' started by Fiffe, Jan 30, 2023.

  1. Fiffe

    Fiffe

    Joined:
    Dec 10, 2014
    Posts:
    19
    Hello, I have been working with Unity's ECS for some time now and I am wondering what are your practices of syncing the ECS based gameplay with Unity's UI.

    For UI that is based on specific events happening in the game this approach has worked for me so far:

    UI MonoBehaviour:
    Code (CSharp):
    1. [SerializeField]
    2. private Canvas _canvas;
    3.  
    4. private EntityManager _entityManager;
    5.  
    6. private void Start()
    7. {
    8.     _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    9.  
    10.     TryInitialize();
    11. }
    12.  
    13. private void OnDisable()
    14. {
    15.     SomeSystemData someSystemData = GetSomeSystemData();
    16.  
    17.     if (someSystemData != null)
    18.     {
    19.         someSystemData.PlayerDied -= OnPlayerDied;
    20.     }
    21.  
    22.     _isInitialized = false;
    23. }
    24.  
    25. private void Update()
    26. {
    27.     if (_isInitialized)
    28.     {
    29.         return;
    30.     }
    31.  
    32.     TryInitialize();
    33. }
    34.  
    35. private void TryInitialize()
    36. {
    37.     var someSystemData = GetSystemData();
    38.  
    39.     if (someSystemData == null)
    40.     {
    41.         return;
    42.     }
    43.  
    44.     someSystemData.PlayerDied += OnPlayerDied;
    45.     _isInitialized = true;
    46. }
    47.  
    48. private SomeSystemData GetSystemData()
    49. {
    50.     SystemHandle systemHandle = World.DefaultGameObjectInjectionWorld.GetExistingSystemManaged(typeof(SomeSystem)).SystemHandle;
    51.  
    52.     return _entityManager.GetComponentData<SomeSystemData>(systemHandle);
    53. }
    54.  
    55. private void OnPlayerDied()
    56. {
    57.     ToggleUI(true);
    58. }
    59.  
    60. private void ToggleView(bool isEnabled)
    61. {
    62.     _canvas.enabled = isEnabled;
    63. }
    ---

    System's managed component:
    Code (CSharp):
    1. public class SomeSystemData : IComponentData
    2. {
    3.     public Action PlayerDied;
    4. }
    ---

    And here's the managed system itself:
    Code (CSharp):
    1. public partial class SomeSystem : SystemBase
    2. {
    3.     private EntityQuery _playerQuery;
    4.  
    5.     protected override void OnCreate()
    6.     {
    7.         EntityManager.AddComponentObject(SystemHandle, new SomeSystemData());
    8.      
    9.         _playerQuery = new EntityQueryBuilder(Allocator.Temp).WithAll<PlayerTag>().WithNone<AliveTag>().Build(this);
    10.      
    11.         RequireForUpdate(_playerQuery);
    12.     }
    13.  
    14.     // Player died
    15.     protected override void OnStartRunning()
    16.     {
    17.         EntityManager.GetComponentObject<SomeSystemData>(SystemHandle).PlayerDied?.Invoke();
    18.     }
    19.  
    20.     protected override void OnUpdate()
    21.     {
    22.  
    23.     }
    24. }
    Right now this UI MonoBehaviour is coupled to SomeSystem but the idea here would be to try and get some interface-like component that ECS systems would change/interact with so there's no direct link between UI and gameplay systems. The biggest downside right now is the Update method which tries to initialize the MonoBehaviour if it failed initializing at Start method. This probably could be prevented by using custom bootstraps/world creation to ensure the correct order of creation and initialization.

    I would be interested to see what are your approaches to creating UI in the new ECS framework!
     
    Last edited: Jan 30, 2023
    bb8_1 and Tony_Max like this.
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Author UI as data (e.g. via EntityManager.AddComponentObject), and query on it via managed (SystemBase) system.

    This is by far most reliable way to communicate for the UI.
    Basically its one-way ECS -> MonoBehaviour;

    Pros:
    + Required data can be directly queried / accessed from entities in a system, just as usual;
    + UI updates only when present (authored);
    + Can be reactive if subscriptions are performed in OnCreate / OnDestroy (cleanup)
    (e.g. Enabled can be used to toggle logic)

    Cons:
    - Managed, obviously. But at the same time MonoBehaviours already are, so no big deal.

    E.g.
    Code (CSharp):
    1. public class SomeMonoBehaviourUI : MonoBehaviour {
    2.     // Author SomeMonoBehaviourUI as managed component to the required World first
    3.     ...
    4.  
    5.     public void DoSomething(UIData data);
    6. }
    7.  
    8. // Then in a system
    9.    public partial class SomeUISystem : SystemBase {
    10.       protected override void OnUpdate() {
    11.         ...
    12.         // E.g. if (!_playerDeadQuery.IsEmpty)
    13.         // Or even better -> use filter in OnCreate alike RequireForUpdate
    14.         Entities.ForEach((in SomeMonoBehaviourUI ui) => {
    15.             ...
    16.             ui.DoSomething(data);
    17.  
    18.             // Or even better, perform logic on the UI component from the system directly,
    19.             // without passing data, e.g. show / hide, start animations etc.
    20.          }).WithoutBurst().Run();
    21.       }
    22.    }
    If you're troubled about decoupling - you don't actually need it in this case.
    All data is already decoupled from UI - its stored on the ECS side.

    To add more features, you'd just write different systems with different logic.
     
    Last edited: Jan 30, 2023
    bb8_1 and Fiffe like this.
  3. Fiffe

    Fiffe

    Joined:
    Dec 10, 2014
    Posts:
    19
    @xVergilx this seems like a very natural approach to creating hybrid UI for ECS - it reduces most of the boilerplate code and coupling issues, thank you.
    One important thing to remember is to clean up the entity on reloading/quitting the scene so the systems won't run into NullReferenceExceptions.

    UI MonoBehaviour:
    Code (CSharp):
    1.  
    2. public class SomeUIMonoBehaviour : MonoBehaviour
    3. {
    4.    [SerializeField]
    5.    private Canvas _canvas;
    6.  
    7.    private EntityManager _entityManager;
    8.    private Entity _associatedEntity;
    9.  
    10.    public void ToggleView(bool isEnabled)
    11.    {
    12.        _canvas.enabled = isEnabled;
    13.    }
    14.  
    15.    private void Start()
    16.    {
    17.        _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    18.  
    19.        _associatedEntity = _entityManager.CreateEntity();
    20.  
    21.        _entityManager.AddComponentObject(_associatedEntity, this);
    22.    }
    23.  
    24.    private void OnDestroy()
    25.    {
    26.      _entityManager.DestroyEntity(_associatedEntity);
    27.    }
    28. }

    ---

    Managed system:
    Code (CSharp):
    1. public partial class SomeSystem : SystemBase
    2. {
    3.     private EntityQuery _playerQuery;
    4.  
    5.     protected override void OnCreate()
    6.     {
    7.         _playerQuery = new EntityQueryBuilder(Allocator.Temp).WithAll<PlayerTag>().WithNone<AliveTag>().Build(this);
    8.  
    9.         RequireForUpdate(_playerQuery);
    10.     }
    11.  
    12.     // Player died
    13.     protected override void OnStartRunning()
    14.     {
    15.         Entities.ForEach((in SomeUIMonoBehaviour someUIMonoBehaviour) =>
    16.                                         {
    17.                                             someUIMonoBehaviour.ToggleView(true);
    18.                                         }).WithoutBurst().Run();
    19.     }
    20.  
    21.     protected override void OnUpdate()
    22.     {
    23.  
    24.     }
    25. }
     
    Last edited: Jan 30, 2023
    UniqueCode and bb8_1 like this.
  4. alexandre-fiset

    alexandre-fiset

    Joined:
    Mar 19, 2012
    Posts:
    702
    This might be fine to enable or disable ui, but we often need to know if some user interface is visible, or if it's showing or hiding, in BurstCompatible code. So we've just made an ECS layer for knowing/controlling the ui, that we call ViewSystem.

    Every ui element has an entity and some states components. For instance, the Ui for showing subtitles is called SubtitleView has an entity composed like this:
    • View
    • ViewObject
      • Canvas
      • CanvasGroup
      • etc.
    • SubtitleView
      • FixedString64 TextId
    • SubtitleViewObject
      • TMProUGUI Text
    • ViewIsHidden
    What has an "Object" suffix is a component object, all the others are IComponentData. So basically, another ECS system could do:
    • SetComponent on SubtitleView to change the text
    • AddComponent ViewShow on the view entity to trigger the showing of the view
    This way the simulation is clean, and somewhere after we apply the new text to the text mesh pro component, and the new alpha value to the Canvas Group. It also allows other systems to know if Subtitles are currently displayed by querying SubtitleView and ViewIsShown tags, all in bursted code.
     
    Last edited: Jan 30, 2023
    Fiffe, RaL, PanMadzior and 2 others like this.
  5. EarlGreay

    EarlGreay

    Joined:
    Nov 13, 2021
    Posts:
    1
    @alexandre-fiset Your solution sounds really interesting. Would you be able to post small code examples that show how that approach works? I'm fairly new to DOTS. Specifically I'm unsure how using SetComponent on SubtitleView would change the text, but I'd be really interested to see how the approach works as a whole.