Search Unity

Question Effective reactive approach to Unity UI updates with DOTS/ECS.

Discussion in 'Entity Component System' started by BenjaminApprill, Jun 4, 2023.

  1. BenjaminApprill

    BenjaminApprill

    Joined:
    Aug 1, 2022
    Posts:
    132
    I would like to use DOTS/ECS for a project I'm working on, but I'm comfortable using the Canvas and GameObject approach for creating the UI.

    I am aware of a few ways to update the UI information after changing the data. For instance, I could simply poll the specific data from the EntityManager and update it in the Update loop... I could also possibly use a Managed component to create a collection of relevant UI elements to update.

    I would rather avoid the polling option on update, which leaves me leaning towards the Managed component to make this work. Any other ideas on how to do this would be greatly appreciated!
     
  2. gencontain

    gencontain

    Joined:
    Nov 15, 2012
    Posts:
    15
  3. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    115
    Change filters and "events" (I'm using a very broad definition of "event" here) are two common ways of handling reactive logic in ECS. Here are some pros/cons:

    The change filters approach adds a small constant cost to every update, and this cost grows with the amount of different change-filtering jobs you need and with the amount of entities that need to be checked for changes

    The events approach doesn't really add a constant update cost, but it adds a bit more of a cost when changes do happen. So for example, if UI needs to react to health changes, your job that changes health might now add a HealthChanged tag component to that entity (and this tag would get consumed and removed by your ui system for applying changes), meaning health changes are now more expensive due to structural changes.

    However, you could also decide to make HealthChanged an enableable component that's always on the entity, and simply enable it when ui needs to update health. This would avoid structural changes, but would have similar drawbacks as the changefilter approach, because jobs that iterate on enableable components need to constantly check which components are enabled and which arent in a chunk (they do this very efficiently, but there is still a cost)

    Yet another way to handle events would be to add them to a NativeQueue of "Health has changed on entity X" events. This adds a very tiny cost to every health change due to adding to queue, but we're now also paying an extra cost when processing these events, because we must get the health data via component lookup instead of with regular entity iteration. Or maybe we can even avoid the cost of the lookup if we store the new health value along with the event. Either way this will perform better than the structural changes approach in almost all cases, but it has the disadvantage of not allowing you to use composition in your events processing

    It's all about figuring out which performance/usability sacrifices make the most sense in your project. There are also plenty of other ways to do this that weren't mentioned here, but these solutions can get very specific for a precise use case. Typically, the approaches that have a constant update cost (change filter, enabled components) are a better choice when there will be lots of things changing very frequently, while the other approaches (structural change, native queue) are better for smaller-scale or more infrequent changes
     
    Last edited: Jun 8, 2023
    OrientedPain, apkdev, Ryiah and 6 others like this.
  4. UniqueCode

    UniqueCode

    Joined:
    Oct 20, 2015
    Posts:
    65
    This is gold. Refactored my UI from C# events to systems and it's much cleaner. The manual *has* to include commonly used patterns. It's not that many.
     
    apkdev and charleshendry like this.
  5. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    One thing to note regarding UI.
    While it is intuitive to place events for it everywhere when switching from OOP and "best practices" -
    Reality often shows that UI logic do run each frame and require constant updates.
    Stuff like value interpolation for animation - all spectrum of, movement, rotation, fading, turning on / off etc;

    Moreover, UI elements are usually 1. Meaning cost of applying once to 1 entity is really non-existent.
    At some point, as project grows, events for UI move into "not worth it" category.
    Like running each frame is cheaper and less error prone than dealing with spaghetti of events.

    E.g. if multiple entities fires multiple events to the same element, you'd be applying / modifying UI multiple times per same frame even if you don't actually need to. Because only result value matters.

    Just decoupling data & dealing with data processing separately immensely improves performance.
    Processing data is the major part of the cost.

    Visualizing is usually very cheap when done once.
    To gate away identical values [and prevent canvas changes] you can put a simple if statement to check against if value has changed. And it is going to be faster when looking from structural changes perspective.

    So do not focus on how much applying data to UI costs unless its a bottleneck.

    TL;DR:
    Analyze first if it is a problem.

    As for the how to apply data to MonoBehaviours - use systems instead of MonoBehaviour's Update to "apply" data to the respective behaviours. Any UnityEngine.Object can be added to the entity and queried over via managed systems.

    Rest is already mentioned.
     
    Last edited: Jun 8, 2023
    apkdev, JesOb and exiguous like this.
  6. DatCong

    DatCong

    Joined:
    Nov 5, 2020
    Posts:
    87
    another idea, go with managed component and scriptableobject. The ScriptableObject is the bridge. For ex when u change health from entity just update the value of ScriptableObject ( usually Scriptableobejct have onchangeevent) so the ui can easily know when to update.
     
  7. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Using SO's as non-readonly data is a bad idea which opens up a different can of worms.
    If you want global application events - just use statics.
    If you want storage for values - make a separate one from SOs.

    Any change done to SO will propagate to the editor. And anything that reads it (assuming SOs are readonly) will read invalid values as well. Plus, you're still bound to the UnityEngine.Object with SO's. So no proper serialization support. Which means it has to be refactored away breaking data layouts.

    Its nasty, don't do it in production code.
     
    toomasio and apkdev like this.