Game Engine Overview, December 28, 2019

Overview & Demo

For the past month I’ve been experimenting with DragonRuby in my spare time, slowly building out a game engine and getting first-hand experience into what goes into making a game engine.

I’m building it in DragonRuby, because I’ve been a Rubyist for 7+ years, it’s designed specifically to be beginner-friendly, and the licensing is straightforward and relatively inexpensive for what it provides.

Here's a quick demo of where it's at so far

Below is an overview of the engine I’m trying to make. The philosophy I’m taking, some of the architectural details, and some of the ways I’ve solved specific problems. Hopefully it’ll be useful if you’re building your own game engine, or are just curious about DragonRuby in general.

What kind of game am I trying to make?

I dunno! Right now I’m just working on the foundation, essential stuff that every game needs. I explain this cavalier attitude below.

Table of Contents


Middleware-centric framework

DragonRuby is really interesting as a framework, because it inverts a lot of traditional expectations for a game engine. It only provides the bare essentials for rendering a program on a screen, and you the code what you need, as you need it.

This makes it freeform and super exciting to play with (just like Ruby itself!). I’ve described it as being similar to the early 2000s, when the Web’s specs were small enough that you could feasibly write your own browser.

I feel this section From my DragonRuby guide summarizes it best:

Okay, so how do I make a pretty game in DragonRuby? Sprites are the answer to everything, followed by labels. The other rendering primitives (solids & lines) are only useful from a debugging standpoint. DragonRuby’s design philosophy is:

  • Use a separate workflow to generate the assets you need for the game (eg: sprites & sounds)
  • Reference all those assets inside your DragonRuby code
  • Use DragonRuby as the “glue”. It’s a framework, and will handle rendering the game, giving you simple helpers you can chain together

Since this is a hobby for me, I want it to be fun to build out a game over time. So I’m using proven, high-quality open-source (or low-cost) middleware for a lot of the asset generation and game’s details, and then writing DragonRuby code that ties all the assets together.

Hot-reload as much as possible

One of the most exciting features of DragonRuby is that it has hot-reloading built in: once you save the code, the game is automatically reloaded with the new content. As the engine gets more complex. I want to preserve that as much as possible, because it makes iterating very fun.

No expectations

Again, this is a hobby, so I’m trying not to set any sort of expectations for what it should be, or timelines. I work on it as I have time, and based on what I want to do. Right now that’s a bit tricky because I’m building the foundation, so it’s a lot of “writing ‘boring’ code that doesn’t translate into a playable game” (I love writing this kind of code though). Once I get past the foundational hurdles, things should get more interesting.


As the engine stands right now, this is the architecture:


Graphics (✅)

I’ve been using some free assets from Pixel Frog on I know I’ll be using Aseprite to make my own pixel art, because it’s one of the best tools out there for pixel art & sprite editing.

Maps (✅)

I’m using Tiled, which is a fantastic map editor. I’ll go into the details in a later section, but it’s very cool to see a tool like this actively being sponsored as an open-source project.

Dialog (in-progress)

Initially I was thinking of using Yarn, but I wasn’t very happy with the UI once I started doing more with it.

After some more searching, I think I’m going to try ink, which looks much more like my style:

Scripting (❓)

I don’t know what I’m going to do for scripting yet. Some ideas:

Music/Sounds (😅)

No idea, to be honest. I don’t know where to begin with this, so I’m saving it for a later version.

Game Code

DragonRuby games have the following pattern:

or, put another way:

def tick(args)

Game Code vs. Engine

The Game Code is what’s written in main.rb, the essential glue that makes the game possible. The Engine is more structured code, broken up into different systems that interact with each other. The Game Code fires off specific methods in the engine, which is how the systems start working.

The Game Code handles:

  1. Initializing (& hot reloading) the components of the game engine:
      if ! ||='maps/map-1.json')
        args.state.player ||=, 0)
  2. Storing the inputs from the user:
      args.state.move_x = 0
      if args.inputs.keyboard.key_held.right
        args.state.move_x = 1
  3. Passing the inputs onto the game engine:
      def calc(args)
        args.state.player.move(0, args.state.gravity.calculate_player_delta_y)
        args.state.player.move(args.state.move_x * 5, args.state.move_y)
  4. Triggering a render:
      def render(args)
        args.outputs.labels << [10, 20, args.gtk.current_framerate.to_i, -5]

I like keeping things inside the Engine, because it helps break the game down into a series of interacting systems, rather than becoming spaghetti code.


The code I’ve written in DragonRuby ties all the assets together into a cohesive interactive game.

Initially the engine was all written into main.rb, but I eventually broke it up into smaller files, because that’s how I like to look at my code. The requires block for the engine gives a pretty good overview of what’s there right now:

require 'app/engine/file_helpers.rb'
require 'app/engine/tile.rb'
require 'app/engine/sprite.rb'
require 'app/engine/animated_sprite.rb'

