前言
- 其实这个项目再我上半年就想着做一下的,但是一直拖到现在,我现在深刻的理解到,不要想那么多,先做,因为永远不可能准备好,都是边做边学便准备的,完成比完美更重要;
- 使用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 = False
self.plants = []
self.summons = []
self.zombies = []
self.allPrivce = 100 # 初始价格
self.priveFont = pygame.font.Font(None, 60) # 字体
self.zombieGenerateTime = 0 # 上一次生成僵尸的时间
# 打僵尸几分
self.zombie = 0
self.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, y
def 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.attack
b.hp -= a.attack
if b.hp <= 0: # a 打败 b
return True
if a.hp <= 0: # b 打败 a
return False
return False
def 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:
return
self.hasPlant[x][y] = 1
# 金币扣除
if self.allPrivce < data_object.data[objjId]['PRICE']:
return
self.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 pygame
class Image(pygame.sprite.Sprite):
def __init__(self, pathFmt, pathIndex, pos, size=None, pathIndexCount=0):
self.pathFmt = pathFmt
self.pathIndex = pathIndex # 存储索引
self.pos = list(pos) # ()元组不支持修改,但list可以,这里可以修改坐标
self.size = size # 窗口大小
self.pathIndexCount = pathIndexCount # 储存图片索引最大下标
self.updateImage() # 显示图片
# 更新图片
def updateImage(self):
path = self.pathFmt
if self.pathIndexCount != 0: # 更新图片目录
path = path % self.pathIndex
self.image = pygame.image.load(path) # 更新图片,贴图
if self.size: # 有大小,则缩放
self.image = pygame.transform.scale(self.image, self.size)
# 更新图片大小
def updateSize(self):
self.size = size
self.updateImage()
# 更新图片索引
def updateIndex(self, pathIndex):
self.pathIndex = pathIndex
self.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_object
class ObjectBase(image.Image):
def __init__(self, id, pos):
# 定义时间,实现自驱动
self.preTimeIndex = 0
self.prePositionTime = 0
self.preSummonTime = 0
# 储存不同物体的id
self.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 + 1
if idx >= self.pathIndexCount:
idx = 0
self.updateIndex(idx)
def checkPosition(self):
if time.time() - self.prePositionTime <= self.getPositionCD(): # 间隔时间和位置更新一致
return False
self.prePositionTime = time.time()
speed = self.getSpeed()
self.pos = (self.pos[0] + speed[0], self.pos[1] + speed[1])
return True
def 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 time
class 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 + 1
if idx == 8 and self.hasShoot:
self.hasBullet = True # 发送
if idx >= self.pathIndexCount: # 不发射情况
idx = 9
self.updateIndex(idx)
- sunflower.py
import objectbase
import sunlight
class SunFlower(objectbase.ObjectBase):
def __init__(self, id, pos):
super(SunFlower, self).__init__(id, pos)
self.hasSunlight = False
def 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