Skip to content

Instantly share code, notes, and snippets.

@ByronMayne
Created November 1, 2017 12:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ByronMayne/5486e58c290ad9f35c7311067c81cfdf to your computer and use it in GitHub Desktop.
Save ByronMayne/5486e58c290ad9f35c7311067c81cfdf to your computer and use it in GitHub Desktop.
Delay Call, A simple to use script used to replace using coroutines to delay functions in Unity.
using System;
using System.Reflection;
using UnityEngine;
public static class DelayCall
{
public delegate void DelayedDelegate();
public delegate void DelayedDelegate<T>(T parameter);
private static int DEFAULT_POOL_SIZE = 5;
private static int POOL_GROWTH_STEP_SIZE = 3;
private static ushort _nextCallID = 0;
private static bool _isSubscribedToUpdate;
private static bool _shouldStaySubscribedToUpdate;
private enum DelayType
{
NotAllocated,
Seconds,
FrameNumber,
}
private struct DelayedCall
{
private static readonly object[] NO_ARGS = new object[0];
private static readonly object[] ONE_ARG = new object[1];
public readonly ushort ID;
public DelayType delayType;
private int _frameNumber;
private float _secondsRemaing;
private MethodInfo _methodInfo;
private object _target;
private object _argument;
private bool _hasArgument;
/// <summary>
/// Creates a new instance of delayed call with a unique id.
/// </summary>
/// <param name="id"></param>
public DelayedCall(ushort id)
{
ID = id;
_secondsRemaing = 0f;
delayType = DelayType.NotAllocated;
_hasArgument = false;
_argument = null;
_target = null;
_methodInfo = null;
_frameNumber = 0;
}
/// <summary>
/// Updates the internal state of the delayed call. Returns true when it's
/// active and false when it's not.
/// </summary>
public bool Tick()
{
switch (delayType)
{
case DelayType.NotAllocated:
// Nothing to do here
return false;
case DelayType.Seconds:
_secondsRemaing -= Time.deltaTime;
if (_secondsRemaing <= 0.0f)
{
Invoke();
Reset();
return false;
}
return true;
case DelayType.FrameNumber:
if (_frameNumber <= Time.frameCount)
{
Invoke();
Reset();
return false;
}
return true;
}
return false;
}
/// <summary>
/// Sets up this delayed call to use seconds.
/// </summary>
public void SetToSeconds(Delegate callback, float seconds, object argument, bool hasArgument)
{
_secondsRemaing = seconds;
delayType = DelayType.Seconds;
_target = callback.Target;
_methodInfo = callback.Method;
_hasArgument = hasArgument;
_argument = argument;
}
/// <summary>
/// Sets up this delayed call to use frames.
/// </summary>
public void SetToFrames(Delegate callback, int frameNumber, object argument, bool hasArgument)
{
_frameNumber = frameNumber;
delayType = DelayType.FrameNumber;
_target = callback.Target;
_methodInfo = callback.Method;
_hasArgument = hasArgument;
_argument = argument;
}
/// <summary>
/// Resets this delegate back to it's default state.
/// </summary>
public void Reset()
{
delayType = DelayType.NotAllocated;
_secondsRemaing = 0.0f;
_frameNumber = 0;
}
/// <summary>
/// Calls the function of our delegate and then resets it.
/// </summary>
private void Invoke()
{
if (_hasArgument)
{
ONE_ARG[0] = _argument;
_methodInfo.Invoke(_target, ONE_ARG);
}
else
{
_methodInfo.Invoke(_target, NO_ARGS);
}
Reset();
}
}
private static DelayedCall[] _delayedCalls;
/// <summary>
/// Invoked the first frame that someone reference this class.
/// </summary>
static DelayCall()
{
// Create our new array with our default size.
_delayedCalls = new DelayedCall[DEFAULT_POOL_SIZE];
// Loop over an initialize them all
for (int i = 0; i < DEFAULT_POOL_SIZE; i++)
{
_delayedCalls[i] = new DelayedCall(_nextCallID);
_nextCallID++;
}
}
/// <summary>
/// Takes a function and invokes it on the following frame.
/// </summary>
/// <param name="delayedCall">The function you want to invoke on the next frame.</param>
/// <returns>The ID of the delayed function. Used to cancel it with <see cref="CancelCall"/></returns>
public static ushort ToNextFrame(DelayedDelegate delayedCall)
{
return AllocateCall(delayedCall, DelayType.FrameNumber, 0f, Time.frameCount + 1, null, false, 0);
}
/// <summary>
/// Takes a function and invokes it on the following frame and takes
/// one parameter.
/// </summary>
/// <typeparam name="T">The type of the parameter of the function.</typeparam>
/// <param name="delayedCall">The function you want to call.</param>
/// <param name="argument">The value of the parameter you want to send</param>
/// <returns>The ID of the delayed function. Used to cancel it with <see cref="CancelCall"/></returns>
public static ushort ToNextFrame<T>(DelayedDelegate<T> delayedCall, T argument)
{
return AllocateCall(delayedCall, DelayType.FrameNumber, 0f, Time.frameCount + 1, argument, true, 0);
}
/// <summary>
/// Delays a function call by a set number of frames with a callback.
/// </summary>
/// <param name="delayedCall">The callback you want to invoke when the frame number is hit.</param>
/// <param name="frameCount">The number of frames you want to delay by.</param>
/// <returns>The id of the call.</returns>
public static ushort ByFrames(DelayedDelegate delayedCall, int frameCount)
{
return AllocateCall(delayedCall, DelayType.FrameNumber, 0f, Time.frameCount + frameCount, null, false, 0);
}
/// <summary>
/// Delays a function call by a set number of frames with a callback.
/// </summary>
/// <typeparam name="T">The parameter type of the function you want to call.</typeparam>
/// <param name="delayedCall">The callback you want to invoke when the frame number is hit.</param>
/// <param name="frameCount">The number of frames you want to delay by.</param>
/// <param name="argument">The argument of the function you want to invoke.</param>
/// <returns>The id of the call.</returns>
public static ushort ByFrames<T>(DelayedDelegate<T> delayedCall, int frameCount, T argument)
{
return AllocateCall(delayedCall, DelayType.FrameNumber, 0f, Time.frameCount + frameCount, argument, true, 0);
}
/// <summary>
/// Takes a function and invokes it a set amount of seconds later.
/// </summary>
/// <param name="delayedCall">The function you want to delay.</param>
/// <param name="seconds">The number of seconds later you want to delay the call.</param>
/// <returns>The ID of the delayed function. Used to cancel it with <see cref="CancelCall"/></returns>
public static ushort BySeconds(DelayedDelegate delayedCall, float seconds)
{
return AllocateCall(delayedCall, DelayType.Seconds, seconds, 0, null, false, 0);
}
/// <summary>
/// Takes a function and invokes it a set amount of seconds later with one parameter.
/// </summary>
/// <param name="delayedCall">The function you want to delay.</param>
/// <param name="seconds">The number of seconds later you want to delay the call.</param>
/// <param name="argument">The argument you want to send when we invoke the function.</param>
/// <returns>The ID of the delayed function. Used to cancel it with <see cref="CancelCall"/></returns>
public static ushort BySeconds<T>(DelayedDelegate<T> delayedCall, float seconds, T argument)
{
return AllocateCall(delayedCall, DelayType.Seconds, seconds, 0, argument, true, 0);
}
/// <summary>
/// Cancels a call based off an id. The ID is given to you when using the return value from
/// <see cref="BySeconds"/> or <see cref="ToNextFrame"/>.
/// </summary>
/// <param name="id">The ID of the call you want to cancel.</param>
/// <returns>True if a call was canceled and false if it was not.</returns>
public static bool CancelCall(ushort id)
{
return false;
}
/// <summary>
/// Loops over array of delayed calls and tries to find one that is not allocated. If none is found the array is resized and invoked again.
/// If an instance is found it's set and allocated.
/// </summary>
/// <param name="callback">The function you want to call</param>
/// <param name="type">The type of callback it has</param>
/// <param name="seconds">The number of seconds you want to delay if the type is <see cref="DelayType.Seconds"/></param>
/// <param name="frame">The frame number you want to invoke this at if the type is <see cref="DelayType.FrameNumber"/></param>
/// <param name="argument">An argument to send to the function only if hasArugment is true.></param>
/// <param name="hasArgument">If we have an argument or not.</param>
/// <param name="startingIndex">The starting index we look for unused DelayedCallas at. Used for when the array is resized.</param>
/// <returns>The ID of the call.</returns>
private static ushort AllocateCall(Delegate callback, DelayType type, float seconds, int frame, object argument, bool hasArgument, int startingIndex = 0)
{
DelayedCall call;
for (int i = startingIndex; i < _delayedCalls.Length; i++)
{
call = _delayedCalls[i];
if (call.delayType == DelayType.NotAllocated)
{
if (type == DelayType.FrameNumber)
{
call.SetToFrames(callback, frame, argument, hasArgument);
_delayedCalls[i] = call;
SubscribeToUpdate();
return call.ID;
}
else if (type == DelayType.Seconds)
{
call.SetToSeconds(callback, seconds, argument, hasArgument);
_delayedCalls[i] = call;
SubscribeToUpdate();
return call.ID;
}
}
}
// We did not find any unused calls so we have to add some more
int arraySize = _delayedCalls.Length;
// Resize it
Array.Resize(ref _delayedCalls, arraySize + POOL_GROWTH_STEP_SIZE);
// Loop over and initialize them
for (int i = arraySize; i < arraySize + POOL_GROWTH_STEP_SIZE; i++)
{
_delayedCalls[i] = new DelayedCall(_nextCallID);
_nextCallID++;
}
// Invoke the function again to get the index
return AllocateCall(callback, type, seconds, frame, argument, hasArgument, arraySize - 1);
}
/// <summary>
/// Subscribes to update if we are not already.
/// </summary>
private static void SubscribeToUpdate()
{
if (!_isSubscribedToUpdate)
{
UpdateRunner.onUpdate += Update;
_isSubscribedToUpdate = true;
}
}
/// <summary>
/// Invoked every frame by <see cref="UpdateRunner"/> when we have any Delayed Calls
/// in our array that are active.
/// </summary>
private static void Update()
{
// Reset out flag telling us to unsubscribe from update
_shouldStaySubscribedToUpdate = false;
// Loop over all elements
for (int i = _delayedCalls.Length - 1; i >= 0; i--)
{
// Get our call
DelayedCall call = _delayedCalls[i];
// Tick it and if any return true we should stay subscribed
_shouldStaySubscribedToUpdate |= call.Tick();
// Set it back to our array
_delayedCalls[i] = call;
}
// Check if we should unsubscribe.
if (!_shouldStaySubscribedToUpdate)
{
UpdateRunner.onUpdate -= Update;
_isSubscribedToUpdate = false;
}
}
}
using UnityEngine;
using JetBrains.Annotations;
using UnityEngine.Assertions;
/// <summary>
/// Allows any class to subscribe to Update callbacks using
/// the static delegate.
/// </summary>
public sealed class UpdateRunner : MonoBehaviour
{
/// <summary>
/// The delegate we use to subscribe update functions.
/// </summary>
public delegate void UpdateDelegate();
/// <summary>
/// Our delegate that we use to invoke our callbacks
/// </summary>
private static UpdateDelegate m_OnUpdate;
#if UNITY_ASSERTIONS
/// <summary>
/// A flag used to make sure we only have one instance created.
/// </summary>
private static bool m_HasInstance;
#endif
/// <summary>
/// Adds or removes a method that will be invoked on every
/// Unity update call.
/// </summary>
public static event UpdateDelegate onUpdate
{
add { m_OnUpdate += value; }
remove { m_OnUpdate -= value; }
}
/// <summary>
/// Creates a new instance of the Update Runner before the first scene is loaded.
/// </summary>
[UsedImplicitly]
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
// Create a Game Object in the scene
GameObject gameObject = new GameObject("[Update Runner]");
// Add our component
UpdateRunner runner = gameObject.AddComponent<UpdateRunner>();
// Mark it so it does not get destroyed
DontDestroyOnLoad(gameObject);
// Hide our runner if we are not in debug mode.
gameObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
}
#if UNITY_ASSERTIONS
/// <summary>
/// Sets our flag that we have an instance. If the flag is already
/// set we destroy the newly created one to stop future bugs.
/// </summary>
private void Awake()
{
Assert.IsFalse(m_HasInstance, "A second instance of Update Runner was created. There should only be one instance or update will be called more then onces per frame");
// Set our instance flag
m_HasInstance = true;
}
/// <summary>
/// Sets the flag that we don't have an instance when
/// this is destroyed.
/// </summary>
private void OnDestroy()
{
m_HasInstance = false;
}
#endif
/// <summary>
/// Invoked by Unity every frame this version is unsafe as in if
/// we get an exception all update calls will be canceled.
/// </summary>
[UsedImplicitly]
private void Update()
{
if(m_OnUpdate != null)
{
m_OnUpdate();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment