I'm writing an application in nodejs based around a game loop. Each iteration of the loop, it fires an event emitter, and calls an update method like so:
updateLoop() {
while (!this.windowShouldClose()) {
this.onUpdate.emit();
this.update();
}
}
It seemed like the event emitter could help to write async functions on game objects that wait frame(s) between different operations, much like a coroutine. I wrote a utility function that would call back, or resolve a promise on the next emit of the event emitter:
nextFrame() {
return new Promise((resolve) => {
this.onUpdate.once(resolve); // event emitter will resolve promise on next emit()
})
}
nextFrameCallback(callback) {
this.onUpdate.once(callback);
}
// example use
async doThingsAsync() {
// do something
await this.nextFrame();
// do something else on the next frame / loop iteration
}
While the callback-based function works as intended, the promise version of nextFrame() does not resolve the promise at the time I would expect. await nextFrame()
only resolves in my example once the outer loop of updateLoop()
has exited. I attatched a console.log to the promise to find out more, and found that the console log and resolve() do get called within the loop, but awaiting it will still wait for the loop to fully terminate.
nextFrameDebug() {
return new Promise((resolve) => {
this.onUpdate.once(() => {
console.log('debug nextFrame'); // this prints during the expected loop iteration
resolve(); // this does not actually resolve until updateLoop() terminates
})
})
}
Here's a JSFiddle demonstrating the functions above: https://jsfiddle.net/8L4wub29/5/
It seems as if I am close to a functional solution, but there is something I'm misunderstanding about promises or async functions. Does this have to do with calling the async function from within a loop? How could I write nextFrame() so that the promise resolves on the next iteration of the loop, instead of after the loop exits? I realize that for most functionality in a game, timeouts with milliseconds are more useful, but there may be some cases like waiting for physics updates where a game might want to wait just one frame as a utility function. The callback-based version works fine, but if you need to use it multiple times then you need to nest it, which does not look as clean as using await
CodePudding user response:
Promises come with a guarantee that their .then
callback will always be called asynchronously. This means that if you have some random promise, and you do something like this:
somePromise.then(() => {
console.log('inside');
});
console.log('outside');
You will consistently see "outside" followed by "inside", even if the promise happens to already be in the resolved state. Promises were designed this way so you can be sure of the order of execution, and thus not have to fix painful bugs involving both orders. Same goes when you use await
instead of explicit .then
's: the code after the await
must wait until the promise resolves, and also that the call stack returns to system code.
So when you switched from callbacks to promises, you went from code that could synchronously call the callback in the middle of the loop, to code which must wait for execution to return. Only once updateLoop
returns or yields can the code after the promise run. Since that loop never returns or yields until it's all over, the promises all get delayed until after the loop.
If you need the updateLoop
to run synchronously, then you can not use promises for this. If instead updateLoop
is supposed to be asynchronous (maybe setting timeouts before running the next step), then promises can be used, but i'll need more details on what you're trying to do to give an example.
CodePudding user response:
Answering my own question because I found something that provides the intended functionality. I simply updated the updateLoop() call in the example to this.
async updateLoop() {
while (!this.windowShouldClose()) {
await null;
this.onUpdate.emit();
this.update();
}
}
Would somebody be able to comment with more clarification why this now produces the intended effect? Changing the function to async does not impact the point at which the promise resolves unless I add an await statement. My assumption that is including an await statement frees the event loop which finally allows the promise to resolve. If you edit the above JSfiddle with this function, the numbers printed in the console won't coincide with what the print statements "expect" - that only has to do with then the counter is incremented. The execution order appears to be correct.