SLIP INTO RUBY - UNDER THE HOOD PART 9: MAP MAP MAP

Map.

  • Trihan
  • 05/14/2015 03:42 PM
  • 5094 views
Hi, map fans! Today we're going to map some maps with the mapping map map. That's right, it's time for

Map into Map

By which I mean



But first, the answers to last week's exercises!

1. How would I get the exp value of enemy 51 in the database?

$data_enemies[51].exp

2. What would I change to make enemy drop rate x4 instead of x2 when the party has the double drop rate ability?

Change line 107 of Game_Enemy (inside the drop_item_rate method) from

$game_party.drop_item_double? ? 2 : 1

to

$game_party.drop_item_double? ? 4 : 1

3. What would I change if I wanted a unit's "agi" method to give the combined value instead of the average?

Change line 54 of Game_Unit (inside the agi method) from

members.inject(0) {|r, member| r += member.agi } / members.size

to

members.inject(0) {|r, member| r += member.agi }

4. How would I change the rate of preemptive attacks to give twice the chance of a preemptive if the party's agi is higher than or equal to the enemy's?

Change line 411 of Game_Party (inside the rate_preemptive method) from

(agi >= troop_agi ? 0.05 : 0.03) * (raise_preemptive? ? 4 : 1)

to

(agi >= troop_agi ? 0.1 : 0.03) * (raise_preemptive? ? 4 : 1)

5. How would I give plural enemies in battle numbers instead of letters?

Replace each letter in LETTER_TABLE_HALF or LETTER_TABLE_FULL of Game_Troop with its respective number.

6. What would I change in the scripts to make it so that every turn in battle, each enemy's ATK increases by 25%?

Add the following to the increase_turn method of Game_Troop:

alive_members.each { |member| member.add_param(2, member.atk * 1.25) }

And now, on with the show!

Game_Map

In case you couldn't tell, today's map is going to be all about maps, so let's map!

This class handles...maps. It's referenced by the global variable $game_map.

Map doesn't seem like a word any more. Let's look at some public instance variables!

attr_reader   :screen                   # map screen state
  attr_reader   :interpreter              # map event interpreter
  attr_reader   :events                   # events
  attr_reader   :display_x                # display X coordinate
  attr_reader   :display_y                # display Y coordinate
  attr_reader   :parallax_name            # parallax background filename
  attr_reader   :vehicles                 # vehicle
  attr_reader   :battleback1_name         # battle background (floor) filename
  attr_reader   :battleback2_name         # battle background (wall) filename
  attr_accessor :name_display             # map name display flag
  attr_accessor :need_refresh             # refresh request flag


These shouldn't need any explanation beyond the comments, but we'll see them all in action soon enough anyway.

def initialize
    @screen = Game_Screen.new
    @interpreter = Game_Interpreter.new
    @map_id = 0
    @events = {}
    @display_x = 0
    @display_y = 0
    create_vehicles
    @name_display = true
  end


Okay, so @screen is set to a new instance of Game_Screen, which we've seen before. A new interpreter is instanced, which we'll look at in the far-flung future. @map_id is initialised at

0, @events is an empty hash, @display_x and y are initialised at 0, create_vehicles is called, and @name_display is set to true. Note that if you don't want to show map names on your

maps even when they have one entered, you can set this to false (or just set it to off using the set map name display event command, but this is quicker).

def setup(map_id)
    @map_id = map_id
    @map = load_data(sprintf("Data/Map%03d.rvdata2", @map_id))
    @tileset_id = @map.tileset_id
    @display_x = 0
    @display_y = 0
    referesh_vehicles
    setup_events
    setup_scroll
    setup_parallax
    setup_battleback
    @need_refresh = false
  end


Setting up a map takes a map_id as a parameter and funnily enough the first thing it does is set @map_id to the passed-in value. We then set @map to the result of loading the data for

that map using sprintf to append the map ID to the filename. We set the @tileset_id to whatever tileset ID the map is using, initialise @display_x and y as 0, call referesh_vehicles, set up

events, set up the map scroll, set up parallax, set up battle background, and finally set @need_refresh to false since at this point we don't need to refresh the map (as you've seen,

several things set this back to true, which is essentially telling the map it needs to update its stuff).

def create_vehicles
    @vehicles = []
    @vehicles[0] = Game_Vehicle.new(:boat)
    @vehicles[1] = Game_Vehicle.new(:ship)
    @vehicles[2] = Game_Vehicle.new(:airship)
  end


This method creates the in-game vehicles. Sets @vehicles to an empty array, then sets its elements to a three new instances of Game_Vehicle, passing in the symbols :boat, :ship and

:airship. We'll see more about how this works when we look at Game_Vehicle in the coming weeks.

def referesh_vehicles
    @vehicles.each {|vehicle| vehicle.refresh }
  end


This method just iterates through the vehicles and calls the refresh method for each one, which, again, we'll look at more when we get to Game_Vehicle.

def vehicle(type)
    return @vehicles[0] if type == :boat
    return @vehicles[1] if type == :ship
    return @vehicles[2] if type == :airship
    return nil
  end


This method gets a particular vehicle with a given type. If we pass in :boat, it gives us the 0 element of @vehicles. We get index 1 for :ship, and index 2 for :airship.

def boat
    @vehicles[0]
  end


This is just a shorthand way of getting the 0 element of @vehicles. Not sure why the vehicle method didn't just have "return boat".

def ship
    @vehicles[1]
  end


Same thing for the ship.

def airship
    @vehicles[2]
  end


You guessed it! Same thing for the airship.

def setup_events
    @events = {}
    @map.events.each do |i, event|
      @events[i] = Game_Event.new(@map_id, event)
    end
    @common_events = parallel_common_events.collect do |common_event|
      Game_CommonEvent.new(common_event.id)
    end
    refresh_tile_events
  end


This method sets up the map events. Sets @events to a blank hash, then iterates through each event in @map. The block variables here are "i" and "event"; as the events property of

RPG::Map (which @map is an instance of) is also a hash, i represents the key and event represents the value. The editor is set up to store instances of RPG::Event with the event's ID as

the key and the event object itself as the value.

Inside the loop, we're setting key i of @events to a new instance of Game_Event, giving as arguments the map ID and the event object.

Next, we set @common_events to a collect on parallel_common_events (which we'll see in a second) and sets each element of the returned array to a new instance of

Game_CommonEvent with the current common event's ID as the argument.

Finally, we call refresh_tile_events, which we'll look at shortly.

def parallel_common_events
    $data_common_events.select {|event| event && event.parallel? }
  end


This is the method called above, and returns a select on $data_common_events with "event"; returns true if event has a value and event.parallel? returns true (which means it's a parallel

process common event).

def setup_scroll
    @scroll_direction = 2
    @scroll_rest = 0
    @scroll_speed = 4
  end


Initial setup for map scrolling. Initialises the direction at 2 (down), rest at 0, and speed at 4. Rest holds the distance left to scroll, and is used to check whether a scroll is still occurring.

def setup_parallax
    @parallax_name = @map.parallax_name
    @parallax_loop_x = @map.parallax_loop_x
    @parallax_loop_y = @map.parallax_loop_y
    @parallax_sx = @map.parallax_sx
    @parallax_sy = @map.parallax_sy
    @parallax_x = 0
    @parallax_y = 0
  end


This method sets up parallax backgrounds. Sets @parallax_name to the name of the parallax used on the map, likewise with @parallax_loop_x and y (which are determined by the

checkboxes for whether the map loops horizontally/vertically) and @parallax_sx and sy (which are the values set in the "auto scroll" boxes). @parallax_x and y are initialised at 0.

def setup_battleback
    if @map.specify_battleback
      @battleback1_name = @map.battleback1_name
      @battleback2_name = @map.battleback2_name
    else
      @battleback1_name = nil
      @battleback2_name = nil
    end
  end


This method sets up the battle background. If the "Specify Battleback" box has been checked, @battleback1_name and @battleback2_name are set to the two files chosen in the editor,

otherwise they're set to nil.

def set_display_pos(x, y)
    x = [0, [x, width - screen_tile_x].min].max unless loop_horizontal?
    y = [0, [y, height - screen_tile_y].min].max unless loop_vertical?
    @display_x = (x + width) % width
    @display_y = (y + height) % height
    @parallax_x = x
    @parallax_y = y
  end


This method sets the map's display position.

To set x, we take the minimum value between (x) and (width - screen_tile_x), then the maximum value between 0 and that value, unless the map is set to loop horizontally. screen_tile_x

and screen_tile_y return the number of tiles the screen shows horizontally and vertically.

We do exactly the same thing to set y, only using height, y and whether the map loops vertically.

Note that we only set X and Y is the map doesn't loop horizontally/vertically.

@display_x is set to the remainder of dividing (x + width) by the width of the map. In our example, this becomes (9 + 17) % 17, which is 9.

@display_y follows the same logic but again for y and height, which in our example is going to yield 8.

@parallax_x and @parallax_y are set to x and y respectively.

Time for another world-famous example! Let's take a 50x50 non-scrolling map. The player is at (30, 17). Bear in mind that when this method is called, the arguments it passes in subtract

the center X/Y coordinate from the player's. By default, the center X/Y is (8, 6) so when this method is called the X and Y passed in will be (22, 11)

Setting x, we take the minimum value between 22 and (50 - 17), which in this case is 22, then the maximum value between 0 and that value, so x = 22.

Setting y, we take the minimum value between 11 and (50 - 13), which in this case is 11, then the maximum value between 0 and that value, so y = 11.

@display_x is set to (22 + 50) % 50, which is 22.
@display_y is set to (11 + 50) % 50, which is 11.

@parallax_x and @parallax_y are set to 22 and 11 respectively.

After all this, (22,11) is the top-left display coordinate. Had the player been at or near a map edge preventing true centering of the camera, these equations would have compensated for

it.

def parallax_ox(bitmap)
    if @parallax_loop_x
      @parallax_x * 16
    else
      w1 = [bitmap.width - Graphics.width, 0].max
      w2 = [width * 32 - Graphics.width, 1].max
      @parallax_x * 16 * w1 / w2
    end
  end


This method calculates the X coordinate of the parallax display's origin, taking as its parameter the parallax's bitmap.

If the parallax is set to loop horizontally, @parallax_x is multiplied by 16 (so if we set autoscroll to "2", @parallax_x would be 32)

Otherwise, we set w1 to the maximum value between (the bitmap's width - Graphics.width) and 0, w2 to the maximum value between (the map's width * 32 - Graphics.width) and 1, and

returns @parallax_x multiplied by 16 multiplied by w1 divided by w2.

Let's say we're using the "BlueSky" parallax from the RTP, which is 544x544, with "Loop Horizontal" unchecked, on a 17x13 map.

w1 is set to the max between (544 - 544) and 0. As both equal 0, we don't have much choice there.
w2 is set to the max between (17 * 32 - 544) and 1, which is 1 as the first equates to 0.
The return value is 2 * 16 * 0 / 1 -> 32 * 0 = 0.

def parallax_oy(bitmap)
    if @parallax_loop_y
      @parallax_y * 16
    else
      h1 = [bitmap.height - Graphics.height, 0].max
      h2 = [height * 32 - Graphics.height, 1].max
      @parallax_y * 16 * h1 / h2
    end
  end


This does the same thing but for y and height instead of x and width.

def map_id
    @map_id
  end


Getter method for map ID.

def tileset
    $data_tilesets[@tileset_id]
  end


Getter method for the map's tileset. Returns the object that corresponds to the @tileset_id variable.

def display_name
    @map.display_name
  end


Getter for the map's display name.

def width
    @map.width
  end


Getter for the map's width.

def height
    @map.height
  end


Getter for the map's height.

def loop_horizontal?
    @map.scroll_type == 2 || @map.scroll_type == 3
  end


Determines whether the map is set to loop horizontally. Returns true if the scroll type is "Horizontal Loop" OR "Both Loop".

def loop_vertical?
    @map.scroll_type == 1 || @map.scroll_type == 3
  end


Determines whether the map is set to loop vertically. Returns true if the scroll type is "Vertical Loop" OR "Both Loop".

def disable_dash?
    @map.disable_dashing
  end


Determines whether dashing is disabled on the map. Returns true if the "Disable Dashing" checkbox has been checked.

def encounter_list
    @map.encounter_list
  end


Getter for the map's encounter list. The entries in this list are instances of RPG::Map::Encounter, which consists of a troop ID, weight, and array of region IDs the troop can be

encountered in.

It may be of interest to note that although the region ID specification in the map properties is limited to 3 IDs, you can use scripts to add more.

def encounter_step
    @map.encounter_step
  end


Getter for the map's "Steps Average", the average number of steps the player will take before a battle occurs.

def data
    @map.data
  end


Getter for the map's data. This is a 3-dimensional tile ID array (itself an instance of the built-in class Table). The Array class doesn't handle large amounts of data effectively, which is why

this class was included to deal with maps. The Table class as a 3-dimensional array has an X size, a Y size, and a Z size, which in this case corresponds to the map's width, the map's

height, and 4 layers of tile IDs.

def overworld?
    tileset.mode == 0
  end


This method determines whether the tileset is for the overworld ("field type" in the dropdown). It's used when working out battle backgrounds, as we'll see when we look at

Spriteset_Battle.

def screen_tile_x
    Graphics.width / 32
  end


This method gets the number of tiles along the X axis by dividing the screen width by 32. By default, as Graphics.width is 544, this gives 17 tiles along.

def screen_tile_y
    Graphics.height / 32
  end


The same thing for the number of tiles along the Y axis. As the default screen height is 416, returns 13 tiles down.

def adjust_x(x)
    if loop_horizontal? && x < @display_x - (width - screen_tile_x) / 2
      x - @display_x + @map.width
    else
      x - @display_x
    end
  end


This method calculates an adjusted X coordinate, taking as its parameter an X coordinate.

If the map is set to loop horizontally AND the passed-in X is less than the map's display X minus (the map's width - the number of tiles shown on the X axis) / 2, then we return x minus

the map's display X + the map's width. Otherwise, we return x minus the map's display X.

Uh, what? Okay, let's take an example. 50x50 map again because it's a nice round number. The player is at (32,15), so we're passing 32 to the method. The map is set to loop

horizontally. The if statement, with values substituted, becomes

if true && 32 < 24 - (50 - 17) / 2

if true && 32 < 7.5

It isn't, so we go to the else and return

32 - 24 = 8

So the adjusted X coordinate (or the one the position is actually centered at) is 8.

