Search Unity

[Case Study] Triggering System

Discussion in 'Entity Component System' started by vincentchu_atalonventures, Aug 7, 2018.

  1. vincentchu_atalonventures

    vincentchu_atalonventures

    Joined:
    May 4, 2018
    Posts:
    16
    Hi everyone,

    When I went through the documentation in Entity Component System principles and vision, I saw a paragraph mentioned ECS aiming for One way of writing code. Currently I found that there is still so many possible way we can implement the same thing and I am not yet able to balance the trade off to see if there is an universal best way of implementing the same logic, and it's not dependent on any condition outside.

    As there is only very few official tutorials and projects available for reference, I would like to try to generalize understanding in this community on how we should implement some simple systems in a single way with Unity ECS. In this topic I would like to discuss about Triggering System. Where the system usually only run once, or very rare in the whole game cycle (etc. GameOver event, Initiation event, Bonus event)

    I hope this topic can create as many possible way as everyone can think of and they are stick to topic and not obviously wrong. I will use a simple "Game Over Event" as my concrete example to continue.

    When I try to implement these triggering systems, the first implementation I came to mind is to call Enabled = false at the end of the OnUpdate:

    Code (CSharp):
    1. public class GamOverSystem : ComponentSystem {
    2.     protected override void OnUpdate()
    3.     {
    4.         Debug.Log("Game Over!");
    5.         Enabled = false;
    6.     }
    7. }
    Then call it from another system:
    Code (CSharp):
    1. public class OtherSystem : ComponentSystem {
    2.     protected override void OnUpdate()
    3.     {
    4.         bool isGameOver = true;
    5.         if (isGameOver)
    6.             World.Active.GetExistingManager<GamOverSystem>().Enabled = true;
    7.     }
    8. }
    9.  
    This immediately created one problem - The GameOver event will be executed once at the begining:


    Here I can only think of two solutions:
    1. Set Enabled = false at OnCreateManager
    Code (CSharp):
    1. public class GamOverSystem : ComponentSystem {
    2.     protected override void OnCreateManager(int capacity)
    3.     {
    4.         Enabled = false;
    5.     }
    6.  
    7.     protected override void OnUpdate()
    8.     {
    9.         Debug.Log("Game Over!");
    10.         Enabled = false;
    11.     }
    12. }
    2. Disable all triggering system in Bootstrap
    Code (CSharp):
    1. public class Bootstrap : MonoBehaviour {
    2.     [RuntimeInitializeOnLoadMethod]
    3.     static void Init ()
    4.     {
    5.         // Disable all triggering system at the begining
    6.         World.Active.GetOrCreateManager<GamOverSystem>().Enabled = false;
    7.     }
    8. }
    If we go back one step before there is another non-ecs style code which is actually much more cleaner:
    Code (CSharp):
    1. public class GamOverSystem : ComponentSystem {
    2.     protected override void OnUpdate()
    3.     {
    4.     }
    5.  
    6.     public void GameOver()
    7.     {
    8.         Debug.Log("Game Over");
    9.     }
    10. }
    Code (CSharp):
    1. public class OtherSystem : ComponentSystem {
    2.     protected override void OnUpdate()
    3.     {
    4.         bool isGameOver = true;
    5.         if (isGameOver)
    6.             World.Active.GetExistingManager<GamOverSystem>().GameOver();
    7.     }
    8. }

    At this point I think everyone will agree as a beginner who approach to Unity ECS, will easily diverge in code implementation tremendously.
    And my questions here will be:
    1. Which is the best way, correct way to implement logic like game over?
    2. Is this way universally better than the others? (In-terms of speed, maintainability, flexibility, etc..)
     

    Attached Files:

    Last edited: Aug 8, 2018
  2. Fido789

    Fido789

    Joined:
    Feb 26, 2013
    Posts:
    343
    The ECS way would be probably to make the GamOverSystem accept entities with GameOver component on it. And when there is a game over, just create a new entity and put a GameOver component on it. This will activate GameOverSystem.
     
    Last edited: Aug 8, 2018
    Antypodish likes this.
  3. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    I see this type of answer often when somebody says "How do I do X in an ECS?". The answer is always 'create an entity and make a system that reacts to that entity'.

    The problem with this is that while yes, maybe it works... it leads to incredibly convoluted architectures at times. There are things which are more easily modeled outside of the scope of an entity or a system. The answer to every problem in an ECS can't be 'create an entity for it', is all I'm saying.
     
  4. Fido789

    Fido789

    Joined:
    Feb 26, 2013
    Posts:
    343
    Well, I am afraid that creating entity to activate system is the correct ECS answer to the problem stated in the original post. I will gladly discuss other options, that would not "lead to incredibly convoluted architectures", if you could provide any.

    EDIT: The options from the original post are actually pretty bad. There will most probably be other systems interested in knowing if there is a game over, and enabling/disabling them one by one will be very error prone.
     
    Last edited: Aug 7, 2018
  5. starikcetin

    starikcetin

    Joined:
    Dec 7, 2017
    Posts:
    340
    I advocate the quite opposite: In my opinion, this way of coding leads to simpler and easier to maintain architectures.

    And yes, the answer to everything in ECS is literally creating an entity. That is the whole philosophy behind the ECS architecture.
     
  6. dartriminis

    dartriminis

    Joined:
    Feb 3, 2017
    Posts:
    157
    I would also like to add the argument that ECS may not be the answer to everything. ECS is very good at transforming sets of data every frame (e.g. position updates, physicals calculations, rendering). But it may not be the correct solution for one time events such as Game Over. Another solution may be to have a plain system (for the sole purpose of being able to Inject it), a GameObject, or even a ScriptableObject, that just has a method that gets called when the game ends.
     
  7. vincentchu_atalonventures

    vincentchu_atalonventures

    Joined:
    May 4, 2018
    Posts:
    16
    I am very happy to see all of you joining this discussion, giving your opinion and input.

    This divergence should be there for a long time, some people support "pure" ecs architecture, the others thought iterating a system where 99.99% of the time we are not using is a waste of computation power, especially when those system grow up to hundreds or even thousands.

    I would love to see if there is a tendency on one side, that more people support and can provide stronger argument on, so that everyone else can confidently code it in the same way, and see we can reach the goal One way of writing code.

    It would be a nightmare when later on "Oooo How can you write the code in this way?!" such of scenarios prompts up, and all the individuals already contributing ecs code in different architecture and creating legacy problems.

    I am not sure if this thread can help, but I do think it's a problem we as a community can, and should try to solve as early as possible.

    Note: "Pure ECS" here does not mean using IComponentData, I mean doing ecs in every circumstances
     
    Last edited: Aug 8, 2018
  8. vincentchu_atalonventures

    vincentchu_atalonventures

    Joined:
    May 4, 2018
    Posts:
    16
    Turning a system off when you are not using has another advantage.

    For example you are debugging your program, you can easily filter out those systems in the entity debugger, and only focus on systems that turned on. But if you do it in components, you need to check every conditions in the components, and open the cs file of each system to ensure if these conditions really return in the OnUpdate, which I think is a way more complicated. For me, turning down system which is not using is an efficient way to narrow down the debugging scope.

    Don't you like the concept:
    "A system is on only when it's mutating values in components"
     
    Last edited: Aug 8, 2018
  9. Fido789

    Fido789

    Joined:
    Feb 26, 2013
    Posts:
    343
    @vincentchu_atalonventures I was actually thinking a bit like you a few days ago, but Joachim_Ante's comment made me to reconsider it.

    Your way of enabling/disabling systems leads to tight coupling and this is bad for a maintenance of your program. You will have to enable and disable many systems as a reaction to game over. On the other side using the ECS way you just throw one component to the entity manager and let the systems react. Be there tens or thousands of them. They know it best if they should run or not when there is a game over.
     
    Last edited: Aug 8, 2018
  10. vincentchu_atalonventures

    vincentchu_atalonventures

    Joined:
    May 4, 2018
    Posts:
    16
    Your reference to the post is very helpful, as Joachim_Ante stated The presence of components drives whether or not a system runs.

    Now we can rewrite Game Over logic like this, although GamOverSystem is enabled but OnUpdate is not executed until there is presence of components in the World.
    #Please kindly let me know if we can DestroyEntity with hybrid ECS.

    Code (CSharp):
    1. public class GamOverSystem : ComponentSystem {
    2.     private struct Group
    3.     {
    4.         public readonly int Length;
    5.         public EntityArray Entities;
    6.         public ComponentDataArray<GameOverComponent> GameOverComponent;
    7.     }
    8.  
    9.     [Inject] Group group;
    10.  
    11.     protected override void OnUpdate()
    12.     {
    13.         Debug.Log("GameOver");
    14.         for (int i = 0; i < group.Length; i++)
    15.             PostUpdateCommands.DestroyEntity(group.Entities[i]);
    16.     }
    17. }
    Code (CSharp):
    1. public struct GameOverComponent : IComponentData {
    2.  
    3. }
    Code (CSharp):
    1. public class OtherSystem : ComponentSystem {
    2.     protected override void OnUpdate()
    3.     {
    4.         bool isGameOver = true;
    5.         if (isGameOver)
    6.             EntityManager.CreateEntity(typeof(GameOverComponent));
    7.     }
    8. }
    I feel very comfortable with this approach as:
    1. System OnUpdate does not execute on every frame
    2. Easy debugging as we can see a clear zero in entity debugger (No need to check for each system .cs file)
     

    Attached Files: