Learning Python Programming - Part 7.3 - Killing Zombies

in #programming8 years ago (edited)

Welcome to the 7.3rd part of an ongoing series, Learning Python!

Join our programming tutorial!
Let's just code something already!


source

Where did we leave off last time?

Last time we made zombies that chase the survivor.
Previous Post

What should we add next?

Next we're going to add the shooting and dying to the game.

What are bullets?


source

Bullets are projectiles that will come from our survivor and travel across the screen at any angle, but if it "hits" a zombie it will do damage and stop crossing the screen.

To make a bullet, we'll use a class with the values: position, angle, speed, colour, radius, penetration, and damage. Before we make this class, we can see the similarities it has with the Entity class. The new Bullet class will inherit the Entity class because it uses all of the values inside it. Now we have a Bullet class that inherits the Entity class and contains lots of values.

class Bullet(Entity):
    def __init__(self, pos, penetration=BULLET_PENETRATION, damage=BULLET_DAMAGE, speed=BULLET_SPEED, rad=BULLET_RAD, colour=BULLET_COLOUR):
        super().__init__(speed, pos, rad, colour)
        self.penetration = penetration
        self.damage = damage

Now we need to define some of those global variables up at the top of our code. These can be changed to adjust the feel of the game.

BULLET_DAMAGE = 100
BULLET_PENETRATION = 1
BULLET_SPEED = 10
BULLET_RAD = 3
BULLET_COLOUR = "Gray"

Now we have the base code for some bullets.

The survivor’s movement sucks