So what if the player is at (47,15) and the map doesn't loop horizontally? Well the very first condition of the if statement is false so there's no point in going any further, so we immediately

head to the else and return

47 - 33 = 14

So the position is tile 14 (3 tiles away from the edge).

def adjust_y(y)
    if loop_vertical? && y < @display_y - (height - screen_tile_y) / 2
      y - @display_y + @map.height
    else
      y - @display_y
    end
  end


Same thing but for adjusted Y coordinate.

def round_x(x)
    loop_horizontal? ? (x + width) % width : x
  end


This method calculates X coordinate after adjusting for looping. If the map is set to loop horizontally, we return the remainder of dividing (x + width) by width, otherwise we return x.

We're going to use a looping example for this one, as we haven't really seen many examples that properly explain how it works. So on our 50x50 map, it loops horizontally and our X is 53

(we've gone 3 tiles beyond the X edge)

The return value will be (53 + 50) % 50, which equates to 103 % 50. 50 goes into 103 twice, and we have 3 left over, so the return value is 3, which is the X tile we're now on. See how it

works?

def round_y(y)
    loop_vertical? ? (y + height) % height : y
  end


Same thing for Y after vertical loop adjustment.

def x_with_direction(x, d)
    x + (d == 6 ? 1 : d == 4 ? -1 : 0)
  end


This method calculates the X coordinate of a given X after moving one tile in a given direction.

Returns the given x + 1 if d is 6 (right), -1 if d is 4 (left), or 0 otherwise (since moving up/down don't affect the X coordinate)

def y_with_direction(y, d)
    y + (d == 2 ? 1 : d == 8 ? -1 : 0)
  end


Same thing but for moving a tile along the Y axis. Adds 1 if d is 2 (down) or -1 if d is 8 (up).

def round_x_with_direction(x, d)
    round_x(x + (d == 6 ? 1 : d == 4 ? -1 : 0))
  end


This method does a similar thing to x_with_direction but adjusts for looping by calling round_x.

def round_y_with_direction(y, d)
    round_y(y + (d == 2 ? 1 : d == 8 ? -1 : 0))
  end


Same thing but for Y, second verse same as the first, yada yada yada.

def autoplay
    @map.bgm.play if @map.autoplay_bgm
    @map.bgs.play if @map.autoplay_bgs
  end


Automatically plays the map's BGM/BGS if they've been set.

def refresh
    @events.each_value {|event| event.refresh }
    @common_events.each {|event| event.refresh }
    refresh_tile_events
    @need_refresh = false
  end


This method is called each time the map is refreshed. Iterates through each element of @events with "event" and calls that event's refresh method (which we'll see later in Game_Event),

then iterates through each element of @common_events again with "event" and calls that event's refresh method (which we'll see next week in Game_CommonEvent), then we refresh

the list of tile events and set @need_refresh to false. You may remember the previous scripts where @need_refresh was being set to true when something happened that made a map

refresh necessary.

def refresh_tile_events
    @tile_events = @events.values.select {|event| event.tile? }
  end


This method refreshes the list of tile events, by iterating through the values of @events with "event" and checking whether it's a tile, adding the event to the returned array if it is.

def events_xy(x, y)
    @events.values.select {|event| event.pos?(x, y) }
  end


This method gets an array of events present at a specified coordinate. Iterates through the values of @events and checks whether the event's position matches the passed-in X/Y, adding

the event to the returned array if it does.

def events_xy_nt(x, y)
    @events.values.select {|event| event.pos_nt?(x, y) }
  end


This is the same as the above method, but excludes events with "pass through" on.

def tile_events_xy(x, y)
    @tile_events.select {|event| event.pos_nt?(x, y) }
  end


This method does the same thing but only for tile events.

def event_id_xy(x, y)
    list = events_xy(x, y)
    list.empty? ? 0 : list[0].id
  end


This method gets the ID of the first event on the given tile using the events_xy method. Returns 0 if the list of events is empty, or the ID of the 0 element of the list otherwise.

def scroll_down(distance)
    if loop_vertical?
      @display_y += distance
      @display_y %= @map.height
      @parallax_y += distance if @parallax_loop_y
    else
      last_y = @display_y
      @display_y = [@display_y + distance, height - screen_tile_y].min
      @parallax_y += @display_y - last_y
    end
  end


This method scrolls the screen downwards. If the map is set to loop vertically, the given distance is added to @display_y, and @display_y is set to the remainder of dividing itself by the

map's height. If the parallax is set to loop vertically, distance is also added to @parallax_y.

If the map is not set to loop vertically, a variable called last_y is set to the @display_y, then @display_y is set to the minimum value between itself plus the distance, and height minus the

number of tiles shown on the Y axis. @parallax_y has the new @display_y minus the previous Y added to it.

def scroll_left(distance)
    if loop_horizontal?
      @display_x += @map.width - distance
      @display_x %= @map.width 
      @parallax_x -= distance if @parallax_loop_x
    else
      last_x = @display_x
      @display_x = [@display_x - distance, 0].max
      @parallax_x += @display_x - last_x
    end
  end


