tickId
is a unique Id counter. Each time tickCallbacks
is assigned a new object, it increments that variable, that way each function added to tickCallbacks
has a unique number assigned to it. For the same reason, tickCallbacks
is an object and not an array, as removing data from an array shifts every id by one and might break the system. Then, tick runs through every function in that object and executes it. This is going to allow us to run coroutines on every tick.
The basic idea
The basic idea of that coroutines mangement system is that when you yield a coroutine, the manager either tries to understand of a yield should wait for a specific event, or if it should just wait for one tick.
In case it should wait for a specific event, there are two cases. Either the yield returned a promise and it will wait until that promise is resolved before coming back, or the yield return a subclass of a custom class I named CoroutineYield.
Introducing a new js file: CoroutinesYield.js. Here is the content of mine
class CoroutineYield {
constructor(){
}
process(){
return new Promise((resolve, reject)=>{
resolve()
})
}
}
class WaitForSeconds extends CoroutineYield{
constructor(time){
super()
this.time = time
}
process(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve()
}, this.time*1000)
})
}
}
CoroutinesYield is a class that contains one method: process. That method returns a promise that the manager can use to know when to start the yielded coroutine again. Here the subclass WaitForSeconds's process function returns a promise that resolved after a number of seconds defined when constructing the instance.
Note that startCoroutine also returns a promise that resolves once the coroutine ends, so it can be used to pause a coroutine while waiting for another one to complete.
let RuntimeMixin = {
startCoroutine(coroutine){
const coroutineId = tickId++
return new Promise((resolve, reject) => {
function progress(){
let ret = coroutine.next()
if(ret.done){
resolve()
delete tickCallbacks[coroutineId]
} else {
let promise;
if(ret.value instanceof Promise){
promise = ret.value
} else if(ret.value instanceof CoroutineYield) {
promise = ret.value.process()
}
if(promise != undefined){
delete tickCallbacks[coroutineId]
promise.then(()=>{
tickCallbacks[coroutineId] = progress
progress()
})
}
}
}
tickCallbacks[coroutineId] = progress
progress()
})
}
}
So basically, the startCoroutine method adds a progress function to the tickCallbacks object. That progress function calls next() on the coroutine and checks the returned value. If it's a Promise or a subclass of CoroutineYield, it removes the function from Tick, waits for the promise to resolve, then adds it back and calls it again. In case yield has any other value, it is ignored and the coroutine will yield for a frame, and start again.
Note that to start a coroutine, you must do it like so:
runtime.startCoroutine(test())
This means that you can pass any arguments you want to the coroutine before starting it.
You can also run code when the coroutine ends, like so
runtime.startCoroutine(test()).then(()=>{
console.log('end of the initial call');
})
Remember these are the two coroutines we will use:
function* test() {
console.log('Hello from coroutine 1!');
yield null;
console.log('back after a frame');
yield new WaitForSeconds(1);
console.log('back after a second');
yield runtime.startCoroutine(test2());
console.log('back after coroutine 2');
console.log('end of coroutine 1');
}
function* test2() {
console.log('---Hello from coroutine 2!');
yield new WaitForSeconds(3);
console.log('---back after three second');
console.log('---end of coroutine 2');
}
And when we run test, this is the console output:
>Hello from coroutine 1!
>back after a frame
>back after a second
>---Hello from coroutine 2!
>---back after three second
>---end of coroutine 2
>back after coroutine 2
>end of coroutine 1
>end of the initial call
That's it for coroutines. Hope you'll make good use of them 😄