I was playing with what we have so far and concluded that our survivor doesn’t move like they should. So let’s redo how I wrote the Survivor movement. (This is part of the programming experience and I'm literally programming this as I write the posts.)

So to make the bullets shoot and move, we need to give them an angle and starting position. The starting position is simple, we'll use the survivor's position, but the angle is something we don't have yet.

I'm thinking that if we make our survivor have an angle of direction that they are "looking" in, we could use that for the angle the bullet gets, and it would make moving around smoother than it is currently.

Moving with the mouse


source

To implement our new movement, we need to give the survivor class a new variable "self.dir" to hold the angle we are facing.

self.dir = 0

This 0 is just a “place holder” and will be overwritten almost immediately. The code technically needs this variable to run, so we need to define it with a value, but because the value doesn’t do much of anything yet, it doesn’t matter what it starts out as.

Now we need to be able to adjust the look angle. We're going to make a new function to use the mouse to set the looking direction and remove the old code in the kb_handler.

def kb_handler(event):
    if event.char == "w":
        survivor.move(True)
    if event.char == "s":
        survivor.move(False)

def motion(event):
    x, y = event.x, event.y
    survivor.dir = get_angle([x,y], survivor.pos)

To use the mouse movement, we need to add define another handler down at the bottom with the <Key> handler.

root.bind('<Motion>', motion)

Now we have turning capabilities.

Helper function

Because of these changes we need to change how the move method works. We can see that there's similar code in the move methods, so we'll rip it out from both and make a helper function to return the change in position, in terms of delta x and delta y coordinates.

def get_pos_from_angle(angle, speed):
    deltax = math.cos(angle)*speed
    deltay = math.sin(angle)*speed
    return [deltax, deltay]

Now we need to redo the movement functions to incorporate using the helper function.
For zombie.move method:

def move(self, poss):
        ###OLD CODE IS HIDDEN

        delta = get_pos_from_angle(get_angle(pos_to_move_to, self.pos),
                                   self.speed)

        self.pos[0] += delta[0]
        self.pos[1] += delta[1]

        return delta

For survivor.move method:

def move(self, forward):
        angle = self.dir
        
        if forward:
            delta = get_pos_from_angle(self.dir, self.speed)
        else:
            delta = get_pos_from_angle(self.dir*180, self.speed)

        self.pos[0] += delta[0]
        self.pos[1] += delta[1]
        
        canvas.move(self.drawn, delta[0], delta[1])

Note: I changed the argument here to be if we are going forward or backwards and implemented backing up.

For bullet.move method:

def move(self):
        delta = get_pos_from_angle(self.dir, self.speed)
        
        self.pos[0] += delta[0]
        self.pos[1] += delta[1]

        if self.pos[0] < WIDTH and self.pos[0] > 0 and self.pos[1] < HEIGHT and self.pos[1] > 0:
            canvas.move(self.drawn, delta[0], delta[1])
        else:
            self.delete()

The if statement is to check if the bullet is still within the screen coordinates. If it is not, we'll stop keeping track of them because they will only build up in the RAM until our game lags enough to crash.

Our delete method mentioned above is pretty simple:

def delete(self):
    bullets.remove(self)
    canvas.delete(self.drawn)

That was a pretty simple change, but now our code is so much more concise.

To the shooting range


source

Now let's make it so that we can have our survivor shoot these bullets. To shoot the bullets, we'll be using a list of anonymous Bullet objects that we loop through to move on every frame. I think the key to shoot the bullets will be most effective as the space bar, so we'll need to add that to our kb_handler.

In the same place we made our survivor objects and zombie objects, we'll put in an empty list to hold the bullet objects.

bullets = []

Using that list, we'll be appending bullet objects as they are made, or "shot."
To shoot, we need to implement the space bar. The character tkinter uses to define the space bar is actually a space, so we need to use a string with a single space in it.

def kb_handler(event):
    if event.char == "w":
        survivor.move(True)
    if event.char == "s":
        survivor.move(False)
    if event.char == " ":
        bullet = survivor.shoot()
        bullet.draw(canvas)

The bullet object is created in survivor.shoot() and returned to be drawn on the next line. You could take the code from survivor.shoot and plop it right here, but in my opinion the separation of who is shooting is more important, especially if we are going to adjust bullet attributes by who is shooting based on future changes like power ups.

With that we need to make the shoot method in the survivor class.

def shoot(self):
        bullet = Bullet(list(self.pos), self.dir)
        bullets.append(bullet)
        return bullet

This code will "shoot" the bullet, but it won't move because we haven't added it to our game loop.

Inside our game loop, we'll add the for loop, "for bullet in bullets" to go through every bullet object in the list bullets. (“bullet” is a temporary variable here and can be any valid variable name.) For each bullet we need to move it on the screen, so we'll call the wonderful move method we made above to move it.

def update():
    for bullet in bullets:
        bullet.move()
        
    for zombie in zombies:
        delta = zombie.move([survivor.pos])
        canvas.move(zombie.drawn, delta[0], delta[1])

Now we can shoot all we want!

Killing those zombies


source

To kill the zombies, we'll need to loop through where the bullets are and where the zombies are to see if they are interacting. If they are, we'll need to damage the zombie and remove a penetration point from the bullet. As you can probably guess, we'll be adding this check in the game loop right after we move the bullet. We'll put the check here to resemble real life as close as possible. If a zombie doesn't survive the bullet's damage, it won't move anymore.
Inside the game loop:

def update():
    for bullet in bullets:
        bullet.move()
        bullet.check_hit(zombies)

### OLD CODE IS HIDDEN

Our new function "check_hit" will take in the argument zombies, which is actually the object list of every zombie.
Bullet.check_hit will loop through every zombie and check if they "overlap" with the bullet object.

def check_hit(self, zombies):
        for zombie in zombies:
            if is_overlap(zombie.pos, zombie.rad, self.pos, self.rad):
                zombie.hit(self.damage)
                self.penetration -= 1
                if self.penetration <= 0:
                    self.delete()

First, we loop through every zombie object. Second, we check if they overlap with the helper function is_overlap that we'll define in a second. Third, if a bullet is overlapping with a zombie object, we need to give damage to the zombie, and damage to the bullet. The bullet's damage is actually just a counter of how many zombies it can pierce through before it is "stopped." Stopping the bullet is the delete method that remove the bullet from memory and the screen. Finally, if the bullet and the zombie do not overlap, we can do nothing with them, so we ignore the pair.

Our is_overlap helper function is pretty simple. All it does is checks if the bullet value are within the square of zombie value +- (zombie radius + bullet radius) with value being x and y. Looking at the code for this may be a little intimidating, but we'll break it down.

def is_overlap(pos0, rad0, pos1, rad1):
    if pos0[0] > pos1[0]-rad1-rad0 and pos0[0] < pos1[0]+rad1+rad0 and pos0[1] > pos1[1]-rad1-rad0 and pos0[1] < pos1[1]+rad1+rad0:
        return True
    return False

Our if statement has four different values that must evaluate to True for the objects to overlap. These four expressions are really just variations of one expression.
We start with the two positions (pos0 and pos1) and their radius (rad0 and rad1). pos0's radius is rad0 and pos1's radius is rad1. We'll use the fact that the difference in positions must be <= the sum of the radii for the objects to overlap. Another way to write this is pos0 > pos1-(radii sum).
Our variations of this expression are simply rewriting the expression to handle each of the four cardinal directions.
If the objects overlap, we'll return True, but if they don't we'll return False.

Our zombie.hit function is simply a function that takes in how much damage to do to the zombie, does the damage to the health, and checks if the zombie has enough health to stay "alive."

def hit(self, damage):
        self.health -= damage
        if self.health <= 0:
            self.delete()

Our zombie.delete method is just like the bullet.delete method except the list we are removing from is the zombies list.

NOW WE CAN KILL!

Link to source code.

Sort:  

Congratulations @thetalcumtaco! You have received a personal award!

Happy Birthday - 1 Year on Steemit Happy Birthday - 1 Year on Steemit
Click on the badge to view your own Board of Honor on SteemitBoard.

For more information about this award, click here

By upvoting this notification, you can help all Steemit users. Learn how here!

Congratulations @thetalcumtaco! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of upvotes

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

By upvoting this notification, you can help all Steemit users. Learn how here!

Congratulations @thetalcumtaco! You have received a personal award!

2 Years on Steemit
Click on the badge to view your Board of Honor.

Support SteemitBoard's project! Vote for its witness and get one more award!

Congratulations @thetalcumtaco! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 3 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!