SLIP INTO RUBY - UNDER THE HOOD PART 4: GAME OBJECTS CONT.

In which I need to think of less verbose article titles.

  • Trihan
  • 04/09/2015 12:56 AM
  • 9021 views
Hello sports fans! You're probably off watching football and drinking beer, so I guess I'll address the programming nerds instead. Yep, that's right, it's time for another



Today we're going to cover some more Game Objects classes!

Game_Message
This class handles the state of the window that appears whenever you talk to an NPC, get some sweet loot or encounter a nasty enemy. Namely, the message window. The class is referenced by the global variable $game_message. There are a lot of public instance variables and they're all pretty self-explanatory so I won't bother reciting them.

The initialize and clear variables are pretty standard fare by now; they simply clear all the settings and make the window invisible. One interesting thing to note is that the default scroll speed for messages is 2.

def add(text)
    @texts.push(text)
  end


This method adds the supplied text as a new element to the @texts array. So I could do, for example, $game_message.add("kentona is awesome"). However, as @texts is an attr_reader, doing $game_message.texts.push("kentona is awesome") would result in an error, and the world would never know how awesome kentona is.

def has_text?
    @texts.size > 0
  end


Really simple method to check whether there's text to show in the window: if the array has anything in it, return true. If it's empty, return false.

The choice?, num_input? and item_choice? methods follow the exact same logic for choices, a variable for number input and a variable for an item choice respectively.

def busy?
    has_text? || choice? || num_input? || item_choice?
  end


Again, a pretty simple method. If the message window has text OR if there are choices for the player OR the player has to input a number OR if the player has to choose an item, return true (in other words, the message window is considered to be busy). Otherwise, return false.

def new_page
    @texts[-1] += "\f" if @texts.size > 0
  end


Ruby supports negative indexing unlike a lot of other languages, so if you want to reference the last element of an array you can just use -1 as the index. What this does is append "\f" (the control character Window_Base uses to determine whether it needs to start a new line) to the end of the most recently-added element in the array, if the array has stuff in it. The if is necessary because should the array be empty @texts won't exist and trying to add to it will give you an error.

def all_text
    @texts.inject("") {|r, text| r += text + "\n" }
  end


.inject is an interesting method. Essentially what it does is combines all the elements of an Enumerable object (which an array is) using the block to determine how to do that. Looking at the Ruby documentation this particular example follows the structure inject(initial) { |memo, obj| block } → obj. initial, in this case being "", means that we're starting with a blank string. memo, in this case r, is a variable which contains the combination of all elements so far. obj, in this case text, is the element being considered in the current iteration. The block in this case adds text to r and then appends it with "\n". To put it in layman's terms: "start with a blank string, loop through each line of text, and add it to the string with a newline character attached".

And that's it for Game_Message!

Game_Switches
This class, as the name suggests, handles variables. Kidding, it handles switches. It's referenced by the global variable $game_switches.

Initialization is pretty much the simplest thing around by this point: it sets an instance variable called @data to an empty array.

def [](switch_id)
    @data[switch_id] || false
  end


This is the getter method for switches, allowing you to simply use $game_switches[id] in a script to return whether a switch is on or off. It returns the value of the switch_idth element of @data (which is either going to be true for ON or false for OFF) or false if there's no data set for that index. The logic is that switches will be off if they haven't been turned on.

def []=(switch_id, value)
    @data[switch_id] = value
    on_change
  end


This is the setter method for switches, allowing you to use $game_switches[id] = true/false. It sets the switch_idth element of @data to the supplied value, then calls the on_change method.

def on_change
    $game_map.need_refresh = true
  end


All on_change does is sets the need_refresh property of $game_map to true, which tells the map it needs to refresh and make sure nothing has visually changed as a result of the switch being turned on/off. If we didn't have this, graphical changes caused by switch manipulation wouldn't be shown immediately.

Game_Variables
Almost identical to Game_Switches; obviously this one handles variables and is referenced by $game_variables, but everything else is pretty much the same. The only difference is that the default value if there's no variable_idth element in @data is 0 instead of false, since variables hold integer values.

Game_SelfSwitches
Again similar to the other two, referenced by $game_self_switches. Rather than being an array, self switches are closer to what's known as a hashmap, which is a collection of key-pair values. To give a layman's example, if I have a "trihan" hashmap, it could contain a "ruby_proficiency" key, with a value of "awesome". Then, if I ever queried trihan, the output would be "awesome". In the case of self switches, a quick glance at Game_Interpreter (don't worry, we'll get there) points out that the key is itself an array and consists of a map ID, an event ID, and the letter of the self switch. Everything else works the same way as for switches and variables.

Because we know what keys are being looked for, we can in fact use script calls in events to remotely manipulate self switches in other events, even if they're on a different map. For example, if I wanted to turn off self switch C for the event with ID 27 on map 5, I could just do $game_self_switches[[27, 5, "C"]] = false

Game_Screen
This class handles pretty much any screen effects, such as weather, screen tone changes, flashing and fadein/fadeout. As is par for the course by now, the public instance variables should be self-explanatory.

Initialising Game_Screen simply consists of setting @pictures to a new instance of Game_Pictures, and calling the clear method, which itself calls a number of individual clear methods that set all of their relevant instance variables to their default values, or in the case of pictures erases all the pictures currently in the array. Note that erasing a picture doesn't actually remove it from the list, as we'll see soon.

We then have starting methods for each type of screen effect, which set a duration to the supplied value and in some cases take other parameters like a target tone, flash color or power/speed. In the case of fadeout and fadein, they also set the opposite effect's duration to 0, since you obviously can't be fading in and out at the same time. start_tone_change and start_flash are interesting ones as they are the first time we've come across .clone. This method basically creates a "shallow copy" of an object, which allows you to manipulate the cloned object without affecting the original. This is necessary because both of these methods need to retain the original tone and color so they can either tween the tone smoothly or return the flashing object to its original colour.

def change_weather(type, power, duration)
    @weather_type = type if type != :none || duration == 0
    @weather_power_target = type == :none ? 0.0 : power.to_f
    @weather_duration = duration
    @weather_power = @weather_power_target if duration == 0
  end


Here's where things get a bit more interesting. Okay, so @weather_type is set to the supplied type if it's not equal to the symbol :none OR the duration is 0. The target weather power is set via a ternary operator: if type is equal to the symbol :none, it's 0.0, otherwise it's the supplied power value converted to a float. Duration is set to the supplied value, and finally if the duration was 0 the weather power is set to the weather power target (a duration of 0 means the transition is instant).

Following this is the update method, which simply calls a number of update methods for each type of effect.

def update_fadeout
    if @fadeout_duration > 0
      d = @fadeout_duration
      @brightness = (@brightness * (d - 1)) / d
      @fadeout_duration -= 1
    end
  end


Okay, so if the fadeout duration is greater than 0, we set a variable called d to the duration value (note that this value is in FRAMES). Then @brightness is set to the result of the equation, and the duration is decreased by 1.

The equation is pretty simple: it's a way of gradually tweening the brightness of the screen according to the duration. Let's say we're just starting a fadeout, so brightness is 255, and we've asked for it to take 60 frames (1 second):

@brightness = (255 * (60 - 1)) / 60
@brightness = (255 * (59)) / 60
@brightness = (15045) / 60
@brightness = 250.75

And so on until duration is 1, at which point brightness will become 0 on the next frame.

update_fadein is almost identical, but the equation is tweening the brightness towards 255 instead of 0. (hence the + 255)

def update_tone
    if @tone_duration > 0
      d = @tone_duration
      @tone.red = (@tone.red * (d - 1) + @tone_target.red) / d
      @tone.green = (@tone.green * (d - 1) + @tone_target.green) / d
      @tone.blue = (@tone.blue * (d - 1) + @tone_target.blue) / d
      @tone.gray = (@tone.gray * (d - 1) + @tone_target.gray) / d
      @tone_duration -= 1
    end


Updating tone is also similar, but it applies the equation result to the red, green, blue and gray components of the tone respectively. Note that this is why we cloned the values earlier: if we hadn't done that both @tone and @tone_target would point to the same object, and changing one would also change the other, meaning after the first frame the target tone would end up being whatever the tone was.

To give an example of how this works, let's say we're trying to change the tone from (0, 0, 0, 0) or "Normal", to (34, -34, -68, 170), or "Sepia", and again our duration is 1 second, or 60 frames:

@tone.red = (0 * (60 - 1) + 34) / 60
@tone.green = (0 * (60 - 1) + -34) / 60
@tone.blue = (0 * (60 - 1) + -68) / 60
@tone.gray = (0 * (60 - 1) + 170) / 60


Which on the first iteration will set the tone to (0.56, -0.56, -1.13, 2.83), and so on and so forth gradually tweening the tone until it gets to a duration of 1 frame, where the final iteration will have set the tone to what the target was.

def update_flash
    if @flash_duration > 0
      d = @flash_duration
      @flash_color.alpha = @flash_color.alpha * (d - 1) / d
      @flash_duration -= 1
    end
  end


For updating a flash, it uses a similar equation to tween the alpha of the flash colour. Note that the actual processing of the colours is done in other classes, this just lays the groundwork so to speak.

def update_shake
    if @shake_duration > 0 || @shake != 0
      delta = (@shake_power * @shake_speed * @shake_direction) / 10.0
      if @shake_duration <= 1 && @shake * (@shake + delta) < 0
        @shake = 0
      else
        @shake += delta
      end
      @shake_direction = -1 if @shake > @shake_power * 2
      @shake_direction = 1 if @shake < - @shake_power * 2
      @shake_duration -= 1
    end
  end


Ooh, updating a shake is a little more exciting!

If the shake duration is greater than 0 OR shake positioning is not equal to 0...
delta is set to (power * speed * direction) / 10.0 (given a power of 8 and a speed of 6 and a direction of 1 since that's the default, our delta for the first iteration will be 4.8).
if the duration is less than or equal to 1 AND the shake positioning multiplied by (positioning * delta) is less than 0... (which it won't be for the first iteration; shake gets initialised as 0 and 0 * (0 * 4.8) is 0)
positioning = 0
otherwise...
add delta to positioning. (positioning will now be 4.8)
The direction of shake is set to -1 if the shake positioning is greater than twice the power. (in this case, it's not)
The direction of shake is set to 1 if the shake positioning is less than negative twice the power. (again in this case, it's not)
Finally, duration is reduced by 1 frame.

On the next iteration the equation becomes 4.8 * (4.8 * 4.8) which is 110.59, which is not less than 0, so we again add 4.8 to positioning and it becomes 9.6. This is not greater than twice the power nor less than negative twice the power. This will continue until the result gets to 19.2 (which takes 4 frames), at which point direction will be set to -1. The delta then becomes -4.8 and we'll shake the other way.

def update_weather
    if @weather_duration > 0
      d = @weather_duration
      @weather_power = (@weather_power * (d - 1) + @weather_power_target) / d
      @weather_duration -= 1
      if @weather_duration == 0 && @weather_power_target == 0
        @weather_type = :none
      end
    end
  end


If the weather duration is greater than 0, we set d to the duration. Power is calculated using the by-now-familiar tweening method towards the target and duration reduced by 1 frame. Then, if the duration is 0 AND the target power is 0, @weather_type is set to :none.

The first iteration of the equation, given an initial @weather_power of 0, a target power of 6 and a duration of 180 frames, will be:

@weather_power = (0 * (180 - 1) + 6) / 180, which is 0.033...

The next iteration will be about 0.066, and so on until the weather power is equal to the target power.

def update_pictures
    @pictures.each {|picture| picture.update }
  end


Simple enough for updating pictures; for each picture in the array, call that picture's update method.

def start_flash_for_damage
    start_flash(Color.new(255,0,0,128), 8)
  end


This method is for flashing damage from damaging tiles, and calls start_flash with a target colour of (255, 0, 0, 128) (half-transparent red) and a duration of 8 frames.

Game_Picture
Needless to say this class handles pictures. As the comments say, it is required internally for the Game_Pictures class only when a picture of a specific number is required.

The initialization is pretty straightforward: it sets the picture index to the supplied number and calls methods for initialising the basic data, target data, tone and rotation.

def init_basic
    @name = ""
    @origin = @x = @y = 0
    @zoom_x = @zoom_y = 100.0
    @opacity = 255.0
    @blend_type = 1
  end


To begin with, @name is set to a blank string, @origin is set to @x = @y = 0 (what this does is sets all three values to 0 at once), @zoom_x and @zoom_y are both set to 100.0, @opacity is set to 255.0, and @blend_type is set to 1.

def init_target
    @target_x = @x
    @target_y = @y
    @target_zoom_x = @zoom_x
    @target_zoom_y = @zoom_y
    @target_opacity = @opacity
    @duration = 0
  end


This is a similar method for initialising the movement targets.

def init_tone
    @tone = Tone.new
    @tone_target = Tone.new
    @tone_duration = 0
  end


Similar method for initialising color tone.

def init_rotate
    @angle = 0
    @rotate_speed = 0
  end


Initialises rotation.

def show(name, origin, x, y, zoom_x, zoom_y, opacity, blend_type)
    @name = name
    @origin = origin
    @x = x.to_f
    @y = y.to_f
    @zoom_x = zoom_x.to_f
    @zoom_y = zoom_y.to_f
    @opacity = opacity.to_f
    @blend_type = blend_type
    init_target
    init_tone
    init_rotate
  end


This is the method called to show a picture; internally, at least in the case of the "Show Picture" command, name is the picture graphic's filename. The other variables are set in accordance with the ones chosen in the dialog box (or supplied by script call if you're calling this manually) and then init_target, init_tone and init_rotate are called.

def move(origin, x, y, zoom_x, zoom_y, opacity, blend_type, duration)
    @origin = origin
    @target_x = x.to_f
    @target_y = y.to_f
    @target_zoom_x = zoom_x.to_f
    @target_zoom_y = zoom_y.to_f
    @target_opacity = opacity.to_f
    @blend_type = blend_type
    @duration = duration
  end


This is the method called for moving a picture, and takes parameters of where to move it, the zoom to move towards, the opacity to move towards, the blend type, and duration the move should take.

def rotate(speed)
    @rotate_speed = speed
  end


Method for changing rotation speed.

def start_tone_change(tone, duration)
    @tone_target = tone.clone
    @tone_duration = duration
    @tone = @tone_target.clone if @tone_duration == 0
  end


Similarly to the tone change method in Game_Screen, we clone the tone so that we can change it without affecting the target, set the duration, and if duration is 0 simply set @tone to the target since there's no need to wait.

Following this we have the erase method, which simply sets to the name to "" and origin to 0.

After this is the frame update method, which calls the methods for updating movement, tone and rotation.

def update_move
    return if @duration == 0
    d = @duration
    @x = (@x * (d - 1) + @target_x) / d
    @y = (@y * (d - 1) + @target_y) / d
    @zoom_x  = (@zoom_x  * (d - 1) + @target_zoom_x)  / d
    @zoom_y  = (@zoom_y  * (d - 1) + @target_zoom_y)  / d
    @opacity = (@opacity * (d - 1) + @target_opacity) / d
    @duration -= 1
  end


Return if duration is 0, there's no need to do anything otherwise. Set d to the duration, then tween X, Y, zoom X, zoom Y and opacity towards their target values, then reduce duration by 1 frame.

update_tone_change is exactly the same principle as the one from Game_Screen.

def update_rotate
    return if @rotate_speed == 0
    @angle += @rotate_speed / 2.0
    @angle += 360 while @angle < 0
    @angle %= 360
  end


Return if the rotate speed is 0 as we don't need to do anything. Then we add the speed / 2.0 to the angle. While angle is less than 0, we add 360 to it. (which I think is to make it still work when rotating backwards) then finally we mod the angle by 360.

Game_Pictures
This class is a wrapper for a picture array, used in Game_Screen. As the comment says, map pictures and battle pictures are handled separately.

Initialization of the class simply sets @data to an empty array.

def [](number)
    @data[number] ||= Game_Picture.new(number)
  end


Getter method. Returns the numberth element of @data if it exists: if not, it creates a new instance of Game_Picture with the number supplied.

def each
    @data.compact.each {|picture| yield picture } if block_given?
  end


Iterator method for pictures. This method is what allows the code in clear_picture and update_pictures in Game_Screen to work.

.compact returns a copy of the calling array with all nil elements removed, so @data.compact creates a copy of @data with only picture data in it. Then that array calls .each on itself. The currently iterated picture is stored in "picture", and we yield picture if block_given?.

Huh?

block_given? is a method which returns true if yielding would execute a block in the current context.

Huh?

Okay, I guess it's time to look at exactly what a block is in Ruby. To put it simply, it's a chunk of code surrounded by curly brackets. {|picture| yield picture } is a block. {|picture| picture.erase } is a block.

Note that when Game_Screen calls @pictures.each, it supplies a block afterwards. The "yield" statement is basically saying "Okay, execute the block from the calling statement please and give it 'picture' as a parameter." and block_given? will return true because a block was supplied. Let's run through this line by line to see exactly what happens; for the sake of example, we currently have 3 pictures on screen and the screen is being cleared.

clear_pictures is called, and calls @pictures.each with the block {|picture| picture.erase }

The each method of @pictures calls @data.compact.each which returns a new array of the 3 pictures currently on-screen and loops through them, executing the block {|picture| yield picture } if block_given?

As a block was given when each was called, it yields to the calling block and sends the current picture as a parameter. Therefore, |picture| in clear_pictures becomes the picture currently being considered by |picture| in Game_Pictures.each, and its .erase method is called.

This is a pretty complex concept so I won't be surprised if a few people have questions about yield and blocks in the comments.

That's it for this week! I was feeling generous so this one's a bit longer than usual, but hopefully you'll find it useful. As always comments are encouraged and welcomed. Please let me know what you think of the series so far, what you like, what you don't, ask me to clarify anything you don't understand. Or even just comment to let me know you read it! I'm starting to think I don't have an audience here.

Until next time.

Posts

Pages: 1
You have a audience, probably just a silent one though.
Thanks for your excellent work. It is both an indispensable discussion of the RPG scripts and a very interesting presentation of some more advanced parts of ruby.

Unfortunately, I do not find something like:
"Under the hood part 3: Game objects" !

I am missing it. Where is it? Please help me !

Sincerely yours
Lucius Dexter
Just a heads up. The new RPG Maker MV won't be supporting the RGSS Ruby scripting anymore & will be switching to Javascript. Anyone who wants to jump on that bandwagon early will probably give up on Ruby & start learning javascript.
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
I just found that out, Animebryan. I'm still gonna finish the series as I imagine a lot of people will still be using Ace, but I'll probably look at doing a Javascript tutorial series after that.
That's good news, Animebrian!
Actually I know Javascript very well. I am looking forward to an english version of RPG Maker MV. It is very promising. When will it appear?

Besides, an internet search found the missing part for me. It is at
http://rpgmaker.net/articles/1114/
and classified under "Game design and Theory" instead of "programming and mathematics".

Trihan, I hope that you will still be finishing the series. The programming language may change - but programming methods and algorithms remain. You explain them very well.

Sincerely yours
Lucius Dexter
author=luciusDexter
That's good news, Animebryan!
Actually I know Javascript very well. I am looking forward to an english version of RPG Maker MV. It is very promising. When will it appear?
It's set to come out around December. Cool thing is, we don't have to wait between the initial Japanese release & English release! It's getting an International release worldwide simultaneously! I'm not sure how much it'll cost though. But since the scripting is changing to javascript, that means scripts will exist as 'Plug-ins', which are suppose to be more user friendly. Actually, here's the page that previews some of the new features: http://www.rpgmakerweb.com/products/programs/rpg-maker-mv
Pages: 1