前言

  • 其实这个项目再我上半年就想着做一下的,但是一直拖到现在,我现在深刻的理解到,不要想那么多,先做,因为永远不可能准备好,都是边做边学便准备的,完成比完美更重要;
  • 使用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
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