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

[制作教程] Runtime macro expansion with plugin commands

[复制链接]
累计送礼:
0 个
累计收礼:
1 个
  • TA的每日心情
    慵懒
    3 天前
  • 签到天数: 207 天

    连续签到: 1 天

    [LV.7]常住居民III

    3646

    主题

    862

    回帖

    2万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

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

    灌水之王

    发表于 前天 18:49 | 显示全部楼层 |阅读模式
    This is a technique you can use to insert reusable code components in your events, while taking advantage of the flexibility JavaScript provides.

    Requirements:

    • Ability to make a plugin
    • Willingness to dig through engine code
    • Knowledge of how the interpreter works and how commands are stored (this will be touched upon in this tutorial, but you will need to read some code yourself as well)
    Note: this is an advanced tutorial that requires tracing through code and directly manipulating engine objects. It's not for the faint of heart, and you should have a good grasp of JavaScript.

    What's macro expansion?
    First, what is a macro? For our purposes, a macro is a shorthand for a collection of actions that, when executed, will perform that set of actions. In the context of RPG Maker, the macro is represented by a plugin command call, and the actions are the commands that you want executed when you make the call. Macro expansion is turning the plugin call into the actions to be executed.

    Why are we doing this?
    An important reason is we need to process arguments. Code duplication makes things hard to maintain. We're going to be running regular event commands, but we need to substitute some parameters of those commands where the commands do not read from the values of variables. Additionally, sometimes RPG Maker isn't able to offer the logic you need, so you have to implement those in JavaScript.

    It sounds like common events will suffice. Why aren't we using that?
    Passing arguments to common events is very awkward. You need to set aside a few variables, and make sure you're not reusing them outside of a call to the common event. In addition, strings are not supported in variables, and trying to use actor names as a workaround is just messy. If you needed to set arguments into variables, you'd need additional commands to do so in addition to calling the common event, whereas we will do everything in one go with a plugin command call.

    What's the difference between this and regular plugin commands?
    Regular plugin commands usually performs some action by calling engine functions directly, but there are some things that are better suited to existing commands provided with RPG Maker, where trying to reimplement them in JavaScript gets tedious.

    Give me an example.
    In this tutorial, we will be working on a real-world problem. Here's the situation: in my game, there are branching dialog trees. Each branch may switch through one or more expression pictures. These pictures are swapped using the commands Move Picture (fade out), Show Picture, and Move Picture (fade in). Sometimes when we jump back to a branch, the picture may be the same as the one that's set after the jump. Without additional logic, this will cause the same picture to fade out and back in, which is a bit weird. We want to not have the picture fade out if we're supposed to switch to the picture we're already on. To keep track of this, we could assign a numerical code to each picture, but as the number of potential pictures increase, it gets increasingly difficult to keep track of the assignment. Instead, we'll look directly at the name of the picture. Unfortunately, there is no way to store string variables without some JavaScript, and we don't really want to copy and paste the same half a dozen commands and changing two parameters every time we need to do this, so we're going to create a macro that will expand itself when executed if the conditions are right.

    How are we doing this?
    This expansion will be implemented through a plugin with a plugin command. When the plugin command is called, we'll take the arguments, evaluate whether we should swap out the image, and if we should, insert the necessary commands after the plugin command call into the current commands list the interpreter is running. That way, after the plugin command finishes executing, the interpreter will run the inserted commands if necessary. We call it runtime macro expansion because the macro is expanded when it's executed by the game, instead of ahead of the time.

    Interpreter? Commands?
    First, a quick tour of what makes up an event. An event consists of ID, name, position, and a number of pages. Each page has the conditions under which it's run, how it appears visually, and a list of commands that is executed when it is run. The list of commands is run by the map's interpreter. We don't need to concern ourselves with the details of an event and pages, only that list of commands.

    A command looks like this:
                    Code:        
    {     code: XXX,     indent: Y,     parameters: [...] }

    The code determines what this command does. The indent is the indentation level you see when you edit the commands list in an event, and is used in branching. Parameters are all of the options for the command for it to do what you want. If you open one of your map JSON files, you can search for "list", and you can see how commands are represented.

    The interpreter goes through this list of commands. It looks at the code, and finds the correct function to run to execute the command. The function then does whatever is necessary to do what the command says, while the interpreter keeps track of state information. The important ones for us are the list, index, and indent level. After running that command, it goes on to the next one, and repeats, until there are no more commands to run.

    The logic
    When we call the plugin command, we want to swap out the current image at an ID only if the current image's name is not the same as the new image's. If that's the case, we want to fade out, show the new image at 0 opacity, and fade it in.

    Plugin command
    This is the syntax of the command:
                    Code:        
    smart_fade <id> <name> [finalOpacity=255] [outTime=15] [inTime=15]

    I won't take you through how to parse the command (you'll see it in the finished script at the end). Just keep in mind the parameter names. Note because of the way RPG Maker splits parameters on spaces, if the file name contains spaces, you'll need to rejoin the entire string and parse it yourself properly.

    Evaluating the condition
    Let's start off with the function prototype. It looks like this:
                    Code:        
    var applySmartFade = function(interpreter, id, name, finalOpacity, outTime, inTime)

    As you can see, it contains the parameters from the plugin command. The interpreter parameter is provided by the plugin command handler as itself, so we can use this plugin command from normal, parallel, and common events. Now we need to find the picture and get its name. We can do it like this:
                    Code:        
    var pic = $gameScreen.picture(id);

    Let's check its name against the one we're trying to show. Note if we don't get a valid picture, we still want to show the new picture.
                    Code:        
    if (!pic || pic.name() !== name) {

    Inside the if block, we'll do our expansion. The block won't run if the name is the same, and we don't have anything further to do.

    Expanding the macro
    First, let's get a few of the variables we need.
                    Code:        
    var list = interpreter._list; var index = interpreter._index; var indent = interpreter._indent;

    Inside the interpreter are the list of commands, the index of the command that is currently being executed, and the current indent level. Note all these variables are being retrieved directly from the interpreter object. There are no accessor functions for them, because they were not intended to be used or modified externally. So take care when you're changing them.

    Next, let's set up the commands we're expanding to. But before that, let's take a look at the sequence we have if we wrote it inside an event page:



                    Code:        
    {"code":232,"indent":1,"parameters":[3,0,0,0,0,0,100,100,0,0,15,true]}, {"code":231,"indent":1,"parameters":[3,"cFortaimeRightThinking",0,0,0,0,100,100,0,0]}, {"code":232,"indent":1,"parameters":[3,0,0,0,0,0,100,100,255,0,15,false]}

    Code 232 is the Move Picture command, and code 231 is the Show Picture command. You can see how the parameters match up, and the differences between the Move Picture commands. In particular, note the number after the second 100, the last number, and the boolean value. Those correspond to opacity, time, and whether we wait, respectively. These are the only three parameters that we'll change for this macro. The first parameter in each command corresponds to the picture ID.

    Recreating the commands as JavaScript objects, here's what we get:
                    Code:        
    // Move picture, fade out var fadeOutCmd = {     code: 232,     indent: indent,     parameters: [         id, 0,         0, 0, 0, 0,         100, 100,         0, 0, outTime, true     ] }; // Show picture var showCmd = {     code: 231,     indent: indent,     parameters: [         id, name,         0, 0, 0, 0,         100, 100,         0, 0     ] }; // Move picture, fade in var fadeInCmd = {     code: 232,     indent: indent,     parameters: [         id, 0,         0, 0, 0, 0,         100, 100,         finalOpacity, 0, inTime, false     ] };

    Note how we replaced some of the parameters with the arguments we receive from our function. The other thing to note is the indent. You will want the indent to match the current indent level, and adjust them as necessary if you have commands under branches (experiment with some sample commands and inspect the map JSON to see how this works).

    A detour: rerunning the same event
    Usually, if the plugin command will only be hit once, you would remove the plugin command from the list and replace it with your expanded commands. But because this is in a dialog tree and this branch may be revisited, it is necessary to reevaluate the picture each time we run the plugin command. We do this by introducing another plugin command named cleanup_smart_fade that removes the inserted commands.
                    Code:        
    // Cleanup var cleanupCmd = {     code: 356,     indent: indent,     parameters: ["cleanup_smart_fade"] };

    Code 356 is the "call plugin command" command, and the parameters array consists of a single string with the command name and all its arguments.

    Inserting the new commands
    Depending on whether the macro condition needs to be evaluated, you either insert your commands at the current index and remove the call to the plugin command:
                    Code:        
    list.splice(index, 1, fadeOutCmd, showCmd, fadeInCmd); interpreter._index = index - 1; // Roll back one command so the interpreter will execute your inserted commands next

    or you insert your commands after the plugin command:
                    Code:        
    list.splice(index + 1, 0, fadeOutCmd, showCmd, fadeInCmd, cleanupCmd);

    After this, you're done. The interpreter will now execute your inserted commands.

    Cleaning up
    If you need to remove your inserted commands, you'll need another function that will remove those commands.
                    Code:        
    var cleanupSmartFade = function(interpreter) {     interpreter._list.splice(interpreter._index - 3, 4);     interpreter._index -= 4; };

    In this case we inserted three commands plus a plugin call to clean up, so we need to remove four commands. Recall that the index points to the currently executing command, so you want to start removing three commands back. Lastly adjust the index to account for the removed commands.

    The full effect
    Here is the full script, complete with plugin command handling.
    Spoiler: Full script code
                    Code:        
    //============================================================================= // cy_PicSmartFade.js //=============================================================================  /*:  * @plugindesc Conditionally fades out and replaces picture if file name is different.  * @author cyanic  *  * @help Omit the fade out, show, fade in commands and replace with the  * smart_fade plugin command.  *  * Plugin commands:  * smart_fade <id> <name> [finalOpacity=255] [outTime=15] [inTime=15]  *     Fades out and changes picture only if current picture is not the same.  *     id: The picture ID.  *     name: The name of the picture, without file extension. Case sensitive!  *     finalOpacity: Opacity of the swapped image (if swapped).  *     outTime: Time to fade out image (in frames).  *     inTime: Time to fade in image (in frames).  */  (function(){  // Plugin command handler var _Game_Interpreter_pluginCommand = Game_Interpreter.prototype.pluginCommand; Game_Interpreter.prototype.pluginCommand = function(command, args) {     _Game_Interpreter_pluginCommand.call(this, command, args);     var cmd = command.toLowerCase();     if (cmd === "smart_fade" && args.length >= 2) {         var id = Number(args[0]);         var name = args[1];         var finalOpacity = Number(args[2]) || 255;         var outTime = Number(args[3]) || 15;         var inTime = Number(args[4]) || 15;         if (isNaN(id))             throw new TypeError("id is invalid.");         applySmartFade(this, id, name, finalOpacity, outTime, inTime);     } else if (cmd === "cleanup_smart_fade") {         // Private helper command, do not call directly         cleanupSmartFade(this);     } };  var applySmartFade = function(interpreter, id, name, finalOpacity, outTime, inTime) {     if (finalOpacity < 0 || finalOpacity > 255)         throw new RangeError("finalOpacity is outside of 0-255 range.");     if (outTime < 0)         throw new RangeError("outTime is negative.");     if (inTime < 0)         throw new RangeError("inTime is negative.");      // 1. Evaluate whether replacement is necessary     var pic = $gameScreen.picture(id);     if (!pic || pic.name() !== name) {         // 2. Insert commands into interpreter if necessary         var list = interpreter._list;         var index = interpreter._index;         var indent = interpreter._indent;         // Define commands         // Move picture, fade out         var fadeOutCmd = {             code: 232,             indent: indent,             parameters: [                 id, 0,                 0, 0, 0, 0,                 100, 100,                 0, 0, outTime, true             ]         };         // Show picture         var showCmd = {             code: 231,             indent: indent,             parameters: [                 id, name,                 0, 0, 0, 0,                 100, 100,                 0, 0             ]         };         // Move picture, fade in         var fadeInCmd = {             code: 232,             indent: indent,             parameters: [                 id, 0,                 0, 0, 0, 0,                 100, 100,                 finalOpacity, 0, inTime, false             ]         };         // Cleanup         var cleanupCmd = {             code: 356,             indent: indent,             parameters: ["cleanup_smart_fade"]         };          // Insert commands         list.splice(index + 1, 0, fadeOutCmd, showCmd, fadeInCmd, cleanupCmd);     } };  var cleanupSmartFade = function(interpreter) {     // Remove fade commands so they don't duplicate if smart fade is run again     interpreter._list.splice(interpreter._index - 3, 4);     interpreter._index -= 4; };  })();


    Using the macro
    All you have to do is call the plugin command. For the example, you write the following:
                    Code:        
    smart_fade 3 cFortaimeRightThinking

    Making this one call is equivalent to doing some advanced logic that conditionally executes the move/show/move commands. It's no different than if you added those commands in the editor, except the logic carried out is something the engine can't do out of the box.

    Appendix: figuring out command parameters
    The best way to figure out your commands is to write them in the event editor, save, and inspect the corresponding map file. You can find out which parameters corresponds to which experimentally, but it would probably be faster if you looked at the engine code. In rpg_objects.js, look for "Game_Interpreter.prototype.commandXXX" where XXX is the command code. The parameters are read from "this._params". They're 0-indexed, as JavaScript arrays usually are. After that, it's just a matter of reading the code to see what it does, and looking at the parameter names of any functions that are called with the parameters. Note that although in most cases parameters are read in order, that doesn't always happen, so pay attention to the index of the parameter that's being read. If you don't see a parameter being read, then it's unused, and you may put any value for it, but you should probably use what RPG Maker would have put there as a convention.

    Wrapup
    In this tutorial, we went through what a macro is, what runtime macro expansion is, why you might want to use one, and how to implement one. Macros are very useful if you have complex sequence of commands that require multiple parameter substitutions, or need to use parameter types that you can't specify in the editor or do comparisons that the engine can't do by itself. Implementing one is just setting up a plugin command and inserting interpreter commands when the plugin command is run. Depending on whether the conditions for the macro change each time it is run, you may either replace the macro plugin command call with the expanded commands, or reevaluate the conditions, insert the commands, then clean them up when they're done running.


    本贴来自国际rpgmaker官方论坛作者:cyanic处,因国际论坛即将永久关站,为了存档多年珍贵资料,署名转载到本论坛存档,由于官方帖子为英文原帖,需要中文翻译请点击论坛顶部切换语言为中文就可以将帖子翻译成中文浏览,方便大家随时查看,原文地址:https://forums.rpgmakerweb.com/threads/runtime-macro-expansion-with-plugin-commands.93982/

    本帖子中包含更多资源

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

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

    使用道具 举报

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

    本版积分规则

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

    幸运抽奖

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

    立即查看

    聊天机器人
    Loading...

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

    GMT+8, 2026-7-10 03:01 , Processed in 0.151376 second(s), 55 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2020, Tencent Cloud.

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