require 'app/engine/game_map.rb'
require 'app/engine/tileset.rb'
require 'app/engine/collision_layer.rb'
require 'app/engine/game_world.rb'
require 'app/engine/camera.rb'

require 'app/engine/gravity.rb'
require 'app/engine/player.rb'

I’ll try to break down how the major sections work below:


There are 2 classes for sprites: Sprite and AnimatedSprite. Whenever I need to add a sprite to the game, I instantiate an instance of one of these 2 classes. They are specifically defined with the attr_sprite declaration that DragonRuby needs to use this instance as a sprite (this fixes a performance issue I ran into when loading a large map):

def initialize(map_x, map_y, sprite_options)
  self.map_x = map_x
  self.map_y = map_y
  self.sprite =

The sprite class is pretty straightforward:

class Sprite

  attr_accessor :original_w, :original_h

  def initialize(options)
    @x = options[:x]
    @y = options[:y]
    @w = options[:w]
    @h = options[:h]
    @r = options[:r]
    @g = options[:g]
    @b = options[:b]
    @a = options[:a]
    @tile_x = options[:tile_x]
    @tile_y = options[:tile_y]
    @tile_w = options[:tile_w]
    @tile_h = options[:tile_h]
    @angle = options[:angle]
    @angle_anchor_x = options[:angle_anchor_x]
    @angle_anchor_y = options[:angle_anchor_y]
    @flip_horizontally = options[:flip_horizontally]
    @flip_vertically = options[:flip_vertically]
    @path = options[:path]

    self.original_w = @w
    self.original_h = @h

  # more on these later

  def scale!(scale_factor)
    @w = original_w * scale_factor
    @h = original_h * scale_factor

  def set_frame!
    return nil

AnimatedSprite is a subclass of Sprite, with some additional options to support animation, which I pulled from the same sample code that DragonRuby comes with:

