Building a Python Plants vs Zombies Clone: Image Loading & Sprite Animation
This article walks through creating a functional Python clone of the classic Plants vs Zombies game, detailing supported plant and zombie types, level data storage, image resource organization, and the implementation of graphics loading, frame handling, state management, and animation logic using Pygame.
Motivated by the popularity of the original Plants vs Zombies game, the author created a Python clone using Pygame, downloading limited graphic assets from GitHub and implementing a subset of plants and zombies.
Supported plant types include Sunflower, Peashooter, Snow Pea, Walnut, Cherry Bomb, with added Double Peashooter and Triple Peashooter. Supported zombie types include Normal, Conehead, Buckethead, and others, with level data stored in JSON and a lawn mower added.
Below are screenshots of the game.
Image Switching
The article explains how to switch images for different zombie states, using the Conehead zombie as an example with seven image types: walking with helmet, attacking with helmet, walking without helmet, attacking without helmet, headless walking, headless attacking, and death.
Image Loading
Each action of a graphic type is stored as a separate image file. The load_all_gfx function traverses the resources/graphics directory, distinguishing between single images and subfolders that contain a series of frames.
<code>def load_all_gfx(directory, colorkey=c.WHITE, accept=('.png', '.jpg', '.bmp', '.gif')):
graphics = {}
for name1 in os.listdir(directory):
dir1 = os.path.join(directory, name1)
if os.path.isdir(dir1):
for name2 in os.listdir(dir1):
dir2 = os.path.join(dir1, name2)
if os.path.isdir(dir2):
for name3 in os.listdir(dir2):
dir3 = os.path.join(dir2, name3)
if os.path.isdir(dir3):
image_name, _ = os.path.splitext(name3)
graphics[image_name] = load_image_frames(dir3, image_name, colorkey, accept)
else:
image_name, _ = os.path.splitext(name2)
graphics[image_name] = load_image_frames(dir2, image_name, colorkey, accept)
break
else:
name, ext = os.path.splitext(name2)
if ext.lower() in accept:
img = pg.image.load(dir2)
if img.get_alpha():
img = img.convert_alpha()
else:
img = img.convert()
img.set_colorkey(colorkey)
graphics[name] = img
return graphics
GFX = load_all_gfx(os.path.join("resources","graphics"))</code>The load_image_frames function groups images by the numeric index in their filenames and returns an ordered list of frames.
<code>def load_image_frames(directory, image_name, colorkey, accept):
frame_list = []
tmp = {}
index_start = len(image_name) + 1
frame_num = 0
for pic in os.listdir(directory):
name, ext = os.path.splitext(pic)
if ext.lower() in accept:
index = int(name[index_start:])
img = pg.image.load(os.path.join(directory, pic))
if img.get_alpha():
img = img.convert_alpha()
else:
img = img.convert()
img.set_colorkey(colorkey)
tmp[index] = img
frame_num += 1
for i in range(frame_num):
frame_list.append(tmp[i])
return frame_list</code>Image Display Switching
In source/component/zombie.py , the Zombie base class loads all supported images via loadImages , sets the sprite’s image and rect , and provides methods for state changes and animation.
<code>class Zombie(pg.sprite.Sprite):
def __init__(self, x, y, name, health, head_group=None, damage=1):
pg.sprite.Sprite.__init__(self)
self.name = name
self.frames = []
self.frame_index = 0
self.loadImages()
self.frame_num = len(self.frames)
self.image = self.frames[self.frame_index]
self.rect = self.image.get_rect()
self.rect.centerx = x
self.rect.bottom = y
def loadFrames(self, frames, name, image_x):
frame_list = tool.GFX[name]
rect = frame_list[0].get_rect()
width, height = rect.w, rect.h
width -= image_x
for frame in frame_list:
frames.append(tool.get_image(frame, image_x, 0, width, height))
</code>The update , handleState , and animation methods drive the zombie’s behavior each tick, while setWalk , setAttack , setDie , and changeFrames adjust the current state and reload the appropriate frame set.
<code>def setWalk(self):
self.state = c.WALK
self.animate_interval = 150
if self.helmet:
self.changeFrames(self.helmet_walk_frames)
elif self.losHead:
self.changeFrames(self.losthead_walk_frames)
else:
self.changeFrames(self.walk_frames)
def setAttack(self, plant):
self.plant = plant
self.state = c.ATTACK
self.animate_interval = 100
if self.helmet:
self.changeFrames(self.helmet_attack_frames)
elif self.losHead:
self.changeFrames(self.losthead_attack_frames)
else:
self.changeFrames(self.attack_frames)
def setDie(self):
self.state = c.DIE
self.animate_interval = 200
self.changeFrames(self.die_frames)
def changeFrames(self, frames):
'''change image frames and modify rect position'''
self.frames = frames
self.frame_num = len(self.frames)
self.frame_index = 0
bottom = self.rect.bottom
centerx = self.rect.centerx
self.image = self.frames[self.frame_index]
self.rect = self.image.get_rect()
self.rect.bottom = bottom
self.rect.centerx = centerx
</code>The ConeHeadZombie subclass implements loadImages to populate the various frame lists for helmet‑walk, helmet‑attack, normal walk, etc., using the naming conventions defined in the graphics folder.
<code>class ConeHeadZombie(Zombie):
def __init__(self, x, y, head_group):
Zombie.__init__(self, x, y, c.CONEHEAD_ZOMBIE, c.CONEHEAD_HEALTH, head_group)
self.helmet = True
def loadImages(self):
self.helmet_walk_frames = []
self.helmet_attack_frames = []
self.walk_frames = []
self.attack_frames = []
self.losthead_walk_frames = []
self.losthead_attack_frames = []
self.die_frames = []
helmet_walk_name = self.name
helmet_attack_name = self.name + 'Attack'
walk_name = c.NORMAL_ZOMBIE
attack_name = c.NORMAL_ZOMBIE + 'Attack'
losthead_walk_name = c.NORMAL_ZOMBIE + 'LostHead'
losthead_attack_name = c.NORMAL_ZOMBIE + 'LostHeadAttack'
die_name = c.NORMAL_ZOMBIE + 'Die'
frame_list = [self.helmet_walk_frames, self.helmet_attack_frames,
self.walk_frames, self.attack_frames,
self.losthead_walk_frames, self.losthead_attack_frames,
self.die_frames]
name_list = [helmet_walk_name, helmet_attack_name,
walk_name, attack_name,
losthead_walk_name, losthead_attack_name,
die_name]
for i, name in enumerate(name_list):
self.loadFrames(frame_list[i], name, tool.ZOMBIE_RECT[name]['x'])
self.frames = self.helmet_walk_frames
</code>The full source code can be downloaded from https://download.csdn.net/download/marble_xu/11639414.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.