Plant vs Zombies Python Game: State Management and Core Modules Overview
This article presents a detailed walkthrough of the Python implementation of a Plant vs Zombies style game, covering the state management architecture, key constants, and main execution flow, while providing complete source code snippets for each module.
The document showcases a Python-based Plant vs Zombies game built with Pygame, focusing on the modular state management system that drives menus, gameplay levels, and victory/defeat screens.
state/tool.py defines the core State abstract class and the Control class that handles the game loop, event processing, state transitions, and utility functions for loading graphics and assets.
import os
import json
from abc import abstractmethod
import pygame as pg
from . import constants as c
class State():
def __init__(self):
self.start_time = 0.0
self.current_time = 0.0
self.done = False
self.next = None
self.persist = {}
@abstractmethod
def startup(self, current_time, persist):
''abstract method''
def cleanup(self):
self.done = False
return self.persist
@abstractmethod
def update(self, surface, keys, current_time):
''abstract method''
class Control():
def __init__(self):
self.screen = pg.display.get_surface()
self.done = False
self.clock = pg.time.Clock()
self.fps = 60
self.keys = pg.key.get_pressed()
self.mouse_pos = None
self.mouse_click = [False, False] # [left, right]
self.current_time = 0.0
self.state_dict = {}
self.state_name = None
self.state = None
self.game_info = {c.CURRENT_TIME:0.0, c.LEVEL_NUM:c.START_LEVEL_NUM}
def setup_states(self, state_dict, start_state):
self.state_dict = state_dict
self.state_name = start_state
self.state = self.state_dict[self.state_name]
self.state.startup(self.current_time, self.game_info)
def update(self):
self.current_time = pg.time.get_ticks()
if self.state.done:
self.flip_state()
self.state.update(self.screen, self.current_time, self.mouse_pos, self.mouse_click)
self.mouse_pos = None
self.mouse_click[0] = False
self.mouse_click[1] = False
def flip_state(self):
previous, self.state_name = self.state_name, self.state.next
persist = self.state.cleanup()
self.state = self.state_dict[self.state_name]
self.state.startup(self.current_time, persist)
def event_loop(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self.done = True
elif event.type == pg.KEYDOWN:
self.keys = pg.key.get_pressed()
elif event.type == pg.KEYUP:
self.keys = pg.key.get_pressed()
elif event.type == pg.MOUSEBUTTONDOWN:
self.mouse_pos = pg.mouse.get_pos()
self.mouse_click[0], _, self.mouse_click[1] = pg.mouse.get_pressed()
print('pos:', self.mouse_pos, ' mouse:', self.mouse_click)
def main(self):
while not self.done:
self.event_loop()
self.update()
pg.display.update()
self.clock.tick(self.fps)
print('game over')
def get_image(sheet, x, y, width, height, colorkey=c.BLACK, scale=1):
image = pg.Surface([width, height])
rect = image.get_rect()
image.blit(sheet, (0, 0), (x, y, width, height))
image.set_colorkey(colorkey)
image = pg.transform.scale(image, (int(rect.width*scale), int(rect.height*scale)))
return image
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
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
def loadZombieImageRect():
file_path = os.path.join('source','data','entity','zombie.json')
f = open(file_path)
data = json.load(f)
f.close()
return data[c.ZOMBIE_IMAGE_RECT]
def loadPlantImageRect():
file_path = os.path.join('source','data','entity','plant.json')
f = open(file_path)
data = json.load(f)
f.close()
return data[c.PLANT_IMAGE_RECT]
pg.init()
pg.display.set_caption(c.ORIGINAL_CAPTION)
SCREEN = pg.display.set_mode(c.SCREEN_SIZE)
GFX = load_all_gfx(os.path.join('resources','graphics'))
ZOMBIE_RECT = loadZombieImageRect()
PLANT_RECT = loadPlantImageRect()state/constants.py contains all game-wide constants such as screen dimensions, grid layout, color definitions, game object identifiers, health values, timing intervals, and state identifiers used throughout the engine.
START_LEVEL_NUM = 1
ORIGINAL_CAPTION = 'Plant VS Zombies Game'
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_SIZE = (SCREEN_WIDTH, SCREEN_HEIGHT)
GRID_X_LEN = 9
GRID_Y_LEN = 5
GRID_X_SIZE = 80
GRID_Y_SIZE = 100
WHITE = (255, 255, 255)
NAVYBLUE = (60, 60, 100)
SKY_BLUE = (39, 145, 251)
BLACK = (0, 0, 0)
LIGHTYELLOW = (234, 233, 171)
RED = (255, 0, 0)
PURPLE = (255, 0, 255)
GOLD = (255, 215, 0)
GREEN = (0, 255, 0)
SIZE_MULTIPLIER = 1.3
CURRENT_TIME = 'current time'
LEVEL_NUM = 'level num'
MAIN_MENU = 'main menu'
LOAD_SCREEN = 'load screen'
GAME_LOSE = 'game los'
GAME_VICTORY = 'game victory'
LEVEL = 'level'
MAIN_MENU_IMAGE = 'MainMenu'
OPTION_ADVENTURE = 'Adventure'
GAME_LOOSE_IMAGE = 'GameLoose'
GAME_VICTORY_IMAGE = 'GameVictory'
BACKGROUND_NAME = 'Background'
BACKGROUND_TYPE = 'background_type'
INIT_SUN_NAME = 'init_sun_value'
ZOMBIE_LIST = 'zombie_list'
MAP_EMPTY = 0
MAP_EXIST = 1
BACKGROUND_OFFSET_X = 220
MAP_OFFSET_X = 35
MAP_OFFSET_Y = 100
CHOOSEBAR_TYPE = 'choosebar_type'
CHOOSEBAR_STATIC = 0
CHOOSEBAR_MOVE = 1
CHOSSEBAR_BOWLING = 2
MENUBAR_BACKGROUND = 'ChooserBackground'
MOVEBAR_BACKGROUND = 'MoveBackground'
PANEL_BACKGROUND = 'PanelBackground'
START_BUTTON = 'StartButton'
CARD_POOL = 'card_pool'
MOVEBAR_CARD_FRESH_TIME = 6000
CARD_MOVE_TIME = 60
PLANT_IMAGE_RECT = 'plant_image_rect'
CAR = 'car'
SUN = 'Sun'
SUNFLOWER = 'SunFlower'
PEASHOOTER = 'Peashooter'
SNOWPEASHOOTER = 'SnowPea'
WALLNUT = 'WallNut'
CHERRYBOMB = 'CherryBomb'
THREEPEASHOOTER = 'Threepeater'
REPEATERPEA = 'RepeaterPea'
CHOMPER = 'Chomper'
CHERRY_BOOM_IMAGE = 'Boom'
PUFFSHROOM = 'PuffShroom'
POTATOMINE = 'PotatoMine'
SQUASH = 'Squash'
SPIKEWEED = 'Spikeweed'
JALAPENO = 'Jalapeno'
SCAREDYSHROOM = 'ScaredyShroom'
SUNSHROOM = 'SunShroom'
ICESHROOM = 'IceShroom'
HYPNOSHROOM = 'HypnoShroom'
WALLNUT_BOWLING = 'WallNutBowling'
REDWALLNUT_BOWLING = 'RedWallNutBowling'
PLANT_HEALTH = 5
WALLNUT_HEALTH = 30
WALLNUT_CRACKED1_HEALTH = 20
WALLNUT_CRACKED2_HEALTH = 10
WALLNUT_BOWLING_DAMAGE = 10
PRODUCE_SUN_INTERVAL = 7000
FLOWER_SUN_INTERVAL = 22000
SUN_LIVE_TIME = 7000
SUN_VALUE = 25
ICE_SLOW_TIME = 2000
FREEZE_TIME = 7500
ICETRAP = 'IceTrap'
CARD_SUNFLOWER = 'card_sunflower'
CARD_PEASHOOTER = 'card_peashooter'
CARD_SNOWPEASHOOTER = 'card_snowpea'
CARD_WALLNUT = 'card_wallnut'
CARD_CHERRYBOMB = 'card_cherrybomb'
CARD_THREEPEASHOOTER = 'card_threepeashooter'
CARD_REPEATERPEA = 'card_repeaterpea'
CARD_CHOMPER = 'card_chomper'
CARD_PUFFSHROOM = 'card_puffshroom'
CARD_POTATOMINE = 'card_potatomine'
CARD_SQUASH = 'card_squash'
CARD_SPIKEWEED = 'card_spikeweed'
CARD_JALAPENO = 'card_jalapeno'
CARD_SCAREDYSHROOM = 'card_scaredyshroom'
CARD_SUNSHROOM = 'card_sunshroom'
CARD_ICESHROOM = 'card_iceshroom'
CARD_HYPNOSHROOM = 'card_hypnoshroom'
CARD_REDWALLNUT = 'card_redwallnut'
BULLET_PEA = 'PeaNormal'
BULLET_PEA_ICE = 'PeaIce'
BULLET_MUSHROOM = 'BulletMushRoom'
BULLET_DAMAGE_NORMAL = 1
ZOMBIE_IMAGE_RECT = 'zombie_image_rect'
ZOMBIE_HEAD = 'ZombieHead'
NORMAL_ZOMBIE = 'Zombie'
CONEHEAD_ZOMBIE = 'ConeheadZombie'
BUCKETHEAD_ZOMBIE = 'BucketheadZombie'
FLAG_ZOMBIE = 'FlagZombie'
NEWSPAPER_ZOMBIE = 'NewspaperZombie'
BOOMDIE = 'BoomDie'
LOSTHEAD_HEALTH = 5
NORMAL_HEALTH = 10
FLAG_HEALTH = 15
CONEHEAD_HEALTH = 20
BUCKETHEAD_HEALTH = 30
NEWSPAPER_HEALTH = 15
ATTACK_INTERVAL = 1000
ZOMBIE_WALK_INTERVAL = 70
ZOMBIE_START_X = SCREEN_WIDTH + 50
IDLE = 'idle'
FLY = 'fly'
EXPLODE = 'explode'
ATTACK = 'attack'
ATTACKED = 'attacked'
DIGEST = 'digest'
WALK = 'walk'
DIE = 'die'
CRY = 'cry'
FREEZE = 'freeze'
SLEEP = 'sleep'
CHOOSE = 'choose'
PLAY = 'play'
BACKGROUND_DAY = 0
BACKGROUND_NIGHT = 1state/main.py wires the state objects together, creates a Control instance, registers the menu, victory, lose, and level states, and starts the main game loop.
from . import tool
from . import constants as c
from .state import mainmenu, screen, level
def main():
game = tool.Control()
state_dict = {c.MAIN_MENU: mainmenu.Menu(),
c.GAME_VICTORY: screen.GameVictoryScreen(),
c.GAME_LOSE: screen.GameLoseScreen(),
c.LEVEL: level.Level()}
game.setup_states(state_dict, c.MAIN_MENU)
game.main()The overall structure demonstrates a clean separation of concerns, making the game extensible and maintainable for further development or educational purposes.
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.