仅用pygame+python实现植物大战僵尸-----完成比完美更重要
前言
- 其实这个项目再我上半年就想着做一下的,但是一直拖到现在,我现在深刻的理解到,不要想那么多,先做,因为永远不可能准备好,都是边做边学便准备的,完成比完美更重要;
- 使用python,是因为简单,我感觉大多数00后程序员应该都一个实现植物大战僵尸的梦吧;
- 这一次我深刻体会到了业务的重要性,很多时候对业务不理解,是很难做出点什么东西的,更难成为架构师;
- 还有,我深刻体会到了一句话:“仅修改少量代码,就实现功能”。
环境
- python:3.11.7,pygame:2.6.1
- 编译器:vscode
- 游戏运行结果截图
- 游戏任务图
- 游戏架构图
- main.py
import pygame
import sys
from pygame.locals import *
from const import *
from game import *pygame.init()DS = pygame.display.set_mode((1280, 600))
game = Game(DS)while True:for event in pygame.event.get():if event.type == QUIT:pygame.quit()sys.exit()elif event.type == pygame.MOUSEBUTTONDOWN:game.mouseClickHandle(event.button) # 鼠标事件触发DS.fill((255, 255, 255))game.draw()game.update()pygame.display.update()
- cosnt.py
# 定义一些常量
GAME_SIZE = (1280, 600) # 游戏地图大小
# 网格游戏,一些参数
LEFT_TOP = (200, 65) # 游戏网格,左上角坐标
GRID_SIZE = (76, 96) # 每一个格子大小
GRID_COUNT = (9, 5) # 网格数量,9列5行
# 图片路径
PATH_BACK = "pic/other/back.png"
PATH_LOSS = "pic/other/lose.png"SUNFLOWER_ID = 3
PEASHOOTER_ID = 4
- data_object.py
# 储存固定参数
data = {0 : { # 豌豆'PATH' : 'pic/other/peabullet.png','IMAGE_INDEX_MAX' : 0, # 图片索引范围'IMAGE_INDEX_CD' : 0.0, # 图片更新频率'POSITION_CD' : 0.008, # 位置更新速度'SUMMON_CD' : -1, # 产生物的CD'SIZE' : (44, 44), # 图片缩放大小'SPEED' : (4, 0), # 速度'CAN_LOOT' : False, # 是否可以捡 'PRICE' : 0, # 价格为0'HP' : 1, # 血量'ATT' : 1, # 攻击力},1 : { # 僵尸'PATH' : 'pic/zombie/0/%d.png','IMAGE_INDEX_MAX' : 15,'IMAGE_INDEX_CD' : 0.2,'POSITION_CD' : 0.2,'SUMMON_CD' : -1, 'SIZE' : (100, 128),'SPEED' : (-2.5, 0),'CAN_LOOT' : False,'PRICE' : 0, 'HP' : 5, 'ATT' : 1, },2 : { # 阳光'PATH' : 'pic/other/sunlight/%d.png','IMAGE_INDEX_MAX' : 30,'IMAGE_INDEX_CD' : 0.06,'POSITION_CD' : 0.05,'SUMMON_CD' : -1, 'SIZE' : (80, 80),'SPEED' : (0, 2),'CAN_LOOT' : True,'PRICE' : 25, 'HP' : 1000000000, 'ATT' : 0, },3 : { # 向日葵'PATH' : 'pic/plant/sunflower/%d.png','IMAGE_INDEX_MAX' : 19,'IMAGE_INDEX_CD' : 0.07,'POSITION_CD' : 10000,'SUMMON_CD' : 8, 'SIZE' : (128, 128),'SPEED' : (0, 0),'CAN_LOOT' : False,'PRICE' : 50, 'HP' : 5, 'ATT' : 0, },4 : { # 射手'PATH' : 'pic/plant/peashooter/%d.png','IMAGE_INDEX_MAX' : 15,'IMAGE_INDEX_CD' : 0.15,'POSITION_CD' : 10000,'SUMMON_CD' : 3, 'SIZE' : (128, 128),'SPEED' : (0, 0),'CAN_LOOT' : False,'PRICE' : 100, 'HP' : 5, 'ATT' : 0, },
}
- game.py
import pygame
import image
import zombiebase
import peabullet
import data_object
import sunlight
import sunflower
import peashooter
import data_object
import time
import random
from const import *class Game(object):def __init__(self, ds):self.ds = ds self.back = image.Image(PATH_BACK, 0, (0, 0), GAME_SIZE, 0) # 储存背景self.loss = image.Image(PATH_LOSS, 0, (0, 0), GAME_SIZE, 0) # 游戏结束self.isGameOver = Falseself.plants = []self.summons = []self.zombies = []self.allPrivce = 100 # 初始价格self.priveFont = pygame.font.Font(None, 60) # 字体self.zombieGenerateTime = 0 # 上一次生成僵尸的时间# 打僵尸几分self.zombie = 0self.zombieFont = pygame.font.Font(None, 60)self.hasPlant = []for i in range(GRID_SIZE[0]): # 赋值的是一个格子的col = []for j in range(GRID_SIZE[1]):col.append(0)self.hasPlant.append(col)# 得到要种植的坐标def getIndexByPos(self, pos): # 得到x,y坐标(注意是那种压缩的,就是x * 每个格子宽度 == 真实位置)x = (pos[0] - LEFT_TOP[0]) // GRID_SIZE[0]y = (pos[1] - LEFT_TOP[1]) // GRID_SIZE[1]return x, ydef renderFont(self):textImage = self.priveFont.render("Glod: " + str(self.allPrivce), True, (0, 0, 0))self.ds.blit(textImage, (13, 23))textImage = self.priveFont.render("Glod: " + str(self.allPrivce), True, (255, 255, 255))self.ds.blit(textImage, (10, 20))textImage = self.zombieFont.render("Score: " + str(self.zombie), True, (0, 0, 0))self.ds.blit(textImage, (13, 83))textImage = self.zombieFont.render("Score: " + str(self.zombie), True, (255, 255, 255))self.ds.blit(textImage, (10, 80))def draw(self):self.back.draw(self.ds)for plant in self.plants: # 植物绘制plant.draw(self.ds)for summon in self.summons: # 生成物绘制summon.draw(self.ds)for zombie in self.zombies:zombie.draw(self.ds)# 绘制金额self.renderFont()# 是否结束if self.isGameOver:self.loss.draw(self.ds)def update(self):self.back.update()for plant in self.plants:plant.update()if plant.hasSummon(): # 有生成物summ = plant.doSummon() # 就生成self.summons.append(summ) # 给game管理生命周期for summon in self.summons:summon.update()for zombie in self.zombies:zombie.update()# 更新一次,看是否能产生僵尸if time.time() - self.zombieGenerateTime > 10:self.zombieGenerateTime = time.time()self.addZombie(14, random.randint(0, 4))self.checkSummonVsZombie()self.checkZombieVsPlant()# 游戏是否结束for z in self.zombies:if z.getRect().x < 0:self.isGameOver = True# 子弹超出屏幕,需要销毁for summon in self.summons:if summon.getRect().x > GAME_SIZE[0] or summon.getRect().y > GAME_SIZE[1]:self.summons.remove(summon)break # 退出是因为[]索引会改变# 僵尸和植物对抗def checkSummonVsZombie(self):for summon in self.summons:for zombie in self.zombies:if summon.isCollide(zombie): # 僵尸和植物对抗self.fight(summon, zombie) # 对抗if zombie.hp <= 0:self.zombies.remove(zombie) # 移除僵尸self.zombie += 1 # 加分if summon.hp <= 0:self.summons.remove(summon) # 移除植物return # 僵尸吃植物def checkZombieVsPlant(self):for zombie in self.zombies:for plant in self.plants:if zombie.isCollide(plant):self.fight(zombie, plant)if plant.hp <= 0:self.plants.remove(plant)break# 产生阳光def addSunFlower(self, i, j):pos = LEFT_TOP[0] + i * GRID_SIZE[0], LEFT_TOP[1] + j * GRID_SIZE[1]sf = sunflower.SunFlower(3, pos)self.plants.append(sf)# 产生豌豆def addPeaShooter(self, x, y):pos = LEFT_TOP[0] + x * GRID_SIZE[0], LEFT_TOP[1] + y * GRID_SIZE[1]sf = peashooter.PeaShooter(PEASHOOTER_ID, pos)self.plants.append(sf)# 产生僵尸def addZombie(self, x, y):pos = LEFT_TOP[0] + x * GRID_SIZE[0], LEFT_TOP[1] + y * GRID_SIZE[1]zom = zombiebase.ZombieBase(1, pos)self.zombies.append(zom)# 对抗def fight(self, a, b):while True:a.hp -= b.attackb.hp -= a.attackif b.hp <= 0: # a 打败 breturn True if a.hp <= 0: # b 打败 areturn Falsereturn Falsedef checkLoot(self, mousePos):for summon in self.summons:if not summon.getIsLoot():continue rect = summon.getRect() # 获取图片矩形if rect.collidepoint(mousePos): # 点击坐标是否在举行区域内self.summons.remove(summon) # 移除内存self.allPrivce += summon.getPrice() # 金额增加return True return False# 种def checkAddPlant(self, mousePos, objjId):x, y = self.getIndexByPos(mousePos)# 判断是否能种植if x < 0 or x >= GRID_COUNT[0]:return if y < 0 or y >= GRID_COUNT[1]:return# 不能重复种种植判断if self.hasPlant[x][y] == 1:returnself.hasPlant[x][y] = 1# 金币扣除if self.allPrivce < data_object.data[objjId]['PRICE']:returnself.allPrivce -= data_object.data[objjId]['PRICE']if objjId == SUNFLOWER_ID: # 种花self.addSunFlower(x, y)elif objjId == PEASHOOTER_ID: # 种射手self.addPeaShooter(x, y)# 鼠标事件def mouseClickHandle(self, btn):# 游戏结束,鼠标不能种植if self.isGameOver:return mousePos = pygame.mouse.get_pos() # 获取鼠标位置if self.checkLoot(mousePos): # 触发,不能再种其他东西了return if btn == 1: # 鼠标左键self.checkAddPlant(mousePos, SUNFLOWER_ID)elif btn == 3:self.checkAddPlant(mousePos, PEASHOOTER_ID)
- image.py
import pygameclass Image(pygame.sprite.Sprite):def __init__(self, pathFmt, pathIndex, pos, size=None, pathIndexCount=0):self.pathFmt = pathFmtself.pathIndex = pathIndex # 存储索引self.pos = list(pos) # ()元组不支持修改,但list可以,这里可以修改坐标self.size = size # 窗口大小self.pathIndexCount = pathIndexCount # 储存图片索引最大下标self.updateImage() # 显示图片# 更新图片def updateImage(self):path = self.pathFmtif self.pathIndexCount != 0: # 更新图片目录path = path % self.pathIndexself.image = pygame.image.load(path) # 更新图片,贴图if self.size: # 有大小,则缩放self.image = pygame.transform.scale(self.image, self.size)# 更新图片大小def updateSize(self):self.size = sizeself.updateImage()# 更新图片索引def updateIndex(self, pathIndex):self.pathIndex = pathIndexself.updateImage() # 索引更新,则更新图片# 获取图片大小和坐标def getRect(self):rect = self.image.get_rect()rect.x, rect.y = self.pos # 移动本质是图片坐标的更改(左上角)return rect# 僵尸移动def doLeft(self):self.pos[0] -= 0.15 # 移动速度,本质是坐标修改def draw(self, ds):ds.blit(self.image, self.getRect())
- objectbase.py
import image
import time
import data_objectclass ObjectBase(image.Image):def __init__(self, id, pos):# 定义时间,实现自驱动self.preTimeIndex = 0self.prePositionTime = 0self.preSummonTime = 0# 储存不同物体的idself.id = id# 血量和攻击力self.hp = self.getData()['HP']self.attack = self.getData()['ATT']# 继承super(ObjectBase, self).__init__(self.getData()['PATH'],0,pos,self.getData()['SIZE'],self.getData()['IMAGE_INDEX_MAX'])# 返回不同的数据def getData(self):return data_object.data[self.id]# 返回速度def getSpeed(self):return self.getData()['SPEED']# 返回更新动画的时间def getPositionCD(self):return self.getData()['POSITION_CD']# 返回图片更新时间def getImageIndexCD(self):return self.getData()['IMAGE_INDEX_CD']# 产生物的时间def getSummonCD(self):return self.getData()['SUMMON_CD']# 返回是否可以捡def getIsLoot(self):return self.getData()['CAN_LOOT']# 返回植物相应的价格def getPrice(self):return self.getData()['PRICE']# 相撞def isCollide(self, other):return self.getRect().colliderect(other.getRect()) # 相撞# 更新动画def update(self):self.checkImageIndex() # 更新帧动画self.checkPosition() # 更新图片坐标self.checkSummon()# 是否产生了生成物def checkSummon(self):if time.time() - self.preSummonTime <= self.getSummonCD():return self.preSummonTime = time.time()# 调用产生阳光self.preSummon()def checkImageIndex(self):#储存更新时间if time.time() - self.preTimeIndex <= self.getImageIndexCD(): # 间隔时间和位置更新一致return self.preTimeIndex = time.time()idx = self.pathIndex + 1if idx >= self.pathIndexCount:idx = 0self.updateIndex(idx)def checkPosition(self):if time.time() - self.prePositionTime <= self.getPositionCD(): # 间隔时间和位置更新一致return Falseself.prePositionTime = time.time()speed = self.getSpeed()self.pos = (self.pos[0] + speed[0], self.pos[1] + speed[1])return Truedef preSummon(self):pass# 是否有产生物def hasSummon(self):pass# 生产产生物def doSummon(self):pass
- peabullet.py
import objectbase class PeaBullet(objectbase.ObjectBase):pass
- peashooter.py
import objectbase
import peabullet
import timeclass PeaShooter(objectbase.ObjectBase):def __init__(self, id, pos):super(PeaShooter, self).__init__(id, pos)self.hasBullet = False # 可以发射子弹self.hasShoot = False # 立即发射def hasSummon(self):return self.hasBullet# 产生阳光def preSummon(self):self.hasShoot = True # 发射self.pathIndex = 0 # 索引为0# 种阳光def doSummon(self):if self.hasSummon():self.hasBullet = False return peabullet.PeaBullet(0, (self.pos[0] + 20, self.pos[1] + 30))def checkImageIndex(self):#储存更新时间if time.time() - self.preTimeIndex <= self.getImageIndexCD(): # 间隔时间和位置更新一致return self.preTimeIndex = time.time()idx = self.pathIndex + 1if idx == 8 and self.hasShoot:self.hasBullet = True # 发送if idx >= self.pathIndexCount: # 不发射情况idx = 9self.updateIndex(idx)
- sunflower.py
import objectbase
import sunlightclass SunFlower(objectbase.ObjectBase):def __init__(self, id, pos):super(SunFlower, self).__init__(id, pos)self.hasSunlight = Falsedef hasSummon(self):return self.hasSunlight# 产生阳光def preSummon(self):self.hasSunlight = True# 种阳光def doSummon(self):if self.hasSummon():self.hasSunlight = False return sunlight.SunLight(2, (self.pos[0] + 20, self.pos[1] + 10))
- sunlight.py
import objectbase class SunLight(objectbase.ObjectBase): pass
- zombiebase.py
import objectbase class ZombieBase(objectbase.ObjectBase): pass