Prevent Custom Update Loop running post playmode

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:

namespace TheFault.Core.Time
{
    using System;
    using UnityEngine;
    using UnityEngine.LowLevel;
    using UnityEngine.PlayerLoop;
    using LizardBrainGames.LowLevel;

#if UNITY_EDITOR
    using UnityEditor;
#endif

    public static class TheFaultGameTimeLoop
    {
        #region Initialisation       

        [RuntimeInitializeOnLoadMethod]
        private static void InitialiseGameTimeLoop()
        {
            PlayerLoopSystem currentPlayerLoop = PlayerLoop.GetDefaultPlayerLoop();

            PlayerLoopSystem gameTimeTickLoop = new PlayerLoopSystem()
            {
                updateDelegate = GameTimeTickLoop,
                type = typeof(GameTimeTickType)
            };

            UnityEnginePlayerLoopUtilities.AddPlayerLoopToSubSystem<Update>(ref currentPlayerLoop, gameTimeTickLoop);

            PlayerLoop.SetPlayerLoop(currentPlayerLoop);

           Application.quitting += () =>
           {
               OnGameTimeTick = null;
           };
        }

        #endregion

        #region Update Struct Types

        public struct GameTimeTickType { }

        #endregion

        #region Internal Properties

       private static bool CanTickGameTime
       {
           get
           {
#if UNITY_EDITOR
               if (EditorApplication.isPlaying == false) return false;

               if (EditorApplication.isPaused == true) return false;
#endif
               return true;
           }
       }

        #endregion

        #region Game Time Global Delegates

        public static event Action OnGameTimeTick;

        #endregion

        #region Game Time Loop

        private static void GameTimeTickLoop()
        {
            if (CanTickGameTime == false) return;

            Debug.Log("Game Time Tick!");
            //Call 'OnGameTimeTick' every 1 second
        }

        #endregion
    }
}

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.

1 Like

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 :stuck_out_tongue:

3 Likes

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.

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.

using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;

public static class MyPlayerLoop
{
    delegate void NativeFunction();
    private static NativeFunction m_NativeFixedUpdateFunction;
    private static float m_CallPeriod = 1f;
    private static float m_CurrentTime;

    private static void CustomFixedUpdate()
    {
        // Our custom timer.
        m_CurrentTime += Time.fixedDeltaTime;
        if (m_CurrentTime > m_CallPeriod)
        {
            m_CurrentTime -= m_CallPeriod;
            Debug.Log("Custom update tick!");
        }
  
        // Call the original native fixed-update function
        m_NativeFixedUpdateFunction.Invoke();
    }

    [RuntimeInitializeOnLoadMethod]
    private static void AppStart()
    {
        var defaultSystems = PlayerLoop.GetDefaultPlayerLoop();
        var customUpdate = new PlayerLoopSystem()
        {
            updateDelegate = CustomFixedUpdate,
            type = typeof(MyPlayerLoop)
        };
        var nativePtr = ReplaceSystem<FixedUpdate.ScriptRunBehaviourFixedUpdate>(ref defaultSystems, customUpdate);

        PlayerLoop.SetPlayerLoop(defaultSystems);
  
        if (nativePtr != IntPtr.Zero)
        {
            unsafe
            {
                nativePtr = new IntPtr(*((Int64*)nativePtr.ToPointer()));
            }
            m_NativeFixedUpdateFunction = Marshal.GetDelegateForFunctionPointer(nativePtr, typeof(NativeFunction)) as NativeFunction;
        }
    }
 
    private static IntPtr ReplaceSystem<T>(ref PlayerLoopSystem system, PlayerLoopSystem replacement)
    {
        if (system.type == typeof(T))
        {
            var nativeUpdatePtr = system.updateFunction;
            system = replacement;
            return nativeUpdatePtr;
        }

        if (system.subSystemList == null)
            return IntPtr.Zero;
  
        for (var i = 0; i < system.subSystemList.Length; i++)
        {
            var nativeUpdatePtr = ReplaceSystem<T>(ref system.subSystemList[i], replacement);
            if (nativeUpdatePtr == IntPtr.Zero)
                continue;

            return nativeUpdatePtr;
        }

        return IntPtr.Zero;
    }
}
2 Likes

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.

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:

static bool FixedUpdateCondition()
{
    return GetTimeManager().StepFixedTime();
}

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.

1 Like

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

1 Like

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:

PlayerLoopSystem defaultPlayerLoopSystem = PlayerLoop.GetDefaultPlayerLoop();
PlayerLoop.SetPlayerLoop(defaultPlayerLoopSystem);

Nonetheless, lots of interesting information in this thread. Probably will be useful for some confused chap like myself down the line.

1 Like

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:

#if USE_MONO_DOMAINS
    GlobalCallbacks::Get().beforeDomainUnload.Register(ResetDefaultPlayerLoop);
#else
    GlobalCallbacks::Get().playerQuit.Register(ResetDefaultPlayerLoop);
#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.

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;

static InputSystem()
{
    InitializeInEditor();
}

And one of the things that happen (quite far down the callstack) in InitializeInEditor is this:

