What's a hero without a villain? How to add one to your Python game

What's a hero without a villain? How to add one to your Python game

In part five of this series on building a Python game from scratch, add a bad guy for your good guy to battle.

Dogs playing chess
Image by : 
NASA on the Commons and Internet Archive Book Images. Modified by Opensource.com. CC BY-SA 4.0
x

Subscribe now

Get the highlights in your inbox every week.

In the previous articles in this series (see part 1, part 2, part 3, and part 4), you learned how to use Pygame and Python to spawn a playable hero character in an as-yet empty video game world. But what's a hero without a villain?

It would make for a pretty boring game if you had no enemies, so in this article, you'll add an enemy to your game and construct a framework for building levels.

It might seem strange to jump ahead to enemies when there's still more to be done to make the player sprite fully functional, but you've learned a lot already, and creating villains is very similar to creating a player sprite. So relax, use the knowledge you already have, and see what it takes to stir up some trouble.

For this exercise, you need an enemy sprite. If you haven't downloaded one already, you can find Creative Commons assets on OpenGameArt.org.

Creating the enemy sprite

Whether you realize it or not, you already know how to implement enemies. The process is similar to creating a player sprite:

  1. Make a class so enemies can spawn.
  2. Create an update function for the enemy, and update the enemy in your main loop.
  3. Create a move function so your enemy can roam around.

Start with the class. Conceptually, it's mostly the same as your Player class. You set an image or series of images, and you set the sprite's starting position.

Before continuing, make sure you have placed your enemy graphic in your game project's images directory (the same directory where you placed your player image). In this article's example code, the enemy graphic is named enemy.png.

A game looks a lot better when everything alive is animated. Animating an enemy sprite is done the same way as animating a player sprite. For now, though, keep it simple, and use a non-animated sprite.

At the top of the objects section of your code, create a class called Enemy with this code:

class Enemy(pygame.sprite.Sprite):
    """
    Spawn an enemy
    """

    def __init__(self,x,y,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img))
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

If you want to animate your enemy, do it the same way you animated your player.

Spawning an enemy

You can make the class useful for spawning more than just one enemy by allowing yourself to tell the class which image to use for the sprite and where in the world you want the sprite to appear. This means you can use this same enemy class to generate any number of enemy sprites anywhere in the game world. All you have to do is make a call to the class, and tell it which image to use, along with the X and Y coordinates of your desired spawn point.

Ao you did when spawning a player sprite, add code to designate a spawn point in the setup section of your script:

enemy = Enemy(300,0,'enemy.png')    # spawn enemy
enemy_list = pygame.sprite.Group()   # create enemy group
enemy_list.add(enemy)                # add enemy to group

In that sample code, you spawn an enemy by creating a new object (called enemy), at 300 pixels on the X axis and 0 on the Y axis. Spawning the enemy at 0 on the Y axis means that its top left corner is located at 0, with the graphic itself descending down from that point. You might need to adjust these numbers, or the numbers for your hero sprite, depending on how big your sprites are, but try to get it to spawn in a place you can reach with your player sprite (accounting for your game's current lack of vertical movement). In the end, I placed my enemy at 0 pixels on the Y axis and my hero at 30 pixels to get them boh to appear on the same plane. Experiment with the spawn points for yourself, keeping in mind that greater Y axis numbers are lower on the screen.

Your hero graphic had an image "hard coded" into its class because there's only one hero, but you may want to use different graphics for each enemy, so the image file is something you can define at sprite creation. The image used for this enemy sprite is enemy.png.

Drawing a sprite on screen

If you were to launch your game now, it would run but you wouldn't see an enemy. You might recall the same problem when you created your player sprite. Do you remember how to fix it?

To get a sprite to appear on screen, you must add them to your main loop. If something is not in your main loop, then it only happens once, and only for a millisecond. If you want something to persist in your game, it must happen in the main loop.

You must add code to draw all enemies in the enemy group (called enemy_list), which you established in your setup section, on the screen. The middle line in this example code is the new line you need to add:

    player_list.draw(world)
    enemy_list.draw(world)  # refresh enemies
    pygame.display.flip()

Right now, you have only one enemy, but you can add more later if you want. As long as you add an enemy to the enemies group, it will be drawn to the screen during the main loop.

Launch your game. Your enemy appears in the game world at whatever X and Y coordinate you chose.

Level one

Your game is in its infancy, but you will probably want to add a series of levels, eventually. It's important to plan ahead when you program so your game can grow as you learn more about programming. Even though you don't even have one complete level yet, you should code as if you plan on having many levels.

Think about what a "level" is. How do you know you are at a certain level in a game?

You can think of a level as a collection of items. In a platformer, such as the one you are building here, a level consists of a specific arrangement of platforms, placement of enemies and loot, and so on. You can build a class that builds a level around your player. Eventually, when you create more than one level, you can use this class to generate the next level when your player reaches a specific goal.

Move the code you wrote to create an enemy and its group into a new function that gets called along with each new level. It requires some modification so that each time you create a new level, you can create and place several enemies:

class Level():
    def bad(lvl,eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0],eloc[1],'enemy.png') # spawn enemy
            enemy_list = pygame.sprite.Group() # create enemy group
            enemy_list.add(enemy)              # add enemy to group
        if lvl == 2:
            print("Level " + str(lvl) )

        return enemy_list

