Developing Games With Ruby (2014)
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
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
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.