Implementing Health And Damage - Developing Games With Ruby (2014)

Developing Games With Ruby (2014)

Implementing Health And Damage

I know you have been waiting for this. We will be implementing health system and most importantly, damage. Soo we will be ready to blow things up.

To implement this, we need to:

1. Add TankHealth component. Start with 100 health.

2. Render tank health next to tank itself.

3. Inflict damage to tank when it is in explosion zone

4. Render different sprite for dead tank.

5. Cut off player input when tank is dead.

Adding Health Component

If we didn’t have Component system in place, it would be way more difficult. Now we just kick in a new class:

07-damage/entities/components/tank_health.rb


1 class TankHealth < Component

2 attr_accessor :health

3

4 def initialize(object, object_pool)

5 super(object)

6 @object_pool = object_pool

7 @health = 100

8 @health_updated = true

9 @last_damage = Gosu.milliseconds

10 end

11

12 def update

13 update_image

14 end

15

16 def update_image

17 if @health_updated

18 if dead?

19 text = '✝'

20 font_size = 25

21 else

22 text = @health.to_s

23 font_size = 18

24 end

25 @image = Gosu::Image.from_text(

26 $window, text,

27 Gosu.default_font_name, font_size)

28 @health_updated = false

29 end

30 end

31

32 def dead?

33 @health < 1

34 end

35

36 def inflict_damage(amount)

37 if @health > 0

38 @health_updated = true

39 @health = [@health - amount.to_i, 0].max

40 if @health < 1

41 Explosion.new(@object_pool, x, y)

42 end

43 end

44 end

45

46 def draw(viewport)

47 @image.draw(

48 x - @image.width / 2,

49 y - object.graphics.height / 2 -

50 @image.height, 100)

51 end

52 end


It hooks itself into the game right away, after we initialize it in Tank class:

class Tank < GameObject

attr_accessor :health

# ...

def initialize(object_pool, input)

# ...

@health = TankHealth.new(self, object_pool)

# ..

end

# ..

end

Inflicting Damage With Bullets

There are two ways to inflict damage - directly and indirectly. When bullet hits enemy tank (collides with tank bounding box), we should inflict direct damage. It can be done in BulletPhysics#check_hit method that we already had:

class BulletPhysics < Component

# ...

def check_hit

@object_pool.nearby(object, 50).each do |obj|

next if obj == object.source # Don't hit source tank

if Utils.point_in_poly(x, y, *obj.box)

# Direct hit - extra damage

obj.health.inflict_damage(20)

object.target_x = x

object.target_y = y

return

end

end

end

# ...

end

Finally, Explosion itself should inflict additional damage to anything that are nearby. The effect will be diminishing and it will be determined by object distance.

class Explosion < GameObject

# ...

def initialize(object_pool, x, y)

# ...

inflict_damage

end

private

def inflict_damage

object_pool.nearby(self, 100).each do |obj|

if obj.class == Tank

obj.health.inflict_damage(

Math.sqrt(3 * 100 - Utils.distance_between(

obj.x, obj.y, x, y)))

end

end

end

end

This is it, we are ready to deal damage. But we want to see if we actually killed somebody, so TankGraphics should be aware of health and should draw different set of sprites when tank is dead. Here is what we need to change in our current TankGraphics to achieve the result:

class TankGraphics < Component

# ...

def initialize(game_object)

super(game_object)

@body_normal = units.frame('tank1_body.png')

@shadow_normal = units.frame('tank1_body_shadow.png')

@gun_normal = units.frame('tank1_dualgun.png')

@body_dead = units.frame('tank1_body_destroyed.png')

@shadow_dead = units.frame('tank1_body_destroyed_shadow.png')

@gun_dead = nil

end

def update

if object.health.dead?

@body = @body_dead

@gun = @gun_dead

@shadow = @shadow_dead

else

@body = @body_normal

@gun = @gun_normal

@shadow = @shadow_normal

end

end

def draw(viewport)

@shadow.draw_rot(x - 1, y - 1, 0, object.direction)

@body.draw_rot(x, y, 1, object.direction)

@gun.draw_rot(x, y, 2, object.gun_angle) if @gun

end

# ...

end

Now we can blow them up and enjoy the view:

Target practice

Target practice

But what if we blow ourselves up by shooting nearby? We would still be able to move around. To fix this, we will simply cut out player input when we are dead:

class PlayerInput < Component

# ...

def update

return if object.health.dead?

# ...

end

# ...

end

And to prevent tank from throttling forever if the pedal was down before it got killed:

class TankPhysics < Component

# ...

def update

if object.throttle_down && !object.health.dead?

accelerate

else

decelerate

end

# ...

end

# ...

end

That’s it. All we need right now is some resistance from those brain dead enemies. We will spark some life into them in next chapter.