Skip to content

Latest commit

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

ActionQueue for Unity

ActionQueue is a lightweight, generic action sequencer for Unity. It helps make it easier to manage complex asynchronous call chains. You can use it for animations, network calls, gameplay events, or anything else.

Installation

Via Package Manager GUI (Simplest)

Unity → Window → Package Manager → [+] → Add package from git URL…

https://github.com/uken/action-queue-unity.git?path=Package#v1.0.0

Via Manifest

Add the following line to your manifest.json:

"com.uken.actionqueue": "https://github.com/uken/action-queue-unity.git?path=Package#v1.0.0",

For Uken internal use, you can instead add the package by name:

"com.uken.actionqueue": "1.0.0",

If you want to run the package's test suite from within the consuming project, you can also add it to the "testables" section, eg:

{
"dependencies": {
...
},
"testables": [
"com.uken.actionqueue"
]
}

👉 In all cases, be sure to replace the version number 1.0.0 with the latest version number found in the package.json file.

👉 If you are upgrading from the non-packaged version of ActionQueue to 1.0.0+, there is a small breaking change with SkippableActionQueue.Enqueue(...):

  • The two parameter version's onSkipped parameter is now a Func that returns a ValueTuple<T1, T2> instead of a Tuple<T1, T2>.
  • The three parameter version's onSkipped parameter is now a Func that returns a ValueTuple<T1, T2, T3> instead of a custom Triplet class.

Setup

Add using Com.Uken.ActionQueue; and you're ready to go.

Requirements

Unity 2020.3+ recommended (any .NET 4.x Equivalent runtime).

License

Apache-2.0. See LICENSE for details.

Usage

Basic Use:

