If you are familiar with Unity, you may be used to work with coroutines. These are special methods whose execution can be suspended at specific points and resumed after a condition is met. They are handy for executing a sequence of steps.
In this tutorial I will show code for providing similar functionality in Phaser. We will use Javascript generator functions and build a small coroutine manager around it.
Generator functions
Regular functions look like this:
function foo() {
console.log("Hi!");
}
While generator functions look like this:
function* foo() {
console.log("Hi!");
}
The asterisk is separate symbol and can be placed close to function keyword function* foo(
) or close to function name function *foo()
.
When you call regular foo() function, it is executed immediately and “Hi!” shows in the console output. When you call the version with asterisk, nothing happens. It is because Generator object is returned instead of immediate execution and you have to call next() on it to start the execution:
const generator = foo();
generator.next();
So, execution is deferred until you call next(). Still nothing super useful. But, you can put keyword yield
in the middle of your generator function and then interesting things start to happen:
function* foo() {
console.log("Hi!");
yield;
console.log("Bye!");
}
Now, when you call next() for the first time, the execution of foo starts and “Hi!” is printed to the console as before. Then, when yield keyword is reached, the execution is suspended and you have to call next() second time to print “Bye!”. In simple words, you have to call next() in the beginning and then after every yield to continue the execution. Imagine, that yield can be also inside the for
or while
loop and you can suspend the execution after each iteration.
Yield
can also return value. We will use it soon to set how to interrupt the execution – how long to wait, wait for a condition, etc.
I will not go here into details, there is a lot of tutorials on the web. Later we will encounter some other features of generator functions. Important is, that these functions will be the base block of coroutine system.
Coroutine
Our coroutine will be a little different from the one in Unity. We will be able to delay initial execution, we will be able to delay every yield step, we will be able to pause / resume it and we will fire some events on every step and when coroutine is finished.
First create small class Yielder
. This class has one abstract property and we will return to it later. It is an object that coroutines will return with yield instruction. But as we also can yield null
, there is no need to implement it now. The delay property is more interesting now. Unlike in Unity, we can add extra delay to every type of yielder.
export abstract class Yielder {
private _delay: number;
public get delay(): number { return this._delay; }
public set delay(delay: number) { this._delay = delay; }
public abstract get keepWaiting(): boolean;
// -------------------------------------------------------------------------
public constructor(delay: number = 0) {
this._delay = delay;
}
}
Create file for Coroutine
class and put this line at the beginning:
export type YieldCallback = (coroutine: Coroutine, yielder: Yielder, step: number) => void;
This just defines type of methods, that can listen to step events fired from Coroutine
. These events are fired whenever yield instruction is processed.
Continue with Coroutine
class itself – with its fields and empty methods. We will step them one by one and add implementation:
export class Coroutine {
public static readonly StepEvent = "yieldStep";
public static readonly FinishedEvent = "finished";
private static _idCounter: number = 0;
private _id: number;
private _running: boolean;
private _finished: boolean;
private _generator: Generator<Yielder>;
private _yielder: Yielder;
private _delay: number;
private _step: number;
private _events: Phaser.Events.EventEmitter;
public get id(): number { return this._id; }
public get running(): boolean { return this._running; }
public get finished(): boolean { return this._finished; }
public get step(): number { return this._step; }
public get yielder(): Yielder { return this._yielder; }
public get events(): Phaser.Events.EventEmitter { return this._events; }
// -------------------------------------------------------------------------
public constructor() { }
public start(generator: Generator<Yielder>, delay: number = 0, paused: boolean = false,
yieldCallback: YieldCallback = null, yieldCallbackCtx: any = null): Coroutine { }
public stop(): void { }
public pause(): void { }
public resume(): void { }
private next(): void { }
public tick(deltaSec: number): boolean { }
}
Going through it:
StepEvent
andFinisedEvent
are constants for event names,_idCounter
is static counter of all started coroutines. It is incremented when coroutine is started and stored in_id
. This is help when debugging and multiple coroutines are running in parallel,_running
and_finished
are internal states of coroutine._finished
coroutine went to its end while_running
tells whether the execution is paused or not,_generator
is reference to generator function. Notice, it’s type is Generator<Yielder>. It says that we have to return Yielder object (or null) when calling yield._yielder
is reference to last Yielder object,_delay
stores remaining delay time before initial execution or next step is taken,- in
_step
we count yield instructions, _events
is standard Phaser events emitter
Next, the implementation of methods is not in order. We will start with ones, that are not interesting:
// -------------------------------------------------------------------------
public constructor() {
this._events = new Phaser.Events.EventEmitter();
}
// -------------------------------------------------------------------------
public stop(): void {
this._finished = true;
this._running = false;
this._events.removeAllListeners();
}
// -------------------------------------------------------------------------
public pause(): void {
this._running = false;
}
// -------------------------------------------------------------------------
public resume(): void {
this._running = true;
}
We are only changing here coroutine states and creating / clearing events. start()
method is more interesting:
// -------------------------------------------------------------------------
public start(generator: Generator<Yielder>, delay: number = 0, paused: boolean = false,
yieldCallback: YieldCallback = null, yieldCallbackCtx: any = null): Coroutine {
this._id = Coroutine._idCounter++;
this._generator = generator;
this._yielder = null;
this._delay = delay;
this._step = 0;
this._finished = false;
this._running = !paused;
if (yieldCallback) {
this._events.on(Coroutine.StepEvent, yieldCallback, yieldCallbackCtx);
}
if (delay === 0 && !paused) {
this.next();
}
return this;
}
Here we assign new id to coroutine we are starting. Then we save reference to generator function and do some initial setup. Except for generator, other arguments are optional with some default value. We can optionally delay initial execution. Or start coroutine in paused state, so no initial execution takes place. Just to be clear, by initial execution I call execution of generator function until the first yield instruction.
As starting unpaused coroutine immediately starts execution, we have to set step event callback here, so we do not miss the first event fired when the first yield is found.
If coroutine start is not delayed and coroutine is not paused, we call generator function next()
to execute the beginning of it.
Next is tick()
method. This method is called every frame. In fact, we will not call it directly when we build coroutines manager. We will call it directly only as a part of some tests soon.
// -------------------------------------------------------------------------
public tick(deltaSec: number): boolean {
if (!this._running) {
return false;
}
if (this._delay > 0 && (this._delay -= deltaSec) > 0) {
return false;
}
if (this._yielder?.keepWaiting) {
return false;
}
this.next();
return true;
}
I believe, code here is easy to understand. If coroutine is paused (not running), nothing is done. If coroutine is delayed and decrease of the delay didn’t get it to zero, nothing is done. If there is yielder and it still says “keep waiting”, nothing is done. If yielder is null, which happens when we use yield null
in generator function, we move to next step as it means we were a frame before instructed to wait single frame, which is now. Series of yield null
splits the execution between consecutive frames. We return true from the method only if next step was taken.
In private next()
method we do the actual step:
// -------------------------------------------------------------------------
private next(): void {
const res = this._generator.next();
if (!res.done) {
this._yielder = res.value;
if (this._yielder && this._yielder.delay > 0) {
this._delay = this._yielder.delay;
}
this._events.emit(Coroutine.StepEvent, this, this._yielder, this._step);
++this._step;
} else {
this._finished = true;
this._events.emit(Coroutine.FinishedEvent, this);
}
}
First, we call generators next()
method. From the returned result we know, whether whole generator function finished and we can finish the coroutine. If not, we take a new yielder and we take delay from it and store it into coroutine. Then we fire event notifying listeners, there was a next step. If generator function is finished, we mark coroutine as finished too and fire finish event.
Why we are storing the yielder delay inside the coroutine? This is kind of optimization. Yielder doesn’t hold any changing values. Delay is decremented inside tick()
method of coroutine. Therefore, we can reuse the same Yielder multiple times and not create new objects every time.
First test
Set your test scene like this and run it:
export class Test extends Phaser.Scene {
private _coroutine: Coroutine;
// -------------------------------------------------------------------------
public create(): void {
this.doTest();
}
// -------------------------------------------------------------------------
private doTest(): void {
this._coroutine = new Coroutine()
this._coroutine.start(this.test());
}
// -------------------------------------------------------------------------
public *test(): Generator<Yielder> {
const loop = this.game.loop;
console.log(`Hi! at time ${loop.time} / frame ${loop.frame}`);
yield null;
console.log(`Working! at time ${loop.time} / frame ${loop.frame}`);
yield null;
console.log(`Bye! at time ${loop.time} / frame ${loop.frame}`);
}
// -------------------------------------------------------------------------
public override update(time: number, deltaMS: number): void {
this._coroutine.tick(deltaMS / 1000);
}
}
You may get result similar to this in browser console:
Hi! at time 657.5399999999998 / frame 6
Working! at time 657.5399999999998 / frame 6
Bye! at time 674.2399999999999 / frame 7
Hmmm… not bad, but why the first and the second line were both executed on frame 6? There was yield null
between them, so we wanted to wait for the next frame. The same as between the second and the third line and there we see, that there is one frame delay.
The reason is, we created coroutine in Phaser’s create()
method and update()
method was called on the same frame. We got two calls to coroutines next()
method. First time when starting the coroutine and second time in its tick()
method. We will solve this later with coroutine manager. For now, you can start coroutine with delay, let’s say 2 seconds this._coroutine.start(this.test(), 2);
and you will get this result:
Hi! at time 2730.875999994039 / frame 127
Working! at time 2747.475999994039 / frame 128
Bye! at time 2764.1759999940386 / frame 129
The result is correct. With the initial delay we skipped the initial execution inside create()
method.
Coroutines manager
In previous test we created single coroutine. It was new Coroutine
instance, that was updated during update()
method. Now, we will build manager class, that will manage coroutines during their lifetime. It will also handle additional things. It will allow us to run coroutine not only in update()
, but also in postUpdate()
. We will also solve the issue with running initial execution and the first step in the same frame.
As the first step, let’s add a new property into existing Coroutine class:
private _startFrame: number;
public get startFrame(): number { return this._startFrame; }
public set startFrame(startFrame: number) { this._startFrame = startFrame; }
And also update the Yielder
class with addition of a new property executeAt
:
export abstract class Yielder {
private _executeAt: CoroutineExecutionAt;
private _delay: number;
public get executeAt(): CoroutineExecutionAt { return this._executeAt; }
public set executeAt(executeAt: CoroutineExecutionAt) { this._executeAt = executeAt; }
public get delay(): number { return this._delay; }
public set delay(delay: number) { this._delay = delay; }
public abstract get keepWaiting(): boolean;
// -------------------------------------------------------------------------
public constructor(delay: number = 0, executeAt: CoroutineExecutionAt = CoroutineExecutionAt.Update) {
this._executeAt = executeAt;
this._delay = delay;
}
}
Coroutines manager will store here the game frame in which the coroutine started.
Create new file for CoroutinesManager
class and put this new enum on the top of it:
export enum CoroutineExecutionAt {
Update,
PostUpdate
}
Here is CoroutinesManger
class with empty methods. These will be implemented one by one:
export class CoroutinesManager {
private static readonly InitialCoroutinePoolCapacity = 20;
private _game: Phaser.Game;
private _coroutinesPool: Pool<Coroutine>;
private _running: { [key in CoroutineExecutionAt]: Coroutine[] } = {
[CoroutineExecutionAt.Update]: [],
[CoroutineExecutionAt.PostUpdate]: []
};
public constructor(game: Phaser.Game) { }
public start(generator: Generator<Yielder>, delay: number = 0, paused: boolean = false,
yieldCallback: YieldCallback = null, yieldCallbackCtx: any = null): Coroutine { }
public stop(coroutine:Coroutine): void { }
public stopAll(): void { }
public tick(deltaSec: number, executeAt: CoroutineExecutionAt): void { }
private storeCoroutine(coroutine: Coroutine): void { }
}
Important note here: my implementation uses my custom Pool
class for reusing Coroutine
instances. I will not list it here, as it has nothing to do with coroutines. You can use any pool implementation you are used to. You can even not use any, but using one makes the whole thing more performance friendly. I will note this whenever the implementation works with pool.
In the code above we store reference to the running game, keep a reference to the pool of coroutines (if you are not using any, you can delete this line) and have reference to object _running
. This object is dictionary, where key is moment in game loop when the coroutine shall be updated and value is array of all live coroutines. You can later update the manager to run coroutines also during other game loop events like before rendering, after rendering, etc.
Constructor is simple:
// -------------------------------------------------------------------------
public constructor(game: Phaser.Game) {
this._game = game;
this._coroutinesPool = new Pool<Coroutine>(Coroutine, CoroutinesManager.InitialCoroutinePoolCapacity);
this._coroutinesPool.canGrow = true;
}
Again, if you decided to not use any pool for Coroutine instances, you can delete last two lines.
start() method is used to start new coroutine and also do some additional work:
// -------------------------------------------------------------------------
public start(generator: Generator<Yielder>, delay: number = 0, paused: boolean = false,
yieldCallback: YieldCallback = null, yieldCallbackCtx: any = null): Coroutine {
const coroutine = this._coroutinesPool.spawn();
if (!coroutine) {
throw new Error(`No free coroutines in pool.`);
}
coroutine.startFrame = this._game.loop.frame;
coroutine.start(generator, delay, paused, yieldCallback, yieldCallbackCtx);
if (coroutine.finished) {
this.stop(coroutine);
return null;
}
this.storeCoroutine(coroutine);
return coroutine;
}
// -------------------------------------------------------------------------
private storeCoroutine(coroutine: Coroutine): void {
const executeAt = coroutine.yielder?.executeAt ?? CoroutineExecutionAt.Update;
this._running[executeAt].push(coroutine);
}
First, if you are not using any pool for coroutines, change the top of the method to const coroutine = new Coroutine();
Then we use the new property startFrame
of coroutine and save the current game frame. Next, we just pass all the parameters to start()
method of coroutine. If, for some reason the coroutine finished immediately after its initial execution, we call stop()
method of the manager and return null – there is no need to manage this coroutine in future. In most cases the coroutine will be still alive, so we call simple helper method storeCoroutine()
to store it into the right array of the dictionary with running coroutines.
Next let’s implement stop()
method to stop a single coroutine and stopAll()
method to stop all running coroutines.
// -------------------------------------------------------------------------
public stop(coroutine:Coroutine): void {
coroutine.stop();
this._coroutinesPool.despawn(coroutine);
}
// -------------------------------------------------------------------------
public stopAll(): void {
for (let key in this._running) {
const coroutines = this._running[key];
while (coroutines.length > 0) {
const coroutine = coroutines.pop();
this.stop(coroutine);
}
}
}
If you are not using pool, you can delete the line that calls despawn in stop()
method. StopAll()
method just iterates through all arrays of running coroutines and stops them one by one.
The last method is tick()
. We will call it from scene instead of calling tick()
of individual coroutines. As our coroutines can now run not only in update()
, but also in postUpdate()
, we have to call it from both these methods.
// -------------------------------------------------------------------------
public tick(deltaSec: number, executeAt: CoroutineExecutionAt): void {
const frame = this._game.loop.frame;
const coroutines = this._running[executeAt];
for (let i = coroutines.length - 1; i >= 0; i--) {
const coroutine = coroutines[i];
if (executeAt === CoroutineExecutionAt.Update && coroutine.startFrame === frame) {
continue;
}
if (coroutine.tick(deltaSec)) {
if (coroutine.finished) {
coroutines.splice(i, 1);
this.stop(coroutine);
} else if (!coroutine.yielder || coroutine.yielder.executeAt !== executeAt) {
coroutines.splice(i, 1);
this.storeCoroutine(coroutine);
}
}
}
}
Inside the loop we solved the problem with running initial execution and the first step in the same frame. We skip tick if the current frame is the same as the frame in which the coroutine started. We check this only for coroutines updated from update()
, because it is valid to tick single coroutine twice in the same frame in some cases – imagine, you start coroutine in update and the first yielder says “wait for end of frame”, so you want to run it again during postUpdate.
In the loop we check if coroutine finished its job and also if the moment in which we want to update it changed – for example from update to postUpdate.
Second test
Here is complete listing of a new test class:
export class Test extends Phaser.Scene {
private _coroutines: CoroutinesManager;
// -------------------------------------------------------------------------
public create(): void {
this._coroutines = new CoroutinesManager(this.game);
this.events.on(Phaser.Scenes.Events.POST_UPDATE, this.postUpdate, this);
this.events.once(Phaser.Scenes.Events.SHUTDOWN, this.shutdown, this);
this.doTest();
}
// -------------------------------------------------------------------------
private doTest(): void {
this._coroutines.start(this.test());
}
// -------------------------------------------------------------------------
public *test(): Generator<Yielder> {
const loop = this.game.loop;
console.log(`Hi! at time ${loop.time} / frame ${loop.frame}`);
yield null;
console.log(`Working! at time ${loop.time} / frame ${loop.frame}`);
yield null;
console.log(`Bye! at time ${loop.time} / frame ${loop.frame}`);
}
// -------------------------------------------------------------------------
public override update(time: number, deltaMS: number): void {
this._coroutines.tick(deltaMS / 1000, CoroutineExecutionAt.Update);
}
// -------------------------------------------------------------------------
public postUpdate(time: number, deltaMS: number): void {
this._coroutines.tick(deltaMS / 1000, CoroutineExecutionAt.PostUpdate);
}
// -------------------------------------------------------------------------
public shutdown(): void {
this._coroutines.stopAll();
}
}
What is different is, that in create()
we are hooking to Phaser’s POST_UPDATE
and SHUTDOWN
events. In doTest()
we start new coroutine through CoroutinesManager
.
If you run it, you will notice, that our problem with frames is gone:
Hi! at time 764.3099999910593 / frame 7
Working! at time 781.1099999910592 / frame 8
Bye! at time 797.6099999910592 / frame 9
If you are adventurous, you can start multiple coroutines and watch how it runs in parallel!
Yielders
Up until now we had only abstract Yielder
and as an abstract class can’t be instantiated, we yielded null, which stands for “wait for next frame”. Now, we will add several yielders. You can later implement your own.
The first one is WaitForSeconds
. Make a new class with this content:
export class WaitForSeconds extends Yielder {
// -------------------------------------------------------------------------
public constructor(delay: number, executeAt: CoroutineExecutionAt = CoroutineExecutionAt.Update) {
super(delay, executeAt);
}
// -------------------------------------------------------------------------
public override get keepWaiting(): boolean {
return false;
}
// -------------------------------------------------------------------------
public setDelay(delay: number): WaitForSeconds {
this.delay = delay;
return this;
}
}
Change *test()
method in testing scene to:
public *test(): Generator<Yielder> {
const loop = this.game.loop;
const wait = new WaitForSeconds(2);
console.log(`Hi! at time ${loop.time} / frame ${loop.frame}`);
yield wait;
console.log(`Working! at time ${loop.time} / frame ${loop.frame}`);
yield wait.setDelay(5);
console.log(`Bye! at time ${loop.time} / frame ${loop.frame}`);
}
Now, if you run the test scene, Hi! is printed immediately. After 2 seconds is printed Working! Then we change the delay to 5 seconds and Bye! is printed when this delay expires.
You can also play with parameters, when starting the coroutine and, for example, add initial delay. Or you can start the coroutine paused and resume it on key press. Or you can add event listeners for step and finish events like this:
private doTest(): void {
const coroutine = this._coroutines.start(this.test(), 2, false,
function (this: Test, coroutine: Coroutine, yielder: Yielder, step: number) {
console.log(`coroutine #${coroutine.id} - step ${step}`);
}, this);
coroutine.events.on(Coroutine.FinishedEvent, function (this: Test, coroutine: Coroutine) {
console.log(`coroutine #${coroutine.id} - finished`);
}, this);
}
Next yielder is WaitForEndOfFrame
. This will execute next part of coroutine call during postUpdate. Unlike in Unity, we can add delay to every kind of yielder. So, you can set delay
property of the base class if it makes sense to you. Code looks like this:
export class WaitForEndOfFrame extends Yielder {
// -------------------------------------------------------------------------
public constructor() {
super(0, CoroutineExecutionAt.PostUpdate);
}
// -------------------------------------------------------------------------
public override get keepWaiting(): boolean {
return false;
}
}
Test it like this:
public *test(): Generator<Yielder> {
const loop = this.game.loop;
const wait = new WaitForSeconds(2);
console.log(`Hi! at time ${loop.time} / frame ${loop.frame}`);
yield wait;
console.log(`Working! at time ${loop.time} / frame ${loop.frame}`);
yield new WaitForEndOfFrame();
console.log(`Bye! at time ${loop.time} / frame ${loop.frame}`);
}
Notice, that Working! and Bye! are printed on the same frame. It is OK. Working! was printed during update and Bye! during postUpdate.
Finally, let’s add two more yielders – WaitWhile
and WaitUntil
. Code for both is here:
export class WaitWhile extends Yielder {
private _predicate: () => boolean;
// -------------------------------------------------------------------------
public constructor(predicate: ()=> boolean, executeAt: CoroutineExecutionAt = CoroutineExecutionAt.Update) {
super(0, executeAt);
this._predicate = predicate;
}
// -------------------------------------------------------------------------
public override get keepWaiting(): boolean {
return this._predicate();
}
}
export class WaitUntil extends Yielder {
private _predicate: () => boolean;
// -------------------------------------------------------------------------
public constructor(predicate: () => boolean, executeAt: CoroutineExecutionAt = CoroutineExecutionAt.Update) {
super(0, executeAt);
this._predicate = predicate;
}
// -------------------------------------------------------------------------
public override get keepWaiting(): boolean {
return !this._predicate();
}
}
These yielders expect predicate in constructor. Then, coroutine every frame checks (if not paused or in delay), if the predicate is true or false. Test it with following code:
public *test(): Generator<Yielder> {
const loop = this.game.loop;
const wait = new WaitForSeconds(2);
console.log(`Hi! at time ${loop.time} / frame ${loop.frame}`);
yield wait;
console.log(`Working! at time ${loop.time} / frame ${loop.frame}`);
yield new WaitUntil(() => {
const pointerInCorner = this.game.input.activePointer.x < 40 && this.game.input.activePointer.y < 40;
if (pointerInCorner) {
console.log("Mouse pointer is in top left corner - we can continue...");
}
return pointerInCorner;
});
console.log(`Bye! at time ${loop.time} / frame ${loop.frame}`);
}
First, wait till Working! is printed into console. Then move mouse pointer into the very top-left corner of the game view to continue.
Conclusion
With coroutines created in this tutorial, you can achieve not only execution in timed steps, but also kind of parallel multitasking. It is manager, that is doing the hard work and through events you can be informed about the progress.
I can imagine using it, for example, for tutorials, where player is asked to do series of steps. Or for end level sequences that do various tasks one after each other – count up score, show stars animation, do particle effect, animate buttons, enable buttons input, …