Skip to content

Instantly share code, notes, and snippets.

@darbotron
Last active March 3, 2022 19:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darbotron/296017d6dce649fee3ed732ec3d77be2 to your computer and use it in GitHub Desktop.
Save darbotron/296017d6dce649fee3ed732ec3d77be2 to your computer and use it in GitHub Desktop.
StoppableCoroutine et al. - a bunch of handy classes for managing coroutines in Unity
//
// License: https://opensource.org/licenses/unlicense
// TL;DR:
// 1) you may do what you like with it...
// 2) ...except blame me for any consequence of acting on rule 1)
//
using System;
using System.Collections;
using UnityEngine;
//
// interface to add a bunch of useful QOL stuff for coroutines in Unity
//
public interface IStoppable
{
bool IsRunning(); // returns true from construction til Stop() or ForceStop() are called, or IEnumerator MoveNext has returned false
void Stop(); // attempts to stop the enumeration, if IsStopping() returns true after this call additional calls to MoveNext() are required to complete the enumeration
bool IsStopping(); // returns true after Stop() if requires additional calls to MoveNext() to complete the enumeration
bool HasStopped(); // returns true when enumeration has stopped (i.e. if this is true MoveNext() must return false when called)
bool IsProcessing(); // returns true when ( ! HasStopped() )
void ForceStop(); // tries to instantly stop the IEnumerator - allowed to throw System.NotSupportedException
}
//
// combine IStoppable with IEnumerator
//
public interface IStoppableEnumerator : IEnumerator, IStoppable
{
// IEnumerator: void Reset(); - MS docs say it's officially part of the IEnumerator spec to throw new NotSupportedException() if this operation isn't supported
// IEnumerator: object Current { get; }
// IEnumerator: bool MoveNext();
}
//
// Wraps a single IEnumerator / MonoBehaviour pairing
//
// The IEnumerator is run as a coroutine on the paired MonoBehaviour at creation using MonoBehaviour.StartCoroutine()
//
// Provides the following super useful extensions to the basic coroutine stuff:
//
// * call InstantStopCoroutine.StartCoroutineOn() to create an instance and start it as a coroutine
// * check if the IEnumerator is running by calling IsRunning()
// * check if the IEnumerator has finished by calling HasStopped()
// * stop at any time by calling Stop() (calls MonoBehaviour.StopCoroutine() on the paired MonoBehaviour) - this instantly transitions from IsRunning() to HasStopped()
// * call a callback Action if/when Stop() is called
// * asserts on operations done in invalid states
//
// CAVEATS
// * DOES NOT support IEnumerator.Reset() - throws NotSupportedException if called
// * WILL NOT call the callback if the coroutine is stopped via the Monobehaviour Coroutine API on m_coroutineMonobehaviour
// * stores a reference to the running monobehaviour (m_coroutineMonobehaviour) so be careful of keeping static collections of these
//
public abstract class InstantStopCoroutineBase : IStoppableEnumerator
{
//////////////////////////////////////////////////////////////////////////
#region IEnumerator interface
public void Reset()
{
throw new NotSupportedException( "InstantStopCoroutine doesn't support Reset()" );
}
public object Current => m_managedEnumerator.Current;
public bool MoveNext()
{
if( HasStopped() )
{
return false;
}
if( m_managedEnumerator.MoveNext() )
{
return true;
}
CurrentState = State.Stopped;
return false;
}
#endregion IEnumerator interface
//////////////////////////////////////////////////////////////////////////
#region IStoppableEnumerator interface
public bool IsRunning() => ( State.Running == CurrentState );
public bool IsStopping() => false; // stops instantly
public bool HasStopped() => ( State.Stopped == CurrentState );
public bool IsProcessing() => ( ! HasStopped() );
public void Stop()
{
if( State.Stopped == CurrentState ) Debug.LogWarning( $"can't stop again, already stopped" );
switch( CurrentState )
{
case State.Running:
m_coroutineMonobehaviour.StopCoroutine( this );
CurrentState = State.Stopped;
m_cbOnWasManuallyStopped?.Invoke();
break;
}
}
public void ForceStop() => Stop();
#endregion IStoppableEnumerator interface
//////////////////////////////////////////////////////////////////////////
#region private
private enum State
{
Running,
Stopped
}
private State CurrentState { get; set; }
//------------------------------------------------------------------------
private readonly MonoBehaviour m_coroutineMonobehaviour = null;
private readonly IEnumerator m_managedEnumerator = null;
private readonly Action m_cbOnWasManuallyStopped = null;
//------------------------------------------------------------------------
// note: protected to allow deriving classes
//------------------------------------------------------------------------
protected InstantStopCoroutineBase( MonoBehaviour coroutineMonobehaviour, IEnumerator managedEnumerator, Action cbOnWasManuallyStopped )
{
Debug.Assert( ( null != managedEnumerator ), $"{nameof(managedEnumerator)} must be a valid IEnumerator" );
m_coroutineMonobehaviour = coroutineMonobehaviour;
m_managedEnumerator = managedEnumerator;
m_cbOnWasManuallyStopped = cbOnWasManuallyStopped;
CurrentState = State.Running;
m_coroutineMonobehaviour.StartCoroutine( this );
}
#endregion private
//////////////////////////////////////////////////////////////////////////
}
//
// basic no-frills concrete implementation of InstantStopCoroutineBase
//
public class InstantStopCoroutine : InstantStopCoroutineBase
{
//------------------------------------------------------------------------
public static InstantStopCoroutine StartCoroutineOn( MonoBehaviour runningMonobehaviour, IEnumerator enumeratorToRun, Action cbOnWasManuallyStopped ) => new InstantStopCoroutine( runningMonobehaviour, enumeratorToRun, cbOnWasManuallyStopped );
//------------------------------------------------------------------------
public static InstantStopCoroutine StartCoroutineOn( MonoBehaviour runningMonobehaviour, IEnumerator enumeratorToRun ) => new InstantStopCoroutine( runningMonobehaviour, enumeratorToRun, null );
//------------------------------------------------------------------------
protected InstantStopCoroutine( MonoBehaviour coroutineMonobehaviour, IEnumerator managedEnumerator, Action cbOnWasManuallyStopped ) : base( coroutineMonobehaviour, managedEnumerator, cbOnWasManuallyStopped )
{}
}
//
// awesome concrete version of InstantStopCoroutineBase which allows cllient code to
// add data to their coroutines which is accessible via the coroutine and to the coroutine
// essentially making the couroutine into a "lite" class object
//
// can be used for stuff like:
// * keeping tabs on running coroutines
// * changing behaviour of running coroutines
// * giving each coroutine its own state accessible from outside
//
public class InstantStopCoroutineWithData< T > : InstantStopCoroutineBase
{
//------------------------------------------------------------------------
public static InstantStopCoroutineWithData< T > StartCoroutineOn( MonoBehaviour runningMonobehaviour, Func< T, IEnumerator > enumeratorFunction, T dataForCoroutine, Action cbOnWasManuallyStopped )
{
return new InstantStopCoroutineWithData< T >( runningMonobehaviour, enumeratorFunction( dataForCoroutine ), dataForCoroutine, cbOnWasManuallyStopped );
}
//------------------------------------------------------------------------
public static InstantStopCoroutineWithData< T > StartCoroutineOn( MonoBehaviour runningMonobehaviour, Func< T, IEnumerator > enumeratorFunction, T dataForCoroutine )
{
return new InstantStopCoroutineWithData< T >( runningMonobehaviour, enumeratorFunction( dataForCoroutine ), dataForCoroutine, null );
}
//------------------------------------------------------------------------
public T Data { get; protected set; }
//------------------------------------------------------------------------
protected InstantStopCoroutineWithData( MonoBehaviour coroutineMonobehaviour, IEnumerator managedEnumerator, T dataForCoroutine, Action cbOnWasManuallyStopped ) : base( coroutineMonobehaviour, managedEnumerator, cbOnWasManuallyStopped )
{
Data = dataForCoroutine;
}
}
//
// Wraps a pair of IEnumerators and associates with a MonoBehaviour
//
// Two IEnumerators are needed for this class, one enumerated to 'run' the coroutine, and another enumerated used for 'stopping' it
//
// The 'run' IEnumerator is run on the paired MonoBehaviour at creation using MonoBehaviour.StartCoroutine()
// IsRunning() will now return true
//
// If Stop() is called before the 'run' enumerator has completed, then the 'stopping' enumerator will be enumerated
// by IEnumerator.MoveNext() (which is what Unity uses to update Coroutines)
//
//
// * call StoppableCoroutineBase.StartCoroutineOn() to create an instance and start it as a coroutine
// * check if the IEnumerator is running by calling IsRunning() - if this is true then the 'run' IEnumerator is being enumerated when IEnumerator.MoveNext() is called
// * stop at any time by calling Stop() (calls MonoBehaviour.StopCoroutine() on the paired MonoBehaviour) - this instantly transitions from IsRunning() to HasStopped()
// * check if the IEnumerator is stopping by calling IsStopping() - if this is true then the 'stopping' IEnumerator is being enumerated when IEnumerator.MoveNext() is called
// * check if the IEnumerator has finished by calling HasStopped() - if this is true then either the 'run' or 'stopping' enumerators enumerated to completion
// * can force an instant transition from IsRunning() to HasStopped() with ForceStop() - however this is not guaranteed to be safe, will depend on what the IEnumerators are actually doing
// * asserts on operations done in invalid states
//
// CAVEATS
// * DOES NOT support IEnumerator.Reset() - throws NotSupportedException if called
// * behaviour will break if the coroutine is stopped via the Monobehaviour Coroutine API on m_coroutineMonobehaviour
// * stores a reference to the running monobehaviour (m_coroutineMonobehaviour) so be careful of keeping static collections of these
//
public abstract class StoppableCoroutineBase : IStoppableEnumerator
{
//////////////////////////////////////////////////////////////////////////
#region IEnumerator interface
public void Reset()
{
throw new NotSupportedException( "StoppableCoroutineBase doesn't support Reset()" );
}
public object Current => CurrentEnumerator.Current;
public bool MoveNext()
{
if( HasStopped() )
{
return false;
}
if( CurrentEnumerator.MoveNext() )
{
return true;
}
CurrentState = State.Stopped;
return false;
}
#endregion IEnumerator interface
//////////////////////////////////////////////////////////////////////////
#region IStoppableEnumerator interface
public bool IsRunning() => ( State.Running == CurrentState );
public bool IsStopping() => ( State.Stopping == CurrentState );
public bool HasStopped() => ( State.Stopped == CurrentState );
public bool IsProcessing() => ( ! HasStopped() );
public void Stop()
{
Debug.Assert( ( State.Stopped != CurrentState ), $"can't stop again, already stopped" );
Debug.Assert( ( State.Stopping != CurrentState ), $"can't stop again, already stopping" );
switch( CurrentState )
{
case State.Running:
// swap to the stopping enumerator - MoveNext must then be called til it returns false
CurrentEnumerator = m_funcToGetEnumeratorForStopping();
CurrentState = State.Stopping;
break;
}
}
public void ForceStop()
{
m_coroutineMonobehaviour.StopCoroutine( this );
CurrentState = State.Stopped;
}
#endregion IStoppableEnumerator interface
//////////////////////////////////////////////////////////////////////////
#region private
private enum State
{
Running,
Stopping,
Stopped
}
private State CurrentState { get; set; }
private IEnumerator CurrentEnumerator {get; set;}
private readonly MonoBehaviour m_coroutineMonobehaviour = null;
private readonly IEnumerator m_managedEnumeratorRunning = null;
private readonly Func< IEnumerator > m_funcToGetEnumeratorForStopping = null;
protected StoppableCoroutineBase( MonoBehaviour coroutineMonobehaviour, IEnumerator managedEnumeratorRunning, Func< IEnumerator > funcToGetEnumeratorForStopping )
{
Debug.Assert( ( null != managedEnumeratorRunning ), $"{nameof(managedEnumeratorRunning)} must be a valid {managedEnumeratorRunning.GetType().Name}" );
Debug.Assert( ( null != funcToGetEnumeratorForStopping ), $"{nameof(funcToGetEnumeratorForStopping)} must be a valid {funcToGetEnumeratorForStopping.GetType().Name}" );
m_coroutineMonobehaviour = coroutineMonobehaviour;
m_managedEnumeratorRunning = managedEnumeratorRunning;
m_funcToGetEnumeratorForStopping = funcToGetEnumeratorForStopping;
CurrentState = State.Running;
CurrentEnumerator = m_managedEnumeratorRunning;
m_coroutineMonobehaviour.StartCoroutine( this );
}
#endregion private
//////////////////////////////////////////////////////////////////////////
}
//
// basic no-frills concrete implementation of StoppableCoroutine
//
public class StoppableCoroutine : StoppableCoroutineBase
{
//------------------------------------------------------------------------
// NOTE: no override for null enumeratorToStop as you wouldn't use this
// StoppableCoroutine unless you needed an enumerator to stop the task
public static StoppableCoroutine StartCoroutineOn( MonoBehaviour runningMonobehaviour, IEnumerator enumeratorToRun, Func< IEnumerator > funcToGetEnumeratorToStop ) => new StoppableCoroutine( runningMonobehaviour, enumeratorToRun, funcToGetEnumeratorToStop );
//------------------------------------------------------------------------
protected StoppableCoroutine( MonoBehaviour runningMonobehaviour, IEnumerator enumeratorToRun, Func< IEnumerator > funcToGetEnumeratorToStop ) : base( runningMonobehaviour, enumeratorToRun, funcToGetEnumeratorToStop )
{}
}
//
// awesome concrete version of StoppableCoroutineBase which allows cllient code to
// add data to their coroutines which is accessible via the coroutine and to the coroutine
// essentially making the couroutine into a "lite" class object
//
// can be used for stuff like:
// * keeping tabs on running coroutines
// * changing behaviour of running coroutines
// * giving each coroutine its own state accessible from outside
//
public class StoppableCoroutineWithData< T > : StoppableCoroutineBase
{
//------------------------------------------------------------------------
public static StoppableCoroutineWithData< T > StartCoroutineOn( MonoBehaviour runningMonobehaviour, Func< T, IEnumerator > funcToRunCoroutine, T dataForCoroutine, Func< T, IEnumerator > funcToStopCoroutine )
{
return new StoppableCoroutineWithData< T >( runningMonobehaviour, funcToRunCoroutine( dataForCoroutine ), dataForCoroutine, () => funcToStopCoroutine( dataForCoroutine ) );
}
//------------------------------------------------------------------------
public T Data { get; protected set; }
//------------------------------------------------------------------------
protected StoppableCoroutineWithData( MonoBehaviour coroutineMonobehaviour, IEnumerator enumeratorToRun, T dataForCoroutine, Func< IEnumerator > functionToGetEnumeratorToStop ) : base( coroutineMonobehaviour, enumeratorToRun, functionToGetEnumeratorToStop )
{
Data = dataForCoroutine;
}
}
//
// wrapper over the inputs to StoppableCoroutine which allows a StoppableCoroutine
// to be explicitly started an arbitrary time later
//
// intended for stuff like for making a command queue using StoppableCoroutines
//
public abstract class StoppableCommandBase : IStoppable
{
//////////////////////////////////////////////////////////////////////////
#region IStoppable interface
public bool IsRunning() => m_wrappedIStoppableCoroutine?.IsRunning() ?? false;
public bool IsStopping() => m_wrappedIStoppableCoroutine?.IsStopping() ?? false;
public bool HasStopped() => m_wrappedIStoppableCoroutine?.HasStopped() ?? false;
public bool IsProcessing() => ( ! HasStopped() );
public void Stop() => m_wrappedIStoppableCoroutine?.Stop();
public void ForceStop() => Stop();
#endregion IStoppable interface
//////////////////////////////////////////////////////////////////////////
//------------------------------------------------------------------------
public void Start()
{
Debug.Assert( ( null == m_wrappedIStoppableCoroutine ), "already started" );
m_wrappedIStoppableCoroutine = VOnStartCommand( m_paramMonobehaviourToRunOn );
}
//------------------------------------------------------------------------
protected StoppableCommandBase( MonoBehaviour monobehaviourToRunCommandOn )
{
m_paramMonobehaviourToRunOn = monobehaviourToRunCommandOn;
}
//------------------------------------------------------------------------
protected abstract IStoppableEnumerator VOnStartCommand( MonoBehaviour monobehaviourToRunOn );
private MonoBehaviour m_paramMonobehaviourToRunOn = null;
private IStoppableEnumerator m_wrappedIStoppableCoroutine = null;
}
//
// InstantStopCoroutine as a command
//
public class InstantStopCommand : StoppableCommandBase
{
//------------------------------------------------------------------------
public static InstantStopCommand CreateCommandFor( MonoBehaviour monobehaviourToRunCommandOn, Func< IEnumerator > funcGetEnumeratorToRun ) => CreateCommandFor( monobehaviourToRunCommandOn, funcGetEnumeratorToRun, null );
public static InstantStopCommand CreateCommandFor( MonoBehaviour monobehaviourToRunCommandOn, Func< IEnumerator > funcGetEnumeratorToRun, Action cbOnWasManuallyStopped ) => new InstantStopCommand( monobehaviourToRunCommandOn, funcGetEnumeratorToRun, cbOnWasManuallyStopped );
//------------------------------------------------------------------------
protected InstantStopCommand( MonoBehaviour monobehaviourToRunCommandOn, Func< IEnumerator > funcGetEnumeratorToRun, Action cbOnWasManuallyStopped ) : base( monobehaviourToRunCommandOn )
{
m_paramFuncGetEnumeratorToRun = funcGetEnumeratorToRun;
m_paramActionOnWasStopped = cbOnWasManuallyStopped;
}
//------------------------------------------------------------------------
protected override IStoppableEnumerator VOnStartCommand( MonoBehaviour monobehaviourToRunOn ) => InstantStopCoroutine.StartCoroutineOn( monobehaviourToRunOn, m_paramFuncGetEnumeratorToRun(), m_paramActionOnWasStopped );
private Func< IEnumerator > m_paramFuncGetEnumeratorToRun = null;
private Action m_paramActionOnWasStopped = null;
}
//
// InstantStopCoroutineWithData as a command
//
public class InstantStopCommandWithData< T > : StoppableCommandBase
{
//------------------------------------------------------------------------
public static InstantStopCommandWithData< T > CreateCommandFor( MonoBehaviour monobehaviourToRunCommandOn, Func< T, IEnumerator > funcGetEnumeratorToRun, T data ) => CreateCommandFor( monobehaviourToRunCommandOn, funcGetEnumeratorToRun, data, null );
public static InstantStopCommandWithData< T > CreateCommandFor( MonoBehaviour monobehaviourToRunCommandOn, Func< T, IEnumerator > funcGetEnumeratorToRun, T data, Action cbOnWasManuallyStopped ) => new InstantStopCommandWithData< T >( monobehaviourToRunCommandOn, funcGetEnumeratorToRun, data, cbOnWasManuallyStopped );
//------------------------------------------------------------------------
protected InstantStopCommandWithData( MonoBehaviour monobehaviourToRunCommandOn, Func< T, IEnumerator > funcGetEnumeratorToRun, T data, Action cbOnWasManuallyStopped ) : base( monobehaviourToRunCommandOn )
{
m_paramFuncGetEnumeratorToRun = funcGetEnumeratorToRun;
m_paramActionOnWasStopped = cbOnWasManuallyStopped;
m_paramData = data;
}
//------------------------------------------------------------------------
protected override IStoppableEnumerator VOnStartCommand( MonoBehaviour monobehaviourToRunOn ) => InstantStopCoroutineWithData< T >.StartCoroutineOn( monobehaviourToRunOn, m_paramFuncGetEnumeratorToRun, m_paramData, m_paramActionOnWasStopped );
private Func< T, IEnumerator > m_paramFuncGetEnumeratorToRun = null;
private Action m_paramActionOnWasStopped = null;
private T m_paramData = default( T );
}
//
// StoppableCoroutine as a command
//
public class StoppableCommand : StoppableCommandBase
{
//------------------------------------------------------------------------
public static StoppableCommand CreateCommandFor( MonoBehaviour monobehaviourToRunCommandOn, Func< IEnumerator > funcGetEnumeratorToRun, Func< IEnumerator > funcGetEnumeratorToStop ) => new StoppableCommand( monobehaviourToRunCommandOn, funcGetEnumeratorToRun, funcGetEnumeratorToStop );
//------------------------------------------------------------------------
protected StoppableCommand( MonoBehaviour monobehaviourToRunCommandOn, Func< IEnumerator > funcGetEnumeratorToRun, Func< IEnumerator > funcGetEnumeratorToStop ) : base( monobehaviourToRunCommandOn )
{
m_paramFuncGetEnumeratorToRun = funcGetEnumeratorToRun;
m_paramFuncGetEnumeratorToStop = funcGetEnumeratorToStop;
}
//------------------------------------------------------------------------
protected override IStoppableEnumerator VOnStartCommand( MonoBehaviour monobehaviourToRunOn ) => StoppableCoroutine.StartCoroutineOn( monobehaviourToRunOn, m_paramFuncGetEnumeratorToRun(), m_paramFuncGetEnumeratorToStop );
private Func< IEnumerator > m_paramFuncGetEnumeratorToRun = null;
private Func< IEnumerator > m_paramFuncGetEnumeratorToStop = null;
}
//
// StoppableCoroutineWithData< T > as a command
//
public class StoppableCommandWithData< T > : StoppableCommandBase
{
//------------------------------------------------------------------------
public static StoppableCommandWithData< T > CreateCommandFor( MonoBehaviour monobehaviourToRunCommandOn, Func< T, IEnumerator > funcGetEnumeratorToRun, T data, Func< T, IEnumerator > funcGetEnumeratorToStop ) => new StoppableCommandWithData< T >( monobehaviourToRunCommandOn, funcGetEnumeratorToRun, data, funcGetEnumeratorToStop );
//------------------------------------------------------------------------
protected StoppableCommandWithData( MonoBehaviour monobehaviourToRunCommandOn, Func< T, IEnumerator > funcGetEnumeratorToRun, T data, Func< T, IEnumerator > funcGetEnumeratorToStop ) : base( monobehaviourToRunCommandOn )
{
m_paramFuncGetEnumeratorToRun = funcGetEnumeratorToRun;
m_paramFuncGetEnumeratorToStop = funcGetEnumeratorToStop;
m_paramData = data;
}
//------------------------------------------------------------------------
protected override IStoppableEnumerator VOnStartCommand( MonoBehaviour monobehaviourToRunOn ) => StoppableCoroutineWithData< T >.StartCoroutineOn( monobehaviourToRunOn, m_paramFuncGetEnumeratorToRun, m_paramData, m_paramFuncGetEnumeratorToStop );
private Func< T, IEnumerator > m_paramFuncGetEnumeratorToRun = null;
private Func< T, IEnumerator > m_paramFuncGetEnumeratorToStop = null;
private T m_paramData = default( T );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment