Simulating Physics - Developing Games With Ruby (2014)

Developing Games With Ruby (2014)

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.