This weekend, I played around with gosu after a talk at RubyConf, making a copy of Missile Command in Ruby. Without some important points from the talk, I would be up the creek, but there were some design patterns not brought up that felt emergent when writing game code.

missile command

Use constants for “magic numbers”

You’ll use numbers everywhere. How many pixels should something move each frame? What is the Z-index of assets? I found it useful to put every number into a constant and put them in the same place (where reasonable). My Utility module ended up looking like this:

module Utility

  FIRING_RATE = 500 # Lower equals higher firing rate
  PROJECTILE_SPEED = 5  # If this gets too high and bullets don't arrive, increase PROJECTILE_PROXIMITY
  PROJECTILE_PROXIMITY = 2
  CURSOR_SPEED = 4
  EXPLOSION_SPEED = 2
  SPACESHIP_SPEED = 1

  module ZIndex
    BACKGROUND = 0
    GROUND     = 1
    HEALTH_BAR = 1
    BUNKER     = 2
    SPACESHIP  = 2
    DEFENDER   = 2
    EXPLOSION  = 3
    CURSOR     = 4
    PROJECTILE = 5
    SCORE      = 6
    DEBUG      = 99
  end
end

When parts of the game feel unrealistic (how fast an enemy moves, how big explosions get), all my numbers are in one place for tweaking.

Draw your hit boxes when debugging collision

I wrote a collision library and, like all parts of my game, I did not test it. When I couldn’t figure out why two objects weren’t registering a collision when they were overlapping, I added a utility function to take an object and draw its width/height around it.

module Utility
  module Debug
    def self.trace(obj, c = nil)
      c ||= Explosion::COLOR
      # Left
      Gosu.draw_line(*obj.top_left, c, obj.top_left[0], obj.bottom_right[1], c, ZIndex::DEBUG, mode = :default)
      # Top
      Gosu.draw_line(*obj.top_left, c, obj.bottom_right[0], obj.top_left[1], c, ZIndex::DEBUG, mode = :default)
      # Right
      Gosu.draw_line(*obj.bottom_right, c, obj.bottom_right[0], obj.top_left[1], c, ZIndex::DEBUG, mode = :default)
      # Bottom
      Gosu.draw_line(*obj.bottom_right, c, obj.top_left[0], obj.bottom_right[1], c, ZIndex::DEBUG, mode = :default)
    end
  end
end

Use keyword args everywhere

Using keyword args makes the message fail in the sender, not the receiver. Usually it’s the sender that’s responsible for sending the wrong arguments, so this shifts the responsibility to the sender by ensuring that every expected argument will be present. Avoid default arguments and nil checks as well.

Abstract common behaviors into modules

This is a given in programming for all applications, but can be super useful when multiple classes behave in more or less the exact same way. If you have 3 types of enemies that are all starships – they change direction when they hit the end of the screen, they fire at a certain rate, they have a particular speed – that can all be abstracted into a module and methods overwritten as needed.

module Mixins
  module Ship
    def self.included(base)
      base.class_eval do
        def damage!(projectile)
          @damage += projectile.damage_value
          if damage >= health
            # Handle being destroyed
          end
        end

        private

        def speed
          # Look up speed from CONSTANT in Utility module
          table_name = "Utility::" + "#{self.class}".split('::').last.upcase + "_SPEED_TABLE"
          Object.const_get(table_name)[@health]
        end

        def health
          # Look up health from CONSTANT in Utility module
          # ...
        end


        def move
          # Move back and forth at speed in direction
        end

      end
    end
  end
end

Then when I make a new class that behaves like a Ship, I just have to include the module, add a speed entry in the Utility CONSTANT, and overwrite methods as necessary:

module Enemy
  class Fortress
    include Mixins::Ship

    WIDTH = 60
    HEIGHT = 60

    private

    # Health in this case is a fixed value, always
    def health
      @health ||= 100
    end

    # Change the image
    def image
      @image ||= Gosu::Image.new("assets/death_star.png")
    end
  end
end

Store levels in a collection

In your main loop, delegate the current action (update, draw) to the current_level variable. Levels should all have a common API, like a .done? method, so in your main loop you say:

current_level = levels.shift if current_level.done?

Generate levels from a config file

Use YML, JSON, or some other format to store information for each level. When the game loads, instantiate a collection of levels that correspond to entries in the file. This way, you can easily change what’s in a particular level by modifying the config file. Like the idea of constants in a Utility module, it’s one place where you can modify far away behavior.

class Levels::Collection

  def initialize(game:)
    @levels = YAML
              .load_file("./config/levels.yml")
              .collect do |_, details|
                Levels::Base.new(game: game, details: details)
              end
  end
end
one:
  spaceships:
    number: 3
  battleships:
    number: 0
  fortresses:
    number: 0
  bunkers:
    number: 1
    ammo: 10
    health: medium
  defenders:
    number: 0

two:
  spaceships:
    number: 5
    height: medium
    weapons: easy
  battleships:
    number: 0
  fortresses:
    number: 0
  bunkers:
    number: 1
    ammo: 10
    health: medium
  defenders:
    number: 0

The main loop of my game just looks like this:

class Game < Gosu::Window
  # ...

  # Called once each frame
  def update
    return if game_over
    current_level.update
    check_game_over
    if !game_over && current_level.over?
      @current_level = levels.shift 
    end
  end

  # ...
end

The code for my game is available on my Github and thanks for reading!

Updated: