Let’s start our little journey into scripting by getting a few orders of business out of the way quickly. First, I don’t intend to spend too much time talking about the details and syntax of the Ruby language, or about programming theory, and concepts in object-oriented programming. These things are all part of scripting for RGSS, of course. However, they are very deep topics that would take dozens of tutorials to cover adequately, and are already well documented elsewhere, so there’s no sense in duplicating the effort here.
Second, before delving into scripting, be sure to have a strong knowledge of using the maker on it’s own, in particular, the details of the database, and a strong understanding of writing events. Although not identical, complicated events that utilize conditional branching, switches, and variable handling, have a lot in common with writing scripts. A good event writer will likely have an easy time starting to learn scripting.
Lastly, realize that when one first starts modifying scripts, it’s unlikely they are going to write a new custom battle system within their first few days. On the contrary, the longest and most complicated scripts come from those who have substantial experience in the various versions of RGSS, or a programming background in other areas. With all of that said and out of the way, we can’t learn to swim without getting into the water, so let’s start taking a look at the existing scripts, and then we’ll learn how to implement a simple change to how the default engine works, as an example.
Almost all of the scripts used are divided into classes, which themselves typically have several methods. It may help to think of a class as a definition of an object that includes all of the information about that object, as well as all of the different functions that can be performed on that object. A common example (drawing an analogy to a real-world object we all know and understand) is a light switch. A light switch contains data for its current position–on or off–and a method, which we’ll call “flip”. A house may contain many objects which are themselves instances of the light switch class, but they all have behavior that can be outlined in the same single definition. In RGSS, enemies, maps, events, items, etc, follow a similar model. Each may have different properties, but we can outline the behavioral elements with a single definition for each class.
A typical piece of scripting code will look something like this (using our light switch example):
class Lightswitch attr_accessor :status def initialize @status = "off" end def flip if @status == "off" @status = "on" else @status = "off" end end end
The class declaration wraps all of the properties and methods for our class, and each def block defines a method (in this case, only one). Notice how each name that is used anywhere in our code is somewhat self-explanatory in what it represents or what it does. Though not strictly necessary, it is generally good practice to do this, as it will make your life easier when you go to edit your scripts later, or even more so, if someone else has to edit your scripts. This same good practice is typically the case with the default scripts, as well. As such, reading through them is often enough to at least get an idea of what is happening in each part of the code. The naming of the classes is also done in such a way that they can be easily grouped together. Each of the “Game_” classes defines some object or aspect of the game. The “Sprite_” classes define the behavior of specific visual elements on screen. “Window_” classes are a specific type of visual elements that look like independent sections of the screen. And, “Scene_” classes can be described as different portions of the game play.
It’s worth calling attention to one specific class, Game_Interpreter, as it is a bit unique. The sole function of the class is to process a list of event commands. As it turns out, there are several comments in the code itself, that make it very easy to follow. The bulk of the methods in this class are named commandXXX, with (more or less) one method for each type of event command. Reading over the code here is a great way to see how event commands actually translate to the data and programming behind the scenes.
So, let’s look at an actual example. In fact, this is the very first scripting change I ever made on my own. Coming from a background in the Dragon Quest series, I was frustrated that there were no battles while riding on a boat or ship. So, how do we change the script so that we get battles while in the boat? First, we need to find the right section of code. As one becomes more familiar with the code base, this gets easier. There are a couple of approaches to this, at first. We can either look at classes that sound promising, or we can try searching for keywords in the code, such as “battle”, “encounter”, etc.
We might start by looking in Scene_Battle, since that’s what controls battles. However, it turns out we won’t find anything there that helps us. Scene_Battle only controls what happens in the battle itself. What we need is the section of code that decides when to initiate a battle. So, let’s ask ourselves where that might be; in what scene would we find ourselves entering into a battle? Ah ha! While we’re on the map! So, we can guess that we need to look at either Game_Map or Scene_Map. If we look in these two classes, and search for the word “encounter”, it’s easy enough to find the section that determines when to start a new battle. The method is copied here, so we can examine it:
def update_encounter return if $game_player.encounter_count > 0 # Check steps return if $game_map.interpreter.running? # Event being executed? return if $game_system.encounter_disabled # Encounters forbidden? troop_id = $game_player.make_encounter_troop_id # Determine troop return if $data_troops[troop_id] == nil # Troop is invalid? $game_troop.setup(troop_id) $game_troop.can_escape = true $game_temp.battle_proc = nil $game_temp.next_scene = "battle" preemptive_or_surprise end
Now, it’s helpful to know that, in Ruby syntax, the pound sign (#) signals a comment; everything after the # in a line is not part of the script code, but rather is just a note, often made to help identify what a particular line is doing. That said, the first five lines of this method make some determinations about whether or not now is the right time to start a new battle. The comments help us to determine which line(s) might be relevant to our end goal. Starting from the top, we’ll now investigate each line, until we find what we want. The first line deal with the “encounter_count” in the “game_player” class. Looking at the game_player class, there are several references to encounter count.
def make_encounter_count if $game_map.map_id != 0 n = $game_map.encounter_step @encounter_count = rand(n) + rand(n) + 1 # As if rolling 2 dice end end
This first example shown above dictates how the scripts determine when you will have your next encounter. $game_map.encounter_step should be familiar from the map editor… the default value for a new map is 30. The line that’s commented “as if rolling 2 dice” literally tells us what it’s doing. It’s picking two random numbers (from 0..29) and adding 1. Remember how in the Scene_Map class, it will not trigger an enemy encounter while the count is greater than 0? So, essentially, our next encounter with an enemy will be anywhere from 1..59 steps from now. Interesting. However, there is nothing here that deals with the issue of encounters while in the boat. So, let’s find the next method that deals with the encounter_count:
def make_encounter_troop_id encounter_list = $game_map.encounter_list.clone for area in $data_areas.values encounter_list += area.encounter_list if in_area?(area) end if encounter_list.empty? make_encounter_count return 0 end return encounter_list[rand(encounter_list.size)] end
As the name somewhat implies, this is the method that is called to determine, once it’s time to go into battle, which troop id the player will face. Notice, however, that there is nothing here, either, that deals with vehicles at all. So, continuing along:
def update_encounter return if $TEST and Input.press?(Input::CTRL) # During test play? return if in_vehicle? # Riding in vehicle? if $game_map.bush?(@x, @y) # If in bush @encounter_count -= 2 # Reduce encounters by 2 else # If not in bush @encounter_count -= 1 # Reduce encounters by 1 end end
The “update_encounter” method appears to be called each time the player moves, and it decreases the “encounter_count” variable. Notice that it checks if the current location is “bush?”, and if so, decreases the count by 2 instead of 1. Essentially, this is what makes encounters more likely when in the deep brush. If we were so inclined, we could change that 2 to a different value to either remove or amplify that affect. Notice also the very first line of this method that checks if we are in testplay mode, and currently holding down the Control key. If both of those things are true, then it returns immediately before updating the count, and thus we are guaranteed to not hit an enemy encounter.
But, for our original goal of getting enemies to attack while we’re in the boat, we need to look at the second line. Exactly as the comment implies, and the script code confirms, this line checks if we are riding in a vehicle, and if so, bails out of the method early, without updating the count. Pay dirt! This is what needs to be updated! If we look at the “in_vehicle?” method, we can get a little more clarity on what’s happening:
def in_vehicle? return @vehicle_type >= 0 end
This is fairly simple to understand. There is a variable within the Game_Player class called “vehicle_type”, that determines what type of vehicle we are in currently. The obvious question being, what values indicate which type of vehicle? If we actually search for “vehicle_type” in this same class, we’ll soon find the following method:
def map_passable?(x, y) case @vehicle_type when 0 # Boat return $game_map.boat_passable?(x, y) when 1 # Ship return $game_map.ship_passable?(x, y) when 2 # Airship return true else # Walking return $game_map.passable?(x, y) end end
Just peeking at the small comments here makes it clear that 0 = boat, 1 = ship, and 2 = airship. Now we know enough to make some changes! We already identified the crucial line: “return if in_vehicle?” and determined that the method will bail out if @vehicle_type is 0, 1, or 2. There are many ways we could change this line, to get slightly different effects:
return if @vehicle_type >= 1 # Encounters in boat, not ship or airship return if @vehicle_type == 2 # Encounters in boat/ship, but not airship return unless @vehicle_type == 1 # Encounters in boat, never otherwise # (Not even when walking!)
We could also remove the line entirely, and find ourselves with encounters regardless of vehicle.
Of course, this was a simple example, but hopefully provides at least a little guidance on how to start knowing where to look to make little changes. Such small changes are important, because as one becomes more familiar with the codebase, it will be easier to know where and how to make larger adjustments. Next time, we will expand upon this with a slightly larger change, and will introduce the concept of method redefinition and aliasing, as cleaner approaches to updating the scripts, without having to overwrite the default scripts.
Tim is a software engineer and member of rpgmakervxace.net