The return statement ensures that when you use the Level.bad function, you're left with an enemy_list containing each enemy you defined.

Since you are creating enemies as part of each level now, your setup section needs to change, too. Instead of creating an enemy, you must define where the enemy will spawn and what level it belongs to.

eloc = []
eloc = [300,0]
enemy_list = Level.bad( 1, eloc )

Run the game again to confirm your level is generating correctly. You should see your player, as usual, and the enemy you added in this chapter.

Hitting the enemy

An enemy isn't much of an enemy if it has no effect on the player. It's common for enemies to cause damage when a player collides with them.

Since you probably want to track the player's health, the collision check happens in the Player class rather than in the Enemy class. You can track the enemy's health, too, if you want. The logic and code are pretty much the same, but, for now, just track the player's health.

To track player health, you must first establish a variable for the player's health. The first line in this code sample is for context, so add the second line to your Player class:

        self.frame  = 0
        self.health = 10

In the update function of your Player class, add this code block:

        hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in hit_list:
            self.health -= 1
            print(self.health)

This code establishes a collision detector using the Pygame function sprite.spritecollide, called enemy_hit. This collision detector sends out a signal any time the hitbox of its parent sprite (the player sprite, where this detector has been created) touches the hitbox of any sprite in enemy_list. The for loop is triggered when such a signal is received and deducts a point from the player's health.

Since this code appears in the update function of your player class and update is called in your main loop, Pygame checks for this collision once every clock tick.

Moving the enemy

An enemy that stands still is useful if you want, for instance, spikes or traps that can harm your player, but the game is more of a challenge if the enemies move around a little.

Unlike a player sprite, the enemy sprite is not controlled by the user. Its movements must be automated.

Eventually, your game world will scroll, so how do you get an enemy to move back and forth within the game world when the game world itself is moving?

You tell your enemy sprite to take, for example, 10 paces to the right, then 10 paces to the left. An enemy sprite can't count, so you have to create a variable to keep track of how many paces your enemy has moved and program your enemy to move either right or left depending on the value of your counting variable.

First, create the counter variable in your Enemy class. Add the last line in this code sample:

        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0 # counter variable

Next, create a move function in your Enemy class. Use an if-else loop to create what is called an infinite loop:

  • Move right if the counter is on any number from 0 to 100.
  • Move left if the counter is on any number from 100 to 200.
  • Reset the counter back to 0 if the counter is greater than 200.

An infinite loop has no end; it loops forever because nothing in the loop is ever untrue. The counter, in this case, is always either between 0 and 100 or 100 and 200, so the enemy sprite walks right to left and right to left forever.

The actual numbers you use for how far the enemy will move in either direction depending on your screen size, and possibly, eventually, the size of the platform your enemy is walking on. Start small and work your way up as you get used to the results. Try this first:

    def move(self):
        '''
        enemy movement
        '''

        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance*2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1

