Hello RPG Makers!
I'm very excited to write my first tutorial on this forum and I'm fairly certain this topic will interest a lot of people of all skill levels. This is something I've wanted to achieve for a long time and I finally did! But most importantly, I'm fairly certain I did it the right way. I know some of you might think the title is a bit clickbaity but believe me, the words were carefully chosen, and I do mean "Definitive solution".
If you don't want to read the tutorial and just want the plugin to start using await inside Script commands (or to look at my implementation and try to find a reason why it's bad), you can just download the plugin here:
Spoiler: Plugin File
I've seen a few posts in this forum mentioning unsuccessful attempts at running an asynchronous script using the Script command, and from what I saw, there is one commonly agreed upon workaround that involves a loop. I have also used that trick in many different ways. While that worked fine, I feel like the solution I'm about to present is the one that interfaces the best with the game engine.
1. Acknowledging the problem
First let's look at the issue we're trying to solve. To illustrate the problem, let's imagine the following situation: I'm hanging out at school and there's this kid nearby. The thing about this kid is that he loves Chuck Norris. He is probably the biggest Chuck Norris fan you've ever seen. So when I talk to him, he will always try to tell me a Chuck Norris joke that I haven't heard before. Simple enough? Let's see.
After a quick google search I see that I can get a random Chuck Norris joke from this free API:
https://api.chucknorris.io
Let's prepare our script now. Just as a side note, jokes can be long and we need to break them into lines that fit into a message window. There's probably a better way but for the sake of simplicity and staying focused on the task at hand (running asynchronous scripts), I'll use this function to format the text:
JavaScript:
function formatText(text) { const words = text.split(/\s+/); const lines = []; let line = ''; while (words.length > 0) { if (line.length + words[0].length >= 60) { lines.push(line); line = ''; } line += words.shift() + ' '; } lines.push(line); return lines.join('\n'); }
I just put this in a JS file inside the plugin directory and load it as a first plugin for my game. Now let's try to use the script command to get a random joke and store its text into variable 1:
JavaScript:
const response = await fetch('https://api.chucknorris.io/jokes/random'); const joke = await response.json(); $gameVariables.setValue(1, formatText(joke.value));
Easy peasy! We get the joke from the API, we extract the JSON response, we format the text and we store it in Variable 1.
Great! Let's try our game:
And here is our the problem. The message:
await is only valid in async functions and the top level bodies of modules
Click to expand...
is telling us that we are trying to use await outside of an async function.
2. Some very bad solutions
Now let me just say this: when I was trying this, I was still very new to RPG Maker MV/MZ. The last time I had used an RPG Maker engine, it was XP in 2005, and the last RPG Maker game I had made, it was with RPG Maker 2000 in the actual year 2000.
With that out of the way, I'll admit, the first solution that came to my mind to solve this issue is to wrap my script inside an asynchronous IIFE (Immediately Invoked Function Expression).
JavaScript:
(async () => { // My Script here })()
Back when async/await was first introduced, this was the standard lazy way to be able to use await in JavaScript whether in browser consoles or in Node.JS.
But you've guessed it, this won't work. What this does is basically telling the interpreter: "here, start running this function, and I'll ping you when it's done". But the interpreter is impatient. It will try to run the next instruction immediately after that which is to display the message from $gameVariables.value(1), and well, we didn't get a response yet from the Chuck Norris API.
My next idea was to look at the function that was encapsulating that Script command call, aka command355 of the Game_Interpreter class that you can find in rmmz_objects.js (or rpg_objects.js on MV. Oh by the way, this plugin and tutorial works for MV too).
Here is the code for that function.
JavaScript:
// Script Game_Interpreter.prototype.command355 = function() { let script = this.currentCommand().parameters[0] + "\n"; while (this.nextEventCode() === 655) { this._index++; script += this.currentCommand().parameters[0] + "\n"; } eval(script); return true; };
So then it seems like an easy fix no? Let's just make that function async.
I'll save you some time, it's not that simple. See, these methods from the Game_Interpreter (there's one for each command), they are called sequentially by this one:
JavaScript:
Game_Interpreter.prototype.executeCommand = function() { const command = this.currentCommand(); if (command) { this._indent = command.indent; const methodName = "command" + command.code; if (typeof this[methodName] === "function") { if (!this[methodName](command.parameters)) { // <-------- HERE return false; } } this._index++; } else { this.terminate(); } return true; };
At the deepest level of indentation, where I added the // <-- HERE comment, is where the interpreter tries to run a command. You'll notice it's inside an if statement. That's because all of these command functions return a boolean (true or false) to indicate if the command has succeeded or failed. If we made one of these commands an async function, they it wouldn't return a boolean anymore.
But in any case, we are back at the problem where the next instruction starts before we get the response from the API.
If only there was a way to make the interpreter
wait...
3. Some better solutions
That's it! We have to wait! There's a command called "Wait" right? Ok but how long should we wait? This Chuck Norris request is pretty quick, one second is probably enough but what if we have slow internet all of a sudden? Do we wait 5 seconds? And make everyone with optical fiber internet suffer because of the few boomers playing on GPRS (oh man, I'm old...)? No, ideally we want to wait for the exact amount of time it takes to make the request. Give or take a few frames.
So maybe if we had our function set the variable to some temporary and recognizable value (like -1 or 'X' or 666) that means "we are waiting for a response", then fetch the result and update the variable, then technically, we could add a little loop between the script and the message that checks the value of the variable, if it's our special "wait" value, then we will wait and try again in a few frames.
Here is approximately how my next attempt looked like:
And it worked!
Ok time to celebrate and to forget about this whole thing. We got the solution now, right?
Not quite. I mean sure, this works but something bothers me about the fact that we run a loop to check if the response is here yet. We're already in a game loop, now the game loop has to go through checking, waiting, waiting, waiting, checking, waiting, waiting, waiting, checking... It's like that kid in the back of the car who asks "are we there yet?" every 5 minutes on a 4h trip (that was me usually). There has to be a better way.
4. The Definitive Solution
The fact is there is a better way. To find it I had to ask myself: how does RPG Maker do it when it naturally has to wait for something to finish before going to the next instruction. For example, when I show a bunch of messages, in a row, they don't just spam my screen and appear all at the same time. I needed to find the way no matter how. I was ready to shake my screen until an answer fell out of it. So I did.
And there it was. I needed to find how that "Wait for completion" checkbox worked. How could I add a "Wait for completion" for my async function?
So I looked at the code for the "Shake Screen" command
JavaScript:
// Shake Screen Game_Interpreter.prototype.command225 = function(params) { $gameScreen.startShake(params[0], params[1], params[2]); if (params[3]) { this.wait(params[2]); } return true; };
hmm... this.wait() could that be the answer? No, this is the same method that is called when I simply run the "Wait" command. It takes a number of frames as a parameter and I don't know in advance how many frames I need to wait. I had to look somewhere else, some command that waits for completion but doesn't wait for a given number of frames. So I started scrolling the map looking for a hint.
There it is. How does the interpreter know when the map is done scrolling?
JavaScript:
// Scroll Map Game_Interpreter.prototype.command204 = function(params) { if (!$gameParty.inBattle()) { if ($gameMap.isScrolling()) { this.setWaitMode("scroll"); return false; } $gameMap.startScroll(params[0], params[1], params[2]); if (params[3]) { this.setWaitMode("scroll"); } } return true; };
When the box "Wait for Completion" is ticked, then it calls this.setWaitMode()
JavaScript:
Game_Interpreter.prototype.setWaitMode = function(waitMode) { this._waitMode = waitMode; };
Ok, now I just had to understand how this _waitMode property worked (we're almost there).
I'll spare you the fat code block for Game_Interpreter.prototype.updateWaitMode but explained simply, that function will check various conditions depending on the wait mode. If the wait mode is "message", it will return true if the game is busy showing you a message. If the wait mode is "scroll", it will return true if the game is busy scrolling, etc... and when that function returns true, it means the game interpreter has to wait for something. When it returns false, then the game interpreter carries on with the execution of the event's commands.
So now I had all I needed for my solution, because yeah, I was still dead set on making await work with Script commands.
The solution is a (tiny) plugin. Here's the full commented code:
First we overload the updateWaitMode method of the game interpreter to have it handle a new special wait mode. I call it "promise" because we are waiting on a promise to resolve. But feel free to call it "async" or "-1" or "X" or "666". Just don't call it "message" or "scroll", those are taken, along with a few others.
JavaScript:
const Game_Interpreter_updateWaitMode = Game_Interpreter.prototype.updateWaitMode; Game_Interpreter.prototype.updateWaitMode = function() { if (this._waitMode === 'promise') { return true; } return Game_Interpreter_updateWaitMode.call(this); }
Basicly, I'm telling the interpreter: if the wait mode is "promise" then you just wait.
Now for the finale, the override of command355 aka Script command.
The first part is the same:
JavaScript:
Game_Interpreter.prototype.command355 = function() { let script = this.currentCommand().parameters[0] + "\n"; while (this.nextEventCode() === 655) { this._index++; script += this.currentCommand().parameters[0] + "\n"; } // ... wait for it }
At this point we're just reading the script line by line and storing it in a variable.
Then instead of the old version:
JavaScript:
eval(script); return true;
we do:
JavaScript:
this.setWaitMode('promise'); // We ask the interpreter to start waiting const fn = new AsyncFunction(script); // We create an async function that will run the code fn.call(this).catch((error) => { console.error(error); // This part is up to you how you want to handle the error }).finally(() => { this.setWaitMode(''); // We tell the interpreter to stop waiting }) return true;
But actually we can make this a bit better using a Function constructor. Or to be more precise, an AsyncFunction constructor. AsyncFunction constructors allow you to create a
Now with this plugin, we can go back to our original Script command using await
Here is the full plugin file:
Spoiler: Plugin File
And here is a useless game where you can get Chuck Norris facts from a fanboy instead of browsing the website:
Spoiler: Useless Game
5. That's it?
That's it! Now just by including this plugin in your plugin list, you can use await inside regular Script commands and the interpreter will wait for whatever asynchronous computation you do there to complete before moving on to the next command.
I'm still not super happy about the error handling. In my version of the plugin, I added a Plugin Parameter called errorHandlerCode to allow any plugin user to put custom JavaScript that handles the error. The script inside that parameter has access to a variable called "error". Maybe the behaviour that would best fit to the RPG Maker default one would be to simply throw the error.
To conclude this tutorial, this solution might not work for everyone (I'm very curious if there is a use-case out there that prefers the standard Script Command) but it is the best I could think of, and personally the one that fits my need the most among the ones I've seen around. I thought surely someone else is going to be happy to find it. And when that happens, I'm happy.
If there is a Definitive Solution somewhere out there, all I can do is wish the Gotcha Gotcha Gods would hear my prayers and make a little update in that direction. The update could be 100% retro-compatible (this plugin is) and would make a lot of people happy.
Merry Christmas Y'all!
本贴来自国际rpgmaker官方论坛作者:Mouradif处,因国际论坛即将永久关站,为了存档多年珍贵资料,署名转载到本论坛存档,由于官方帖子为英文原帖,需要中文翻译请点击论坛顶部切换语言为中文就可以将帖子翻译成中文浏览,方便大家随时查看,原文地址:
https://forums.rpgmakerweb.com/threads/definitive-solution-to-using-await-inside-the-script-command.164123/