Game Development 15 min read

Build a Plants vs Zombies Clone in Python with Pygame – Step‑by‑Step Tutorial

This tutorial walks you through creating a simple Plants vs Zombies‑style game in Python using Pygame, covering module imports, asset configuration, map and plant classes, bullet mechanics, zombie behavior, event handling, and the main game loop, complete with runnable code examples.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Build a Plants vs Zombies Clone in Python with Pygame – Step‑by‑Step Tutorial

Hello, I’m 面师兄. Today I recommend a Python practice project: building a Plants vs Zombies clone using Pygame.

1. Import required modules

import pygame
import random

2. Configure image path

IMAGE_PATH = 'imgs/'

3. Set screen dimensions

scrrr_width = 800
scrrr_height = 560

4. Game‑over flag

GAMEOVER = False

5. Image loading error handling

LOG = 'File:{} method:{} error'.format(__file__, __name__)

6. Map class

class Map():
    pass

7. Store map image names

map_names_list = [IMAGE_PATH + 'map1.png', IMAGE_PATH + 'map2.png']

8. Initialize map

def __init__(self, x, y, img_index):
    self.image = pygame.image.load(Map.map_names_list[img_index])
    self.position = (x, y)

9. Plantability flag

self.can_grow = True

10. Load map onto screen

def load_map(self):
    MainGame.window.blit(self.image, self.position)

11. Plant base class

class Plant(pygame.sprite.Sprite):
    def __init__(self):
        super(Plant, self).__init__()
        self.live = True

12. Load plant image

def load_image(self):
    if hasattr(self, 'image') and hasattr(self, 'rect'):
        MainGame.window.blit(self.image, self.rect)
    else:
        print(LOG)

13. Sunflower class

class Sunflower(Plant):
    def __init__(self, x, y):
        super(Sunflower, self).__init__()
        self.image = pygame.image.load('imgs/sunflower.png')
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.price = 50
        self.hp = 100
        self.time_count = 0

14. Sunflower generates money

def produce_money(self):
    self.time_count += 1
    if self.time_count == 25:
        MainGame.money += 5
        self.time_count = 0

15. Display sunflower

def display_sunflower(self):
    MainGame.window.blit(self.image, self.rect)

16. PeaShooter class

class PeaShooter(Plant):
    def __init__(self, x, y):
        super(PeaShooter, self).__init__()
        self.image = pygame.image.load('imgs/peashooter.png')
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.price = 50
        self.hp = 200
        self.shot_count = 0

17. Shooting method

def shot(self):
    should_fire = False
    for zombie in MainGame.zombie_list:
        if zombie.rect.y == self.rect.y and zombie.rect.x < 800 and zombie.rect.x > self.rect.x:
            should_fire = True
    if self.live and should_fire:
        self.shot_count += 1
        if self.shot_count == 25:
            peabullet = PeaBullet(self)
            MainGame.peabullet_list.append(peabullet)
            self.shot_count = 0

18. Display PeaShooter

def display_peashooter(self):
    MainGame.window.blit(self.image, self.rect)

19. PeaBullet class

class PeaBullet(pygame.sprite.Sprite):
    def __init__(self, peashooter):
        self.live = True
        self.image = pygame.image.load('imgs/peabullet.png')
        self.damage = 50
        self.speed = 10
        self.rect = self.image.get_rect()
        self.rect.x = peashooter.rect.x + 60
        self.rect.y = peashooter.rect.y + 15
    def move_bullet(self):
        if self.rect.x < scrrr_width:
            self.rect.x += self.speed
        else:
            self.live = False
    def hit_zombie(self):
        for zombie in MainGame.zombie_list:
            if pygame.sprite.collide_rect(self, zombie):
                self.live = False
                zombie.hp -= self.damage
                if zombie.hp <= 0:
                    zombie.live = False
                self.nextLevel()
    def nextLevel(self):
        MainGame.score += 20
        MainGame.remnant_score -= 20
        for i in range(1, 100):
            if MainGame.score == 100 * i and MainGame.remnant_score == 0:
                MainGame.remnant_score = 100 * i
                MainGame.shaoguan += 1
                MainGame.produce_zombie += 50
    def display_peabullet(self):
        MainGame.window.blit(self.image, self.rect)

20. Zombie class

class Zombie(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super(Zombie, self).__init__()
        self.image = pygame.image.load('imgs/zombie.png')
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.hp = 1000
        self.damage = 2
        self.speed = 1
        self.live = True
        self.stop = False
    def move_zombie(self):
        if self.live and not self.stop:
            self.rect.x -= self.speed
        if self.rect.x < -80:
            MainGame().gameOver()
    def hit_plant(self):
        for plant in MainGame.plants_list:
            if pygame.sprite.collide_rect(self, plant):
                self.stop = True
                self.eat_plant(plant)
    def eat_plant(self, plant):
        plant.hp -= self.damage
        if plant.hp <= 0:
            a = plant.rect.y // 80 - 1
            b = plant.rect.x // 80
            map = MainGame.map_list[a][b]
            map.can_grow = True
            plant.live = False
            self.stop = False
    def display_zombie(self):
        MainGame.window.blit(self.image, self.rect)

21. Main game class

class MainGame():
    shaoguan = 1
    score = 0
    remnant_score = 100
    money = 200
    map_points_list = []
    map_list = []
    plants_list = []
    peabullet_list = []
    zombie_list = []
    count_zombie = 0
    produce_zombie = 100
    def init_window(self):
        pygame.display.init()
        MainGame.window = pygame.display.set_mode([scrrr_width, scrrr_height])
    def draw_text(self, content, size, color):
        pygame.font.init()
        font = pygame.font.SysFont('kaiti', size)
        return font.render(content, True, color)
    def load_help_text(self):
        text1 = self.draw_text('1.左键创建向日葵 2.右键创建豌豆射手', 26, (255,0,0))
        MainGame.window.blit(text1, (5,5))
    def init_plant_points(self):
        for y in range(1,7):
            points = []
            for x in range(10):
                points.append((x,y))
            MainGame.map_points_list.append(points)
    def init_map(self):
        for points in MainGame.map_points_list:
            temp_map_list = []
            for point in points:
                if (point[0] + point[1]) % 2 == 0:
                    map = Map(point[0]*80, point[1]*80, 0)
                else:
                    map = Map(point[0]*80, point[1]*80, 1)
                temp_map_list.append(map)
            MainGame.map_list.append(temp_map_list)
    def load_map(self):
        for temp_map_list in MainGame.map_list:
            for map in temp_map_list:
                map.load_map()
    def load_plants(self):
        for plant in MainGame.plants_list:
            if plant.live:
                if isinstance(plant, Sunflower):
                    plant.display_sunflower()
                    plant.produce_money()
                elif isinstance(plant, PeaShooter):
                    plant.display_peashooter()
                    plant.shot()
                else:
                    MainGame.plants_list.remove(plant)
    def load_peabullets(self):
        for b in MainGame.peabullet_list:
            if b.live:
                b.display_peabullet()
                b.move_bullet()
                b.hit_zombie()
            else:
                MainGame.peabullet_list.remove(b)
    def deal_events(self):
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                self.gameOver()
            elif e.type == pygame.MOUSEBUTTONDOWN:
                x = e.pos[0] // 80
                y = e.pos[1] // 80
                map = MainGame.map_list[y-1][x]
                if e.button == 1:
                    if map.can_grow and MainGame.money >= 50:
                        sunflower = Sunflower(map.position[0], map.position[1])
                        MainGame.plants_list.append(sunflower)
                        map.can_grow = False
                        MainGame.money -= 50
                elif e.button == 3:
                    if map.can_grow and MainGame.money >= 50:
                        peashooter = PeaShooter(map.position[0], map.position[1])
                        MainGame.plants_list.append(peashooter)
                        map.can_grow = False
                        MainGame.money -= 50
    def init_zombies(self):
        for i in range(1,7):
            dis = random.randint(1,5) * 200
            zombie = Zombie(800 + dis, i*80)
            MainGame.zombie_list.append(zombie)
    def load_zombies(self):
        for zombie in MainGame.zombie_list:
            if zombie.live:
                zombie.display_zombie()
                zombie.move_zombie()
                zombie.hit_plant()
            else:
                MainGame.zombie_list.remove(zombie)
    def start_game(self):
        self.init_window()
        self.init_plant_points()
        self.init_map()
        self.init_zombies()
        while not GAMEOVER:
            MainGame.window.fill((255,255,255))
            MainGame.window.blit(self.draw_text('当前钱数$: {}'.format(MainGame.money), 26, (255,0,0)), (500,40))
            MainGame.window.blit(self.draw_text('当前关数{},得分{},距离下关还差{}分'.format(MainGame.shaoguan, MainGame.score, MainGame.remnant_score), 26, (255,0,0)), (5,40))
            self.load_help_text()
            self.load_map()
            self.load_plants()
            self.load_peabullets()
            self.deal_events()
            self.load_zombies()
            MainGame.count_zombie += 1
            if MainGame.count_zombie == MainGame.produce_zombie:
                self.init_zombies()
                MainGame.count_zombie = 0
            pygame.time.wait(10)
            pygame.display.update()
    def gameOver(self):
        MainGame.window.blit(self.draw_text('游戏结束', 50, (255,0,0)), (300,200))
        pygame.time.wait(400)
        global GAMEOVER
        GAMEOVER = True
if __name__ == '__main__':
    game = MainGame()
    game.start_game()

Resulting screenshot of the finished game:

Game screenshot
Game screenshot
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PythonPygamePlants vs Zombies
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.