After you enter this code, PyCharm will offer to simplify the "chained comparison". You can accept its suggestion to optimize your code, and to learn some advanced Python syntax. You can also safely ignore PyCharm. The code works, either way.

You can adjust the distance and speed as needed.

The question is: does this code work if you launch your game now?

Of course not! And you know why: you must call the move function in your main loop.

The first line in this sample code is for context, so add the last two lines:

    enemy_list.draw(world) #refresh enemy
    for e in enemy_list:
        e.move()

Launch your game and see what happens when you hit your enemy. You might have to adjust where the sprites spawn so that your player and your enemy sprite can collide. When they do collide, look in the console of IDLE or PyCharm to see the health points being deducted.

You may notice that health is deducted for every moment your player and enemy are touching. That's a problem, but it's a problem you'll solve later, after you've had more practice with Python.

For now, try adding some more enemies. Remember to add each enemy to the enemy_list. As an exercise, see if you can think of how you can change how far different enemy sprites move.

Code so far

For you reference, here's the code so far:

#!/usr/bin/env python3
# by Seth Kenlon

# GPLv3
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import pygame
import sys
import os

'''
Variables
'''


worldx = 960
worldy = 720
fps = 40
ani = 4
world = pygame.display.set_mode([worldx, worldy])

BLUE = (25, 25, 200)
BLACK = (23, 23, 23)
WHITE = (254, 254, 254)
ALPHA = (0, 255, 0)

'''
Objects
'''



class Player(pygame.sprite.Sprite):
    """
    Spawn a player
    """


    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.images = []
        for i in range(1, 5):
            img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect = self.image.get_rect()

    def control(self, x, y):
        """
        control player movement
        """

        self.movex += x
        self.movey += y

    def update(self):
        """
        Update sprite position
        """


        self.rect.x = self.rect.x + self.movex
        self.rect.y = self.rect.y + self.movey

        # moving left
        if self.movex < 0:
            self.frame += 1
            if self.frame > 3*ani:
                self.frame = 0
            self.image = pygame.transform.flip(self.images[self.frame // ani], True, False)

        # moving right
        if self.movex > 0:
            self.frame += 1
            if self.frame > 3*ani:
                self.frame = 0
            self.image = self.images[self.frame//ani]

        hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in hit_list:
            self.health -= 1
            print(self.health)


class Enemy(pygame.sprite.Sprite):
    """
    Spawn an enemy
    """

    def __init__(self, x, y, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img))
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        """
        enemy movement
        """

        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance*2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1


class Level():
    def bad(lvl, eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0], eloc[1], 'enemy.png')
            enemy_list = pygame.sprite.Group()
            enemy_list.add(enemy)
        if lvl == 2:
            print("Level " + str(lvl) )

        return enemy_list


'''
Setup
'''


backdrop = pygame.image.load(os.path.join('images', 'stage.png'))
clock = pygame.time.Clock()
pygame.init()
backdropbox = world.get_rect()
main = True

player = Player()  # spawn player
player.rect.x = 0  # go to x
player.rect.y = 30  # go to y
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10

eloc = []
eloc = [300, 0]
enemy_list = Level.bad(1, eloc )

'''
Main Loop
'''


while main:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            try:
                sys.exit()
            finally:
                main = False

        if event.type == pygame.KEYDOWN:
            if event.key == ord('q'):
                pygame.quit()
                try:
                    sys.exit()
                finally:
                    main = False
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(-steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(steps, 0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps, 0)

    world.blit(backdrop, backdropbox)
    player.update()
    player_list.draw(world)
    enemy_list.draw(world)
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)

 

Topics

About the author

Seth Kenlon
Seth Kenlon - Seth Kenlon is a UNIX geek, free culture advocate, independent multimedia artist, and D&D nerd. He has worked in the film and computing industry, often at the same time. He is one of the maintainers of the Slackware-based multimedia production project Slackermedia.

About the author

Jess Weichler - Jess Weichler is a digital artist using open source software and hardware to create works digitally and in the physical world at CyanideCupcake.com. She is also an award-winning educator for (and founder of) MakerBox.org.nz an organization that teaches kids of all ages how to use technology, from sewing needles to Arduinos, to make their ideas a reality.