5. The rules of the game

We’ve got to stop those missiles from flying around indefinitely. Let’s do that by giving them a fixed lifetime. Each time a missile is created, we’ll give it a positive time-to-live value (TTL) which we’ll decrement on every tick of the clock. When a missile’s TTL reaches zero, we’ll delete the missile.

This TTL value will be generally useful, so let’s add it as an attribute of the Sprite class.

New attribute for Sprites [ruby]
class Sprite < OSX::NSObject
  attr_accessor :position, :velocity, :radius, :color, :ttl

Initialize it to -1 in Sprite initWithPosition_. We’ll say that objects with negative TTL live indefinitely.

Initialize Time To Live in Sprite initializer [ruby]
  def initWithPosition_(position)
    @position = position
    @velocity = OSX::NSPoint.new(0, 0)
    @ttl = -1
    self
  end

Decrement the TTL value in Sprite moveWithBounds_. Add the following to the beginning of the method.

New code for Sprite moveWithBounds [ruby]
    @ttl -= 1 if @ttl > 0

Add MISSILE_LIFE = 50 to the top of your file, then add the following line to Missile initWithPosition_velocity_color_:

Initialize Time To Live for missiles [ruby]
   @ttl = MISSILE_LIFE

We need to remove missiles when their TTL value reaches zero. We can do this by adding a single line of Ruby code at the end of the Game tick method:

Remove expired missiles in Game tick [ruby]
    @missiles.delete_if {|missile| missile.ttl == 0}

Try it! Your missiles will now fly about halfway across the screen and then disappear.

One more step and we’ll have a playable game.

We need to detect collisions between objects. We can make this easy by treating all our objects as circles. Objects collide when the distance between their centers is less than the sum of their radii. We add a collision detection method to our Sprite class:

Check for collisions in the Sprite class [ruby]
  def collidesWith_?(sprite)
    dx = @position.x - sprite.position.x
    dy = @position.y - sprite.position.y
    r = @radius + sprite.radius
    return false if dx > r or -dx > r or dy > r or -dy > r
    dx*dx + dy*dy < r*r
  end

You may notice two tests in the above code. The first excludes obvious misses where the x or y coordinates are too far apart. The second test compares distance but uses distance squared to avoid calculating unnecessary square roots.

Now we modify our Game tick function to check for collisions and delete objects that are affected by them. Here’s the entire function:

Handle collisions in Game tick [ruby]
 def tick(timer)
    @ship.moveWithBounds_(@bounds) if @ship
    @rocks.each {|rock| rock.moveWithBounds_(@bounds)}
    @missiles.each {|missile| missile.moveWithBounds_(@bounds)}

    @rocks.each {|rock|
        @missiles.each {|missile| 
             missile.ttl = rock.ttl = 0 if missile.collidesWith_?(rock)
        }
        @ship.ttl = rock.ttl = 0 if @ship and @ship.collidesWith_?(rock)
    }

    @ship = nil if @ship and @ship.ttl == 0
    @rocks.delete_if {|rock| rock.ttl == 0}
    @missiles.delete_if {|missile| missile.ttl == 0}
  end

Now play it! As we’ve currently written our game, you only get one ship per game, but you can easily start over by creating a new game with File->New. And since this is a Cocoa document-based application, you can create as many games as you want and play them all concurrently! (yes, that would be weird, but you don’t have to tell anyone.)

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (0) post