Implementing Powerups

Game would become more strategic if there were ways to repair your damaged tank, boost it’s speed or increase rate of fire by picking up various powerups. This should not be too difficult to implement. We will use some of these images:

Powerups

Powerups

For now, there will be four kinds of powerups:

  1. Repair damage. Wrench badge will restore damaged tank’s health back to 100 when picked up.
  2. Health boost. Green +1 badge will add 25 health, up to 200 total, if you keep picking them up.
  3. Fire boost. Double bullet badge will increase reload speed by 25%, up to 200% if you keep picking them up.
  4. Speed boost. Airplane badge will increase movement speed by 10%, up to 150% if you keep picking them up

These powerups will be placed randomly around the map, and will automatically respawn 30 seconds after pickup.

Implementing Base Powerup

Before rushing forward to implement this, we have to do some research and think how to elegantly integrate this into the whole game. First, let’s agree that Powerup is a GameObject. It will have graphics, sounds and it’s coordinates. Effects can by applied by harnessing GameObject#on_collision - when Tank collides with Powerup, it gets it.

11-powerups/entities/powerups/powerup.rb


 1 class Powerup < GameObject
 2   def initialize(object_pool, x, y)
 3     super
 4     PowerupGraphics.new(self, graphics)
 5   end
 6 
 7   def box
 8     [x - 8, y - 8,
 9      x + 8, y - 8,
10      x + 8, y + 8,
11      x - 8, y + 8]
12   end
13 
14   def on_collision(object)
15     if pickup(object)
16       PowerupSounds.play(object, object_pool.camera)
17       remove
18     end
19   end
20 
21   def pickup(object)
22     # override and implement application
23   end
24 
25   def remove
26     object_pool.powerup_respawn_queue.enqueue(
27       respawn_delay,
28       self.class, x, y)
29     mark_for_removal
30   end
31 
32   def respawn_delay
33     30
34   end
35 end

Ignore Powerup#remove, we will get to it when implementing PowerupRespawnQueue. The rest should be straightforward.

Implementing Powerup Graphics

All powerups will use the same sprite sheet, so there could be a single PowerupGraphics class that will be rendering given sprite type. We will use gosu-texture-packer gem, since sprite sheet is conveniently packed already.

11-powerups/entities/components/powerup_graphics.rb


 1 class PowerupGraphics < Component
 2   def initialize(object, type)
 3     super(object)
 4     @type = type
 5   end
 6 
 7   def draw(viewport)
 8     image.draw(x - 12, y - 12, 1)
 9     Utils.mark_corners(object.box) if $debug
10   end
11 
12   private
13 
14   def image
15     @image ||= images.frame("#{@type}.png")
16   end
17 
18   def images
19     @@images ||= Gosu::TexturePacker.load_json(
20       $window, Utils.media_path('pickups.json'))
21   end
22 end

Implementing Powerup Sounds

It’s even simpler with sounds. All powerups will emit a mellow “bleep” when picked up, so PowerupSounds can be completely static, like ExplosionSounds or BulletSounds:

11-powerups/entities/components/powerup_sounds.rb


 1 class PowerupSounds
 2   class << self
 3     def play(object, camera)
 4       volume, pan = Utils.volume_and_pan(object, camera)
 5       sound.play(object.object_id, pan, volume)
 6     end
 7 
 8     private
 9 
10     def sound
11       @@sound ||= StereoSample.new(
12         $window, Utils.media_path('powerup.mp3'))
13     end
14   end
15 end

Implementing Repair Damage Powerup

Repairing broken tank is probably the most important powerup of them all, so let’s implement it first:

11-powerups/entities/powerups/repair_powerup.rb


 1 class RepairPowerup < Powerup
 2   def pickup(object)
 3     if object.class == Tank
 4       if object.health.health < 100
 5         object.health.restore
 6       end
 7       true
 8     end
 9   end
10 
11   def graphics
12     :repair
13   end
14 end

This was incredibly simple. Health#restore already existed since we had to respawn our tanks. We can only hope other powerups are as simple to implement as this one.

Implementing Health Boost

Repairing damage is great, but how about boosting some extra health for upcoming battles? Health boost to the rescue:

11-powerups/entities/powerups/health_powerup.rb


 1 class HealthPowerup < Powerup
 2   def pickup(object)
 3     if object.class == Tank
 4       object.health.increase(25)
 5       true
 6     end
 7   end
 8 
 9   def graphics
10     :life_up
11   end
12 end

This time we have to implement Health#increase, but it is pretty simple:

class Health < Component
  # ...
  def increase(amount)
    @health = [@health + 25, @initial_health * 2].min
    @health_updated = true
  end
  # ...
end

Since Tank has @initial_health equal to 100, increasing health won’t go over 200, which is exactly what we want.

Implementing Fire Rate Boost

How about boosting tank’s fire rate?

11-powerups/entities/powerups/fire_rate_powerup.rb


 1 class FireRatePowerup < Powerup
 2   def pickup(object)
 3     if object.class == Tank
 4       if object.fire_rate_modifier < 2
 5         object.fire_rate_modifier += 0.25
 6       end
 7       true
 8     end
 9   end
10 
11   def graphics
12     :straight_gun
13   end
14 end