// inside NativeInputRuntime, this setter gets called:
public Action onPlayerLoopInitialization
{
    get => m_PlayerLoopInitialization;
    set
    {
        // This is a hot-fix for a critical problem in input system, case 1368559, case 1367556, case 1372830
        // TODO move it to a proper native callback instead
        if (value != null)
        {
            // Inject ourselves directly to PlayerLoop.Initialization as first subsystem to run,
            // Use InputSystemPlayerLoopRunnerInitializationSystem as system type
            var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop();
            var initStepIndex = playerLoop.subSystemList.IndexOf(x => x.type == typeof(PlayerLoop.Initialization));
            if (initStepIndex >= 0)
            {
                var systems = playerLoop.subSystemList[initStepIndex].subSystemList;

                // Check if we're not already injected
                if (!systems.Select(x => x.type)
                    .Contains(typeof(InputSystemPlayerLoopRunnerInitializationSystem)))
                {
                    ArrayHelpers.InsertAt(ref systems, 0, new UnityEngine.LowLevel.PlayerLoopSystem
                    {
                        type = typeof(InputSystemPlayerLoopRunnerInitializationSystem),
                        updateDelegate = () => m_PlayerLoopInitialization?.Invoke()
                    });

                    playerLoop.subSystemList[initStepIndex].subSystemList = systems;
                    UnityEngine.LowLevel.PlayerLoop.SetPlayerLoop(playerLoop);
                }
            }
        }

        m_PlayerLoopInitialization = value;
    }
}

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!

3 Likes

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.

Just to note that I’m learning stuff here too. :slight_smile:

1 Like

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:

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.

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 :slight_smile:

1 Like

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);

1 Like

Oh hey, this old thread.

For what it’s worth, I ended up making a wrapper player loop system:

namespace LizardBrainGames.LowLevel
{
    using System;
    using UnityEngine;
    using UnityEngine.LowLevel;
   
    /// <summary>
    /// Base class for custom player loop types. Will add itself into the existing player loop system when it has any
    /// subscribers, and remove itself when it no longer has any subscribers.
    /// </summary>
    /// <typeparam name="T">The type of existing <see cref="UnityEngine.PlayerLoop"/> <see cref="PlayerLoopSystem"/>
    /// that this system will be added into.</typeparam>
    /// <remarks>
    /// These systems will only operate in play mode, and will reset themselves if active when play mode is exited.
    /// </remarks>
    public abstract class LizardPlayerLoopSystem<T>
    {
        #region Internal Members

        private int _loopSubCount = 0;

        private PlayerLoopSystem _playerLoopSystem;

        #endregion

        #region Constructor

        public LizardPlayerLoopSystem()
        {
            _playerLoopSystem = new PlayerLoopSystem()
            {
                updateDelegate = CallSystemLoop,
                type = this.GetType()
            };
        }

        #endregion

        #region Delegates/Events

        public event Action LoopCallback
        {
            add
            {
                if (_loopSubCount == 0)
                {
                    AddSystemIntoPlayerLoop();
                }

                _loopSubCount++;
                LoopCallbackInternal += value;
            }
            remove
            {
                _loopSubCount--;
                LoopCallbackInternal -= value;

                if (_loopSubCount == 0)
                {
                    RemoveSystemFromPlayerLoop();
                }
            }
        }

        private event Action LoopCallbackInternal;

        #endregion

        #region System Methods

        private void CallSystemLoop()
        {
            if (Application.isPlaying)
            {
                LoopCallbackInternal?.Invoke();
            }
        }

        private void AddSystemIntoPlayerLoop()
        {
            PlayerLoopSystem rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop();
            PlayerLoopUtilities.AddPlayerLoopToSubSystem<T>(ref rootPlayerLoop, _playerLoopSystem);
            PlayerLoop.SetPlayerLoop(rootPlayerLoop);

#if UNITY_EDITOR
            UnityEditor.EditorApplication.playModeStateChanged += (state) =>
            {
                if (_loopSubCount > 0 && state == UnityEditor.PlayModeStateChange.ExitingPlayMode)
                {
                    RemoveSystemFromPlayerLoop();
                    _loopSubCount = 0;
                    LoopCallbackInternal = null;
                }
            };
#endif
        }

        private void RemoveSystemFromPlayerLoop()
        {
            PlayerLoopSystem rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop();
            PlayerLoopUtilities.RemoveSubSystemFromPlayerLoop<T>(ref rootPlayerLoop, this.GetType());
            PlayerLoop.SetPlayerLoop(rootPlayerLoop);
        }

        #endregion
    }
}

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:

namespace LizardBrainGames.LowLevel
{
    using UnityEngine.PlayerLoop;

    public class LizardEarlyUpdate : LizardPlayerLoopSystem<EarlyUpdate> { }

    public class LizardUpdate : LizardPlayerLoopSystem<Update> { }

    public class LizardFixedUpdate : LizardPlayerLoopSystem<FixedUpdate> { }

    public class LizardPreLateUpdate : LizardPlayerLoopSystem<PreLateUpdate> { }
}

And then throw in some static instances:

namespace LizardBrainGames
{
    using LizardBrainGames.LowLevel;

    /// <summary>
    /// Use this class to hook into various stages of the player loop without requiring monobehaviours.
    /// </summary>
    public static class LizardPlayerLoop
    {
        #region Internal Members

        private static readonly LizardEarlyUpdate _earlyUpdate = new();

        private static readonly LizardUpdate _update = new();

        private static readonly LizardFixedUpdate _fixedUpdate = new();

        private static readonly LizardPreLateUpdate _preLateUpdate = new();

        #endregion

        #region Properties

        public static LizardEarlyUpdate EarlyUpdate => _earlyUpdate;

        public static LizardUpdate Update => _update;

        public static LizardFixedUpdate FixedUpdate => _fixedUpdate;

        public static LizardPreLateUpdate PreLateUpdate => _preLateUpdate;

        #endregion
    }
}

And now your non-monobehaviour systems can tie into the player loop nice and easily.

4 Likes

Thank you for your reply!
I think It’s a good wrapper and good utility.
And please share your PlayerLoopUtilites class.

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

4 Likes