class AnimatedSprite < Sprite
  attr_accessor :number_of_sprites,

  def initialize(options)

    self.start_looping_at = options[:start_looping_at] || 0
    self.looping_sprite = options[:looping_sprite] || false

    self.number_of_sprites = options[:number_of_sprites]
    self.number_of_frames_to_show_each_sprite = options[:number_of_frames_to_show_each_sprite]

  def set_frame!
    frame = start_looping_at.frame_index(

    return if frame.nil?

    self.tile_x = frame * self.tile_w

Sprite also has a set_frame! method, which returns nil; so that I can easily update both types of sprite in the rendering loop

I’ll get into scale! later. It’s part of how the game is actually rendered on the screen (but it can also be used for other purposes, like enlarging a player after they use a power-up)

Loading Tiled maps & tilesets

I’m writing my own parse for Tiled files, because they export as JSON and the spec is pretty straightforward

The Tiled code is its own sub-engine, with the following structure:


The player class is pretty rudimentary right now. When Player is instantiated, it is positioned in the game world’s X/Y coordinates (more on this later). It also sets up all the sprites that will be needed, so I don’t have to prepare them during a rendering cycle.

I made sure to have 2 types of movement methods:

This allows me to reuse the same logic when positioning on my own, or animating the player based on the inputs.


GameWorld is a central class that ties everything together. It hold references to:


When setting up the game world, you need to have an initialized GameMap, Player, and Camera. I didn’t want to overload the class with all of the initialization logic; its purpose is manipulating the existing instances to build the world.

As part of its initialization method it builds the tile cache, based on the layers of the GameMap. The structure of the method is:

  1. Check that the layer is visible and has data
  2. Get the positioning, opacity, and dimensions of the layer
  3. Check if this layer should be added to the collision layer
  4. For the layer’s data array:
  5. Split the array into rows, based on the layer’s width
  6. Iterate through each row, and for each column: 1. Calculate the tile’s x/y position in the game world 2. Get the tile’s sprite options 3. Add the tile to the cached_tiles 4. Apply the tile to the collision layer, if necessary
The Game World’s coordinate system, and the Camera

There are actually 2 coordinate systems that are in the game engine:

I hate working in multiple coordinate systems, so I use the game world for everything, and try to move everything over to the camera coordinates as late as possible.

The Camera has the following properties:

The camera is actually what handles rendering the world onto the screen:

def render
Translating the game world into the camera

The camera has an area that it covers in the Game World’s coordinate space, which I’ve called camera_box:

def world_width
  @world_width ||= width/window_scale.abs

def world_height
  @world_height ||= height/window_scale.abs

# The camera box needs to be inversely scaled, since it's either
# zooming in, or zooming out of the map
def camera_box
  [world_x, world_y, world_width, world_height]

We can use camera_box to determine what tiles intersect with the camera’s viewport, and only render those ones. The structure of the method is:

  1. For each of the world’s GameWorld.cached_tiles:
  2. Get the rectangle that defines the tile, scaled the to match the window’s scale (using Tile.dimensions)
  3. Check if the tile intersects with the camera_box If so: 3. Convert the tile’s Game World X/Y coordinates into the Camera’s X/Y coordinates, giving us the sprite_x/y 4. Position the Tile.sprite based on the sprite_x/y 5. Call tile.sprite.scale!(window_scale) to scale the tile 6. Render the tile
This is a bit hard to explain with words, so below is a demo of a smaller camera window, and some debugging code where I render the map outside of the camera's window as semi-transparent

Below is the full class, if you’re curious:

Camera Class
class Camera
  attr_accessor :window_x, :window_y, :window_scale, :world_x, :world_y, :width, :height, :world, :args

  def draw_debug_border
    args.outputs.borders << [window_x, window_y, width, height]

  def initialize(window_x, window_y, window_scale, world_x, world_y, width, height, world, args)
    self.window_x = window_x
    self.window_y = window_y
    self.window_scale = window_scale
    self.world_x = world_x
    self.world_y = world_y
    self.width = width
    self.height = height = world
    self.args = args

  def render

  def world_width
    @world_width ||= width/window_scale.abs

  def world_height
    @world_height ||= height/window_scale.abs

  # The camera box needs to be inversely scaled, since it's either
  # zooming in, or zooming out of the map
  def camera_box
    [world_x, world_y, world_width, world_height]

  # Get the relative position of the tile (relative to the camera
        # then add the x/y that the camera is offset from the window itself
  def convert_x_coordinate(given_world_x)
    ((given_world_x - self.world_x) * self.window_scale) + self.window_x

  def convert_y_coordinate(given_world_y)
    ((given_world_y - self.world_y) * self.window_scale) + self.window_y

  def render_player
    world.player.update_sprite(convert_x_coordinate(world.player.x),  convert_y_coordinate(world.player.y))
    args.outputs.sprites << world.player.sprite_for_render

  def render_intersecting_tiles
    world.cached_tiles.each do |tile|
      # Currently just render the whole map for debugging purposes
      if tile.dimensions(window_scale).intersect_rect?(camera_box)
        sprite_x = convert_x_coordinate(tile.map_x)
        sprite_y = convert_y_coordinate(tile.map_y)

        tile.sprite.x = sprite_x
        tile.sprite.y = sprite_y

        args.outputs.sprites << tile.sprite

CollisionLayer (in-progress)

When loading a map, I add tiles to the CollisionLayer. This creates a series of shapes (usually squares, but I want to support other types in the future) in the game world’s coordinate space.

This class is still in progress. You can see I was experimenting with using a subgrid algorithm to help speed up collision detection, but I removed it until I can revisit the problem.

class CollisionLayer
  attr_accessor :collision_rectangles, :collision_subgrid


  def initialize
    self.collision_rectangles = []
    self.collision_subgrid = {}

  def add_collision_shape(shape_details)
    normalized_x = shape_details[:x] + shape_details[:shape_x]
    normalized_y = shape_details[:y] + shape_details[:shape_y]

    rectangle = [normalized_x, normalized_y, shape_details[:shape_width], shape_details[:shape_height]]
    self.collision_rectangles << rectangle
    x_subgrid = subgrid_range(normalized_x)
    y_subgrid = subgrid_range(normalized_y)

    collision_subgrid[x_subgrid] ||= {}
    collision_subgrid[x_subgrid][y_subgrid] ||= []
    collision_subgrid[x_subgrid][y_subgrid] << rectangle

  def subgrid_range(value)

  def collision?(x, y)
    # x_subgrid = self.collision_subgrid.find{|k,v| k == subgrid_range(x) }
    # return false if x_subgrid.nil?

    # y_subgrid = x_subgrid[1].find{|k,v| k == subgrid_range(y) }
    # return false if y_subgrid.nil?

    collision_rectangles.any?{|rectangle| [x,y].inside_rect?(rectangle)}

  def draw_debug_rects(args)
    self.collision_rectangles.each do |rectangle|
      args.outputs.solids << rectangle

  def self.shape_from_tile(tile_data)
    shape = tile_data["objectgroup"]["objects"].first

    return {
      shape_height: shape["height"],
      shape_width: shape["width"],
      shape_x: shape["x"],
      shape_y: shape["y"],
      shape_rotation: shape["rotation"]

When the GameWorld processes the layers in its GameMap, it adds the collision shapes to the collision layer:

# ... game_world.rb
if apply_collision
  collision_shape = map.tile_collision_shape_by_id(tile)
  next if collision_shape.nil?
    x: tile_x,
    y: tile_y,


# ... tileset.rb
def get_tile_collision_shape(id)
  return if self.tiles[id].nil?
  return unless self.tiles[id].has_key?("objectgroup")


We can then check if there’s a collision at the given x,y coordinates:

collision_layer.collision?(player.x, player.x)

Gravity (in-progress)

The physics engine is very rudimentary, mainly just calculating the player’s delta-y when they jump. This needs expanded.

class Gravity
  attr_accessor :player, :collision_layer


  def initialize(player, collision_layer)
    self.player = player
    self.collision_layer = collision_layer

  def calculate_player_delta_y
    return 0 if collision_layer.collision?(player.x, player.y)