Syncing ECS gameplay with Unity UI

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:

[SerializeField]
private Canvas _canvas;

private EntityManager _entityManager;

private void Start()
{
    _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

    TryInitialize();
}

private void OnDisable()
{
    SomeSystemData someSystemData = GetSomeSystemData();

    if (someSystemData != null)
    {
        someSystemData.PlayerDied -= OnPlayerDied;
    }

    _isInitialized = false;
}

private void Update()
{
    if (_isInitialized)
    {
        return;
    }
 
    TryInitialize();
}

private void TryInitialize()
{
    var someSystemData = GetSystemData();

    if (someSystemData == null)
    {
        return;
    }

    someSystemData.PlayerDied += OnPlayerDied;
    _isInitialized = true;
}

private SomeSystemData GetSystemData()
{
    SystemHandle systemHandle = World.DefaultGameObjectInjectionWorld.GetExistingSystemManaged(typeof(SomeSystem)).SystemHandle;
 
    return _entityManager.GetComponentData<SomeSystemData>(systemHandle);
}

private void OnPlayerDied()
{
    ToggleUI(true);
}

private void ToggleView(bool isEnabled)
{
    _canvas.enabled = isEnabled;
}

System’s managed component:

public class SomeSystemData : IComponentData
{
    public Action PlayerDied;
}

And here’s the managed system itself:

public partial class SomeSystem : SystemBase
{
    private EntityQuery _playerQuery;
 
    protected override void OnCreate()
    {
        EntityManager.AddComponentObject(SystemHandle, new SomeSystemData());
     
        _playerQuery = new EntityQueryBuilder(Allocator.Temp).WithAll<PlayerTag>().WithNone<AliveTag>().Build(this);
     
        RequireForUpdate(_playerQuery);
    }

    // Player died
    protected override void OnStartRunning()
    {
        EntityManager.GetComponentObject<SomeSystemData>(SystemHandle).PlayerDied?.Invoke();
    }

    protected override void OnUpdate()
    {
 
    }
}

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!

2 Likes

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.

public class SomeMonoBehaviourUI : MonoBehaviour {
    // Author SomeMonoBehaviourUI as managed component to the required World first
    ...

    public void DoSomething(UIData data);
}

// Then in a system
   public partial class SomeUISystem : SystemBase {
      protected override void OnUpdate() {
        ...
        // E.g. if (!_playerDeadQuery.IsEmpty)
        // Or even better -> use filter in OnCreate alike RequireForUpdate
        Entities.ForEach((in SomeMonoBehaviourUI ui) => {
            ...
            ui.DoSomething(data);

            // Or even better, perform logic on the UI component from the system directly,
            // without passing data, e.g. show / hide, start animations etc.
         }).WithoutBurst().Run();
      }
   }

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.

2 Likes

@VergilUa 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:

public class SomeUIMonoBehaviour : MonoBehaviour
{
   [SerializeField]
   private Canvas _canvas;

   private EntityManager _entityManager;
   private Entity _associatedEntity;

   public void ToggleView(bool isEnabled)
   {
       _canvas.enabled = isEnabled;
   }

   private void Start()
   {
       _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

       _associatedEntity = _entityManager.CreateEntity();

       _entityManager.AddComponentObject(_associatedEntity, this);
   }

   private void OnDestroy()
   {
     _entityManager.DestroyEntity(_associatedEntity);
   }
}

Managed system:

public partial class SomeSystem : SystemBase
{
    private EntityQuery _playerQuery;

    protected override void OnCreate()
    {
        _playerQuery = new EntityQueryBuilder(Allocator.Temp).WithAll<PlayerTag>().WithNone<AliveTag>().Build(this);
 
        RequireForUpdate(_playerQuery);
    }

    // Player died
    protected override void OnStartRunning()
    {
        Entities.ForEach((in SomeUIMonoBehaviour someUIMonoBehaviour) =>
                                        {
                                            someUIMonoBehaviour.ToggleView(true);
                                        }).WithoutBurst().Run();
    }

    protected override void OnUpdate()
    {

    }
}
2 Likes

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.

5 Likes

@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.