Pretty much the same thing but for scrolling left, so uses the X axis.

def scroll_right(distance)
    if loop_horizontal?
      @display_x += distance
      @display_x %= @map.width
      @parallax_x += distance if @parallax_loop_x
    else
      last_x = @display_x
      @display_x = [@display_x + distance, (width - screen_tile_x)].min
      @parallax_x += @display_x - last_x
    end
  end


Same thing, for scrolling right.

def scroll_up(distance)
    if loop_vertical?
      @display_y += @map.height - distance
      @display_y %= @map.height
      @parallax_y -= distance if @parallax_loop_y
    else
      last_y = @display_y
      @display_y = [@display_y - distance, 0].max
      @parallax_y += @display_y - last_y
    end
  end


Same thing, scrolling up. Yawn.

def valid?(x, y)
    x >= 0 && x < width && y >= 0 && y < height
  end


This method checks whether a set of given coordinates are valid. Returns true if the given X is greater than or equal to 0 AND the x is less than the map's width AND the given Y is greater

than or equal to 0 AND the y is less than the map's height.

BEFORE WE CONTINUE, A PRIMER
The next few methods are going to be dealing heavily with a concept called bit shifting, as well as bitwise operations, so in order to better understand them let's take a look at these

concepts in a bit more detail.

As you may or may not know, everything in a computer, from the program I'm using to write this tutorial to each individual character in the tutorial itself, from the operating system on

which you're viewing the tutorial to the program the tutorial is about, is made up of sequences of 1s and 0s, or "binary". Each 1 and 0 is called a "bit", with 8 bits making up a "byte".

Binary is base 2, meaning each bit represents a power of 2. Starting from the rightmost bit representing 1, the next left is 2, then 4, then 8, then 16, then 32, then 64 etc.

Because each combination of bits in binary represents a unique value, we often use values at the binary level to represent flags, any of which can be on or off. Bit shifting is the process of

moving the bits in a value either left or right to form a new value. You have left shift, <<, and right shift, >>.

Let's say we have the number 256. We can easily work out its representation by dividing it by 2 and taking the remainder, then reading the results backwards to form the binary value:

256 / 2 = 128 remainder 0
128 / 2 = 64 remainder 0
64 / 2 = 32 remainder 0
32 / 2 = 16 remainder 0
16 / 2 = 8 remainder 0
8 / 2 = 4 remainder 0
4 / 2 = 2 remainder 0
2 / 2 = 1 remainder 0
1 / 2 = 0 remainder 1

Remember we read backwards, so 256 in binary is 0000 0001 0000 0000. (best practice when representing binary numbers is to put them in groups of 4 bits and pad out 0s to make full

bytes).

If we bit shift 256 2 places left, it becomes 0000 0100 0000 0000, which is 1024. If we bit shift it 4 places right, it becomes 0001 0000, or 16.

Bitwise operations are when we compare each column of bits making up a number of values. The main operation we're concerned with here is bitwise AND. Let's say we want to perform

a bitwise AND on 25 and 14.

25 = 0001 1001
14 = 0000 1110

Bitwise AND gives 1 if all bits are 1, and 0 otherwise. So the value returned in this case would be

0000 1000, which is the binary representation of 8.

So what is this useful for? Well, let's say we have a 16-bit binary value comprising a number of flags which each represent a certain possibility. Let's say we want to see if the flag

represented by "16" is on. Rather than trawling through the whole number, we can perform a bitwise AND on it. So if, for example, we have a total flag value of 1087:

1087 = 0000 0100 0011 1111
16 = 0000 0000 0001 0000

The return value of the bitwise AND will be 0000 0000 0001 0000, which is 16. So if we had a method return, for example, (1087 & 16 == 16) the return value would be true.

NOTE: Bitwise AND is represented by &, but please don't mistake this for &&, which is the logical AND used for combining boolean comparisons.

Now let's be honest: people aren't supercomputers, and remembering all those 1s and 0s is a bit taxing. Unfortunately, we can't really join up their representation in our usual number

system, base 10. However, there is another number system which can: base 16, or hexadecimal.

Base 16, as the name suggests, is a system where each digit place represents a power of 16. As base 10 only covers 0-9, hexadecimal adds the letters A-F to represent the decimal

values 10-15.

So why can base 16 represent binary better than base 10 can? Well, the maximum value that 4 bits can show is 15, or 1111, which...wait, isn't that the maximum value you can show with

a single hex character? Yeah, it's F! So F in binary is 1111. We can represent any combination of 4 bits with any single character in hexadecimal (and subsequently any byte with 2 hex

characters), making it much easier to work with. This will be relevant later.

In Ruby, when we want to refer to a hexadecimal value, we prefix it with "0x", so F would be 0xF. Convention when writing them, similarly to working with binary, is to pad out 0s

depending on how many bytes we're working with. We tend to use 16-bit, which is 2 bytes, so F would be written as 0x000F, but there's no actual difference between 0xF and 0x000F.

However, there IS a difference between that and 0xF000, which actually represents the decimal value 61440!

END OF PRIMER

def check_passage(x, y, bit)
    all_tiles(x, y).each do |tile_id|
      flag = tileset.flags[tile_id]
      next if flag & 0x10 != 0            # [?]: No effect on passage
      return true  if flag & bit == 0     # [?] : Passable
      return false if flag & bit == bit   # [×] : Impassable
    end
    return false                          # Impassable
  end


This method checks whether the given coordinate can be passed through, taking as its parameters the X, the Y, and an "inhibit passage" check bit.

Iterates through an array of all the tiles on the given coordinate with "tile_id". First, we set a variable called flag to the tile_id element of the tileset's flags. Flags is an array containing bit

patterns (a list is available in the help file under RPG::Tileset). We'll be looking at a few methods which call this one shortly so I'll be able to give an example.

Anyway, the next thing we do is skip to the next tile if the result of a bitwise AND operation on the flag and 0x10 (15 or 1111) is not equal to 0. We return true if the result of a bitwise AND

on flag and bit equals 0, and false if a bitwise AND on flag and bit equals bit. Finally, after the each loop, we return false (if we went through every tile on the coordinate and didn't find a

passage flag, logically it must be impassable).

def tile_id(x, y, z)
    @map.data[x, y, z] || 0
  end


This method gets the ID of the tile at specified coordinates. Remember that the map is a 3-dimensional table, so we pass in not only X and Y but Z, which is the layer we want to check.

Returns the x, y, z element of the map's data if there's a tile there, or 0 otherwise.

def layered_tiles(x, y)
    [2, 1, 0].collect {|z| tile_id(x, y, z) }
  end


This method gets an array of all tiles in a given coordinate, from top to bottom. Iterates through each possible Z value (2, 1 and 0) with "z" and returns the result of calling tile_id passing

in the given x, the given y, and the current z.

def all_tiles(x, y)
    tile_events_xy(x, y).collect {|ev| ev.tile_id } + layered_tiles(x, y)
  end


This is a similar method but adds in events on the given tile as well.

def autotile_type(x, y, z)
    tile_id(x, y, z) >= 2048 ? (tile_id(x, y, z) - 2048) / 48 : -1
  end


This method gets the type of autotile at the given coordinates (x, y and z). If the result of calling tile_id on the given coordinate is greater than or equal to 2048, this method returns the

(result - 2048) / 48, otherwise it returns -1.

I suppose this would be a good opportunity for a crash course into how tile IDs work. I could explain it myself, but PK8 already put together a pretty comprehensive thread on it, so I'll link

you to that instead: Autotile Tile IDs

def passable?(x, y, d)
    check_passage(x, y, (1 << (d / 2 - 1)) & 0x0f)
  end


This method checks whether a given tile is passable in a given direction. Calls check_passage with "bit" as (1 << (d / 2 - 1)) & 0x0f. That might look a bit overwhelming, so let's break it

down.

d is a direction, and as you may already know directons are represented in RPG Maker as a number: 2 is down, 4 is left, 6 is right and 8 is up. Let's say we're checking passability going

right. d is 6, so the expression (d / 2 - 1) becomes (6 / 2 - 1) which becomes (3 - 1) which becomes (2).

So now we've got (1 << 2) & 0x0f.

1 is represented in binary as 0001. Shifting it 2 bits to the left gives us 0100, or "4" in decimal. (if you look at the help file, this corresponds to the bit flag representing "impassable

rightward")

Now we're performing a bitwise AND on 0100 (4 decimal, 0x04 hex) and 0x0F, which is 15 decimal or 1111 binary.

Following the logic of the above explanation on the bitwise AND operation, 1111 & 0100 gives us 0100, which is 0x04 in hexadecimal.

So the call becomes check_passage(x, y, 0x04).

Let's take an example tile with a flag value of 1536. This is represented in binary as

0001 0101 0011 0110

Running a bitwise AND with the passed-in value (0100) gives us

0001 0101 0011 0110
& 0000 0000 0000 0100

Which gives us 0100, meaning the tile is impassable rightwards.

Let's take another example flag value, 1550. Represented in binary this is

0000 0110 0000 1110
& 0000 0000 0000 0100

This gives us 1, so the tile still can't be passed moving rightwards.

def boat_passable?(x, y)
    check_passage(x, y, 0x0200)
  end


This method checks whether the boat vehicle can pass through a given tile. 0x0200 is the hex representation of the binary value 0000 0010 0000 0000, or 512 in decimal, which is used to

determine whether a tile is impassable by boat.

def ship_passable?(x, y)
    check_passage(x, y, 0x0400)
  end


This method checks whether the ship vehicle can pass through a given tile. 0x0400 is the hex representation of the binary value 0000 0100 0000 0000, or 1024 in decimal, used for ship

impassability.

def airship_land_ok?(x, y)
    check_passage(x, y, 0x0800) && check_passage(x, y, 0x0f)
  end


This method checks whether it's okay for the airship to land on a given tile. 0x0800 is the hex representation of the binary value 0000 1000 0000 0000, or 2056 in decimal, used for airship

impassability. Note that this also needs to check whether the player is able to stand on the tile you're trying to land on, as it wouldn't make any sense for the airship to be able to land on a

tile the player can't move on.

def layered_tiles_flag?(x, y, bit)
    layered_tiles(x, y).any? {|tile_id| tileset.flags[tile_id] & bit != 0 }
  end


This method checks a specified bit flag on a specified coordinate. Checks whether any tile on the given X/Y contains a flag which returns a value other than 0 when a bitwise AND is

performed against the given bit.

def ladder?(x, y)
    valid?(x, y) && layered_tiles_flag?(x, y, 0x20)
  end


This method determines whether the given coordinate contains a ladder tile, represented as the hex value 0x20, which is 32 in decimal, which is 0010 0000 in binary. Checks whether the

given X/Y are valid and calls layered_tiles_flag? passing in the x, the y, and the ladder flag.

def bush?(x, y)
    valid?(x, y) && layered_tiles_flag?(x, y, 0x40)
  end


Same thing but checks whether the tile is a bush.

def counter?(x, y)
    valid?(x, y) && layered_tiles_flag?(x, y, 0x80)
  end


Same thing but checks whether the tile is a counter.

def damage_floor?(x, y)
    valid?(x, y) && layered_tiles_flag?(x, y, 0x100)
  end


Same thing but checks whether the tile is a damaging floor.

def terrain_tag(x, y)
    return 0 unless valid?(x, y)
    layered_tiles(x, y).each do |tile_id|
      tag = tileset.flags[tile_id] >> 12
      return tag if tag > 0
    end
    return 0
  end


This method gets the terrain tag on a specific coordinate.

Returns 0 unless the x and y are valid.

Iterates through each tile in each layer on the given coordinate with "tile_id". Sets a variable called tag to the result of shifting the bits in the flags for the current tile ID 12 places to the

right. Returns tag if tag is greater than 0. Returns 0 if every tile has been checked and no terrain tag was returned. Let's take 26126 as our example flag value, checking for terrain tag 6.

26126 in binary is

0110 0110 0000 1110

Shifting the bits 12 places to the right gives us

0000 0000 0000 0110

which is 6 in binary, so we know that the terrain tag is 6.

So why do we bitshift 12 places to the right? Well the internal tile ID flag starts at 4096 for tag 1. 4096 takes 16 bits to represent in binary (0001 0000 0000 0000) but our terrain tags are

the single digits 0-7, which only take 4 bits to represent (0000, 0001, 0010, 0011, 0100, 0101, 0110, 0111) so shifting the 16-bit version 12 places right will convert it to a single-digit

decimal.

def region_id(x, y)
    valid?(x, y) ? @map.data[x, y, 3] >> 8 : 0
  end


This method gets the region ID of a given X and Y coordinate. If the coordinate is valid, returns the x, y, 3 element of @map.data which corresponds to the 4th layer on the given

coordinate (which contains region ID information), with bits shifted 8 places to the right. Returns 0 otherwise.

Similarly with terrain tags, we bitshift 8 places right because the internal region ID flag starts at 256 for tag 1, and 256 takes 12 bits to represent (0001 0000 0000). Shifting 8 places right

converts it to a decimal value between 1 and 63.

def start_scroll(direction, distance, speed)
    @scroll_direction = direction
    @scroll_rest = distance
    @scroll_speed = speed
  end


This method starts the map scrolling in a given direction, distance, and speed.

def scrolling?
    @scroll_rest > 0
  end


This method checks whether the map is currently scrolling. Returns true if @scroll_rest is greater than 0, false otherwise.

def update(main = false)
    refresh if @need_refresh
    update_interpreter if main
    update_scroll
    update_events
    update_vehicles
    update_parallax
    @screen.update
  end


Okay, here we have the method that deals with the map updating. It takes one parameter, main, which defaults to false. This is a flag which determines whether the interpreter needs to

update as well.

First we call refresh if @need_refresh is true (as you've seen by now, this is set to true in several places). Then if main is true, we call update_interpreter. Then we update the scroll, then

the events, then the vehicles, then the parallax, and finally @screen.

def update_scroll
    return unless scrolling?
    last_x = @display_x
    last_y = @display_y
    do_scroll(@scroll_direction, scroll_distance)
    if @display_x == last_x && @display_y == last_y
      @scroll_rest = 0
    else
      @scroll_rest -= scroll_distance
    end
  end


This method updates the scroll. Returns unless the map is currently scrolling.

Sets a variable called last_x to the @display_x, and last_y to the @display_y. Calls do_scroll (which we'll see in a minute) passing in the scroll direction and distance.

If the display X is equal to the last X AND the display Y is equal to the last Y, set @scroll_rest to 0. Otherwise, subtract distance from @scroll_rest.

def scroll_distance
    2 ** @scroll_speed / 256.0
  end


This is the method that determines the distance to scroll. The value is 2 to the power of scroll speed divided by 256. For example, a scroll speed of "5" will move 0.125 pixels per frame,

whereas a scroll speed of "1" will move 0.0078125 pixels per frame.

def do_scroll(direction, distance)
    case direction
    when 2;  scroll_down (distance)
    when 4;  scroll_left (distance)
    when 6;  scroll_right(distance)
    when 8;  scroll_up   (distance)
    end
  end


This is the method that handles the scrolling. Takes as its parameters direction and distance (which as we've just seen is calculated in the scroll_distance method). When direction is 2,

we scroll down. When it's 4, we scroll left. 6, we scroll right, and 8, we scroll up.

def update_events
    @events.each_value {|event| event.update }
    @common_events.each {|event| event.update }
  end


This is the method which updates events. Iterates through each map event with "event" and calls its update method, then does the same thing with each common event.

def update_vehicles
    @vehicles.each {|vehicle| vehicle.update }
  end


This method updates the vehicles by iterating through each vehicle with "vehicle" and calling its update method.

def update_parallax
    @parallax_x += @parallax_sx / 64.0 if @parallax_loop_x
    @parallax_y += @parallax_sy / 64.0 if @parallax_loop_y
  end


This method updates the parallax. If parallax is set to loop horizontally, adds @parallax_sx / 64 to the @parallax_x. Same thing with the y if it's set to loop vertically. This means that an

autoscroll speed of 1 will move the parallax 0.015625 pixels per frame, and 32 (the maximum value for autoscroll) will move 0.5 pixels per frame.

def change_tileset(tileset_id)
    @tileset_id = tileset_id
    refresh
  end


This method changes the map's tileset. Sets @tileset_id to the given ID, and calls refresh.

def change_battleback(battleback1_name, battleback2_name)
    @battleback1_name = battleback1_name
    @battleback2_name = battleback2_name
  end


Changes the map's battle backgrounds. Self-explanatory.

def change_parallax(name, loop_x, loop_y, sx, sy)
    @parallax_name = name
    @parallax_x = 0 if @parallax_loop_x && !loop_x
    @parallax_y = 0 if @parallax_loop_y && !loop_y
    @parallax_loop_x = loop_x
    @parallax_loop_y = loop_y
    @parallax_sx = sx
    @parallax_sy = sy
  end


Changes the parallax settings. Takes as its arguments the new graphic filename and the autoscroll settings.

def update_interpreter
    loop do
      @interpreter.update
      return if @interpreter.running?
      if @interpreter.event_id > 0
        unlock_event(@interpreter.event_id)
        @interpreter.clear
      end
      return unless setup_starting_event
    end
  end


This method updates the interpreter. In an endless loop, calls the update method of interpreter and returns if the interpreter is running. If the interpreter's event ID is greater than 0,

unlock the event then clear the interpreter. (we'll see more about how this works when we look at Game_Event and Game_Interpreter). Finally, return unless we're setting up a starting

event.

def unlock_event(event_id)
    @events[event_id].unlock if @events[event_id]
  end


This method unlocks an event, which again we'll see more of in Game_Event.

def setup_starting_event
    refresh if @need_refresh
    return true if @interpreter.setup_reserved_common_event
    return true if setup_starting_map_event
    return true if setup_autorun_common_event
    return false
  end


This method determines whether a starting event is being set up. Calls refresh if @need_refresh is true. Returns true if the interpreter is setting up a reserved common event, returns true

if we're setting up a starting map event, returns true if we're setting up an autorun common event, and otherwise returns false.

def any_event_starting?
    @events.values.any? {|event| event.starting }
  end


Determines whether there are any starting events on the map. Iterates through all values in @events and returns true if calling the starting method in any of them returns true.

def setup_starting_map_event
    event = @events.values.find {|event| event.starting }
    event.clear_starting_flag if event
    @interpreter.setup(event.list, event.id) if event
    event
  end


This method sets up a starting event. Sets event to the first event it can find for which starting returns true. Clears the starting flag if one is found, calls the setup method of interpreter

with the event details, and returns the event. We'll see more about how this works when we get to Game_Event and Game_Interpreter.

def setup_autorun_common_event
    event = $data_common_events.find do |event|
      event && event.autorun? && $game_switches[event.switch_id]
    end
    @interpreter.setup(event.list) if event
    event
  end


This method sets up an autorun common event.

Sets event to the first event it can find for which the condition is autorun and its activation switch is on. If it finds one, calls the interpreter's setup method, and returns the event.

Phew! I don't know about you guys, but I'm shattered after that. Game_Map was a biggie and I'm glad it's out of the way. Since there's so much to pore through here I'm not giving

homework exercises this week; I think you've got enough to read already. Until next time!

Posts

Pages: 1
Pages: 1