My own four walls

Last week I finished with an at sign walking across the screen. That is already a nice start and the next thing I'd wanted to do was adding some walls to restrict the player's movement as well as a door which can be opened and closed via button press.

Separating input and execution

In order to implement walls I will need to check whether I can actually move in the direction I pressed before executing the movement.

Since the current, very simple first version of my movement code immediately and directly updates the player's position when an arrow key is pressed, naively adding such a check to each of the four directions would introduce unwelcome duplication into my input handling code. Because the movement check is probably something that is going to evolve quite a bit as the game becomes more complex, duplication would definitely mean that this would be harder to change.

One thing I do usually very early when creating a game is separating the game logic from the input parsing - by transforming the input values into an intermediate data structure, which I like to call Input Action. And in this case it would naturally help us the aforementioned duplication.

For now I'll be going with a simple hash (which are really cheap to create in DragonRuby) containing a :type key which specifies the type of input action together with other action specific parameters.

In my game's case that would look like this for the movement:

keyboard = args.inputs.keyboard
if keyboard.key_down.up
  player_position[:y] += 1
elsif keyboard.key_down.down
  player_position[:y] -= 1
elsif keyboard.key_down.left
  player_position[:x] -= 1
elsif keyboard.key_down.right
  player_position[:x] += 1
end
player_action = {}
keyboard = args.inputs.keyboard
if keyboard.key_down.up
  player_action = { type: :move, direction: { x:  0, y:  1 } }
elsif keyboard.key_down.down
  player_action = { type: :move, direction: { x:  0, y: -1 } }
elsif keyboard.key_down.left
  player_action = { type: :move, direction: { x: -1, y:  0 } }
elsif keyboard.key_down.right
  player_action = { type: :move, direction: { x:  1, y:  0 } }
end
afterbefore

And later in the tick when handling the player input action we can use a simple case statement like this:

case player_action[:type]
when :move
  player_position[:x] += player_action[:direction][:x]
  player_position[:y] += player_action[:direction][:y]
end

We could now easily add the movement check here but to actually have anything the player could collide with, we will first need to introduce a map data structure. And in order to comfortably render our map let's clean up our rendering logic a bit.

Refactoring the sprite rendering

Our very first hard coded sprite rendering looked like this:

args.outputs.sprites << {
  x: player_position[:x] * 32, y: player_position[:y] * 32, w: 32, h: 32,
  path: 'sprites/zilk-16x16.png',
  source_x: 0 * 16, source_y: 11 * 16, source_w: 16, source_h: 16
}

We are going to need to render other tiles from the same tileset, so as first step let's extract a tileset_sprite method:

def tileset_sprite(tile_x, tile_y)
  {
    w: 32, h: 32,
    path: 'sprites/zilk-16x16.png',
    source_x: tile_x * 16, source_y: tile_y * 16, source_w: 16, source_h: 16
  }
end

which will allow the sprite above to be simplified to:

args.outputs.sprites << tileset_sprite(0, 11).merge(
  x: player_position[:x] * 32, y: player_position[:y] * 32
)

Instead of having a nondescript sprite from tile (0, 11) let's prepare a hash of named sprites used in the game:

def build_sprites
  {
    player: tileset_sprite(0, 11)
  }
end

so we can change our tick parts like this

sprites = args.state.sprites ||= build_sprites

# ...

args.outputs.sprites << sprites[:player].merge(
  x: player_position[:x] * 32, y: player_position[:y] * 32
)

One final improvement we can do is extracting a method for positioning a sprite on the grid:

def sprite_at_position(sprite, position)
  sprite.merge(x: position[:x] * 32, y: position[:y] * 32)
end

which will cause our player sprite rendering to become a concise

args.outputs.sprites << sprite_at_position(sprites[:player], player_position)

Authoring a simple map using hot reloading

Now that we got ourselves a better rendering setup, let's start building a data structure for the map.

For now let's keep it simple and stupid and just create a nested array of hashes containing the cell information. To get a nice interactive map editing workflow let's do a little file organizing trick:

# --- app/main.rb ---
def tick(args)
  current_map = args.state.current_map ||= build_map
  # ...
end

# --- app/map.rb ---
def build_map
  result = 40.times.map { 22.times.map { {} } }
  result[20][10][:wall] = true
  result
end

$state.current_map = nil

This will give us a 40 by 22 array (which is the screen dimension for now) filled with empty hashes and a single wall at coordinates (20, 10). For now a wall is just being represented by a wall: true value inside the cell hash.

I put the function building our map into a separate file combined with with a precisely targeted state reset statement in the global scope. Now everytime I edit and save this file DragonRuby's hotloading will immediately evaluate the new version of the file and the map state will be reinitialized to the newly edited version of the map.

Building ourselves a little prison

We have an editing workflow set up, so let's add some rendering, so we can actually see the walls we're adding:

# In the rendering part of the tick
current_map.each_with_index do |column, x|
  column.each_with_index do |cell, y|
    if cell[:wall]
      args.outputs.sprites << sprite_at_position(sprites[:wall], { x: x, y: y })
    end
  end
end

Now we can update our build_map function to create four enclosing walls around us:

def build_map
  result = 40.times.map { 22.times.map { {} } }
  result[20][10][:wall] = true
  result
end
def build_map
  result = 40.times.map { 22.times.map { {} } }
  # horizontal walls
  result[14..25].each do |column|
    column[15][:wall] = true
    column[5][:wall] = true
  end
  # vertical walls
  (6..14).each do |y|
    result[14][y][:wall] = true
    result[25][y][:wall] = true
  end
  result
