March 7, 2020

Generating a Map with Dragon Ruby

Earlier this week I downloaded a copy of the DragonRuby game toolkit. I’ve always wanted to work on a sprite-based 2D game and so far I’ve been impressed with how simple it is to use for a gamedev beginner like me. With this blog post, I’m going to record some of my experiences developing a simple map for the player to navigate with DragonRuby.

What is DragonRuby?

DragonRuby is a cross-platform 2D game engine written in the Ruby programming language. The engine is designed to remove many complexities modern game engines are known to have by providing straight-forward APIs that allow anyone to quickly develop their game.

You can begin your game by simply adding to main.rb:

def tick args
  # insert game logic here
end

That’s it. tick is the game loop. The code written there is executed 60 times per second. If you’ve worked with basic console-based games before then you know that with every tick the engine is rendering and updating game state. With DragonRuby, we get immediate access to the game’s state and rendering primitives by accessing them off the args parameter as args.state and args.outputs. I highly suggest checking out the page for DragonRuby on itch.io for more information on args.outputs's primitives. It does a great job providing explanations on what each of them do.

Generating the Dungeon Map

When I decided to work on a roguelike, learning how to draw map tiles, the player, and enemies with sprite art was an important factor.

To render a sprite, we add to the “sprites” output primitive by doing: args.output.sprites << [x, y, w, h, path] where x, y, w, h, and path correspond to the position (x, y), size (w, h), and file path to the sprite image we want to render to the Dragon Ruby console.

So to represent the map tiles, I made the decision to draw a single sprite image (that are 16x16 pixels) for each tile. The map would be 32x32, so it meant I’d be drawing at least 1024 16x16 sprites on screen, per tick.

This is where things got a little complicated. Turns out trying to render this many at once was too much for performance. This was problematic as I have yet to add enemies, weapons, etc… to the rendering process! I needed to rethink how I initially approached calculating and drawing the map. So I identified a few places in my code where things could be optimized and narrowed it down to these steps:

  1. Calculate the layout of the map once.
  2. Divide the 32x32 map into four quadrants.
  3. Render the map tiles corresponding to the quadrant the player is in.

Step 1: Calculating the map

Here the layout of the dungeon is calculated and stored on state. Quite a few things happen during this step, so let’s walk through it. But first let’s make sure we ensure that we only initialize the map once by creating a piece of state to hold it with: args.state.map.tiles ||= [].

||= means that we only initialize a variable if it hasn’t already. This is important because we don’t want to be re-initializing the map everytime tick is run.

Now let’s store the map tiles in this array we’ve created. I decided to create a Tile class that takes in a “x” and “y” coordinate. This represents where the tile is placed on the map. A Tile can also be made not “passable” by calling it’s method wall, which determines whether or not the player can move through the tile.

Now that we have a way to represent a tile on the map, now let’s actually create it!

def tick args
  # defaults
  args.state.map.tiles || = []

  # Step 1. Calculate layout of map
  if args.state.map.tiles == []
    # 1A. Create map tilee and add to map
    for coordX in 0..MAP_WIDTH - 1 do
      for coordY in 0..MAP_HEIGHT - 1 do
        new_tile = Tile.new(coordX, coordY)
        # initialize new tile as a wall
        new_tile.wall
        # place the tile on the map
        args.state.map.tiles << new_tile
      end
    end

    #1B. Generate rooms on map
    #1C. Create tunnels connecting the rooms to each other
  end
end

The map we just created is now just a bunch of walls. The next steps now are carving out rooms and tunnels the player can move through. To do these parts I’ve been using this Roguelike Rust tutorial as a guide. So the concepts presented there will not be much different from the code I have written.

Once that’s done we will have an 1024 item array of Tile whose individual properties make up the entire map.

Step 2. Divide the map into four quadrants

I mentioned earlier that trying to render the entire map, all 1024 sprites, caused some noticeable performance drops. To work around this, I decided we can divide up the map into four quadrants:

                     16,31
                       |
        +--------------|--------------+
        |              |              |
        |              |              |
        |    QUAD 3    |    QUAD 4    |
        |              |              |
        |              |              |
        |              |              |
 0,16 ----------------------------------- 31,16
        |              |              |
        |              |              |
        |    QUAD 1    |    QUAD 2    |
        |              |              |
        |              |              |
        |              |              |
    0,0 +--------------|--------------+ 31,0
                       |
                     16,0

The idea here is to separate parts of the map so that only 256 map tiles are rendered at a time. And because we have a representation of the entire map stored in args.state.map.tiles, we can make another piece of state with quadrants that contain their corresponding tiles.

  # organize tiles into four quads
  #quad1
  args.state.map.quads << args.state.map.tiles.select { |t| t.x < 16 && t.y < 16 }
  #quad2
  args.state.map.quads << args.state.map.tiles.select { |t| (t.x >= 16 && t.x < 32) && t.y < 16 }
  #quad3
  args.state.map.quads << args.state.map.tiles.select { |t| t.x < 16 && t.y >= 16 }
  #quad4
  args.state.map.quads << args.state.map.tiles.select { |t| (t.x >= 16 && t.x < 32) && t.y >= 16 }

There are probably more clever ways to access a tile for a given quadrant, but for now let’s use the above approach for simplicity’s sake. Since map.quads is also an array, accessing quads can be done by using quadrant (n-1).

Step 3: Rendering the map based on player location

Our map data is in a state where we can easily find which tiles to render. So now I’ll create another default variable on state: args.state.player.current_quad ||= 0, where 0 represents the first quadrant. With this, we can iterate over the quadrant tiles and render.

  for tile in args.state.map.quads[args.state.player.current_quad] do
    if tile.passable
      args.outputs.sprites << tile_in_game(tile.x.mod(16), tile.y.mod(16), "sprites/tiles/floor_1.png")
    else
      args.outputs.sprites << tile_in_game(tile.x.mod(16), tile.y.mod(16), "sprites/tiles/wall_mid.png")
    end
  end

The code responsible for handling keyboard input needs to also change the player’s quadrant position once a boundary is crossed.

  # check if player is...
  # moving into quad 1
  if args.state.player.x < 16 && args.state.player.y < 16
    args.state.player.current_quad = 0
  # moving into quad 2
  elsif args.state.player.x >= 16 && args.state.player.y < 16
    args.state.player.current_quad = 1
  # moving into quad 3
  elsif args.state.player.x < 16 && args.state.player.y >= 16
    args.state.player.current_quad = 2
    # moving into quad 4
  elsif args.state.player.x >= 16 && args.state.player.y >= 16
    args.state.player.current_quad = 3
  end

Now with every call to tick, the game will know when to change what tiles to render based on the player’s location!

Introspection

It was interesting finding a way to solve the performance issue for rendering 1024 sprites at once. However, the solution I proposed and wrote this entire post about skirted around the reason I think was contributing to performance issues. If we look at the code snippet where we render the tile sprites, we can see that we load two files for a wall/floor tile. By loading another sprite file, we end up swapping textures for every draw. So one way we can look at optimizing this is to use a single spritesheet.

Current code for this post can be found here.

Resources

© Micah Tigley 2020

Powered by Hugo & Kiss.