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/