Hey all, so I needed a way to call Invoke() with a time delay that isn’t affected by Time.timeScale. To do this, I wrote the following helper class:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public delegate void Invokable();
public static class Common {
private struct InvokableItem
{
public Invokable func;
public float executeAtTime;
public InvokableItem(Invokable func, float delaySeconds)
{
this.func = func;
this.executeAtTime = Time.realtimeSinceStartup + delaySeconds;
}
}
static List<InvokableItem> invokeList = new List<InvokableItem>();
static List<InvokableItem> invokeListPendingAddition = new List<InvokableItem>();
static List<InvokableItem> invokeListExecuted = new List<InvokableItem>();
/// <summary>
/// Invokes the function with a time delay. This is NOT
/// affected by timeScale like the Invoke function in Unity.
/// </summary>
/// <param name='func'>
/// Function to invoke
/// </param>
/// <param name='delaySeconds'>
/// Delay in seconds.
/// </param>
public static void InvokeDelayed(Invokable func, float delaySeconds)
{
invokeListPendingAddition.Add(new InvokableItem(func, delaySeconds));
}
// must be maanually called from a game controller or something similar every frame
public static void Update()
{
// Copy pending additions into the list (Pending addition list
// is used because some invokes add a recurring invoke, and
// this would modify the collection in the next loop,
// generating errors)
foreach(InvokableItem item in invokeListPendingAddition)
{
invokeList.Add(item);
}
invokeListPendingAddition.Clear();
// Invoke all items whose time is up
foreach(InvokableItem item in invokeList)
{
if(item.executeAtTime <= Time.realtimeSinceStartup)
{
if(item.func != null)
item.func();
invokeListExecuted.Add(item);
}
}
// Remove invoked items from the list.
foreach(InvokableItem item in invokeListExecuted)
{
invokeList.Remove(item);
}
invokeListExecuted.Clear();
}
}
In my game controller script (a monoBehavior), I call the following code in the update function to process any pending invokes:
Common.Update()
My game controller script also calls the InvokeDelayed() function in one of its instance methods, reinvoking itself:
function FindNearestDock()
{
// this == null here, but only sometimes, typically a few seconds after reloading the game
Common.InvokeDelayed(new Invokable(this.FindNearestDock), 1); // recall this function every second
//Invoke("FindNearestDock", 1); // old code that works fine!
}
Does anyone know why FindNearestDock() is occasionally being called on a non-instance object, meaning that “this” is null, like its trying to call it as a static function?
Thank you for the quick reply. Clearing the static lists on startup did seem to fix the issue!!! But, now I’m confused I also tried using a singleton version of this script where the lists were not static, and I had the same issues, so i didn’t suspect the static lists were a problem. Here’s my class in its singleton form:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public delegate void Invokable();
public class Common {
private struct InvokableItem
{
public Invokable func;
public float executeAtTime;
public InvokableItem(Invokable func, float delaySeconds)
{
this.func = func;
this.executeAtTime = Time.realtimeSinceStartup + delaySeconds;
}
}
private static Common _instance = null;
public static Common Instance
{
get
{
if( _instance == null )
_instance = new Common();
return _instance;
}
}
List<InvokableItem> invokeList = new List<InvokableItem>();
List<InvokableItem> invokeListPendingAddition = new List<InvokableItem>();
List<InvokableItem> invokeListExecuted = new List<InvokableItem>();
/// <summary>
/// Invokes the function with a time delay. This is NOT
/// affected by timeScale like the Invoke function in Unity.
/// </summary>
/// <param name='func'>
/// Function to invoke
/// </param>
/// <param name='delaySeconds'>
/// Delay in seconds.
/// </param>
public void InvokeDelayed(Invokable func, float delaySeconds)
{
invokeListPendingAddition.Add(new InvokableItem(func, delaySeconds));
}
// must be maanually called from a game controller or something similar every frame
public void Update()
{
// Copy pending additions into the list (Pending addition list
// is used because some invokes add a recurring invoke, and
// this would modify the collection in the next loop,
// generating errors)
foreach(InvokableItem item in invokeListPendingAddition)
{
invokeList.Add(item);
}
invokeListPendingAddition.Clear();
// Invoke all items whose time is up
foreach(InvokableItem item in invokeList)
{
if(item.executeAtTime <= Time.realtimeSinceStartup)
{
if(item.func != null)
item.func();
invokeListExecuted.Add(item);
}
}
// Remove invoked items from the list.
foreach(InvokableItem item in invokeListExecuted)
{
invokeList.Remove(item);
}
invokeListExecuted.Clear();
}
}
I was then calling Common.Instance.InvokeDelayed(…), and Common.Instance.Update() to use it as a singleton.
Is this possibly a problem because _instance itself is a static and needs to be nullified or something? If nothing else, you helped me get up and running, so thanks for that!! I’m just trying to learn what was wrong with my singleton now
Correct. I have a lot of things in my project that go very unhappy when i let unity recompile at runtime, so I just don’t do that. I always restart the game after code changes.
At the moment you only check if the delegate object is not null before calling it. I think you should also be checking that the delegate’s target is not null because that may have been destroyed since the delegate was added to your list.
I usually make my singletons inherit MonoBehaviour and place them in a GameObject. Like that I have a little more direct control. E.g. in your scenario, you would only need to implement OnLevelWasLoaded and clear the lists in there.
Possibly. What happens when a component issues an invoke and then something else happens that means the component should be destroyed (eg. something based on user input). Is it up to the component to make sure it doesn’t destroy itself until the invoke happens? Should there be a way for the component to remove it’s invoke from the list before it’s destroyed? Should these components only be destroyed at fixed times when it’s safe? IMHO all of these would make the system harder to use.
For me there is no question that there should be a manual deletion of all pending invokes when a component is destroyed. That’s pretty much a no brainer. You have to add this deletion in OnDestroy. This avoids lots of maintenance in the long run.
Otherwise you may be in the situation that you know, I started this invoke, why did nothing happen? You may look at many different places why it is not executed. Whereas if you use the deletion function in OnDestroy, you will directly notice that this is related to the invoke and clears everything because it is destroyed.
The check for null was just there because I had read about doing that, not necessarily because it was needed by me. As far as checking the target, with the singleton method, the target should always be valid. Its a game controller script monobehavior in my scene, and it was never destroyed since the singleton instance was created (so the list in the instance should never have a null target).
I do keep a static var instance : GameControllerScript in my game controller for easy access everywhere else in code, is this possibly still running after a reload (along with a new instance that overwrites the variable?) Maybe that old instance is adding it’s invokes, then being garbage collected?
I guess I don’t know much about statics and how they survive across launches.
Your singleton will persist across play sessions (until Unity recompiles the assemblies) just like the static lists would unless you let Unity manage it (by turning it into a MonoBehaviour or ScriptableObject). Personally, I use MonoBehaviours so that I can easily visualize “Oh, holy crap - there are two WhateverManager’s in the scene… that shouldn’t happen!”
Generally, from what I can tell, Unity only reinitializes static variables/objects when it recompiles (or when the app is relaunched outside of the editor).