SLIP INTO RUBY - UNDER THE HOOD PART 20: WINDOWS 2: ELECTRIC BOOGALOO

In which the 3-year hiatus finally ends.

  • Trihan
  • 06/28/2020 11:13 AM
  • 2599 views
Hello, sports fans! I can't believe it's been 3 years since I wrote the last one of these. Let it never be said that my genius is timely. But better late than never, right? Let's slip into some Ruby!



Today: Windows 2: Electric Boogaloo

Window_KeyItem
This is the window that appears when you use the "Select Key Item..." event command. It inherits from Window_ItemList, which we looked at several years ago and you're guaranteed to have forgotten about by now. Section 3 is the one where I covered it if you want a refresher.

def initialize(message_window)
    @message_window = message_window
    super(0, 0, Graphics.width, fitting_height(4))
    self.openness = 0
    deactivate
    set_handler(:ok,     method(:on_ok))
    set_handler(:cancel, method(:on_cancel))
  end


The constructor takes a single parameter, message_window. As you'll see in a bit when we look at Window_Message, this parameter will be provided by that class, and it will just pass in itself, because this window is going to be created by the message window. This probably sounds confusing right now, but all will become clear.

So we set an instance variable called @message_window to the value passed in by the parameter. Then we call the super method (initialize of Window_ItemList) passing in 0 and 0 for the X and Y coordinates (top left of the screen), a width of Graphics.width, and a height of fitting_height(4), or in other words the whole width of the screen and enough height for 4 lines of text.

We set the openness property of self to 0, which will initialise it as completely closed (because we don't want the window open unless we've used the event command), and deactivate the window for the same reason. We call set_handler twice, one for the :ok symbol which calls the on_ok method, and one for the :cancel symbol which calls on_cancel.

def start
    self.category = :key_item
    update_placement
    refresh
    select(0)
    open
    activate
  end


The start method is called to get the ball rolling on the key item choice, which we'll look at, again, when we cover Game_Message in a bit.

We set the category property of self to the symbol :key_item. You've probably forgotten, but we looked at category in Window_ItemList. Then we call update_placement, which is the next method we'll look at. Then we call refresh, which is just the parent class's version of it since there's no overload in this class. Then we select the (0)th item (the first one in the list). Then we call open, which animates the window opening (we looked at this in Window_Base in part 5, section 1), and then we activate the window so that the player is able to move the cursor in it.

def update_placement
    if @message_window.y >= Graphics.height / 2
      self.y = 0
    else
      self.y = Graphics.height - height
    end
  end


This method does exactly what it says on the tin; it updates the placement of the window. If the Y coordinate of the message window is greater than half the game window height, we set self.y to 0. Otherwise, we set it to the game window height minus the height of the window (which places it on the bottom).

The purpose of this method is to ensure that the key item choice window is on top of the screen when the message window is on the bottom, and on the bottom when the message window is on top. In other words, they will never clash positionally. We did the same thing with Window_ChoiceList and Window_NumberInput.

def on_ok
    result = item ? item.id : 0
    $game_variables[$game_message.item_choice_variable_id] = result
    close
  end


Our handler method for the :ok symbol (which as we looked at before is called when we hit the OK key) does a few things. First, we set a variable called result using an inline if. If the "item" method (from the parent class) returns a value, we set it to item's id property. If not, we set it to 0.

The second line looks more complicated than it is. We're just setting a variable to the value of result. Which variable? The one stored in the item_choice_variable_id property of $game_message (which will be whichever variable you chose in the event command).

Finally, we close the window because its work is done.

def on_cancel
    $game_variables[$game_message.item_choice_variable_id] = 0
    close
  end


This is the handler method for the :cancel symbol, and is similar to the previous one. But because we're cancelling, we know there's no result, so we just skip to setting the chosen variable to 0 and closing the window.

And that's all for this class! Now let's look at the one that actually makes use of it.

Window_Message
Since this is the window responsible for showing all your game's text, it's a pretty big and complex one. But as I've hopefully demonstrated in this series, there's no class that's insurmountably complicated. So let's hit it! We'll start by noting that this class inherits from Window_Base, and so has all the properties and methods we looked at in part 5 section 1.

def initialize
    super(0, 0, window_width, window_height)
    self.z = 200
    self.openness = 0
    create_all_windows
    create_back_bitmap
    create_back_sprite
    clear_instance_variables
  end


The constructor is mainly just calling a bunch of other methods after the initial setup. First, we call the parent class's initialize method, passing in coordinates of (0, 0) and window_width and window_height for the width and height, which are methods we'll look at shortly. We set self.z to 200, meaning it will appear above every other window in the game. We set self.openness to 0 so that the message window won't always be on the screen. Then we call the create_all_windows, create_back_bitmap, create_back_sprite and clear_instance_variables methods. We'll be looking at all of those in a bit.

def window_width
    Graphics.width
  end


window_width is the method whose return value we passed into the constructor to determine how wide a window to create. It just returns Graphics.width, which is the full width of the game window. In other words, the message window will be as wide as it possibly can be.

def window_height
    fitting_height(visible_line_number)
  end


window_height is the method whose return value as passed into the constructor to determine how tall a window to create. It returns the result of calling fitting_height, passing in visible_line_number as the argument. visible_line_number is a method we'll also look at shortly, and we already looked at fitting_height years ago as it's a method of the parent class.

def clear_instance_variables
    @fiber = nil                # Fiber
    @background = 0             # Background type
    @position = 2               # Display position
    clear_flags
  end


The clear_instance_variables method is another one that does what it says on the tin. We set the variable @fiber to nil, @background to 0 (normal window), @position to 2 (bottom), and then call the clear_flags method, which is the next one that's coming up. Note that background and position correspond to the same variables from Game_Message, and are in fact driven by those values, as we'll see.

def clear_flags
    @show_fast = false          # Fast forward flag
    @line_show_fast = false     # Fast forward by line flag
    @pause_skip = false         # Input standby omission flag
  end


This method clears the message flags by setting them all to false: @show_fast determines whether the player is fast forwarding the message, @line_show_fast determines whether a particular line is being shown fast, and @pause_skip determines whether the message runs by itself or requires player input.

def visible_line_number
    return 4
  end


This is the method which determines how many lines to show in the window, and simply returns 4. If you ever want to make your game message window bigger, this is the number to change.

def dispose
    super
    dispose_all_windows
    dispose_back_bitmap
    dispose_back_sprite
  end


Pretty standard memory freeing method; we call the parent class dispose method, then dispose_all_windows, then dispose_back_bitmap, then dispose_back_sprite. They do exactly what they say on the tin, but we'll see that in a sec.

def update
    super
    update_all_windows
    update_back_sprite
    update_fiber
  end


Again, the update method is quite straightforward. Call the parent method, then update_all_windows, then update_back_sprite, then update_fiber. Nothing complex here.

def update_fiber
    if @fiber
      @fiber.resume
    elsif $game_message.busy? && !$game_message.scroll_mode
      @fiber = Fiber.new { fiber_main }
      @fiber.resume
    else
      $game_message.visible = false
    end
  end


Here's where it starts to get a bit more interesting. Okay, so this method updates the currently-running fiber (a topic we covered years ago but you've probably forgotten). Quick refresher: a Fiber in Ruby is essentially a self-contained block of code that can be paused and resumed. Sort of like a thread if you're familiar with multithreading.

Okay, so. If @fiber already contains data, we call resume on it. Otherwise, if the busy? method of $game_message returns true, and the scroll_mode flag is false (in other words, if we're displaying a message that isn't set to scroll), we set @fiber to a new instance of the Fiber class, and the block calls fiber_main, which we'll see in a bit. Then we resume the fiber (which will immediately call fiber_main and continue processing from there). Otherwise, we set the visible flag of $game_message to false, because there's no running fiber and nothing needs to be displayed.

def create_all_windows
    @gold_window = Window_Gold.new
    @gold_window.x = Graphics.width - @gold_window.width
    @gold_window.y = 0
    @gold_window.openness = 0
    @choice_window = Window_ChoiceList.new(self)
    @number_window = Window_NumberInput.new(self)
    @item_window = Window_KeyItem.new(self)
  end


The create_all_windows method...well, it creates all the windows. So we have @gold_window, set to a new instance of Window_Gold. Its x coordinate is set to the width of the game window minus the width of the gold window, and its Y to 0. This will place it at the top right of the screen. Its openness is set to 0 because we don't want the gold window showing every time we display a message. Then there's @choice_window, @number_window and @item_window, which are respectively set to new instances of Window_ChoiceList, Window_NumberInput and Window_KeyItem. The arguments passed in are all "self", meaning that Window_Message will pass its own reference in as the message_window parameter for each window that requires one. (this is what creates the link that allows those windows to reference the message window in their own code)

Note that we don't have to do anything with the position or openness of the other windows because they don't need to reference their own dimensions for placement, and their openness is handled by their own constructors.

def create_back_bitmap
    @back_bitmap = Bitmap.new(width, height)
    rect1 = Rect.new(0, 0, width, 12)
    rect2 = Rect.new(0, 12, width, height - 24)
    rect3 = Rect.new(0, height - 12, width, 12)
    @back_bitmap.gradient_fill_rect(rect1, back_color2, back_color1, true)
    @back_bitmap.fill_rect(rect2, back_color1)
    @back_bitmap.gradient_fill_rect(rect3, back_color1, back_color2, true)
  end


The method for creating the background bitmap looks scarier than it is.

Okay, so we set @back_bitmap to a new Bitmap instance, passing in width and height as parameters (which as we've covered in the past are built-in methods of the base Window class which just return the window's dimensions). In other words, our bitmap will be as large as the message window is. We set a variable called rect1 to a new Rect with a top-left coordinate of (0, 0), "width" pixels wide and 12 pixels high. rect2 we set to a similar Rect, but the start coordinate is (0, 12) and the height is "height" minus 24. Then we set rect3 to a Rect with start coordinate (0, "height" minus 12) and a height of 12 pixels. So basically, rect1 will cover the top 12 pixels of the window, rect2 will cover from the 12th pixel to 12 pixels from the bottom, and rect3 will cover the bottom 12 pixels.

Then we call gradient_fill_rect, passing in rect1 as the rect, back_color2 as the starting colour, back_color1 as the colour to transition to, and true for a vertical gradient. After that, we call fill_rect passing in rect2 and back_color1. Then finally we call gradient_fill_rect again, this time passing in rect 3 as the rect, back_color1 as the starting colour, back_color2 as the transition colour, and true again for a vertical gradient. We'll come back to this after the next two methods, because...

def back_color1
    Color.new(0, 0, 0, 160)
  end


...it'll explain the back_color1 and back_color2 nonsense. So back_color1 turns out to be a method, which returns a new instance of the Color class, where the parameters are 0, 0, 0 and 160. These are the values of red, green, blue, and alpha (transparency). So effectively, this is a slightly transparent black.

def back_color2
    Color.new(0, 0, 0, 0)
  end


And back_color2 is fully transparent black.

So if you've ever created a Show Text command using the "Dim Background" background setting, you can see what all this has just done in action. Look very carefully at the very top and very bottom of the message window. Notice that it gradually fades out?

So effectively what we've done here is created a gradient that goes from completely transparent black to semi-transparent black for 12 pixels, then filled all but the last 12 pixels after that in the semi-transparent black, then done the opposite gradient to transition from semi-transparent back to fully-transparent black for the last 12 pixels. This creates a slight fuzziness to the top and bottom edges of the message window, which is more aesthetically-pleasing than just having solid colour. Considering how rarely people seem to use Dim Background, whether this was worth the effort or not is an exercise for the reader.

def create_back_sprite
    @back_sprite = Sprite.new
    @back_sprite.bitmap = @back_bitmap
    @back_sprite.visible = false
    @back_sprite.z = z - 1
  end


create_back_sprite is a pretty simple method. First, we set the @back_sprite instance variable to a new instance of the Sprite class, then set its bitmap property to @back_bitmap (which we just created in the create_back_bitmap method), set its visible property to false so that it's initially invisible, and set its z coordinate to the z of the message window minus 1, meaning the background will always be below the window (otherwise it would overlap the text, which would make it appear dimmer).

def dispose_all_windows
    @gold_window.dispose
    @choice_window.dispose
    @number_window.dispose
    @item_window.dispose
  end


The method for disposing all windows is pretty straight forward; we're just calling the dispose method on each window variable, which consists of @gold_Window, @choice_window, @number_window and @item_window. If we didn't dispose them, they would retain references in memory, which will eventually cause memory leaks. And memory leaks are bad.

def dispose_back_bitmap
    @back_bitmap.dispose
  end


Similarly, dispose_back_bitmap calls the dispose method on @back_bitmap.

def dispose_back_sprite
    @back_sprite.dispose
  end


And the same thing for @back_sprite. Freeing up memory is important, yo.

def update_all_windows
    @gold_window.update
    @choice_window.update
    @number_window.update
    @item_window.update
  end


The less destructive counterpart to dispose_all_windows, update_all_windows updates them instead of disposing them. So we call the update method on @gold_window, @choice_window, @number_window and @item_window. These are all windows we've already looked at, so you can reference previous Slip into Ruby articles to see what this does.

def update_back_sprite
    @back_sprite.visible = (@background == 1)
    @back_sprite.y = y
    @back_sprite.opacity = openness
    @back_sprite.update
  end


The method for updating the background sprite does a few things. First, we set its visible property to true if the @background variable is equal to 1 (meaning the "Dim Background" option was chosen in the Show Text command). Then we set the back sprite's Y coordinate to the message window's Y (in case it's been moved by the update_placement method), then set its opacity to the value of openness (this will cause the dim background to "fade in" like the default window animates opening), and finally we update the sprite itself.

def fiber_main
    $game_message.visible = true
    update_background
    update_placement
    loop do
      process_all_text if $game_message.has_text?
      process_input
      $game_message.clear
      @gold_window.close
      Fiber.yield
      break unless text_continue?
    end
    close_and_wait
    $game_message.visible = false
    @fiber = nil
  end


Finally we reach the meat and potatoes of the message window; the main fiber processing method.

First, we set the visible property of $game_message to true (you may not remember this, but that property is used in many places in the Game_ classes, like determining whether the player can move). Then we call update_background and update_placement, which we'll look at shortly. Then we have a big infinite loop:

We call process_all_text (which we'll look at soon) if the has_text? method of $game_message returns true (meaning there is text we want to show in the message window). Then we call process_input, which again we'll see soon. After that we call the clear method of $game_message, then the close method on @gold_window (once everything is processed we don't want the gold window on screen any more), then Fiber.yield, then we break out of the infinite loop unless text_continue? returns true, in which case we'll go back to the beginning.

So effectively, one the text has been processed, we yield back to the part of the code that called Fiber.resume (which is update_fiber) where it will either resume the existing fiber, create one if there's text to show and no fiber exists, or make the message window invisible. In other words, text will just continue churning out until there isn't any left (which is why you keep getting new message windows whenever you have more text to show than will fit in one window). This will become clearer once we look at the methods being called that we haven't covered yet.

Outside the loop, we call close_and_wait, set the visible property of $game_message to false, and set @fiber to nil, which removes the Fiber update_fiber created (which is necessary so that update_fiber won't try to resume a fiber that isn't being yielded now; without this, you'd get a "dead fiber called" error)

def update_background
    @background = $game_message.background
    self.opacity = @background == 0 ? 255 : 0
  end


The method for updating the background first sets @background to the background property of $game_message, which will either be 0 for "Normal Window", 1 for "Dim Background", or 2 for "Transparent". Then we set self.opacity to 255 if @background is 0, or 0 otherwise. In other words, the window will be fully opaque with "Normal Window" and fully transparent for both other options (which means a background of 1 will show the black gradient we covered above, and 2 will just show the text with no background). Without this opacity setting, we'd see the window skin regardless of background selection.

def update_placement
    @position = $game_message.position
    self.y = @position * (Graphics.height - height) / 2
    @gold_window.y = y > 0 ? 0 : Graphics.height - @gold_window.height
  end


This method updates window placement, which is required to ensure windows don't overlap each other. First, we set @position to the position property of $game_message, which will either be 0 for top, 1 for middle or 2 for bottom. We set self.y to that value multiplied by the height of the game window minus the height of the message window, halved.

If position is 0, this ends up being "0 * (game window height - window height) / 2", which is obviously 0 (and so the y coordinate will be the top of the screen). If 1, it's "1 * (game window height - window height) / 2", which will result in a y coordinate halfway down the screen. If 2, it's "2 * (game window height - window height) / 2", which will end up placing it on the bottom.

Then, we set the y coordinate of the @gold_window: if the message window's y coordinate is greater than 0, we set the gold window's to 0 (in other words, the message window isn't on the top of the screen so that's where the gold window goes); otherwise, we set it to the height of the game window minus the height of the gold window (meaning if the message window is at the top or in the middle, the gold window will be at the bottom).

def process_all_text
    open_and_wait
    text = convert_escape_characters($game_message.all_text)
    pos = {}
    new_page(text, pos)
    process_character(text.slice!(0, 1), text, pos) until text.empty?
  end


This method is what processes the text, as the name might suggest. First we call open_and_wait, a method we'll see after this one. We set a variable called text to the result of calling convert_escape_characters, passing in the all_text property of $game_message as the argument. We covered convert_escape_characters way back in the Window_Base days; if you've forgotten, it basically converts text codes into their relevant variable values, actor names, party member names etc. and returns the text with those substitutions.

Then we iniitalise a hashmap called pos, which starts out empty. We call the new_page method, passing in text and pos as arguments. This will become clearer soon.

After that, we call the process_character method (again, we looked at this back when we covered Window_Base). If you've forgotten, it takes three parameters: c, text and pos. c is the character being processed, text is a character string buffer, and pos is the draw position (with x, y, new x and height).

What's interesting here is that first argument: text.slice!(0, 1). What this does is returns the first character of text, and cuts it off of the string. So if our text is "This is a message." the first time this runs we slice off the T, resulting in text now being "his is a message." which is what gets passed as the second argument. The next time it runs, we'll slice off the "h" and text will now be "is is a message." and so on and so forth. In Ruby, when you see a method that ends with !, conventionally it means that whatever it's doing will act on the object that calls it. If we just did text.slice(0, 1), we'd slice off the first character and return it, but text would remain unchanged.

That process_character call is repeated in an infinite loop until the empty? method of text returns true, which will be the case once slice! has removed the final character of your message.

def process_input
    if $game_message.choice?
      input_choice
    elsif $game_message.num_input?
      input_number
    elsif $game_message.item_choice?
      input_item
    else
      input_pause unless @pause_skip
    end
  end


This method processes input. So if the choice? method of $game_message returns true (meaning we've set up choices for the player) we call input_choice. Otherwise, if num_input? returns true (meaning the player needs to enter a number), we call input_number. Otherwise, if item_choice? returns true (if we've set up a key item choice) we call input_item. Otherwise, we call input_pause unless the @pause_skip flag is true. (if it isn't, we won't pause input and the message will just continue processing by itself, which is how we get "autorun" message boxes)

def open_and_wait
    open
    Fiber.yield until open?
  end


open_and_wait is the method we called at the beginning of process_all_text. First we call the open method, which as you probably forgot from Window_Base, sets @opening to true, which is what causes update_open to start being called, which gradually increases the openness value of the window. We call Fiber.yield until the open? method returns true so that processing won't continue in process_all_text until the window is fully open.

def close_and_wait
    close
    Fiber.yield until all_close?
  end


This method does the same thing, but for closing the window instead of opening it. Instead of open? we're using all_close? as the loop termination trigger, which we'll look at next.

def all_close?
    close? && @choice_window.close? &&
    @number_window.close? && @item_window.close?
  end


And now we see why. In addition to close?, we don't want to return true in this method unless the close? methods of @choice_window, @number_window and @item_window also return true, which means all relevant windows are fully closed.

def text_continue?
    $game_message.has_text? && !settings_changed?
  end


The text_continue? method determines whether to continue displaying text, and returns true if $game_message's has_text? method returns true and settings_changed? returns false. This is another new method that we'll look at next.

def settings_changed?
    @background != $game_message.background ||
    @position != $game_message.position
  end


settings_changed? will return true if @background is not equal to the background property of $game_message OR @position is not equal to the position property, and false otherwise. This checks whether the developer has set different options for a later message window, meaning it will end up showing as a separate message rather than continuing smoothly.

def wait(duration)
    duration.times { Fiber.yield }
  end


The wait method takes one parameter, duration, which is measured in frames. We call the times method on whatever number is passed in, and run a block which consists only of Fiber.yield. Since this yields back to update_fiber, which is called every frame and just resumes the fiber that yielded to it, effectively this means we'll do nothing for "duration" frames, which is how the message gets paused.

def update_show_fast
    @show_fast = true if Input.trigger?(:C)
  end


This method updates the @show_fast flag, and sets it to true if the player has pressed the :C key (which defaults to enter).

def wait_for_one_character
    update_show_fast
    Fiber.yield unless @show_fast || @line_show_fast
  end


The wait_for_one_character method is called after processing each character; first we call update_show_fast, then call Fiber.yield unless the @show_fast or @line_show_fast flags are true. Effectively, this pauses processing for 1 frame per character, resulting in the text being "written" rather than all shown on screen at once. The flags prevent this frame of pause, which will either show the entire text in one go or just the current line.

def new_page(text, pos)
    contents.clear
    draw_face($game_message.face_name, $game_message.face_index, 0, 0)
    reset_font_settings
    pos[:x] = new_line_x
    pos[:y] = 0
    pos[:new_x] = new_line_x
    pos[:height] = calc_line_height(text)
    clear_flags
  end


The new_page method takes two parameters: text and pos. As we've already seen and looked at, text is the string containing the text for the message, and pos is a hashmap containing the coordinates and height.

The first thing we do is clear the contents of the window, which removes any existing text. Then we call draw_face, passing in the face_name and face_index properties of $game_message for the graphic filename and face index, and 0/0 for the x and y coordinates. Then we call reset_font_settings, which is a Window_Base method we looked at many moons ago.

This is also where we set the different keys of the pos hashmap. So the :x key is set to new_line_x which we'll look at in a sec. The :y key is set to 0, as we start a new page at the top of the message window. :new_x is also set to new_line_x, and :height is set to the result of calling calc_line_height passing in text (another Window_Base method we've already covered). Finally, we call clear_flags, which we've already looked at.

Effectively, this clears the screen, draws the face graphic if one has been chosen, and sets up the text to start drawing at the top left of the window.

def new_line_x
    $game_message.face_name.empty? ? 0 : 112
  end


new_line_x is the method we've just been using to calculate :x and :new_x in the pos hashmap, and it's fairly simple. If the face_name property of $game_message is an empty string, we return 0. Otherwise, we return 112.

In other words, if no face is present the message will start displaying at the far left of the screen; if there is one, we shift the message over 112 pixels so that it doesn't overlap the face graphic.

def process_normal_character(c, pos)
    super
    wait_for_one_character
  end


This method processes a normal (non-special) character, taking as parameters c (the single character being processed) and pos, the position hashmap. First, we call the parent method (in this case, process_normal_character in Window_Base, which we already looked at) and then call wait_for_one_character, which again we've now covered.

In essence, this displays one character of the message, and then checks to see if it needs to just show the rest in one go based on whether the player presses enter or whether there was a \> command in the line.

def process_new_line(text, pos)
    @line_show_fast = false
    super
    if need_new_page?(text, pos)
      input_pause
      new_page(text, pos)
    end
  end


This method processes a new line, taking as parameters text (the remaining string) and pos (the positional hashmap). First we set @line_show_fast to false, so that if the previous line *was* being shown in the \> fast mode this one will go back to processing normally. Then we call the parent method, that is to say process_new_line in Window_Base. Then if need_new_page? returns true when we pass in text and pos, we call input_pause (a method we'll see soon) and then new_page, also passing in text and pos.

def need_new_page?(text, pos)
    pos[:y] + pos[:height] > contents.height && !text.empty?
  end


This method determines whether a new page is needed. We return true if the :y key plus :height key of pos results in a value greater than the height of the window contents (meaning there isn't enough space left in the window to show another line) AND the text is not yet empty. Otherwise we'll return false, obviously.

def process_new_page(text, pos)
    text.slice!(/^\n/)
    input_pause
    new_page(text, pos)
  end


This is the method that processes a new page and takes two parameters which are...you guessed it, text and pos. First we slice off any \n newline character from the beginning of text using a regex pattern (^ marks the beginning of the string), then call input_pause, and finally call new_page passing in text and pos.

def process_draw_icon(icon_index, pos)
    super
    wait_for_one_character
  end


This overload of process_draw_icon takes two parameters, icon_index and pos. First we call the parent method, then call wait_for_one_character. This essentially replicates the functionality of the original method but ensures that icons being drawn in a message follow the same character-per-frame "typewriter" effect as text has. Without this, drawing an icon would result in the character following it appearing pretty much simultaneously, which would be somewhat jarring.

def process_escape_character(code, text, pos)
    case code.upcase
    when '$'
      @gold_window.open
    when '.'
      wait(15)
    when '|'
      wait(60)
    when '!'
      input_pause
    when '>'
      @line_show_fast = true
    when '<'
      @line_show_fast = false
    when '^'
      @pause_skip = true
    else
      super
    end
  end


This overload of process_escape_character takes three parameters: code, text and pos. It adds a number of new functions the one in Window_Base didn't have, because there are several character sequences you can use in a Show Text command that don't do anything in other kinds of window.

We have a case statement using code.upcase (which converts code to its uppercase equivalent; useless for the specific codes here, but if we're using C for colours, I for icons etc. it means we won't run into problems if we forget to capitalise it in the message window itself)

When the code is \$, we call open on @gold_window, because we've just told it that we want to show the player's gold. This is normally used for things like inn prompts or if an NPC wants to sell you an item outwith the shop interface; not strictly necessary, but it's always useful to the player to know how much money they have in cases where they might need to lose some.

\. and \| create a 15-frame and 60-frame wait respectively, by simply calling the wait method and passing in the values. If you ever want to change those text codes to different wait times, or even add your own custom text code for a different wait length, you can edit/add them here and just modify the number of frames passed to the method. By default, this will pause for a quarter of a second and a second, since the engine runs at 60fps.

\! calls input_pause, meaning the message will stop when it hits this character and only resume when the player hits the :C key.

\> and \< control the @line_show_fast flag. Basically, once your message hits \> it will display all remaining characters in one go because it's no longer yielding to update_fiber every frame. That is unless and until it hits a \<, which turns this off and resumes the typewriter style of displaying text.

\^ sets @pause_skip to true, meaning from the point it hits this character on, the player no longer needs to hit a key to continue the message. This is useful for creating dialogue that's coordinated with music; your epic fight scene where the lines correspond with different musical cues falls a bit flat if the player just leaves the message there for ages without hitting anything.

In any other case, we call the parent method, which is the one from Window_Base that deals with the other codes we looked at years ago.

def input_pause
    self.pause = true
    wait(10)
    Fiber.yield until Input.trigger?(:B) || Input.trigger?(:C)
    Input.update
    self.pause = false
  end


We've seen input_pause a lot throughout this class, and now we finally see what it does. As the name suggests, it pauses input, waiting for the player to hit something.

First of all, we set self.pause to true, which causes the "continue" arrow to appear at the bottom of the window. Next, we call wait, passing in 10 as the argument for the number of frames (1/6 of a second, basically). Then, we call Fiber.yield until the player hits :B or :C (esc or enter by default). Finally, we call the update method of the Input module (a step I don't think is actually needed as it doesn't appear to accomplish anything) and set self.pause to false to remove the continue arrow.

def input_choice
    @choice_window.start
    Fiber.yield while @choice_window.active
  end


input_choice is the method called in process_input when $game_message.choice? is true, as we saw earlier. It calls the start method of @choice_window (which opens and activates it) and then called Fiber.yield while the choice window is active. This is necessary because otherwise, the next message after the choice will be displayed at the same time as the choices are, and the engine will crash when trying to call the branch for your choice because the interpreter will already have moved past it. We need to keep pausing the fiber for each frame the player is making choices to prevent this.

def input_number
    @number_window.start
    Fiber.yield while @number_window.active
  end


Second verse, same as the first. This is exactly the same as above but for @number_window.

def input_item
    @item_window.start
    Fiber.yield while @item_window.active
  end


And completing the trifecta is exactly the same thing for @item_window.

We've done it! We've tackled the behemoth that is the message window. Hopefully now you understand better how it ticks and what makes it do so.

Window_ScrollText

We'll finish off this edition with the window that displays scrolling text. This one also inherits from Window_Base.

def initialize
    super(0, 0, Graphics.width, Graphics.height)
    self.opacity = 0
    self.arrows_visible = false
    hide
  end


The constructor doesn't do much new. We call the parent method passing in coordinates of (0, 0) and the full width/height of the game window, set self.opacity to 0 so the window has no background, and set self.arrows_visible to false to avoid any scroll arrows showing. This is necessary because with the text starting below the bottom of the screen, by default there would be an arrow displayed on the top/bottom as long as there was text above/below that wasn't fitting in the window. Finally, we hide the window, because we don't want it to be visible during the game unless we're processing a scrolling text command.

def update
    super
    if $game_message.scroll_mode
      update_message if @text
      start_message if !@text && $game_message.has_text?
    end
  end


The update method first calls the parent method, as most update methods do. Then if the scroll_mode property of $game_message is true (which as you've probably forgotten from when we looked at Game_Interpreter, will be the case after a scrolling text command) we call update_message if @text contains data, and start_message if @text is nil and the has_text? method of $game_message returns true. (meaning we have text to display but haven't yet set it up in the scrolling text window)

def start_message
    @text = $game_message.all_text
    refresh
    show
  end


start_message is the method we called above. It sets @text to the return value of the all_text method of $game_message (which returns all lines separated by newline \n characters), then calls the refresh and show methods.

def refresh
    reset_font_settings
    update_all_text_height
    create_contents
    draw_text_ex(4, 0, @text)
    self.oy = @scroll_pos = -height
  end


The overwritten refresh method calls reset_font_settings, update_all_text_height, create_contents and draw_text_ex (passing in 4, 0 and @text as the arguments for the x, y and text parameters). Three of those we've seen before, the next we'll look at in a second. Then we set self.oy and @scroll_pos to the inverse of the height of the window. (meaning the contents will originate below the window, off-screen)

def update_all_text_height
    @all_text_height = 1
    convert_escape_characters(@text).each_line do |line|
      @all_text_height += calc_line_height(line, false)
    end
    reset_font_settings
  end


update_all_text_height is the method from refresh that we haven't come across yet, so let's dissect it.

First, we set @all_text_height to 1. Easy enough.

each_line is an iteration method of strings that we haven't seen before; mainly because this is the only line in the default scripts that uses it.

So basically we're calling convert_escape_characters, passing in @text, then iterating through each line of the converted text and storing the current iteration in the variable "line". Then in the block, we add to @all_text_height the result of calling calc_line_height passing in line and false for the arguments to the parameters "text" and "restore_font_size", which usually defaults to true. In this case though, we want to preserve font size changes throughout the text without reverting it since it's all being shown in one go rather than in separate windows.

After the block, we call reset_font_settings.

def contents_height
    @all_text_height ? @all_text_height : super
  end


This overwrite of the contents_height method returns the value of @all_text_height if the variable exists, and the result of calling the parent method otherwise. (this avoids the game crashing when the window is first created but before it has been refreshed, since @all_text_height doesn't exist until start_message is called)

def update_message
    @scroll_pos += scroll_speed
    self.oy = @scroll_pos
    terminate_message if @scroll_pos >= contents.height
  end


The update_message method is what causes the actual scroll to occur.

So first we increment @scroll_pos by the result of calling scroll_speed, which we'll look at in a sec. Then, we set self.oy to @scroll_pos, which updates the contents origin to be that many pixels higher (thus the message moves higher on the screen). Finally, we call terminate_message if @scroll_pos is greater than or equal to the height of the window contents, because if that's the case the message is completely off the screen now.

def scroll_speed
    $game_message.scroll_speed * (show_fast? ? 1.0 : 0.5)
  end


This method determines how many pixels to move the message per frame. The return value is the scroll speed defined in the event command, multiplied by 1 if show_fast? is true, and 0.5 otherwise.

Effectively what this means is that if you set a speed of "4", the message will move 2 pixels per frame, unless you hold :A or :C and then it'll move at 4 pixels per frame. Or in other words, the speed you set in the editor is the *fast forward* speed, not the normal speed it'll move at. Because of this, when using the scrolling text event command you should set the number at twice the number of pixels you want it to move at without player input.

def show_fast?
    !$game_message.scroll_no_fast && (Input.press?(:A) || Input.press?(:C))
  end


This method determines whether to scroll fast or not. The return value is the inverse of the scroll_no_fast flag of $game_message (which will be true if the player cannot fast forward, meaning we only want to return true here if it's false) AND the player is either holding the :A key OR the :C key, which by default are shift and enter.

def terminate_message
    @text = nil
    $game_message.clear
    hide
  end


terminate_message is the method that's called once the scrolled message has passed beyond the top of the screen.

First we set @text to nil, as there's no need to retain a reference to it (keeping blocks of text in memory would potentially cause some amount of lag during gameplay when it isn't being displayed), then we call the clear method of $game_message to clear it out as well since it's no longer needed, and finally we hide the scrolling text window as there's no longer a need to have it on screen.

And that's it! We've covered a lot of ground, especially for the end of a 3-year hiatus, so I'll leave it at that. We'll kick off next time by breaking down Window_MapName and see how many classes we can run through before it starts getting unwieldy. Until next time!