设为首页收藏本站同能贴吧 切换语言 繁体中文
开启辅助访问 切换到窄版
扫描二维码关注官方公众号
返回列表
+ 发新帖
查看: 90|回复: 0

[制作教程] Definitive solution to using 'await' inside the Script command

[复制链接]
累计送礼:
0 个
累计收礼:
1 个
  • TA的每日心情
    开心
    4 天前
  • 签到天数: 209 天

    连续签到: 2 天

    [LV.7]常住居民III

    4609

    主题

    864

    回帖

    2万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    VIP
    7
    卡币
    23225
    OK点
    16
    推广点
    0
    同能卷
    50
    积分
    28728

    灌水之王

    发表于 2026-7-8 08:12:47 | 显示全部楼层 |阅读模式
    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 FileAnd here is a useless game where you can get Chuck Norris facts from a fanboy instead of browsing the website:
    Spoiler: Useless Game
    Chuck Norris Fanboy



                                            chuck.rpg.tf                                




    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/

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    x
    天天去同能,天天有童年!
    回复 送礼论坛版权

    使用道具 举报

    文明发言,和谐互动
    文明发言,和谐互动
    高级模式
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    简体中文
    繁體中文
    English(英语)
    日本語(日语)
    Deutsch(德语)
    Русский язык(俄语)
    بالعربية(阿拉伯语)
    Türkçe(土耳其语)
    Português(葡萄牙语)
    ภาษาไทย(泰国语)
    한어(朝鲜语/韩语)
    Français(法语)
    关闭

    幸运抽奖

    社区每日抽奖来袭,快来试试你是欧皇还是非酋~

    立即查看

    聊天机器人
    Loading...

    QQ|Archiver|手机版|小黑屋|同能RPG制作大师 ( 沪ICP备12027754号-3 )

    GMT+8, 2026-7-16 17:32 , Processed in 0.117914 second(s), 52 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2020, Tencent Cloud.

    快速回复 返回顶部 返回列表