Simulating Physics

To make the game more realistic, we will spice things up with some physics. This is the feature set we are going to implement:

  1. Collision detection. Tank will bump into other objects - stationary tanks. Bullets will not go through them either.
  2. Terrain effects. Tank will go fast on grass, slower on sand.

Adding Enemy Objects

It’s boring to play alone, so we will make a quick change and spawn some stationary tanks that will be deployed randomly around the map. They will be stationary in the beginning, but we will still need a dummy AI class to replace PlayerInput:

06-physics/entities/components/ai_input.rb


1 class AiInput < Component
2   def control(obj)
3     self.object = obj
4   end
5 end

A quick and dirty way to spawn some tanks would be when initializing PlayState:

class PlayState < GameState
  # ...
  def initialize
    @map = Map.new
    @camera = Camera.new
    @object_pool = ObjectPool.new(@map)
    @tank = Tank.new(@object_pool, PlayerInput.new(@camera))
    @camera.target = @tank
    # ...
    50.times do
      Tank.new(@object_pool, AiInput.new)
    end
  end
  # ...
end

And unless we want all stationary tanks face same direction, we will randomize it:

class Tank < GameObject
  # ...
  def initialize(object_pool, input)
    # ...
    @direction = rand(0..7) * 45
    @gun_angle = rand(0..360)
  end
  # ...
end

Fire up the game, and wander around frozen tanks. You can pass through them as if they were ghosts, but we will fix that in a moment.

Brain dead enemies

Brain dead enemies

Adding Bounding Boxes And Detecting Collisions

We want our collision detection to be pixel perfect, that means we need to have a bounding box and check colisions against it. Get ready for some math!

First, we need to find a correct way to construct a bounding box. Tank has it’s body image, so let’s see how it’s boundaries look like. We will add some code to TankGraphics component to see it:

class TankGraphics < Component
  def draw(viewport)
    # ...
    draw_bounding_box
  end

  def draw_bounding_box
    $window.rotate(object.direction, x, y) do
      w = @body.width
      h = @body.height
      $window.draw_quad(
        x - w / 2, y - h / 2, Gosu::Color::RED,
        x + w / 2, y - h / 2, Gosu::Color::RED,
        x + w / 2, y + h / 2, Gosu::Color::RED,
        x - w / 2, y + h / 2, Gosu::Color::RED,
        100)
    end
  end
  # ...
end

Result is pretty good, we have tank shaped box, so we will be using body image dimensions to determine our bounding box corners:

Tank's bounding box visualized

Tank’s bounding box visualized

There is one problem here though. Gosu::Window#rotate does the rotation math for us, and we need to perform these calculations on our own. We have four points that we want to rotate around a center point. It’s not very difficult to find how to do this. Here is a Ruby method for you:

module Utils
  # ...
  def self.rotate(angle, around_x, around_y, *points)
    result = []
    points.each_slice(2) do |x, y|
      r_x = Math.cos(angle) * (x - around_x) -
        Math.sin(angle) * (y - around_y) + around_x
      r_y = Math.sin(angle) * (x - around_x) +
        Math.cos(angle) * (y - around_y) + around_y
      result << r_x
      result << r_y
    end
    result
  end
  # ...
end

We can now calculate edges of our bounding box, but we need one more function which tells if point is inside a polygon. This problem has been solved million times before, so just poke the internet for it and drink from the information firehose until you understand how to do this.

If you wasn’t familiar with the term yet, by now you should discover what vertex is. In geometry, a vertex (plural vertices) is a special kind of point that describes the corners or intersections of geometric shapes.

Here’s what I ended up writing:

module Utils
  # ...
  # http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
  def self.point_in_poly(testx, testy, *poly)
    nvert = poly.size / 2 # Number of vertices in poly
    vertx = []
    verty = []
    poly.each_slice(2) do |x, y|
      vertx << x
      verty << y
    end
    inside = false
    j = nvert - 1
    (0..nvert - 1).each do |i|
      if (((verty[i] > testy) != (verty[j] > testy)) &&
         (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) /
         (verty[j] - verty[i]) + vertx[i]))
        inside = !inside
      end
      j = i
    end
    inside
  end
  # ...

It is Jordan curve theorem reimplemented in Ruby. Looks ugly, but it actually works, and is pretty fast too.

Also, this works on more sophisticated polygons, and our tank is shaped more like an H rather than a rectangle, so we could define a pixel perfect polygon. Some pen and paper will help.

class TankPhysics < Component
  #...

  # Tank box looks like H. Vertices:
  # 1   2   5   6
  #     3   4
  #
  #    10   9
  # 12 11   8   7
  def box
    w = box_width / 2 - 1
    h = box_height / 2 - 1
    tw = 8 # track width
    fd = 8 # front depth
    rd = 6 # rear depth
    Utils.rotate(object.direction, x, y,
                 x + w,      y + h,      #1
                 x + w - tw, y + h,      #2
                 x + w - tw, y + h - fd, #3

                 x - w + tw, y + h - fd, #4
                 x - w + tw, y + h,      #5
                 x - w,      y + h,      #6

                 x - w,      y - h,      #7
                 x - w + tw, y - h,      #8
                 x - w + tw, y - h + rd, #9

                 x + w - tw, y - h + rd, #10
                 x + w - tw, y - h,      #11
                 x + w,      y - h,      #12
                )
  end
  # ...
end

To visually see it, we will improve our draw_bounding_box method:

class TankGraphics < Component
  # ...
  DEBUG_COLORS = [
    Gosu::Color::RED,
    Gosu::Color::BLUE,
    Gosu::Color::YELLOW,
    Gosu::Color::WHITE
  ]
  # ...
  def draw_bounding_box
    i = 0
    object.box.each_slice(2) do |x, y|
      color = DEBUG_COLORS[i]
      $window.draw_triangle(
        x - 3, y - 3, color,
        x,     y,     color,
        x + 3, y - 3, color,
        100)
      i = (i + 1) % 4
    end
  end
  # ...

Now we can visually test bounding box edges and see that they actually are where they belong.

High precision bounding boxes

High precision bounding boxes

Time to pimp our TankPhysics to detect those collisions. While our algorithm is pretty fast, it doesn’t make sense to check collisions for objects that are pretty far apart. This is why we need our ObjectPool to know how to query objects in close proximity.

class ObjectPool
  # ...
  def nearby(object, max_distance)
    @objects.select do |obj|
      distance = Utils.distance_between(
        obj.x, obj.y, object.x, object.y)
      obj != object && distance < max_distance
    end
  end
end

Back to TankPhysics:

class TankPhysics < Component
  # ...
  def can_move_to?(x, y)
    old_x, old_y = object.x, object.y
    object.x = x
    object.y = y
    return false unless @map.can_move_to?(x, y)
    @object_pool.nearby(object, 100).each do |obj|
      if collides_with_poly?(obj.box)
        # Allow to get unstuck
        old_distance = Utils.distance_between(
          obj.x, obj.y, old_x, old_y)
        new_distance = Utils.distance_between(
          obj.x, obj.y, x, y)
        return false if new_distance < old_distance
      end
    end
    true
  ensure
    object.x = old_x
    object.y = old_y
  end
  # ...
  private

  def collides_with_poly?(poly)
    if poly
      poly.each_slice(2) do |x, y|
        return true if Utils.point_in_poly(x, y, *box)
      end
      box.each_slice(2) do |x, y|
        return true if Utils.point_in_poly(x, y, *poly)
      end
    end
    false
  end
  # ...
end

It’s probably not the most elegant solution you could come up with, but can_move_to? temporarily changes Tank location to make a collision test, and then reverts old coordinates just before returning the result. Now our tanks stop with banging sound when they hit each other.

Tanks colliding

Tanks colliding

Catching Bullets

Right now bullets fly right through our tanks, and we want them to collide. It’s a pretty simple change, which mostly affects BulletPhysics class:

# 06-physics/entities/components/bullet_physics.rb
class BulletPhysics < Component
  # ...
  def update
    # ...
    check_hit
    object.explode if arrived?
  end
  # ...
  private

  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)
        object.target_x = x
        object.target_y = y
        return
      end
    end
  end
  # ...
end

Now bullets finally hit, but don’t do any damage yet. We will come back to that soon.

Bullet hitting enemy tank

Bullet hitting enemy tank

Implementing Turn Speed Penalties

Tanks cannot make turns and go into reverse at full speed while keeping it’s inertia, right? It is easy to implement. Since it’s related to physics, we will delegate changing Tank’s @direction to our TankPhysics class:

# 06-physics/entities/components/player_input.rb
class PlayerInput < Component
  # ...
  def update
    # ...
    motion_buttons = [Gosu::KbW, Gosu::KbS, Gosu::KbA, Gosu::KbD]

    if any_button_down?(*motion_buttons)
      object.throttle_down = true
      object.physics.change_direction(
        change_angle(object.direction, *motion_buttons))
    else
      object.throttle_down = false
    end
    # ...
  end
  # ...
end

# 06-physics/entities/components/tank_physics.rb
class TankPhysics < Component
  # ...
  def change_direction(new_direction)
    change = (new_direction - object.direction + 360) % 360
    change = 360 - change if change > 180
    if change > 90
      @speed = 0
    elsif change > 45
      @speed *= 0.33
    elsif change > 0
      @speed *= 0.66
    end
    object.direction = new_direction
  end
  # ...
end

Implementing Terrain Speed Penalties

Now, let’s see how can we make terrain influence our movement. It sounds reasonable for TankPhysics to consult with Map about speed penalty of current tile:

# 06-physics/entities/map.rb
class Map
  # ...
  def movement_penalty(x, y)
    tile = tile_at(x, y)
    case tile
    when @sand
      0.33
    else
      0
    end
  end
  # ...
end

# 06-physics/entities/components/tank_physics.rb
class TankPhysics < Component
  # ...
  def update
    # ...
      speed = apply_movement_penalty(@speed)
      shift = Utils.adjust_speed(speed)
    # ...
  end
  # ...

  private

  def apply_movement_penalty(speed)
    speed * (1.0 - @map.movement_penalty(x, y))
  end
  # ...
end

This makes all tanks move 33% slower on sand.