Inspired by Renaud’s neat little waiting system, I decided to have a go at making a nicer task/timing system in Unity. I had three basic requirements:
- Garbage should not be allocated from the core. Use of lambdas will cause some garbage but that should be up to the caller to decide if they want to use it.
- Task management should be scoped. If I learned anything with my old timing systems is that there are times when you need some things to stop while others keep going. For example, the game may be using some timers but you want them to stop when the pause menu comes up (which may use its own timers).
- It should be easy to chain together tasks without huge nested lambdas.
To that end I worked over the last day or so trying out different systems. Eventually I landed on my current TaskManager system which feels pretty good to me. It’s a fluent API to make things fun and interesting and meets all the criteria above. The only thing that’s not incredibly convenient is repeating tasks, but they’re easy enough to implement with named functions.
To solve the scope problem I made the main TaskManager a component you can attach to any object you want. So you can have multiple task managers and scope them however you want. However to solve the easy/prototype case of having global access, the first TaskManager that gets Awake() called on it will be considered the “main” task manager and can be accessed through the static “main” field on TaskManager. I’m sure there are better ways of flagging the task manager as main, but this was the quickest and easiest solution.
For my demo I have three scenarios I coded up. The first is about the simplest it gets. I simply start a timer for 2 seconds and when that’s done I turn off the light.
public class Demo1 : MonoBehaviour
{
void Start()
{
// wait for two seconds and then turn off the light
TaskManager.main.WaitFor(2f)
.Then(() => DemoManager.demoLight.enabled = false);
}
}
The second demo builds on the first using a named method in order to create an infinitely repeating task to toggle the lights. The nice thing here is you could easily put checks into ToggleLight for things like repeat count so that you repeat only as long as you’d like. As I mentioned above, it’s nice but not the most convenient. However I couldn’t figure out how to add repeating into the fluent API in a clear way.
One other note is that you can use the Then method as many times as you’d like and the callbacks fire in that order. This is nice because you don’t need one big lambda with all your callback logic but can instead chain up multiple methods. It makes it nice if you ever want to take out just some small piece of callback logic.
public class Demo2 : MonoBehaviour
{
void Start()
{
// call our method to start the loop of actions
ToggleLight();
}
void ToggleLight()
{
// wait for two seconds, toggle the light, and then call
// the method again so it repeats forever
Light l = DemoManager.demoLight;
TaskManager.main.WaitFor(2f)
.Then(() => l.enabled = !l.enabled)
.Then(ToggleLight);
}
}
The last demo shows some of the real power of the fluent API. In this demo I wait for two seconds and then change the light color to some random value. Next I use the WaitUntil to animate the cube in a sine wave on the X axis. Once that completes the light turns off. The fluent API makes this pretty readable and avoids nesting a bunch of lambdas which can be quite annoying and hard to manage.
public class Demo3 : MonoBehaviour
{
void Start()
{
// wait for two seconds, then change the light to a random color,
// then wait for the cube to make a little sine pattern,
// and then turn off the light
Light l = DemoManager.demoLight;
Transform c = DemoManager.demoCube;
TaskManager.main.WaitFor(2f)
.Then(() => l.color = RandomColor())
.ThenWaitUntil(t =>
{
c.position = new Vector3(Mathf.Sin(t) * 3f, 0.5f, 0);
return t >= Mathf.PI * 2f;
})
.Then(() => l.enabled = false);
}
Color RandomColor()
{
return new Color(Random.value, Random.value, Random.value);
}
}
Pretty neat, huh?
You can either snag TaskManager.cs or you can import TaskManager.unitypackage which contains all the demo code and a simple drop-in prefab for the TaskManager.