Skip to content

Instantly share code, notes, and snippets.

@modyari
Last active May 20, 2022 21:42
Show Gist options
  • Save modyari/a2c2b44a5810e296fd8eedaec3466be1 to your computer and use it in GitHub Desktop.
Save modyari/a2c2b44a5810e296fd8eedaec3466be1 to your computer and use it in GitHub Desktop.
using System;
using System.Collections;
using UnityEngine;
namespace RoutineUtility
{
/// <summary>
/// A routine utility that tracks whether a routine is still active or not
/// </summary>
public class Routine
{
/// <summary>
/// The halting type of the routine
/// </summary>
public enum RoutineHaltType
{
Error,
Disabled,
}
/// <summary>
/// Triggers when the routine finished execution
/// </summary>
public event Action<Routine> OnRoutineFinished;
/// <summary>
/// Triggers when the routine unexpectedly halts
/// </summary>
public event Action<Routine, RoutineHaltType> OnRoutineHalted;
private readonly IEnumerator routineEnumerator;
private readonly MonoBehaviour owner;
private Coroutine coroutine = null;
/// <summary>
/// Indicates whether the routine is still running
/// </summary>
public bool IsRunning => coroutine != null;
// Private so we can only start it with the static method
private Routine(MonoBehaviour owner, IEnumerator routineEnumerator)
{
this.owner = owner;
this.routineEnumerator = routineEnumerator;
}
/// <summary>
/// Creates a new routine object and starts it
/// </summary>
/// <param name="owner">The owner of the coroutine</param>
/// <param name="routineEnumerator">The routine enumerator</param>
/// <returns>The newly created Routine reference</returns>
public static Routine StartRoutine(MonoBehaviour owner, IEnumerator routineEnumerator)
{
Routine routine = new Routine(owner, routineEnumerator);
routine.StartInternal();
return routine;
}
/// <summary>
/// Creates a new routine object and starts it, using the RoutineManager as the owner
/// </summary>
/// <param name="routineEnumerator">The routine enumerator</param>
/// <returns>The newly created Routine reference</returns>
public static Routine StartRoutine(IEnumerator routineEnumerator)
{
return StartRoutine(RoutineManager.Instance, routineEnumerator);
}
/// <summary>
/// Stops the coroutine if it's running
/// </summary>
public void StopCoroutine()
{
if (coroutine != null)
{
owner.StopCoroutine(coroutine);
coroutine = null;
}
}
private void StopInternal(bool reportFinished = false)
{
StopCoroutine();
if (reportFinished)
{
OnRoutineFinished?.Invoke(this);
}
}
private void StartInternal()
{
if (coroutine != null)
{
return;
}
coroutine = owner.StartCoroutine(ProcessRoutineAsync(routineEnumerator));
RoutineManager.Instance.AddTrackedRoutine(this, owner);
}
/// <summary>
/// Notifies the routine that it was halted, providing the halt type
/// </summary>
/// <param name="haltType">The reported halt type</param>
public void NotifyHalted(RoutineHaltType haltType)
{
Debug.Log($"Routine halted. Halt type: {haltType.ToString()}");
OnRoutineHalted?.Invoke(this, haltType);
if (coroutine != null)
{
owner.StopCoroutine(coroutine);
coroutine = null;
}
}
private IEnumerator ProcessRoutineAsync(IEnumerator enumerator)
{
while (true)
{
try
{
// No next steps, that's when we stop the current routine
if (!enumerator.MoveNext())
{
// This is the root routine? Then stop it and and report that it's finished
if (enumerator == routineEnumerator)
{
StopInternal(true);
}
break;
}
}
// Indicate end of coroutine and fire halting due to an error event
catch (Exception e)
{
Debug.LogError(e);
NotifyHalted(RoutineHaltType.Error);
break;
}
// Four possibilities here: either Current is null (yield return null in the instructions)
// It's a YieldInstruction (a Unity instruction like WaitUntil())
// Or it's a CustomYieldInstruction, which is a custom-specified instruction (like this class)..in all cases, yield return it.
if (enumerator.Current is null or YieldInstruction or CustomYieldInstruction)
{
yield return enumerator.Current;
continue;
}
// The fourth case is that it's an IEnumerator (yield routine SomeRoutine()),
// in which case we need to process its instructions within our loop
yield return ProcessRoutineAsync(enumerator.Current as IEnumerator);
}
}
#region Comparison
// To ensure routine == null will return true if it's not running
public static bool operator == (Routine a, Routine b)
{
if (b is null)
{
if (a is null)
{
return true;
}
return !a.IsRunning;
}
if (a is null)
{
return !b.IsRunning;
}
return a.Equals(b);
}
public static bool operator != (Routine a, Routine b)
{
return !(a == b);
}
public override bool Equals(object obj)
{
if (obj is null)
{
return IsRunning;
}
return base.Equals(obj);
}
public override int GetHashCode()
{
return HashCode.Combine(owner.GetHashCode(), routineEnumerator.GetHashCode(),RoutineManager.Instance.CreatedRoutineCount);
}
#endregion
}
public static class RoutineExtensions
{
public static Routine StartRoutine(this MonoBehaviour owner, IEnumerator routineEnumerator)
{
return Routine.StartRoutine(owner, routineEnumerator);
}
}
}
using System.Collections.Generic;
using UnityEngine;
namespace RoutineUtility
{
/// <summary>
/// Manages routines created through Routine
/// </summary>
public class RoutineManager : Singleton<RoutineManager>
{
private class RoutineInfo
{
public Routine Routine { get; set; }
public MonoBehaviour Owner { get; set; }
}
private List<RoutineInfo> routineInfo = new();
public int CreatedRoutineCount { get; private set; }
/// <summary>
/// Adds a routine to be tracked
/// </summary>
/// <param name="routine">The routine to track</param>
/// <param name="owner">Its owner</param>
public void AddTrackedRoutine(Routine routine, MonoBehaviour owner)
{
CreatedRoutineCount++;
if (owner == this)
{
return;
}
routineInfo.Add(new RoutineInfo(){Routine = routine, Owner = owner});
}
private void Update()
{
UpdateRoutines();
}
private void UpdateRoutines()
{
for (int i = routineInfo.Count - 1; i >= 0; i--)
{
RoutineInfo info = routineInfo[i];
if (info.Owner == null || !info.Routine.IsRunning)
{
routineInfo.RemoveAt(i);
continue;
}
if (!info.Owner.isActiveAndEnabled)
{
info.Routine.NotifyHalted(Routine.RoutineHaltType.Disabled);
routineInfo.RemoveAt(i);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment