SLIP INTO RUBY - UNDER THE HOOD PART 1: THE MODULES

We cover new things! Exciting!

  • Trihan
  • 03/17/2015 07:55 PM
  • 10546 views


Modules? What the fuck are modules?

Think of a module as a way to group a bunch of methods, classes and constants under one name. It also lets you approximate multiple inheritance via something called a "mixin", but that's a topic for another time. All you really need to know is that modules let you lump stuff together so you can refer to it more easily.

In terms of their use within the default classes of RGSS3, it seems that Enterbrain has opted to use modules for something akin to global functions: that is to say, the stuff that's in the default modules isn't instantiated anywhere and can be called from any other place in script, even in scripts you're writing yourself. I'll provide examples of this while I'm explaining them.

The first module in the list is also the simplest: Vocab. In fact, there probably isn't even much point in including it in this series because it's so self-explanatory, but in the interests of completeness we're going to look at it anyway, so here goes.

Vocab

Vocab, as the name suggests, is a module that defines all of the terms and phrases used in various parts of your game, from how enemies are announced in battle to what your currency is called. Many of the constants defined here are simple strings and can be edited in the script editor to your heart's content. Some grab information from the settings in the database. We're going to look at a couple of notable, nifty little pieces of code.

# Basic Status
  def self.basic(basic_id)
    $data_system.terms.basic[basic_id]
  end


This is known as a shortcut method: near the bottom of Vocab, things like self.level are defined simply as "basic(0)" and that's because of this method. If we didn't have that, it would have had to be "$data_system.terms.basic[0]" instead. Considering there are 8 methods that draw from "basic" and 23 which draw from "command", you can see why having a shortcut would be handy. As you can see, the method doesn't really do anything by itself, but it makes referring to a long nest of objects much easier in other places.

$data_system.terms is where the terms from the database are located, and subsequently contains the arrays "basic", "params", "etypes" and "commands". These correspond to the Basic Status, Parameters, Equip Types and Commands sections of the terms tab.

If you ever want to call something from Vocab, all you have to do is type Vocab, two colons, and the name of the method/property. For example, if I have an event where I want to display the name of the player's party, all I have to do is "Vocab::PartyName". This is the universal way to call stuff from modules, and you'll probably see it a lot in 3rd party scripts.

Sound

Sound, as the name suggests, is the module dealing with...well, sounds. The first thing in it is a shortcut method, play_system_sound, which calls $data_system.sounds[n].play, where n is the number of the sound. This array is defined by the Sound Effects section in the System tab of the database.

As with Vocab, if I want to do something from Sound, for example if I want to play the buzzer sound right after an NPC makes a joke, I would do "Sound::play_buzzer".

Cache

Cache is one we've come across before during the bestiary tutorial, and as I explained then it loads graphics into memory so there's less overhead when the same graphic is requested multiple times. If we didn't use Cache, every time you wanted a particular bitmap it would create a new instance of it, and your game's memory usage would skyrocket.

Most of the methods are pretty self-explanatory, but there is some code here that might look confusing to beginners.

def self.load_bitmap(folder_name, filename, hue = 0)
    @cache ||= {}
    if filename.empty?
      empty_bitmap
    elsif hue == 0
      normal_bitmap(folder_name + filename)
    else
      hue_changed_bitmap(folder_name + filename, hue)
    end
  end


Let's look at this.

@cache ||= {}


The logical OR equals is actually a point of contention among Ruby users as to its exact sequence of logic, but all you really need to know if that it's essentially saying "set the variable on the left to the expression on the right but only if that variable doesn't already contain a value". In other words, @cache is set to an empty hashmap but only if @cache was empty already. If there's already something in it, this line does nothing.

if filename.empty?
      empty_bitmap
    elsif hue == 0
      normal_bitmap(folder_name + filename)
    else
      hue_changed_bitmap(folder_name + filename, hue)
    end


If the filename is empty, call empty_bitmap (which we'll get to in a second). If filename is not empty, and the hue is 0, call normal_bitmap with folder_name + filename as a parameter. If filename is not empty and hue is not 0, call hue_changed_bitmap with folder_name + filename, and hue as parameters.

Obviously, the situation in which filename will be empty is if you've got a graphic in the database set to "none". Hue kicks in for stuff like enemy graphics, where you have a slider that changes it.

def self.empty_bitmap
    Bitmap.new(32, 32)
  end


Not much to say here, the method simply creates a new 32x32 bitmap.

def self.normal_bitmap(path)
    @cache[path] = Bitmap.new(path) unless include?(path)
    @cache[path]
  end


Here's where things get interesting. We know from load_bitmap that the path parameter is going to be folder_name + filename, which obviously gives you the full path to the file. The path + filename is added as a new key to the @cache hashmap, and its value is set to a new instance of bitmap using the file as the graphic to use. UNLESS that path is already present as a key in @cache, in which case it just returns that element of the hashmap.

If you can't see what this is doing, basically this is the very meat and potatoes of why you don't get memory leaks when you're caching graphics. It only creates a new instance if that instance isn't already in the graphics cache, otherwise it returns the object that's already there.

def self.hue_changed_bitmap(path, hue)
    key = [path, hue]
    unless include?(key)
      @cache[key] = normal_bitmap(path).clone
      @cache[key].hue_change(hue)
    end
    @cache[key]
  end


Obviously if we're dealing with hue changes there's a little bit of extra magic that needs to happen. Rather than simply being the path to the file, the key being added to @cache here is the path AND the hue. If it's not already cached, we call a clone of normal_bitmap and then call the hue_change method on it (this is a built-in method of the Bitmap class). And again, if it's already cached we just return the graphic that's already there.

def self.include?(key)
    @cache[key] && !@cache[key].disposed?
  end


.include? is built in to certain Ruby objects like arrays, but sometimes we need to either overload the built-in method or provide our own logic to determine whether something is "included". In this case, the check is that key (which consists of folder + filename + hue if set) AND that key has not been disposed. disposed? is another built-in method of the Bitmap class which checks whether a particular bitmap has been freed from memory. Obviously, if a given bitmap has been freed, we don't want it in the cache any more.

def self.clear
    @cache ||= {}
    @cache.clear
    GC.start
  end


Hopefully another self-explanatory method. Cache is set to an empty hashmap if it doesn't already contain data, then the hashmap is cleared, then GC.start is called. Wait, the fuck? GC? What's that?

Glad you asked, campers! GC is another built-in module which handles garbage collection, which is a fancy way to refer to a piece of code which frees up memory that's no longer needed. If we didn't do this, all your bitmaps would just keep floating around in space taking up memory until your computer exploded. Yay for science!

DataManager

DataManager is the module that handles all your saving, and loading, and starting new games, and creating game objects, and setting stuff up, and pretty much anything to do with data. It's a pretty integral part of your game, and now we're going to rip it open and look at its shiny, shiny innards.

@last_savefile_index = 0                # most recently accessed file


Comment already explains it: this is just an instance variable which tracks which save file you accessed last. By default this is used so that when you go to load your game it automatically highlights the file you last loaded, but if you wanted to you could actually have a continue option on the menu which automatically loads the most recent save rather than requiring the player to pick one! Cool, huh?

def self.init
    @last_savefile_index = 0
    load_database
    create_game_objects
    setup_battle_test if $BTEST
  end


Okay, so @last_savefile_index is initialised at 0, cool. Then load_database is called. Let's look at that.

def self.load_database
    if $BTEST
      load_battle_test_database
    else
      load_normal_database
      check_player_location
    end
  end


$BTEST is a global boolean variable which is true if the player is in the Battle Test part of the database, and false otherwise. It's set by the editor and there isn't really any reason or benefit to messing with it outwith that, so just content yourself with knowing what it's there for. If $BTEST is true, which is to say we are testing a battle, call load_battle_test_database. Cool! Let's look at that!

ef self.load_battle_test_database
    $data_actors        = load_data("Data/BT_Actors.rvdata2")
    $data_classes       = load_data("Data/BT_Classes.rvdata2")
    $data_skills        = load_data("Data/BT_Skills.rvdata2")
    $data_items         = load_data("Data/BT_Items.rvdata2")
    $data_weapons       = load_data("Data/BT_Weapons.rvdata2")
    $data_armors        = load_data("Data/BT_Armors.rvdata2")
    $data_enemies       = load_data("Data/BT_Enemies.rvdata2")
    $data_troops        = load_data("Data/BT_Troops.rvdata2")
    $data_states        = load_data("Data/BT_States.rvdata2")
    $data_animations    = load_data("Data/BT_Animations.rvdata2")
    $data_tilesets      = load_data("Data/BT_Tilesets.rvdata2")
    $data_common_events = load_data("Data/BT_CommonEvents.rvdata2")
    $data_system        = load_data("Data/BT_System.rvdata2")
  end


What's interesting about this is that even data structures not necessary for battles are still loaded in for battle tests. load_data is a built-in function which...well, loads the data file indicated and assigns it to an object. This is how $data_items knows you're talking about the items in the database, for example. The BT_ data files seem to be temporary ones created when a battle test is initiated, as they don't seem to be present in the data folder until then. Also, just a minor observation: I don't actually think $data_tilesets is necessary in the battle test setup, but it's there anyway so meh.

Okay, so back to load_database, we see that if $BTEST is not true it calls load_normal_database instead.

def self.load_normal_database
    $data_actors        = load_data("Data/Actors.rvdata2")
    $data_classes       = load_data("Data/Classes.rvdata2")
    $data_skills        = load_data("Data/Skills.rvdata2")
    $data_items         = load_data("Data/Items.rvdata2")
    $data_weapons       = load_data("Data/Weapons.rvdata2")
    $data_armors        = load_data("Data/Armors.rvdata2")
    $data_enemies       = load_data("Data/Enemies.rvdata2")
    $data_troops        = load_data("Data/Troops.rvdata2")
    $data_states        = load_data("Data/States.rvdata2")
    $data_animations    = load_data("Data/Animations.rvdata2")
    $data_tilesets      = load_data("Data/Tilesets.rvdata2")
    $data_common_events = load_data("Data/CommonEvents.rvdata2")
    $data_system        = load_data("Data/System.rvdata2")
    $data_mapinfos      = load_data("Data/MapInfos.rvdata2")
  end


Almost identical to the battle test setup, but with the addition of $data_mapinfos, which contains information on the map. Shocking stuff!

It also calls check_player_location.

def self.check_player_location
    if $data_system.start_map_id == 0
      msgbox(Vocab::PlayerPosError)
      exit
    end
  end


Oh ho. So this checks to see if start_map_id is 0 (which is only the case if there is no player spawn point on any map) in which case it shows a message box containing PlayerPosError from the Vocab module. Interestingly, this means you could actually have the game do other stuff if no player position is set, but I can't think of a practical use for that at the moment. If you have an idea there, feel free to mention it in the comments!

Right, so back to init. After load_database it calls create_game_objects. Great, let's take a look.

def self.create_game_objects
    $game_temp          = Game_Temp.new
    $game_system        = Game_System.new
    $game_timer         = Game_Timer.new
    $game_message       = Game_Message.new
    $game_switches      = Game_Switches.new
    $game_variables     = Game_Variables.new
    $game_self_switches = Game_SelfSwitches.new
    $game_actors        = Game_Actors.new
    $game_party         = Game_Party.new
    $game_troop         = Game_Troop.new
    $game_map           = Game_Map.new
    $game_player        = Game_Player.new
  end


Oh, so that's how we end up with the global variables for those classes. It may not be immediately obviously, but this shows that you can actually have your own $game_whatevers based on your new Game_Whatever class, and use them in exactly the same way (my dynamic bestiary script actually does this with Game_Bestiary / $game_bestiary).

And then we also call setup_battle_test if we're testing a battle:

def self.setup_battle_test
    $game_party.setup_battle_test
    BattleManager.setup($data_system.test_troop_id)
    BattleManager.play_battle_bgm
  end


This calls the setup_battle_test method of $game_party, which we'll look at when we get to Game_Party in the series. Then calls the setup method of BattleManager, which we'll be covering next week, and then play_battle_bgm from the BattleManager module as well. Basically, it sets up the characters and enemies, then players the battle music.

Next up in DataManager we have setup_new_game.

def self.setup_new_game
    create_game_objects
    $game_party.setup_starting_members
    $game_map.setup($data_system.start_map_id)
    $game_player.moveto($data_system.start_x, $data_system.start_y)
    $game_player.refresh
    Graphics.frame_count = 0
  end


This calls create_game_objects, which we've just looked at. Then calls setup_starting_members in $game_party, then setup in $game_map, then moveto in $game_player, then refresh in $game_player and finally sets Graphics.frame_count to 0.

We'll look at these methods more in-depth when we get to the parts dealing with their respective classes, but for now suffice to say what this does is sets up the starting party, initialises the map, moves the player to wherever the game's creator set the spawn point, refreshes the graphics so everything appears on screen and initialises the number of frames which have elapsed in the game to 0. Graphics is a built-in module, and frame_count is used to determine things like how long the game has been played for. Every time Graphics.update is called, the game advances by 1 frame.

Next up we have save_file_exists?.

!Dir.glob('Save*.rvdata2').empty?
  end


Dir is a built-in Ruby class for dealing with directories. glob is a pattern-matching file search method. Basically it's looking for any file starting with "Save" (can have anything after it) with the extension .rvdata2, which is VX Ace's save file extension.

def self.savefile_max
    return 16
  end


Simple method, returns 16 as the maximum number of save files. The obvious implication here is that if you want more than 16 save files, just use a bigger number.

def self.make_filename(index)
    sprintf("Save%02d.rvdata2", index + 1)
  end


This is a method which makes the filename for when files are being saved, though not the one that actually saves them. sprintf is a method which basically replaces patterns in a string with data: in this case, we're taking the string "Save.rvdata2" and planting a number 2 digits long (so single-digit filenames have a leading 0) between the Save and the dot, which will be the supplied index + 1. As index starts at 0, obviously the first file you create will be Save01.rvdata2. There are a couple of important points to take away from this: if you want to have more than 99 save files and still maintain the "tidiness" of the file naming, you should change %02d to %03d or however many max digits you have.

def self.save_game(index)
    begin
      save_game_without_rescue(index)
    rescue
      delete_save_file(index)
      false
    end
  end


begin/rescue is Ruby's equivalent of a try/catch block. What we're essentially saying is "Please attempt to do the stuff after "begin". If you run into trouble, do the stuff after "rescue" instead. So we're going to -attempt- to call save_game_without_rescue, using index as a parameter. Let's see where that takes us.

def self.save_game_without_rescue(index)
    File.open(make_filename(index), "wb") do |file|
      $game_system.on_before_save
      Marshal.dump(make_save_header, file)
      Marshal.dump(make_save_contents, file)
      @last_savefile_index = index
    end
    return true
  end


Okay, so first we call the open method of the built-in class File, using make_filename(index) and "wb" as the arguments and set it up as a block of code to be executed, with "file" as the reference to the file opened. When a block is used with File.open in this way, it will execute the block and then automatically close the file for us afterwards.

The mode "wb" opens the file in binary write mode. I'm not too well-versed in binary files, but from what I understand it's a format used because it's quick, easy and efficient, if not difficult to read by a person. The write part should be self-explanatory.

We then call the on_before_save method of $game_system; we'll cover it in-depth when we cover Game_System, but for now I'll say it increases the save count by 1, sets the save file's version ID, sets the frame count, and sets the most recent BGM/BGS that were playing.

Marshal is yet another built-in module for writing/reading files. We're calling its .dump method twice, with make_save_header and make_save_contents as arguments. Let's see what this does.

def self.make_save_header
    header = {}
    header[:characters] = $game_party.characters_for_savefile
    header[:playtime_s] = $game_system.playtime_s
    header
  end


So header is initialised as an empty hashmap. A key is created for the symbol :characters (I'll cover symbols at a later date) with the value $game_party.characters_for_savefile, which basically collates the charsets for everyone in the party (that's how they're all displayed in the save file list). Another key is created for the symbol :playtime_s with the value $game_system.playtime_s, which just returns the time played in hours, minutes and seconds as a string. And finally, it returns header.

def self.make_save_contents
    contents = {}
    contents[:system]        = $game_system
    contents[:timer]         = $game_timer
    contents[:message]       = $game_message
    contents[:switches]      = $game_switches
    contents[:variables]     = $game_variables
    contents[:self_switches] = $game_self_switches
    contents[:actors]        = $game_actors
    contents[:party]         = $game_party
    contents[:troop]         = $game_troop
    contents[:map]           = $game_map
    contents[:player]        = $game_player
    contents
  end


Similarly, here we're initialising contents as an empty hashmap and saving all the game data. As mentioned before, if you've created your own game_whatever classes and you want the data to be retained when the game is saved, you'll have to add your own symbol here. For example, I've added the line "contents = $game_bestiary" to the end in my project that has the dynamic bestiary in it.

So that covers what happens if we manage to carry out the stuff in the begin block. The only place where anything can actually throw an exception is in File.open, which essentially means that rescue is going to fire in the event that the file can't be opened. Let's take a look at what happens then:

def self.delete_save_file(index)
    File.delete(make_filename(index)) rescue nil
  end


Short and sweet. Calls File.delete on the file (deletes it, basically) with a rescue of "nil". This means if the file fails to delete...nothing happens. Chances are that the way things are set up, the only way files can fail to delete is if they aren't there to begin with (for example, if someone deletes the save file in Windows explorer between the begin and rescue, which I don't think is humanly possible but you gotta provide for every eventuality I guess!)

We're almost done for today. The methods for loading a game are almost identical to the ones for saving, if not exactly opposite (in the case of extract_save_contents). load_game_without_rescue does call a method which updates the map if the version number is different, but that's more or less the only difference. There are, however, a few more interesting methods to look at.

def self.savefile_time_stamp(index)
    File.mtime(make_filename(index)) rescue Time.at(0)
  end


This method is only used in latest_savefile_index, which we'll look at in a second. mtime returns the time the supplied file was last modified. If this fails, the rescue will instead return 00:00 on the 1st of January 1970 (what's considered the Unix Epoch). This ensures that the timestamp will at least have a default if the file can't be assigned one for some reason.

def self.latest_savefile_index
savefile_max.times.max_by {|i| savefile_time_stamp(i) }
end

Okay, this looks a little complex but it's not as bad as it seems. The important thing to remember is that in Ruby EVERYTHING is an object, even numbers, and so numbers can call methods just like anything else. Basically what's happening here is that we're calling .times on savefile_max (which is set by default to 16) so this is going to repeat what follows 16 times. The loop is calling max_by, which is a method that returns the maximum value returned by the block that follows. The block uses i (the current iteration) as the block reference and calls savefile_time_stamp with that as a parameter.

To put it more simply, we're comparing the timestamps on each of the save files, then returning the one that's highest (most recent).

def self.last_savefile_index
    @last_savefile_index
  end


And finally, this method just returns the value of last_savefile_index. Simples!

And there we have it. Next week we'll be looking at the last two modules, SceneManager and BattleManager. There was too much to include them in this edition unfortunately. As usual comments, criticisms, corrections, death threats and marriage proposals are welcome.

Until next time.

Posts

Pages: 1
kentona
I am tired of Earth. These people. I am tired of being caught in the tangle of their lives.
21217
Now THIS is what I've been waiting for! (because I am a lazy bastard who felt like waiting for someone to explain it to me was the best course of action)
My head...it hurts... Programming just isn't my thing. There's just so many attributes to learn...ugh.
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3119
If you feel that way, Ljink, I'm not explaining it simply enough, as the series is aimed at people with literally no experience of programming.

Is there anything you didn't understand that I might be able to clear up for you?
author=Trihan
If you feel that way, Ljink, I'm not explaining it simply enough, as the series is aimed at people with literally no experience of programming.

Is there anything you didn't understand that I might be able to clear up for you?


Oh no, you're probably doing a fantastic job explaining it. However, programming(math in general) and me are like oil and water...we don't mix. My brain can wrap around it yet.
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3119
Well if there's ever anything specific you're struggling to make sense of feel free to PM me and I'll do what I can to help.
kentona
I am tired of Earth. These people. I am tired of being caught in the tangle of their lives.
21217
I think my biggest hurdle will be the peculiarities of Ruby syntax
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3119
It's actually a lot nicer than other languages because you don't have to explicitly type variables, there are no semicolons and you don't need to put brackets around blocks.
kentona
I am tired of Earth. These people. I am tired of being caught in the tangle of their lives.
21217
On the other hand, it is harder to follow than other languages because none of the variables are explicitly typed, there are no semi-colons to indicate end of line/function, and blocks of code aren't neatly contained inside brackets.
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3119
The more pressing question, kentona, is would you like the next part early? ;)
Pages: 1