注:这个系列文章的全部内容里面包含自己写的一些思路,难免会有时候同一个文章中需要多次修改代码的情况,但是编程就是这样(个人觉得)在修改中不断的完善代码,慢慢解决bug,最后的效果我虽然不清楚能不能完全做出来,但是不试试怎么知道 !
前面我们已经完成了对场景的布置,包括选择卡片的效果,那么今天,我们的目标是实现植物的放置。
因为我们前面完成了对card类的编写,但是还差一个功能,就是卡片还有冷却效果,那么接下来,我们看看怎么编写冷却效果,先来看看冷却效果:
那么这个冷却效果是怎么实现的呢,我们需要进入到Card.py文件当中,编写CD函数,帮助我们实现矩形的填充绘制,在编写CD函数之前,我们先要对init函数添加一些数据
def __init__(self, window, data, column):
"""
卡片角色
:param window: 当前的绘图窗口
:param data: 卡片附带的数据
:param column: 这是第几个卡牌,布局使用,从0开始传入
"""
... # 这里原来代码不变
self.is_cd = True
self.cd = time.time()
self.cd_time = self.data["cd"] # 这个是这个卡片的cd时间
if self.data["start"]:
self.cd -= self.cd_time
在这之后,我们就可以开始编写CD函数了,这个函数用来控制CD填充
def CD(self):
"""进入CD的冷却动画,矩形填充"""
if time.time() - self.cd < self.cd_time:
# 计算冷却进度百分比
progress = (time.time() - self.cd) / self.cd_time
# 计算冷却矩形的高度
height = self.h - int(self.h * progress) # 如果没有self.h - 那么就会让矩形倒过来绘制,即从上往下
# 绘制冷却矩形
cooldown_surface = pygame.Surface((self.w, height))
cooldown_surface.set_colorkey((255, 255, 255)) # 设置颜色键
cooldown_surface.fill((0, 0, 0))
cooldown_surface.set_alpha(140) # 设置透明度
# 开始绘制矩形填充
self.window.blit(cooldown_surface, self.topleft)
else:
self.is_cd = False
填充功能完毕后,我们也需要同时将click事件处理一下,这样方便我们卡槽的使用,参考如下:
# Card.py
def click(self):
"""鼠标点击的事件"""
mouse_x, mouse_y = pygame.mouse.get_pos()
if not self.is_cd and self.x < mouse_x < self.x + self.w and self.y < mouse_y < self.y + self.h:
# TODO: 后续这里编写的是监测是否放置成功,现在先直接进入CD,也不扣除阳光
self.is_cd = True
self.cd = time.time()
return True # 当鼠标处于图片这个区间内,返回真的内容,点击事件需要额外被调用
接下来就可以开始同步Other_function.py文件里面的植物卡槽类,我们给它同步更新编写一个点击事件,用于调用的逻辑:
# Other_function.py
def click(self, click_type='LEFT'):
"""判断鼠标是否有被点击"""
if click_type == "LEFT" and not self.draw_plant: # 点击左键,并且没有绘制植物的情况
for i in self.card:
if i.click():
self.draw_plant = pygame.image.load(i.data["image"]) # 加载图片
break
elif click_type == "RIGHT" and self.draw_plant:
self.draw_plant = None
最后,我们再main里面同步一下点击的事件,这样就可以做到main.py → Other_function.py → Card.py这样的话我们的逻辑就比较清晰了,在main这里,你也得告诉电脑你点击了左键还是右键,所以,我们需要在main.py当中添加一些内容,下面是完整的main.py文件,点击事件我暂时放在了other_event中,当然,也可以放入mouse_click中,我这里为了方便,就放other_event当中了:
import pygame
import Setting
import Other_function
from pygame.locals import *
pygame.init()
window = pygame.display.set_mode((Setting.WIDTH, Setting.HEIGHT)) # 窗口
BG = pygame.image.load(Setting.BackGround[0]) # 先准备加载素材
Seed_Bank = Other_function.SeedBank(window, 150, [str(i) for i in range(1, 7)]) # 植物银行,存放植物种子
# card = pygame.transform.scale(pygame.image.load("File/Card/SunFlower.png"), (60, 75))
mouse_dicts = {1: "LEFT", 2: "MIDDLE", 3: "RIGHT"} # 鼠标按下后对应按钮返回的数据
def mouse_click():
"""鼠标的点击事件,用于处理卡片选择和植物种植,以及铲子的功能"""
def mouse_right():
"""右键事件,点击后取消卡片选择和铲子的功能"""
def set_up():
"""初始化数据加载,每一关开始就运行一次,对于一些数据的初始化"""
pass
def other_event():
"""一些其他事件,如点击了关闭按钮"""
for event in pygame.event.get():
if event.type == QUIT:
return True
elif event.type == pygame.MOUSEBUTTONDOWN:
"""鼠标点击下的事件"""
Seed_Bank.click(mouse_dicts[event.button])
def draw():
"""绘制函数,控制绘制内容的主逻辑"""
window.blit(BG, (-200, 0))
Seed_Bank.draw()
def run():
"""控制整个程序的主逻辑"""
clock = pygame.time.Clock() # 开启游戏时钟
pygame.display.set_caption("植物大战僵尸 - Pygame") # 设置窗口标题
while True:
clock.tick(60) # 设置游戏帧率
if other_event(): # 如点击了退出,那么程序结束
break
draw() # 运行绘制函数
pygame.display.update() # 更新屏幕
if __name__ == '__main__':
run()
到这里开始,我们的大体功能完成的差不多了,但是还是有一些小的功能,比如阳光的检测,阳光不足我们也可以带有提示,但是直接选择的话因为卡片和植物卡槽是区别开的内容,所以这里,我们可能需要一个通用的数据来记录阳光,在这里,我的做法是,建立一个Other_data.py文件,专门用来储存初始阳光,后续哪怕关卡读取,我们也可以在这里进行操作
下面是Other_data.py
sun = 150 # 初始阳光
def change_sun(sub):
global sun
sun -= sub
但是这样的影响就是,我们之前写的植物卡槽关于阳光的内容,就得全部去除了,并且将Other_data的sun变量引入,来控制sun的阳光,同时在main.py的调用也得受到一些影响,我们实例化植物卡槽的时候,就不能传入阳光了。
main.py文件改
Seed_Bank = Other_function.SeedBank(window, [str(i) for i in range(1, 7)]) # 植物银行,存放植物种子
Card.py文件改
import Other_data
class card(pygame.sprite.Sprite):
def __init__(self, window, data, column):
... # 前面的代码不变
def click(self):
"""鼠标点击的事件"""
mouse_x, mouse_y = pygame.mouse.get_pos()
if not self.is_cd and self.x < mouse_x < self.x + self.w and self.y < mouse_y < self.y + self.h:
if Other_data.sun - self.data["sun"] < 0:
return False
# TODO: 后续这里编写的是监测是否放置成功,现在先直接进入CD,也不扣除阳光
self.is_cd = True
self.cd = time.time()
return True # 当鼠标处于图片这个区间内,返回真的内容,点击事件需要额外被调用
... # 后面代码也不变
Other_function.py文件改
# Other_function.py
# ... 表示原来内容不变
class SeedBank(pygame.sprite.Sprite):
def __init__(self, window, card_list: list, **kwargs):
...
self.Sun = Text(self.window, str(sun),
self.sun_pos[0] + self.sun_img.get_rect().size[0] / 2,
self.sun_pos[1] + self.sun_img.get_rect().bottom,
**kwargs)
...
def change_sun(self):
"""更新阳光数据内容"""
self.Sun.change_src(str(sun))
...
当然,这样做的好处就是,我可以在植物卡片的界面,丰富我们的点击事件,当我们点击卡片的时候,就可以帮助我们去放置植物等操作!
那么接下来我们就可以开始编写放置植物的事件了,下面先看看效果
要想完成这个效果,我们需要对一些功能进行完善
完成这个效果,那么关于放置的任务我们就需要在Other_function.py文件当中进行,而且关于场地我们也要有限制,只能在场地没有东西的时候我们才可以在这里放置我们的植物,那么,我们就还需要有东西来帮助我们进行场地的确定,当我选择这块地方的时候,那么我就需要确定这里有没有植物,可以在Other_data里面,我们写入场地列表,用0表示没植物,1表示有植物,而且还需要先计算好每个草地的格子,这个计算还是花了不少时间的,实际情况可能会因为自己的文件有所差异,需要自己调整,同时,我们要实现植物的放置,还得有一个精灵组来储存我们的每一个植物,现在Other_data.py大体上是这样的:
Other_data.py
import pygame.sprite
sun = 100 # 初始阳光
# 场地相关
pos = [[50 + x * 80, 50 + (x + 1) * 80, 50 + y * 100, 50 + (y + 1) * 100] for x in range(9) for y in range(5)]
# print(pos)
field = [0] * 45 # 场地记录,判断场地是否有植物,0表示没有,1表示有
plants = pygame.sprite.Group() # 精灵组,储存全部的植物
bullets = pygame.sprite.Group() # 子弹组,储存全部的子弹
zombie_num = [0 for _ in range(5)] # 记录僵尸在每一行的个数,用于植物自动攻击
zombies = pygame.sprite.Group() # 记录全部的僵尸
def change_sun(sub):
global sun
sun -= sub
在这里,pos表示的是每一个格子的位置(大致),我是取一定格子,在main.py文件当中显示了位置,最后通过计算平均值计算的大体位置,总的来看还不错,这是全部格子的图片:
也有一些缺点,就是有时候点击的格子位置有一点问题,这个需要去慢慢的调整
那么现在关于场地的内容我们也做完了,接下来就是点击的时候,我们需要放置植物了,但是我们现在还没有植物,所以,我们应该要先创建一个植物类,因为植物类和僵尸类都是属于gif图片,pygame本身不支持gif图片,所以我们可以再写一个py文件来储存一个通用的精灵类,使植物继承这个类的属性,在这里,我们需要使用新的py文件来储存,因为Other_function涉及到植物类的引用,我们如果在plant文件当中再写上导入Other_function大概率会出错,所以,我们新建一个名字叫function的py文件,用来储存植物和僵尸通用的gif加载类。
在这之前,请先下载PIL库用来处理gif图片
pip install pillow
下面是编写的function文件,其实当时我这边也不怎么会使用这个库,后来节约时间,在这里,我直接让GPT4给编写了一个,然后代码的注释和格式我这稍微改变了一下,其他地方的具体含义我不是特别清楚,不过用起来暂时没问题
import os
import pygame
from PIL import Image # 导入PIL库的Image模块
class ImageLoader:
"""根据GPT4的说法,使用类可以在一定程度上优化性能和内存,尤其是图片帧数超出10帧之后,空间和时间都可以节约,这里的类共享"""
_cache = {}
@staticmethod
def load_image(path):
if path not in ImageLoader._cache:
if not os.path.exists(path):
raise FileNotFoundError(f"No file '{path}' found.")
image = pygame.image.load(path)
ImageLoader._cache[path] = image
return ImageLoader._cache[path]
@staticmethod
def load_animation_frames(path):
if path not in ImageLoader._cache:
if not os.path.exists(path):
raise FileNotFoundError(f"No file '{path}' found.")
gif = Image.open(path)
frames = []
for frame_index in range(gif.n_frames):
gif.seek(frame_index)
frame = gif.convert("RGBA")
frame_pygame = pygame.image.fromstring(frame.tobytes(), frame.size, 'RGBA')
frames.append(frame_pygame)
ImageLoader._cache[path] = frames
return ImageLoader._cache[path]
class Sprite(pygame.sprite.Sprite):
def __init__(self, window, x: int, y: int, image_path: str, data: dict, **kwargs):
"""
实例化一个通用的类,加载gif图片的精灵
:param window: 游戏绘制的窗口
:param x: 这个精灵的x坐标
:param y: 这个精灵的y坐标
:param image_path: 精灵图片路径
:param data: 包含精灵各种属性的字典
"""
# 初始化
super().__init__()
# 基础参数
self.window = window
self.kwargs = kwargs # 存储其他参数,以防报错
# 图片处理
# self.frames = ImageLoader.load_frames(image_path) # 使用ImageLoader加载帧
self.frames = ImageLoader.load_animation_frames(image_path)
self.image_index = 0 # 初始化图片索引值
self.image = self.frames[self.image_index] # 加载第一帧图片
# 通用属性
self.rect = self.image.get_rect()
self.rect.center = (x, y) # x 坐标我给的是中心点
self.rect.y = y # y按我给的
self.data = data # 字典数据,包含精灵的各种属性
# 单独属性,从字典当中取出的属性,方便我们进行操作
self.health = data["life"] # 获取对应的生命值
self.attack_interval = data["attack_interval"] # 设置植物的攻击间隔(单位:毫秒)
# 关于时间的变量
self.last_attack_time = 0
# 图片动画的时间相关变量
self.last_frame_update_time = 0 # 记录上一帧更新的时间
self.frame_interval = 100 # 设置帧间隔(单位:毫秒)
def update(self):
if self.health <= 0:
self.kill()
time_since_last_attack = pygame.time.get_ticks() - self.last_attack_time
if time_since_last_attack >= self.attack_interval:
self.last_attack_time = pygame.time.get_ticks() # 后续需要刷新时间需要在attack函数当中编写,先预留
self.attack()
time_since_last_frame_update = pygame.time.get_ticks() - self.last_frame_update_time
if time_since_last_frame_update >= self.frame_interval:
self.last_frame_update_time = pygame.time.get_ticks()
self.image_index = (self.image_index + 1) % len(self.frames)
self.image = self.frames[self.image_index]
return None
def attack(self):
"""这里写上攻击的逻辑"""
在这里,我使用了类来确定加载每一帧的图片,附上和GPT4的对话(下方蓝色字体,不想知道原理的可以不看下面蓝字部分),后续慢慢的理解,有大佬知道的也可以告诉我是不是。一开始是直接每一次运行updata会加载一次图片,感觉多了之后可能会有点卡,慢慢的去优化了算法这些东西,具体对不对我就不清楚了。
------------------------------------------------------- 对话即将开始 -------------------------------------------------------
Q:这样写效率会下降吗?我觉得好像每次都要打开图片。
A:你是对的。在每次帧更新的函数中打开图像文件load_image
效率低下。更好的方法是在类初始化期间打开一次图像文件Plant
,然后将所有帧存储在一个列表中。
Q:与之前的方法相比,这样可以显着提高效率,但是图片太多会不会导致空间不足呢?你能推荐我用空间换时间还是用时间换空间。
A:是的,这种更新的方法显着提高了效率,因为它一次将所有帧加载到内存中,避免了重复打开图像文件的需要。但是,正如您所指出的,将所有帧加载到内存中可能会占用大量空间,尤其是当您有大量图像或高分辨率图像时。
时间和空间之间总是存在权衡,最佳方法取决于项目的具体要求和限制。以下是一些建议:
1.如果您有足够的内存并且您最关心的是应用程序的性能,您可以使用更新的方法将所有帧加载到内存中(空间换时间)。这将显着减少运行时花费在图像处理上的时间。
2.如果内存有限或者您正在处理大量高分辨率图像,则可以使用之前的方法,即为每个帧更新(时间换空间)打开图像文件。这将消耗更少的内存,但可能会导致性能下降。
3.或者,您可以通过将有限数量的帧加载到内存中并根据需要动态加载剩余的帧来在这两种方法之间取得平衡。这种方法将在性能和内存消耗之间保持合理的平衡。
总之,最佳方法取决于项目的具体要求,您应该根据应用程序的限制考虑时间和空间之间的权衡。
Q:能不能把加载的图片放到一个类里,既省空间又省时间?因为我不是很明白,你能根据你的想法给我一个建议吗?如果确实节省空间和时间,我该如何修改代码?如果不可以,请告诉我不能的原因是什么。
A:将加载的图像放入单独的类中可以帮助您更有效地管理和重用图像。您可以创建一个ImageLoader
类来加载和存储图像,然后在您的Plant
类中使用此类来加载必要的帧。Plant
如果类的多个实例使用相同的图像,这种方法可以节省时间和空间,因为图像将在ImageLoader
类中仅加载和存储一次。
ImageLoader
这是该类和更新后的类的建议实现Plant
:在此实现中,该类ImageLoader
加载帧并将其存储在名为 的字典中_cache
。如果特定图像路径的帧已经加载,该load_frames
方法将返回缓存的帧而不是再次加载它们。这种方法可以节省时间(通过避免冗余图像加载)和空间(通过只存储图像一次)。
但是,如果该类的每个实例都Plant
使用唯一的图像,这种方法可能不会节省太多空间或时间,因为仍然需要为每个实例单独加载图像。在这种情况下,您可能需要找到其他方法来优化您的代码,例如使用较低分辨率的图像或减少动画中的帧数。
Q:我的图片大约有10帧,场地需要存储多张图片,可能超过100张
A:如果您有多个类实例Plant
使用同一组图像,ImageLoader
前面提到的方法仍然有助于减少内存使用和提高性能。但是,如果您有许多独特的图像,为每个图像加载所有帧会消耗大量内存。
在这种情况下,您可以考虑使用结合了这两种方法的混合方法。您可以为每种植物类型将有限数量的帧加载到内存中,并在需要时动态加载剩余的帧。这样,您将在性能和内存消耗之间保持平衡。
下面是该类的修改版本ImageLoader
,它实现了最近最少使用 (LRU) 缓存以存储有限数量的帧:此实现将缓存中存储的唯一图像数量限制为_cache_size
. 当超过缓存大小时,最近最少使用的图像将被删除。这样,您可以控制内存使用,同时仍然受益于缓存常用图像的性能改进。
调整_cache_size
参数以在您的特定用例的内存使用和性能之间找到最佳平衡点。
Q:多个class调用不会降低效率吗。
A:使用多个类调用会对效率产生较小的影响,但与改进的代码组织、可重用性和类的缓存机制所提供的好处相比,这种影响通常可以忽略不计ImageLoader
。
在实践中,额外的类调用引入的开销是最小的,因为现代编程语言和硬件已经过优化以有效地处理此类操作。使用该类获得的优势ImageLoader
(例如减少文件读取和图像处理以及更好的内存管理)超过了额外类调用的轻微性能成本。
总而言之,虽然使用多个类调用的性能成本很小,但类提供的缓存和内存管理带来的整体效率提升ImageLoader
使这种方法值得。
Q:图片均为gif动画(约10帧)。我的图片中的每一个字符都是一个类,类中有一个ImageLoader来加载缓存。在这种情况下,您是否仍建议使用 ImageLoader 类来节省空间和时间。
A:是的,在这种情况下使用ImageLoader
带有缓存的类仍然是有益的,因为它有助于减少加载图像时的磁盘访问时间。当您有多个具有相同动画的类实例时,缓存机制可以帮助将图像仅加载到内存中一次,然后在所有实例之间共享它们,从而节省空间和时间。
但是,由于您的所有图像都是 GIF 动画,因此必须有效地管理缓存以确保您的游戏不会耗尽内存或出现性能问题。您可以考虑对类进行以下改进ImageLoader
以优化其性能:
1.实施 LRU(最近最少使用)缓存或其他缓存逐出策略,以根据最近的访问模式更好地管理内存使用情况。这样,当缓存达到一定大小时,您可以通过驱逐最近最少使用的图像来释放内存。
2.按需加载 GIF 帧:您可以在需要时只加载所需的帧,而不是一次加载每个动画的所有帧。这将减少内存使用并提高性能,尤其是在多个动画未同时播放的情况下。您仍然可以使用缓存来存储最近使用的帧以避免磁盘访问开销。
3.使用更高效的图像格式,例如 WebP 或类似格式,可以提供更好的压缩效果而不会显着降低质量。这将有助于进一步减少内存使用。
4.优化动画中的帧速率或帧数。降低帧速率或帧总数有助于降低内存使用量和处理开销。
这些优化可以帮助您根据游戏的具体要求在内存使用、处理时间和视觉质量之间取得平衡。
Q:请尽力帮助我编写 ImageLoader 类。我需要尽可能节省空间和时间。我现在告诉你,在我的 50 张图片中,有 30 张实际上是重复的,谢谢。
A:如果您的 50 张图像中有 30 张是重复的,则使用ImageLoader
缓存可以节省空间和时间。这是该类的改进版本ImageLoader
,它还处理 gif 类动画中帧序列的缓存,现在,在您的Plant
类或任何其他使用图像或动画的类中,您可以使用该类ImageLoader
有效地加载图像和帧序列:通过此设置,该类ImageLoader
将缓存图像和动画,确保它们只加载一次并在需要相同图像或动画的所有实例之间共享。这将帮助您节省内存并提高性能,尤其是当您的实例中有重复的图像或动画时。
请记住,这是一个基本的实现。您可能需要考虑更高级的缓存策略(例如 LRU)或按需帧加载以实现更好的优化。
------------------------------------------------------- 对话暂时结束 -------------------------------------------------------
当我们完成了通用类加载后,就可以开始编写plant.py了,因为我这边暂时就只有6个植物,所以在这里我就写了6个植物类!
import inspect
import function
class PeaShooter(function.Sprite):
def __init__(self, window, x: int, y: int, image_path: str, data: dict, **kwargs):
super().__init__(window, x, y, image_path, data, **kwargs)
class SunFlower(function.Sprite):
def __init__(self, window, x: int, y: int, image_path: str, data: dict, **kwargs):
super().__init__(window, x, y, image_path, data, **kwargs)
class PotatoMine(function.Sprite):
def __init__(self, window, x: int, y: int, image_path: str, data: dict, **kwargs):
super().__init__(window, x, y, image_path, data, **kwargs)
class IceArcher(function.Sprite):
def __init__(self, window, x: int, y: int, image_path: str, data: dict, **kwargs):
super().__init__(window, x, y, image_path, data, **kwargs)
class WallNut(function.Sprite):
def __init__(self, window, x: int, y: int, image_path: str, data: dict, **kwargs):
super().__init__(window, x, y, image_path, data, **kwargs)
class CherryBomb(function.Sprite):
def __init__(self, window, x: int, y: int, image_path: str, data: dict, **kwargs):
super().__init__(window, x, y, image_path, data, **kwargs)
# 获取当前脚本中所有定义的对象
def get_all_class(not_save: list = None) -> list:
"""
得到当前的py文件下全部的类
:param not_save: 不储存的类名字
:return: 一个列表,列表当中不包含不储存的类
"""
members = inspect.getmembers(
inspect.getmodule(inspect.currentframe()),
lambda member: inspect.isclass(member)
)
data = []
for i in members:
if not_save:
if i[0] not in not_save:
data.append(i[1])
else:
data.append(i[1])
return data
# 筛选出所有类对象
AllPlant = get_all_class()
if __name__ == '__main__':
for a in AllPlant:
print(a.__name__)
获取类的名字也是GPT4告诉我怎么办的,然后我直接修改为了自己需要的东西,效果上是没问题的,这样的好处就是,我可以无限制的添加植物,并且电脑可以自动识别我要的内容,减去了不少的麻烦,后面更新和维护会方便很多。
到这里,plant类也完成了,接下来就可以对卡片类,即card.py进行操作了,在这里,我先是修改了一下放置的代码click,如果没有阳光,那么返回假,不能放置。
def click(self):
"""鼠标点击的事件"""
mouse_x, mouse_y = pygame.mouse.get_pos()
if not self.is_cd and self.x < mouse_x < self.x + self.w and self.y < mouse_y < self.y + self.h:
if Other_data.sun - self.data["sun"] < 0:
return False
return True # 当鼠标处于图片这个区间内,返回真的内容,点击事件需要额外被调用
然后,我们就可以开始编写CD函数了,这样写的时候不会进入到CD当中,因为我们知道,在植物大战僵尸当中,只有你成功放置,才可以进入CD,所以,我在card.py文件下Card类加了一个函数在clcik后面,控制进入CD,调用一次即可进入CD:
def get_plant(self):
"""成功放置植物,刷新CD"""
self.is_cd = True
self.cd = time.time()
至此,我的Card函数完成,因为中间间隔时间有点久,不清楚到底有没有遗漏,这里附上完整的card.py代码
import time
import Plant
import pygame
import Other_data
# print([i for i in dir(Plant) if isinstance(getattr(Plant, i), type)]) # 获取文档当中全部类的方法
class card(pygame.sprite.Sprite):
def __init__(self, window, data, column):
"""
卡片角色
:param window: 当前的绘图窗口
:param data: 卡片附带的数据
:param column: 这是第几个卡牌,布局使用,从0开始传入
"""
pygame.sprite.Sprite.__init__(self) # 初始格式化为角色
self.window = window
self.data = data
self.column = column
self.ima = pygame.image.load(self.data["CardImage"]) # 图片内容加载
self.x, self.y = 96 + self.column * 60, 5 # 植物卡片的x和y位置
self.w, self.h = 60, 75 # 一张图片的高度和宽度
self.rect = self.ima.get_rect() # 得到图片的矩形选框
self.topleft = self.rect[0] + self.x, self.rect[1] + self.y # 计算左边顶点的位置,方便底下绘制
self.ima = pygame.transform.scale(self.ima, (self.w, self.h)) # 重设大小
self.is_cd = True
self.cd = time.time()
self.cd_time = self.data["cd"] # 这个是这个卡片的cd时间
if self.data["start"]:
self.cd -= self.cd_time
def CD(self):
"""进入CD的冷却动画,矩形填充"""
if time.time() - self.cd < self.cd_time:
# 计算冷却进度百分比
progress = (time.time() - self.cd) / self.cd_time
# 计算冷却矩形的高度
height = self.h - int(self.h * progress) # 如果没有self.h - 那么就会让矩形倒过来绘制,即从上往下
# 绘制冷却矩形
cooldown_surface = pygame.Surface((self.w, height))
cooldown_surface.set_colorkey((255, 255, 255)) # 设置颜色键
cooldown_surface.fill((0, 0, 0))
cooldown_surface.set_alpha(140) # 设置透明度
# 开始绘制矩形填充
self.window.blit(cooldown_surface, self.topleft)
else:
self.is_cd = False
def click(self):
"""鼠标点击的事件"""
mouse_x, mouse_y = pygame.mouse.get_pos()
if not self.is_cd and self.x < mouse_x < self.x + self.w and self.y < mouse_y < self.y + self.h:
if Other_data.sun - self.data["sun"] < 0:
return False
return True # 当鼠标处于图片这个区间内,返回真的内容,点击事件需要额外被调用
def get_plant(self):
"""成功放置植物,刷新CD"""
self.is_cd = True
self.cd = time.time()
def draw(self):
"""绘制功能实现"""
self.window.blit(self.ima, (self.x, self.y)) # 绘制卡片
if self.is_cd:
self.CD()
def run(self):
"""此角色类的运行逻辑"""
接下来,就是最后两步了,需要给种子银行添加数据,代码较多,时间也有点久了,所以这里我直接给上种子银行的全部代码,里面有很多清晰的注释:
import Card
import json
import Plant
import pygame
import Other_data
class SeedBank(pygame.sprite.Sprite):
def __init__(self, window, card_list: list, **kwargs):
"""
植物卡槽
:param window: 绘制的窗口
:param card_list: 卡片列表,传入全部卡片,实例化使用
:param kwargs: 用于Text对象的调用
"""
pygame.sprite.Sprite.__init__(self) # 初始格式化
self.window = window # 窗口
self.card_list = card_list # 植物的数量,决定种子银行的宽度,一张图片的大小是 60 * 80
self.num = len(self.card_list) # 得到卡片的数量决定卡槽大小
self.w = self.num * 60 + (self.num - 1) * 2 + self.num # 宽度等于卡牌的总长度加上间隙
self.card = [] # 植物卡槽里面的种子
self.draw_plant = None # 绘制出鼠标跟随的植物,即选中植物后的操作
self.choose_data = None # 选中的植物卡片内容,这应该会是一个类,Card类
self.sun_img = pygame.image.load("File/OtherPicture/SUN.png")
self.seed_bank = pygame.transform.scale( # 植物卡槽的图片信息
pygame.image.load("File/OtherPicture/seed_bank.png"), (self.w, 87))
self.sun_pos = (20, 0) # 获取阳光显示位置的坐标,用于阳光的数量显示
self.seed_pos = (85, 0) # 获取到种子银行的坐标
self.Sun = Text(self.window, str(Other_data.sun),
self.sun_pos[0] + self.sun_img.get_rect().size[0] / 2,
self.sun_pos[1] + self.sun_img.get_rect().bottom,
**kwargs)
self.load_card()
def load_card(self):
with open("File/FileData/Card.json", "r", encoding="utf-8") as f:
data = json.load(f)
for num, i in enumerate(self.card_list): # 遍历全部的编号数据内容
card = Card.card(self.window, data[i], num) # 实例化卡片内容
self.card.append(card) # 添加进实例化后的内容
def click(self, click_type='LEFT'):
"""判断鼠标是否有被点击"""
if click_type == "LEFT" and not self.draw_plant: # 点击左键,并且没有绘制植物的情况
for i in self.card:
if i.click():
self.draw_plant = pygame.image.load(i.data["image"]) # 加载图片
self.choose_data = i # 得到名字
break
elif click_type == "LEFT" and self.draw_plant and self.choose_data:
self.place_plant()
elif click_type == "RIGHT" and self.draw_plant:
self.draw_plant = None
self.choose_data = None
def place_plant(self):
"""放置植物"""
for num, data in enumerate(Other_data.pos):
mouse_x, mouse_y = pygame.mouse.get_pos()
x, x1, y, y1 = data
if x < mouse_x <= x1 and y < mouse_y <= y1:
for i in Plant.AllPlant: # 遍历全部的类,找名字,找一下到底加入什么植物
if i.__name__ == self.choose_data.data["EnglishName"] and Other_data.field[num] < 1\
and Other_data.sun - self.choose_data.data["sun"] >= 0:
# 如果名字是一样的,并且场地没有植物,阳光足够,那么就可以开始放置
plant_x, plant_y = (x + x1) / 2, (y + y1) / 2
Other_data.plants.add(i(self.window, plant_x, plant_y,
self.choose_data.data["image"], self.choose_data.data)) # 传入必要参数实例化精灵
Other_data.field[num] = 1
Other_data.change_sun(self.choose_data.data["sun"])
self.choose_data.get_plant() # 获得植物,进入冷却
self.change_sun()
self.draw_plant = None
self.choose_data = None
return True # 放置完毕,返回,并且不再判断
def change_sun(self):
"""更新阳光数据内容"""
self.Sun.change_src(str(Other_data.sun))
def draw(self):
self.window.blit(self.seed_bank, self.seed_pos) # 画出种子银行
self.window.blit(self.sun_img, self.sun_pos) # 画出阳光槽
self.Sun.draw()
for i in self.card:
i.draw()
if self.draw_plant: # 如果选择了植物,那么就绘制选中的植物
x, y = pygame.mouse.get_pos()
x -= self.draw_plant.get_width() / 2
y -= self.draw_plant.get_height() / 2
self.window.blit(self.draw_plant, (x, y))
核心函数:place_plant
至此,功能全部完成,最后就是更新一些内容了,这些全部放在main函数当中,我们需要更新植物动画的数据和绘制这些植物出来,这些就在main函数当中实现了
main.py
import pygame
import Setting
import Other_data
import Other_function
from pygame.locals import *
pygame.init()
window = pygame.display.set_mode((Setting.WIDTH, Setting.HEIGHT)) # 窗口
BG = pygame.image.load(Setting.BackGround[0]) # 先准备加载素材
Seed_Bank = Other_function.SeedBank(window, [str(i) for i in range(1, 7)]) # 植物银行,存放植物种子
# card = pygame.transform.scale(pygame.image.load("File/Card/SunFlower.png"), (60, 75))
mouse_dicts = {1: "LEFT", 2: "MIDDLE", 3: "RIGHT"} # 鼠标按下后对应按钮返回的数据
def set_up():
"""初始化数据加载,每一关开始就运行一次,对于一些数据的初始化"""
pass
def other_event():
"""一些其他事件,如点击了关闭按钮"""
for event in pygame.event.get():
if event.type == QUIT:
return True
elif event.type == pygame.MOUSEBUTTONDOWN:
"""鼠标点击下的事件"""
# print(event.pos) # 测试:用于检测点击位置计算方格
try:
Seed_Bank.click(mouse_dicts[event.button])
except KeyError: # 键不存在的话就直接到这
pass
def draw():
"""绘制函数,控制绘制内容的主逻辑"""
window.blit(BG, (-200, 0))
Seed_Bank.draw()
# 绘制一些精灵组内的角色
Other_data.plants.draw(window) # 当我点击的时候,精灵会放在屏幕当中
Other_data.zombies.draw(window)
Other_data.bullets.draw(window)
def run():
"""控制整个程序的主逻辑"""
clock = pygame.time.Clock() # 开启游戏时钟
pygame.display.set_caption("植物大战僵尸 - Pygame") # 设置窗口标题
while True:
clock.tick(40) # 设置游戏帧率
if other_event(): # 如点击了退出,那么程序结束
break
Other_data.plants.update() # 植物组当中植物更新
Other_data.zombies.update() # 僵尸组当中僵尸更新
draw() # 运行绘制函数
pygame.display.update() # 更新屏幕
if __name__ == '__main__':
run()
历时一周,第三部分结束,下次就可以开始编写植物的攻击内容了,先这样吧