Implementing Powerups - Developing Games With Ruby (2014)

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

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.