Collision Detection

In this lesson, you will detect collisions between objects.


Before you get started, create a new version of your program. Save the spaceGame_v3.py file and then re-save the file as spaceGame_v4.py.


Detect Collisions

Detecting collision is a behavior, so it makes sense that a method should be created to complete this action. However, is this a behavior of the Player or of the Bullet?

This could be debated and it could be implemented either way, and you could even add the behavior to any object in the game that needs to detect collisions. In fact, in more advanced games, it is common to for objects to have a Collider if it will be colliding with other objects.

For this simple game, we will not create Collider objects and we will add the collision detection behavior only to the Bullet class.

Define a hittest method for the Bullet class.

    def move(self):
        self.turtle.fd(Bullet.SPEED)

    def hittest(self, other):
        if self.turtle.distance(other.turtle) < 10 :
            return True
        else :
            return False

    def drawBullet(self):

The method can be called at the moment a collision test is needed between the bullet (self) and some other object.

In the approach taken here, the determination of whether a collision is occuring is based on the distance between the two objects. If the distance between them is less than 10, the method returns True to indicate that there is a collision. Otherwise, False is returned to indicate that there is no collision.

Update this method to be more concise.

    def move(self):
        self.turtle.fd(Bullet.SPEED)

    def hittest(self, other):
        return self.turtle.distance(other.turtle) < 10

    def drawBullet(self):

While novice programmers likely find the longer version more readable, experienced programmers are accustomed to immediately returning the result of a conditional statement.

So, when should this function be called?

Logically, every time objects in the game move a collision may occur. Let's check for a collision at the moment any bullet is moved.

Add a collision test to the move method of the Bullet class. If a collision is detected, print out a message to indicate what the bullet collided with.

    def move(self):
        self.turtle.fd(Bullet.SPEED)

        if self.hittest(enemy) :
            print("Hit " + enemy.color + " ship!")

    def hittest(self, other):
        return self.turtle.distance(other.turtle) < 10

This conditional statement should call the hittest method of the bullet that was just moved (self) to check if it is colliding with the enemy.

However, enemy does not exist yet, and it might not be immediately clear if the enemy should be the first player (p1) or the second player (p2). That depends on which player fired the bullet.

Add a conditional statement that sets the enemy based on who owns the bullet.

    def move(self):
        self.turtle.fd(Bullet.SPEED)

        if self.owner == p1 : enemy = p2
        else : enemy = p1

        if self.hittest(enemy) :
            print("Hit " + enemy.color + " ship!")

Notice the condition statement is written on a single line instead of indenting code under the condition. This is acceptable when only a single statement is needed after the condition.

The program should run without error until one of the players attempts to fire. At that moment, a runtime error occurs!

Cannot load image

Most IDEs provide pretty good information when an error occurs to help programmers debug their code. This error provided by IDLE pretty clearly states that the Bullet object has no attribute named owner.

Additionally, it identifies line 19 as the line of code in which the error is occuring and shows that line of code as well. Apparently, self.owner does not exist.

You will get better at reading error messages as your programming experience grows. Some error messages are more readable and helpful than others. In any case, getting an error message is far better than getting no message when an error occurs!

So now what? Let's check the __init__ method of the Bullet class to see if we have created an owner property or not.

class Bullet:
    SPEED = 100

    def __init__(self, owner):
        self.turtle = turtle.Turtle()
        self.turtle.ht()
        self.turtle.speed(0)
        self.turtle.setpos(owner.turtle.pos())
        self.turtle.setheading(owner.turtle.heading())

Ok, so it looks like we did pass in a value to an owner parameter to define which Player object the Bullet being created belongs to. We even used that variable to determine the bullet's initial position and heading.

However, we never stored the value as a property of the Bullet. The variable owner is only a local variable and ceases to exist when the __init__ method is finished executing.

Store the value of the local variable owner into an owner property.

class Bullet:
    SPEED = 100

    def __init__(self, owner):
        self.owner = owner
        self.turtle = turtle.Turtle()
        self.turtle.ht()
        self.turtle.speed(0)
        self.turtle.setpos(owner.turtle.pos())
        self.turtle.setheading(owner.turtle.heading())

Run the program again and the error should not occur when you fire.

You may notice that the message still does not appear when a bullet hits an enemy. Remember the bullets move 100 pixels each frame so can jump right past an enemy without causing a collision.

You can see by flying slowly at the enemy while shooting that eventually a bullet will collide and cause the message to be printed to the console.



Clearly, we need to improve our algorithm for detecting collisions!


Improved Detection

One solution to our collision detection issue is to move the bullet much slower so that it cannot jump past objects without colliding. Of course, you probably do not want your bullets to move much slower.

