SLIP INTO RUBY - UNDER THE HOOD PART 11: DO NINE MEN INTERPRET(ER)? NINE MEN, I NOD.
We finally sift through the gooey innards that is VX Ace's Game_Interpreter class.
- Trihan
- 01/19/2017 01:03 AM
- 7316 views
Okay, so it's been over a year. But it's back! Tell all your friends! If you don't have any friends tell your cat! The time has finally come for the next exciting edition of
So if you remember last time (it was October 2015, I wouldn't blame you if you didn't) we looked at everything from Game_CommonEvent up to Game_Event. Now it's time. The time I never wanted to come. Yes, it's time to crack open the behemoth that makes this whole mother do what it does. It's time to look at Game_Interpreter.
Game_Interpreter
This is going to take up a whole episode by itself, so fasten your seatbelts and get ready to unravel one of the biggest scripts in the entirety of RPG Maker VX Ace. This is the one responsible for making all your fancy event commands -do stuff-, so it's an important one to understand. An in-depth understanding of the interpreter and how it works will enable you to change the behaviour of existing event commands however you see fit.
For future reference, Game_Map, Game_Troop and Game_Event all have their own instance of the interpreter, which is why event commands still work in battle etc.
To begin with, it has two public instance variables:
They're pretty self-explanatory: Map ID is the ID of the map the interpreter is for (when used on Game_Map), and Event ID is the ID of the event the interpreter is for (when used on Game_Event).
As you can see here, the constructor takes one argument, depth. The instance variable @depth is set to the supplied value (default of 0), then we call check_overflow (which we'll look at in a sec) and finally we call clear (which we'll also look at in a sec).
As stated in the comments for this method, under normal conditions depth will not exceed 100. This method is basically here to catch infinite loops resulting from recursive event calls (events calling themselves). Essentially, if @depth is ever equal to or greater than 100, an event overflow error is displayed and the game quits.
Clear is a method that basically just starts you off with a fresh slate interpreter with no data. The map ID and event ID are both set to 0, the list of commands is set to nil, the command index is set to 0, the data of the current branch is set to an empty array, and fiber is set to nil (we'll cover fiber shortly).
Setup is a method for setting up map events and takes two arguments: a list of event commands and the ID of the event in question. First, we call the clear method, same as with the constructor. The @map_id instance variable is set to the ID of the map the player is currently on ($game_map being a global variable containing all data for the current map, as we've covered before). @event_id is set to the supplied event ID, which defaults to 0. @list is set to the supplied list of commands, and then we call create_fiber. This isn't part of a balanced diet, it's an integral part of how RPG Maker VX Ace even works.
Well. This is...not very illuminating, is it? To put it in layman's terms: "if @list contains data, set @fiber to a new instance of Fiber, and then...do something with run in brackets?"
To quote the Ruby documentation: "Fibers are primitives for implementing light weight cooperative concurrency in Ruby. Basically they are a means of creating code blocks that can be paused and resumed, much like threads. The main difference is that they are never preempted and that the scheduling must be done by the programmer and not the VM."
A Fiber will not run automatically when created; it must be explicitly asked using the Fiber.resume method (which, you guessed it, we'll look at in a bit). You can also have the fiber give up control to the code that called it using Fiber.yield, which, again, we'll see in a bit.
This is a lot to take in for people who aren't used to Object-Oriented Programming or multithreading, so suffice to say that Fiber basically starts a self-sufficient bit of code which retains state and can be paused/resumed as needed, and in this case, all it's doing is calling the "run" method of Game_Interpreter, which is coming up soon.
This method returns an array consisting of the depth, the map ID, the event ID, the list of commands, the current index incremented by 1, and the branch data. Although you won't see a call to this method anywhere in the default scripts, it's kind of built-in to the language and will be called whenever Ruby is trying to serialise an interpreter. Without this, you wouldn't be able to save games.
This is the counterpart to marshal_dump, used when loading data (again, because fibers are not compatible with Marshal); it takes one argument, obj (which is an array of data as defined in marshal_dump) and simply sets each respective instance variable to the element of obj corresponding to it, then creates the fiber using that data.
This also demonstrates a pretty nifty feature of Ruby that I don't think I've ever really gone into, which is that you can assign multiple values in one line by setting a list of variables to an array. For example, if I were to do
basically what would happen is that the array would be iterated, and each value would be assigned to the next variable specified. So @test1 would hold the value 1, @test2 the value 2, and so on. If you supply fewer values in the array than there are variables, the other variables will be set to nil.
To see in action the ramifications of not having this set of methods here, try commenting out marshal_dump and then save your game. What's that? You can't do it? Yeah, that's because Ruby isn't able to serialise your interpreter. :D (similarly, if you comment out marshal_load, you can save your game but can't load it).
This method is used to determine if the map the event started on is the same as the one the player is on currently, and is used for a few purposes which we'll see later. It simply compares the @map_id variable to the map_id method of $game_map, returning true if they're equal and false if they're not.
This method sets up reserved common events: you probably don't remember, but we looked at common_event_reserved? way back in Slip into Ruby: Under the Hood part 3.
So first what we're doing is checking to see if a common event has even been reserved in the first place, and simply returns false if there isn't: if there isn't one waiting to run, what's the point in setting one up?
If an event is waiting to run, we call the setup method, supplying the list of commands for that common event, then clear the common event ID from $game_temp (so that it doesn't keep setting up the same common event forever) and finally returns true to show that the event is ready to run.
This is the meat and potatoes of the interpreter, and is essentially what makes it work. You'll remember from create_fiber that this method is the only thing the Fiber block contains, so as soon as we call resume on a fiber it'll call this method.
First we call wait_for_message, which as we'll see a bit later simply...waits for a message (as in the boxes you use for dialogue and stuff). Then it starts a while loop for as long as @list contains data in the element corresponding to @index. Each loop all that will happen is that execute_command will be called, then the index will be incremented by 1. Once the list has been fully iterated through, we call Fiber.yield (which returns control to the code that called the fiber) and set @fiber to nil.
Interestingly enough, because the fiber is only resumed in frame update methods (as, you guessed it, we'll see soon), I'm not actually sure what Fiber.yield even -does- here. As far as I can tell everything still works fine without it, because all it's doing is returning control to a method that it goes back to after it runs anyway. If anyone can shed some light on this please feel free.
This method determines whether the interpreter is running or not, and simply returns true if @fiber is not nil, and false otherwise.
The frame update method. If @fiber contains data, resume it. As we've already seen, this means that every frame during which a fiber exists, the interpreter's "run" method will be called (if the fiber isn't already running), which will take over control until the event has run its course (which is also why other non-parallel-process events on a map stop what they're doing when you interact with an event).
This method is an actor ID iterator, which takes one parameter (param); as specified in the comments, if the value is 0, goes through each actor. If 1 or more, yields the actor corresponding to that ID.
This looks more complicated than it is, and once we come across a command that uses this and upcoming iteration methods I'll go through the logic in detail.
Actor variable iterator, taking two parameters: param1 and param2. param1 specifies whether the variable being used is fixed (0) or itself a variable (1), while param2 specifies the actor or variable ID. If param1 is 0, then we call iterate_actor_id with param2 as the param. If it's 1 or more, we call iterate_actor_id using element param2 of $game_variables and yield that actor instead.
Actor index iterator, taking one parameter: param. If it's 0 or more, yields the actor corresponding to that value. If -1, yields the whole party.
Enem index iterator, taking one parameter: param. If it's 0 or more, yields the enemy corresponding to that value. If -1, yields the whole enemy troop.
Battler iterator, taking two parameters: param1 and param2. param1 specifies whether to iterate enemies (0) or actors (1), while param2 specifies the enemy index or actor ID. This check is only done if the party is in battle.
Determines the target of a command that affects the screen. If the party is in battle, return the screen of $game_troop; otherwise, return the screen of $game_map.
This method is the one that actually executes event commands. First, the variable command is set to the element of @list corresponding to the current @index. @params is set to the parameters of that command, @indent is set to the indent of the command, the method name is set to the string "command_" followed by the code of the command, then if that method name responds to calls, send it.
WTF did you just say, Trihan? Okay, so not sure if I've already covered this but you can call a method in Ruby by using send(name_of_method) as well as calling it directly by name. This allows you to call methods with dynamically-generated names, as is the case here. respond_to? simply checks that the object in question (in this case, Game_Interpreter) responds to (or can call) the given method. This is why all of the event command methods are named command_ (as we'll see), so that they can be called dynamically.
This method skips commands which are deeper than the current index and increments it.
To put it more simply, while the indent of the next command in the list is greater than that of the current one, increment the index. I'll explain this in more detail when we get to a method that calls it.
This method simply gets the code of the next event command, by returning the .code property of the element of @list corresponding to 1 more than @index.
This method returns a particular character and takes one parameter, param. If the party is in battle, returns nil. If the parameter is -1, returns the player. If any other value, gets a list of events on the map if we're still on the same map as the event started on (otherwise an empty array) and then returns events if param is greater than 0, or events[@event_id] if it's 0 (in other words, "this event").
This method calculates operated values, given three parameters: operation, operand type, and operand.
Sets the variable "value" to the value of operand if operand_type is 0, or the game variable with index operand if it's 1. Sets the variable "operation" to value if it's 0, and the negative of value if it's 1. I'll explain further once we get to a method that calls it.
This method "waits" a number of frames, specified by the parameter duration. Simply yields the fiber to its caller for as many times as the supplied duration.
This method waits while a message is displaying (without this messages would never actually show as the event would just blow past them without waiting for input). Simply yields the fiber to its caller as long as $game_message.busy? returns true. For those who don't remember the time we covered this, it'll return true as long as the message has text, a choice, number input, or an item choice.
That's right, we're only just now getting to the first event command. We're going to be here a while...
All this time you've just been using the "Show Text..." command without really thinking about it. Well, that's all about to change, because we're about to dive into its innards and find out exactly how it works.
First, we call wait_for_message, so if a message is displaying, it'll yield to the caller. I honestly don't actually think the first call here is needed, as it's also called at the end and that'll stop to wait for input anyway.
The face_name property of $game_message is set to @params[0], which is the filename of the face graphic you choose when you double click the box in the "Show Text..." dialog. face_index is set to @params[1], which is the face that you choose from the box on the right showing the actual graphic. background is set to @params[2], which is the numerical value corresponding to "Normal Window", "Dim Background" and "Transparent" in the "Background:" drop-down list. position is set to @params[3], which is the numerical value corresponding to "Bottom", "Middle" and "Top" in the "Position:" drop-down list.
Unfortunately there's no way to change the parameters supplied to these commands without hacking into the actual editor since they're tied into the dialog boxes themselves, but it's handy to know that everything you select in an event command dialog box is stored in the params array so you can refer to it in your code.
Then we enter a while loop, for as long as next_event_code returns 401. This is the internal code used by the event editor to denote "text data" and this is how multiple messages are displayed "smoothly" without the box closing and opening after each one. The while loop simply goes to the next index and adds the parameters[0] property of the element of @list corresponding to @index to $game_message. In English, we add the text of the next message command to the content of the global message variable.
You may have noticed something interesting from this: although "101" is the code of the "Show Text..." command, the actual text to be displayed is created as an entirely separate command with code 401. This is why parameters[0] of command 101 is the faceset name, while parameters[0] of command 401 is the text.
Once the while loop has processed all of the continuous text, we check the next_event_code again to see if it's a command that needs to display something alongside the message box. If it's 102 (Show Choices), we increment the index and call setup_choices with the parameters of the command as an argument. If it's 103 (Input Number), we increment the index and call setup_num_input with the parameters of the command as an argument. If it's 104 (Select Item), we...you get the idea. This basically just sets up choices, numbers or item selection so that they appear alongside the message. If we didn't have this, you'd have to close the message box before the other windows appeared, which would be pretty terrible game design.
And finally, we call wait_for_message, which we've already looked at.
The code for Show Choices is surprisingly brief. It calls wait_for_message, then setup_choices supplying @params as the argument (this is the list of choices and the cancellation choice), and yields the fiber to its caller while $game_message.choice? returns true. For those who don't remember that lesson, it returns true if the size of @choices is > 0, which will obviously be the case if you've put choices in the boxes.
So here we come to another interesting note. Although Show Choices can be used by itself, if you have a Show Choices command following a Show Text, command_102 will never actually be called, because the Show Text does the choice processing for you! I actually think the way Enterbrain did this is pretty neat.
This method sets up the choices you select from when using the Show Choices event command. It iterates through each element of params[0] (which is the list of choices you wrote in the boxes) and runs a block on it, with s as the block variable (meaning each time it iterates, s will be whatever string you put in the next choice box, and it will be pushed to the choices array of $game_message).
The choice_cancel_type of $game_message is set to params[1]; this is the numerical representation of "Disallow", "Choice 1", "Choice 2", "Choice 3", "Choice 4", or "Branch" depending on what you chose in the dialog.
Finally, choice_proc of $game_message is set to a new Proc, with block variable n, where the element of @branch corresponding to @indent is set to n. Wait, what?
Okay, so Proc is basically just a fancy way to write a block of code that can be called in different contexts; if choice_proc is called, the value passed to it will be passed as "n", which will then be stored in @branch[@indent]. We'll revisit this when we finally get to Window_ChoiceList, which won't be for a while yet.
command_402 is a special one which is called when the interpreter reaches a choice branch. It skips the command if @branch[@indent] (which contains the index of the chosen option) is not equal to @params[0] (the ID of the current choice).
Here's another interesting one. The comment says "When Cancel" so you'd think it would be connected to the cancellation of a choice, but as far as I can tell it's never called by anything. What it does is skips the command if @branch[@indent] isn't equal to 4. Intriguingly, although command_403 never seems to be called by Show Choice, it does try to call command_404...which doesn't exist. I'm not sure whether this is a mistake in the coding of the editor. Another interesting note is that as far as I'm aware it's not actually possible for @branch[@indent] to ever equal 4 in a choice window anyway, as it's 0-indexed and there are only 4 options (0, 1, 2, 3).
er.yield while $game_message.num_input?
end
This method processes number input (when it's not immediately following Show Text). Simply waits for messages, calls setup_num_input with @params as the argument, and yields the fiber to the caller while num_input? from $game_message returns true (which is the case as long as @num_input_variable_id > 0, which will be true for the duration of the number input since this is the variable you picked in the dialog of the command).
And here we see the ever-so-complicated code for setting up number input, which is...setting the num_input_variable_id property of $game_message to params[0], which is the variable you picked in the dialog, and num_inputs_digits_max to params[1], which is the value you entered in the "Digits:" box.
This method processes the "select item" box (when it's not immediately following Show Text). Simply waits for messages, calls setup_item_choice with @params as the argument, and yields the fiber to the caller while item_choice? from $game_message returns true (as above, but for the item_choice_variable_id instead).
Pretty much the same as setting up number input, only we don't have max digits to worry about this time.
Show Scrolling Text! So it yields the fiber to the caller while $game_message.visible is true (so that if a message is currently showing, the scrolling text won't interfere with it) then sets some properties in $game_message: scroll_mode is set to true, scroll_speed is set to @params[0], which is the value you set in the dialog, and scroll_no_fast is set to @params[1], which determines whether or not the text can be fast forwarded.
We then enter a while loop as long as next_event_code is 405 (the scrolling text equivalent of 401 for text) and in each iteration increment the index and add parameters[0] (the text) to $game_message.
And finally, we wait for the message to finish scrolling.
This method is for the "Comment" command. The @comments array is populated with an array consisting of @params[0], which is just the comment text. Then we enter a while loop as long as next_event_code is 408, where we increment @index and push parameters[0] to @comments. I honestly have no idea what this loop does as I've never been able to engineer an event that had a command with code 408 in it, regardless of how many comments I created and where.
Conditional Branch is an absolute monster. Let's slay it.
So the first line is as simple as they come: set result to false. Moving on.
Then we have a case statement for @params[0]: this is the value of the radio button in the dialog determining whether it's Switch, Variable, Self Switch, Timer, Actor, Enemy, Character, Vehicle, Gold, Item, Weapon, Armour, Button, or Script.
When 0 (Switch), result is set to the result of comparing $game_switches[@params[1]] to (@params[2] == 0)--in other words, whether the two values are the same. @params[1] is the ID of the switch you chose from the selection dialog. @params[2] is 0 if you chose ON, and 1 if you chose OFF. So if you're doing a branch for switch 43 being ON, @params[1] will be 43, @params[2] will be 0, and it'll then be comparing $game_switches[43] to (0 == 0); if the switch is on, this will end up being [switch state] == true, which will return true if the switch is true, and false otherwise. Obviously this is turned around if checking for the switch being false, as the comparison then becomes (1 == 0) which is false, so if the switch is also false it'll return true.
When 1 (Variable), value1 is set to $game_variables[@params[1]] (the ID of the variable you set in the dialog); then if @params[2] is 0 ("Constant" in the dialog), value2 is set to @params[3], otherwise it's set to $game_variables[@params[3]] ("Variable" in the dialog). We then have a case statement for @params[4]; if it's 0 (Equal to), result is set to value1 == value2. If it's 1 (Greater than or equal to) result is set to value1 >= value2. You've done maths before, you should be able to work out the rest: 2 is less than or equal to, 3 is greater than, 4 is less than, 5 is not equal to.
When 2 (Self Switch), first we check whether @event_id is greater than 0 (because only events can have self switches), then key is set to an array consisting of the current map ID, the current event ID, and @params[1] (the letter of the self switch being checked) and result is set to $game_self_switches[key] == (@params[2] == 0)). So if we're currently on map 5 and checking that event 6 has self switch B ON, key becomes [5, 6, "B"] and result becomes $game_self_switches[[5, 6, "B"]] == (0 == 0); if the self switch is on, this will return true, otherwise it'll return false.
When 3 (Timer), first we check whether $game_timer.working? is true (because there's no point in checking if there isn't even a timer running); if so, and if @params[2] == 0, result is set to $game_timer.sec >= @params[1]. Otherwise, it's set to $game_timer.sec <= @params[1]. @params[2] is 0 if you chose "or More" and 1 if you chose "or Less", while @params[1] is the total number of seconds in the timer, which I believe the editor automatically calculates from the minutes and seconds boxes.
When 4 (Actor), the variable actor is set to $game_actors[@params[1]] (the ID of the actor you chose in the dialog). Then if actor exists, we have a case statement for @params[2]: when 0 (in party) result is true if $game_party includes the given actor, and false otherwise. When 1 (name), result is true if the actor's name is equal to @params[3] (the string entered in the name box). When 2 (Class), result is true if the actor's class ID is equal to @params[3] (the ID of the class selected in the box). When 3 (Skills), result is true if the actor has learned the skill with ID @params[3]. When 4 (Weapons), result is true if the actor has equipped the weapon with ID @params[3]. When 5 (Armours), result is true if the actor has equipped the armour with ID @params[3]. When 6 (States), result is true if the actor has the state with ID @params[3].
When 5 (Enemy), the variable enemy is set to $game_troop.members[@params[1]] (The enemy ID chosen in the dialog). Then if enemy exists, we have a case statement for @params[2]: when 0 (Appeared) result is true if the enemy with the given ID is currently alive. When 1 (State) result is true if the enemy with the given ID has the state with ID @params[3].
When 6 (Character), the variable character is set to the result of calling get_character with @params[1] as its argument. As we saw earlier, this will either be -1 (player), 0 (this event), or the ID of an event on the map. Then if character exists, result is true if the character's direction is equal to @params[2]. Note that internally directions have a numerical representation: down is 2, left is 4, right is 6, up is 8 (which is, incidentally, also the number on a numpad corresponding to that arrow key).
Interesting note: although "Vehicle" is the next option in the list, it actually has a higher number internally.
When 7 (Gold), we have a case statement for @params[2]: when 0 (greater than or equal to) result is true if the party's gold >= @params[1] (the entered value). When 1 (less than or equal to) result is true if the party's gold <= @params[1]. When 2 (less than) result is true if the party's gold < @params[1].
When 8 (Item), result is true if the party possesses the item with the ID @params[1].
When 9 (Weapon), result is true if the party possesses the weapon with ID @params[1]. If you checked the "include equipment" box, @params[2] will be true, false otherwise.
When 10 (Armour), result is true if the party possesses the armour with ID @params[1]. If you checked the "include equipment" box, @params[2] will be true, false otherwise.
When 11 (Button), result is true if the player is pressing the button corresponding to @params[1]. The directions are the same as previously mentioned (down = 2, left = 4, right = 6, up = 8), button A is 11, B is 12, C is 13, X is 14, Y is 15, Z is 16, L is 17 and R is 18.
When 12 (Script), result is true if the evaluated script stored in @params[1] (whatever you entered in the box) returns true.
When 13 (Vehicle), result is true if $game_player.vehicle is the same as $game_map.vehicles[@params[1]]. 0 is Boat, 1 is Ship, 2 is Airship.
Finally, @branch[@indent] is set to result, and we call command_skip if the result was false (because if the condition wasn't met, we don't want to process the code inside it)
This method is basically just there to skip the commands in an "else" if the branch result was true (the method itself will be called when the interpreter reaches the "else" line because its internal code is 411). Without this, the code in the else would still run even if one of the other branches had its conditions met.
It's...an empty method. Yep, the command method for "Loop" doesn't actually do anything. In fact, you can delete this method and loop will still work fine. I think it's basically just there for completion's sake.
This is what makes the loop actually work. The contents of the begin-end block will repeat until the indent of the current line of code is equal to the current indent. The only thing in the block is a decrement of the index.
Think about how indenting works in event commands, and you'll realise how this actually results in a loop. Picture a loop with a message in it:
Loop
Show Text: "Hello World!"
Repeat Above
112 is the "Loop", 413 is the "Repeat Above", and both have an indent of 0. The Show Text, 101, would have an indent of 1. So what the command_413 is doing is going backwards in the event commands until it finds another command with an indent of 0...which will basically take us back to the Loop. Execution will then continue from there, so it'll execute everything inside the loop again, at which point it'll go backwards until it finds indent 0...and so on and so forth. Unless...
This is how Break Loop works. Oddly enough, breaking a loop starts...with a loop. Inside that loop, we increment the index by 1, and return if the new index is greater than or equal to the size of the event commands list less 1. We also return if the code of the new command is 413 (Repeat Above) and the indent of the new command is less than the one we've just come from.
Note that the first return means that putting a Break Loop command in an event outside of a Loop-Repeat Above block will cause anything after the break to be ignored. GreatRedSpirit has also pointed out that there's a bug where having a Break Loop inside a conditional branch before a nested loop where there are no more commands between the Repeat Above of the inner loop and the Repeat Above of the outer loop will soft lock your game.
Exit Event Processing couldn't be simpler. All it does is set the current index to the size of the commands list, so the next time it tries to process a command it's at the end of the event.
This is the method for Call Common Event. First, common_event is set to the data pertaining to the common event specified by @params[0] (the event chosen in the dialog). If that common event exists, child is set to a new instance of Game_Interpreter with depth 1 greater than the current depth, we call setup on the child event, and then call its run method. Note that due to the recursive creation of new interpreters, if you have a map event call a common event which calls itself you'll eventually end up running afoul of the check_overflow method. Don't do this.
Another one that doesn't do anything! The method for Label is just as pointless as the one for Loop.
Jump to Label is, as with looping, the meat and potatoes of the block. First, label_name is set to @params[0] (which is the label name specified in the dialog). Then we enter a times loop with @list.size iterations (in other words, a number of iterations equal to the number of commands in the event) using i as an iteration variable. If the code of the list element with index i is 118 (label) and its parameters[0] (which for a label is its name) is equal to label_name, we set @index to i and return (in other words, if we find a label with the supplied name, we'll continue processing the event from that label).
Control Switches is simple enough. If a single switch is selected, I believe @params[0] and @params[1] will be the same value, while "Batch" will result in 0 being the lower bound and 1 being the upper. Then for each value from the first value to the last (inclusive), we loop through using i as an iteration variable and set $game_switches[i] to @params[2] == 0, which will be true if you selected ON, and false if you selected OFF (since they have values of 0 and 1 respectively).
Control Variables is a little more complex, obviously because it has more options. First of all, value is set to 0. Then we have a case statement for @params[3], which as the comment says is for the operand. When 0 (constant), value is set to @params[4] (the value in the box). When 1 (Variable), value is set to the element of $game_variables corresponding to @params[4]. When 2 (Random), value is set to @params[4] (the value in the first box) plus a random number from 0 to 1 less than the difference between @params[5] (the value in the second box) and @params[4]. Basically, if you wanted, say, a random number from 20 to 40, value would be 20 + a random number from 0 to 21 exclusive (so 0-20). When 3 (Game Data), value is set to the result of calling game_data_operand with @params[4] (type), @params[5] (param 1) and @params[6] (param 2) as arguments. We'll look at game_data_operand in a sec. When 4 (Script), value is set to the result of evaluating the script entered in the box, stored in @params[4].
Finally, we'll loop through each value from @params[0] to @params[1] (which will either be a single value or a lower and upper bound for "Batch" as with switches) using i as the iteration variable, and each iteration we'll call operate_variable with i, @params[2] and value as arguments. Again, we'll look at that in a sec.
This is the method which returns the game data being used for the "Game Data" setting of Control Variables. It takes three parameters: type, param1 and param2.
First, we have a case statement for type.
When 0 (Items), we return the party's currently-held number of the given item.
When 1 (Weapons), we return the party's currently-held number of the given weapon.
When 2 (Armour), we return the party's currently-held number of the given armour.
When 3 (Actors), we set actor to the element of $game_actors with ID param1 (the actor chosen in the box), then if the variable contains data we have another case statement for param2 (the parameter in the second box). When 0 (Level), we return the actor's level. When 1 (Exp), we return the actor's experience. When 2 (HP), we return the actor's HP. When 3 (MP) we return the actor's MP. When between 4 and 11 inclusive (Parameters) we return the actor's parameter with ID param2 - 4 (because obviously the internal parameter IDs start at 0 but param2 starts at 4, so we need to subtract the difference).
When 4 (Enemies), we set enemy to the element of $game_troop with ID param1, then if the variable contains data we have another case statement for param2. When 0 (HP), we return the enemy's HP. When 1 (MP), we return the enemy's MP. When between 2 and 9 inclusive (Parameters) we return the enemy's parameter with ID param2 - 2.
When 5 (Character), we set character to the result of get_character with param1 as its argument, then if the variable contains data we have a case statement for param2. When 0 (X coordinate), we return the character's x. When 1 (Y coordinate), we return the character's Y. When 2 (Direction), we return the character's direction. When 3 (Screen X), we return the character's screen x. When 4 (Screen Y), we return the character's screen y.
When 6 (Party), we set actor to the element of $game_party.members with ID param1, then if the variable contains data we return the actor's ID, otherwise we return 0 (because they're not in the party).
When 7 (Other), we have a case statement for param1. When 0 (Map ID), we return the map ID from $game_map. When 1 (Number of party members) we return the size of $game_party.members. When 2 (Gold), we return the gold of $game_party. When 3 (Steps), we return the steps of $game_party. When 4 (Play time), we return the number of passed frames divided by the frame rate (so for example, say the frame_count is 5938 frames, we divide that by the frame rate (60 fps by default) to see that we've been playing for 98 seconds. When 5 (Timer), we return the number of seconds elapsed in $game_timer. When 6 (Save count), we return the number of times the player has saved the game. When 7 (Battle count), we return the number of battles the player has fought.
Finally, we return 0 if no other case has been hit.
The operate_variable method executes the variable operation selected in the Control Variables command, and takes three parameters: variable ID, operation type, and value. First we have a case statement for operation_type. I shouldn't have to go through it at this point after everything else I've covered, but basically 0 sets the variable to value, 1 adds value to it, 2 subtracts value from it, 3 multiplies it by value, 4 divides it by value, and 5 divides it by value and returns the remainder. The rescue part of the begin-end block will catch any exceptions (if we try to divide by 0, for example) and set the variable to 0 instead.
This is the method for the Control Self Switch command. First we make sure @event_id is greater than 0, because if we're not in a map event there's no point in doing anything with self switches. key is set to an array consisting of the map ID, event ID and @params[0] (which is the letter of the self switch being checked), then $game_self_switches[key] is set to @params[1] == 0. So if, for example, you're turning self switch C of event 5 on map 3 OFF, $game_self_switches[3, 5, "C"] will be set to (1 == 0), or false.
This is the method for the Control Timer command. If @params[0] is 0 (start), call the start method of $game_timer with @params[1] multiplied by the frame rate as an argument (which converts the seconds of the timer dialog into the number of frames). If it's 1 (stop), we call $game_timer's stop method. Simples!
This is the method for the Change Gold command. Sets value to the result of operate_value with @params[0], @params[1] and @params[2] as arguments. As you'll no doubt remember from way earlier when we still had hopes and dreams and this episode wasn't the length of all the previous ones combined, these are for the parameters operation (increase or decrease), operand type (constant or variable), and operand (numeric value of variable ID). Then calls the gain_gold method of $game_party with value as the argument.
So carrying on our fine tradition of worked examples, if I choose "Increase", "Constant" and value 500, the values passed to operate_value will be 0, 0 and 500. Then the check for value setting will end up being 500 because operand_type is constant, and operation is increase so the returned value is still value. Had it been "decrease", the value returned would have been -500.
Exactly the same thing as before but with items. Only thing to add is that gain_item takes an additional parameter, which is the item object being gained.
Same thing as before but with weapons. Again we're adding an additional parameter for the "include equipment" checkbox, but that's only relevant if we're subtracting weapons.
Exactly the same thing as before but with armour. This time I have nothing more to say.
This is the method for the Change Party Member command. First, sets actor to the element of $game_actors with ID @params[0]. If the actor exists, then checks to see whether @params[1] is 0 (Add). If so, first checks whether @params[2] is 1 (Initialise checkbox) and if so calls the actor's setup method. Then regardless of the value of @params[2], we call the add_actor method of $game_party, with @params[0] as the argument. If @params[1] is 1 (Remove), we simply call the remove_actor method of $game_party, with @params[0] as the argument.
The Change Battle BGM command. Simply sets the battle_bgm property of $game_system to @params[0]. This will end up being an instance of RPG::BGM consisting of the filename, volume and pitch.
Same thing but for the battle_end_me, the ME that plays when a battle finishes (to this day I can't figure out what ME stands for).
This is the method for the Change Save Access command, and simply sets the save_disabled property of $game_system to the result of @params[0] == 0. As with everything else using this pattern, 0 is ON, 1 is OFF.
Same thing but for the Change Menu Access command.
Same thing but for Change Encounter Disable. Calls make_encounter_count in case you're re-enabling encounters after they've been disabled, otherwise the encounter count won't be re-initialised properly.
Same thing but for Change Formation Access.
This is the method for the Change Window Colour command. Sets the window_tone property of $game_system to @params[0], which will be the R, G, B and grey values of the chosen colour (grey will always be 0 as you can't modify it in the window colour dialog).
This is the method for the Transfer Player command. Returns if the party is in battle (because there's no map for the player to be transferred from/to). Yields the fiber to its caller while the player is in the process of transferring or a message is visible.
If @params[0] is 0 (direct designation), map_id is set to @params[1], x is set to @params[2] and y is set to @params[3]. If params[0] is 1, these variables are set to the element of $game_variables with ID @params[1], @params[2] and @params[3] instead.
Then, we call the reserve_transfer method of $game_player, passing in the map id, x, y and @params[4] (the direction to face after transfer, using the usual values for directions), and the fade_type property of $game_temp is set to @params[5] (which will be the numerical value for normal, white or none) and finally we once again yield the fiber to its caller if the player is transferring.
More or less the same thing but for Set Vehicle Location. The main differences are that we have a parameter for which vehicle to move, no parameter for direction, and we're using its set_location method.
More or less the same thing but for Set Event Location. Uses get_character as usual to get the character object being moved, and uses the moveto method. Note that unlike the previous two methods, this one can't move an event to a different map (which makes sense, as events data is tied to the map it's created on; in order to have the same event on multiple maps, you'd have to make a copy of it) though we do have an additional option for @params[1], which is 2 (Exchange with another event) which just uses the swap method we looked at way back when we covered Game_Character. Finally, if @params[4] > 0 (we didn't choose "retain" for the direction) we call set_direction supplying @params[4] as the argument.
This is the method for the Scroll Map command. Returns if the party is in battle, as there's no map to scroll. Yields the fiber to its caller while the map is in the process of scrolling, then calls the start_scroll method of $game_map, providing @params[0], @params[1] and @params[2] as arguments (the direction, distance and speed for the scroll).
This is the method for the Set Move Route command. Calls the refresh method of $game_map if need_refresh is true. Calls get_character with @params[0] as the argument and assigns the return value to the character variable, then if that variable contains data calls the force_move_route method with @params[1] as the argument. Yields the fiber to its caller while the character is being forced into a move route if the "wait for completion" box was checked.
This is the method for the Get on/off Vehicle command, and simply calls the get_on_off_vehicle method of $game_player.
This is the method for the Change Transparency command, and simply sets the transparent property of $game_player to the result of @params[0] == 0; as with most other assignments that use this structure, a value of 0 means transparency ON and 1 turns it OFF.
This is the method for the Show Animation command. Stores the return value of get_character in the character variable, with @params[0] as its argument, then if the character exists sets its animation ID to @params[1]. Yields the fiber to its caller while the character's animation ID is greater than 0 if the "wait for completion" box was checked.
Exactly the same for Show Balloon Icon, only difference is that we're setting balloon_id instead of animation_id.
This is the method for the Temporarily Erase Event command. Calls the erase method of the current event if the player is on the same map as the event is and @event_id is greater than 0 (you may not remember from last time, but all the erase method does is set a flag that stops event pages from processing).
This is the method for the Change Player Followers command. Sets the visible property of $game_player.followers to @params[0] == 0 (this works exactly the same as every other method that's used for setting boolean properties) and then calls $game_player's refresh method.
This is the method for the Gather Followers command. Returns if the party is in battle (because obviously battle doesn't have any followers) then calls the gather method of $game_player.followers. Yields the fiber to its caller until all followers are in the same tile.
This is the method for the Fadeout Screen command. Yields the fiber to its caller while $game_message.visible returns true. Then calls the start_fadeout method of screen, with 30 as the argument (by default fades take 30 frames), and finally calls wait(30) to wait for 30 frames as well.
Exactly the same as previous but calls start_fadein instead of fadeout.
This is the method for the Tint Screen command. Calls the start_tone_change method of screen, with @params[0] and @params[1] as arguments (@params[0] is a Tone object containing the RGBGr values chosen for the tint, @params[1] is the time in frames) and then also calls wait for @params[1] frames if the "wait for completion" box was checked.
Same thing but for Screen Flash.
Same thing but for Screen Shake. There's actually a bug here because the second line should be "wait(@params[2]) if @params[3]"; they got the indexes wrong. The way it's written, it's waiting for [speed] frames if a duration is set (which is always true). This means that by default waiting for completion on a screen shake doesn't work.
This is the method for the Wait command, and simply waits for @params[0] frames.
This is the method for the Show Picture command. If @params[3] is 0, meaning direct coordinate designation, then x is set to @params[4] and y to @params[5] (obviously, these are the X and Y coordinates you entered in the dialog). Otherwise, the same variables are used as indexes for $game_variables instead. Regardless, we call the show method of screen.pictures[@params[0]], which is the picture number entered in the dialog. As parameters, we supply the name, origin, x, y, zoom x, zoom y, opacity and blend type, as defined by the various elements of @params.
This is the method for the Move Picture command, which is almost identical to Show Picture, but calls the move method instead of show, and waits for @params[10] frames if the wait until completed box was checked. The main difference in the arguments is that we don't have a picture number, and we're adding an additional argument for duration. command_232 doesn't actually have a @params[1], presumably to keep the parameter indexes the same for variables common to showing and moving.
This is the method for Rotate Picture. Not much to say about this one, it simply calls the rotate method of the given picture with @params[1] (speed) as the argument.
This is the method for Tint Picture. Again quite a simple one, calls start_tone_change on the chosen picture with @params[1] (the tone) and @params[2] (duration) as arguments, waiting for duration frames if the wait for completion box was checked.
This is the method for Erase Picture. Calls the erase method of the picture. We should be getting the idea by now.
This is the method for Set Weather. Slightly more complex this time but not that bad: returns if the party is in battle, because weather effects can't happen in battles (though all you really have to do to enable weather in battles is comment out that line and add the appropriate creation/disposal/update of a Spriteset_Weather instance to Spriteset_Battle). Calls the change_weather method of screen with @params[0] (weather type), @params[1] (power) and @params[2] (duration) as arguments. As with everything else that has a wait until completion checkbox, will wait @params[2] frames if it's checked.
This is the method for Play BGM, and simply calls the play method of @params[0], which is the BGM object created by the dialog.
This is the method for Fadeout BGM, and calls the fade method of the RPG::BGM class with the number of seconds * 1000 as an argument (because the parameter for fade is in milliseconds).
This is the method for the Save BGM command, and simply calls the save_bgm method of $game_system.
This is the method for Resume BGM. Surprise surprise, same thing as before but with replay_bgm instead.
Play BGS, pretty much identical to Play BGM.
Fadeout BGS, identical to Fadeout BGM. Noticing a pattern?
Play ME.
Play SE.
Stop SE! I'm honestly not sure why they didn't include a fadeout/stop ME command, but there's nothing stopping you from setting up commands you can call via script if you need them. RPG::ME does have fade and stop methods.
This is the method for Play Movie. Yields the fiber to its caller while a game message is visible, then yields the fiber again (this gives any message box preceding the movie time to fully close before the movie plays; without this line it ends up still being partially visible), sets name to @params[0] (the filename of the movie you chose in the dialog) then calls the play_movie method of the Graphics class with the relative path to the movie as its argument, unless the name is an empty string (this prevents exceptions from trying to play a movie when the choice was "none").
This is the method for the Change Map Name Display command. Simply sets the name_display property of $game_map to true if you picked ON (0) and false if you picked OFF (1).
This is the method for the Change tileset command, and simply calls the change_tileset method of $game_map with @params[0] as the argument, which is the ID of the tileset to change to.
This is the method for the Change Battle Background command, and calls $game_map's change_battleback method with @params[0] (the name of the first background) and @params[1] (the name of the second background) as arguments.
This is the method for the Change Parallax Background command, and calls $game_map's change_parallax method with @params[0] (background name), @params[1] (whether scroll horizontal is checked), @params[2] (horizontal speed), @params[3] (whether scroll vertical is checked) and @params[4] (vertical speed) as arguments.
This is the method for the Get Location Info command. If @params[2] is 0 (direct designation) then x is @params[3] (the X coordinate of the target tile) and y is @params[4] (the Y coordinate of the target tile). If it's 1 (designation with variables) those parameters are used as indexes to $game_variables instead. We then have a case statement for @params[1] (info type). When 0 (terrain tag), value is set to the terrain tag set at the given X/Y. When 1 (event ID), value is set to the ID of the event at the given X/Y. When 2 to 4 inclusive (tile ID layers 1, 2 and 3), value is set to the ID of the tile on the chosen layer (subtracting 2 from @params[1] because the tile IDs are 0-indexed). Otherwise (technically 5, region ID), value is set to the ID of the region the given tile is part of. Following the case statement, we set the variable with index @params[0] to value.
This is the method for the Battle Processing command. Returns if the party is in battle (do I really have to explain why?). If @params[0] is 0 (direct designation of troop), then troop_id is set to @params[1] (the ID of the troop you'll be fighting). If it's 1 (designation with variables), @params[1] is used as an index for $game_variables instead. Otherwise, it's a map-designated troop so we call the make_encounter_troop_id method of $game_player to figure out what the encounter is. If there's a troop corresponding to the calculated ID, we call the setup method of BattleManager, passing in the troop ID, @params[2] (whether the player can escape) and @params[3] (whether the player can lose) as arguments.
Then we set the event_proc property of BattleManager to a new Proc where @branch[@indent] is set to the passed-in value n. This allows the battle end method of BattleManager to set @branch[@indent] to 0, 1 or 2 depending on whether the player won, escaped or lost, via BattleManager.event_proc.call, which you may not remember us looking at way back in Under the Hood part 2. After that we call the make_encounter_count method of $game_player, which refreshes the encounter counter on the map (because it would kind of suck to end up in a random battle immediately after fighting a boss) and then the call method of SceneManager is called, with Scene_Battle as its argument. This obviously transitions the scene to the battle scene. Finally, we yield the fiber to its caller, which prevents the rest of the event commands from processing before the battle is finished.
601 is the code for the "win" branch of a battle. Skips the command if @branch[@indent] isn't 0, because that means the player didn't win.
Same as before, but for 602 (escape).
Same as before, but for 603 (lose).
This is the method for the Shop Processing command. Returns if the party is in battle (though who wouldn't want to buy some potions during a boss fight?) and sets goods to @params as an array element.
Initially, @params is just an array consisting of the item type (0 = item, 1 = weapon, 2 = armour), item ID, standard price flag (0 = standard, 1 = specific), 0 if standard or the price if specific, and a true/false value for whether the shop is purchase-only. We then have a while loop as long as the next event code is 605 (additional shop goods), in which the index is incremented and we push the current event command's parameters to goods (those parameters will be an array with the same elements as the initial one, with the exception of the purchase-only flag). Then we call the call method of SceneManager, with Scene_Shop as the argument, and call the prepare method of SceneManager's scene property (which is now an instance of Scene_Shop), with goods and @params (the purchase-only flag) as arguments. This just initialises the instance variables of Scene_Shop, as we'll see when we eventually get to it. Then, as before, the fiber is yielded to its caller so that further event commands aren't processed before we're done shopping.
This is the method for the Name Input Processing command. Returns if the party is in battle (seriously, Enterbrain, you're missing a trick here. If Alex does terribly in battle I want to rename him to something mean). If an actor exists with the ID @params[0], we call Scene_Name, and the prepare method of the scene with @params[0] (the ID of the actor) and @params[1] (the maximum number of characters) as arguments. Then, we yield the fiber to prevent processing of further commands.
This is the method for the Change HP command. Sets value to the result of calling operate_value with the options chosen in the dialog (I shouldn't have to explain the inner workings of this by now, we've gone over it so many times). We call iterate_actor_var passing in @params[0] (0 for fixed actor, 1 for variable) and @params[1] (actor or variable ID) and use the method as an iterator for a loop, using actor as the iteration variable. We go to the next iteration if the current actor is dead; if not, we call change_hp using value and @params[5] (whether the actor can die) as arguments. If the actor is now dead, we call perform_collapse_effect (only relevant in battle). Once the loop is done, we go to Scene_Gameover if the entire party has died. (You may not remember from when we looked at the modules, but goto is similar to call but doesn't use a stack, because if we're going to the game over screen there are no further scenes to transition to anyway).
As I promised we'd look into iterate_actor_var more when we got to it, let's look at a practical example. I've chosen to decrease Eric's HP by the value of variable 50 (let's say it contains 25), not allowing him to die.
This makes the first line: value = operate_value(1, 1, 50). In operate_value, value becomes the value of variable 50, which is 25, and because it's a decrease this is converted to -25.
We then call: iterate_actor_var(0, 1) and as param1 is 0, we call iterate_actor_id(1) with the block {|actor| yield actor}. In iterate_actor_id, param is 1 so actor is set to $game_actors, and because actor exists the "yield actor" line is processed. This passes the actor object to the block from iterate_actor_var, which then yields the actor to command_311, and the object is stored in the "actor" iteration variable. Alex isn't dead, so we skip the next line. We call actor.change_hp(-25, 0), which sets Alex's self.hp to += -25, so his HP ends up going down by 25. He's not dead, so we don't perform the collapse effect, and the party isn't dead so we don't have to switch to the game over scene.
This is the method for the Change MP command. Sets value using operate_value as usual, then iterates through the actors chosen in the same way as Change HP does. Then actor.mp is increased by value (which obviously results in a decrease if value is a negative number).
This is the method for the Change State command. Iterates through the actors chosen in the same way as usual. Sets already_dead to true if the actor is dead and false otherwise. If @params[2] is 0 (add state), we call the actor's add_state method with @params[3] (the ID of the state chosen) and if it's 1 (remove state) we call remove_state instead with the same argument. After this, we call perform_collapse_effect if the actor is now dead and wasn't already before the state was added/removed, and after the iteration loop we call clear_results on $game_party (which clears each party member's result property). I'm honestly not quite sure why the call to clear_results is there, as adding a state like this doesn't show in the battle log anyway.
This is the method for Recover All. Iterates through the actors chosen using iterate_actor_var and then calls recover_all for each actor.
This is the method for Change EXP. Pretty much the same as the other Change commands, but calling change_exp instead. @params[5] is the value of the "show level up message" checkbox.
Same as before, but for Change Level.
Same as before but for Change Parameters. @params[2] contains the ID of the parameter to be changed.
This is the method for Change Skills. Iterates through the chosen actors; if @params[2] is 0 (learn skill) we call the actor's learn_skill method. If it's 1 (forget skill) we call the actor's forget_skill method. In both cases, @params[3] is used as the argument (the ID of the skill to learn/forget).
This is the method for Change Equipment. Gets the actor with ID @params[0] and calls their change_equip_by_id method, passing in @params[1] (slot ID) and @params[2] (item ID) if the actor exists.
This is the method for Change Name. Gets the actor with ID @params[0] and calls their name method, passing in @params[1] (new name) if the actor exists.
Same as before but for Change Class. @params[1] is the ID of the chosen class, and there's an additional validation check to make sure that class actually exists in $data_classes.
This is the method for Change Actor Graphic. I'm not sure why the if statement was done as a block here instead of having it on one line like the others, but whatever. After calling set_graphic on the actor with @params[1] (character name), @params[2] (character index), @params[3] (face name) and @params[4] (face index) as arguments, we call the refresh method of $game_player (this refreshes the character name/index and followers).
This is the method for Change Vehicle Graphic. @params[0] contains the ID of the vehicle chosen in the dialog. We call the set_graphic method of the vehicle with @params[1] (character name) and @params[2] (character index) if the vehicle exists.
This is the method for Change Nickname. Gets the actor with ID @params[0] and calls their nickname method, passing in @params[1] (new nickname) if the actor exists.
This is the method for Change Enemy HP. It's pretty much identical to Change HP, only it uses iterate_enemy_index and you can't use a variable to determine which enemy is affected.
This is the method for Change Enemy MP. Pretty much the same as Change MP. Again, it uses iterate_enemy_index and you can't use a variable for the enemy ID.
This is the method for Change Enemy State. Again, pretty much the same as Change State with the aforementioned changes.
This is the method for Enemy Recover All. Second verse, same as the first.
This is the method for Enemy Appear. Calls iterate_enemy_index as usual and calls the chosen enemy's appear method, which just sets its hidden flag to false. We then call the make_unique_names method of $game_troop in case there are already living enemies of the same type in the battle.
This is the method for Enemy Transform. Calls iterate_enemy_index as before and calls the chosen enemy's transform method with @params[1] (The ID of the enemy to transform into) as the argument. Then, as before, we call make_unique_names in case there are already living enemies of the same type.
This is the method for Show Battle Animation. Calls iterate_enemy_index and then sets the chosen enemy's animation_id to @params[1] (the ID of the animation to show) if the enemy is alive.
This is the method for Force Action. Calls iterate_battler (because we don't know whether we're forcing an action for an actor or an enemy) with arguments @params[0] (0 for enemy, 1 for actor) and @params[1] (ID of the actor or enemy) using the iteration variable battler. We go to the next iteration if the battler has the death state, because you can't force dead people to do anything. Next, we call the force_action method of the battler, passing in @params[2] (ID of the skill to force) and @params[3] (the index of the target). Then we call the force_action method of BattleManager passing in the battler, which removes the battler from the main action order and flags it as having its action forced.
Finally, we yield the fiber to its caller while an action is being forced, so that no other battle event commands will be processed until the action is done.
This is the method for Abort Battle. Calls the abort method of BattleManager, which sets the phase to :aborting, as we already looked at in an earlier episode. Finally, yields the fiber to its caller to stop any remaining event commands from executing.
This is the method for Open Menu Screen. Returns if the party is in battle, then calls SceneManager's call method passing in Scene_Menu. Directly calls the init_command_position of the Window_MenuCommand class (which sets the class variable @@last_command_symbol to nil) then yields the fiber to its caller so that any further event commands won't process.
This is the method for Open Save Screen. Pretty much identical to the menu one above.
This is the method for Game Over. As mentioned previously, we use goto here instead of call because logic dictates that no scenes are going to be called after game over.
This is the method for Return to Title Screen. Nothing more to say.
And this is the method for Script. Sets script to the event command's parameters[0] (which is the first line of the script) plus a newline character. While the next event code is 655 (further script lines), we increment index by 1 and add the new command's parameters[0] to the script. Finally, we evaluate the full script and return the result.
And...that's it! We're finally done! That is the whole interpreter! This took me like three days! I'm overusing exclamation marks and I don't care!
Now that we're done with the behemoth that is the game objects, we're moving on to sprites. This shouldn't take too long as for the most part these scripts are pretty small. After that, we'll cover windows, then scenes. I promise the next part won't take as long as this one did, either. :)
As usual, if you have any comments, queries, criticisms, corrections or death threats, you know where to write it. I'm expecting a fair few clarification questions as we've covered a couple of pretty advanced topics, especially Fiber and Proc.
Until next time.
So if you remember last time (it was October 2015, I wouldn't blame you if you didn't) we looked at everything from Game_CommonEvent up to Game_Event. Now it's time. The time I never wanted to come. Yes, it's time to crack open the behemoth that makes this whole mother do what it does. It's time to look at Game_Interpreter.
Game_Interpreter
This is going to take up a whole episode by itself, so fasten your seatbelts and get ready to unravel one of the biggest scripts in the entirety of RPG Maker VX Ace. This is the one responsible for making all your fancy event commands -do stuff-, so it's an important one to understand. An in-depth understanding of the interpreter and how it works will enable you to change the behaviour of existing event commands however you see fit.
For future reference, Game_Map, Game_Troop and Game_Event all have their own instance of the interpreter, which is why event commands still work in battle etc.
To begin with, it has two public instance variables:
attr_reader :map_id # Map ID attr_reader :event_id # Event ID (normal events only)
They're pretty self-explanatory: Map ID is the ID of the map the interpreter is for (when used on Game_Map), and Event ID is the ID of the event the interpreter is for (when used on Game_Event).
def initialize(depth = 0) @depth = depth check_overflow clear end
As you can see here, the constructor takes one argument, depth. The instance variable @depth is set to the supplied value (default of 0), then we call check_overflow (which we'll look at in a sec) and finally we call clear (which we'll also look at in a sec).
def check_overflow if @depth >= 100 msgbox(Vocab::EventOverflow) exit end end
As stated in the comments for this method, under normal conditions depth will not exceed 100. This method is basically here to catch infinite loops resulting from recursive event calls (events calling themselves). Essentially, if @depth is ever equal to or greater than 100, an event overflow error is displayed and the game quits.
def clear @map_id = 0 @event_id = 0 @list = nil # Execution content @index = 0 # Index @branch = {} # Branch data @fiber = nil # Fiber end
Clear is a method that basically just starts you off with a fresh slate interpreter with no data. The map ID and event ID are both set to 0, the list of commands is set to nil, the command index is set to 0, the data of the current branch is set to an empty array, and fiber is set to nil (we'll cover fiber shortly).
def setup(list, event_id = 0) clear @map_id = $game_map.map_id @event_id = event_id @list = list create_fiber end
Setup is a method for setting up map events and takes two arguments: a list of event commands and the ID of the event in question. First, we call the clear method, same as with the constructor. The @map_id instance variable is set to the ID of the map the player is currently on ($game_map being a global variable containing all data for the current map, as we've covered before). @event_id is set to the supplied event ID, which defaults to 0. @list is set to the supplied list of commands, and then we call create_fiber. This isn't part of a balanced diet, it's an integral part of how RPG Maker VX Ace even works.
def create_fiber @fiber = Fiber.new { run } if @list end
Well. This is...not very illuminating, is it? To put it in layman's terms: "if @list contains data, set @fiber to a new instance of Fiber, and then...do something with run in brackets?"
To quote the Ruby documentation: "Fibers are primitives for implementing light weight cooperative concurrency in Ruby. Basically they are a means of creating code blocks that can be paused and resumed, much like threads. The main difference is that they are never preempted and that the scheduling must be done by the programmer and not the VM."
A Fiber will not run automatically when created; it must be explicitly asked using the Fiber.resume method (which, you guessed it, we'll look at in a bit). You can also have the fiber give up control to the code that called it using Fiber.yield, which, again, we'll see in a bit.
This is a lot to take in for people who aren't used to Object-Oriented Programming or multithreading, so suffice to say that Fiber basically starts a self-sufficient bit of code which retains state and can be paused/resumed as needed, and in this case, all it's doing is calling the "run" method of Game_Interpreter, which is coming up soon.
def marshal_dump [@depth, @map_id, @event_id, @list, @index + 1, @branch] end
This method returns an array consisting of the depth, the map ID, the event ID, the list of commands, the current index incremented by 1, and the branch data. Although you won't see a call to this method anywhere in the default scripts, it's kind of built-in to the language and will be called whenever Ruby is trying to serialise an interpreter. Without this, you wouldn't be able to save games.
def marshal_load(obj) @depth, @map_id, @event_id, @list, @index, @branch = obj create_fiber end
This is the counterpart to marshal_dump, used when loading data (again, because fibers are not compatible with Marshal); it takes one argument, obj (which is an array of data as defined in marshal_dump) and simply sets each respective instance variable to the element of obj corresponding to it, then creates the fiber using that data.
This also demonstrates a pretty nifty feature of Ruby that I don't think I've ever really gone into, which is that you can assign multiple values in one line by setting a list of variables to an array. For example, if I were to do
@test1, @test2, @test3, @test4 = [1, 2, 3, 4]
basically what would happen is that the array would be iterated, and each value would be assigned to the next variable specified. So @test1 would hold the value 1, @test2 the value 2, and so on. If you supply fewer values in the array than there are variables, the other variables will be set to nil.
To see in action the ramifications of not having this set of methods here, try commenting out marshal_dump and then save your game. What's that? You can't do it? Yeah, that's because Ruby isn't able to serialise your interpreter. :D (similarly, if you comment out marshal_load, you can save your game but can't load it).
def same_map? @map_id == $game_map.map_id end
This method is used to determine if the map the event started on is the same as the one the player is on currently, and is used for a few purposes which we'll see later. It simply compares the @map_id variable to the map_id method of $game_map, returning true if they're equal and false if they're not.
def setup_reserved_common_event if $game_temp.common_event_reserved? setup($game_temp.reserved_common_event.list) $game_temp.clear_common_event true else false end end
This method sets up reserved common events: you probably don't remember, but we looked at common_event_reserved? way back in Slip into Ruby: Under the Hood part 3.
So first what we're doing is checking to see if a common event has even been reserved in the first place, and simply returns false if there isn't: if there isn't one waiting to run, what's the point in setting one up?
If an event is waiting to run, we call the setup method, supplying the list of commands for that common event, then clear the common event ID from $game_temp (so that it doesn't keep setting up the same common event forever) and finally returns true to show that the event is ready to run.
def run wait_for_message while @list[@index] do execute_command @index += 1 end Fiber.yield @fiber = nil end
This is the meat and potatoes of the interpreter, and is essentially what makes it work. You'll remember from create_fiber that this method is the only thing the Fiber block contains, so as soon as we call resume on a fiber it'll call this method.
First we call wait_for_message, which as we'll see a bit later simply...waits for a message (as in the boxes you use for dialogue and stuff). Then it starts a while loop for as long as @list contains data in the element corresponding to @index. Each loop all that will happen is that execute_command will be called, then the index will be incremented by 1. Once the list has been fully iterated through, we call Fiber.yield (which returns control to the code that called the fiber) and set @fiber to nil.
Interestingly enough, because the fiber is only resumed in frame update methods (as, you guessed it, we'll see soon), I'm not actually sure what Fiber.yield even -does- here. As far as I can tell everything still works fine without it, because all it's doing is returning control to a method that it goes back to after it runs anyway. If anyone can shed some light on this please feel free.
def running? @fiber != nil end
This method determines whether the interpreter is running or not, and simply returns true if @fiber is not nil, and false otherwise.
def update @fiber.resume if @fiber end
The frame update method. If @fiber contains data, resume it. As we've already seen, this means that every frame during which a fiber exists, the interpreter's "run" method will be called (if the fiber isn't already running), which will take over control until the event has run its course (which is also why other non-parallel-process events on a map stop what they're doing when you interact with an event).
def iterate_actor_id(param) if param == 0 $game_party.members.each {|actor| yield actor } else actor = $game_actors[param] yield actor if actor end end
This method is an actor ID iterator, which takes one parameter (param); as specified in the comments, if the value is 0, goes through each actor. If 1 or more, yields the actor corresponding to that ID.
This looks more complicated than it is, and once we come across a command that uses this and upcoming iteration methods I'll go through the logic in detail.
def iterate_actor_var(param1, param2) if param1 == 0 iterate_actor_id(param2) {|actor| yield actor } else iterate_actor_id($game_variables[param2]) {|actor| yield actor } end end
Actor variable iterator, taking two parameters: param1 and param2. param1 specifies whether the variable being used is fixed (0) or itself a variable (1), while param2 specifies the actor or variable ID. If param1 is 0, then we call iterate_actor_id with param2 as the param. If it's 1 or more, we call iterate_actor_id using element param2 of $game_variables and yield that actor instead.
def iterate_actor_index(param) if param < 0 $game_party.members.each {|actor| yield actor } else actor = $game_party.members[param] yield actor if actor end end
Actor index iterator, taking one parameter: param. If it's 0 or more, yields the actor corresponding to that value. If -1, yields the whole party.
def iterate_enemy_index(param) if param < 0 $game_troop.members.each {|enemy| yield enemy } else enemy = $game_troop.members[param] yield enemy if enemy end end
Enem index iterator, taking one parameter: param. If it's 0 or more, yields the enemy corresponding to that value. If -1, yields the whole enemy troop.
def iterate_battler(param1, param2) if $game_party.in_battle if param1 == 0 iterate_enemy_index(param2) {|enemy| yield enemy } else iterate_actor_id(param2) {|actor| yield actor } end end end
Battler iterator, taking two parameters: param1 and param2. param1 specifies whether to iterate enemies (0) or actors (1), while param2 specifies the enemy index or actor ID. This check is only done if the party is in battle.
def screen $game_party.in_battle ? $game_troop.screen : $game_map.screen end
Determines the target of a command that affects the screen. If the party is in battle, return the screen of $game_troop; otherwise, return the screen of $game_map.
def execute_command command = @list[@index] @params = command.parameters @indent = command.indent method_name = "command_#{command.code}" send(method_name) if respond_to?(method_name) end
This method is the one that actually executes event commands. First, the variable command is set to the element of @list corresponding to the current @index. @params is set to the parameters of that command, @indent is set to the indent of the command, the method name is set to the string "command_" followed by the code of the command, then if that method name responds to calls, send it.
WTF did you just say, Trihan? Okay, so not sure if I've already covered this but you can call a method in Ruby by using send(name_of_method) as well as calling it directly by name. This allows you to call methods with dynamically-generated names, as is the case here. respond_to? simply checks that the object in question (in this case, Game_Interpreter) responds to (or can call) the given method. This is why all of the event command methods are named command_ (as we'll see), so that they can be called dynamically.
def command_skip @index += 1 while @list[@index + 1].indent > @indent end
This method skips commands which are deeper than the current index and increments it.
To put it more simply, while the indent of the next command in the list is greater than that of the current one, increment the index. I'll explain this in more detail when we get to a method that calls it.
def next_event_code @list[@index + 1].code end
This method simply gets the code of the next event command, by returning the .code property of the element of @list corresponding to 1 more than @index.
def get_character(param) if $game_party.in_battle nil elsif param < 0 $game_player else events = same_map? ? $game_map.events : {} events[param > 0 ? param : @event_id] end end
This method returns a particular character and takes one parameter, param. If the party is in battle, returns nil. If the parameter is -1, returns the player. If any other value, gets a list of events on the map if we're still on the same map as the event started on (otherwise an empty array) and then returns events if param is greater than 0, or events[@event_id] if it's 0 (in other words, "this event").
def operate_value(operation, operand_type, operand) value = operand_type == 0 ? operand : $game_variables[operand] operation == 0 ? value : -value end
This method calculates operated values, given three parameters: operation, operand type, and operand.
Sets the variable "value" to the value of operand if operand_type is 0, or the game variable with index operand if it's 1. Sets the variable "operation" to value if it's 0, and the negative of value if it's 1. I'll explain further once we get to a method that calls it.
def wait(duration) duration.times { Fiber.yield } end
This method "waits" a number of frames, specified by the parameter duration. Simply yields the fiber to its caller for as many times as the supplied duration.
def wait_for_message Fiber.yield while $game_message.busy? end
This method waits while a message is displaying (without this messages would never actually show as the event would just blow past them without waiting for input). Simply yields the fiber to its caller as long as $game_message.busy? returns true. For those who don't remember the time we covered this, it'll return true as long as the message has text, a choice, number input, or an item choice.
def command_101 wait_for_message $game_message.face_name = @params[0] $game_message.face_index = @params[1] $game_message.background = @params[2] $game_message.position = @params[3] while next_event_code == 401 # Text data @index += 1 $game_message.add(@list[@index].parameters[0]) end case next_event_code when 102 # Show Choices @index += 1 setup_choices(@list[@index].parameters) when 103 # Input Number @index += 1 setup_num_input(@list[@index].parameters) when 104 # Select Item @index += 1 setup_item_choice(@list[@index].parameters) end wait_for_message end
That's right, we're only just now getting to the first event command. We're going to be here a while...
All this time you've just been using the "Show Text..." command without really thinking about it. Well, that's all about to change, because we're about to dive into its innards and find out exactly how it works.
First, we call wait_for_message, so if a message is displaying, it'll yield to the caller. I honestly don't actually think the first call here is needed, as it's also called at the end and that'll stop to wait for input anyway.
The face_name property of $game_message is set to @params[0], which is the filename of the face graphic you choose when you double click the box in the "Show Text..." dialog. face_index is set to @params[1], which is the face that you choose from the box on the right showing the actual graphic. background is set to @params[2], which is the numerical value corresponding to "Normal Window", "Dim Background" and "Transparent" in the "Background:" drop-down list. position is set to @params[3], which is the numerical value corresponding to "Bottom", "Middle" and "Top" in the "Position:" drop-down list.
Unfortunately there's no way to change the parameters supplied to these commands without hacking into the actual editor since they're tied into the dialog boxes themselves, but it's handy to know that everything you select in an event command dialog box is stored in the params array so you can refer to it in your code.
Then we enter a while loop, for as long as next_event_code returns 401. This is the internal code used by the event editor to denote "text data" and this is how multiple messages are displayed "smoothly" without the box closing and opening after each one. The while loop simply goes to the next index and adds the parameters[0] property of the element of @list corresponding to @index to $game_message. In English, we add the text of the next message command to the content of the global message variable.
You may have noticed something interesting from this: although "101" is the code of the "Show Text..." command, the actual text to be displayed is created as an entirely separate command with code 401. This is why parameters[0] of command 101 is the faceset name, while parameters[0] of command 401 is the text.
Once the while loop has processed all of the continuous text, we check the next_event_code again to see if it's a command that needs to display something alongside the message box. If it's 102 (Show Choices), we increment the index and call setup_choices with the parameters of the command as an argument. If it's 103 (Input Number), we increment the index and call setup_num_input with the parameters of the command as an argument. If it's 104 (Select Item), we...you get the idea. This basically just sets up choices, numbers or item selection so that they appear alongside the message. If we didn't have this, you'd have to close the message box before the other windows appeared, which would be pretty terrible game design.
And finally, we call wait_for_message, which we've already looked at.
def command_102 wait_for_message setup_choices(@params) Fiber.yield while $game_message.choice? end
The code for Show Choices is surprisingly brief. It calls wait_for_message, then setup_choices supplying @params as the argument (this is the list of choices and the cancellation choice), and yields the fiber to its caller while $game_message.choice? returns true. For those who don't remember that lesson, it returns true if the size of @choices is > 0, which will obviously be the case if you've put choices in the boxes.
So here we come to another interesting note. Although Show Choices can be used by itself, if you have a Show Choices command following a Show Text, command_102 will never actually be called, because the Show Text does the choice processing for you! I actually think the way Enterbrain did this is pretty neat.
def setup_choices(params) params[0].each {|s| $game_message.choices.push(s) } $game_message.choice_cancel_type = params[1] $game_message.choice_proc = Proc.new {|n| @branch[@indent] = n } end
This method sets up the choices you select from when using the Show Choices event command. It iterates through each element of params[0] (which is the list of choices you wrote in the boxes) and runs a block on it, with s as the block variable (meaning each time it iterates, s will be whatever string you put in the next choice box, and it will be pushed to the choices array of $game_message).
The choice_cancel_type of $game_message is set to params[1]; this is the numerical representation of "Disallow", "Choice 1", "Choice 2", "Choice 3", "Choice 4", or "Branch" depending on what you chose in the dialog.
Finally, choice_proc of $game_message is set to a new Proc, with block variable n, where the element of @branch corresponding to @indent is set to n. Wait, what?
Okay, so Proc is basically just a fancy way to write a block of code that can be called in different contexts; if choice_proc is called, the value passed to it will be passed as "n", which will then be stored in @branch[@indent]. We'll revisit this when we finally get to Window_ChoiceList, which won't be for a while yet.
def command_402 command_skip if @branch[@indent] != @params[0] end
command_402 is a special one which is called when the interpreter reaches a choice branch. It skips the command if @branch[@indent] (which contains the index of the chosen option) is not equal to @params[0] (the ID of the current choice).
def command_403 command_skip if @branch[@indent] != 4 end
Here's another interesting one. The comment says "When Cancel" so you'd think it would be connected to the cancellation of a choice, but as far as I can tell it's never called by anything. What it does is skips the command if @branch[@indent] isn't equal to 4. Intriguingly, although command_403 never seems to be called by Show Choice, it does try to call command_404...which doesn't exist. I'm not sure whether this is a mistake in the coding of the editor. Another interesting note is that as far as I'm aware it's not actually possible for @branch[@indent] to ever equal 4 in a choice window anyway, as it's 0-indexed and there are only 4 options (0, 1, 2, 3).
def command_103 wait_for_message setup_num_input(@params) Fib
end
This method processes number input (when it's not immediately following Show Text). Simply waits for messages, calls setup_num_input with @params as the argument, and yields the fiber to the caller while num_input? from $game_message returns true (which is the case as long as @num_input_variable_id > 0, which will be true for the duration of the number input since this is the variable you picked in the dialog of the command).
def setup_num_input(params) $game_message.num_input_variable_id = params[0] $game_message.num_input_digits_max = params[1] end
And here we see the ever-so-complicated code for setting up number input, which is...setting the num_input_variable_id property of $game_message to params[0], which is the variable you picked in the dialog, and num_inputs_digits_max to params[1], which is the value you entered in the "Digits:" box.
def command_104 wait_for_message setup_item_choice(@params) Fiber.yield while $game_message.item_choice? end
This method processes the "select item" box (when it's not immediately following Show Text). Simply waits for messages, calls setup_item_choice with @params as the argument, and yields the fiber to the caller while item_choice? from $game_message returns true (as above, but for the item_choice_variable_id instead).
def setup_item_choice(params) $game_message.item_choice_variable_id = params[0] end
Pretty much the same as setting up number input, only we don't have max digits to worry about this time.
def command_105 Fiber.yield while $game_message.visible $game_message.scroll_mode = true $game_message.scroll_speed = @params[0] $game_message.scroll_no_fast = @params[1] while next_event_code == 405 @index += 1 $game_message.add(@list[@index].parameters[0]) end wait_for_message end
Show Scrolling Text! So it yields the fiber to the caller while $game_message.visible is true (so that if a message is currently showing, the scrolling text won't interfere with it) then sets some properties in $game_message: scroll_mode is set to true, scroll_speed is set to @params[0], which is the value you set in the dialog, and scroll_no_fast is set to @params[1], which determines whether or not the text can be fast forwarded.
We then enter a while loop as long as next_event_code is 405 (the scrolling text equivalent of 401 for text) and in each iteration increment the index and add parameters[0] (the text) to $game_message.
And finally, we wait for the message to finish scrolling.
def command_108 @comments = [@params[0]] while next_event_code == 408 @index += 1 @comments.push(@list[@index].parameters[0]) end end
This method is for the "Comment" command. The @comments array is populated with an array consisting of @params[0], which is just the comment text. Then we enter a while loop as long as next_event_code is 408, where we increment @index and push parameters[0] to @comments. I honestly have no idea what this loop does as I've never been able to engineer an event that had a command with code 408 in it, regardless of how many comments I created and where.
def command_111 result = false case @params[0] when 0 # Switch result = ($game_switches[@params[1]] == (@params[2] == 0)) when 1 # Variable value1 = $game_variables[@params[1]] if @params[2] == 0 value2 = @params[3] else value2 = $game_variables[@params[3]] end case @params[4] when 0 # value1 is equal to value2 result = (value1 == value2) when 1 # value1 is greater than or equal to value2 result = (value1 >= value2) when 2 # value1 is less than or equal to value2 result = (value1 <= value2) when 3 # value1 is greater than value2 result = (value1 > value2) when 4 # value1 is less than value2 result = (value1 < value2) when 5 # value1 is not equal to value2 result = (value1 != value2) end when 2 # Self switch if @event_id > 0 key = [@map_id, @event_id, @params[1]] result = ($game_self_switches[key] == (@params[2] == 0)) end when 3 # Timer if $game_timer.working? if @params[2] == 0 result = ($game_timer.sec >= @params[1]) else result = ($game_timer.sec <= @params[1]) end end when 4 # Actor actor = $game_actors[@params[1]] if actor case @params[2] when 0 # in party result = ($game_party.members.include?(actor)) when 1 # name result = (actor.name == @params[3]) when 2 # Class result = (actor.class_id == @params[3]) when 3 # Skills result = (actor.skill_learn?($data_skills[@params[3]])) when 4 # Weapons result = (actor.weapons.include?($data_weapons[@params[3]])) when 5 # Armors result = (actor.armors.include?($data_armors[@params[3]])) when 6 # States result = (actor.state?(@params[3])) end end when 5 # Enemy enemy = $game_troop.members[@params[1]] if enemy case @params[2] when 0 # appear result = (enemy.alive?) when 1 # state result = (enemy.state?(@params[3])) end end when 6 # Character character = get_character(@params[1]) if character result = (character.direction == @params[2]) end when 7 # Gold case @params[2] when 0 # Greater than or equal to result = ($game_party.gold >= @params[1]) when 1 # Less than or equal to result = ($game_party.gold <= @params[1]) when 2 # Less than result = ($game_party.gold < @params[1]) end when 8 # Item result = $game_party.has_item?($data_items[@params[1]]) when 9 # Weapon result = $game_party.has_item?($data_weapons[@params[1]], @params[2]) when 10 # Armor result = $game_party.has_item?($data_armors[@params[1]], @params[2]) when 11 # Button result = Input.press?(@params[1]) when 12 # Script result = eval(@params[1]) when 13 # Vehicle result = ($game_player.vehicle == $game_map.vehicles[@params[1]]) end @branch[@indent] = result command_skip if !@branch[@indent] end
Conditional Branch is an absolute monster. Let's slay it.
So the first line is as simple as they come: set result to false. Moving on.
Then we have a case statement for @params[0]: this is the value of the radio button in the dialog determining whether it's Switch, Variable, Self Switch, Timer, Actor, Enemy, Character, Vehicle, Gold, Item, Weapon, Armour, Button, or Script.
When 0 (Switch), result is set to the result of comparing $game_switches[@params[1]] to (@params[2] == 0)--in other words, whether the two values are the same. @params[1] is the ID of the switch you chose from the selection dialog. @params[2] is 0 if you chose ON, and 1 if you chose OFF. So if you're doing a branch for switch 43 being ON, @params[1] will be 43, @params[2] will be 0, and it'll then be comparing $game_switches[43] to (0 == 0); if the switch is on, this will end up being [switch state] == true, which will return true if the switch is true, and false otherwise. Obviously this is turned around if checking for the switch being false, as the comparison then becomes (1 == 0) which is false, so if the switch is also false it'll return true.
When 1 (Variable), value1 is set to $game_variables[@params[1]] (the ID of the variable you set in the dialog); then if @params[2] is 0 ("Constant" in the dialog), value2 is set to @params[3], otherwise it's set to $game_variables[@params[3]] ("Variable" in the dialog). We then have a case statement for @params[4]; if it's 0 (Equal to), result is set to value1 == value2. If it's 1 (Greater than or equal to) result is set to value1 >= value2. You've done maths before, you should be able to work out the rest: 2 is less than or equal to, 3 is greater than, 4 is less than, 5 is not equal to.
When 2 (Self Switch), first we check whether @event_id is greater than 0 (because only events can have self switches), then key is set to an array consisting of the current map ID, the current event ID, and @params[1] (the letter of the self switch being checked) and result is set to $game_self_switches[key] == (@params[2] == 0)). So if we're currently on map 5 and checking that event 6 has self switch B ON, key becomes [5, 6, "B"] and result becomes $game_self_switches[[5, 6, "B"]] == (0 == 0); if the self switch is on, this will return true, otherwise it'll return false.
When 3 (Timer), first we check whether $game_timer.working? is true (because there's no point in checking if there isn't even a timer running); if so, and if @params[2] == 0, result is set to $game_timer.sec >= @params[1]. Otherwise, it's set to $game_timer.sec <= @params[1]. @params[2] is 0 if you chose "or More" and 1 if you chose "or Less", while @params[1] is the total number of seconds in the timer, which I believe the editor automatically calculates from the minutes and seconds boxes.
When 4 (Actor), the variable actor is set to $game_actors[@params[1]] (the ID of the actor you chose in the dialog). Then if actor exists, we have a case statement for @params[2]: when 0 (in party) result is true if $game_party includes the given actor, and false otherwise. When 1 (name), result is true if the actor's name is equal to @params[3] (the string entered in the name box). When 2 (Class), result is true if the actor's class ID is equal to @params[3] (the ID of the class selected in the box). When 3 (Skills), result is true if the actor has learned the skill with ID @params[3]. When 4 (Weapons), result is true if the actor has equipped the weapon with ID @params[3]. When 5 (Armours), result is true if the actor has equipped the armour with ID @params[3]. When 6 (States), result is true if the actor has the state with ID @params[3].
When 5 (Enemy), the variable enemy is set to $game_troop.members[@params[1]] (The enemy ID chosen in the dialog). Then if enemy exists, we have a case statement for @params[2]: when 0 (Appeared) result is true if the enemy with the given ID is currently alive. When 1 (State) result is true if the enemy with the given ID has the state with ID @params[3].
When 6 (Character), the variable character is set to the result of calling get_character with @params[1] as its argument. As we saw earlier, this will either be -1 (player), 0 (this event), or the ID of an event on the map. Then if character exists, result is true if the character's direction is equal to @params[2]. Note that internally directions have a numerical representation: down is 2, left is 4, right is 6, up is 8 (which is, incidentally, also the number on a numpad corresponding to that arrow key).
Interesting note: although "Vehicle" is the next option in the list, it actually has a higher number internally.
When 7 (Gold), we have a case statement for @params[2]: when 0 (greater than or equal to) result is true if the party's gold >= @params[1] (the entered value). When 1 (less than or equal to) result is true if the party's gold <= @params[1]. When 2 (less than) result is true if the party's gold < @params[1].
When 8 (Item), result is true if the party possesses the item with the ID @params[1].
When 9 (Weapon), result is true if the party possesses the weapon with ID @params[1]. If you checked the "include equipment" box, @params[2] will be true, false otherwise.
When 10 (Armour), result is true if the party possesses the armour with ID @params[1]. If you checked the "include equipment" box, @params[2] will be true, false otherwise.
When 11 (Button), result is true if the player is pressing the button corresponding to @params[1]. The directions are the same as previously mentioned (down = 2, left = 4, right = 6, up = 8), button A is 11, B is 12, C is 13, X is 14, Y is 15, Z is 16, L is 17 and R is 18.
When 12 (Script), result is true if the evaluated script stored in @params[1] (whatever you entered in the box) returns true.
When 13 (Vehicle), result is true if $game_player.vehicle is the same as $game_map.vehicles[@params[1]]. 0 is Boat, 1 is Ship, 2 is Airship.
Finally, @branch[@indent] is set to result, and we call command_skip if the result was false (because if the condition wasn't met, we don't want to process the code inside it)
def command_411 command_skip if @branch[@indent] end
This method is basically just there to skip the commands in an "else" if the branch result was true (the method itself will be called when the interpreter reaches the "else" line because its internal code is 411). Without this, the code in the else would still run even if one of the other branches had its conditions met.
def command_112 end
It's...an empty method. Yep, the command method for "Loop" doesn't actually do anything. In fact, you can delete this method and loop will still work fine. I think it's basically just there for completion's sake.
def command_413 begin @index -= 1 end until @list[@index].indent == @indent end
This is what makes the loop actually work. The contents of the begin-end block will repeat until the indent of the current line of code is equal to the current indent. The only thing in the block is a decrement of the index.
Think about how indenting works in event commands, and you'll realise how this actually results in a loop. Picture a loop with a message in it:
Loop
Show Text: "Hello World!"
Repeat Above
112 is the "Loop", 413 is the "Repeat Above", and both have an indent of 0. The Show Text, 101, would have an indent of 1. So what the command_413 is doing is going backwards in the event commands until it finds another command with an indent of 0...which will basically take us back to the Loop. Execution will then continue from there, so it'll execute everything inside the loop again, at which point it'll go backwards until it finds indent 0...and so on and so forth. Unless...
def command_113 loop do @index += 1 return if @index >= @list.size - 1 return if @list[@index].code == 413 && @list[@index].indent < @indent end end
This is how Break Loop works. Oddly enough, breaking a loop starts...with a loop. Inside that loop, we increment the index by 1, and return if the new index is greater than or equal to the size of the event commands list less 1. We also return if the code of the new command is 413 (Repeat Above) and the indent of the new command is less than the one we've just come from.
Note that the first return means that putting a Break Loop command in an event outside of a Loop-Repeat Above block will cause anything after the break to be ignored. GreatRedSpirit has also pointed out that there's a bug where having a Break Loop inside a conditional branch before a nested loop where there are no more commands between the Repeat Above of the inner loop and the Repeat Above of the outer loop will soft lock your game.
def command_115 @index = @list.size end
Exit Event Processing couldn't be simpler. All it does is set the current index to the size of the commands list, so the next time it tries to process a command it's at the end of the event.
def command_117 common_event = $data_common_events[@params[0]] if common_event child = Game_Interpreter.new(@depth + 1) child.setup(common_event.list, same_map? ? @event_id : 0) child.run end end
This is the method for Call Common Event. First, common_event is set to the data pertaining to the common event specified by @params[0] (the event chosen in the dialog). If that common event exists, child is set to a new instance of Game_Interpreter with depth 1 greater than the current depth, we call setup on the child event, and then call its run method. Note that due to the recursive creation of new interpreters, if you have a map event call a common event which calls itself you'll eventually end up running afoul of the check_overflow method. Don't do this.
def command_118 end
Another one that doesn't do anything! The method for Label is just as pointless as the one for Loop.
def command_119 label_name = @params[0] @list.size.times do |i| if @list[i].code == 118 && @list[i].parameters[0] == label_name @index = i return end end end
Jump to Label is, as with looping, the meat and potatoes of the block. First, label_name is set to @params[0] (which is the label name specified in the dialog). Then we enter a times loop with @list.size iterations (in other words, a number of iterations equal to the number of commands in the event) using i as an iteration variable. If the code of the list element with index i is 118 (label) and its parameters[0] (which for a label is its name) is equal to label_name, we set @index to i and return (in other words, if we find a label with the supplied name, we'll continue processing the event from that label).
def command_121 (@params[0]..@params[1]).each do |i| $game_switches[i] = (@params[2] == 0) end end
Control Switches is simple enough. If a single switch is selected, I believe @params[0] and @params[1] will be the same value, while "Batch" will result in 0 being the lower bound and 1 being the upper. Then for each value from the first value to the last (inclusive), we loop through using i as an iteration variable and set $game_switches[i] to @params[2] == 0, which will be true if you selected ON, and false if you selected OFF (since they have values of 0 and 1 respectively).
def command_122 value = 0 case @params[3] # Operand when 0 # Constant value = @params[4] when 1 # Variable value = $game_variables[@params[4]] when 2 # Random value = @params[4] + rand(@params[5] - @params[4] + 1) when 3 # Game Data value = game_data_operand(@params[4], @params[5], @params[6]) when 4 # Script value = eval(@params[4]) end (@params[0]..@params[1]).each do |i| operate_variable(i, @params[2], value) end end
Control Variables is a little more complex, obviously because it has more options. First of all, value is set to 0. Then we have a case statement for @params[3], which as the comment says is for the operand. When 0 (constant), value is set to @params[4] (the value in the box). When 1 (Variable), value is set to the element of $game_variables corresponding to @params[4]. When 2 (Random), value is set to @params[4] (the value in the first box) plus a random number from 0 to 1 less than the difference between @params[5] (the value in the second box) and @params[4]. Basically, if you wanted, say, a random number from 20 to 40, value would be 20 + a random number from 0 to 21 exclusive (so 0-20). When 3 (Game Data), value is set to the result of calling game_data_operand with @params[4] (type), @params[5] (param 1) and @params[6] (param 2) as arguments. We'll look at game_data_operand in a sec. When 4 (Script), value is set to the result of evaluating the script entered in the box, stored in @params[4].
Finally, we'll loop through each value from @params[0] to @params[1] (which will either be a single value or a lower and upper bound for "Batch" as with switches) using i as the iteration variable, and each iteration we'll call operate_variable with i, @params[2] and value as arguments. Again, we'll look at that in a sec.
def game_data_operand(type, param1, param2) case type when 0 # Items return $game_party.item_number($data_items[param1]) when 1 # Weapons return $game_party.item_number($data_weapons[param1]) when 2 # Armor return $game_party.item_number($data_armors[param1]) when 3 # Actors actor = $game_actors[param1] if actor case param2 when 0 # Level return actor.level when 1 # EXP return actor.exp when 2 # HP return actor.hp when 3 # MP return actor.mp when 4..11 # Parameter return actor.param(param2 - 4) end end when 4 # Enemies enemy = $game_troop.members[param1] if enemy case param2 when 0 # HP return enemy.hp when 1 # MP return enemy.mp when 2..9 # Parameter return enemy.param(param2 - 2) end end when 5 # Character character = get_character(param1) if character case param2 when 0 # x-coordinate return character.x when 1 # y-coordinate return character.y when 2 # direction return character.direction when 3 # screen x-coordinate return character.screen_x when 4 # screen y-coordinate return character.screen_y end end when 6 # Party actor = $game_party.members[param1] return actor ? actor.id : 0 when 7 # Other case param1 when 0 # map ID return $game_map.map_id when 1 # number of party members return $game_party.members.size when 2 # gold return $game_party.gold when 3 # steps return $game_party.steps when 4 # play time return Graphics.frame_count / Graphics.frame_rate when 5 # timer return $game_timer.sec when 6 # save count return $game_system.save_count when 7 # battle count return $game_system.battle_count end end return 0 end
This is the method which returns the game data being used for the "Game Data" setting of Control Variables. It takes three parameters: type, param1 and param2.
First, we have a case statement for type.
When 0 (Items), we return the party's currently-held number of the given item.
When 1 (Weapons), we return the party's currently-held number of the given weapon.
When 2 (Armour), we return the party's currently-held number of the given armour.
When 3 (Actors), we set actor to the element of $game_actors with ID param1 (the actor chosen in the box), then if the variable contains data we have another case statement for param2 (the parameter in the second box). When 0 (Level), we return the actor's level. When 1 (Exp), we return the actor's experience. When 2 (HP), we return the actor's HP. When 3 (MP) we return the actor's MP. When between 4 and 11 inclusive (Parameters) we return the actor's parameter with ID param2 - 4 (because obviously the internal parameter IDs start at 0 but param2 starts at 4, so we need to subtract the difference).
When 4 (Enemies), we set enemy to the element of $game_troop with ID param1, then if the variable contains data we have another case statement for param2. When 0 (HP), we return the enemy's HP. When 1 (MP), we return the enemy's MP. When between 2 and 9 inclusive (Parameters) we return the enemy's parameter with ID param2 - 2.
When 5 (Character), we set character to the result of get_character with param1 as its argument, then if the variable contains data we have a case statement for param2. When 0 (X coordinate), we return the character's x. When 1 (Y coordinate), we return the character's Y. When 2 (Direction), we return the character's direction. When 3 (Screen X), we return the character's screen x. When 4 (Screen Y), we return the character's screen y.
When 6 (Party), we set actor to the element of $game_party.members with ID param1, then if the variable contains data we return the actor's ID, otherwise we return 0 (because they're not in the party).
When 7 (Other), we have a case statement for param1. When 0 (Map ID), we return the map ID from $game_map. When 1 (Number of party members) we return the size of $game_party.members. When 2 (Gold), we return the gold of $game_party. When 3 (Steps), we return the steps of $game_party. When 4 (Play time), we return the number of passed frames divided by the frame rate (so for example, say the frame_count is 5938 frames, we divide that by the frame rate (60 fps by default) to see that we've been playing for 98 seconds. When 5 (Timer), we return the number of seconds elapsed in $game_timer. When 6 (Save count), we return the number of times the player has saved the game. When 7 (Battle count), we return the number of battles the player has fought.
Finally, we return 0 if no other case has been hit.
def operate_variable(variable_id, operation_type, value) begin case operation_type when 0 # Set $game_variables[variable_id] = value when 1 # Add $game_variables[variable_id] += value when 2 # Sub $game_variables[variable_id] -= value when 3 # Mul $game_variables[variable_id] *= value when 4 # Div $game_variables[variable_id] /= value when 5 # Mod $game_variables[variable_id] %= value end rescue $game_variables[variable_id] = 0 end end
The operate_variable method executes the variable operation selected in the Control Variables command, and takes three parameters: variable ID, operation type, and value. First we have a case statement for operation_type. I shouldn't have to go through it at this point after everything else I've covered, but basically 0 sets the variable to value, 1 adds value to it, 2 subtracts value from it, 3 multiplies it by value, 4 divides it by value, and 5 divides it by value and returns the remainder. The rescue part of the begin-end block will catch any exceptions (if we try to divide by 0, for example) and set the variable to 0 instead.
def command_123 if @event_id > 0 key = [@map_id, @event_id, @params[0]] $game_self_switches[key] = (@params[1] == 0) end end
This is the method for the Control Self Switch command. First we make sure @event_id is greater than 0, because if we're not in a map event there's no point in doing anything with self switches. key is set to an array consisting of the map ID, event ID and @params[0] (which is the letter of the self switch being checked), then $game_self_switches[key] is set to @params[1] == 0. So if, for example, you're turning self switch C of event 5 on map 3 OFF, $game_self_switches[3, 5, "C"] will be set to (1 == 0), or false.
def command_124 if @params[0] == 0 # Start $game_timer.start(@params[1] * Graphics.frame_rate) else # Stop $game_timer.stop end end
This is the method for the Control Timer command. If @params[0] is 0 (start), call the start method of $game_timer with @params[1] multiplied by the frame rate as an argument (which converts the seconds of the timer dialog into the number of frames). If it's 1 (stop), we call $game_timer's stop method. Simples!
def command_125 value = operate_value(@params[0], @params[1], @params[2]) $game_party.gain_gold(value) end
This is the method for the Change Gold command. Sets value to the result of operate_value with @params[0], @params[1] and @params[2] as arguments. As you'll no doubt remember from way earlier when we still had hopes and dreams and this episode wasn't the length of all the previous ones combined, these are for the parameters operation (increase or decrease), operand type (constant or variable), and operand (numeric value of variable ID). Then calls the gain_gold method of $game_party with value as the argument.
So carrying on our fine tradition of worked examples, if I choose "Increase", "Constant" and value 500, the values passed to operate_value will be 0, 0 and 500. Then the check for value setting will end up being 500 because operand_type is constant, and operation is increase so the returned value is still value. Had it been "decrease", the value returned would have been -500.
def command_126 value = operate_value(@params[1], @params[2], @params[3]) $game_party.gain_item($data_items[@params[0]], value) end
Exactly the same thing as before but with items. Only thing to add is that gain_item takes an additional parameter, which is the item object being gained.
def command_127 value = operate_value(@params[1], @params[2], @params[3]) $game_party.gain_item($data_weapons[@params[0]], value, @params[4]) end
Same thing as before but with weapons. Again we're adding an additional parameter for the "include equipment" checkbox, but that's only relevant if we're subtracting weapons.
def command_128 value = operate_value(@params[1], @params[2], @params[3]) $game_party.gain_item($data_armors[@params[0]], value, @params[4]) end
Exactly the same thing as before but with armour. This time I have nothing more to say.
def command_129 actor = $game_actors[@params[0]] if actor if @params[1] == 0 # Add if @params[2] == 1 # Initialize $game_actors[@params[0]].setup(@params[0]) end $game_party.add_actor(@params[0]) else # Remove $game_party.remove_actor(@params[0]) end end end
This is the method for the Change Party Member command. First, sets actor to the element of $game_actors with ID @params[0]. If the actor exists, then checks to see whether @params[1] is 0 (Add). If so, first checks whether @params[2] is 1 (Initialise checkbox) and if so calls the actor's setup method. Then regardless of the value of @params[2], we call the add_actor method of $game_party, with @params[0] as the argument. If @params[1] is 1 (Remove), we simply call the remove_actor method of $game_party, with @params[0] as the argument.
def command_132 $game_system.battle_bgm = @params[0] end
The Change Battle BGM command. Simply sets the battle_bgm property of $game_system to @params[0]. This will end up being an instance of RPG::BGM consisting of the filename, volume and pitch.
def command_133 $game_system.battle_end_me = @params[0] end
Same thing but for the battle_end_me, the ME that plays when a battle finishes (to this day I can't figure out what ME stands for).
def command_134 $game_system.save_disabled = (@params[0] == 0) end
This is the method for the Change Save Access command, and simply sets the save_disabled property of $game_system to the result of @params[0] == 0. As with everything else using this pattern, 0 is ON, 1 is OFF.
def command_135 $game_system.menu_disabled = (@params[0] == 0) end
Same thing but for the Change Menu Access command.
def command_136 $game_system.encounter_disabled = (@params[0] == 0) $game_player.make_encounter_count end
Same thing but for Change Encounter Disable. Calls make_encounter_count in case you're re-enabling encounters after they've been disabled, otherwise the encounter count won't be re-initialised properly.
def command_137 $game_system.formation_disabled = (@params[0] == 0) end
Same thing but for Change Formation Access.
def command_138 $game_system.window_tone = @params[0] end
This is the method for the Change Window Colour command. Sets the window_tone property of $game_system to @params[0], which will be the R, G, B and grey values of the chosen colour (grey will always be 0 as you can't modify it in the window colour dialog).
def command_201 return if $game_party.in_battle Fiber.yield while $game_player.transfer? || $game_message.visible if @params[0] == 0 # Direct designation map_id = @params[1] x = @params[2] y = @params[3] else # Designation with variables map_id = $game_variables[@params[1]] x = $game_variables[@params[2]] y = $game_variables[@params[3]] end $game_player.reserve_transfer(map_id, x, y, @params[4]) $game_temp.fade_type = @params[5] Fiber.yield while $game_player.transfer? end
This is the method for the Transfer Player command. Returns if the party is in battle (because there's no map for the player to be transferred from/to). Yields the fiber to its caller while the player is in the process of transferring or a message is visible.
If @params[0] is 0 (direct designation), map_id is set to @params[1], x is set to @params[2] and y is set to @params[3]. If params[0] is 1, these variables are set to the element of $game_variables with ID @params[1], @params[2] and @params[3] instead.
Then, we call the reserve_transfer method of $game_player, passing in the map id, x, y and @params[4] (the direction to face after transfer, using the usual values for directions), and the fade_type property of $game_temp is set to @params[5] (which will be the numerical value for normal, white or none) and finally we once again yield the fiber to its caller if the player is transferring.
def command_202 if @params[1] == 0 # Direct designation map_id = @params[2] x = @params[3] y = @params[4] else # Designation with variables map_id = $game_variables[@params[2]] x = $game_variables[@params[3]] y = $game_variables[@params[4]] end vehicle = $game_map.vehicles[@params[0]] vehicle.set_location(map_id, x, y) if vehicle end
More or less the same thing but for Set Vehicle Location. The main differences are that we have a parameter for which vehicle to move, no parameter for direction, and we're using its set_location method.
def command_203 character = get_character(@params[0]) if character if @params[1] == 0 # Direct designation character.moveto(@params[2], @params[3]) elsif @params[1] == 1 # Designation with variables new_x = $game_variables[@params[2]] new_y = $game_variables[@params[3]] character.moveto(new_x, new_y) else # Exchange with another event character2 = get_character(@params[2]) character.swap(character2) if character2 end character.set_direction(@params[4]) if @params[4] > 0 end end
More or less the same thing but for Set Event Location. Uses get_character as usual to get the character object being moved, and uses the moveto method. Note that unlike the previous two methods, this one can't move an event to a different map (which makes sense, as events data is tied to the map it's created on; in order to have the same event on multiple maps, you'd have to make a copy of it) though we do have an additional option for @params[1], which is 2 (Exchange with another event) which just uses the swap method we looked at way back when we covered Game_Character. Finally, if @params[4] > 0 (we didn't choose "retain" for the direction) we call set_direction supplying @params[4] as the argument.
def command_204 return if $game_party.in_battle Fiber.yield while $game_map.scrolling? $game_map.start_scroll(@params[0], @params[1], @params[2]) end
This is the method for the Scroll Map command. Returns if the party is in battle, as there's no map to scroll. Yields the fiber to its caller while the map is in the process of scrolling, then calls the start_scroll method of $game_map, providing @params[0], @params[1] and @params[2] as arguments (the direction, distance and speed for the scroll).
def command_205 $game_map.refresh if $game_map.need_refresh character = get_character(@params[0]) if character character.force_move_route(@params[1]) Fiber.yield while character.move_route_forcing if @params[1].wait end end
This is the method for the Set Move Route command. Calls the refresh method of $game_map if need_refresh is true. Calls get_character with @params[0] as the argument and assigns the return value to the character variable, then if that variable contains data calls the force_move_route method with @params[1] as the argument. Yields the fiber to its caller while the character is being forced into a move route if the "wait for completion" box was checked.
def command_206 $game_player.get_on_off_vehicle end
This is the method for the Get on/off Vehicle command, and simply calls the get_on_off_vehicle method of $game_player.
def command_211 $game_player.transparent = (@params[0] == 0) end
This is the method for the Change Transparency command, and simply sets the transparent property of $game_player to the result of @params[0] == 0; as with most other assignments that use this structure, a value of 0 means transparency ON and 1 turns it OFF.
def command_212 character = get_character(@params[0]) if character character.animation_id = @params[1] Fiber.yield while character.animation_id > 0 if @params[2] end end
This is the method for the Show Animation command. Stores the return value of get_character in the character variable, with @params[0] as its argument, then if the character exists sets its animation ID to @params[1]. Yields the fiber to its caller while the character's animation ID is greater than 0 if the "wait for completion" box was checked.
def command_213 character = get_character(@params[0]) if character character.balloon_id = @params[1] Fiber.yield while character.balloon_id > 0 if @params[2] end end
Exactly the same for Show Balloon Icon, only difference is that we're setting balloon_id instead of animation_id.
def command_214 $game_map.events[@event_id].erase if same_map? && @event_id > 0 end
This is the method for the Temporarily Erase Event command. Calls the erase method of the current event if the player is on the same map as the event is and @event_id is greater than 0 (you may not remember from last time, but all the erase method does is set a flag that stops event pages from processing).
def command_216 $game_player.followers.visible = (@params[0] == 0) $game_player.refresh end
This is the method for the Change Player Followers command. Sets the visible property of $game_player.followers to @params[0] == 0 (this works exactly the same as every other method that's used for setting boolean properties) and then calls $game_player's refresh method.
def command_217 return if $game_party.in_battle $game_player.followers.gather Fiber.yield until $game_player.followers.gather? end
This is the method for the Gather Followers command. Returns if the party is in battle (because obviously battle doesn't have any followers) then calls the gather method of $game_player.followers. Yields the fiber to its caller until all followers are in the same tile.
def command_221 Fiber.yield while $game_message.visible screen.start_fadeout(30) wait(30) end
This is the method for the Fadeout Screen command. Yields the fiber to its caller while $game_message.visible returns true. Then calls the start_fadeout method of screen, with 30 as the argument (by default fades take 30 frames), and finally calls wait(30) to wait for 30 frames as well.
def command_222 Fiber.yield while $game_message.visible screen.start_fadein(30) wait(30) end
Exactly the same as previous but calls start_fadein instead of fadeout.
def command_223 screen.start_tone_change(@params[0], @params[1]) wait(@params[1]) if @params[2] end
This is the method for the Tint Screen command. Calls the start_tone_change method of screen, with @params[0] and @params[1] as arguments (@params[0] is a Tone object containing the RGBGr values chosen for the tint, @params[1] is the time in frames) and then also calls wait for @params[1] frames if the "wait for completion" box was checked.
def command_224 screen.start_flash(@params[0], @params[1]) wait(@params[1]) if @params[2] end
Same thing but for Screen Flash.
def command_225 screen.start_shake(@params[0], @params[1], @params[2]) wait(@params[1]) if @params[2] end
Same thing but for Screen Shake. There's actually a bug here because the second line should be "wait(@params[2]) if @params[3]"; they got the indexes wrong. The way it's written, it's waiting for [speed] frames if a duration is set (which is always true). This means that by default waiting for completion on a screen shake doesn't work.
def command_230 wait(@params[0]) end
This is the method for the Wait command, and simply waits for @params[0] frames.
def command_231 if @params[3] == 0 # Direct designation x = @params[4] y = @params[5] else # Designation with variables x = $game_variables[@params[4]] y = $game_variables[@params[5]] end screen.pictures[@params[0]].show(@params[1], @params[2], x, y, @params[6], @params[7], @params[8], @params[9]) end
This is the method for the Show Picture command. If @params[3] is 0, meaning direct coordinate designation, then x is set to @params[4] and y to @params[5] (obviously, these are the X and Y coordinates you entered in the dialog). Otherwise, the same variables are used as indexes for $game_variables instead. Regardless, we call the show method of screen.pictures[@params[0]], which is the picture number entered in the dialog. As parameters, we supply the name, origin, x, y, zoom x, zoom y, opacity and blend type, as defined by the various elements of @params.
def command_232 if @params[3] == 0 # Direct designation x = @params[4] y = @params[5] else # Designation with variables x = $game_variables[@params[4]] y = $game_variables[@params[5]] end screen.pictures[@params[0]].move(@params[2], x, y, @params[6], @params[7], @params[8], @params[9], @params[10]) wait(@params[10]) if @params[11] end
This is the method for the Move Picture command, which is almost identical to Show Picture, but calls the move method instead of show, and waits for @params[10] frames if the wait until completed box was checked. The main difference in the arguments is that we don't have a picture number, and we're adding an additional argument for duration. command_232 doesn't actually have a @params[1], presumably to keep the parameter indexes the same for variables common to showing and moving.
def command_233 screen.pictures[@params[0]].rotate(@params[1]) end
This is the method for Rotate Picture. Not much to say about this one, it simply calls the rotate method of the given picture with @params[1] (speed) as the argument.
def command_234 screen.pictures[@params[0]].start_tone_change(@params[1], @params[2]) wait(@params[2]) if @params[3] end
This is the method for Tint Picture. Again quite a simple one, calls start_tone_change on the chosen picture with @params[1] (the tone) and @params[2] (duration) as arguments, waiting for duration frames if the wait for completion box was checked.
def command_235 screen.pictures[@params[0]].erase end
This is the method for Erase Picture. Calls the erase method of the picture. We should be getting the idea by now.
def command_236 return if $game_party.in_battle screen.change_weather(@params[0], @params[1], @params[2]) wait(@params[2]) if @params[3] end
This is the method for Set Weather. Slightly more complex this time but not that bad: returns if the party is in battle, because weather effects can't happen in battles (though all you really have to do to enable weather in battles is comment out that line and add the appropriate creation/disposal/update of a Spriteset_Weather instance to Spriteset_Battle). Calls the change_weather method of screen with @params[0] (weather type), @params[1] (power) and @params[2] (duration) as arguments. As with everything else that has a wait until completion checkbox, will wait @params[2] frames if it's checked.
def command_241 @params[0].play end
This is the method for Play BGM, and simply calls the play method of @params[0], which is the BGM object created by the dialog.
def command_242 RPG::BGM.fade(@params[0] * 1000) end
This is the method for Fadeout BGM, and calls the fade method of the RPG::BGM class with the number of seconds * 1000 as an argument (because the parameter for fade is in milliseconds).
def command_243 $game_system.save_bgm end
This is the method for the Save BGM command, and simply calls the save_bgm method of $game_system.
def command_244 $game_system.replay_bgm end
This is the method for Resume BGM. Surprise surprise, same thing as before but with replay_bgm instead.
def command_245 @params[0].play end
Play BGS, pretty much identical to Play BGM.
def command_246 RPG::BGS.fade(@params[0] * 1000) end
Fadeout BGS, identical to Fadeout BGM. Noticing a pattern?
def command_249 @params[0].play end
Play ME.
def command_250 @params[0].play end
Play SE.
def command_251 RPG::SE.stop end
Stop SE! I'm honestly not sure why they didn't include a fadeout/stop ME command, but there's nothing stopping you from setting up commands you can call via script if you need them. RPG::ME does have fade and stop methods.
def command_261 Fiber.yield while $game_message.visible Fiber.yield name = @params[0] Graphics.play_movie('Movies/' + name) unless name.empty? end
This is the method for Play Movie. Yields the fiber to its caller while a game message is visible, then yields the fiber again (this gives any message box preceding the movie time to fully close before the movie plays; without this line it ends up still being partially visible), sets name to @params[0] (the filename of the movie you chose in the dialog) then calls the play_movie method of the Graphics class with the relative path to the movie as its argument, unless the name is an empty string (this prevents exceptions from trying to play a movie when the choice was "none").
def command_281 $game_map.name_display = (@params[0] == 0) end
This is the method for the Change Map Name Display command. Simply sets the name_display property of $game_map to true if you picked ON (0) and false if you picked OFF (1).
def command_282 $game_map.change_tileset(@params[0]) end
This is the method for the Change tileset command, and simply calls the change_tileset method of $game_map with @params[0] as the argument, which is the ID of the tileset to change to.
def command_283 $game_map.change_battleback(@params[0], @params[1]) end
This is the method for the Change Battle Background command, and calls $game_map's change_battleback method with @params[0] (the name of the first background) and @params[1] (the name of the second background) as arguments.
def command_284 $game_map.change_parallax(@params[0], @params[1], @params[2], @params[3], @params[4]) end
This is the method for the Change Parallax Background command, and calls $game_map's change_parallax method with @params[0] (background name), @params[1] (whether scroll horizontal is checked), @params[2] (horizontal speed), @params[3] (whether scroll vertical is checked) and @params[4] (vertical speed) as arguments.
def command_285 if @params[2] == 0 # Direct designation x = @params[3] y = @params[4] else # Designation with variables x = $game_variables[@params[3]] y = $game_variables[@params[4]] end case @params[1] when 0 # Terrain Tag value = $game_map.terrain_tag(x, y) when 1 # Event ID value = $game_map.event_id_xy(x, y) when 2..4 # Tile ID value = $game_map.tile_id(x, y, @params[1] - 2) else # Region ID value = $game_map.region_id(x, y) end $game_variables[@params[0]] = value end
This is the method for the Get Location Info command. If @params[2] is 0 (direct designation) then x is @params[3] (the X coordinate of the target tile) and y is @params[4] (the Y coordinate of the target tile). If it's 1 (designation with variables) those parameters are used as indexes to $game_variables instead. We then have a case statement for @params[1] (info type). When 0 (terrain tag), value is set to the terrain tag set at the given X/Y. When 1 (event ID), value is set to the ID of the event at the given X/Y. When 2 to 4 inclusive (tile ID layers 1, 2 and 3), value is set to the ID of the tile on the chosen layer (subtracting 2 from @params[1] because the tile IDs are 0-indexed). Otherwise (technically 5, region ID), value is set to the ID of the region the given tile is part of. Following the case statement, we set the variable with index @params[0] to value.
def command_301 return if $game_party.in_battle if @params[0] == 0 # Direct designation troop_id = @params[1] elsif @params[0] == 1 # Designation with variables troop_id = $game_variables[@params[1]] else # Map-designated troop troop_id = $game_player.make_encounter_troop_id end if $data_troops[troop_id] BattleManager.setup(troop_id, @params[2], @params[3]) BattleManager.event_proc = Proc.new {|n| @branch[@indent] = n } $game_player.make_encounter_count SceneManager.call(Scene_Battle) end Fiber.yield end
This is the method for the Battle Processing command. Returns if the party is in battle (do I really have to explain why?). If @params[0] is 0 (direct designation of troop), then troop_id is set to @params[1] (the ID of the troop you'll be fighting). If it's 1 (designation with variables), @params[1] is used as an index for $game_variables instead. Otherwise, it's a map-designated troop so we call the make_encounter_troop_id method of $game_player to figure out what the encounter is. If there's a troop corresponding to the calculated ID, we call the setup method of BattleManager, passing in the troop ID, @params[2] (whether the player can escape) and @params[3] (whether the player can lose) as arguments.
Then we set the event_proc property of BattleManager to a new Proc where @branch[@indent] is set to the passed-in value n. This allows the battle end method of BattleManager to set @branch[@indent] to 0, 1 or 2 depending on whether the player won, escaped or lost, via BattleManager.event_proc.call, which you may not remember us looking at way back in Under the Hood part 2. After that we call the make_encounter_count method of $game_player, which refreshes the encounter counter on the map (because it would kind of suck to end up in a random battle immediately after fighting a boss) and then the call method of SceneManager is called, with Scene_Battle as its argument. This obviously transitions the scene to the battle scene. Finally, we yield the fiber to its caller, which prevents the rest of the event commands from processing before the battle is finished.
def command_601 command_skip if @branch[@indent] != 0 end
601 is the code for the "win" branch of a battle. Skips the command if @branch[@indent] isn't 0, because that means the player didn't win.
def command_602 command_skip if @branch[@indent] != 1 end
Same as before, but for 602 (escape).
def command_603 command_skip if @branch[@indent] != 2 end
Same as before, but for 603 (lose).
def command_302 return if $game_party.in_battle goods = [@params] while next_event_code == 605 @index += 1 goods.push(@list[@index].parameters) end SceneManager.call(Scene_Shop) SceneManager.scene.prepare(goods, @params[4]) Fiber.yield end
This is the method for the Shop Processing command. Returns if the party is in battle (though who wouldn't want to buy some potions during a boss fight?) and sets goods to @params as an array element.
Initially, @params is just an array consisting of the item type (0 = item, 1 = weapon, 2 = armour), item ID, standard price flag (0 = standard, 1 = specific), 0 if standard or the price if specific, and a true/false value for whether the shop is purchase-only. We then have a while loop as long as the next event code is 605 (additional shop goods), in which the index is incremented and we push the current event command's parameters to goods (those parameters will be an array with the same elements as the initial one, with the exception of the purchase-only flag). Then we call the call method of SceneManager, with Scene_Shop as the argument, and call the prepare method of SceneManager's scene property (which is now an instance of Scene_Shop), with goods and @params (the purchase-only flag) as arguments. This just initialises the instance variables of Scene_Shop, as we'll see when we eventually get to it. Then, as before, the fiber is yielded to its caller so that further event commands aren't processed before we're done shopping.
def command_303 return if $game_party.in_battle if $data_actors[@params[0]] SceneManager.call(Scene_Name) SceneManager.scene.prepare(@params[0], @params[1]) Fiber.yield end end
This is the method for the Name Input Processing command. Returns if the party is in battle (seriously, Enterbrain, you're missing a trick here. If Alex does terribly in battle I want to rename him to something mean). If an actor exists with the ID @params[0], we call Scene_Name, and the prepare method of the scene with @params[0] (the ID of the actor) and @params[1] (the maximum number of characters) as arguments. Then, we yield the fiber to prevent processing of further commands.
def command_311 value = operate_value(@params[2], @params[3], @params[4]) iterate_actor_var(@params[0], @params[1]) do |actor| next if actor.dead? actor.change_hp(value, @params[5]) actor.perform_collapse_effect if actor.dead? end SceneManager.goto(Scene_Gameover) if $game_party.all_dead? end
This is the method for the Change HP command. Sets value to the result of calling operate_value with the options chosen in the dialog (I shouldn't have to explain the inner workings of this by now, we've gone over it so many times). We call iterate_actor_var passing in @params[0] (0 for fixed actor, 1 for variable) and @params[1] (actor or variable ID) and use the method as an iterator for a loop, using actor as the iteration variable. We go to the next iteration if the current actor is dead; if not, we call change_hp using value and @params[5] (whether the actor can die) as arguments. If the actor is now dead, we call perform_collapse_effect (only relevant in battle). Once the loop is done, we go to Scene_Gameover if the entire party has died. (You may not remember from when we looked at the modules, but goto is similar to call but doesn't use a stack, because if we're going to the game over screen there are no further scenes to transition to anyway).
As I promised we'd look into iterate_actor_var more when we got to it, let's look at a practical example. I've chosen to decrease Eric's HP by the value of variable 50 (let's say it contains 25), not allowing him to die.
This makes the first line: value = operate_value(1, 1, 50). In operate_value, value becomes the value of variable 50, which is 25, and because it's a decrease this is converted to -25.
We then call: iterate_actor_var(0, 1) and as param1 is 0, we call iterate_actor_id(1) with the block {|actor| yield actor}. In iterate_actor_id, param is 1 so actor is set to $game_actors, and because actor exists the "yield actor" line is processed. This passes the actor object to the block from iterate_actor_var, which then yields the actor to command_311, and the object is stored in the "actor" iteration variable. Alex isn't dead, so we skip the next line. We call actor.change_hp(-25, 0), which sets Alex's self.hp to += -25, so his HP ends up going down by 25. He's not dead, so we don't perform the collapse effect, and the party isn't dead so we don't have to switch to the game over scene.
def command_312 value = operate_value(@params[2], @params[3], @params[4]) iterate_actor_var(@params[0], @params[1]) do |actor| actor.mp += value end end
This is the method for the Change MP command. Sets value using operate_value as usual, then iterates through the actors chosen in the same way as Change HP does. Then actor.mp is increased by value (which obviously results in a decrease if value is a negative number).
def command_313 iterate_actor_var(@params[0], @params[1]) do |actor| already_dead = actor.dead? if @params[2] == 0 actor.add_state(@params[3]) else actor.remove_state(@params[3]) end actor.perform_collapse_effect if actor.dead? && !already_dead end $game_party.clear_results end
This is the method for the Change State command. Iterates through the actors chosen in the same way as usual. Sets already_dead to true if the actor is dead and false otherwise. If @params[2] is 0 (add state), we call the actor's add_state method with @params[3] (the ID of the state chosen) and if it's 1 (remove state) we call remove_state instead with the same argument. After this, we call perform_collapse_effect if the actor is now dead and wasn't already before the state was added/removed, and after the iteration loop we call clear_results on $game_party (which clears each party member's result property). I'm honestly not quite sure why the call to clear_results is there, as adding a state like this doesn't show in the battle log anyway.
def command_314 iterate_actor_var(@params[0], @params[1]) do |actor| actor.recover_all end end
This is the method for Recover All. Iterates through the actors chosen using iterate_actor_var and then calls recover_all for each actor.
def command_315 value = operate_value(@params[2], @params[3], @params[4]) iterate_actor_var(@params[0], @params[1]) do |actor| actor.change_exp(actor.exp + value, @params[5]) end end
This is the method for Change EXP. Pretty much the same as the other Change commands, but calling change_exp instead. @params[5] is the value of the "show level up message" checkbox.
def command_316 value = operate_value(@params[2], @params[3], @params[4]) iterate_actor_var(@params[0], @params[1]) do |actor| actor.change_level(actor.level + value, @params[5]) end end
Same as before, but for Change Level.
def command_317 value = operate_value(@params[3], @params[4], @params[5]) iterate_actor_var(@params[0], @params[1]) do |actor| actor.add_param(@params[2], value) end end
Same as before but for Change Parameters. @params[2] contains the ID of the parameter to be changed.
def command_318 iterate_actor_var(@params[0], @params[1]) do |actor| if @params[2] == 0 actor.learn_skill(@params[3]) else actor.forget_skill(@params[3]) end end end
This is the method for Change Skills. Iterates through the chosen actors; if @params[2] is 0 (learn skill) we call the actor's learn_skill method. If it's 1 (forget skill) we call the actor's forget_skill method. In both cases, @params[3] is used as the argument (the ID of the skill to learn/forget).
def command_319 actor = $game_actors[@params[0]] actor.change_equip_by_id(@params[1], @params[2]) if actor end
This is the method for Change Equipment. Gets the actor with ID @params[0] and calls their change_equip_by_id method, passing in @params[1] (slot ID) and @params[2] (item ID) if the actor exists.
def command_320 actor = $game_actors[@params[0]] actor.name = @params[1] if actor end
This is the method for Change Name. Gets the actor with ID @params[0] and calls their name method, passing in @params[1] (new name) if the actor exists.
def command_321 actor = $game_actors[@params[0]] actor.change_class(@params[1]) if actor && $data_classes[@params[1]] end
Same as before but for Change Class. @params[1] is the ID of the chosen class, and there's an additional validation check to make sure that class actually exists in $data_classes.
def command_322 actor = $game_actors[@params[0]] if actor actor.set_graphic(@params[1], @params[2], @params[3], @params[4]) end $game_player.refresh end
This is the method for Change Actor Graphic. I'm not sure why the if statement was done as a block here instead of having it on one line like the others, but whatever. After calling set_graphic on the actor with @params[1] (character name), @params[2] (character index), @params[3] (face name) and @params[4] (face index) as arguments, we call the refresh method of $game_player (this refreshes the character name/index and followers).
def command_323 vehicle = $game_map.vehicles[@params[0]] vehicle.set_graphic(@params[1], @params[2]) if vehicle end
This is the method for Change Vehicle Graphic. @params[0] contains the ID of the vehicle chosen in the dialog. We call the set_graphic method of the vehicle with @params[1] (character name) and @params[2] (character index) if the vehicle exists.
def command_324 actor = $game_actors[@params[0]] actor.nickname = @params[1] if actor end
This is the method for Change Nickname. Gets the actor with ID @params[0] and calls their nickname method, passing in @params[1] (new nickname) if the actor exists.
def command_331 value = operate_value(@params[1], @params[2], @params[3]) iterate_enemy_index(@params[0]) do |enemy| return if enemy.dead? enemy.change_hp(value, @params[4]) enemy.perform_collapse_effect if enemy.dead? end end
This is the method for Change Enemy HP. It's pretty much identical to Change HP, only it uses iterate_enemy_index and you can't use a variable to determine which enemy is affected.
def command_332 value = operate_value(@params[1], @params[2], @params[3]) iterate_enemy_index(@params[0]) do |enemy| enemy.mp += value end end
This is the method for Change Enemy MP. Pretty much the same as Change MP. Again, it uses iterate_enemy_index and you can't use a variable for the enemy ID.
def command_333 iterate_enemy_index(@params[0]) do |enemy| already_dead = enemy.dead? if @params[1] == 0 enemy.add_state(@params[2]) else enemy.remove_state(@params[2]) end enemy.perform_collapse_effect if enemy.dead? && !already_dead end end
This is the method for Change Enemy State. Again, pretty much the same as Change State with the aforementioned changes.
def command_334 iterate_enemy_index(@params[0]) do |enemy| enemy.recover_all end end
This is the method for Enemy Recover All. Second verse, same as the first.
def command_335 iterate_enemy_index(@params[0]) do |enemy| enemy.appear $game_troop.make_unique_names end end
This is the method for Enemy Appear. Calls iterate_enemy_index as usual and calls the chosen enemy's appear method, which just sets its hidden flag to false. We then call the make_unique_names method of $game_troop in case there are already living enemies of the same type in the battle.
def command_336 iterate_enemy_index(@params[0]) do |enemy| enemy.transform(@params[1]) $game_troop.make_unique_names end end
This is the method for Enemy Transform. Calls iterate_enemy_index as before and calls the chosen enemy's transform method with @params[1] (The ID of the enemy to transform into) as the argument. Then, as before, we call make_unique_names in case there are already living enemies of the same type.
def command_337 iterate_enemy_index(@params[0]) do |enemy| enemy.animation_id = @params[1] if enemy.alive? end end
This is the method for Show Battle Animation. Calls iterate_enemy_index and then sets the chosen enemy's animation_id to @params[1] (the ID of the animation to show) if the enemy is alive.
def command_339 iterate_battler(@params[0], @params[1]) do |battler| next if battler.death_state? battler.force_action(@params[2], @params[3]) BattleManager.force_action(battler) Fiber.yield while BattleManager.action_forced? end end
This is the method for Force Action. Calls iterate_battler (because we don't know whether we're forcing an action for an actor or an enemy) with arguments @params[0] (0 for enemy, 1 for actor) and @params[1] (ID of the actor or enemy) using the iteration variable battler. We go to the next iteration if the battler has the death state, because you can't force dead people to do anything. Next, we call the force_action method of the battler, passing in @params[2] (ID of the skill to force) and @params[3] (the index of the target). Then we call the force_action method of BattleManager passing in the battler, which removes the battler from the main action order and flags it as having its action forced.
Finally, we yield the fiber to its caller while an action is being forced, so that no other battle event commands will be processed until the action is done.
def command_340 BattleManager.abort Fiber.yield end
This is the method for Abort Battle. Calls the abort method of BattleManager, which sets the phase to :aborting, as we already looked at in an earlier episode. Finally, yields the fiber to its caller to stop any remaining event commands from executing.
def command_351 return if $game_party.in_battle SceneManager.call(Scene_Menu) Window_MenuCommand::init_command_position Fiber.yield end
This is the method for Open Menu Screen. Returns if the party is in battle, then calls SceneManager's call method passing in Scene_Menu. Directly calls the init_command_position of the Window_MenuCommand class (which sets the class variable @@last_command_symbol to nil) then yields the fiber to its caller so that any further event commands won't process.
def command_352 return if $game_party.in_battle SceneManager.call(Scene_Save) Fiber.yield end
This is the method for Open Save Screen. Pretty much identical to the menu one above.
def command_353 SceneManager.goto(Scene_Gameover) Fiber.yield end
This is the method for Game Over. As mentioned previously, we use goto here instead of call because logic dictates that no scenes are going to be called after game over.
def command_354 SceneManager.goto(Scene_Title) Fiber.yield end
This is the method for Return to Title Screen. Nothing more to say.
def command_355 script = @list[@index].parameters[0] + "\n" while next_event_code == 655 @index += 1 script += @list[@index].parameters[0] + "\n" end eval(script) end
And this is the method for Script. Sets script to the event command's parameters[0] (which is the first line of the script) plus a newline character. While the next event code is 655 (further script lines), we increment index by 1 and add the new command's parameters[0] to the script. Finally, we evaluate the full script and return the result.
And...that's it! We're finally done! That is the whole interpreter! This took me like three days! I'm overusing exclamation marks and I don't care!
Now that we're done with the behemoth that is the game objects, we're moving on to sprites. This shouldn't take too long as for the most part these scripts are pretty small. After that, we'll cover windows, then scenes. I promise the next part won't take as long as this one did, either. :)
As usual, if you have any comments, queries, criticisms, corrections or death threats, you know where to write it. I'm expecting a fair few clarification questions as we've covered a couple of pretty advanced topics, especially Fiber and Proc.
Until next time.
Posts
Pages:
1
Hi Trihan, I'm here to say you're saving my life with those tutorials!
Before them, I could never understand well the way RMVXA worked with scripts.
Please, keep it coming, I'm studying them hard.
Before them, I could never understand well the way RMVXA worked with scripts.
Please, keep it coming, I'm studying them hard.
Glad to hear it Shadow_Fox! It's for people like you that I'm doing this, so that means a lot. I'm hard at work on the next one!
Question is, is an article of this length digestible enough, or would it be better to keep breaking it up the way I have been? I could probably cover the entire set of Sprite scripts in an article of a similar size to this one, but I'm concerned that too much text might make it more difficult to get to grips with.
Question is, is an article of this length digestible enough, or would it be better to keep breaking it up the way I have been? I could probably cover the entire set of Sprite scripts in an article of a similar size to this one, but I'm concerned that too much text might make it more difficult to get to grips with.
author=Trihan
Question is, is an article of this length digestible enough, or would it be better to keep breaking it up the way I have been? I could probably cover the entire set of Sprite scripts in an article of a similar size to this one, but I'm concerned that too much text might make it more difficult to get to grips with.
For me, you can keep with this size.
author=Trihan
Question is, is an article of this length digestible enough, or would it be better to keep breaking it up the way I have been? I could probably cover the entire set of Sprite scripts in an article of a similar size to this one, but I'm concerned that too much text might make it more difficult to get to grips with.
It's probably best if you split them up by parts. Something like one article for character sprites, another for battle sprites, and that sort of thing. A lot of people are put off by large walls of text, regardless of how useful the content may be.
Plus, you'd get more ms XP
Pages:
1