varq=newActionQueue();q.Enqueue(resume =>{// Do something async, then...resume();});q.Enqueue(resume =>{// Do something else async, then...resume();});q.Start();// (Call `resume` exactly once per step. If it's not called, the queue stays waiting.)

For example,

publicvoidCharacterDemo(){varq=newActionQueue();q.Enqueue(this.WalkToStage);q.Enqueue(this.Dance);q.Enqueue(this.Bow);q.Start();}privatevoidWalkToStage(UnityActiononComplete){// onComplete() should be called once this individual action is completed.}
...

Or, something a bit more complicated:

publicvoidFightMonster(GameObjectmonster){varq=newActionQueue();q.Enqueue(this.DrawSword);q.Enqueue(resume =>this.WalkTo(monster,resume));q.Enqueue(resume =>this.Attack(monster,resume));q.Start();}privatevoidDrawSword(UnityActiononComplete){// onComplete() should be called once this individual action is completed.}privatevoidWalkTo(GameObjecttarget,UnityActiononComplete){// onComplete() should be called once this individual action is completed.}
...

Delays:

Delay for two seconds: (pass in a MonoBehaviour to host the coroutine)

q.EnqueueDelay(this/* MonoBehaviour */,2f);

Immediate Actions:

Run an action immediately without needing to call an onComplete:

q.EnqueueImmediateConsuming(()=>{/* Do something... */});

Another example,

varq=newActionQueue();q.Enqueue(player.DrawBow);q.Enqueue(player.LoadArrow);q.Enqueue(player.AimAtTarget);q.EnqueueDelay(player,1f);q.EnqueueImmediateConsuming(logic.DecrementArrowCount);q.Enqueue(player.ShootArrow);q.EnqueueDelay(player,2f);q.Enqueue(player.LetDownBow);q.Start();

Conditional Steps:

Execute a step only if the conditional is true. The conditional is evaluated at the time that the step is due to execute.

q.Enqueue(player.Cheer,conditional:logic.PlayerIsAlive);
q.EnqueueImmediateConsuming(player.Cheer,conditional:logic.PlayerIsAlive);
q.EnqueueDelay(player,10f,conditional:logic.PlayerIsAlive);

This is different than enclosing an enqueue operation in an if statement. In that case, the if statement is evaluated at the time that the step is queued up. For example:

boola=false;q.EnqueueDelay(this,10f);q.EnqueueImmediateConsuming(()=>a=true);if(a){// This does not run, because `a` was set after the if statement was evaluated.q.EnqueueImmediateConsuming(()=>Debug.Log("`a` is true."));}
boola=false;q.EnqueueDelay(this,10f);q.EnqueueImmediateConsuming(()=>a=true);// This does run, because the conditional is evaluated right before the step is due to execute:q.EnqueueImmediateConsuming(()=>Debug.Log("`a` is true."),conditional:()=>a);

Pause, Unpause:

A running queue can be paused (for example, from another asynchronous task); in this case, it will complete its current step but will not move on to the next one. When unpaused, the queue will continue from the next incomplete step.

q.Pause();q.Unpause();
varq1=newActionQueue();varq2=newActionQueue();q1.EnqueueDelay(this,5f);q1.EnqueueImmediateConsuming(()=>Debug.Log("Hello world."));q2.EnqueueDelay(this,1f);q2.EnqueueImmediateConsuming(q1.Pause);q2.EnqueueDelay(this,10f);q2.EnqueueImmediateConsuming(q1.Unpause);q1.Start();q2.Start();// After 11 seconds, "Hello world." is printed.

Cancel:

Cancel a running queue. The currently executing step will finish, but subsequent steps will not start.

q.Cancel();

Parameters:

Up to three generic parameters can be passed through a queue. Start() can be called with initial parameter values, or they can be left empty until populated.

This is useful if you want later steps to depend on the results of earlier ones. For example, if you want to queue up several network operations in a waterfall.

For example, one parameter:

varq=newActionQueue<int>();q.Enqueue((resume,x)=>resume(x+2));q.EnqueueImmediateConsuming(x =>Debug.Log($"value = {x}"));q.Start(initialParam:10);// "value = 12" is printed.

Three parameters:

varq=newActionQueue<int,float,string>();q.Enqueue((resume,a,b,c)=>resume(a+1,0.5f,"hi"));// Parameters pass through if they are not used:q.Enqueue(resume =>resume());q.EnqueueImmediateConsuming(()=>{/* No-op */});q.EnqueueDelay(this,1f);q.EnqueueImmediateConsuming((a,b,c)=>Debug.Log($"{a}, {b}, {c}"));q.Start(1);// Not all parameters need to be populated initially.// After 1 second, "2, 0.5, hi" is printed.

A useful example with network calls:

// Retrieve the `guildID` for a given `playerID`, then use that to retrieve the list of guild members:publicvoidGetGuildMembers(Playerplayer,UnityAction<List<Player>>onComplete){varq=newActionQueue<string,string,List<Player>>();q.Enqueue((resume,playerID,_,__)=>GetGuildIDForPlayer(playerID, guildID =>resume(playerID,guildID,__)));q.Enqueue((resume,playerID,guildID,_)=>GetGuildMembers(guildID, guildMembers =>resume(playerID,guildID,guildMembers)));q.EnqueueImmediateConsuming((playerID,guildID,guildMembers)=>onComplete(guildMembers));q.Start(player.ID);}

Skippable Queues:

The SkippableActionQueue variant is useful for setting up a queue that can be interrupted. For example, this could be useful if you want to play a multi-part animation that the player can skip by pressing a button.

When setting up a SkippableActionQueue, each step requires an onSkipped handler. When a queue is skipped, each step, including the currently running step, calls its onSkipped handler. If you are playing an animation, you may want onSkipped to handle cancelling the animation and setting the object to its completed state. If the step does not require cancelling, you can pass null for onSkipped.

SkippableActionQueue includes the following two additional methods for the control flow:

q.EnqueueSkipCheckpoint();
// Skip to the next checkpoint. If no checkpoint is defined, skip to the end of the queue:q.SkipToNextCheckpoint();

Example:

// This sets up an example post-game results scene that the player can skip through:privateSkippableActionQueueq=newSkippableActionQueue();publicvoidPlayPostGameAnimations(){// Show the 'scorePopup':q.Enqueue(scorePopup.PlayScoreIncrementAnimation,onSkipped:scorePopup.CancelAndHide);q.EnqueueImmediateConsuming(logic.IncrementScore);// Immediate steps always execute, even if skippedq.EnqueueSkipCheckpoint();// Show the 'achievementsPopup':q.Enqueue(achievementsPopup.PlayAchievementEarnedAnimation,onSkipped:achievementsPopup.CancelAndHide);q.EnqueueImmediateConsuming(logic.RecordAchievements);// Immediate steps always execute, even if skippedq.EnqueueSkipCheckpoint();// Finally, show the 'playAgainPopup':q.Enqueue(playAgainPopup.ShowWithAnimation,onSkipped:playAgainPopup.ShowImmediately);q.Start();}publicvoidOnPlayerInput(){q.SkipToNextCheckpoint();}

The SkippableActionQueue supports all features of ActionQueue, including parameters:

varq=newSkippableActionQueue<int,string>();q.Enqueue((resume,a,b)=>{/* lock, for illustration */},onSkipped:(a,b)=>(a+1,b+"!"));// onSkipped is `null`, so the parameters will not be incremented if this is skipped:q.Enqueue((resume,a,b)=>resume(a+1,b+"?"),onSkipped:null);q.EnqueueSkipCheckpoint();q.EnqueueImmediateConsuming((a,b)=>Debug.Log($"{a}, {b}"));q.Start(10,"hello");q.SkipToNextCheckpoint();// "11, 'hello!'" is printed.

API Overview (Public Surface):

usingUnityEngine;usingUnityEngine.Events;namespaceCom.Uken.ActionQueue{// 0-parampublicclassActionQueue{publicvoidStart();publicvoidEnqueue(UnityAction<UnityAction>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}// 1-parampublicclassActionQueue<T>{publicvoidStart(TinitialParam=default);publicvoidEnqueue(UnityAction<UnityAction,T>action,System.Func<bool>conditional=null);publicvoidEnqueue(UnityAction<UnityAction<T>,T>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityAction<T>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}// 2-parampublicclassActionQueue<T1,T2>{publicvoidStart(T1initialParam1=default,T2initialParam2=default);publicvoidEnqueue(UnityAction<UnityAction,T1,T2>action,System.Func<bool>conditional=null);publicvoidEnqueue(UnityAction<UnityAction<T1,T2>,T1,T2>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityAction<T1,T2>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}// 3-parampublicclassActionQueue<T1,T2,T3>{publicvoidStart(T1initialParam1=default,T2initialParam2=default,T3initialParam3=default);publicvoidEnqueue(UnityAction<UnityAction,T1,T2,T3>action,System.Func<bool>conditional=null);publicvoidEnqueue(UnityAction<UnityAction<T1,T2,T3>,T1,T2,T3>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityAction<T1,T2,T3>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}// Skippable (adds onSkipped, checkpoints, skipping)publicclassSkippableActionQueue{publicvoidStart();publicvoidEnqueue(UnityAction<UnityAction>action,UnityActiononSkipped,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidEnqueueSkipCheckpoint(System.Func<bool>checkIfCheckpointIsActive=null);publicvoidSkipToNextCheckpoint();publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}publicclassSkippableActionQueue<T>{publicvoidStart(TinitialParam=default);publicvoidEnqueue(UnityAction<UnityAction,T>action,UnityAction<T>onSkipped,System.Func<bool>conditional=null);publicvoidEnqueue(UnityAction<UnityAction<T>,T>action,System.Func<T,T>onSkipped,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityAction<T>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidEnqueueSkipCheckpoint(System.Func<bool>checkIfCheckpointIsActive=null);publicvoidSkipToNextCheckpoint();publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}publicclassSkippableActionQueue<T1,T2>{publicvoidStart(T1initialParam1=default,T2initialParam2=default);publicvoidEnqueue(UnityAction<UnityAction,T1,T2>action,UnityAction<T1,T2>onSkipped,System.Func<bool>conditional=null);publicvoidEnqueue(UnityAction<UnityAction<T1,T2>,T1,T2>action,System.Func<T1,T2,(T1,T2)>onSkipped,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityAction<T1,T2>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidEnqueueSkipCheckpoint(System.Func<bool>checkIfCheckpointIsActive=null);publicvoidSkipToNextCheckpoint();publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}publicclassSkippableActionQueue<T1,T2,T3>{publicvoidStart(T1initialParam1=default,T2initialParam2=default,T3initialParam3=default);publicvoidEnqueue(UnityAction<UnityAction,T1,T2,T3>action,UnityAction<T1,T2,T3>onSkipped,System.Func<bool>conditional=null);publicvoidEnqueue(UnityAction<UnityAction<T1,T2,T3>,T1,T2,T3>action,System.Func<T1,T2,T3,(T1,T2,T3)>onSkipped,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityAction<T1,T2,T3>action,System.Func<bool>conditional=null);publicvoidEnqueueImmediateConsuming(UnityActionaction,System.Func<bool>conditional=null);publicvoidEnqueueDelay(MonoBehaviourmono,floatdelay,System.Func<bool>conditional=null);publicvoidEnqueueSkipCheckpoint(System.Func<bool>checkIfCheckpointIsActive=null);publicvoidSkipToNextCheckpoint();publicvoidPause();publicvoidUnpause();publicvoidCancel();publicboolPaused{get;}}}

About

A lightweight, generic action sequencer for Unity.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages