Rooms and Maps

Strange Errors and the Backrooms

If you’ve tried using room_goto to go to a Room, you’ve probably run into some strange behavior. Weird crashes, a room that you can never leave, ending up in unexpected locations, what’s happening?

oroom_transition

While GameMaker does have Rooms, it doesn’t have a standardized method of linking Rooms together. So how does Rusted Moss do it? The main example is the oroom_transition Game Object, which is responsible for the standard transition between adjacent rooms. Let’s investigate.

// gml_Object_oroom_transition_Create_0
...
// convert `image_angle` from degrees to a range [0-3]
dir = ((image_angle + 45) % 360) div 90
// store two mystery globals (we'll need these later)
cx = global.player_map_x_
cy = global.player_map_y_
// get adjacent coordinate based on the `image_angle`
switch dir
{
  case 0:
    // `jump_dis` is specified by the Room editor
    cx -= jump_dis
    break
  case 1:
    cy += jump_dis
    break
  case 2:
    cx += jump_dis
    break
  case 3:
    cy -= jump_dis
    break
}
// read from a mystery data structure, using our new coordinates
m = ds_grid_get(global.map_grid_, cx, cy)
if (m != 0)
  // set a `target_room` based on another data structure, ...
  target_room = global.map_data_[m][0].rom
else
  // ... or use the backrooms as a fallback
  target_room = rm_sea_8
...

First is some initialization code. This generates three Instance variable outputs: cx, cy, and target_room. Let’s see what they’re used for.

// gml_Object_oroom_transition_Draw_75
...
// @ IF ( READY_TO_TRANSITION ) THEN
  // set those two mystery globals using the values from earlier
  global.player_map_x_ = cx
  global.player_map_y_ = cy
  // store the player's velocity for later
  global.player_map_hsp_ = oplayer.hsp
  global.player_map_vsp_ = oplayer.vsp
  // hey, it's how the climb hardmode works, neat
  if (force_room != 0 && (instance_exists(oclimb_hardmode) || force_push_room))
  {
    global.player_map_x_ = global.map_data_[force_room][0].xx
    global.player_map_y_ = global.map_data_[force_room][0].yy
    target_room = force_room
  }
  // go to the target Room
  room_goto(target_room)
  // keeps this Instance around after the Room transition
  persistent = true
// @ END IF
...

This code is what actually moves the Player between Rooms. Notice that we’re not just using room_goto directly, we’re also setting the globals player_map_x_ and player_map_y_. If we don’t then the oroom_transition Instances in the next room won’t find the correct adjacent Rooms, leading to a desync.

If you’re curious how the Player is created in the next room (since the Player isn’t peristent), check out the room_start Event for oroom_transition (in gml_Object_oroom_transition_Other_4).

You’ll notice that there are two load-bearing data structures here, global.map_data_ and global.map_grid_. Both of these are necessary for oroom_transition to work properly, so let’s inspect them.

global.map_data_

global.map_data_ is two-dimensional array that maps internal Room indexes to an array of sub-rooms, each containing Room metadata.

[
  ...
  [-1.0], // no data, this Room index is unused
  [
    // a single room
    {
      // internal room index
      "rom": 2.0,
      // x/y coordinates for `player_map_[x/y]_`
      "xx": 47.0, "yy": 31.0,
      // map rendering metadata (for the tab menu)
      "index": 2.0, "angle": 90.0, "color": 6,
      // pickup metadata
      "extra": 6.0, "found": 0.0,
    }
  ],
  [
    // a two-high room
    {
      "rom": 4.0,
      "xx": 49.0, "yy": 30.0,
      "index": 5.0, "angle": 90.0, "color": 6,
      "extra": false, "found": 0.0,
    },
    {
      "rom": 4.0,
      "xx": 49.0, "yy": 29.0,
      "index": 5.0, "angle": -90.0, "color": 6,
      "extra": false, "found": 0.0,
    }
  ],
  ...
]

global.map_grid_

This is a much simpler data structure. It is a ds_grid, and translates the xx and yy coordinates in global.map_data_ to a Room index.

let room = rm_test
let room_meta = global.map_data_[rm_test][0]
let xx = room_meta.xx
let yy = room_meta.yy
-- read from map
let roomFromMap = ds_grid_get(global.map_grid_, xx, yy)

if room == roomFromMap {
  show_message("Success!")
}

This structure is mostly empty. It also contains many rows that similarly completely empty.

Drawing the Map

An additional upside is that we can draw the game’s map very easily. The map_draw() function in gml_GlobalScript_map_scripts contains the code that draws the map, using the metadata from global.map_data_.

Just give me the Code

Ok, ok, enough yapping. Here’s how to go to any standard Room.

-- takes us to the chosen room
global.room_goto_fix = fun (room) {
  let room_meta = global.map_data_[room][0]
  global.player_map_x_ = room_meta.xx
  global.player_map_y_ = room_meta.yy
  room_goto(room)
}

If you want to go to an arbitrary Room (such as one made by room_add or a Room that isn’t part of map_data_), you’ll need to populate global.map_data_ and global.map_grid_ with fake data to trick oroom_transition Instances, or implement your own Room transition logic.

You might want to dump global.map_grid_, so here’s some code to JSON stringify a ds_grid.

global.map_stringify = fun(grid) {
  let w = ds_grid_width(grid)
  let h = ds_grid_height(grid)
  let str = "["
  let x = 0
  while x < w {
    let y = 0
    str += "["
    while y < h {
      str += string(ds_grid_get(grid, x, y))
      y += 1
      if y != h {
        str += ","
      }
    }
    str += "]"
    x += 1
    if x != w {
      str += ","
    }
  }
  str += "]"
  return str
}