Another approach is to quickly move the bullet several times and complete several collision checks on each frame. This can all be done within the move method of the Bullet class.

    def move(self):
        steps = 10
        stepSpeed = Bullet.SPEED / steps

        if self.owner == p1 : enemy = p2
        else : enemy = p1

        for i in range(steps):
            self.turtle.fd(stepSpeed)
            
            if self.hittest(enemy) :
                print("Hit " + enemy.color + " ship!")

The variable steps is set up to define how many moves (or steps) per frame the bullet will make. Setting steps to 10 will mean that the bullet will move forward 10 times each frame.

The variable stepSpeed is used to determine how far forward the bullet should move each step. The amount the bullet should move forward each step (stepSpeed) should be the full amount the bullet should move each frame (Bullet.SPEED) divided by the number of steps per frame (steps).

In Python, you can use a for loop with the range method to repeat a block of code a specified number of times. The range returned by range(10) is 0 through 9. The value of i is zero the first time through the loop and increases by one at the end of each loop.

Finally, the amount the bullet's turtle moves forward should be changed to stepSpeed. Instead of moving forward 100 pixels at a time, the bullet will now move forward 10 pixels at a time.

Be sure the collision detection code is part of the loop so that it executes for each step of the bullet's movement.

The code that determines who the enemy is should be moved above the loop as there is no reason to repeat this once the value has been set.



Destroy Enemy

Now that collision between bullets and enemies are being detected, the final piece to complete the game is to deal with that collision. Printing a message to the console just won't suffice.

Add an isAlive property to the Player class to keep track of whether the player is alive or not. Initialize the property to True to indicate that the player is alive when the game starts.

class Player:
    MAX_SPEED = 10
    ROTATION_SPEED = 5
    MAX_BULLETS = 10
    
    def __init__(self, color, position, heading):
        self.isAlive = True
        self.isReadyToFire = True

In the move method of the Bullet class, replace the print statement that occurs when a collision is detected with a line that set's the isAlive property of the enemy to False.

            if self.hittest(enemy) :
                enemy.isAlive = False

After a player dies, their ship should no longer be drawn on the screen but their bullets that they had fired before dying should remain.

Update the drawShip method to only draw the ship if the player is still alive.

This could be done be wrapping an conditional statement around all of the draw code.

    def drawShip(self):
        if self.isAlive : 
            t = self.turtle
            startPos = t.pos()
            startHeading = t.heading()

However, this would require indenting the large block of code used to draw the ship.

Alternatively, you could just immediately return out of the function if the player is not alive.

    def drawShip(self):
        if not self.isAlive : return
            
        t = self.turtle
        startPos = t.pos()
        startHeading = t.heading()

A player who is dead should also not be able to fire bullets.

Add a conditional statement to the fire method that immediately returns, exiting the function, if the player is not alive.

    def fire(self):
        if not self.isAlive : return
        
        self.bullets.append(Bullet(self))
        if len(self.bullets) > Player.MAX_BULLETS:
            self.bullets[0].turtle.clear()
            self.bullets.pop(0)

Play the game to ensure that each player can successfully destroy their opponent.



To make it easier to hit your enemy, update the condition in the hittest method count any distance between the objects less than 20 as a hit.


Challenges

In Game Design terms, you have created a minimal viable product. It includes the core gameplay and you can test it out to see if it is fun before spending time on art and building new features and game mechancis.

While the focus of this class is not game design, adding some more to the game without following a step-by-step guide will definitely help you develop as a programmer.

Of course, there are many ways to add polish to the game and many features and game mechanics that could be added. Try out some of the following:

LEVEL 1 CHALLENGES - for beginners!

  1. Draw a flame in the back of the ship only when the player is accelerating.
  2. Add a pac-man style wrap ability so that players who exit the game on one side appear on the opposite side.
LEVEL 2 CHALLENGES - for experienced programmers!

  1. Add a title screen that appears when the game begins and then dissappears and starts the gameloop after the player clicks on it.
  2. Create a health property for the player and have bullets deal damage to the health of a player instead of immediately destoying them. A player should be destroyed only when their health is depleted.
LEVEL 3 CHALLENGES - for programming geniuses!

  1. Download an image of space and apply it as the background of the game.
  2. Create a powerup object that a player can collect to boost their max speed. Set up a timer to spawn a new powerup every 10 seconds
  3. Track the number of ammo a player has and only allow them to shoot if they have ammo remaining. Then, create an Ammo object that a player can collect to increase their amount of ammo. Set up a timer to spawn a new Ammo object every 10 seconds.