Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Question Prevent Custom Update Loop running post playmode

Discussion in 'Scripting' started by spiney199, Mar 22, 2022.

  1. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,396
    Perhaps a bit of an odd question, but a while ago I read on here about the Low level player loop system with which you can use to add your own update loops.

    So in my efforts to decouple the core of my project's time system from any other system, I went about implementing my own loops to give me global callbacks for when the game 'ticks' every 1 second. The issue is, once I leave play mode, the loop keeps getting called.

    Here's the code so far, with editor code to prevent the ticking outside of play mode:
    Code (CSharp):
    1. namespace TheFault.Core.Time
    2. {
    3.     using System;
    4.     using UnityEngine;
    5.     using UnityEngine.LowLevel;
    6.     using UnityEngine.PlayerLoop;
    7.     using LizardBrainGames.LowLevel;
    8.  
    9. #if UNITY_EDITOR
    10.     using UnityEditor;
    11. #endif
    12.  
    13.     public static class TheFaultGameTimeLoop
    14.     {
    15.         #region Initialisation      
    16.  
    17.         [RuntimeInitializeOnLoadMethod]
    18.         private static void InitialiseGameTimeLoop()
    19.         {
    20.             PlayerLoopSystem currentPlayerLoop = PlayerLoop.GetDefaultPlayerLoop();
    21.  
    22.             PlayerLoopSystem gameTimeTickLoop = new PlayerLoopSystem()
    23.             {
    24.                 updateDelegate = GameTimeTickLoop,
    25.                 type = typeof(GameTimeTickType)
    26.             };
    27.  
    28.             UnityEnginePlayerLoopUtilities.AddPlayerLoopToSubSystem<Update>(ref currentPlayerLoop, gameTimeTickLoop);
    29.  
    30.             PlayerLoop.SetPlayerLoop(currentPlayerLoop);
    31.  
    32.            Application.quitting += () =>
    33.            {
    34.                OnGameTimeTick = null;
    35.            };
    36.         }
    37.  
    38.         #endregion
    39.  
    40.         #region Update Struct Types
    41.  
    42.         public struct GameTimeTickType { }
    43.  
    44.         #endregion
    45.  
    46.         #region Internal Properties
    47.  
    48.        private static bool CanTickGameTime
    49.        {
    50.            get
    51.            {
    52. #if UNITY_EDITOR
    53.                if (EditorApplication.isPlaying == false) return false;
    54.  
    55.                if (EditorApplication.isPaused == true) return false;
    56. #endif
    57.                return true;
    58.            }
    59.        }
    60.  
    61.         #endregion
    62.  
    63.         #region Game Time Global Delegates
    64.  
    65.         public static event Action OnGameTimeTick;
    66.  
    67.         #endregion
    68.  
    69.         #region Game Time Loop
    70.  
    71.         private static void GameTimeTickLoop()
    72.         {
    73.             if (CanTickGameTime == false) return;
    74.  
    75.             Debug.Log("Game Time Tick!");
    76.             //Call 'OnGameTimeTick' every 1 second
    77.         }
    78.  
    79.         #endregion
    80.     }
    81. }
    UnityEnginePlayerLoopUtilities
    is my own class with helper methods to add in my own player loop subsystem, as it's a bit of an epic to do so.

    Ideally I'd prefer if the loops just stopped once I left play mode and not require any editor code. I've tried copying over the loopConditionFunction from some of the other existing loops, such as Update, to no avail.

    Would anyone here who's also used this system have any insight in how to do so?

    The alternative of course is to just use a monobehaviour with static delegates, but here I was hoping to make a non-monobehaviour alternative.
     
    ansdyd0803 likes this.
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,353
    Yeah, this is a known problem!

    I have a package available on GitHib to make interfacing with the PlayerLoopSystem a bit easier. You can either use it directly, or look at how I have solved the same problem. Essentially I do what you're suggesting, loop through the entire player loop system when the game quits, and remove all the ones I have added.

    I use a hidden MonoBehaviour and OnApplicationQuit instead of Application.quitting. I wasn't really aware of Application.quitting, and I would switch over to it if the docs had actually clearly listed when it's invoked and not :p
     
    Whatever560, noio and spiney199 like this.
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,396
    Seems like a pretty big known problem to me! At least I'm not the one doing anything wrong.

    But that's at least good to know, and thank you for the good resource. Looks like I'll be extracting all the inserted types when quitting the game.
     
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,675
    I know it's a minor thing but you can use Application.isPlaying instead of EditorApplication.

    Anyway, I experimented with this last year because I'd never used it (personal experiment). I wanted to go a little further and essentially get my system to do its own work and call into the native system it was replacing therefore executing beforehand. I found the same as you did and I also found that hooking into the ScriptRunBehaviourFixedUpdate meant it wasn't called outside of play-mode either and I could call my stuff before or after all the scripts too.

    The above helper is neat and could be used here too. This is essentially what I was experimenting with although I cannot state how safe it is to do. It's might actually be a disasterous thing to do!! There might be something useful here, not sure.

    I modified it so it logs every 1 second but always calls into the fixed-update. It stops when out of play-mode because it's based upon the fixed-update.

    Code (CSharp):
    1.  
    2. using System;
    3. using System.Runtime.InteropServices;
    4. using UnityEngine;
    5. using UnityEngine.LowLevel;
    6. using UnityEngine.PlayerLoop;
    7.  
    8. public static class MyPlayerLoop
    9. {
    10.     delegate void NativeFunction();
    11.     private static NativeFunction m_NativeFixedUpdateFunction;
    12.     private static float m_CallPeriod = 1f;
    13.     private static float m_CurrentTime;
    14.  
    15.     private static void CustomFixedUpdate()
    16.     {
    17.         // Our custom timer.
    18.         m_CurrentTime += Time.fixedDeltaTime;
    19.         if (m_CurrentTime > m_CallPeriod)
    20.         {
    21.             m_CurrentTime -= m_CallPeriod;
    22.             Debug.Log("Custom update tick!");
    23.         }
    24.  
    25.         // Call the original native fixed-update function
    26.         m_NativeFixedUpdateFunction.Invoke();
    27.     }
    28.  
    29.     [RuntimeInitializeOnLoadMethod]
    30.     private static void AppStart()
    31.     {
    32.         var defaultSystems = PlayerLoop.GetDefaultPlayerLoop();
    33.         var customUpdate = new PlayerLoopSystem()
    34.         {
    35.             updateDelegate = CustomFixedUpdate,
    36.             type = typeof(MyPlayerLoop)
    37.         };
    38.         var nativePtr = ReplaceSystem<FixedUpdate.ScriptRunBehaviourFixedUpdate>(ref defaultSystems, customUpdate);
    39.  
    40.         PlayerLoop.SetPlayerLoop(defaultSystems);
    41.  
    42.         if (nativePtr != IntPtr.Zero)
    43.         {
    44.             unsafe
    45.             {
    46.                 nativePtr = new IntPtr(*((Int64*)nativePtr.ToPointer()));
    47.             }
    48.             m_NativeFixedUpdateFunction = Marshal.GetDelegateForFunctionPointer(nativePtr, typeof(NativeFunction)) as NativeFunction;
    49.         }
    50.     }
    51.  
    52.     private static IntPtr ReplaceSystem<T>(ref PlayerLoopSystem system, PlayerLoopSystem replacement)
    53.     {
    54.         if (system.type == typeof(T))
    55.         {
    56.             var nativeUpdatePtr = system.updateFunction;
    57.             system = replacement;
    58.             return nativeUpdatePtr;
    59.         }
    60.  
    61.         if (system.subSystemList == null)
    62.             return IntPtr.Zero;
    63.  
    64.         for (var i = 0; i < system.subSystemList.Length; i++)
    65.         {
    66.             var nativeUpdatePtr = ReplaceSystem<T>(ref system.subSystemList[i], replacement);
    67.             if (nativeUpdatePtr == IntPtr.Zero)
    68.                 continue;
    69.  
    70.             return nativeUpdatePtr;
    71.         }
    72.  
    73.         return IntPtr.Zero;
    74.     }
    75. }
     
    ansdyd0803 and spiney199 like this.
  5. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,353
    Yeah, note that when I tested this with entities installed (over a year ago), the entities package reset things as you exited play mode, so in that case all this juggling wasn't necessary.

    @MelvMay, do you have a chance to look at the code and checkif is there any way we can actually use the loopConditionFunction? I've tried to copy it from builtin systems, but that doesn't seem to do anything, and the documentation doesn't really tell me why I would want to.
     
  6. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,675
    AFAIK you cannot as it's internal only. It's a IntPtr to some native method. I'm not even sure I understand the benefit of exposing it either.

    The only one I can see that uses it is the fixed-update one which points to a C++ method:
    Code (CSharp):
    1. static bool FixedUpdateCondition()
    2. {
    3.     return GetTimeManager().StepFixedTime();
    4. }
    5.  
    Likely it's only used in the fixed-update one I mentioned. This is why it's not called when out of play mode because fixed-update doesn't run there whereas all the updates do/can.

    Sounds like a bug to me although you'd need to check that behaviour on the forums. Way outside my area of expertise here with Unity.
     
    Baste likes this.
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,353
    Thanks!

    I reported it as a bug and QA reported that both it not resetting when you exit play mode, and it resetting when you exit play mode with Entities installed was intended behavior. This was apparently 2018, though, so Entities has changed, and QA's attitude about what's a bug has changed (for the better).
     
    MelvMay likes this.
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,396
    So I guess I had one of those moments where I was so caught up trying to solve a problem, I couldn't see the dead obvious solution in front of me.

    To prevent your custom loops from being called post play mode, you just need to reset the state of the player loop when leaving play mode with two measly lines of code:
    Code (CSharp):
    1. PlayerLoopSystem defaultPlayerLoopSystem = PlayerLoop.GetDefaultPlayerLoop();
    2. PlayerLoop.SetPlayerLoop(defaultPlayerLoopSystem);
    Nonetheless, lots of interesting information in this thread. Probably will be useful for some confused chap like myself down the line.
     
    MelvMay likes this.
  9. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,675
    Maybe the intention is to always have the default player loop when exiting play mode although I don't see that mentioned anywhere. Might make sense because otherwise you could seriously hinder the Editor if you were not careful.

    I did a little digging for you but my, the code here is pretty complex. I eventually stumbled on the following C++ which confirms what you said:
    Code (CSharp):
    1. #if USE_MONO_DOMAINS
    2.     GlobalCallbacks::Get().beforeDomainUnload.Register(ResetDefaultPlayerLoop);
    3. #else
    4.     GlobalCallbacks::Get().playerQuit.Register(ResetDefaultPlayerLoop);
    5. #endif
    So both editor/player reset to the default loop yes.

    What makes me curious about the above is whether the fast-play-mode options skip this call because AFAIK the domain wouldn't be unloaded which is why Static state persists in that mode. Worth investigating, even if just as a gotcha check.
     
  10. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,353
    That's actually a very bad idea! I did that, and it broke our game in a very subtle way that was insanely hard to debug. The reason (this time) is that we're both using the new input system and fast enter play mode (ie. no domain reloads). This breaks, as the new input system does this;

    Code (csharp):
    1. static InputSystem()
    2. {
    3.     InitializeInEditor();
    4. }
    And one of the things that happen (quite far down the callstack) in InitializeInEditor is this:

    Code (csharp):
    1.  
    2. // inside NativeInputRuntime, this setter gets called:
    3. public Action onPlayerLoopInitialization
    4. {
    5.     get => m_PlayerLoopInitialization;
    6.     set
    7.     {
    8.         // This is a hot-fix for a critical problem in input system, case 1368559, case 1367556, case 1372830
    9.         // TODO move it to a proper native callback instead
    10.         if (value != null)
    11.         {
    12.             // Inject ourselves directly to PlayerLoop.Initialization as first subsystem to run,
    13.             // Use InputSystemPlayerLoopRunnerInitializationSystem as system type
    14.             var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop();
    15.             var initStepIndex = playerLoop.subSystemList.IndexOf(x => x.type == typeof(PlayerLoop.Initialization));
    16.             if (initStepIndex >= 0)
    17.             {
    18.                 var systems = playerLoop.subSystemList[initStepIndex].subSystemList;
    19.  
    20.                 // Check if we're not already injected
    21.                 if (!systems.Select(x => x.type)
    22.                     .Contains(typeof(InputSystemPlayerLoopRunnerInitializationSystem)))
    23.                 {
    24.                     ArrayHelpers.InsertAt(ref systems, 0, new UnityEngine.LowLevel.PlayerLoopSystem
    25.                     {
    26.                         type = typeof(InputSystemPlayerLoopRunnerInitializationSystem),
    27.                         updateDelegate = () => m_PlayerLoopInitialization?.Invoke()
    28.                     });
    29.  
    30.                     playerLoop.subSystemList[initStepIndex].subSystemList = systems;
    31.                     UnityEngine.LowLevel.PlayerLoop.SetPlayerLoop(playerLoop);
    32.                 }
    33.             }
    34.         }
    35.  
    36.         m_PlayerLoopInitialization = value;
    37.     }
    38. }
    So it takes the current player loop, inserts itself into it, and relies on that for a bunch of things to function (in the editor).

    Since that static constructor is called only once for every domain reload, editing the root player loop when leaving play mode will break the new input system, if you don't have domain reload on enter play mode enabled.

    What that looked like for us was that the second time we entered play mode without recompiling, some input things did not work.

    Even if this is fixed down the line (as the @todo there says), there's no guarantee that no other system (either Unity-made or otherwise) doesn't do the same kind of thing. In essence, what you're doing when you're resetting the player loop system to default is to reset a shared global state that other systems may or may not use, without notifying those systems in any way.

    So it's a very, very bad idea!
     
    Whatever560, noio and spiney199 like this.
  11. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,396
    Huh, well that's good to know.

    I admittedly do have domain reloads on when entering playmode, and don't plan to turn that off at any point. So I'll keep the potential side effects in mind should I encounter strage behaviour down the line.
     
  12. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,675
    Just to note that I'm learning stuff here too. :)
     
    NotaNaN likes this.
  13. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,396
    That's why I love theads like these.

    Quite literally just encountered a bug I caused by hooking into this system. Turns out the UniTask package also hooks into this player loop (which is how it lets you write anync code that works nicely with Unity), so a number of my asynchronous methods were not running due to this very reason:
    Code (CSharp):
    1. PlayerLoopSystem currentPlayerLoop = PlayerLoop.GetDefaultPlayerLoop();
    Seems I was overriding the loops the UniTask package was hooking in. So replacing
    GetDefaultPlayerLoop()
    with
    GetCurrentPlayerLoop()
    solved that one for me.
     
  14. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,193
    I just came across this thread and just want to add a possible explanation why the player loop actually runs during edit mode. Anyone remembers the ExecuteInEditMode attribute? It can only work if the basis of the player loop actually runs during edit mode. Most systems of the player loop do not run during edit mode (time does not advance, etc) but some are, just that they usually don't affect the loaded scripts. So when you implement a low level system in the player loop, you probably should simply check the Application.isPlaying property like mentioned above.

    I'm wondering what's the actual purpose of the PlayerLoopSystem.loopConditionFunction. The documentatipn doesn't really say anything about the purpose. Though the name could suggest that it may be related in the process to determine if the system should run / loop or not. A proper documentation would of course help ^^. Though since the whole PlayerLoopSystem is a kinda niche topic that only a few users ever use, there is not much pressure to write a proper documentation for it :)
     
    spiney199 likes this.
  15. ansdyd0803

    ansdyd0803

    Joined:
    Nov 27, 2020
    Posts:
    4
    Hello,
    Thank you for the sample. It's helpful to me very much.

    And I tried to remove unsafe code in the sample script.
    I hope my approach helps some people.

    change

    unsafe
    {
    nativePtr = new IntPtr(*((Int64*)nativePtr.ToPointer()));
    }

    into

    nativePtr = Marshal.ReadIntPtr(nativePtr);
     
    Last edited: Apr 26, 2023
    MelvMay likes this.
  16. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,396
    Oh hey, this old thread.

    For what it's worth, I ended up making a wrapper player loop system:
    Code (CSharp):
    1. namespace LizardBrainGames.LowLevel
    2. {
    3.     using System;
    4.     using UnityEngine;
    5.     using UnityEngine.LowLevel;
    6.    
    7.     /// <summary>
    8.     /// Base class for custom player loop types. Will add itself into the existing player loop system when it has any
    9.     /// subscribers, and remove itself when it no longer has any subscribers.
    10.     /// </summary>
    11.     /// <typeparam name="T">The type of existing <see cref="UnityEngine.PlayerLoop"/> <see cref="PlayerLoopSystem"/>
    12.     /// that this system will be added into.</typeparam>
    13.     /// <remarks>
    14.     /// These systems will only operate in play mode, and will reset themselves if active when play mode is exited.
    15.     /// </remarks>
    16.     public abstract class LizardPlayerLoopSystem<T>
    17.     {
    18.         #region Internal Members
    19.  
    20.         private int _loopSubCount = 0;
    21.  
    22.         private PlayerLoopSystem _playerLoopSystem;
    23.  
    24.         #endregion
    25.  
    26.         #region Constructor
    27.  
    28.         public LizardPlayerLoopSystem()
    29.         {
    30.             _playerLoopSystem = new PlayerLoopSystem()
    31.             {
    32.                 updateDelegate = CallSystemLoop,
    33.                 type = this.GetType()
    34.             };
    35.         }
    36.  
    37.         #endregion
    38.  
    39.         #region Delegates/Events
    40.  
    41.         public event Action LoopCallback
    42.         {
    43.             add
    44.             {
    45.                 if (_loopSubCount == 0)
    46.                 {
    47.                     AddSystemIntoPlayerLoop();
    48.                 }
    49.  
    50.                 _loopSubCount++;
    51.                 LoopCallbackInternal += value;
    52.             }
    53.             remove
    54.             {
    55.                 _loopSubCount--;
    56.                 LoopCallbackInternal -= value;
    57.  
    58.                 if (_loopSubCount == 0)
    59.                 {
    60.                     RemoveSystemFromPlayerLoop();
    61.                 }
    62.             }
    63.         }
    64.  
    65.         private event Action LoopCallbackInternal;
    66.  
    67.         #endregion
    68.  
    69.         #region System Methods
    70.  
    71.         private void CallSystemLoop()
    72.         {
    73.             if (Application.isPlaying)
    74.             {
    75.                 LoopCallbackInternal?.Invoke();
    76.             }
    77.         }
    78.  
    79.         private void AddSystemIntoPlayerLoop()
    80.         {
    81.             PlayerLoopSystem rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop();
    82.             PlayerLoopUtilities.AddPlayerLoopToSubSystem<T>(ref rootPlayerLoop, _playerLoopSystem);
    83.             PlayerLoop.SetPlayerLoop(rootPlayerLoop);
    84.  
    85. #if UNITY_EDITOR
    86.             UnityEditor.EditorApplication.playModeStateChanged += (state) =>
    87.             {
    88.                 if (_loopSubCount > 0 && state == UnityEditor.PlayModeStateChange.ExitingPlayMode)
    89.                 {
    90.                     RemoveSystemFromPlayerLoop();
    91.                     _loopSubCount = 0;
    92.                     LoopCallbackInternal = null;
    93.                 }
    94.             };
    95. #endif
    96.         }
    97.  
    98.         private void RemoveSystemFromPlayerLoop()
    99.         {
    100.             PlayerLoopSystem rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop();
    101.             PlayerLoopUtilities.RemoveSubSystemFromPlayerLoop<T>(ref rootPlayerLoop, this.GetType());
    102.             PlayerLoop.SetPlayerLoop(rootPlayerLoop);
    103.         }
    104.  
    105.         #endregion
    106.     }
    107. }
    Idea being when something subscribes to it, it engages itself into the player loop, and disengages when nothing is subscribed.

    I just declare some types for the main player loop types:
    Code (CSharp):
    1. namespace LizardBrainGames.LowLevel
    2. {
    3.     using UnityEngine.PlayerLoop;
    4.  
    5.     public class LizardEarlyUpdate : LizardPlayerLoopSystem<EarlyUpdate> { }
    6.  
    7.     public class LizardUpdate : LizardPlayerLoopSystem<Update> { }
    8.  
    9.     public class LizardFixedUpdate : LizardPlayerLoopSystem<FixedUpdate> { }
    10.  
    11.     public class LizardPreLateUpdate : LizardPlayerLoopSystem<PreLateUpdate> { }
    12. }
    And then throw in some static instances:
    Code (CSharp):
    1. namespace LizardBrainGames
    2. {
    3.     using LizardBrainGames.LowLevel;
    4.  
    5.     /// <summary>
    6.     /// Use this class to hook into various stages of the player loop without requiring monobehaviours.
    7.     /// </summary>
    8.     public static class LizardPlayerLoop
    9.     {
    10.         #region Internal Members
    11.  
    12.         private static readonly LizardEarlyUpdate _earlyUpdate = new();
    13.  
    14.         private static readonly LizardUpdate _update = new();
    15.  
    16.         private static readonly LizardFixedUpdate _fixedUpdate = new();
    17.  
    18.         private static readonly LizardPreLateUpdate _preLateUpdate = new();
    19.  
    20.         #endregion
    21.  
    22.         #region Properties
    23.  
    24.         public static LizardEarlyUpdate EarlyUpdate => _earlyUpdate;
    25.  
    26.         public static LizardUpdate Update => _update;
    27.  
    28.         public static LizardFixedUpdate FixedUpdate => _fixedUpdate;
    29.  
    30.         public static LizardPreLateUpdate PreLateUpdate => _preLateUpdate;
    31.  
    32.         #endregion
    33.     }
    34. }
    And now your non-monobehaviour systems can tie into the player loop nice and easily.
     
  17. ansdyd0803

    ansdyd0803

    Joined:
    Nov 27, 2020
    Posts:
    4
    Thank you for your reply!
    I think It's a good wrapper and good utility.
    And please share your `PlayerLoopUtilites` class.
     
  18. If you're interested in another solution, feel free to take a look and use it if you like it:

    https://github.com/LurkingNinja/com.lurking-ninja.player-loop