Hello people. I’ve written a small library for control flow logic I find useful in my own projects. I’m putting this up here in case anyone else finds this useful.
CommandQueue
CommandQueue is a BSD licensed C# control flow library designed to simplify writing complex timed logic in Unity. The library is modular and highly extensible. It’s design is similar to the Cocos2d, or LibGDX Action systems.
Why?
I’ve seen a lot of unity code which uses coroutines for complex programmatic animations. Coroutines can be a bad choice for this, as they can suddenly be cancelled when a monobehaviour is disabled, potentially leaving the object is a difficult to recover from state. It can also be hard to express logic which should operate in parallel,. So I wrote a small Command system, where you can sequence and schedule commands, and update them from within a monobehaviour. I’ve also included a small tweening engine to give an example of how this kind of system is useful. Anyway, it’s a bit of a thought experiment on how to clean up and simplify code. I’m still looking for feedback, so If anyone finds this useful or lacking crucial features, let me know.
Basic Usage
Below is a simple tweening example:
void Start()
{
CommandQueue queue = new CommandQueue();
// In sequence, move the current gameObject to (10.0f, 0.0f, 0.0f) over 8 seconds, wait
// 2 seconds, then translate by -5 units in the x axis over 4 seconds.
queue.Enqueue(
Commands.MoveTo(gameObject, new Vector2(10.0f, 0.0f, 0.0f), 8.0f),
Commands.Wait(2.0f),
Commands.MoveBy(gameObject, new Vector2(-5.0f, 0.0f, 0.0f), 4.0f, Ease.InOutHermite())
);
StartCoroutine(queue.WaitTillFinished());
}
CommandQueue executes commands sequentially. CommandStack is also available. Alternatively, to
execute commands in parallel, you can do the following:
void Start()
{
CommandQueue commandQueue = new CommandQueue();
commandQueue.Enqueue(
Commands.Parallel(
Commands.MoveTo(gameObject, new Vector2(10.0f, 0.0f, 0.0f), 8.0f),
Commands.TintTo(gameObject, Color.blue, 4.0f)
)
);
StartCoroutine(queue.WaitTillFinished());
}
These examples all use coroutines to update the CommandQueue. However, it is also possible to update a CommandQueue manually
private CommandQueue _queue = new CommandQueue();
void Update()
{
_queue.Update(Time.deltaTime);
}
void MoveRight()
{
_queue.Enqueue(
Commands.MoveBy(gameObject, new Vector2(5.0f, 0.0f, 0.0f), 2.0f, Ease.InOutHermite()),
);
}
void GoBlue()
{
_queue.Enqueue(
Commands.TintTo(gameObject, Color.blue, 10.0f * 60.0f, Ease.InOutHermite())
);
}
void GoGray()
{
_queue.Enqueue(
Commands.TintTo(gameObject, Color.gray, 2.0f)
);
}
void Say(string text, float duration = 5.0f)
{
_queue.Enqueue(
Commands.ActionDo( () => {
guiText.enabled = true;
guiText.text = text;
}),
Commands.Wait(duration),
Commands.ActionDo( () => {
guiText.enabled = false;
guiText.text = "";
})
);
}
void Start()
{
MoveRight();
Say("I can hold my breath for 10 whole minutes!");
GoBlue();
GoGray();
Say("*gasp*");
}
And now for a more complex example:
// Use this for initialization
void Start ()
{
_queue = new CommandQueue();
BasicAnimation();
}
// Update is called once per frame
void Update ()
{
_queue.Update(Time.deltaTime);
}
void BasicAnimation()
{
bool condition = false;
_queue.Enqueue(
Commands.ScaleTo(gameObject, 0.05f, 2.0f, Ease.OutQuart()),
Commands.ScaleTo(gameObject, 1.0f, 1.0f, Ease.OutBounce()),
Commands.RepeatForever(
Commands.Parallel(
Commands.Repeat(2,
Commands.ScaleBy(gameObject, 1.5f, 1.0f, Ease.OutBounce())
),
Commands.RotateBy(gameObject, Quaternion.Euler(180.0f,0.0f, 90.0f), 0.25f, Ease.InOutHermite())
),
Commands.WaitForSeconds(0.25f),
Commands.TintTo(gameObject, Color.red, 0.5f, Ease.InBack(0.2f)),
Commands.TintBy(gameObject, Color.blue, 0.5f),
Commands.Condition(delegate() { condition = !condition; return condition; },
Commands.MoveBy(gameObject, new Vector3(0.0f, 2.0f, 0.0f), 0.25f, Ease.InOutHermite()),
Commands.MoveBy(gameObject, new Vector3(0.0f, -2.0f, 0.0f), 0.25f, Ease.InOutHermite())
),
Commands.MoveTo(gameObject, new Vector3(0.0f, 0.0f, 0.0f), 0.25f, Ease.InOutHermite()),
Commands.Parallel(
Commands.ScaleTo(gameObject, 0.5f, 1.0f, Ease.OutBounce()),
Commands.RotateTo(gameObject, Quaternion.identity, 0.5f, Ease.InOutHermite())
),
Commands.TintTo(gameObject, Color.white, 0.25f, Ease.InOutSin()),
Commands.While(delegate(double elapsedTime) {
return elapsedTime <= 0.5f;
}),
Commands.MoveFrom(gameObject, new Vector3(0.0f, 0.0f, 0.8f), 0.5f, Ease.OutElastic()),
Commands.RotateFrom(gameObject, Quaternion.Euler(0.0f, 45.0f, 45.0f), 0.5f, Ease.InOutExpo()),
Commands.ScaleFrom(gameObject, 0.25f, 0.75f, Ease.InOutHermite()),
Commands.TintFrom(gameObject, Color.green, 0.25f, Ease.InOutQuint())
)
);
}
private CommandQueue _queue;
Coroutines
Coroutines can be useful for expressing certain kinds of logic which CommandQueue’s can’t. The main advantage of using a CommandQueue coroutine over a unity coroutine is you control how a coroutine is updated. For instance, you could choose to run a coroutine at double speed, fast forward to the end, pause/stop updating it and resume later. They look like this :
CommandQueue _queue = new CommandQueue();
IEnumerator<CommandDelegate> CoroutineMethod(int firstVal, int secondVal, int thirdVal)
{
Debug.Log(firstVal);
yield return Commands.WaitForSeconds(1.0f); // You can return any CommandDelegate here.
Debug.Log(secondVal);
yield return null; // Wait a single frame.
Debug.Log(thirdVal);
yield Commands.Coroutine( () => ASecondCoroutine()); // Launch another coroutine
// Execute whatever CommandDelegates we want in parallel, and wait for them to finish
// before returning.
yield Commands.Parallel(
Commands.Coroutine( () => AThirdCoroutine()),
Commands.Coroutine( () => AForthCoroutine())
);
yield break; // Force exits the coroutine.
}
void Start()
{
_queue.Enqueue(
Commands.Coroutine( () => CoroutineCommand(1,2,3)
);
}
void Update()
{
_queue.Update(Time.deltaTime);
}
You may have noticed the weird lambda syntax for starting a coroutine
Commands.Coroutine( () => CoroutineCommand(1,2,3));
Basically, this is to make coroutines restartable, which means you can stick it in a repeat block and expect it to execute again.
CommandScheduler
Queue’s are useful in insuring commands happen in an exact sequence and don’t overlap each other. Sometimes you want multiple things going on at once. So to support this, you can use CommandScheduler in much the same way:
CommandScheduler scheduler = new CommandScheduler();
// All three of the following will execute in parallel.
scheduler.Add(commandOne);
scheduler.Add(commandTwo);
scheduler.Add(commandThree);
scheduler.Update(Time.deltaTime);
More complex examples can be found under Tests/Examples
Download
You can clone it as a git repository with the following command:
git clone https://bitbucket.org/Darcy_Rayner/commandqueue.git
Or download the latest commit from here.
I recommend putting the top level directory in you plugins folder.
Anything Else
I’m not sure if this library is the kind of thing other unity programmers find interesting. I’m interested in hearing other people’s thoughts on writing clean maintainable code in unity, and the kinds of patterns they use to achieve this, (or conversely, the kinds of bugs other patterns may lead to). Also, this is still library is still undergoing development, so any bugs you find it would be nice to know about.
License
Modified BSD