Search Unity

Updating Systems from Inside a System

Discussion in 'Entity Component System' started by orionburcham, Dec 28, 2018.

  1. orionburcham

    orionburcham

    Joined:
    Jan 31, 2010
    Posts:
    488
    Is it valid to manually update one system from inside of another system (assuming there aren't dependency issues)?

    Code (CSharp):
    1. public class SystemA : ComponentSystem
    2. {
    3.     ScriptBehaviourManager systemB;
    4.     ScriptBehaviourManager systemC;
    5.  
    6.     protected override void OnUpdate()
    7.     {
    8.         while(condition == true)
    9.         {
    10.             systemB.Update();
    11.         }
    12.  
    13.         systemC.Update();
    14.     }
    15. }
     
    Last edited: Dec 28, 2018
  2. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    It’s fine if your another system not in player loop, because if inside, then this system updates twice (by player loop and by your own update)
     
  3. orionburcham

    orionburcham

    Joined:
    Jan 31, 2010
    Posts:
    488
    Thanks!

    In the example above, would there be no way to exclude only SystemB and SystemC from the player loop (so that they're only ever updated by SystemA)?
     
  4. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    If you create systemA and inside have call other systems from world by GetOrCreateManager/etc. in OnCreate or inject them, they automatically creates and if you use updating player loop they automatically handled by loop, but if you not use player loop and manually manage update then all must be fine, but I have one expectation about player loop but now I can’t check it from mobile :) tomorrow maybe
     
    Last edited: Dec 28, 2018
    orionburcham likes this.
  5. orionburcham

    orionburcham

    Joined:
    Jan 31, 2010
    Posts:
    488
    I'd like to manually manage some of the player loop, but not all.

    For example, the automatic player loop might look something like this:

    Code (CSharp):
    1. systemA.Update(); // conditionally calls SystemB and SystemC.
    2. systemD.Update();
    3. systemE.Update();
    SystemB and SystemC would be called manually from inside SystemA. But all other systems would be called automatically by Unity. Is this possible? Is there a way for me to exclude only SystemB and SystemC from the automatic Unity loop?
     
  6. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
     
  7. orionburcham

    orionburcham

    Joined:
    Jan 31, 2010
    Posts:
    488
    The qestion is for anyone:

    Thanks for any advice.
     
  8. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    Can you not achieve this in the "normal" way?
    1. set system update order to A->B->C
    2. system A adds a conditional tag "UpdateB" and a tag "UpdateC" to all entities it processes
    3. system B looks for the tag "UpdateB" and removes it in postupdatecommands
    4. system C as system B
     
    orionburcham likes this.
  9. orionburcham

    orionburcham

    Joined:
    Jan 31, 2010
    Posts:
    488
    Thanks sngdan! I appreciate that, and it would mostly work. The only thing it wouldn't handle is the looping logic in my OP example:

    Code (CSharp):
    1. public class SystemA : ComponentSystem
    2. {
    3.     ScriptBehaviourManager systemB;
    4.     ScriptBehaviourManager systemC;
    5.  
    6.     protected override void OnUpdate()
    7.     {
    8.         while(condition == true)
    9.         {
    10.             systemB.Update();
    11.         }
    12.         systemC.Update();
    13.     }
    14. }
    'SystemB' needs to run an unknown number of times before SystemC is called. How would you imagine doing that the 'normal' way?
     
    Last edited: Dec 28, 2018
  10. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    ah, i see - it would be easier to respond, if I understood the actual problem you are trying to resolve (or at least a little less abstract)

    Code (CSharp):
    1. public struct TestComponent : IComponentData {
    2. public int ValueA;
    3. public int ValueB;
    4. public int ValueC;
    5. }
    6.  
    7. condition = Input.anyKey
    8.  
    9. // all systems increment respective Value of TestComponent
    10.  
    after 5 frames (2 frames without keypress) what would you expect ValueA,B,C to be

    EDIT: to clarify -> my question is "what is the condition" and how does it change?
     
    Last edited: Dec 28, 2018
  11. orionburcham

    orionburcham

    Joined:
    Jan 31, 2010
    Posts:
    488
    Hi! Alright, the real world problem would probably take us too deep into the weeds for this thread, but here's a simplified version of what's going on:

    I'm building a generic behavior system for use with ai agents. This behavior system is made up of multiple "behavior queues". Individual behaviors can be added to these queues, and will then execute in sequence, one at a time. Take this queue for example (the numbers represent the indices of the queue. Lower numbers are processed first):

    3: [ Animate Into Cover ]
    2: [ Run to Cover ]
    1: [ Find Cover ]
    0: [ Take Healing Potion ]


    Each behavior queue only has one behavior active at a time (the one located at the 0 index). In this example, the only active behavior is "Take Healing Potion". When "Take Healing Potion" is complete, it is removed from the queue, and everything shifts down one index. Then the new 0 index behavior would be activated (in this case "Find Cover"). <- More on how this is actually done with Systems, below.

    Behaviors come in two types: Immediate (which can be completed in a single frame) and Persistent (which can only be completed over multiple frames). In the example, two of the behaviors are immediate, and two are persistent:

    3: [ Animate Into Cover ]
    2: [ Run to Cover ]
    1: [ Find Cover ]
    0: [ Take Healing Potion ]

    On each game loop, the goal of each behavior queue is to process every immediate behavior, until a persistent behavior is reached. That way, the agent is doing as much work as it can on each frame, and it only stops when it needs to wait for a multi-frame activity to complete.

    So if the above example is what that behavior queue looks like at the start of a frame, at the end of that frame, it should look like this:

    3: [ <None> ]
    2: [ <None> ]
    1:
    [ Animate Into Cover ]
    0: [ Run to Cover ]

    - - - System Order - - -

    Each behavior has a corresponding Component and a corresponding System. When a behavior become active, its corresponding Component is added to the agent entity.

    Later in the update loop, that behavior's corresponding System will catch this newly added component, and perform the actual logic that defines the behavior.

    If that System determines that the behavior has been completed, it will add an "Complete Active Behavior" Component to the entity which represents the behavior queue.

    At the beginning of the next frame, a "Behavior Queue Update" System will catch that Component, remove the active behavior from the correct behavior queue, and activate the next behavior in line.

    Here's a general idea of what that looks like, in terms of System Execution Order over one frame:

    Code (CSharp):
    1. // Update Behavior Queue Systems
    2. CompleteActiveBehaviorSystem.Update();
    3.  
    4. // Behavior Activity Systems
    5. TakeHealingPotionBehaviorSystem.Update();
    6. FindCoverBehaviorSystem.Update();
    7. AnimateIntoCoverBehaviorSystem.Update();
    8. RunToCoverBehaviorSystemUpdate();

    But this loop is too simple. We need to process all possible immediate behaviors in the same frame. So the System execution actually looks like this:

    Code (CSharp):
    1. do
    2. {
    3.     // Update Behavior Queue Systems
    4.     CompleteActiveBehaviorSystem.Update();
    5.  
    6.     // Immediate Behavior Activity Systems
    7.     TakeHealingPotionBehaviorSystem.Update();
    8.     FindCoverBehaviorSystem.Update();
    9. }
    10. while (/* immediate behaviors are active */);
    11.  
    12. // Persistent Behavior Activity Systems
    13. AnimateIntoCoverBehaviorSystem.Update();
    14. RunToCoverBehaviorSystemUpdate();

    And that works correctly. All immediate behaviors are completed in the same frame, until only persistent behaviors are left active. Then, the Persistent Behavior systems get a chance to advance those persistent behaviors ahead one frame. At the start of the next frame, the behavior queues get a chance to update if any of these multi-frame behaviors have completed.

    So that's a simple version of the actual case. The loop of "Updating Behavior Queues" and "Immediate Behavior Work" needs to run a different number of times each frame. The condition is whether or not any "immediate" behaviors are still active.

    I know that's a lot. Does this give you any ideas about how to loop systems?
     
    Last edited: Dec 28, 2018
  12. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    too much, too late - will take a look tomorrow.

    My approach was to achieve your logic by a messaging system that achieves the result you wanted from "calling systems from systems"
     
    orionburcham likes this.
  13. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    I know I am not answering your question to the dot and you likely ruled out what I am going to ask

    1a. Do immediate behaviors require ordered execution?
    1b. Can the same immediate behavior be queued twice and not be consolidated (ie 1. heal, 2. find 3. cover, heal cannot be consolidated to 1. Double heal, 2. Cover)?

    2. Whould it be feasible to run only one behavior per frame (ie the two immediate in your example run in 2 frames after each other)

    3. Could you have one system dealing with all immediate behaviors?

    4. Before messing with system to system updates, I would split into a simulation (ticking at high frequency) and rendering world (ticking at render frequency)
     
  14. orionburcham

    orionburcham

    Joined:
    Jan 31, 2010
    Posts:
    488
    First of all, thank you for reading through my long explanation. I appreciate it, and everything you're suggesting. :)

    To your questions:

    1a. Execution order might be required, depending on the needs of the project. The current implementation supports either approach.

    1b. So far, I have not found a good way to consolidate behaviors of the same type that exist at multiple spots in a queue. It may not even be desirable - for example, a common pattern used with Behavior Queues is to use one queue to listen for an event, and a second queue to respond to that event, like this:

    Code (CSharp):
    1. [ <None> ]                  [ Heal ]
    2. [ <None> ]                  [ Attack ]
    3. [ Listen for Enemy ]        [ Heal ]
    4. ____________________        ____________________
    5. Listen Behavior Queue       Response Behavior Queue
    ...If a second enemy shows up before all the 'Response' behaviors have completed, the 'Listen for Enemy' behavior might clear out the 'Response' Behavior Queue, and refill it with a new set of response behaviors.

    In that case, you wouldn't want to consolidate both "Heal" behaviors in the Response queue, because the second one might never happen.

    That said, the idea of having multiple immediate behaviors of the same type sitting next to each other on the queue, can be avoided with good behavior design. For example, I wouldn't want to see this case in a project:

    Code (CSharp):
    1. [ Heal Immediate ]
    2. [ Heal Immediate ]
    3. [ Heal Immediate ]
    That could instead be done with a single
    [ Heal to Target Percent ]
    Behavior, which causes the agent to take as many healing potions as needed to reach a target HP. Then there's no need to consolidate all those "Heal" behaviors.

    2. This is possible, and something I've considered before...In some cases it would make the AI code very slow. It would also 'punish' the use of more modular, granular behaviors, which should generally be a good thing. I'd like to avoid it if possible. BQs are much more versatile without that restriction.

    3. I see three downsides to having a single "Immediate Behavior" system:
    1. System bloat. Such a system would be stretching the "Single responsibility" principle *pretty* far.
    2. It would fight the modularity of behaviors. Adding a new behavior would involve going into this system and adding some behavior-specific logic.
    3. The would prevent batching of behavior logic when/if it *is* possible.

    If there's something I'm missing here please let me know.
    4. I'm familiar and used to this approach, but I'm not sure how it relates to this setup. Everything involving this behavior processing would be on the "Sim" side of that pattern. I'll have to think more- maybe I'm missing something.

    - - -

    That said, I'm not sure there's actually a problem. If calling Systems from inside of another System is supported, then that seems like it could work in this case.

    I started the thread in case that idea makes someone more familiar with Unity ECS cry out in terror. :) If that's the case, I'd still like to know! :)
     
    Last edited: Dec 29, 2018
  15. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    Ok. Now I'm on PC and can give you some suggestions for excluding some systems from player loop. This is very basic solutions - disable this systems and if you need Update them, then enable->update->disable, in this case this systems can be in loop but loop newer call them automatically because they are disabled. Second solution, in your world initialization not create thes systems, create other systems, like sysA, sysD, sysE and call update player loop (but remember not include sysB and sysC in sysA by calling GetOrCreateManager in OnCreate, because it creates them too before calling player loop and then they too being in player loop, instead you can call World.GetOrCreateManager directly in your condition code, it's fast without systems cache, because it's just type lookup from dictionary but anyway you can cache systems in first conditional call, lazy load pattern). Other solution is change player loop content but it's little bit complicated than previous.

    SECOND SAMPLE CODE:

    Bootstrap:

    Code (CSharp):
    1. using System.Linq;
    2. using Unity.Entities;
    3. using UnityEngine;
    4.  
    5. public class ConditionalCallBootstrap : MonoBehaviour
    6. {
    7.     void Start()
    8.     {
    9.         World.Active.CreateManager<ConditionalCallSystemsA>();
    10.         World.Active.CreateManager<ConditionalCallSystemsD>();
    11.         World.Active.CreateManager<ConditionalCallSystemsE>();
    12.         ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World.AllWorlds.ToArray());
    13.     }
    14. }
    Systems:
    Code (CSharp):
    1. using Unity.Entities;
    2. using UnityEngine;
    3.  
    4. [DisableAutoCreation, AlwaysUpdateSystem]
    5. public class ConditionalCallSystemsA : ComponentSystem
    6. {
    7.     private int counter = 0;
    8.     protected override void OnUpdate()
    9.     {
    10.         Debug.Log("<color=red>----------START LOOP----------</color>");
    11.         Debug.Log("A");
    12.         if (counter > 1)
    13.         {
    14.             counter = 0;
    15.             World.GetOrCreateManager<ConditionalCallSystemsB>().Update();
    16.             World.GetOrCreateManager<ConditionalCallSystemsC>().Update();
    17.             World.GetOrCreateManager<ConditionalCallSystemsB>().Update();
    18.             World.GetOrCreateManager<ConditionalCallSystemsC>().Update();
    19.         }
    20.         else
    21.         {
    22.             counter++;
    23.         }
    24.     }
    25. }
    26.  
    27. [DisableAutoCreation, AlwaysUpdateSystem]
    28. public class ConditionalCallSystemsB : ComponentSystem
    29. {
    30.     protected override void OnUpdate()
    31.     {
    32.         Debug.Log("<color=green>B</color>");
    33.     }
    34. }
    35.  
    36. [DisableAutoCreation, AlwaysUpdateSystem]
    37. public class ConditionalCallSystemsC : ComponentSystem
    38. {
    39.     protected override void OnUpdate()
    40.     {
    41.         Debug.Log("<color=green>C</color>");
    42.     }
    43. }
    44.  
    45. [DisableAutoCreation, AlwaysUpdateSystem, UpdateAfter(typeof(ConditionalCallSystemsA))]
    46. public class ConditionalCallSystemsD : ComponentSystem
    47. {
    48.     protected override void OnUpdate()
    49.     {
    50.         Debug.Log("D");
    51.     }
    52. }
    53.  
    54. [DisableAutoCreation, AlwaysUpdateSystem, UpdateAfter(typeof(ConditionalCallSystemsD))]
    55. public class ConditionalCallSystemsE : ComponentSystem
    56. {
    57.     protected override void OnUpdate()
    58.     {
    59.         Debug.Log("E");
    60.         Debug.Log("<color=red>----------END LOOP----------</color>");
    61.     }
    62. }
    63.  
    Result:
    upload_2018-12-29_10-57-0.png

    SAMPLE WITH MODIFIED CURREN PLAYER LOOP CODE:

    Bootstrap:

    Code (CSharp):
    1. using System.Linq;
    2. using Unity.Entities;
    3. using UnityEngine;
    4. using UnityEngine.Experimental.LowLevel;
    5.  
    6. public class ConditionalCallBootstrap : MonoBehaviour
    7. {
    8.     void Start()
    9.     {
    10.         World.Active.CreateManager<ConditionalCallSystemsA>();
    11.         World.Active.CreateManager<ConditionalCallSystemsB>();
    12.         World.Active.CreateManager<ConditionalCallSystemsC>();
    13.         World.Active.CreateManager<ConditionalCallSystemsD>();
    14.         World.Active.CreateManager<ConditionalCallSystemsE>();
    15.    
    16.         var loop = ScriptBehaviourUpdateOrder.CurrentPlayerLoop;
    17.         var topLevelLoop = loop.subSystemList;
    18.         for (int i = 0; i < topLevelLoop.Length; i++)
    19.         {
    20.             //In this place we add our system to Update loop (also we can put it in to Initialization, FixedUpdate and other
    21.             if (topLevelLoop[i].type == typeof(UnityEngine.Experimental.PlayerLoop.Update))
    22.             {
    23.                 var systemsList = topLevelLoop[i].subSystemList.ToList();
    24.                 systemsList.Add(
    25.                     new PlayerLoopSystem()
    26.                     {
    27.                         type = typeof(ConditionalCallSystemsA),
    28.                         updateDelegate = World.Active.GetExistingManager<ConditionalCallSystemsA>().Update
    29.                     }
    30.                 );
    31.                 systemsList.Add(
    32.                     new PlayerLoopSystem()
    33.                     {
    34.                         type           = typeof(ConditionalCallSystemsD),
    35.                         updateDelegate = World.Active.GetExistingManager<ConditionalCallSystemsD>().Update
    36.                     }
    37.                 );
    38.                 systemsList.Add(
    39.                     new PlayerLoopSystem()
    40.                     {
    41.                         type           = typeof(ConditionalCallSystemsE),
    42.                         updateDelegate = World.Active.GetExistingManager<ConditionalCallSystemsE>().Update
    43.                     }
    44.                 );
    45.                 topLevelLoop[i].subSystemList = systemsList.ToArray();
    46.                 break;
    47.             }
    48.         }
    49.  
    50.         loop.subSystemList = topLevelLoop;
    51.         PlayerLoop.SetPlayerLoop(loop);
    52.     }
    53. }
    54.  

    Systems:

    Code (CSharp):
    1. using Unity.Entities;
    2. using UnityEngine;
    3.  
    4. [DisableAutoCreation, AlwaysUpdateSystem]
    5. public class ConditionalCallSystemsA : ComponentSystem
    6. {
    7.     private int counter = 0;
    8.     protected override void OnUpdate()
    9.     {
    10.         Debug.Log("<color=red>----------START LOOP----------</color>");
    11.         Debug.Log("A");
    12.         if (counter > 1)
    13.         {
    14.             counter = 0;
    15.             //Now we use GetExistingManager for sure that systems was created in bootstrap and not added to loop
    16.             World.GetExistingManager<ConditionalCallSystemsB>().Update();
    17.             World.GetExistingManager<ConditionalCallSystemsC>().Update();
    18.             World.GetExistingManager<ConditionalCallSystemsB>().Update();
    19.             World.GetExistingManager<ConditionalCallSystemsC>().Update();
    20.         }
    21.         else
    22.         {
    23.             counter++;
    24.         }
    25.     }
    26. }
    27.  
    28. [DisableAutoCreation, AlwaysUpdateSystem]
    29. public class ConditionalCallSystemsB : ComponentSystem
    30. {
    31.     protected override void OnUpdate()
    32.     {
    33.         Debug.Log("<color=green>B</color>");
    34.     }
    35. }
    36.  
    37. [DisableAutoCreation, AlwaysUpdateSystem]
    38. public class ConditionalCallSystemsC : ComponentSystem
    39. {
    40.     protected override void OnUpdate()
    41.     {
    42.         Debug.Log("<color=green>C</color>");
    43.     }
    44. }
    45.  
    46. [DisableAutoCreation, AlwaysUpdateSystem, UpdateAfter(typeof(ConditionalCallSystemsA))]
    47. public class ConditionalCallSystemsD : ComponentSystem
    48. {
    49.     protected override void OnUpdate()
    50.     {
    51.         Debug.Log("D");
    52.     }
    53. }
    54.  
    55. [DisableAutoCreation, AlwaysUpdateSystem, UpdateAfter(typeof(ConditionalCallSystemsD))]
    56. public class ConditionalCallSystemsE : ComponentSystem
    57. {
    58.     protected override void OnUpdate()
    59.     {
    60.         Debug.Log("E");
    61.         Debug.Log("<color=red>----------END LOOP----------</color>");
    62.     }
    63. }
    64.  

    Result:
    upload_2018-12-29_11-24-3.png


    Maybe someone have more elegant solutions, but it's my vision :)
     
    Last edited: Dec 29, 2018
    orionburcham likes this.
  16. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    Yes, I hope you also get feedback from others to get different perspectives.

    My take is that the behavior you want is not compatible with the ecs logic. Effectively you want one master system (system A) to call other systems based on the individual state of the entities processed by system A (entity specific update loop). I would dequeue immediate behavior and call into a utility class for immediate behaviors and then switch to attaching a tag component for the multi frame behaviors. (If the player loop runs fast also one behavior per frame sounds ok, unless you expect very long chains of immediate behaviors)