Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Voting for the Unity Awards are OPEN! We’re looking to celebrate creators across games, industry, film, and many more categories. Cast your vote now for all categories
    Dismiss Notice
  3. Dismiss Notice

Custom callback singleton, instead of Monobehavior.Update - better performance

Discussion in 'Scripting' started by IgorAherne, May 26, 2018.

  1. IgorAherne

    IgorAherne

    Joined:
    May 15, 2013
    Posts:
    383
    Here is a component which allows you to Register for Update

    As you know Monobehavior.Update() is very slow compared to custom callbacks. My 3D game managed to get extra 3 frames on iPhone 6, after I switched to them.

    attach this component on an object in scene.

    In unity, go into "Edit->Project Settings->ScriptExecution Order" add Core.cs, then set its compilation time to something like -5000, so it comes first on the list.


    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Collections;
    4. using System.Linq;
    5.  
    6.  
    7. // It acts as a singleton, allowing classes to register for their Update() to be invoked even
    8. // if they don't inherit from Monobehavior.
    9. //  YOU MUST MANUALLY CREATE AN INSTANCE OF THIS CLASS IN EVERY SCENE WHICH USES IT
    10. //
    11. // NOTICE: In unity, go into "Edit->Project Settings->ScriptExecution Order" add Core.cs, then
    12. // set its compilation time to something like -5000, so it comes first on the list.
    13. //
    14. //It also sets initial settings, like target framerate, etc.
    15. //
    16. // NOTICE - DON'T ADD ANY HARD-CODED CALLBACKS HERE.
    17. // CORE CAN BE IN ANY SCENE, EVEN WHERE YOUR OBJECTS CAN BE ABSENT.
    18. // INSTEAD, CREATE THEIR OWN "INITIALIZER" MONOBEHAVIOR, AND PLACE IT IN SCENE
    19. //
    20. // PUT ME IN CREDITS MATE :D
    21. //
    22. // Igor Aherne  May 2018  facebook.com/igor.aherne
    23. public class Core : MonoBehaviour {
    24.  
    25.  
    26. #region in-editor debugging
    27. #if UNITY_EDITOR
    28.     public static bool stacktraceToFile() {
    29.         System.IO.File.AppendAllText("Y:\\unity error logging temp\\FindObjectsOfTypeLog.txt",
    30.                                      "\n\nnew envocation: " + get_stacktrace());
    31.         return true;
    32.     }
    33.  
    34.  
    35.     public static string get_stacktrace() {
    36.  
    37.         string[] excludeTheseFunctions = new string[] {
    38.             "stacktraceToFile",
    39.             "get_stacktrace"
    40.         };
    41.  
    42.         var stackFrames = new System.Diagnostics.StackTrace(true)
    43.                                                 .GetFrames()
    44.                                                 .Where(frame => excludeTheseFunctions.Contains(frame.GetMethod().Name) == false);
    45.         string stacktrace = "";
    46.  
    47.         foreach(var stackFrame in stackFrames) {
    48.             stacktrace += "\n" + stackFrame.GetType() +"." + stackFrame.GetMethod() + ",  (at " + stackFrame.GetFileName() +", " + stackFrame.GetFileLineNumber();
    49.         }
    50.         return stacktrace;
    51.     }
    52. #endif
    53. #endregion
    54.  
    55. #region singleton stuff
    56.     private static Core _instance;
    57.     public static Core instance {
    58.         get { return _instance; }//end get
    59.     }
    60.  
    61.  
    62.     private void Awake() {
    63.         if(_instance != null) {
    64.             #if UNITY_EDITOR
    65.                 MiscTools.Editor_Misc.printf_inEditor_duplicateSingleton_Removed( this,
    66.                                                                                   _instance.gameObject );
    67.             #endif
    68.             DestroyImmediate(this.gameObject);
    69.             return;
    70.         }
    71.         _instance = this;
    72.         // not really needed (as every redundant core will be deleted anyway from this Awake(),
    73.         // but why not to keep the same core? :)  Don't destroy it when changin levels. Static _instance won't
    74.         // loose its value, so it's safe:
    75.         transform.SetParent(null); //make a root, so DontDestroyOnLoad dones't complain in xCode
    76.         DontDestroyOnLoad(this);
    77.     }
    78. #endregion
    79.  
    80.  
    81.  
    82.     //is the whole application shutting down
    83.     private static bool _isShuttingDownPlay = false;
    84.  
    85.     //tells anyone in our game if the .exe is shutting down.
    86.     //calling this inside of Singleton's instance getter will allow to avoid unity errors
    87.     // "some objects were created inside OnDestroy()"
    88.     public static bool isShuttingDown() {
    89.  
    90.        #if UNITY_EDITOR == true
    91.            if (UnityEditor.EditorApplication.isCompiling) {
    92.                return true;
    93.            }
    94.        #endif
    95.        return _isShuttingDownPlay;
    96.     }
    97.  
    98.  
    99.     //invoked by unity when the whole .exe is shutting down:
    100.     public void OnApplicationQuit() {
    101.         _isShuttingDownPlay = true;
    102.     }
    103.  
    104.  
    105.  
    106. #region callbacks
    107.     // Important to be static. This helps to avoid issues where Core is already null, but attempt to unregister is issued.
    108.     // Object is the trustedObj of the function.
    109.     private static Dictionary<ZeroArgFunctionPointer, UnityEngine.Object> updates_to_invoke  = new Dictionary<ZeroArgFunctionPointer, UnityEngine.Object>();
    110.     private static Dictionary<ZeroArgFunctionPointer, UnityEngine.Object> updates_to_insert  = new Dictionary<ZeroArgFunctionPointer, UnityEngine.Object>();
    111.  
    112.     // NOTICE - will be cleared in the begining of Update,  to ensure nobody alters our callback collection
    113.     // while we are iterating over the callbacks.
    114.     private static HashSet<ZeroArgFunctionPointer> updates_to_remove = new HashSet<ZeroArgFunctionPointer>();
    115.  
    116.     //any subscriber will receive a callback when scene needs restart (player died, etc).
    117.     //Object is used to prevent calling function on an object already destroyed by Unity.
    118.     private static Dictionary<ZeroArgFunctionPointer, UnityEngine.Object> _OnRestartTheScene
    119.                                                                 = new Dictionary<ZeroArgFunctionPointer, UnityEngine.Object>();
    120.  
    121.     //callbacks to invoke, when the game is paused/unpaused.  Argument is 'false' if the game is no longer paused
    122.     private static Dictionary<BoolArgFunctionPointer, UnityEngine.Object> _OnPaused
    123.                                                                 = new Dictionary<BoolArgFunctionPointer, UnityEngine.Object>();
    124.     // NOTICE - will be cleared in the begining of Update,  to ensure nobody alters our callback collection
    125.     // while we are iterating over the callbacks.
    126.     private static HashSet<BoolArgFunctionPointer> onPaused_toRemove = new HashSet<BoolArgFunctionPointer>();
    127.  
    128.  
    129.     struct data_invokeAfter {
    130.         public ZeroArgFunctionPointer _callback;
    131.         public float _invoke_time; //when to invoke (a specific time)
    132.         public UnityEngine.Object _trustedObj;//to check if it's null (was destroyed). If yes, we will forget this callback
    133.     }
    134.  
    135.  
    136.     // NOTICE: we will check if 'trustedObj' is 'null' before invoking function.
    137.     // If object is 'null', we will discard it.
    138.     //
    139.     // NOTICE: if your 'trustedObj' will become 'null', we will auto-remove it during LateUpdate
    140.     //
    141.     // both non-monobehavior AND monobehavior-extending classes can "subscribe" for updates to be invoked in their functions,
    142.     // through this delegate. It's faster than unity's "magical", behind-the-scenes invocation of Update.
    143.     // Duplicates will be auto-ignored.
    144.     //
    145.     // NOTICE: important to be static. This helps to avoid issues where Core is already null, but attempt to unregister is issued.
    146.     public static void registerForUpdate(UnityEngine.Object trustedObj, ZeroArgFunctionPointer function) {
    147. #if UNITY_EDITOR
    148.         if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode == false) {
    149.             return;
    150.         }
    151. #endif
    152.         Core.updates_to_insert.Add(function, trustedObj);
    153.         //will be removed from  this.updates_to_invoke,  inside LateUpdate()
    154.     }
    155.  
    156.  
    157.     //for any class that was subscribed to our Update() service via "registerForUpdate()", make sure to
    158.     //call this function BEFORE such an object gets destroyed. Otherwise you will have a memory leak.
    159.     //
    160.     // NOTICE: important to be static. This helps to avoid issues where Core is already null, but attempt to unregister is issued.
    161.     public static void unregisterForUpdate(ZeroArgFunctionPointer function) {
    162.         updates_to_remove.Add(function);
    163.     }
    164.  
    165.  
    166.  
    167.     // NOTICE - if you have c# child entities that need to be notified, it's cleaner to subscribe them
    168.     // separatelly from your parent entity. In other words, each child entity (using its own initialization function) subscribes to Core.
    169.     //
    170.     // NOTICE: if your trustedObj will ever become 'null', we will auto-remove it during LateUpdate
    171.     //
    172.     // Auto-avoids duplicate functions :)
    173.     public static void registerFor_OnRestartScene(UnityEngine.Object trustedObj, ZeroArgFunctionPointer toCall) {
    174.         if(_OnRestartTheScene.ContainsKey(toCall)) {return; }
    175.         _OnRestartTheScene.Add(toCall, trustedObj);
    176.     }
    177.  
    178.  
    179.  
    180.  
    181.     // NOTICE - if you have c# child entities that need to be notified, it's cleaner to subscribe them
    182.     // separatelly from your parent entity. In other words, each child entity (using its own initialization function) subscribes to Core.
    183.     //
    184.     // NOTICE: if your trustedObj will ever become 'null', we will auto-remove it during LateUpdate
    185.     //
    186.     // Auto-avoids duplicate functions :)
    187.     //
    188.     // Argument is 'false' if the game is no longer paused
    189.     public static void registerFor_OnPause(UnityEngine.Object trustedObj, BoolArgFunctionPointer toCall){
    190.         if(_OnPaused.ContainsKey(toCall)) { return; }
    191.         _OnPaused.Add(toCall, trustedObj);
    192.     }
    193.  
    194.  
    195.     public static void unregisterFor_OnPause(BoolArgFunctionPointer func){
    196.         onPaused_toRemove.Add(func);
    197.     }
    198. #endregion callbacks
    199.  
    200.  
    201.     //Invoke this if player died, and wishes to replay the current scene instead of trying some other scene.
    202.     //The Core in turn will issue notification for all appropriate subscribers so they can reset themselves
    203.     public void Restart_CurrentScenes(){
    204.         int i=-1;
    205.         foreach(var kvp in _OnRestartTheScene) {
    206.             ++i;
    207.             UnityEngine.Object trustedObj  = kvp.Value;
    208.             if(trustedObj == null) { continue; }
    209.  
    210.             kvp.Key();
    211.         }//end foreach
    212.     }
    213.  
    214.  
    215.  
    216.     static bool _isPaused = false;
    217.     public static bool isPaused {get {return _isPaused; } }
    218.  
    219.     // doesn't set the timescale to zero.  Instead, issues callbacks to subscribers, so they will decide what to do.
    220.     public static void PauseGame(bool isGamePaused){
    221.         if(isGamePaused) {
    222.             //setting to pure zero would create issues where UI-animation functions are not working:
    223.             Time.timeScale = 0.0001f;
    224.         }
    225.         else {  Time.timeScale = 1.0f;  }
    226.  
    227.         _isPaused = isGamePaused; //REFACTOR NOTICE:  not using "isPaused",  to avoid name clash with member variable
    228.  
    229.         foreach(var kvp in _OnPaused) {
    230.             UnityEngine.Object trustedObj  = kvp.Value;
    231.             if(trustedObj == null) { continue; } //NOTICE - entries with null will be removed during update, in one sweep.
    232.             kvp.Key(isPaused);
    233.         }//end foreach
    234.     }
    235.  
    236.  
    237. #region particle helper functions
    238.     //toggle emission on GameObject and all sub-children
    239.     public void ToggleEmission(GameObject go, bool isOn,  float delay) {
    240.         Core.instance.StartCoroutine( ToggleEmissionParticles_delayed(go, isOn, delay) );
    241.     }
    242.  
    243.  
    244.     // 1) waits
    245.     // 2) disables emission of particles (or slowly fades out if fadeOutDuration > 0),
    246.     // 3) waits
    247.     // 4) if destroyDelay > 0  destroys gameObject & children
    248.     // 5) NOTICE - checks if they are already destroyed. So, will be safe regardless :)
    249.     public void DelayDisableParticleEmission_WithDestroy( GameObject go_safeIfNull,
    250.                                                           float disableEmission_delay, //disable emission after waiting a time (measured from the moment of invocation)
    251.                                                           float OnEmissionDisabled_destroyDelay_orNegativeToSurvive) {
    252.  
    253.         Core.instance.StartCoroutine(disableParticlesEmissionWithDestroy( go_safeIfNull,
    254.                                                                           disableEmission_delay,
    255.                                                                           OnEmissionDisabled_destroyDelay_orNegativeToSurvive ) );
    256.     }
    257.  
    258.  
    259.  
    260.     //fades out ENTIRE system.  Will work if shader has  _Color  or  _TintColor  property.
    261.     //You  can provide optional function to be called when everything is done.  For example:
    262.     //
    263.     // FadeParticleSystem_WithCallback( myGo,                          callback
    264.     //                                  1,    //<---delay b4 fade            |
    265.     //                                  1.5,  //<---how long to fade,        V
    266.     //                                  ()=>{Core.instance.DelayDisableParticleEmission_WithDestroy(myGO, 0, 0)}  );
    267.     //
    268.     // NOTICE: will make a new instance of material, no longer will be using shared material. But it probably
    269.     // doesn't matter for particle system anyway
    270.     //
    271.     // NOTICE: if delay is provided, then will OPTIMISE component-searching PERFORMANCE by getting references over 'this' and 'next' frame.
    272.     public void FadeParticleSystem_WithCallback( GameObject go_safeIfNull,
    273.                                                  float destinationAlpha,
    274.                                                  float waitBeforeStartFading = 0,
    275.                                                  float fadingDuration = 0.75f,
    276.                                                  bool useUnscaledTime = false,
    277.                                                  System.Action functionToCall_whenComplete = null) {
    278.  
    279.        Core.instance.StartCoroutine(fadeParticleSystem_WithCallback( go_safeIfNull,
    280.                                                                      destinationAlpha,
    281.                                                                      waitBeforeStartFading,
    282.                                                                      fadingDuration,
    283.                                                                      useUnscaledTime,
    284.                                                                      functionToCall_whenComplete) );
    285.     }
    286.  
    287.  
    288.  
    289.     //called by  void DelayDisableParticleEmission_WithDestroy()
    290.     IEnumerator disableParticlesEmissionWithDestroy( GameObject go_safeIfNull,
    291.                                                      float disableEmission_delay,
    292.                                                      float OnEmissionDisabled_destroyDelay_orNegativeToSurvive) {
    293.  
    294.         yield return Core.instance.StartCoroutine(ToggleEmissionParticles_delayed(go_safeIfNull,
    295.                                                                                   false,
    296.                                                                                   disableEmission_delay));
    297.  
    298.         if(OnEmissionDisabled_destroyDelay_orNegativeToSurvive < 0) {
    299.             yield break;//user doesn't want us to destroy the gameObject
    300.         }
    301.  
    302.         yield return new WaitForSeconds(OnEmissionDisabled_destroyDelay_orNegativeToSurvive);
    303.         if(go_safeIfNull != null) {
    304.             //check if something has already destroy the gameObject
    305.             Destroy(go_safeIfNull);
    306.         }
    307.     }
    308.  
    309.  
    310.  
    311.      IEnumerator ToggleEmissionParticles_delayed( GameObject go_safeIfNull,
    312.                                                   bool isOn,
    313.                                                   float disableEmission_delay) {
    314.         if(go_safeIfNull == null) {
    315.             yield break;
    316.         }
    317.  
    318.         var childrenParticles = go_safeIfNull.GetComponentsInChildren<ParticleSystem>();
    319.  
    320.         if (disableEmission_delay > 0) {
    321.             yield return new WaitForSeconds(disableEmission_delay);
    322.         }
    323.  
    324.         foreach (var particleSyst in childrenParticles) {
    325.             if(particleSyst == null) {
    326.                 continue;
    327.             }
    328.             var emissionModule = particleSyst.emission;
    329.             emissionModule.enabled = isOn;
    330.         }//end foreach
    331.     }
    332.  
    333.  
    334.  
    335.     // called by  void FadeParticleSystem_WithCallback()
    336.     // NOTICE: if delay is provided, then will OPTIMISE component-searching PERFORMANCE by getting references over 'this' and 'next' frame.
    337.     //
    338.     // NOTICE: will make a new instance of material, no longer will be using shared material. But it probably
    339.     // doesn't matter for particle system anyway
    340.     IEnumerator fadeParticleSystem_WithCallback( GameObject go_safeIfNull,
    341.                                                  float destinationAlpha,
    342.                                                  float waitBeforeStartFading = 0,
    343.                                                  float fadingDuration = 0.75f,
    344.                                                  bool useUnscaledTime = false,
    345.                                                  System.Action functionToCall_whenComplete = null ) {
    346.         if(go_safeIfNull == null) {
    347.             yield break;
    348.         }
    349.  
    350.         var childrenParticles = go_safeIfNull.GetComponentsInChildren<ParticleSystem>();
    351.         var childrenRenderers = new List<Renderer>();
    352.  
    353.         #region this frame
    354.             int processedNumber = 0;
    355.  
    356.             //if we have to wait some amount, then we will search half of children's renderers on THIS frame,
    357.             //and remaining half of children's renderers on NEXT frame.
    358.             //Otherwise, if we don't have to wait before starting to fade, we will search for all renderers on this same frame.
    359.             if(waitBeforeStartFading > 0.0f) {
    360.                 processedNumber = (int)(childrenParticles.Length * 0.5f);
    361.  
    362.                 for (int i = 0; i < childrenParticles.Length * 0.5f; ++i) {
    363.                     childrenRenderers.Add(  childrenParticles[i].GetComponent(typeof(Renderer)) as Renderer );
    364.                 }//end for
    365.             }
    366.         #endregion
    367.  
    368.         //wait as required before fading out
    369.         if (waitBeforeStartFading > 0) {
    370.             yield return new WaitForSeconds(waitBeforeStartFading);
    371.         }
    372.  
    373.         #region next (or same frame)
    374.             //get references to the remaining renderers
    375.             for (int i = processedNumber; i < childrenParticles.Length; ++i) {
    376.                 childrenRenderers.Add( childrenParticles[i].GetComponent(typeof(Renderer)) as Renderer );
    377.             }
    378.  
    379.  
    380.             foreach (var renderer  in childrenRenderers) {
    381.                 Core.instance.StartCoroutine(  fadeColor(renderer.material,
    382.                                                destinationAlpha,
    383.                                                fadingDuration,
    384.                                                useUnscaledTime)  );
    385.             }//end foreach
    386.  
    387.             yield return new WaitForSeconds( fadingDuration );
    388.  
    389.             if (functionToCall_whenComplete != null) {
    390.                 functionToCall_whenComplete();
    391.             }
    392.         #endregion
    393.     }
    394.  
    395.  
    396.     IEnumerator fadeColor(Material mat,  float destinationAlpha,  float fadeDuration,  bool useUnscaledTime=false){
    397.         Color color;
    398.  
    399.         bool hasColor = mat.HasProperty("_Color");
    400.         bool hasTintColor = mat.HasProperty("_TintColor");
    401.  
    402.         if(!hasColor  &&  !hasTintColor) { yield break; }
    403.  
    404.         if(hasColor) { color = mat.GetColor("_Color"); }
    405.         else { color = mat.GetColor("_TintColor"); }
    406.         float startAlpha  = color.a;
    407.         float startTime  = useUnscaledTime ? Time.unscaledTime : Time.time;
    408.  
    409.         while(true) {
    410.             float currTime = useUnscaledTime ? Time.unscaledTime : Time.time;
    411.             float lerp01  = (currTime - startTime) / fadeDuration;
    412.             lerp01 = Mathf.Clamp01(lerp01);
    413.  
    414.             color.a = Mathf.Lerp(startAlpha,  destinationAlpha,  lerp01);
    415.             mat.SetColor("_Color", color);
    416.         }//end while
    417.     }
    418. #endregion
    419.  
    420.  
    421.  
    422.  
    423.     //Core's Update is called before anyone else's Update()
    424.     //The first Update is entirely dedicated to initialization (no usual "Update()" callbacks are issued to core's subscribers).
    425.     void Update() {
    426.         // execute updates on all the external functions registered for our updates:
    427.         foreach (var kvp in Core.updates_to_invoke) {
    428.             ZeroArgFunctionPointer function = kvp.Key;
    429.             UnityEngine.Object trustedObj = kvp.Value;
    430.  
    431.             if(trustedObj == null || updates_to_remove.Contains(function) ) {//updates_to_remove is a HashSet
    432.                 updates_to_remove.Add(function);//<--will be removed in LateUpdate, not to modify the collection while iterating over it.
    433.                 continue;
    434.             }
    435.             function();
    436.         }
    437.  
    438.         // NOTICE - DON'T ADD ANY HARD-CODED CALLBACKS HERE.
    439.         // CORE CAN BE IN ANY SCENE, EVEN WHERE YOUR OBJECTS CAN BE ABSENT.
    440.     }//end Update()
    441.  
    442.  
    443.  
    444.  
    445.     void LateUpdate() {
    446.         //insert and remove function pointers to  updates_to_invoke  Dictonary.   NOTICE - doing here, to avoid altering collections while iterating.
    447.         foreach (ZeroArgFunctionPointer func in Core.updates_to_remove) {
    448.             // NOTICE - this.Update() already noticed if some trustedObj are 'null'.  it already added them to this 'updates_to_remove':
    449.             if (Core.updates_to_invoke.ContainsKey(func)) {
    450.                 Core.updates_to_invoke.Remove(func);
    451.             }
    452.         }
    453.  
    454.         foreach (var kvp in Core.updates_to_insert) {
    455.             Core.updates_to_invoke.Add( kvp.Key, //callback
    456.                                         kvp.Value );//'trusted obj' of the callback
    457.         }
    458.  
    459.         //OnRestartScene, auto-remove any null trustedObjs:
    460.         if(_OnRestartTheScene.Any(kvp=>kvp.Value == null)) {
    461.             var toRmv = _OnRestartTheScene.Where(kvp=>kvp.Value == null)
    462.                                           .Select(kvp=>kvp.Key)
    463.                                           .ToList();
    464.             toRmv.ForEach( f=> _OnRestartTheScene.Remove(f) );
    465.         }
    466.  
    467.  
    468.         //OnPaused, auto-remove any null trustedObjs:
    469.         foreach(var kvp in Core._OnPaused) {
    470.             if(kvp.Value == null) {
    471.                 Core.onPaused_toRemove.Add(kvp.Key);
    472.             }
    473.         }
    474.  
    475.         //any functions that were manually requested to be removed (their trustedObj might not yet be null):
    476.         foreach(var func in onPaused_toRemove) {
    477.             if(Core._OnPaused.ContainsKey(func) == false) { continue; }
    478.             Core._OnPaused.Remove(func);
    479.         }
    480.  
    481.         Core.updates_to_insert.Clear();
    482.         Core.updates_to_remove.Clear();
    483.         onPaused_toRemove.Clear();
    484.  
    485.         // NOTICE - DON'T ADD ANY HARD-CODED CALLBACKS HERE.
    486.         // CORE CAN BE IN ANY SCENE, EVEN WHERE YOUR OBJECTS CAN BE ABSENT.
    487.         // INSTEAD, CREATE THEIR OWN "INITIALIZER" MONOBEHAVIOR, AND PLACE IT IN SCENE
    488.     }//end()
    489. }
    490.  
    491.  


    How to use:

    Code (CSharp):
    1. class MyClass{
    2.       void Awake(){
    3.               Core.registerForUpdate(this,  UpdatedByCore);
    4.        }
    5.  
    6.        //will be invoked every frame, by Core.
    7.        //NOTICE:  core will automatically "forget" your function if THIS component gets destroyed,
    8.        //So you don't need to manually 'unregister', although you could :)
    9.        void UpdatedByCore(){
    10.  
    11.        }
    12. }
     
    Last edited: May 26, 2018
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,378
    There's a lot going on in that class.

    I'd like to point a few things out.

    1) public static string get_stacktrace()
    Unity already has StackTraceUtility.ExtractStackTrace which does the same thing.

    2) You could have the singleton lazy instantiate itself the first time it's accessed (maybe through its instance property or something), and use DontDestroyOnLoad to allow it to survive the life of the project. Instead of having to stick an instance into each scene.

    3) predefined delegates like System.Action exist so you don't have to define things like ZeroArgFunctionPointer (also that's a misnomer, function implies the method returns a value. Hence the System.Func delegate type as well).

    4) You could use some sort of 'IUpdatable' interface instead of delegates. This would allow you to have a reference to the object and the function in the same place for testing if destroyed. Reducing the need for the fat dictionary and instead use just a HashSet or something.

    5) Note that everything is unordered since you use HashSet's and Dictionaries. This fact it's unordered is one of the reasons it's technically faster than the unity system. Unity allows for ordering your calls to Update with execution order.

    6) Why does this have stuff for ParticleSystem in it? KISS your class types. This makes your class really difficult to read/maintain. I mean seriously, more than half of this class is dedicated to this.

    7) Speaking of readability... my god that thing is torture on my eyes to read. Again KISS, if you need that much comments to explain what you're doing, there might be an issue. Otherwise known as 'code smell'. It's not necessarily bad, but it points to the potential something is wrong.

    Something a little more trimmed down:
    Code (csharp):
    1.  
    2. public interface IUpdateable
    3. {
    4.     void Update();
    5. }
    6.  
    7. public class GameLoop : MonoBehaviour
    8. {
    9.    
    10.     #region Static Singleton
    11.    
    12.     private static GameLoop _instance;
    13.    
    14.     private void Awake()
    15.     {
    16.         if (_instance != null && this != _instance)
    17.         {
    18.             Destroy(this);
    19.             return;
    20.         }
    21.        
    22.         _instance = this;
    23.     }
    24.    
    25.     #endregion
    26.    
    27.     #region Fields
    28.    
    29.     private static HashSet<IUpdateable> _updateSet = new HashSet<IUpdateable>();
    30.     private static HashSet<IUpdateable> _toAddToUpdate = new HashSet<IUpdateable>();
    31.     private static HashSet<IUpdateable> _toRemoveFromUpdate = new HashSet<IUpdateable>();
    32.     private static bool _inUpdate;
    33.    
    34.     private static System.Action _updateCallback;
    35.    
    36.     #endregion
    37.    
    38.     #region Methods
    39.    
    40.     public static void Init()
    41.     {
    42.         if(_instance != null) return;
    43.        
    44.         var go = new GameObject("GameLoop.Singleton");
    45.         _instance = go.AddComponent<GameLoop>();
    46.         DontDestroyOnLoad(go);
    47.     }
    48.    
    49.     public static void RegisterUpdate(IUpdateable obj)
    50.     {
    51.         if(_instance == null) Init();
    52.        
    53.         if(_inUpdate)
    54.             _toAddToUpdate.Add(obj);
    55.         else
    56.             _updateSet.Add(obj);
    57.     }
    58.    
    59.     public static void RegisterUpdate(System.Action callback)
    60.     {
    61.         if(_instance == null) Init();
    62.        
    63.         _updateCallback += callback;
    64.     }
    65.    
    66.     public static void UnregisterUpdate(IUpdateable obj)
    67.     {
    68.         if(_inUpdate)
    69.             _toRemoveFromUpdate.Add(obj);
    70.         else
    71.             _updateSet.Remove(obj);
    72.     }
    73.    
    74.     public static void UnregisterUpdate(System.Action callback)
    75.     {
    76.         _updateCallback -= callback;
    77.     }
    78.    
    79.     #endregion
    80.    
    81.     #region Messages
    82.    
    83.     private void Update()
    84.     {
    85.         if(_toAddToUpdate.Count > 0)
    86.         {
    87.             foreach(var obj in _toAddToUpdate)
    88.             {
    89.                 _updateSet.Add(obj);
    90.             }
    91.             _toAddToUpdate.Clear();
    92.         }
    93.        
    94.         _inUpdate = true;
    95.         foreach(var obj in _updateSet)
    96.         {
    97.             if(obj is UnityEngine.Object && (obj as UnityEngine.Object) == null)
    98.             {
    99.                 _toRemoveFromUpdate.Add(obj);
    100.                 continue;
    101.             }
    102.            
    103.             obj.Update();
    104.         }
    105.        
    106.         var d = _updateCallback;
    107.         if(d != null) d();
    108.         _inUpdate = false;
    109.        
    110.         if(_toRemoveFromUpdate.Count > 0)
    111.         {
    112.             foreach(var obj in _toRemoveFromUpdate)
    113.             {
    114.                 _updateSet.Remove(obj);
    115.             }
    116.             _toRemoveFromUpdate.Clear();
    117.         }
    118.     }
    119.    
    120.     #endregion
    121.    
    122. }
    123.  
     
    Last edited: May 26, 2018
  3. IgorAherne

    IgorAherne

    Joined:
    May 15, 2013
    Posts:
    383
    Thanks for the heads up, GameLoop.cs looks a lot cleaner!
    Yes, IUpdatable interface looks like a good approach, will use it. But I think checking for 'null' won't work if user decides to put it on a usual c# object. Currently I force UnityEngine.Object as a subscriber, so it can get a fake "null" from unity, but usual object won't.

    Requirement of UnityEngine.Object was very handy when converting all 3rd party code to use my update delegates, couldn't be bothered with OnDestroy() function (had about 600 files to adjust, some of them also had ExecuteInEditMode to watch out for)

    My component was used for a year, so it got cluttered with particle systems. Need to take it out :)
    I remember having issues with lazy instantiation, but that was mostly when I tried to hook up to things during editor, from ScriptableObjects. Issues happened the moment I switched to gamePlay mode ...If I remember correctly.

    Also cheers for StackTraceUtility, didn't know about it

    HEY! I am a fan of
    brackets{
    }

    not brackets
    {

    }

    I am on the Evil side :D
     
    Last edited: May 26, 2018
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,378
    Yes it won't work, which is why I check if it is a UnityEngine.Object first and foremost.

    Thing is why force the user to have to associate it with a UnityEngine.Object?

    If they want to register a none UnityEngine.Object it's obvious that it won't unregister on destroy, because non-UnityEngine.Object's aren't eligible to be destroyed anyways. So clearly you'd have to unregister it.
     
  5. IgorAherne

    IgorAherne

    Joined:
    May 15, 2013
    Posts:
    383
    True!
     
    Last edited: May 30, 2018
  6. petersvp

    petersvp

    Joined:
    Dec 20, 2013
    Posts:
    60
    2021: Now there is [DefaultExecutionOrder(x)] and i use it everywhere