Managing Async Actions
Every game has to deal with async operations. How does your code-base know when one operation is done, so an other can then begin? Think of these cases:
- Button clicked.
- Animation finished.
- Sound effect finished.
- Particle effect animation finished.
- Wait for an arbitrary amount of time.
- Other any other similar wait action.
For Unity games the most common solution is to use coroutines. They work fine for simple cases but if the game needs to wait on multiple async operations, or even an unknown number it quickly breaks down into a mess.
Use Promises
When developing Rifle Storm, I decided to use this C Sharp Promise library (it's quite amazing). It creates a new class called Promise
, very similar to Javascript promises and it makes handling any async call a breeze.
These promises are used everywhere in the games code-base, I wouldn't start a new C# project without it.
The Basic Flow
A basic promise flow looks like this:
var promise = entity.PlayJump();
promise.Done(() => {
Debug.Log("finished!");
});
// Entity.cs
private IPromise promise;
public IPromise PlayJump() {
// save for later
promise = new Promise();
animator.SetTrigger("...");
return promise;
}
void OnAnimationComplete() {
promise.Resolve();
}
Note
All promises must have a Done call at the end, or else errors will not be caught.
Chain Promises
The real power comes from chaining promises back to back. Once the first promise resolves, then the next will run. This simplifies the whole process of running async actions, now all you need to do is make all your async methods return promises. Once you have a promise being returned, it's very easy to chain them up.
entity.PlayJump()
.Then(entity.PlayJump())
.Then(entity.PlayJump())
.Done(() => {
// ...
});
Handle An Unknown Sequence Of Operations
Here is a real example from Rifle Storm. When entities get healed from a healing ability, they are added to a list called triggerHealed
. This list can contain any number of entities.
Afterwards this list then gets looped through and for each one runs the TriggerHealed
method. Because you won't know how many entities got healed from the ability, it's not possible to hardcode all the .Then
calls, you'll have to loop through the list.
This would be a real headache to do with only coroutines. But it's quite simple with promises.
// loop through all heal entities one by one calling TriggerHealed for each
var healReactions = triggerHealed.Select(en => (Func<IPromise>)(() => TriggerHealed(currentEntity, en)));
// promise resolves when all promises in the list a resolved
return Promise.Sequence(healReactions);
// method
public IPromise TriggerHealed(IEntity attacker, IEntity defender) {
var promise = new Promise();
// code here...
return promise;
}
Some Examples
Promises are used extensively throughout Rifle Storm, here are a few examples to give you ideas on how they can be used in your own game.
Weapon Attacks
Some weapons in the game can apply Fire, this method will apply fire if it's able to. If it can, then it'll run an effect on the defender
, this effect will then run animations, particle effects, and maybe even kill the target. All these things must complete before the original soldiers attack can be completed.
public IPromise ApplyIgnite(IEntity attacker, IEntity defender, Weapon weapon, int finalDamage) {
if (attacker.equip.HasVal(Stat.Ignite)) {
igniteEffect.statusPower = (int)(finalDamage * attacker.equip.GetMod(Stat.Ignite));
return effectUtil.StartEffect(attacker, igniteEffect, defender.iso.tile);
}
return Promise.Resolved();
}
Color Tweens
They can be easily integrated into other system, for example this is DoTween.
public IPromise ToColor(Color color, float time = 0.5f, float tintPercent = 0.5f) {
var promise = new Promise();
DOTween.To(() => outfit.GetTintPercent(), (v) => outfit.SetTintPercent(v), tintPercent, time);
DOTween.To(() => outfit.GetColor(), (v) => outfit.SetColor(v), color, time)
.OnComplete(promise.Resolve);
return promise;
}
Squad Select Window
Promises can even be used for basic UI actions, for example it's used to wait for a users selection. This waits for a user to select a squad, if one isn't select then the squadData
is null.
selectWindow.Show()
.Done((squadData) => {
if (squadData == null) {
// canceled
} else {
// squad chosen
}
});
More Info
Some useful links:
Back to tutorials