SLIP INTO RUBY - UNDER THE HOOD PART 16: BO (WINDOW_)SELECTA(BLE)!

In which we demystify commands.

  • Trihan
  • 02/23/2017 11:02 PM
  • 7962 views
Hot off the press comes another exciting episode of



Now that we've gotten the base window class out of the way, it's time to learn the inner workings of the windows we spend the most time in during a game--the ones where you pick stuff from lists. It's time, gentle readers, to delve into Window_Selectable.

Window_Selectable

This class is the one that handles cursor movement, scrolling etc. First let's take a look at its public instance variables:

attr_reader   :index                    # cursor position
  attr_reader   :help_window              # help window
  attr_accessor :cursor_fix               # fix cursor flag
  attr_accessor :cursor_all               # select all cursors flag


Okay, so we've got :index, which tracks the cursor's current position; :help_window, which determines the window object considered to be the window with explanatory text for what's happening in the selectable window; a flag for whether the cursor is movable; and a flag for whether the cursor will cover all of the items in the list.

def initialize(x, y, width, height)
    super
    @index = -1
    @handler = {}
    @cursor_fix = false
    @cursor_all = false
    update_padding
    deactivate
  end


In the constructor, first we call the super constructor (Window_Base's), then we initialise @index at -1 (because 0 would be the first item and to begin with we want to have no item selected), @handler to an empty hash, and @cursor_fix/@cursor_all to false. Then we update the padding, and deactivate the window so the cursor doesn't appear in it yet.

def col_max
    return 1
  end


This method determines the maximum number of columns in the window. It's hardcoded as 1 by default, though child window classes will overwrite it depending on their own column needs.

def spacing
    return 32
  end


This method determines the number of pixels between items arranged side by side, and is hardcoded at 32 by default.

def item_max
    return 0
  end


This method determines the maximum number of displayable items in the window. It's hardcoded at 0 by default, and this will obviously be overwritten by child classes.

def item_width
    (width - standard_padding * 2 + spacing) / col_max - spacing
  end


This method determines the width of an item in the list. Returns the width of the window minus double the standard padding plus the amount of space between items, divided by the maximum number of columns minus space.

For example, in a 300-pixel-wide window with 2 max columns, item width will be (300 - 12 * 2 + 32) / 2 - 32 = 308 / 2 - 32 = 122 pixels. We add the spacing before division to ensure that it's taken into account, otherwise the items end up too wide when placed beside each other.

def item_height
    line_height
  end


This method determines the height of an item, which by default is just the same height as a line of text.

def row_max
    [(item_max + col_max - 1) / col_max, 1].max
  end


This method determines the maximum number of rows, and is returned as either (the max items plus the max columns minus 1) divided by the max columns, or 1, whichever is greater.

For example, in a window with 30 max items and 2 columns, row_max will be [(30 + 2 - 1) / 2, 1].max = [31 / 2, 1].max = [15, 1].max = 15.

def contents_height
    [super - super % item_height, row_max * item_height].max
  end


This method determines the height of the window contents. Returns either the base height minus base height mod item height, or max rows multiplied by item height, whichever is greater.

For example, in a 100x100 window with 15 max rows, contents_height will be [76 - 76 % 24, 15 * 24].max = [72, 360].max = 360 pixels. Obviously this means the contents are larger than the window itself: think of the window as a "viewport" for the contents.

def update_padding
    super
    update_padding_bottom
  end


The overwrite for update_padding first calls the parent method, and then we update the window's bottom padding.

def update_padding_bottom
    surplus = (height - standard_padding * 2) % item_height
    self.padding_bottom = padding + surplus
  end


To update the bottom padding, we calculate surplus as the height of the window minus double the standard padding mod the item height, then add surplus to its padding property.

Again taking a 100x100 window, this will result in surplus being (100 - 12 * 2) % 24 = (76) % 24 = 4, so we'll have 4 additional pixels of padding for a total of 16. This ensures that the cursor doesn't end up getting in the way of the window contents.

def height=(height)
    super
    update_padding
  end


This method is a setter for window height, taking height as a parameter. We call the parent height setter, and then update padding.

def active=(active)
    super
    update_cursor
    call_update_help
  end


This method is a setter for the active property, taking active as a parameter. We call the parent active setter, then update the cursor and update the help window.

def select(index)
    self.index = index if index
  end


This method selects an item from the window, taking index as a parameter. We set the window's index property to the passed-in value if a non-nil value was passed in.

def unselect
    self.index = -1
  end


This method deselects the item in the window, and simply sets the window's index property to -1.

def row
    index / col_max
  end


This method gets the current row, returned as index divided by maximum columns. So for example if we had item #5 selected in a 2-column window, it would be 4 / 2 = 2 (which is the third row as it's 0-indexed).

def top_row
    oy / item_height
  end


This method gets the top row of the window, returned as oy (the y coordinate of the contents starting point) divided by item height.

def top_row=(row)
    row = 0 if row < 0
    row = row_max - 1 if row > row_max - 1
    self.oy = row * item_height
  end


This is a setter method for top row, taking row as a parameter. If a negative value was passed in, we set row to 0 instead. If row is greater than 1 less than the maximum rows, we set row to 1 less than maximum rows instead (because anything higher would cause an array out of bounds error). Then we set the window's oy property to row multiplied by item height.

So if we set the top row to 4 in a 15-row window, for example, oy would be set to 4 * 24 = 96.

def page_row_max
    (height - padding - padding_bottom) / item_height
  end


This method gets the number of rows that can be displayed on a single page, and is returned as (window height minus padding minus bottom padding) divided by item height.

Taking our ever-popular 100x100 window, this results in (100 - 12 - 16) / 24 = 72 / 24 = 3. A 100x100 window can display 3 rows at a time (it's not particularly large).

def page_item_max
    page_row_max * col_max
  end


This method gets the number of items that can be displayed on a single page, and is returned as the maximum number of rows per page multiplied by the maximum columns. If we have 3 max rows and 2 columns, we can display 3 * 2 = 6 items. One of the simpler calculations here.

def horizontal?
    page_row_max == 1
  end


This method determines whether the window selection is horizontal; will return true of the maximum rows per page is 1, and false otherwise.

def bottom_row
    top_row + page_row_max - 1
  end


This method gets the bottom row of the window, returned as the top row plus 1 less than the maximum rows. So if the top row is 4 and the window displays 10 rows per page, bottom_row will be 4 + 10 - 1 = 13.

def bottom_row=(row)
    self.top_row = row - (page_row_max - 1)
  end


This method sets the bottom row, taking row as a parameter. We set the top row of the window to the passed-in row minus (1 less than rows per page). So if we wanted to set the bottom row to row 9 in a window that had 6 rows per page, top_row would be set to 9 - (6 - 1) = 9 - 5 = 4.

def item_rect(index)
    rect = Rect.new
    rect.width = item_width
    rect.height = item_height
    rect.x = index % col_max * (item_width + spacing)
    rect.y = index / col_max * item_height
    rect
  end


This method gets the rectangle required for drawing items (not to be confused with the cursor, this is the space that is allotted for drawing the item itself on the screen). It takes index as a parameter.

We create a new instance of Rect and assign it to the variable 'rect'. Its width is set to the calculated width of an item, and its height to the calculated item height. Its x coordinate is set to the passed-in index mod maximum columns multiplied by (item width plus spacing) and its y coordinate to the passed-in index divided by maximum columns multiplied by item height. Finally, we return rect.

So let's say we want the item_rect for index 9 in a 2-column layout of a 200x300 window. x will be 9 % 2 * (72 + 32) = 1 * (104) = 104, and y will be 9 / 2 * 24 = 96.

def item_rect_for_text(index)
    rect = item_rect(index)
    rect.x += 4
    rect.width -= 8
    rect
  end


This method gets the rectangle required for drawing a piece of text, taking index as a parameter. We set rect to the return value of item_rect passing in index, then add 4 to its x coordinate and reduce its width by 8. Finally, we return rect.

def help_window=(help_window)
    @help_window = help_window
    call_update_help
  end


This method sets the help window associated with the instance, taking help_window as a parameter. The instance variable @help_window is set to the passed-in object, and then we check whether the help needs to be updated.

def set_handler(symbol, method)
    @handler[symbol] = method
  end


This method sets the handler method that will be called when a particular item is selected, taking symbol and method as parameters. This sets the key corresponding to 'symbol' in the @handler hash to the passed-in method.

For example, if I call set_hander(:item, item_use) then @handler will be set to item_use, and subsequently when :item's handler is called, that's the method that will be processed. We'll see how this works in more detail shortly.

def handle?(symbol)
    @handler.include?(symbol)
  end


This method checks whether a given symbol has a handler set, by using Ruby's built-in include? method for collections on @handler passing in the symbol as an argument. This will return true if the symbol is present in the hash, or false otherwise.

def call_handler(symbol)
    @handler[symbol].call if handle?(symbol)
  end


This method calls a given symbol's handler method by using the call method on the key of @handler corresponding to the passed-in symbol, but only if that symbol has a handler set (as otherwise we'd be trying to call a nonexistent method, which would cause an error).

def cursor_movable?
    active && open? && !@cursor_fix && !@cursor_all && item_max > 0
  end


This method determines whether the cursor can be moved in the window; returns true if the window is active AND the window is open AND the cursor is not fixed AND the cursor doesn't cover all items AND the maximum number of items is greater than 0.

def cursor_down(wrap = false)
    if index < item_max - col_max || (wrap && col_max == 1)
      select((index + col_max) % item_max)
    end
  end


This method moves the cursor down one item, taking an optional parameter, wrap, which defaults to false.

If the current index is less than the maximum number of items less the maximum columns, OR wrap is true and the maximum number of columns is 1, we select the item with index (current index plus max columns) mod max items.

Let's take a window with 10 items and 2 columns, currently at index 6.

If 6 is less than 8 (it is)...we select (6 + 2) % 10 = 8 % 10 = 8. This is correct because with 2 columns, moving down is actually advancing by 2 items instead of 1.

Taking a 1-column, 10-item window currently at index 9, the second condition will return true and so we'll select (9 + 1) % 10 = 10 % 10 = 0, so we'll end up back at the top of the list. This is how wrapping works, generally.

def cursor_up(wrap = false)
    if index >= col_max || (wrap && col_max == 1)
      select((index - col_max + item_max) % item_max)
    end
  end


Same thing for moving the cursor up; obviously we're checking whether index is greater than or equal to max columns so that we can't move up if we're on the top row (unless col_max is 1 and wrap is true) and we're subtracting from index rather than adding for the selection.

def cursor_right(wrap = false)
    if col_max >= 2 && (index < item_max - 1 || (wrap && horizontal?))
      select((index + 1) % item_max)
    end
  end


Not much different for moving the cursor right, really. If the max columns is greater than or equal to 2 AND (index is less than max items minus 1 OR (wrap is true and the list is horizontal)) then we select index (current index plus 1) mod max items.

The col_max >= 2 check is obvious; you can't move right if you only have one column. Checking that index is less than max items minus 1 is to prevent processing a move right if we're already on the last item (again, unless the list is horizontal and wrap is true).

Selecting index + 1 is self-explanatory, and we mod by item max so that if we're wrapping the selection returns to the first item.

def cursor_left(wrap = false)
    if col_max >= 2 && (index > 0 || (wrap && horizontal?))
      select((index - 1 + item_max) % item_max)
    end
  end


Pretty much the same thing for moving left, just the other way round.

def cursor_pagedown
    if top_row + page_row_max < row_max
      self.top_row += page_row_max
      select([@index + page_item_max, item_max - 1].min)
    end
  end


This method moves the cursor down a full page.

If adding the top row number to the maximum number of rows per page results in a value less than the maximum number of rows: we add the maximum number of rows per page to the top row value, and then select either the current index plus maximum items per page, or max items minus 1, whichever is smaller.

Okay, so let's say we're currently scrolled down 1 row in a 2-column window that can show 15 rows per page, with 62 items in it, and we have the second item selected.

if 1 + 15 < 31 (it is):
top_row += 15 (so it's now 16)
select([1 + 30, 62 - 1].min) (returns 31 as it's less than 61)

So we'll end up selecting item 31, which is the second item of the second page.

def cursor_pageup
    if top_row > 0
      self.top_row -= page_row_max
      select([@index - page_item_max, 0].max)
    end
  end


Moving the page up is pretty much just the opposite of the previous method, not much to say there.

def update
    super
    process_cursor_move
    process_handling
  end


The frame update method calls its parent update method, then processes cursor movement and handlers for other inputs like ok/cancel etc.

def process_cursor_move
    return unless cursor_movable?
    last_index = @index
    cursor_down (Input.trigger?(:DOWN))  if Input.repeat?(:DOWN)
    cursor_up   (Input.trigger?(:UP))    if Input.repeat?(:UP)
    cursor_right(Input.trigger?(:RIGHT)) if Input.repeat?(:RIGHT)
    cursor_left (Input.trigger?(:LEFT))  if Input.repeat?(:LEFT)
    cursor_pagedown   if !handle?(:pagedown) && Input.trigger?(:R)
    cursor_pageup     if !handle?(:pageup)   && Input.trigger?(:L)
    Sound.play_cursor if @index != last_index
  end


This is the method that processes cursor movement when you're using movement input.

We return unless the cursor is movable, because if it isn't you can't move it. last_index is set to @index so that we can track whether it's moved after the cursor processing. If the player is pressing the :DOWN, :UP, :RIGHT or :LEFT keys, we call the appropriate cursor movement method passing in the trigger? method for that key as the argument to the "wrap" parameter. This will allow wrapping as long as the player used a single tap (it won't wrap if you're holding it). If the player is pressing :R or :L AND the window has no specified handlers for the :pagedown/:pageup symbols, we call the appropriate paging method. Finally, if @index is not equal to the last index, meaning we've moved items in some direction, we play the cursor sound effect.

def process_handling
    return unless open? && active
    return process_ok       if ok_enabled?        && Input.trigger?(:C)
    return process_cancel   if cancel_enabled?    && Input.trigger?(:B)
    return process_pagedown if handle?(:pagedown) && Input.trigger?(:R)
    return process_pageup   if handle?(:pageup)   && Input.trigger?(:L)
  end


This method handles processing for handlers like OK and cancel.

We return unless the window is open and active, because otherwise we don't want to process input from it. We return process_ok if OK is enabled and the player is pressing the :C key (enter by default), process_cancel if cancel is enabled and the player is pressing the :B key (escape by default), process_pagedown if there's a :pagedown handler for the window and the player is pressing the :R key, and process_pageup if there's a :pageup handler and the player is pressing :L.

def ok_enabled?
    handle?(:ok)
  end


This method determines whether the OK handler is enabled for the window, and simply returns the result of a check for :ok being a key in the handler hash.

def cancel_enabled?
    handle?(:cancel)
  end


Same thing but for cancel.

def process_ok
    if current_item_enabled?
      Sound.play_ok
      Input.update
      deactivate
      call_ok_handler
    else
      Sound.play_buzzer
    end
  end


This method is processed when the player presses the OK key. If the currently-highlighted item is enabled, we play the "OK" sound effect from the database, update the input data, deactivate the window, and call the ok handler. If the item is not enabled, we play the "Buzzer" sound.

def call_ok_handler
    call_handler(:ok)
  end


This method calls the ok handler by calling call_handler with :ok as an argument. This didn't really need a separate method IMO but who am I to judge?

def process_cancel
    Sound.play_cancel
    Input.update
    deactivate
    call_cancel_handler
  end


This method is processed when the player pressed the cancel key. Plays the "Cancel" sound effect, updates input data, deactivates the window, and calls call_cancel_handler...

def call_cancel_handler
    call_handler(:cancel)
  end


...which calls call_handler passing in :cancel. Which, again, could easily have been done as part of process_cancel.

def process_pageup
    Sound.play_cursor
    Input.update
    deactivate
    call_handler(:pageup)
  end


Exactly the same thing for processing page up, only we play the cursor sound and obviously the handler is :pageup.

def process_pagedown
    Sound.play_cursor
    Input.update
    deactivate
    call_handler(:pagedown)
  end


And the same again for page down.

def update_cursor
    if @cursor_all
      cursor_rect.set(0, 0, contents.width, row_max * item_height)
      self.top_row = 0
    elsif @index < 0
      cursor_rect.empty
    else
      ensure_cursor_visible
      cursor_rect.set(item_rect(@index))
    end
  end


This method updates the cursor.

If the cursor should be shown on all selectable items, we set the cursor rect to start at (0, 0), with a width as wide as the contents of the window, and a height of the max rows multiplied by item height. This will put a selection cursor on the entire window, basically (which is what happens with skills that heal all allies in the menu for example). The top_row property is set to 0; when you're selecting everything you want to start at the top.

Otherwise, if the index is less than 0, we empty the cursor rect. This is why when you set a selectable window index to -1, it's considered to have selected "nothing".

Otherwise, we make sure the cursor is visible by scrolling to the row our selected item is in, and then set the cursor rect to an item rect for the selected index.

def ensure_cursor_visible
    self.top_row = row if row < top_row
    self.bottom_row = row if row > bottom_row
  end


This method scrolls the cursor to ensure its visibility. We set top_row to the selected row property if it's less than the top, and bottom_row to the selected row if it's greater than the bottom row.

def call_update_help
    update_help if active && @help_window
  end


This method determines whether the help window needs to be updated. Calls the update_help method if the window is active and it has a help window defined.

def update_help
    @help_window.clear
  end


As this is just the base selectable window class, and child classes will implement their own versions, this update_help method simply clears the help window.

def current_item_enabled?
    return true
  end


This method determines whether the selected item is "enabled" that is to say you can press the ok key on it and it'll do something. Returns true by default.

def draw_all_items
    item_max.times {|i| draw_item(i) }
  end


This method is just a wrapped to a variable number of calls to draw_item. We start a loop with a number of iterations equal to the number of displayable items with iteration variable "i", and call draw_item in each iteration passing in i as the index.

def draw_item(index)
  end


This method draws an item. It's blank by default as child classes will implement their own item drawing methods.

def clear_item(index)
    contents.clear_rect(item_rect(index))
  end


This method erases an item from the window, taking index as a parameter. We call the clear_rect method of contents, passing in the item rect for the index.

def redraw_item(index)
    clear_item(index) if index >= 0
    draw_item(index)  if index >= 0
  end


The method for redrawing an item takes index as a parameter. We clear the item at index if index is greater than or equal to 0, then draw the item at index, also if index is greater than or equal to 0.

def redraw_current_item
    redraw_item(@index)
  end


This method redraws the currently-selected item by calling redraw_item with @index as the argument.

def refresh
    contents.clear
    draw_all_items
  end


The refresh method (usually called in response to input) clears the contents and then draws all items.

Window_Command

This class handles windows with general command choices, such as you'd see on the title screen and menu. It has no public instance variables.

def initialize(x, y)
    clear_command_list
    make_command_list
    super(x, y, window_width, window_height)
    refresh
    select(0)
    activate
  end


The constructor takes x and y as parameters. First we clear the command list and remake it, then we call the parent class constructor passing in the arguments sent to x and y, window width and window height. Then we refresh the window, select the first item and activate it.

def window_width
    return 160
  end


The width of a Window_Command is hardcoded at 160.

def window_height
    fitting_height(visible_line_number)
  end


The window height is defined as the number of pixels needed to fit the number of visible lines.

def visible_line_number
    item_max
  end


And the number of visible lines is defined as the maximum number of items!

def item_max
    @list.size
  end


Aaaand the maximum number of items is defined as the size of the @list variable, which contains the list of commands the window will have.

def clear_command_list
    @list = []
  end


This method clears the command list, and simply sets the list variable to an empty array.

def add_command(name, symbol, enabled = true, ext = nil)
    @list.push({:name=>name, :symbol=>symbol, :enabled=>enabled, :ext=>ext})
  end


This is the method that actually adds commands to the window. It takes four parameters: a name for the command, a symbol to associate with it, a flag to determine whether it's enabled/disabled, and extended data, which is optional.

The code for the method itself is pretty simple; we push a hash to @list with the keys :name, :symbol, :enabled and :ext, which are assigned the values of the corresponding parameters.

def command_name(index)
    @list[index][:name]
  end


This method gets the name of a command, taking index as a parameter. It returns the :name key of the element of @list at the value of index.

def command_enabled?(index)
    @list[index][:enabled]
  end


This method gets whether the command is enabled. Pretty much the same thing as before but returning :enabled instead.

def current_data
    index >= 0 ? @list[index] : nil
  end


This method gets the data for the current selection. If the current index is greater than or equal to 0, we return the element of @list at the value of index, otherwise we return nil.

def current_item_enabled?
    current_data ? current_data[:enabled] : false
  end


This overwrite to the base method gets whether the current selection is enabled. If current_data returns a value, we return its :enabled key. Otherwise, we return false.

def current_symbol
    current_data ? current_data[:symbol] : nil
  end


This method gets the symbol of the current data. If current_data returns a value, we return its :symbol key. Otherwise, we return nil.

def current_ext
    current_data ? current_data[:ext] : nil
  end


This method gets the extended data of the selected item. If current_data returns a value, we return its :ext key. Otherwise, we return nil.

def select_symbol(symbol)
    @list.each_index {|i| select(i) if @list[i][:symbol] == symbol }
  end


This method selects a specific symbol, and obviously takes symbol as a parameter.

We iterate through the index of each element in @list with the iteration variable 'i'. In the block, we call select passing in i if its corresponding symbol in @list matches the passed-in symbol.

That may sound a bit confusing, so let's take an example. The actor command window uses :item as the symbol for the "Item" command, so in order to select it we would use select_symbol(:item).

The block would then look like this:

|i| select(i) if @list[i][:symbol] == :item


In the first iteration, our index is 0. @list will return :attack, as "Attack" is the first command in the actor command list. We'll then have a :skill symbol for however many skill type commands the actor has. Then we'll have :guard, and finally it'll get to :item. The condition will be met at this point, so we'll call select on whatever the index of the :item command is, and subsequently the item command will be selected.

def select_ext(ext)
    @list.each_index {|i| select(i) if @list[i][:ext] == ext }
  end


This method selects the command with specified extended data. It's pretty much the same code as select_symbol but uses :ext instead. The default scripts only use this in one place: in the window for skill commands, each command has its skill type ID passed in as extended data when the command is created, and the method for restoring the previous selection position calls select_ext with the last-used skill's skill type ID as the argument. We'll see this when we look at Window_SkillCommand in about a thousand years.

def draw_item(index)
    change_color(normal_color, command_enabled?(index))
    draw_text(item_rect_for_text(index), command_name(index), alignment)
  end


This overwrite of the item drawing method takes index as a parameter. We change the text colour to normal_color, determining its enabled state on the result of calling command_enabled with the passed-in index as its argument. Then we call draw_text, passing in the rect for an item at the given index as the rect, the name of the command at index for the text to draw, and 'alignment' as the alignment, which we'll see in a second.

def alignment
    return 0
  end


The method for determining text alignment returns 0, which isn't actually necessary as it's the default for draw_text if no value is passed in for it anyway.

def ok_enabled?
    return true
  end


This method determines whether the 'ok' key will be enabled in the window. Returns true by default, child classes will overwrite this.

def call_ok_handler
    if handle?(current_symbol)
      call_handler(current_symbol)
    elsif handle?(:ok)
      super
    else
      activate
    end
  end


This method calls the 'ok' handler. If the current synbol has a handler, we call the associated method. Otherwise, if the :ok symbol has a handler, we call the call_ok_handler method of the parent class. Otherwise, we just activate the window.

def refresh
    clear_command_list
    make_command_list
    create_contents
    super
  end


In the refresh method, we clear the command list and remake it, then create the window contents and call the parent class's refresh method.

Window_HorzCommand

This class is a child of Window_Command which handles horizontal command windows.

def visible_line_number
    return 1
  end


The overwrite of visible_line_number returns 1, because in a horizontal window we obviously only want 1 line.

def col_max
    return 4
  end


The overwrite of col_max returns 4, meaning a horizontal command window is hardcoded to have a maximum of 4 items visible at a time (4 columns on 1 line).

def spacing
    return 8
  end


The overwrite to the spacing method returns 8, meaning there will be 8 pixels between side-by-side items.

def contents_width
    (item_width + spacing) * item_max - spacing
  end


The overwrite to contents_width returns (item width plus spacing) multiplied by maximum items minus spacing. This will basically allot an area wide enough for all items with the appropriate spacing between them, subtracting spacing at the end so that the width doesn't end up overlapping the edge of the window.

def contents_height
    item_height
  end


The contents height for a horizontal command window is just returned as the height of an item.

def top_col
    ox / (item_width + spacing)
  end


This method determines the top column (or to put it more simply, which index is leftmost in the window) by returning the starting x coordinate of the window contents divided by (item width plus spacing).

def top_col=(col)
    col = 0 if col < 0
    col = col_max - 1 if col > col_max - 1
    self.ox = col * (item_width + spacing)
  end


This method sets the top column, taking col as a parameter. col is set to 0 if it's less than 0, then it's set to max columns minus 1 if it's greater than that. Finally, the window's ox is set to the column index multiplied by item width plus spacing.

def bottom_col
    top_col + col_max - 1
  end


This method determines the bottom column (the one on the far right) by returning the top column plus the maximum columns less 1.

def bottom_col=(col)
    self.top_col = col - (col_max - 1)
  end


This method sets the bottom column, taking col as a parameter. As there is no bottom_col property, we set the bottom column by setting tol_col to the passed-in value minus the max columns less 1. By setting the top column to this value, we're essentially making sure the bottom column is the number we specified.

def ensure_cursor_visible
    self.top_col = index if index < top_col
    self.bottom_col = index if index > bottom_col
  end


This overwrite of the parent ensure_cursor_visible method sets top_col to the current index if the index is less than top_col, and sets bottom_col to index if the index is greater than bottom_col.

def item_rect(index)
    rect = super
    rect.x = index * (item_width + spacing)
    rect.y = 0
    rect
  end


This overwrite of the parent item_rect method initially defines the same rect by calling the parent method, but then sets the rect x to index multiplied by item width plus spacing, and the y to 0, then returns rect. We do this because we need to account for item spacing in our rects, and Y isn't going to change because everything it on one line.

def alignment
    return 1
  end


This overwrite of the parent alignment method returns 1 instead of 0, as the horizontal command window will have its text centred in the rect.

def cursor_down(wrap = false)
  end


We need to rewrite cursor_down and replace it with an empty method so that the player can't use the down key.

def cursor_up(wrap = false)
  end


Same with cursor up...

def cursor_pagedown
  end


...and page down...

def cursor_pageup
  end


...and page up.

And that completes our breakdown of Window_Selectable, Window_Command and Window_HorzCommand! Most of the later ones, at least up to Window_NameInput, are quite light in content, so we should be able to cover a fair few of them in the next issue. Comments are, as always, welcome and encouraged.

Until next time.

Posts

Pages: 1
Shouldn't this be part 16? ;p
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
It totally says part 16 in the title, I have no idea what you're talking about.
Suuuuuuuuure you don't~ ;p
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
What edit?

Also while you're here I sure would appreciate a comment on the actual article content rather than pointing out I had the number wrong. :P
It would make more sense if I understood the jargon used a bit better (constructor, child window, etc) and it was dumbed down a bit more, but then I'm really bad when it comes to script comprehension, so don't take my word as law on this kind of thing.

I did understand some parts, though, so the clarity is decent enough that even a script-idiot like me can read it well enough. The writing is clear, too.

I'm more a fudge it til it works type person, who does the ol' prod it til it does something so...
^.^;

Sorry if that's not helpful.
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
Yeah, when this series first started I was really dumbing everything down to the point of being insulting, but now that we're (hopefully) a fair ways into the breakdown I'd like to think that anyone following along since the start will know terms like that like the back of their hands.

Though you have just brought up an interesting possibility I hadn't considered: there may be people who just start reading halfway through, so perhaps I should retain the newbie-friendly language I used in the beginning throughout the series.
Hey Trihan, long time no see.
I don't know if you remember my last post, but I'm here to tell that your ruby tutorial is doing wonders for me.
The reason I wasn't posting is because I've got an internship and... I'm working with rails!
By the way, keep the good work with your ruby tutorials, they're really helping me on my game.(Yes, I'm making one on my free time, and I'll publish here soon)
C ya!
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
Hey Shadow_Fox, thanks for the update. Glad to hear things are going well for you. :)
Pages: 1