end
afterbefore

Now that we can see walls around us, let's make them actually block our movement:

case player_action[:type]
when :move
  player_position[:x] += player_action[:direction][:x]
  player_position[:y] += player_action[:direction][:y]
end
case player_action[:type]
when :move
  if can_move?(player_position, player_action[:direction], current_map)
    player_position[:x] += player_action[:direction][:x]
    player_position[:y] += player_action[:direction][:y]
  end
end
afterbefore

With can_move? being defined as:

def can_move?(position, direction, current_map)
  new_x = position[:x] + direction[:x]
  new_y = position[:y] + direction[:y]
  cell = cell_at(current_map, new_x, new_y)
  !cell[:wall]
end

A_WALL = { wall: true }.freeze

def cell_at(current_map, x, y)
  return A_WALL unless x.between?(0, current_map.length - 1)

  column = current_map[x]
  return A_WALL unless y.between?(0, column.length - 1)

  column[y]
end

I abstracted the cell retrieval into its own method to be able to concisely take care of invalid coordinates. Returning a wall cell for any invalid coordinates is a form of the so called Null Object Pattern and allows us to keep the main logic in can_move? clear and without many boundary condition checks.

A door to freedom

Now that we have walls, next let's build a door that we can open with the space key.

First let's make one of our wall tiles into a door. For this we will introduce a new key to our map cell hash: :door. Unlike :wall which is either there or not, a door needs to hold some state, namely whether its closed or not.

def build_map
  result = 40.times.map { 22.times.map { {} } }
  # horizontal walls
  result[14..25].each do |column|
    column[15][:wall] = true
    column[5][:wall] = true
  end
  # vertical walls
  (6..14).each do |y|
    result[14][y][:wall] = true
    result[25][y][:wall] = true
  end
  result
end
def build_map
  result = 40.times.map { 22.times.map { {} } }
  # horizontal walls
  result[14..25].each do |column|
    column[15][:wall] = true
    column[5][:wall] = true
  end
  # vertical walls
  (6..14).each do |y|
    result[14][y][:wall] = true
    result[25][y][:wall] = true
  end
  # Replace a wall with a closed door
  result[20][15].delete(:wall)
  result[20][15][:door] = { closed: true }
  result
end
afterbefore

Later on we will probably need a more sophisticated way of handling maps, cells and entities inside those cells but for now this simple hash should be more than enough.

Next, let's take care of the rendering (assuming we have defined two sprites for open and closed doors):

current_map.each_with_index do |column, x|
  column.each_with_index do |cell, y|
    if cell[:wall]
      args.outputs.sprites << sprite_at_position(sprites[:wall], { x: x, y: y })
    end
  end
end
current_map.each_with_index do |column, x|
  column.each_with_index do |cell, y|
    if cell[:wall]
      args.outputs.sprites << sprite_at_position(sprites[:wall], { x: x, y: y })
    elsif cell[:door]
      sprite = if cell[:door][:closed]
                 sprites[:closed_door]
               else
                 sprites[:open_door]
               end
      args.outputs.sprites << sprite_at_position(sprite, { x: x, y: y })
    end
  end
end
afterbefore

This too is quite coupled to the actual map data structure and not optimized at all - but one step at a time - until we actually need all that.

Next on our list is making sure that we can actually only walk through doors when they are not closed, so let's update our can_move? function accordingly:

def can_move?(position, direction, current_map)
  new_x = position[:x] + direction[:x]
  new_y = position[:y] + direction[:y]
  cell = cell_at(current_map, new_x, new_y)
  !cell[:wall]
end
def can_move?(position, direction, current_map)
  new_x = position[:x] + direction[:x]
  new_y = position[:y] + direction[:y]
  cell = cell_at(current_map, new_x, new_y)
  return false if cell[:door] && cell[:door][:closed]

  !cell[:wall]
end
afterbefore

For the actual opening and closing of the door I will introduce a generic "Interact" input action which will eventually be responsible for all kinds of default interactions with people and objects.

So first adding it to our input parsing code:

player_action = {}
keyboard = args.inputs.keyboard
if keyboard.key_down.up
  player_action = { type: :move, direction: { x:  0, y:  1 } }
elsif keyboard.key_down.down
  player_action = { type: :move, direction: { x:  0, y: -1 } }
elsif keyboard.key_down.left
  player_action = { type: :move, direction: { x: -1, y:  0 } }
elsif keyboard.key_down.right
  player_action = { type: :move, direction: { x:  1, y:  0 } }
end
player_action = {}
keyboard = args.inputs.keyboard
if keyboard.key_down.up
  player_action = { type: :move, direction: { x:  0, y:  1 } }
elsif keyboard.key_down.down
  player_action = { type: :move, direction: { x:  0, y: -1 } }
elsif keyboard.key_down.left
  player_action = { type: :move, direction: { x: -1, y:  0 } }
elsif keyboard.key_down.right
  player_action = { type: :move, direction: { x:  1, y:  0 } }
elsif keyboard.key_down.space
  player_action = { type: :interact }
end
afterbefore

And then adding the actual interaction to our player action handling code:

case player_action[:type]
when :move
  # ...
when :interact
  neighboring_cells = [[0, 1], [0, -1], [1, 0], [-1, 0]].map { |direction|
    x = player_position[:x] + direction[0]
    y = player_position[:y] + direction[1]
    current_map[x][y]
  }
  door = neighboring_cells.find { |cell| cell[:door] }
  if door
    door[:door][:closed] = !door[:door][:closed]
  end
end

And with that we have implemented doors:

Leaving the room