We need to introduce @fire_rate_modifier in Tank class and use it when calling Tank#can_shoot?:

class Tank < GameObject
  # ...
  attr_accessor :fire_rate_modifier
  # ...
  def can_shoot?
    Gosu.milliseconds - (@last_shot || 0) >
      (SHOOT_DELAY / @fire_rate_modifier)
  end
  # ...
  def reset_modifiers
    @fire_rate_modifier = 1
  end
  # ...
end

Tank#reset_modifier should be called when respawning, since we want tanks to lose their powerups when they die. It can be done in TankHealth#after_death:

class TankHealth < Health
  # ...
  def after_death
    object.reset_modifiers
    # ...
  end
end

Implementing Tank Speed Boost

Tank speed boost is very similar to fire rate powerup:

11-powerups/entities/powerups/tank_speed_powerup.rb


 1 class TankSpeedPowerup < Powerup
 2   def pickup(object)
 3     if object.class == Tank
 4       if object.speed_modifier < 1.5
 5         object.speed_modifier += 0.10
 6       end
 7       true
 8     end
 9   end
10 
11   def graphics
12     :wingman
13   end
14 end

We have to add @speed_modifier to Tank class and use it in TankPhysics#update when calculating movement distance.

# 11-powerups/entities/tank.rb
class Tank < GameObject
  # ...
  attr_accessor :speed_modifier
  # ...
  def reset_modifiers
    # ...
    @speed_modifier = 1
  end
  # ...
end

# 11-powerups/entities/components/tank_physics.rb
class TankPhysics < Component
  # ...
  def update
    # ...
      new_x, new_y = x, y
      speed = apply_movement_penalty(@speed)
      shift = Utils.adjust_speed(speed) * object.speed_modifier
    # ...
  end
  # ...
end

Camera#update has also refer to Tank#speed_modifier, otherwise the operator will fail to catch up and camera will be lagging behind.

class Camera
  # ...
  def update
    # ...
    shift = Utils.adjust_speed(
      @target.physics.speed).floor *
      @target.speed_modifier + 1
    # ...
  end
  # ...
end

Spawning Powerups On Map

Powerups are implemented, but not yet spawned. We will spawn 20 - 30 random powerups when generating the map:

class Map
  # ...
  def initialize(object_pool)
    # ...
    generate_powerups
  end
  # ...
  def generate_powerups
    pups = 0
    target_pups = rand(20..30)
    while pups < target_pups do
      x = rand(0..MAP_WIDTH * TILE_SIZE)
      y = rand(0..MAP_HEIGHT * TILE_SIZE)
      if tile_at(x, y) != @water
        random_powerup.new(@object_pool, x, y)
        pups += 1
      end
    end
  end

  def random_powerup
    [HealthPowerup,
     RepairPowerup,
     FireRatePowerup,
     TankSpeedPowerup].sample
  end
  # ...
end

The code is very similar to generating boxes. It’s probably not the best way to distribute powerups on map, but it will have to do for now.

Respawning Powerups After Pickup

When we pick up a powerup, we want it to reappear in same spot 30 seconds later. A thought “we can start a new Thread with sleep and initialize the same powerup there” sounds very bad, but I had it for a few seconds. Then PowerupRespawnQueue was born.

First, let’s recall how Powerup#remove method looks like:

class Powerup < GameObject
  # ...
  def remove
    object_pool.powerup_respawn_queue.enqueue(
      respawn_delay,
      self.class, x, y)
    mark_for_removal
  end
  # ...
end

Powerup enqueues itself for respawn when picked up, providing it’s class and coordinates. PowerupRespawnQueue holds this data and respawns powerups at right time with help of ObjectPool:

11-powerups/entities/powerups/powerup_respawn_queue.rb


 1 class PowerupRespawnQueue
 2   RESPAWN_DELAY = 1000
 3   def initialize
 4     @respawn_queue = {}
 5     @last_respawn = Gosu.milliseconds
 6   end
 7 
 8   def enqueue(delay_seconds, type, x, y)
 9     respawn_at = Gosu.milliseconds + delay_seconds * 1000
10     @respawn_queue[respawn_at.to_i] = [type, x, y]
11   end
12 
13   def respawn(object_pool)
14     now = Gosu.milliseconds
15     return if now - @last_respawn < RESPAWN_DELAY
16     @respawn_queue.keys.each do |k|
17       next if k > now # not yet
18       type, x, y = @respawn_queue.delete(k)
19       type.new(object_pool, x, y)
20     end
21     @last_respawn = now
22   end
23 end

PowerupRespawnQeueue#respawn is called from ObjectPool#update_all, but is throttled to run once per second for better performance.

class ObjectPool
  # ...
  attr_accessor :powerup_respawn_queue
  # ...
  def update_all
    # ...
    @powerup_respawn_queue.respawn(self)
  end
  # ...
end

This is it, the game should now contain randomly placed powerups that respawn 30 seconds after picked up. Time to enjoy the result.

Playing with powerups

Playing with powerups

We haven’t done any changes to AI though, that means enemies will only be picking those powerups by accident, so now you have a significant advantage and the game has suddenly became too easy to play. Don’t worry, we will be fixing that when overhauling the AI.