Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- * Based on: https://forum.unity.com/threads/a-more-flexible-coroutine-interface.94220/
- *
- * This is a wrapper for Unity coroutines that adds some extra functionality.
- * Wrapped coroutines are known as tasks.
- *
- * Features:
- * - Pause/Unpause a task while it executes.
- * - Callback event that occurs when a task is finished.
- * - Creating a sequence of tasks that run one after another.
- * - Creating a batch of tasks that run in parallel.
- * - = Pausing affects all tasks in the batch.
- * - Animation task to go from one value to another.
- */
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using UnityEngine;
- public class Tasks : MonoBehaviour
- {
- private static Tasks _instance;
- private static Tasks GetInstance()
- {
- if (_instance) return _instance;
- var g = new GameObject("Tasks");
- _instance = g.AddComponent<Tasks>();
- return _instance;
- }
- /// <summary>
- /// Get an action where tasks can be submitted. Tasks given to this action will
- /// be executed by the main task GameObject.
- /// </summary>
- /// <returns>Task action.</returns>
- public static Action<IEnumerator> TaskRunner()
- {
- return work => GetInstance().StartCoroutine(work);
- }
- /// <summary>
- /// Get a task for a coroutine.
- /// </summary>
- /// <param name="task">A coroutine.</param>
- /// <returns>New task.</returns>
- public static Task Wrap(IEnumerator task)
- {
- return new Task(task, TaskRunner());
- }
- /// <summary>
- /// Get a task that runs the coroutines in parallel.
- /// </summary>
- /// <param name="work">Coroutines.</param>
- /// <returns>Batch task.</returns>
- public static BatchTask Batch(IEnumerable<IEnumerator> work)
- {
- return Batch(work.Select(Wrap));
- }
- /// <summary>
- /// Get a task that runs multiple tasks in parallel.
- /// </summary>
- /// <param name="tasks">Tasks.</param>
- /// <returns>Batch task.</returns>
- public static BatchTask Batch(IEnumerable<ITask> tasks)
- {
- return new BatchTask(tasks, TaskRunner());
- }
- public static AnimateTask<T> Animate<T>(T from, T to, Action<T> update, float speed, Func<T, T, float, T> interpolate, Func<float, float, float, float> ease = null)
- {
- return new AnimateTask<T>(from, to, update, speed, interpolate, TaskRunner()).WithEase(ease);
- }
- /// <summary>
- /// Create a task builder.
- /// </summary>
- /// <returns>New task builder.</returns>
- public static TaskBuilder Create() => new TaskBuilder();
- /// <summary>
- /// Create an animation builder.
- /// </summary>
- /// <returns>New animation builder.</returns>
- public static AnimationBuilder CreateAnimation() => new AnimationBuilder();
- }
- /// <summary>
- /// Event that is triggered when a task finishes.
- /// Argument specifies if .Stop() was called to end the task.
- /// </summary>
- public delegate void TaskFinishHandler(bool manualStop);
- public interface ITask
- {
- /// <summary>
- /// Event which is called when the task completes.
- /// </summary>
- event TaskFinishHandler OnFinish;
- bool Running { get; }
- bool Paused { get; }
- bool Stopped { get; }
- /// <summary>
- /// Start the task.
- /// Behavior is undefined if called more than once.
- /// </summary>
- void Start();
- /// <summary>
- /// Stop the task. Execution stops the next time the coroutine yields.
- /// </summary>
- void Stop();
- /// <summary>
- /// Pause the task. Execution stops the next time the coroutine yields.
- /// </summary>
- void Pause();
- /// <summary>
- /// Unpause the task. Execution continues on the next frame.
- /// </summary>
- void Continue();
- /// <summary>
- /// If the task has not run yet, complete this task instantly.
- /// This does not execute the work of the task, but executes
- /// the OnFinish event.
- /// </summary>
- void Complete();
- }
- /// <summary>
- /// Represents a task that can be started again once it has finished.
- /// </summary>
- public interface IMultiRunTask
- {
- /// <summary>
- /// Reset a task that has completed so it can run again.
- /// </summary>
- void Reset();
- }
- public static class TaskExtensions
- {
- /// <summary>
- /// Toggle playing/pausing a task.
- /// </summary>
- public static void TogglePlay(this ITask task)
- {
- if (task.Paused) task.Continue();
- else task.Pause();
- }
- }
- /// <summary>
- /// Build a sequence of tasks.
- /// </summary>
- public class TaskBuilder
- {
- private List<ITask> _tasks = new List<ITask>();
- private bool _delayFrame;
- private bool _delayFrameBetween;
- /// <summary>
- /// Get a task representing all the tasks in the builder.
- /// The tasks will run in a sequence.
- /// </summary>
- /// <returns>Built task.</returns>
- public ITask Build()
- {
- var seq = new TaskSequence(_tasks, Tasks.TaskRunner());
- seq.DelayFrame(_delayFrame);
- seq.DelayFrameBetween(_delayFrameBetween);
- return seq;
- }
- /// <summary>
- /// Add a coroutine to the sequence.
- /// </summary>
- /// <param name="task">Coroutine to add.</param>
- /// <returns>Builder instance.</returns>
- public TaskBuilder Add(IEnumerator task)
- {
- _tasks.Add(Tasks.Wrap(task));
- return this;
- }
- /// <summary>
- /// Add a task to the sequence.
- /// </summary>
- /// <param name="task"></param>
- /// <returns>Builder instance.</returns>
- public TaskBuilder Add(ITask task)
- {
- _tasks.Add(task);
- return this;
- }
- /// <summary>
- /// Add an action to the task sequence.
- /// </summary>
- /// <param name="action">Action to run.</param>
- /// <returns>Builder instance.</returns>
- public TaskBuilder Add(Action action)
- {
- _tasks.Add(new ActionTask(action, Tasks.TaskRunner()));
- return this;
- }
- /// <summary>
- /// Add an object to the task sequence that will be yielded while
- /// running the internal coroutine.
- /// </summary>
- /// <param name="o">Object to yield.</param>
- /// <returns>Builder instance.</returns>
- public TaskBuilder Add(object o)
- {
- _tasks.Add(new SingleActionTask(o, Tasks.TaskRunner()));
- return this;
- }
- /// <summary>
- /// Add a batch of tasks that will execute in parallel.
- /// The next task in the sequence will not begin until every task
- /// in the batch is complete.
- /// </summary>
- /// <param name="tasks">Tasks to add.</param>
- /// <returns>Builder instance.</returns>
- public TaskBuilder AddBatch(IEnumerable<IEnumerator> tasks)
- {
- _tasks.Add(Tasks.Batch(tasks));
- return this;
- }
- /// <inheritdoc cref="AddBatch(System.Collections.Generic.IEnumerable{System.Collections.IEnumerator})"/>
- public TaskBuilder AddBatch(IEnumerable<ITask> tasks)
- {
- _tasks.Add(Tasks.Batch(tasks));
- return this;
- }
- /// <summary>
- /// Add a batch of tasks given a collection and function to convert each
- /// to a coroutine.
- /// </summary>
- /// <returns>Builder instance.</returns>
- public TaskBuilder AddBatch<T>(IEnumerable<T> ie, Func<T, IEnumerator> workFunc)
- {
- return AddBatch(ie.Select(workFunc));
- }
- /// <summary>
- /// Add a batch of tasks given a collection and function to convert each
- /// to a task.
- /// </summary>
- /// <returns>Builder instance.</returns>
- public TaskBuilder AddBatch<T>(IEnumerable<T> ie, Func<T, ITask> workFunc)
- {
- return AddBatch(ie.Select(workFunc));
- }
- /// <inheritdoc cref="AddBatch(System.Collections.Generic.IEnumerable{System.Collections.IEnumerator})"/>
- public TaskBuilder AddBatch(params ITask[] tasks)
- {
- return AddBatch((IEnumerable<ITask>) tasks);
- }
- /// <summary>
- /// Set whether the delay frame is enabled.
- /// If true, then the tasks will begin on the next frame instead
- /// of the first frame they are started.
- /// </summary>
- /// <param name="delay"></param>
- /// <returns></returns>
- public TaskBuilder DelayFrame(bool delay = true)
- {
- _delayFrame = delay;
- return this;
- }
- /// <summary>
- /// Set whether there is a delay frame between each task in the sequence.
- /// If true, then one frame will be skipped before starting the
- /// next task in the sequence.
- /// </summary>
- /// <param name="delay">Whether to enable the delay frame.</param>
- /// <returns>Builder instance.</returns>
- public TaskBuilder DelayFrameBetween(bool delay = true)
- {
- _delayFrameBetween = delay;
- return this;
- }
- }
- /// <summary>
- /// Base class which runs a coroutine.
- ///
- /// Can be paused, unpaused, and stopped.
- /// Event can be subscribed to which notifies of task completion.
- /// </summary>
- public class Task : ITask
- {
- private IEnumerator _work;
- private Action<IEnumerator> _taskRunner;
- private bool _delayFrame;
- /// <summary>Whether the task is currently executing.</summary>
- public bool Running { get; private set; }
- /// <summary>Whether the task is currently paused.</summary>
- public bool Paused { get; private set; }
- /// <summary>Whether the task has completed execution.</summary>
- public bool Stopped { get; protected set; }
- /// <summary>Event which is fired when the task completes.</summary>
- public event TaskFinishHandler OnFinish;
- /// <summary>Whether the task is in a state where it can be started.</summary>
- public bool CanStart => !(Running || Stopped);
- public Task(IEnumerator work, Action<IEnumerator> taskRunner)
- {
- _work = work;
- _taskRunner = taskRunner;
- }
- protected Task(Action<IEnumerator> taskRunner) => _taskRunner = taskRunner;
- /// <summary>Called by a subclass to set the work function for this task.</summary>
- protected void SetTask(IEnumerator work) => _work = work;
- public void Start()
- {
- if (!CanStart) return;
- Running = true;
- _taskRunner(WorkWrapper());
- }
- public virtual void Stop()
- {
- Stopped = true;
- Running = false;
- }
- public virtual void Pause() => Paused = true;
- public virtual void Continue() => Paused = false;
- public virtual void Complete()
- {
- if (!CanStart) return;
- CompleteInternal();
- }
- private void CompleteInternal()
- {
- (_work as IDisposable)?.Dispose();
- var stop = Stopped;
- Stopped = true;
- OnFinish?.Invoke(stop);
- }
- /// <summary>
- /// Set if a frame is skipped before the start of the coroutine.
- /// </summary>
- /// <param name="delay">Whether to delay a frame before starting the coroutine.</param>
- public void DelayFrame(bool delay = true) => _delayFrame = delay;
- private IEnumerator WorkWrapper()
- {
- if (_delayFrame) yield return null;
- while (Running)
- {
- if (Paused) yield return null;
- else if (_work?.MoveNext() == true)
- {
- yield return _work.Current;
- }
- else Running = false;
- }
- CompleteInternal();
- }
- }
- /// <summary>
- /// Runs many tasks at once.
- /// </summary>
- public class BatchTask : Task
- {
- private List<ITask> _tasks;
- private int _working;
- public BatchTask(IEnumerable<ITask> tasks, Action<IEnumerator> taskRunner) : base(taskRunner)
- {
- _tasks = tasks.ToList();
- SetTask(HandleTasks());
- foreach (var task in _tasks)
- {
- task.OnFinish += OnTaskFinish;
- }
- }
- // Called for each task in the batch.
- private void OnTaskFinish(bool manualStop)
- {
- _working--;
- }
- public override void Stop()
- {
- foreach (var task in _tasks) task.Stop();
- }
- public override void Pause()
- {
- foreach (var task in _tasks) task.Pause();
- }
- public override void Continue()
- {
- foreach (var task in _tasks) task.Continue();
- }
- private IEnumerator HandleTasks()
- {
- foreach (var task in _tasks)
- {
- _working++;
- task.Start();
- }
- while (_working > 0) yield return null;
- }
- }
- /// <summary>
- /// Run one task after another.
- /// </summary>
- public class TaskSequence : Task
- {
- private List<ITask> _tasks;
- private int _index;
- private bool _executed;
- private bool _delayFrame;
- public TaskSequence(IEnumerable<ITask> tasks, Action<IEnumerator> taskRunner) : base(taskRunner)
- {
- _tasks = tasks.ToList();
- SetTask(HandleTasks());
- foreach (var task in _tasks)
- {
- task.OnFinish += OnTaskFinish;
- }
- }
- // Called for each task in the sequence.
- private void OnTaskFinish(bool manualStop)
- {
- _index++;
- _executed = false;
- }
- public override void Stop()
- {
- base.Stop();
- _tasks[_index].Stop();
- }
- public override void Pause()
- {
- base.Pause();
- _tasks[_index].Pause();
- }
- public override void Continue()
- {
- base.Continue();
- _tasks[_index].Continue();
- }
- /// <summary>
- /// Should an extra frame be waited before starting the next task in the sequence.
- /// If false, the next task might start on the same frame the last one ends.
- /// </summary>
- /// <param name="delay">Whether to delay a frame between tasks.</param>
- public void DelayFrameBetween(bool delay = true) => _delayFrame = delay;
- private IEnumerator HandleTasks()
- {
- while (_index < _tasks.Count)
- {
- if (_executed)
- {
- // Wait because one of the tasks is currently running.
- yield return null;
- continue;
- }
- if (_delayFrame) yield return null;
- _executed = true;
- _tasks[_index].Start();
- yield return null;
- }
- }
- }
- /// <summary>
- /// Task that executes an action and completes immediately.
- /// </summary>
- public class ActionTask : Task, IMultiRunTask
- {
- private readonly Action _action;
- public ActionTask(Action action, Action<IEnumerator> taskRunner) : base(taskRunner)
- {
- _action = action;
- SetTask(RunAction());
- }
- public void Reset()
- {
- if (Stopped)
- {
- Stopped = false;
- SetTask(RunAction());
- }
- }
- private IEnumerator RunAction()
- {
- _action();
- yield break;
- }
- }
- /// <summary>
- /// Task that yields an object to the internal coroutine.
- /// </summary>
- public class SingleActionTask : Task, IMultiRunTask
- {
- private readonly object _object;
- public SingleActionTask(object o, Action<IEnumerator> taskRunner) : base(taskRunner)
- {
- _object = o;
- SetTask(RunTask());
- }
- public void Reset()
- {
- if (Stopped)
- {
- Stopped = false;
- SetTask(RunTask());
- }
- }
- private IEnumerator RunTask()
- {
- yield return _object;
- }
- }
- public interface IAnimationTask : ITask, IMultiRunTask
- {
- bool Reverse { get; set; }
- }
- /// <summary>
- /// Class for building animation sequences.
- /// </summary>
- public class AnimationBuilder
- {
- private List<IAnimationTask> _parts = new List<IAnimationTask>();
- /// <summary>
- /// Get the animation representing this sequence of animations.
- /// </summary>
- /// <returns>Full animation.</returns>
- public AnimationSequence Build()
- {
- return new AnimationSequence(_parts, Tasks.TaskRunner());
- }
- /// <summary>
- /// Add an animation to the sequence.
- /// </summary>
- /// <returns>Animation builder.</returns>
- public AnimationBuilder Add(IAnimationTask animation)
- {
- _parts.Add(animation);
- return this;
- }
- /// <summary>
- /// Append animations to the end of the sequence.
- /// </summary>
- /// <returns>Animation builder.</returns>
- public AnimationBuilder Add(IEnumerable<IAnimationTask> animations)
- {
- _parts.AddRange(animations);
- return this;
- }
- /// <summary>
- /// Add a set of animations to the end of the sequence that
- /// will execute in parallel. The next animation in the sequence
- /// will begin on the frame after all animations in the batch
- /// have completed.
- /// </summary>
- /// <returns>Animation builder.</returns>
- public AnimationBuilder AddBatch(List<IAnimationTask> animations)
- {
- if (animations.Count == 0) return this;
- return Add(new AnimationBatch(animations, Tasks.TaskRunner()));
- }
- /// <inheritdoc cref="AddBatch(System.Collections.Generic.List{IAnimationTask})"/>
- public AnimationBuilder AddBatch(IEnumerable<IAnimationTask> animations)
- {
- return AddBatch(animations.ToList());
- }
- /// <inheritdoc cref="AddBatch(System.Collections.Generic.List{IAnimationTask})"/>
- public AnimationBuilder AddBatch(params IAnimationTask[] animations)
- {
- return AddBatch(animations.ToList());
- }
- }
- /// <summary>
- /// Sequence of animations. The next animation will begin to play on the
- /// frame after the current one ends.
- /// </summary>
- public class AnimationSequence : Task, IAnimationTask
- {
- private readonly List<IAnimationTask> _animations;
- private bool _executing;
- private IAnimationTask _currentAnimation;
- public bool Reverse { get; set; }
- public ILoopControl LoopControl { get; set; }
- public AnimationSequence(List<IAnimationTask> animations, Action<IEnumerator> taskRunner) : base(taskRunner)
- {
- _animations = animations;
- SetTask(Animate());
- }
- private void AnimationFinished(bool manualStop)
- {
- _executing = false;
- }
- public override void Stop()
- {
- base.Stop();
- _currentAnimation?.Stop();
- }
- public override void Pause()
- {
- base.Pause();
- _currentAnimation?.Pause();
- }
- public override void Continue()
- {
- base.Continue();
- _currentAnimation?.Continue();
- }
- public void Reset()
- {
- if (Stopped)
- {
- Stopped = false;
- SetTask(Animate());
- }
- }
- private IEnumerator Animate()
- {
- while (true)
- {
- var reverse = Reverse;
- var animations = _animations;
- for (var i = 0; i < animations.Count; i++)
- {
- var index = Reverse ? animations.Count - 1 - i : i;
- var animation = animations[index];
- var reversed = animation.Reverse;
- if (reverse) animation.Reverse = !reversed;
- animation.OnFinish += AnimationFinished;
- animation.Reset();
- _currentAnimation = animation;
- animation.Start();
- _executing = true;
- while (_executing) yield return null;
- _currentAnimation = null;
- animation.OnFinish -= AnimationFinished;
- animation.Reverse = reversed;
- }
- if (LoopControl?.TryLoop(this) != true) yield break;
- }
- }
- }
- /// <summary>
- /// Execute multiple animations at the same time.
- /// This task completes on the frame after all contained animations have completed.
- ///
- /// All animations are started at the same time, even when reversed. Which could
- /// cause strange behavior if animations have different lengths and are played
- /// in reverse.
- /// </summary>
- public class AnimationBatch : Task, IAnimationTask
- {
- private readonly List<IAnimationTask> _animations;
- private int _running;
- public bool Reverse { get; set; }
- public ILoopControl LoopControl { get; set; }
- public AnimationBatch(List<IAnimationTask> animations, Action<IEnumerator> taskRunner) : base(taskRunner)
- {
- _animations = animations;
- SetTask(Animate());
- }
- private void AnimationFinished(bool manualStop)
- {
- _running--;
- }
- public void Reset()
- {
- if (Stopped)
- {
- Stopped = false;
- SetTask(Animate());
- }
- }
- private IEnumerator Animate()
- {
- var reverse = Reverse;
- var reversed = new bool[_animations.Count];
- _running = _animations.Count;
- for (var i = 0; i < _animations.Count; i++)
- {
- var animation = _animations[i];
- if (reverse)
- {
- reversed[i] = animation.Reverse;
- animation.Reverse = !animation.Reverse;
- }
- animation.OnFinish += AnimationFinished;
- animation.Reset();
- animation.Start();
- }
- while (_running > 0) yield return null;
- for (var i = 0; i < _animations.Count; i++)
- {
- var animation = _animations[i];
- if (reverse) animation.Reverse = reversed[i];
- animation.OnFinish -= AnimationFinished;
- }
- }
- }
- /// <summary>
- /// Task that executes an animation from one value to another.
- ///
- /// Required values:
- /// - Start and ending value
- /// - Function that uses the interpolated value
- /// - Speed of the animation (in seconds)
- /// -
- /// </summary>
- /// <typeparam name="T"></typeparam>
- public class AnimateTask<T> : Task, IAnimationTask
- {
- public T From { get; set; }
- public T To { get; set; }
- public Action<T> Update { get; set; }
- public float Speed { get; set; }
- public Func<T, T, float, T> Interpolate { get; set; }
- public Func<float, float, float, float> Ease { get; set; }
- public bool Reverse { get; set; }
- public float Delay { get; set; }
- public ILoopControl<AnimateTask<T>> LoopControl { get; set; }
- /// <summary>
- /// Create an animation task.
- /// </summary>
- /// <param name="from">Starting value</param>
- /// <param name="to">Ending value</param>
- /// <param name="update">Function that uses the interpolated value</param>
- /// <param name="speed">Animation speed (in seconds)</param>
- /// <param name="interpolate">Function to interpolate between the start and end (this should be a linear interpolation).</param>
- /// <param name="taskRunner">Task runner.</param>
- public AnimateTask(T from, T to, Action<T> update, float speed, Func<T, T, float, T> interpolate, Action<IEnumerator> taskRunner) : base(taskRunner)
- {
- From = from;
- To = to;
- Update = update;
- Speed = speed;
- Interpolate = interpolate;
- Ease = (a, b, t) => t;
- SetTask(Animate());
- OnFinish += _ => { Update(Reverse ? From : To); };
- }
- /// <summary>
- /// Set the loop controller for this animation.
- /// </summary>
- /// <param name="loopControl">Loop controller.</param>
- /// <returns>Current task.</returns>
- public AnimateTask<T> WithLoopControl(ILoopControl<AnimateTask<T>> loopControl)
- {
- LoopControl = loopControl;
- return this;
- }
- /// <summary>
- /// Set the easing function that is used when animating forwards.
- /// </summary>
- /// <param name="easeFunction">Easing function.</param>
- /// <returns>Current task.</returns>
- public AnimateTask<T> WithEase(Func<float, float, float, float> easeFunction)
- {
- Ease = easeFunction ?? Ease;
- return this;
- }
- /// <inheritdoc cref="WithEase(System.Func{float,float,float,float})"/>
- public AnimateTask<T> WithEase(Func<float, float> easeFunction)
- {
- return WithEase((a, b, t) => easeFunction(t));
- }
- /// <summary>
- /// If true, the animation will animate from the final value to the initial value.
- /// </summary>
- /// <param name="reverse">Whether to reverse the animation.</param>
- /// <returns>Current task.</returns>
- public AnimateTask<T> Reversed(bool reverse = true)
- {
- Reverse = reverse;
- return this;
- }
- /// <summary>
- /// Set the delay in seconds that occurs before the animation.
- /// This delay occurs before each playback of the animation, and occurs after
- /// the animation when played in reverse.
- /// </summary>
- /// <param name="delay">Delay time in seconds.</param>
- /// <returns>Current task.</returns>
- public AnimateTask<T> WithDelay(float delay)
- {
- Delay = delay;
- return this;
- }
- void IMultiRunTask.Reset()
- {
- if (Stopped)
- {
- Stopped = false;
- SetTask(Animate());
- }
- }
- private IEnumerator Animate()
- {
- var t = 0f;
- while (true)
- {
- var speed = Speed;
- var ease = Ease;
- var update = Update;
- var interpolate = Interpolate;
- var from = From;
- var to = To;
- var reverse = Reverse;
- var delay = Delay;
- if (reverse) (from, to) = (to, from);
- if (!reverse && delay > 0)
- {
- while (t < delay)
- {
- yield return null;
- t += Time.deltaTime;
- }
- t -= delay;
- }
- for (; t < 1f; t += speed * Time.deltaTime)
- {
- var u = ease(0, 1, t);
- update(interpolate(from, to, u));
- yield return null;
- }
- t -= 1f;
- if (reverse && delay > 0)
- {
- while (t < delay)
- {
- yield return null;
- t += Time.deltaTime;
- }
- t -= delay;
- }
- if (LoopControl?.TryLoop(this) != true) yield break;
- }
- }
- }
- public interface ILoopControl<in TAnimation>
- {
- /// <summary>
- /// Update the animation and return whether the animation should play again.
- /// </summary>
- /// <param name="animation">Current animation.</param>
- /// <returns>True if the animation should play again, false otherwise.</returns>
- bool TryLoop(TAnimation animation);
- }
- public interface ILoopControl : ILoopControl<IAnimationTask> { }
- public class LoopForever : ILoopControl
- {
- public bool TryLoop(IAnimationTask task) => true;
- }
- public class LoopReverse : ILoopControl
- {
- public bool TryLoop(IAnimationTask task)
- {
- task.Reverse = !task.Reverse;
- return true;
- }
- }
Add Comment
Please, Sign In to add comment