python项目:从零写一个老式塞尔达小游戏(保姆级教程)_开发一个升级的小游戏
前言
前段时间学了一下 Python,看的一本入门书籍有一个关于 pygame 的小游戏,做完之后感觉很有意思,就在网上搜索了一下其他游戏相关的项目,当看到这个项目的时候感觉很不错,可惜原视频是英文的,就着边敲代码边写了一篇文章,或许同样对 pygame 游戏开发感兴趣的人又所帮助。
游戏的总体开发难度不高,比较简单,适合刚学习完 Python 语法的小伙伴或编程爱好者学习
项目简简简介
程序运行起来的效果
这是一款老式塞尔达风格RPG小游戏,包含了许多游戏所需的元素,如加载并绘制地图,处理人物移动、攻击,升级机制,武器切换,释放技能,背景音乐等功能,详细内容可以浏览目录
相关链接
原作者链接:https://youtu.be/QU1pPzEGrqw
原作者代码链接:https://github.com/clear-code-projects/Zelda
B站视频链接:【教程】用Python做一款2D老塞尔达式游戏(含Github文件)【生肉字幕】_哔哩哔哩_bilibili
我自己编写的代码链接:project: 一些练手的小项目 目前有C/C++实现的线程池、内存池 - Gitee.com
这篇文章并不会介绍 Python 基础,如果没有学习过 Python 的小伙伴可以尝试一下这本书,我上面提到的 pygame 小游戏就是这本书上的 “外星人入侵” 项目,可以看完这个项目回来再看这篇文章
获取《python编程:从入门到实践 》学习版地址:https://zhuanlan.zhihu.com/p/665135869
项目文件简介
audio、graphics、map 都是游戏的资源文件夹,分别包含游戏音效、游戏动画图片、游戏地图相关资源,code 是源码文件夹,所以首先需要根据原作者代码链接或我上传的代码链接下载代码获取相关资源文件才能编写程序,如果有基础的开发者想直接阅读源码的话推荐下载原作者代码,原作者的代码是分章节保存的,更方便阅读
下面正式开始开发,这也是我第一次写比较大的 Python 项目,如果有发现描述错误或不清楚的地方请指出,我看到后会及时修改
一:绘制一个简单的地图
1.创建文件夹
如果跟着文章写代码的话,下载代码后直接清空 code 文件夹的内容即可
2.安装 Pygame
pip install pygame #windowspip3 install pygame #linuxpip3.13 install pygame #如果Linux上有多个python版本,可以指定版本进行安装
3.创建一个 pygame 窗口并响应用户输入
创建⼀个表⽰游戏的类,以创建空的 Pygame 窗⼝。在 code 文件夹下创建一个 main.py 的文件,输入如下代码
main.py
import pygame, sysclass Game:def __init__(self):#初始化游戏并创建游戏资源pygame.init()#创建游戏窗口self.screen = pygame.display.set_mode((1280,720))#设置游戏窗口标题pygame.display.set_caption(\'Zelda\')#开始游戏的主循环def run(self):while True:#侦听键盘和⿏标事件for event in pygame.event.get():if event.type == pygame.QUIT:pygame.quit()sys.exit()# 让最近绘制的屏幕可⻅ 并将屏幕设置为黑色self.screen.fill(\'black\')#更新屏幕pygame.display.update()if __name__ == \'__main__\':#创建游戏实例并运⾏游戏game = Game()game.run()
⾸先,导⼊模块 sys 和 pygame。pygame 模块包含开发游戏所需的功能。当玩家退出时,将使⽤ sys 模块中的⼯具来退出游戏。
创建⼀个名为 Game 的类。在这个类的 __init__() ⽅法中,调⽤ pygame.init() 函数来初始化背 景,让 Pygame 能够正确地⼯作
pygame.display.set_mode() 创建⼀个显⽰游戏窗⼝,所有图形元素都将在其中绘制。实参 (1280,720)是⼀个元组,指定 了游戏窗⼝的尺⼨——宽 1280 像素、⾼ 720 像素。将这个显⽰窗⼝赋给属性 self.screen,让这个类的所有 ⽅法都能够使⽤它。self.screen 的对象是⼀个 surface。在 Pygame 中,surface 是 屏幕的⼀部分,⽤于显⽰游戏元素。
游戏由 run() ⽅法控制。该⽅法包含⼀个不断运⾏的 while 循环⽽这个循环包含⼀个事件循环以及管理屏幕更新的代码。 事件是⽤户玩游戏时执⾏的操作,如按键或移动⿏标。为了让程序能够响 应事件,可编写⼀个事件循环,以侦听事件并根据发⽣的事件类型执⾏适当的任务。嵌套在 while 循环中的 for 循环就是⼀个事件循环。
使⽤ pygame.event.get() 函数来访问 Pygame 检测到的事件。这 个函数返回⼀个列表,其中包含它在上⼀次调⽤后发⽣的所有事件。所有 键盘和⿏标事件都将导致这个 for 循环运⾏。在这个循环中,我们将编写 ⼀系列 if 语句来检测并响应特定的事件。例如,当玩家单击游戏窗⼝的关 闭按钮时,将检测到 pygame.QUIT 事件,进⽽调⽤ sys.exit() 来退出游戏
pygame.display.update(),命令 Pygame 让最近绘制的屏幕 可⻅。在移动游戏元素时, pygame.display.update() 将不断更新屏幕,以显⽰新位置上的元素并隐藏原来位置上的元素,从⽽营造平滑移动的效果
self.screen.fill(\'black\') ⽅法使用指定的背景⾊填充屏幕
创建⼀个游戏实例并调⽤ run()。这些代码被放 在⼀个 if 代码块中,仅当直接运⾏该⽂件时,它们才会执⾏。如果此时运 ⾏ main.py,将看到⼀个黑色的 Pygame 窗⼝。
Pygame 中,原点(0, 0)位于屏幕的左上⾓,向右为X轴,向下为Y轴
4.控制帧率
创建⼀个时钟(clock),并确保它在主循环每次通过后都进⾏计时(tick)。当这个循环的通过速度 超过我们定义的帧率时,Pygame 会计算需要暂停多⻓时间,以便游戏的运 ⾏速度保持⼀致。
在 __init__() ⽅法中定义这个时钟:
main.py
def __init__(self): ...#设置游戏窗口标题 pygame.display.set_caption(\'Zelda\')#创建游戏时钟 +self.clock = pygame.time.Clock() +
在 run() 的 while 循环末尾让这个时钟进⾏计时:
#开始游戏的主循环def run(self): ...#更新屏幕pygame.display.update()#控制游戏速度 +self.clock.tick(60) +
tick() ⽅法接受⼀个参数:游戏的帧率。这⾥使⽤的值为 60, Pygame 将尽可能确保这个循环每秒恰好运⾏ 60 次。
5.创建 settings.py 文件
每次给游戏添加新功能时,通常会引⼊⼀些新设置。下⾯来编写⼀个名为 settings 的模块,⽤于将所有设置都存储在⼀个地⽅,以免在代码中到处添加设置。 在 code 文件夹下创建 ⼀个名为 settings.py 的⽂件,添加如下代码 settings.py
#游戏设置WIDTH = 1280#窗口宽度HEIGTH = 720 #窗口高度FPS = 60 #帧率
根据这些设置修改 main.py文件 main.py
import pygame, sysfrom settings import * +class Game:def __init__(self):#初始化游戏并创建游戏资源pygame.init()#创建游戏窗口self.screen = pygame.display.set_mode((WIDTH,HEIGTH)) + ...#开始游戏的主循环def run(self): ...#控制游戏速度self.clock.tick(FPS) +...
⾸先导⼊ Settings 类,然后替换相应的游戏设置
6.添加障碍和人物图像
在 code 文件夹下添加 tile.py 和 player.py 文件,并添加如下代码
player.py
import pygame from settings import *class Player(pygame.sprite.Sprite):def __init__(self,pos,groups):super().__init__(groups)#加载图片,并确保图像支持透明度self.image = pygame.image.load(\'../graphics/test/player.png\').convert_alpha() #获取图像的矩形区域self.rect = self.image.get_rect(topleft = pos)
tile.py
import pygame from settings import *class Tile(pygame.sprite.Sprite):def __init__(self,pos,groups):super().__init__(groups)self.image = pygame.image.load(\'../graphics/test/rock.png\').convert_alpha()self.rect = self.image.get_rect(topleft = pos)
创建 Player 和 Tile 类,分别表示玩家角色和障碍
他们都继承自pygame.sprite.Sprite类,pygame.sprite.Sprite是 Pygame 中用于处理精灵(游戏对象)的基类,通过使⽤精灵(sprite),可将游戏中相关的元素编组,进⽽同时操作编组中的所有元素。
def __init__(self, pos, groups): init函数有两个参数,pos是一个元组,表示图像左上角的坐标,groups 表示精灵组,我们要将图像本身添加到这个精灵组中,方便后面绘制图像
super().__init__(groups):调用父类 pygame.sprite.Sprite 的初始化方法,并将 groups 参数传递给它。groups 参数用于指定这个精灵(图像本身)属于哪些精灵组,方便管理
pygame.image.load():是加载指定目录下的图片文件,convert_alpha():是确保加载的图像支持透明度,防止图片边缘部分遮挡背景
self.image.get_rect():获取一个与图像大小相同的矩形对象。这个矩形对象用于表示图像的位置和大小。topleft = pos:设置矩形对象的左上角位置为pos
7.添加临时的游戏地图
在 settings.py文件中添加一个临时的地图,用于验证是否可以正确绘制障碍和人物图像
settings.py
# game setupWIDTH = 1280HEIGTH = 720FPS = 60TILESIZE = 64 +WORLD_MAP = [[\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\'p\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\',\'x\',\'x\',\'x\',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\'x\',\'x\',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\'x\',\'x\',\'x\',\'x\',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\'x\',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\' \',\'x\'],[\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\',\'x\'],] +
WORLD_MAP 是一个二维数组,用于表示地图,其中 ‘x’ 表示障碍物, ‘p’ 表示游戏角色,‘ ’空格表示空地,一会儿用图像替换‘x’和 ‘p’从而绘制一个简单的地图
TILESIZE 表示一会绘制图像像素是 64*64 的, 例如,第一行一共要绘制20个障碍物,那么一共 64*20 = 1280 像素,刚好等于游戏屏幕的宽度WIDTH
8.创建关卡类,并绘制地图
在 code 文件夹下创建 level.py,并添加如下代码:
level.py
import pygame from settings import *from tile import Tilefrom player import Playerclass Level:def __init__(self):#获取当前显示窗口的表面对象 可以使用这个表面对象来绘制游戏内容self.display_surface = pygame.display.get_surface()#创建两个精灵组,一个是可见的精灵组,一个是障碍物精灵组self.visible_sprites = pygame.sprite.Group()self.obstacle_sprites = pygame.sprite.Group()#调用create_map方法创建地图self.create_map()def create_map(self):for row_index,row in enumerate(WORLD_MAP):for col_index, col in enumerate(row):x = col_index * TILESIZEy = row_index * TILESIZEif col == \'x\':Tile((x,y),[self.visible_sprites,self.obstacle_sprites])if col == \'p\':Player((x,y),[self.visible_sprites])def run(self):#调用draw方法绘制可见的精灵组self.visible_sprites.draw(self.display_surface)
pygame.display.get_surface(): 获取当前显示窗口的表面对象,后面绘制图像时要绘制到上面
self.visible_sprites 和 self.obstacle_sprites 是两个精灵组,分别用于存储对应的精灵方便绘制
create_map() 函数通过遍历WORLD_MAP,根据地图布局创建地图上的障碍物和玩家位置。它使用Tile类来表示障碍物,使用Player类来表示玩家位置,并将它们添加到相应的精灵组中,以便后续进行绘制
enumerate用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,row_index, row:外层循环遍历 WORLD_MAP 的每一行,row_index 是当前行的索引,row 是当前行的内容(一个列表)。col_index, col:内层循环遍历当前行的每一个元素,col_index 是当前元素的列索引,col 是当前元素的值(一个字符)。这样就可以根据WORLD_MAP 计算出每个精灵要绘制的左上角位置坐标,从而创建对应的精灵
self.visible_sprites.draw 将指定精灵组绘制到表面对象
最后,回到 main.py, 创建并绘制 Level 类
main.py
import pygame, sysfrom settings import *from level import Level +class Game:def __init__(self):#初始化游戏并创建游戏资源pygame.init()#创建游戏窗口self.screen = pygame.display.set_mode((WIDTH,HEIGTH))#设置游戏窗口标题pygame.display.set_caption(\'Zelda\')#创建游戏时钟self.clock = pygame.time.Clock()#创建游戏关卡 +self.level = Level() +#开始游戏的主循环def run(self):while True:#侦听键盘和⿏标事件for event in pygame.event.get():if event.type == pygame.QUIT:pygame.quit()sys.exit()# 让最近绘制的屏幕可⻅ 并将屏幕设置为黑色self.screen.fill(\'black\')#运行游戏关卡 +self.level.run() +#更新屏幕pygame.display.update()#控制游戏速度self.clock.tick(FPS)
运行程序得到如下效果:python3.13 main.py
二:移动游戏角色
1.更新角色坐标
移动角色,就是不断更改角色图像的位置坐标,并将图像绘制到屏幕上,根据上面讲过的坐标系,上、下、左、右移动分别对应 y 坐标减小、y 坐标增大、x 坐标减小和 x 坐标增大
在 Player 类的 __init__ 函数添加如下代码
player.py
def __init__(self,pos,groups):...#获取图像的矩形区域self.rect = self.image.get_rect(topleft = pos)#创建一个向量 +self.direction = pygame.math.Vector2() +#设置移动速度 +self.speed = 5 +
pygame.math.Vector2(): 创建一个零向量,默认为:(x=0, y=0),后面会根据x 和 y 的正负判断角色移动的方向
self.speed 表示角色移动速度,每帧移动 5 个像素
在 Player 类中添加 input 函数,用于接收用户输入的按键,并修改向量的值,从而确定角色移动方向
player.py
class Player(pygame.sprite.Sprite):def __init__(self,pos,groups):...def input(self):keys = pygame.key.get_pressed()if keys[pygame.K_UP] or keys[pygame.K_w]:self.direction.y = -1elif keys[pygame.K_DOWN] or keys[pygame.K_s]:self.direction.y = 1else:self.direction.y = 0if keys[pygame.K_RIGHT] or keys[pygame.K_d]:self.direction.x = 1elif keys[pygame.K_LEFT] or keys[pygame.K_a]:self.direction.x = -1else:self.direction.x = 0
pygame.key.get_pressed():这个函数返回一个包含所有键盘按键状态的列表。如果某个按键被按下,那么对应位置的值为True,否则为False
在条件判断的最后将向量设置为0,表示在该方向上停止移动。
在 Player 类中添加 move 函数,用于根据角色移动方向修改角色坐标
player.py
class Player(pygame.sprite.Sprite):def __init__(self,pos,groups):...def input(self):...def move(self,speed):self.rect.x += self.direction.x * speedself.rect.y += self.direction.y * speed
2.绘制角色
在 Player 类中添加 update 函数
player.py
class Player(pygame.sprite.Sprite):...def update(self):self.input()self.move(self.speed)
在 Level 中 run 函数中添加如下代码
level.py
class Level:...def run(self):#调用draw方法绘制可见的精灵组self.visible_sprites.draw(self.display_surface)self.visible_sprites.update() +
visible_sprites 精灵组的 update() 函数会调用这个精灵组中所有精灵的 update() 函数,所以这里会自动调用Player 类中 update 函数,从而接收用户输入已经更新角色坐标,更新后self.visible_sprites.draw()方法会将更新后的内容绘制到屏幕上
现在运行 main.py 角色就可以移动了
3.碰撞检测
虽然可以移动了,但目前角色会穿过障碍,下面来解决这个问题
首先在 Level 类中创建 Player 类时将 obstacle_sprites 障碍精灵组传递到 Player 类中,并保存到 Player 类中,方便后面检测角色与障碍的碰撞,注意这里使用 self.player 保存了角色精灵
level.py
def create_map(self):...if col == \'x\':Tile((x,y),[self.visible_sprites,self.obstacle_sprites])if col == \'p\':self.player = Player((x,y),[self.visible_sprites],self.obstacle_sprites) +
player.py
def __init__(self, pos, groups, obstacle_sprites):...#创建一个零向量,默认为:(x=0, y=0)self.direction = pygame.math.Vector2()#设置移动速度self.speed = 5#保存障碍物精灵组 +self.obstacle_sprites = obstacle_sprites +
在 Player 类中添加碰撞检测函数
player.py
class Player(pygame.sprite.Sprite):...def collision(self,direction):if direction == \'horizontal\':for sprite in self.obstacle_sprites:if sprite.rect.colliderect(self.rect):if self.direction.x > 0: self.rect.right = sprite.rect.leftif self.direction.x 0: self.rect.bottom = sprite.rect.topif self.direction.y < 0: self.rect.top = sprite.rect.bottom
以 horizontal 水平方向为例,首先遍历障碍精灵组,通过 colliderect 函数判断是否有障碍精灵与角色发生碰撞,如果 x > 0,说明角色当前向右移动,那么角色一定碰到的是障碍的左边界,将角色的右边界设置为障碍物的左边界,以防止角色穿过障碍物。
x< 0 表示角色正在向左移动 ,则将角色的左边界设置为障碍物的右边界。
最后,在 move 函数中添加 collision 函数
player.py
def move(self,speed):self.rect.x += self.direction.x * speedself.collision(\'horizontal\') +self.rect.y += self.direction.y * speedself.collision(\'vertical\') +
现在运行 main.py 角色就不会穿过障碍了
4.归一化处理
当让角色斜着移动时,角色的移动速度要比在单一方向的移动速度快,例如向右上方移动一帧,x 方向移动了 5 像素,y 方向移动了 5 像素,实际相当于: (1, -1) → 实际速度 = 5 * 1.414 ≈ 7.075 移动了7.075像素
为了解决这个问题,需要用到归一化处理,让向量的长度变成 1,方向不变。也就是确保斜向移动速度与单方向一致,避免速度异常。归一化后(0.707, -0.707) → 实际速度 = 5 * 1= 5(和单方向一致)。
pygame 中已经对归一化做好了封装,在 Player 类的 move 函数这中添加如下代码:
player.py
def move(self,speed):if self.direction.magnitude() != 0: +self.direction = self.direction.normalize() +self.rect.x += self.direction.x * speedself.collision(\'horizontal\')self.rect.y += self.direction.y * speedself.collision(\'vertical\')
self.direction.magnitude() 检查 self.direction 向量的模是否不为 0(也就是向量不为(0, 0)时,说明有方向键按下),这时计算向量归一化才有意义
self.direction.normalize() 就是向量归一化
现在运行 main.py 就可以正常移动了
三:基于玩家的视角移动并修改碰撞体积
实现思路:不管玩家当前在哪个位置,都把它移动到屏幕的中心,玩家位置坐标和屏幕中心坐标都是已知的,这样就可以计算出玩家到屏幕中心的向量(方向和距离),然后根据计算出的向量移动每一个障碍物,并绘制到屏幕上,就实现了基于玩家的视角移动的效果
1.实现自己的精灵组
为了方便绘制基于玩家精灵为中心的地图,我们需要实现自己的精灵组,在 Level 类的下面写一个新的类:
level.py
class Level:...class YSortCameraGroup(pygame.sprite.Group):def __init__(self):super().__init__()self.display_surface = pygame.display.get_surface()self.half_width = self.display_surface.get_size()[0] // 2self.half_height = self.display_surface.get_size()[1] // 2self.offset = pygame.math.Vector2()def custom_draw(self,player):# 计算玩家与屏幕中心的偏移量self.offset.x = player.rect.centerx - self.half_widthself.offset.y = player.rect.centery - self.half_heightfor sprite in self.sprites():#计算障碍精灵坐标offset_pos = sprite.rect.topleft - self.offsetself.display_surface.blit(sprite.image,offset_pos)
YSortCameraGroup 类继承自 pygame.sprite.Group,这样可以方便我们编写自己的绘制函数。后面会将所有的可见精灵 visible_sprites 存储到 YSortCameraGroup 类中并绘制
__init__ 函数很简单,获取了屏幕一半的宽和屏幕一半的高,也就是屏幕中心点的坐标,用于在下面计算偏移量,self.offset 用于保存偏移量
重点是 custom_draw 函数,参数 player 表示传入玩家角色精灵。
前面两行代码计算角色精灵与屏幕中心的偏移量,并保存到 self.offset 中,假设玩家角色精灵为橙色的框框,中心坐标为 (160, 160),那么可以根据代码计算出和屏幕中心的偏移量为(-480, -200),也就是红色的线。
遍历所有精灵,然后根据 self.offset 计算要绘制所有精灵坐标,假设一个障碍精灵左上角坐标之前为(0, 0),也就是绿色的框框,通过偏移计算 (0 - (-480), 0 - (-200))得到的新坐标为(480, 200),根据这个坐标绘制障碍精灵,大致位置就是虚线绿色的框框,对于角色精灵中心坐标来说,(160 - (-480), 160 - (-200))得到坐标刚好就是屏幕中心坐标 (640, 360),根据偏移后的坐标绘制得到的地图,就可以基于玩家的视角移动了
2.使用自己的精灵组
修改 Level 类的 __init__ 方法,使用 YSortCameraGroup 来创建可见精灵组,这样可以方便调用自己实现的绘制方法
level.py
def __init__(self):#获取当前显示窗口的表面对象 可以使用这个表面对象来绘制游戏内容self.display_surface = pygame.display.get_surface()#创建两个精灵组,一个是可见的精灵组,一个是障碍物精灵组self.visible_sprites = YSortCameraGroup() +self.obstacle_sprites = pygame.sprite.Group()#调用create_map方法创建地图self.create_map()
修改 Level 类的 run 方法,调用 custom_draw 方法绘制地图
level.py
def run(self):#调用draw方法绘制可见的精灵组self.visible_sprites.custom_draw(self.player) +self.visible_sprites.update()
3.修改角色和障碍物碰撞体积
现在效果是角色碰到障碍物时让角色贴在障碍物上,这样的效果有些生硬,下面设置一下 player 的碰撞体积,让人物效果更自然一些,目标效果:
player.py
class Player(pygame.sprite.Sprite):def __init__(self, pos, groups, obstacle_sprites):super().__init__(groups)#加载图片,并确保图像支持透明度self.image = pygame.image.load(\'../graphics/test/player.png\').convert_alpha() #获取图像的矩形区域self.rect = self.image.get_rect(topleft = pos)#创建一个矩形,比原始矩形小26像素,以便在碰撞检测中排除头部 +self.hitbox = self.rect.inflate(0, -26) +#创建一个零向量,默认为:(x=0, y=0)self.direction = pygame.math.Vector2() ...
self.rect.inflate(0, -26):设置碰撞体积,将 self.rect
矩形在垂直方向上缩小了26个像素(矩形的顶部和底部都会向中心移动,总共减少26个像素的高度),并保存到 self.hitbox 中
self.hitbox 保存碰撞检测区域,用于实现精灵在视觉上覆盖另一个精灵的效果
同样设置一下障碍物的碰撞体积,修改 Tile 类的 init 方法
tile.py
import pygame from settings import *class Tile(pygame.sprite.Sprite):def __init__(self,pos,groups):super().__init__(groups)self.image = pygame.image.load(\'../graphics/test/rock.png\').convert_alpha()self.rect = self.image.get_rect(topleft = pos)self.hitbox = self.rect.inflate(0,-10) +
修改移动和碰撞检测函数,使用新的碰撞体积来检测碰撞:
player.py
... def move(self,speed):#码检查 self.direction 向量的模是否不为 0(也就是向量不为(0, 0)时,说明有方向键按下)if self.direction.magnitude() != 0:self.direction = self.direction.normalize()self.hitbox.x += self.direction.x * speed +self.collision(\'horizontal\')self.hitbox.y += self.direction.y * speed +self.collision(\'vertical\') # 更新 self.rect 的位置,使其与 self.hitbox 的位置相同 +self.rect.center = self.hitbox.center +def collision(self,direction):if direction == \'horizontal\':for sprite in self.obstacle_sprites:if sprite.rect.colliderect(self.hitbox): +if self.direction.x > 0: self.hitbox.right = sprite.rect.left +if self.direction.x 0: self.hitbox.bottom = sprite.rect.top +if self.direction.y < 0: self.hitbox.top = sprite.rect.bottom +
现在就有覆盖效果了,但有时会出现下面这种情况,障碍物的下边缘会覆盖角色的上边缘:
这是因为在pygame中,后绘制的图片会覆盖先绘制的图片,想要解决这个问题,要保证按照Y轴的顺序依次绘制图片
level.py
class YSortCameraGroup(pygame.sprite.Group): ...def custom_draw(self,player):# 计算玩家与屏幕中心的偏移量self.offset.x = player.rect.centerx - self.half_widthself.offset.y = player.rect.centery - self.half_heightfor sprite in sorted(self.sprites(),key = lambda sprite: sprite.rect.centery): +#计算障碍精灵坐标offset_pos = sprite.rect.topleft - self.offsetself.display_surface.blit(sprite.image,offset_pos)
四:绘制正式地图
1.绘制地图背景
修改 YSortCameraGroup 类中的 __init__ 方法,在该方法中加载背景图片并设置背景图片的坐标
level.py
class YSortCameraGroup(pygame.sprite.Group):def __init__(self):super().__init__()self.display_surface = pygame.display.get_surface()self.half_width = self.display_surface.get_size()[0] // 2self.half_height = self.display_surface.get_size()[1] // 2self.offset = pygame.math.Vector2()#加载背景图像 +self.floor_surf = pygame.image.load(\'../graphics/tilemap/ground.png\').convert() +#指定背景矩形对象左上角的位置 +self.floor_rect = self.floor_surf.get_rect(topleft = (0,0)) + ...
然后就可以修改 custom_draw 方法绘制背景图片了,为了保持基于玩家视角移动,绘制背景图片时也需要按照之前计算偏移量进行偏移。
背景图需要在绘制其他精灵前绘制,防止被背景图片覆盖
level.py
def custom_draw(self,player):# 计算玩家与屏幕中心的偏移量self.offset.x = player.rect.centerx - self.half_widthself.offset.y = player.rect.centery - self.half_heightfloor_offset_pos = self.floor_rect.topleft - self.offset +self.display_surface.blit(self.floor_surf,floor_offset_pos) +for sprite in sorted(self.sprites(),key = lambda sprite: sprite.rect.centery): ...
2.读取地图信息和图片信息
要读取的地图数据放在 map 文件的 .csv 文件中,和之前在 settings.py 中的 WORLD_MAP 变量差不多,只是复杂一些。每个 .csv 文件都是 50 行 57 列的矩阵,读取时需要将矩阵读取到二维列表中,每一个二维列表的元素代表 64X64 像素的大小,50X64,57X64 = 3200X3648 ,刚好是上面绘制的地图背景图片的大小。
这是其中的 map_FloorBlocks.csv 文件:
其中 -1 表示空白,395 表示不可见精灵,也就是地图边界,用来限制玩家移动。还要用到 map_Grass.csv 文件,里面的 8、9、10 表示绘制小草的位置,map_Objects.csv 文件中 0 ~ 20 的数字表示 graphics/objects 文件中对应的图片所绘制的位置
在 code 文件夹下新建 support.py 文件,在这个文件中编写读取 CSV 文件方法
support.py
from csv import readerdef import_csv_layout(path):terrain_map = []with open(path) as level_map:layout = reader(level_map,delimiter = \',\')for row in layout:terrain_map.append(list(row))return terrain_map
import_csv_layout
函数从一个 CSV 文件中读取地图布局数据,并将这些数据转换为一个二维列表,参数 path 表示要读取的 CSV 文件的路径
reader(level_map, delimiter=\',\')
创建一个 CSV 读取器对象,指定逗号为分隔符。
for 循环将每一行数据(一个字符串列表),并添加到 terrain_map
中,最后返回二维列表
然后编再写一个读取图片的方法
support.py
from csv import readerfrom os import walk +import pygame +def import_csv_layout(path): ...def import_folder(path):surface_list = []for _,__,img_files in walk(path): sorted_img_files = sorted(img_files)for image in img_files:full_path = path + \'/\' + imageimage_surf = pygame.image.load(full_path).convert_alpha()surface_list.append(image_surf)return surface_list
import_folder方法
从指定文件夹中导入所有图像文件,参数 path 为读取文件夹的路径
os.walk
返回一个三元组(dirpath, dirnames, filenames)
,其中dirpath
是目录路径,dirnames
是目录名列表,filenames
是文件名列表。这里我们只关心文件名列表img_files
之后对文件名列表进行排序,这行代码是我自己添加的,因为 ../graphics/objects 路径下的图片名称需要在列表中的位置对应,但在 linux 环境中读取的顺序是乱的,所以需要排序(试了一下window 环境不需要排序也可以)
之后 for 循环通过
文件名列表拼接好路径,读取对应的图片就可以了
读取方法编写好后,就可以传入对应的路径读取对应的内容了
level.py
...from player import Playerfrom support import * +class Level:def __init__(self): ...def create_map(self): layouts = {\'boundary\': import_csv_layout(\'../map/map_FloorBlocks.csv\'),\'grass\': import_csv_layout(\'../map/map_Grass.csv\'),\'object\': import_csv_layout(\'../map/map_Objects.csv\'),} +graphics = {\'grass\': import_folder(\'../graphics/grass\'),\'objects\': import_folder(\'../graphics/objects\')} + for row_index,row in enumerate(WORLD_MAP): ...
layouts 字典中,boundary 存储了地图边界的位置,grass 存储了小草的位置,object 存储了障碍物的位置
graphics 字典中,grass 存储了小草的图片,小草的图片有三种。objects 存储了障碍物的图片,其中障碍物的图片名字是按数字编号的,编号对应着在 layouts[\"object \"] 二维中绘制的位置,所以文件夹的排序要按照名称排序,这样读取后 graphics[\"objects\"] 的列表下标正好对应 该图片
3.绘制地图信息和图片信息
修改 create_map 函数,重新设置精灵组的内容
level.py
...from player import Playerfrom support import *from random import choice +class Level:def __init__(self): ... def create_map(self):layouts = { ...}graphics = { ...}for style,layout in layouts.items():for row_index,row in enumerate(layout):for col_index, col in enumerate(row):if col != \'-1\':x = col_index * TILESIZEy = row_index * TILESIZEif style == \'boundary\':Tile((x,y),[self.obstacle_sprites],\'invisible\')if style == \'grass\':random_grass_image = choice(graphics[\'grass\'])Tile((x,y),[self.visible_sprites,self.obstacle_sprites],\'grass\',random_grass_image)if style == \'object\':surf = graphics[\'objects\'][int(col)]Tile((x,y),[self.visible_sprites,self.obstacle_sprites],\'object\',surf)self.player = Player((2000,1430),[self.visible_sprites],self.obstacle_sprites)
外层循环:遍历layouts
字典的项。每个项是一个键值对,其中键(style
)是布局的名称(boundary
、grass
、object
),值(layout
)是一个二维列表
中层循环:遍历二维列表中的每一行。并获取每行的索引(row_index
)和行内容(row
)。
内层循环:遍历每一行的内容,也就是每个单元格的内容,上面提到的 64X64 像素格,获取每个单元格的索引(col_index
)和单元格内容(col
)
内层逻辑:如果 col != \'-1\',说明在该位置上需要绘制图片,x、y坐标的偏移计算和之前相同。
style == \'boundary\': 表示地图边界,因为看不到并且为障碍物,所以将边界添加到障碍物精灵组中,第三个参数 invisible 表示该精灵类型为不可见,后面会详细说明
style == \'grass\' 表示绘制小草的单元格,choice方法会随机一张小草的图片,小草是障碍物并且可见,所以添分别添加到两个精灵组中,第三个参数表示精灵类型为小草,第四个参数为要绘制的小草图片
style == \'object\' 要表示绘制其他可见障碍物,graphics[\'objects\'][int(col)] 表示取出列表下标对应的图片,然后添加到两个精灵组中,并传递精灵类型和要绘制的图片
最后设置玩家图像,和之前一样,只是手动设置了起始坐标
最后,修改一下 Tile 类的 __init__ 函数
tile.py
import pygame from settings import *class Tile(pygame.sprite.Sprite):def __init__(self,pos,groups,sprite_type,surface = pygame.Surface((TILESIZE,TILESIZE))):super().__init__(groups)self.sprite_type = sprite_typeself.image = surfaceif sprite_type == \'object\':self.rect = self.image.get_rect(topleft = (pos[0],pos[1] - TILESIZE))else:self.rect = self.image.get_rect(topleft = pos)self.hitbox = self.rect.inflate(0,-10)
添加了两个参数,sprite_type 表示该障碍物的类型,surface 表示要绘制的图片,是一个默认参数,如果是绘制地图边界,就绘制一个 64X64 的空气
‘object’ 类型的障碍物的高度都是128像素的,绘制 ‘object’ 类型图片时要将左上角 y 轴坐标上移 64 像素再绘制(所以为什么当时不把 map_Objects.csv 文件中对应的数字上移一行呢?)
其他类型的障碍物正常绘制就可以
五:绘制角色动画效果
1.绘制移动动画
首先在 Player 类中添加新函数,将角色移动时用到的图片全都加载到字典中
player.py
import pygame from settings import *from support import import_folder +class Player(pygame.sprite.Sprite):def __init__(self,pos,groups,obstacle_sprites): ...def import_player_assets(self):character_path = \'../graphics/player/\'self.animations = {\'up\': [],\'down\': [],\'left\': [],\'right\': [],\'right_idle\':[],\'left_idle\':[],\'up_idle\':[],\'down_idle\':[]}for animation in self.animations.keys():full_path = character_path + animationself.animations[animation] = import_folder(full_path)
self.animations 的 key值 \'up\', \'down\', \'left\', \'right\' 分别表示角色在不同方向移动的状态,\'right_idle\', \'left_idle\' ,\'up_idle\' ,\'down_idle\' 分别表示角色在不同方向静止的状态,同时字典的 key 值也是存放角色移动图片最内层文件夹的名称,所以通过路径拼接文件夹名称将其中的图片存放到对应 key 值的列表中
修改 Player 类的 init 方法,初始化角色移动的状态信息,调用刚刚封装的方法加载图片,设置角色的初始状态,设置角色当前帧的索引为0,动画速度为0.15(后面会说明这两个值的作用)
player.py
def __init__(self, pos, groups, obstacle_sprites): ...self.hitbox = self.rect.inflate(0, -26)#加载动画图片 +self.import_player_assets() + #设置初始状态 +self.status = \'down\' +#初始化帧索引 +self.frame_index = 0 +#设置动画速度 +self.animation_speed = 0.15 +#创建一个零向量,默认为:(x=0, y=0)self.direction = pygame.math.Vector2() ...
修改 input 函数,按下对应的方向键时修改角色状态
player.py
def input(self):keys = pygame.key.get_pressed()if keys[pygame.K_UP] or keys[pygame.K_w]:self.direction.y = -1self.status = \'up\' +elif keys[pygame.K_DOWN] or keys[pygame.K_s]:self.direction.y = 1self.status = \'down\' +else:self.direction.y = 0if keys[pygame.K_RIGHT] or keys[pygame.K_d]:self.direction.x = 1self.status = \'right\' +elif keys[pygame.K_LEFT] or keys[pygame.K_a]:self.direction.x = -1self.status = \'left\' +else:self.direction.x = 0
在 Player 类中添加新函数,用于处理动画效果,并在 update 函数中调用
player.py
def animate(self):animation = self.animations[self.status]#设置帧索引,以便循环播放动画self.frame_index += self.animation_speedif self.frame_index >= len(animation):self.frame_index = 0self.image = animation[int(self.frame_index)]self.rect = self.image.get_rect(center = self.hitbox.center)def update(self):self.input()self.animate() +self.move(self.speed)
首先取出当前角色移动状态对应的图片列表
通过逐步增加 self.frame_index
来播放动画的每一帧,并在到达动画末尾时重置索引以开始新的播放循环。self.animation_speed
参数用于调整动画的播放速度
假设状态为 right, right方向的图片列表一共有4张图片,每秒60帧,动画速度为 0.15 ,也就是说每张图片绘制的帧数大约为:1/0.15 = 6.6 帧,每秒可以完成的完整播放循环次数大约为:60 / (6.6 * 4)= 2.27次,即大约每0.44秒完成一次播放循环。
根据当前计算出的索引取出对应图片并,并在update中将图片绘制出来
现在角色已经可以移动了,但有个小问题,当角色停止移动时还在继续绘制移动的图片,看起来像是在“原地踏步”,所以要添加一个函数更新当前角色的状态,当角色停止移动时设置为静止状态
player.py
def get_status(self):if self.direction.x == 0 and self.direction.y == 0:if not \'idle\' in self.status:self.status = self.status + \'_idle\'def update(self):self.input()self.get_status()self.animate() +self.move(self.speed)
get_status 会检查当前角色是否停止移动(x == 0, y==0),如果角色已经停止移动,并且为移动状态时,就将当前的状态字符串拼接上 \'_idle\' 字符串,使角色状态在该方向上改为静止状态
2.绘制攻击动画
修改 import_player_assets 函数,加载攻击时的图片,分别表示不同方向上的攻击状态和图片列表
player.py
def import_player_assets(self):character_path = \'../graphics/player/\'self.animations = {\'up\': [],\'down\': [],\'left\': [],\'right\': [],\'right_idle\':[],\'left_idle\':[],\'up_idle\':[],\'down_idle\':[], +\'right_attack\':[],\'left_attack\':[],\'up_attack\':[],\'down_attack\':[]} +for animation in self.animations.keys():full_path = character_path + animationself.animations[animation] = import_folder(full_path)
修改 init 函数添加攻击的状态信息
player.py
class Player(pygame.sprite.Sprite):def __init__(self, pos, groups, obstacle_sprites): ...self.animation_speed = 0.15 #当前是否处于攻击状态 +self.attacking = False +#设置攻击冷却时间,即400毫秒 +self.attack_cooldown = 400 +#设置攻击时间 +self.attack_time = None +#创建一个零向量,默认为:(x=0, y=0)self.direction = pygame.math.Vector2() ...
修改 input 函数,监听攻击按键,设置攻击状态并且记录攻击时间
player.py
def input(self):if not self.attacking:keys = pygame.key.get_pressed()if keys[pygame.K_UP] or keys[pygame.K_w]: ...else:self.direction.x = 0 # 普攻if keys[pygame.K_SPACE] or keys[pygame.K_j]:self.attacking = Trueself.attack_time = pygame.time.get_ticks()# 魔法攻击if keys[pygame.K_LCTRL] or keys[pygame.K_k]:self.attacking = Trueself.attack_time = pygame.time.get_ticks()
添加了判断条件,只有角色当前处于非攻击状态时才可以移动并且再次发动攻击
在 Player 类中添加新方法,用于判断攻击冷却时间
player.py
def cooldowns(self):current_time = pygame.time.get_ticks()if self.attacking:if current_time - self.attack_time >= self.attack_cooldown:self.attacking = False def update(self):self.input()self.cooldowns() +self.get_status()self.animate()self.move(self.speed)
获取当前时间,并通过 update 方法循环检查冷却时间是否就绪,如果未达到冷却时间就将攻击状态持续设置为 false
最后,修改 get_status 函数检查当前角色处于何种状态,并将 self.status 设置为对应的状态
player.py
def get_status(self):if self.direction.x == 0 and self.direction.y == 0:if not \'idle\' in self.status and not \'attack\' in self.status:self.status = self.status + \'_idle\'if self.attacking:self.direction.x = 0self.direction.y = 0if not \'attack\' in self.status:if \'idle\' in self.status:self.status = self.status.replace(\'_idle\',\'_attack\')else:self.status = self.status + \'_attack\'else:if \'attack\' in self.status:self.status = self.status.replace(\'_attack\',\'\')
修改之前的条件判断,如果当前角色停止移动,并且角色不为禁止状态和攻击状态,就将角色状态设置为静止
如果当前玩家正在攻击,那么强制角色停止移动,先判断角色是否不为攻击状态,如果不为攻击状态则将当前状态改为攻击状态
如果当前玩家停止攻击,那么判断角色当前是否为攻击状态,如果为攻击状态则将当前状态改为非攻击状态
animate 函数会根据当前的状态绘制对应的图片
六:绘制武器
1.加载武器图片信息
在 settings.py 中添加不同武器的信息
settings.py
#游戏设置WIDTH = 1280#窗口宽度HEIGTH = 720 #窗口高度FPS = 60 #帧率TILESIZE = 64weapon_data = { +\'sword\': {\'cooldown\': 100, \'damage\': 15,\'graphic\':\'../graphics/weapons/sword/full.png\'},\'lance\': {\'cooldown\': 400, \'damage\': 30,\'graphic\':\'../graphics/weapons/lance/full.png\'},\'axe\': {\'cooldown\': 300, \'damage\': 20, \'graphic\':\'../graphics/weapons/axe/full.png\'},\'rapier\':{\'cooldown\': 50, \'damage\': 8, \'graphic\':\'../graphics/weapons/rapier/full.png\'},\'sai\':{\'cooldown\': 80, \'damage\': 10, \'graphic\':\'../graphics/weapons/sai/full.png\'}}
第一列的 key 值表示不同武器的名称,也是存储对应武器文件夹的名称,武器的对应方向的图片和图片名称相同。cooldown 表示该武器的冷却时间,damage 表示该武器伤害,graphic 表示对应武器完整图片的路径
在 code 文件夹下新建 weapon.py 文件,创建 weapon 类,用于设置当前使用武器的图片位置
weapon.py
import pygame class Weapon(pygame.sprite.Sprite):def __init__(self,player,groups):super().__init__(groups)direction = player.status.split(\'_\')[0]# 加载武器图片full_path = f\'../graphics/weapons/{player.weapon}/{direction}.png\'self.image = pygame.image.load(full_path).convert_alpha()# 获取武器的矩形区域if direction == \'right\':self.rect = self.image.get_rect(midleft = player.rect.midright + pygame.math.Vector2(0,16))elif direction == \'left\': self.rect = self.image.get_rect(midright = player.rect.midleft + pygame.math.Vector2(0,16))elif direction == \'down\':self.rect = self.image.get_rect(midtop = player.rect.midbottom + pygame.math.Vector2(-10,0))else:self.rect = self.image.get_rect(midbottom = player.rect.midtop + pygame.math.Vector2(-10,0))
Weapon 类继承自 pygame.sprite.Sprite,武器图片也会按照精灵的方式管理
init 函数有两个参数,player 表示玩家类,groups 是可见精灵组
direction 表示从玩家类中获取当前角色的方向
player.weapon 表示当前角色所使用的武器(后面会添加武器切换),这样就可以根据武器和方向拼接对应武器图片的完整路径 full_path 了,之后根据 full_path 加载对应图片
最后的 if else 语句是为了设置武器图片的坐标,以向右为例,那么武器图片的左边缘坐标肯定要贴在角色图片的右边缘,player.rect.midright
返回的是玩家矩形右侧中点的位置pygame.math.Vector2(0,16)
向量被加到这个位置上,意味着武器的位置将在玩家右侧中点的垂直方向上偏移 16 个像素,这是为了武器图片可以和人物的手手对齐,向上和向下攻击都是水平向右偏移10个像素,是因为向上攻击出的是左手,向下攻击出的是右手。。。
2.武器切换
在 Player 类中的 init 函数中添加武器的相关属性变量
player.py
class Player(pygame.sprite.Sprite):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack): + ...#保存障碍物精灵组self.obstacle_sprites = obstacle_sprites #创建武器精灵 +self.create_attack = create_attack +#销毁武器精灵 +self.destroy_attack = destroy_attack +#当前使用的武器索引值 +self.weapon_index = 0 +#当前使用的武器 +self.weapon = list(weapon_data.keys())[self.weapon_index] +#是否可以切换武器 +self.can_switch_weapon = True +#保存上一次武器切换的时间 +self.weapon_switch_time = None +#武器切换冷却时间 +self.switch_duration_cooldown = 200 +
create_attack 和 destroy_attack 是两个回调函数,就是调用 init 函数时传递的实参是对应的函数地址(函数名),之后可以根据保存的函数地址来调用对应的函数
self.weapon = list(weapon_data.keys())[self.weapon_index] 是根据weapon_data的key值创建了一个列表,并根据 weapon_index 来返回对应武器名称,切换武器时会修改 weapon_index 的值,从而修改 weapon 的值
修改 Player 类中的 input 函数,添加切换武器功能
player.py
def input(self): ...# 普攻if keys[pygame.K_SPACE] or keys[pygame.K_j]:self.attacking = Trueself.attack_time = pygame.time.get_ticks()self.create_attack() +# 魔法攻击if keys[pygame.K_LCTRL] or keys[pygame.K_k]:self.attacking = Trueself.attack_time = pygame.time.get_ticks()# 武器切换 +if (keys[pygame.K_q] or keys[pygame.K_h]) and self.can_switch_weapon:self.can_switch_weapon = Falseself.weapon_switch_time = pygame.time.get_ticks()if self.weapon_index < len(list(weapon_data.keys())) - 1:self.weapon_index += 1else:self.weapon_index = 0self.weapon = list(weapon_data.keys())[self.weapon_index]
如果按下切换武器的按键并且当前可以切换武器,记录切换武器的时间、更新武器的列表的索引值,并更新当前使用的武器
按下普攻键时,调用 init 函数中保存的创建武器精灵函数
修改 cooldowns 函数,在攻击冷却时间结束后销毁武器精灵,并检查切换武器的冷却时间
def cooldowns(self):current_time = pygame.time.get_ticks()if self.attacking:if current_time - self.attack_time >= self.attack_cooldown:self.attacking = Falseself.destroy_attack() +if not self.can_switch_weapon: +if current_time - self.weapon_switch_time >= self.switch_duration_cooldown: +self.can_switch_weapon = True+
3.创建和销毁武器精灵
修改 Level 类的 init 函数,添加武器精灵
level.py
...from random import choicefrom weapon import Weapon +class Level:def __init__(self): ...self.obstacle_sprites = pygame.sprite.Group()#武器精灵 +self.current_attack = None +#调用create_map方法创建地图self.create_map()
在 Level 类中添加创建和销毁武器精灵的函数
level.py
def create_attack(self):self.current_attack = Weapon(self.player,[self.visible_sprites])def destroy_attack(self):if self.current_attack:self.current_attack.kill()self.current_attack = None
create_attack:使用当前角色所持有的武器类型创建武器精灵,并将其添加到可见精灵组中,用于后面绘制
destroy_attack 中的 kill 方法是 Weapon 继承自 pygame.sprite.Sprite 的方法,kill 会将武器精灵从可见精灵组中移除。最后 destroy_attack 方法会将武器精灵从新置空
最后修改 create_map 函数,修改 Player 类的构造方法,将上诉的两个方法传递进去
level.py
def create_map(self): ...self.player = Player((2000,1430),[self.visible_sprites],self.obstacle_sprites,self.create_attack,self.destroy_attack) +
现在就可以绘制对应的武器了,之所以把武 current_attack 变量创建在 Level 类中,搞得创建和销毁武器精灵变得有些复杂,是因为后面在 Level 类中使用它
七:绘制部分UI
这小结内容会在屏幕绘制部分UI,左上角绘制血条和蓝条,左下角绘制物品栏,显示当前使用的武器,切换武器时修改物品栏边框的颜色,右下角绘制当前经验值
每个UI都先绘制背景色,然后在背景色上绘制对应的图片,背景色周围还会绘制一圈边框
1.绘制血条和蓝条
在 settings.py 文件中添加血条和蓝条的相关信息
settings.py
WIDTH = 1280#窗口宽度HEIGTH = 720 #窗口高度FPS = 60 #帧率TILESIZE = 64BAR_HEIGHT = 20 #血条和蓝条高度 +HEALTH_BAR_WIDTH = 200 #血条宽度 +ENERGY_BAR_WIDTH = 140 #蓝条宽度 +UI_BG_COLOR = \'#222222\' #血条和蓝条背景色 +UI_BORDER_COLOR = \'#111111\' #UI条框颜色HEALTH_COLOR = \'red\' #血条颜色 +ENERGY_COLOR = \'blue\' #蓝条颜色 +weapon_data = { ...
修改 Player 类的 init 函数,在最下面添加相应的设置信息
class Player(pygame.sprite.Sprite):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack): ...#武器切换冷却时间self.switch_duration_cooldown = 200#初始化玩家的状态self.stats = {\'health\': 100, \'energy\':60, \'speed\': 5}self.health = self.stats[\'health\'] * 0.5 # 玩家当前的生命值self.energy = self.stats[\'energy\'] * 0.8 # 玩家当前的能量值#设置移动速度self.speed = self.stats[\'speed\']
self.stats 中记录了玩家最大生命值、最大蓝量值和玩家移动速度
玩家生命值和蓝量值没有设置为满状态是为了验证背景色是否绘制成功。注意其中 self.speed 的值重新初始化了
在 code 文件夹下新建 ui.py,并在其中添加如下代码:
ui.py
import pygamefrom settings import *class UI: def __init__(self): #获取当前显示窗口的表面对象 self.display_surface = pygame.display.get_surface() #创建矩形对象,用于表示血条和能量条的位置和大小 self.health_bar_rect = pygame.Rect(10,10,HEALTH_BAR_WIDTH,BAR_HEIGHT) self.energy_bar_rect = pygame.Rect(10,34,ENERGY_BAR_WIDTH,BAR_HEIGHT)
init 函数中首先获取了显示窗口的表面对象,方便再上面绘制 UI,之后确定了血条和蓝条的绘制区域
在 UI 类中添加 show_bar 方法,用于绘制血条和蓝条
ui.py
def show_bar(self,current,max_amount,bg_rect,color): # 绘制背景色矩形 pygame.draw.rect(self.display_surface,UI_BG_COLOR,bg_rect) # 计算当前值所占的比例,然后计算当前值所占的宽度 ratio = current / max_amount current_width = bg_rect.width * ratio current_rect = bg_rect.copy() current_rect.width = current_width # 绘制当前值所占的宽度 pygame.draw.rect(self.display_surface,color,current_rect) # 绘制边框 pygame.draw.rect(self.display_surface,UI_BORDER_COLOR,bg_rect,3)
参数:current 表示玩家当前的生命值或能量值,max_amount:表示最大生命值或能量值,bg_rect:表示 init 函数中定义的血条和蓝条绘制区域,color 表示生命值或能量值的绘制颜色
函数第一行代码绘制了背景色
之后根据当前的生命值或能量值对最大生命值或能量值的比例设置绘制宽度并绘制
最后一行代码为血条或蓝条绘制边框,宽度为3像素
在 UI 类中添加 display 方法,用于调用绘制 UI 的函数,这里先调用 show_bar 方法绘制血条和蓝条
ui.py
def display(self,player): self.show_bar(player.health,player.stats[\'health\'],self.health_bar_rect,HEALTH_COLOR) self.show_bar(player.energy,player.stats[\'energy\'],self.energy_bar_rect,ENERGY_COLOR)
最后修改 Level 类,添加 UI 对象,并在 run 函数中调用 UI 类的 display 方法绘制 UI
level.py
...from weapon import Weaponfrom ui import UI +class Level:def __init__(self): ...#调用create_map方法创建地图self.create_map()#创建UI对象 +self.ui = UI() + ...def run(self):#调用draw方法绘制可见的精灵组self.visible_sprites.custom_draw(self.player)self.visible_sprites.update() #绘制UI +self.ui.display(self.player) +
2.绘制物品栏
在 settings.py 文件中添加物品栏的设置
...BAR_HEIGHT = 20 #血条和蓝条高度HEALTH_BAR_WIDTH = 200 #血条宽度ENERGY_BAR_WIDTH = 140 #蓝条宽度ITEM_BOX_SIZE = 80 #物品栏边长 +UI_BG_COLOR = \'#222222\' #血条和蓝条背景色UI_BORDER_COLOR = \'#111111\' #UI条框颜色HEALTH_COLOR = \'red\' #血条颜色ENERGY_COLOR = \'blue\' #蓝条颜色UI_BORDER_COLOR_ACTIVE = \'gold\' #切换物品时物品栏边框颜色 +...
修改 UI 类的 init 函数,加载物品栏中要显示的武器图片对象
class UI: def __init__(self): #获取当前显示窗口的表面对象 self.display_surface = pygame.display.get_surface() #创建矩形对象,用于表示血条和能量条的位置和大小 self.health_bar_rect = pygame.Rect(10,10,HEALTH_BAR_WIDTH,BAR_HEIGHT) self.energy_bar_rect = pygame.Rect(10,34,ENERGY_BAR_WIDTH,BAR_HEIGHT) #加载武器图像 + self.weapon_graphics = [] + for weapon in weapon_data.values(): + path = weapon[\'graphic\'] + weapon = pygame.image.load(path).convert_alpha() + self.weapon_graphics.append(weapon) +
根据之前保存在 weapon_data 中完整图片的路径来加载图片,并保留图像的透明度信息,最后添加到 self.weapon_graphics 列表中,用于后面再物品栏中显示
通过之前
在 UI 类中添加 selection_box 函数,用于绘制物品栏的背景和边框
ui.py
def selection_box(self, x, y, has_switched): bg_rect = pygame.Rect(x, y,ITEM_BOX_SIZE,ITEM_BOX_SIZE) pygame.draw.rect(self.display_surface,UI_BG_COLOR,bg_rect) if has_switched: pygame.draw.rect(self.display_surface,UI_BORDER_COLOR_ACTIVE,bg_rect,3) else: pygame.draw.rect(self.display_surface,UI_BORDER_COLOR,bg_rect,3) return bg_rect
函数参数:x, y 表示物品栏的左上角坐标,has_switched 表示当前是否在切换武器
首先绘制物品栏的背景色,然后判断 has_switched 值,如果正在切换武器,就将边框绘制为金色,否则绘制为黑色,最后返回矩形对象
在 UI 类中添加 weapon_overlay 函数,用于绘制物品栏中的武器
ui.py
def weapon_overlay(self,weapon_index,has_switched): bg_rect = self.selection_box(10,630,has_switched) weapon_surf = self.weapon_graphics[weapon_index] weapon_rect = weapon_surf.get_rect(center = bg_rect.center) self.display_surface.blit(weapon_surf,weapon_rect)
函数参数:weapon_index 是当前玩家使用武器的索引,has_switched 是当前玩家是否在切换武器
调用 selection_box 函数获取绘制好的物品栏背景和边框的矩形对象
根据 weapon_index 获取要绘制的武器图像,并将武器图片的中心点坐标设置到物品栏背景的中心
之后将物品栏背景、边框、和武器都绘制到屏幕上
修改 UI 类的 display 方法,调用 weapon_index 方法,传入当前使用武器的索引值和是否正在切换武器。其中 can_switch_weapon 表示当前是否可以切换武器,当不可以切换武器时就说明当前正在切换武器。
ui.py
def display(self,player): self.show_bar(player.health,player.stats[\'health\'],self.health_bar_rect,HEALTH_COLOR) self.show_bar(player.energy,player.stats[\'energy\'],self.energy_bar_rect,ENERGY_COLOR) self.weapon_overlay(player.weapon_index,not player.can_switch_weapon) +
3.绘制当前经验值
在 settings.py 中添加经验值的相关设置
settings.py
...ENERGY_BAR_WIDTH = 140 #蓝条宽度ITEM_BOX_SIZE = 80 #物品栏边长 UI_FONT = \'../graphics/font/joystix.ttf\' #经验值字体路径 +UI_FONT_SIZE = 18 #经验值字体大小 +UI_BG_COLOR = \'#222222\' #血条和蓝条背景色UI_BORDER_COLOR = \'#111111\' #UI条框颜色TEXT_COLOR = \'#EEEEEE\' #经验值字体颜色 +...
修改 Player 类的 init 函数,添加经验值变量,变量值123用于显示测试
player.py
class Player(pygame.sprite.Sprite):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack): ...#设置移动速度self.speed = self.stats[\'speed\']#经验值+self.exp = 123+
修改 UI 类的 init 函数,加载经验值字体对象
ui.py
class UI: def __init__(self): #获取当前显示窗口的表面对象 self.display_surface = pygame.display.get_surface() #创建字体对象 + self.font = pygame.font.Font(UI_FONT,UI_FONT_SIZE) + #创建矩形对象,用于表示血条和能量条的位置和大小 self.health_bar_rect = pygame.Rect(10,10,HEALTH_BAR_WIDTH,BAR_HEIGHT) ...
pygame.font.Font:是 Pygame 库中用于创建字体对象的函数。该函数接受字体文件的路径和字体大小作为参数,返回一个字体对象。
在 UI 类中添加 show_exp 函数,用于绘制经验值
ui.py
def show_exp(self,exp): text_surf = self.font.render(str(int(exp)),False,TEXT_COLOR) x = self.display_surface.get_size()[0] - 20 y = self.display_surface.get_size()[1] - 20 text_rect = text_surf.get_rect(bottomright = (x,y)) pygame.draw.rect(self.display_surface,UI_BG_COLOR,text_rect.inflate(20,20)) self.display_surface.blit(text_surf,text_rect) pygame.draw.rect(self.display_surface,UI_BORDER_COLOR,text_rect.inflate(20,20),3)
函数参数 exp 表示要绘制的数字
self.font.render:用于将文本渲染为一个 Surface
对象。这个 Surface
对象可以被绘制到游戏屏幕上。str(int(exp))表示要渲染的文本内容,False
表示不对文本进行抗锯齿处理,TEXT_COLOR 为文本颜色
接下来计算要渲染的 Surface
对象 右下角坐标,self.display_surface.get_size()
返回一个包含两个元素的元组 (width, height)
,分别表示显示表面的宽度和高度,所以文本被放置在屏幕的右下角,距离边缘 20 像素。
第一个 pygame.draw.rect 是绘制文本的背景色,text_rect.inflate(20,20)):将文本矩形的大小增加了 20 像素,以便为文本提供一些内边距绘制边框
然后将将渲染好的文本 text_surf
绘制到屏幕上
第二个 pygame.draw.rect 是在背景周围绘制一个边框
最后,在 display 方法中调用 show_exp 方法就绘制完成了
ui.py
def display(self,player): self.show_bar(player.health,player.stats[\'health\'],self.health_bar_rect,HEALTH_COLOR) self.show_bar(player.energy,player.stats[\'energy\'],self.energy_bar_rect,ENERGY_COLOR) self.weapon_overlay(player.weapon_index,not player.can_switch_weapon) self.show_exp(player.exp) +
八:绘制技能(魔法)栏UI
在物品栏的旁边绘制技能栏,和绘制武器的物品栏思路相同
1.为玩家添加技能释放和切换功能
首先在 settings.py 中添加技能的基本信息,第一列表示技能名称,也是文件夹名称,strength 表示伤害值,cost 表示好蓝量,graphic 表示技能图片
settings.py
weapon_data = { ...magic_data = { +\'flame\': {\'strength\': 5,\'cost\': 20,\'graphic\':\'../graphics/particles/flame/fire.png\'},\'heal\' : {\'strength\': 20,\'cost\': 10,\'graphic\':\'../graphics/particles/heal/heal.png\'}}
修改 Player 类的 init 函数,添加关于技能的相关变量
player.py
class Player(pygame.sprite.Sprite):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack,create_magic): + ...#武器切换冷却时间self.switch_duration_cooldown = 200#魔法攻击 +self.create_magic = create_magic +#当前使用技能索引 +self.magic_index = 0 +#当前使用的技能 +self.magic = list(magic_data.keys())[self.magic_index] +#是否可以切换技能 +self.can_switch_magic = True +#保存上一次技能切换的时间 +self.magic_switch_time = None +#初始化玩家的状态self.stats = {\'health\': 100, \'energy\':60, \'speed\': 5} ...
新添加的 create_magic 参数也是一个回调函数,之后在这个函数中绘制技能攻击的动画,剩下的变量作用和武器部分相同,技能切换冷却时间也使用 switch_duration_cooldown
修改 input 函数,添加技能释放和技能切换功能
player.py
def input(self):if not self.attacking:keys = pygame.key.get_pressed() ... # 普攻if keys[pygame.K_SPACE] or keys[pygame.K_j]:self.attacking = Trueself.attack_time = pygame.time.get_ticks()self.create_attack()# 魔法攻击if keys[pygame.K_LCTRL] or keys[pygame.K_k]:self.attacking = Trueself.attack_time = pygame.time.get_ticks()style = list(magic_data.keys())[self.magic_index] +strength = list(magic_data.values())[self.magic_index][\'strength\'] +cost = list(magic_data.values())[self.magic_index][\'cost\'] +self.create_magic(style,strength,cost) +# 武器切换if (keys[pygame.K_q] or keys[pygame.K_h]) and self.can_switch_weapon:self.can_switch_weapon = Falseself.weapon_switch_time = pygame.time.get_ticks()if self.weapon_index < len(list(weapon_data.keys())) - 1:self.weapon_index += 1else:self.weapon_index = 0self.weapon = list(weapon_data.keys())[self.weapon_index]# 魔法切换 +if (keys[pygame.K_e] or keys[pygame.K_i]) and self.can_switch_magic: +self.can_switch_magic = False +self.magic_switch_time = pygame.time.get_ticks() +if self.magic_index < len(list(magic_data.keys())) - 1: +self.magic_index += 1 +else: +self.magic_index = 0 +self.magic = list(magic_data.keys())[self.magic_index] +
按下技能攻击键时,根据当前技能的索引值获取当前使用的技能名称、技能伤害和技能消耗值,传入并调用 create_magic 函数绘制技能动画
按下切换技能的按键并且当前可以切换技能时,记录切换技能的时间、更新技能的列表的索引值,并更新当前使用的技能
修改 Player 类的 cooldowns 函数,添加对切换技能冷却时间的判断
player.py
def cooldowns(self):current_time = pygame.time.get_ticks()if self.attacking: ...if not self.can_switch_weapon: ...if not self.can_switch_magic: +if current_time - self.magic_switch_time >= self.switch_duration_cooldown: +self.can_switch_magic = True +
在 Level 类中添加 create_magic 方法(目前为了程序可以运行只做了简单的打印,绘制流程后面实现),并修改构造 Player 类时的参数,传入create_magic :
level.py
def create_map(self): layouts = {...graphics = {... for style,layout in layouts.items(): ...self.player = Player((2000,1430),[self.visible_sprites],self.obstacle_sprites,self.create_attack,self.destroy_attack,self.create_magic) + def create_magic(self,style,strength,cost): +print(style) +print(strength) +print(cost) +
2.绘制技能栏
和绘制武器物品栏的方式一样,首先修改 UI 类的 init 函数,将技能栏要显示的图片保存的列表中
ui.py
class UI: def __init__(self): ... #加载武器图像 self.weapon_graphics = [] ... #加载技能图片 + self.magic_graphics = [] + for magic in magic_data.values(): + magic = pygame.image.load(magic[\'graphic\']).convert_alpha() + self.magic_graphics.append(magic) +
在 UI 类中添加 magic_overlay 函数,用于绘制技能栏,实现思路和绘制物品栏是相同的
ui.py
def magic_overlay(self,magic_index,has_switched): bg_rect = self.selection_box(80,635,has_switched) magic_surf = self.magic_graphics[magic_index] magic_rect = magic_surf.get_rect(center = bg_rect.center) self.display_surface.blit(magic_surf,magic_rect)
在 display 中调用 magic_overlay 函数绘制技能栏
ui.py
def display(self,player): self.show_bar(player.health,player.stats[\'health\'],self.health_bar_rect,HEALTH_COLOR) self.show_bar(player.energy,player.stats[\'energy\'],self.energy_bar_rect,ENERGY_COLOR) self.weapon_overlay(player.weapon_index,not player.can_switch_weapon) self.magic_overlay(player.magic_index,not player.can_switch_magic) + self.show_exp(player.exp)
这里注意 weapon_overlay 和 magic_overlay 的调用顺序,后绘制的图像会覆盖先绘制的图像
九:绘制敌人
1.加载敌人信息
在 settings.py 中添加敌人的所有信息
settings.py
weapon_data = { ...magic_data = { ...monster_data = {\'squid\': {\'health\': 100,\'exp\':100,\'damage\':20,\'attack_type\': \'slash\', \'attack_sound\':\'../audio/attack/slash.wav\', \'speed\': 3, \'resistance\': 3, \'attack_radius\': 80, \'notice_radius\': 360},\'raccoon\': {\'health\': 300,\'exp\':250,\'damage\':40,\'attack_type\': \'claw\', \'attack_sound\':\'../audio/attack/claw.wav\',\'speed\': 2, \'resistance\': 3, \'attack_radius\': 120, \'notice_radius\': 400},\'spirit\': {\'health\': 100,\'exp\':110,\'damage\':8,\'attack_type\': \'thunder\', \'attack_sound\':\'../audio/attack/fireball.wav\', \'speed\': 4, \'resistance\': 3, \'attack_radius\': 60, \'notice_radius\': 350},\'bamboo\': {\'health\': 70,\'exp\':120,\'damage\':6,\'attack_type\': \'leaf_attack\', \'attack_sound\':\'../audio/attack/slash.wav\', \'speed\': 3, \'resistance\': 3, \'attack_radius\': 50, \'notice_radius\': 300}}
1.第一列表示敌人的名字,也是 graphics/monsters 目录下存放对应敌人的文件夹名称,文件夹中有对应敌人攻击、静止、移动的图片。2.health 表示敌人的生命值。3.exp 表示击杀敌人获得的经验值。4.damage 表示敌人攻击的伤害值。5.attack_type 表示敌人攻击技能的名称,也是 graphics/particles 目录下存放该技能图片的文件夹名称。6.attack_sound 表示敌人攻击时的音效文件路径。7.speed 表示敌人的移动速度。8.resistance 表示击退距离(玩家攻击并击中敌人后,会对敌人产生击退效果)。9.attack_radius 表示敌人的攻击距离,当敌人和玩家小于等于该距离时,敌人会对玩家发起攻击。10.notice_radius 表示跟踪距离,当敌人和玩家小于等于该距离时,敌人会向玩家移动
2.抽象实体类
敌人和玩家有很多相似的功能,比如说他们都需要添加到可见精灵组中进行绘制、都需要对障碍物进行碰撞检测、都需要相同的移动方法、相同的动画播放速度。可以将相同的功能抽象出一个父类,从而减少代码量
在 code 文件夹下创建 entity.py ,并添加如下代码:
entity.py
import pygameclass Entity(pygame.sprite.Sprite):def __init__(self,groups):super().__init__(groups)#初始化帧索引self.frame_index = 0#设置动画播放速度self.animation_speed = 0.15#创建一个零向量,默认为:(x=0, y=0)self.direction = pygame.math.Vector2()def move(self,speed):#码检查 self.direction 向量的模是否不为 0(也就是向量不为(0, 0)时,说明有方向键按下)if self.direction.magnitude() != 0:self.direction = self.direction.normalize()self.hitbox.x += self.direction.x * speedself.collision(\'horizontal\')self.hitbox.y += self.direction.y * speedself.collision(\'vertical\')# 更新 self.rect 的位置,使其与 self.hitbox 的位置相同self.rect.center = self.hitbox.centerdef collision(self,direction):if direction == \'horizontal\':for sprite in self.obstacle_sprites:if sprite.rect.colliderect(self.hitbox):if self.direction.x > 0: self.hitbox.right = sprite.rect.leftif self.direction.x 0: self.hitbox.bottom = sprite.rect.topif self.direction.y < 0: self.hitbox.top = sprite.rect.bottom
init 函数的参数就是可见精灵组,并将该实体类添加到可见精灵组中。然后将之前玩家类的 direction 变量、frame_index 变量 、animation_speed 变量、移动和检测碰撞的代码全部抽象到 Entity 类中
修改 Player 类
prayer.py
import pygame from settings import *from support import import_folderfrom entity import Entity +class Player(Entity): +def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack,create_magic):super().__init__(groups)#加载图片,并确保图像支持透明度self.image = pygame.image.load(\'../graphics/test/player.png\').convert_alpha() #获取图像的矩形区域
修改 Player 类的父类为 Entity。注意在 Player 类中的 init 方法中要删除 self.direction 变量、self.frame_index 变量、self.animation_speed 变量,并删除 move 和 collision 函数
3.绘制并让敌人移动
在 code 文件夹下创建 enemy.py 文件,添加敌人类,并添加 init 函数,和 Player 类很相似,就不详细解释了:
enemy.py
import pygamefrom settings import *from entity import Entityfrom support import *class Enemy(Entity):def __init__(self,monster_name,pos,groups,obstacle_sprites):super().__init__(groups)#设置精灵类型为 敌人self.sprite_type = \'enemy\' #导入敌人不同状态的动画图像,并将其保存在字典中self.import_graphics(monster_name)#设置初始状态为静止self.status = \'idle\'#设置敌人初始图片为静止图片列表中的第一个元素self.image = self.animations[self.status][self.frame_index]#设置敌人左上角位置self.rect = self.image.get_rect(topleft = pos)#设置敌人碰撞体积self.hitbox = self.rect.inflate(0,-10)#保存障碍物精灵组self.obstacle_sprites = obstacle_sprites#设置敌人名称self.monster_name = monster_name#从monster_data字典中获取敌人信息monster_info = monster_data[self.monster_name]#设置初始血量self.health = monster_info[\'health\']#设置击杀验值self.exp = monster_info[\'exp\']#设置移动速度self.speed = monster_info[\'speed\']#设置攻击伤害self.attack_damage = monster_info[\'damage\']#设置击退距离self.resistance = monster_info[\'resistance\']#设置攻击距离self.attack_radius = monster_info[\'attack_radius\']#设置跟踪距离self.notice_radius = monster_info[\'notice_radius\']#设置攻击技能类型self.attack_type = monster_info[\'attack_type\']#当前是否可以攻击self.can_attack = True#保存上一次攻击时间self.attack_time = None#攻击冷却时间self.attack_cooldown = 400def import_graphics(self,name):self.animations = {\'idle\':[],\'move\':[],\'attack\':[]}main_path = f\'../graphics/monsters/{name}/\'for animation in self.animations.keys():self.animations[animation] = import_folder(main_path + animation)
在 Enemy 类中添加 get_player_distance_direction 函数,用于计算敌人与玩家之间的距离和方向
def get_player_distance_direction(self,player):enemy_vec = pygame.math.Vector2(self.rect.center)player_vec = pygame.math.Vector2(player.rect.center)distance = (player_vec - enemy_vec).magnitude()if distance > 0:direction = (player_vec - enemy_vec).normalize()else:direction = pygame.math.Vector2()return (distance,direction)
magnitude
方法用于计算向量的长度,即敌人与玩家之间的直线距离。
normalize
方法为归一化,将向量转换为单位向量,即保持向量的方向不变,但将其长度缩放到 1,用于判断玩家相对于敌人的位置,确定敌人移动的方向。在二维空间中,一个单位向量 (dx, dy)
可以用来表示一个方向,和之前的玩家移动类似,dx > 0
,则方向是向右 dx < 0
,则方向是向左。dx = 0 说明在水平方向位置相同。dy > 0
,则方向是向上。dy < 0
,则方向是向下。dy = 0 说明在垂直方向位置相同。
为了方便理解,直接带入数据,假设敌人中心坐标为 (100, 150),玩家中心坐标为(300, 200),
distance = (300 - 100, 200 - 150).magnitude() = sqrt(200^2 + 50^2) ≈ 206.16
此时 distance 大于0,说明敌人和玩家不在同一个位置,就可以确定方向:
direction = (200, 50).normalize() = (200/206.16, 50/206.16) ≈ (0.97, 0.24)
接下来在 Enemy 类中添加 get_status 函数,根据敌人与玩家的距离,判断敌人的状态
def get_status(self, player):distance = self.get_player_distance_direction(player)[0]if distance <= self.notice_radius:self.status = \'move\'else:self.status = \'idle\'
在 Enemy 类中添加 actions 函数,根据当前玩家状态,设置敌人的行为
def actions(self,player):if self.status == \'move\':self.direction = self.get_player_distance_direction(player)[1]else:self.direction = pygame.math.Vector2()
如果当前敌人为移动状态,就设置敌人的移动方向,否则将敌人的移动方向设置为 (0,0)
在 Enemy 类中添加 animate 函数,根据敌人当前的状态设置当前帧要绘制的图片
enemy.py
def animate(self):animation = self.animations[self.status]self.frame_index += self.animation_speedif self.frame_index >= len(animation):self.frame_index = 0self.image = animation[int(self.frame_index)]self.rect = self.image.get_rect(center = self.hitbox.center)
animation:根据当前状态选择相应状态的图片列表
self.frame_index += self.animation_speed:更新当前帧索引,animation_speed 为 015,大约 1 / 0.15 = 6.6 帧后切换该状态的下一张动画图片,当图片列表绘制完时重置帧索引值
最后根据当前帧索引设置当前要绘制的图片和碰撞体积
编写 updata 函数调用移动功能相关的函数
enemy.py
def update(self):self.move(self.speed)self.animate()def enemy_update(self,player):self.get_status(player) self.actions(player)
因为 get_status 需要 player 对象作为参数,所以只能单独写一个 enemy_update 函数调用
接下来加载实体的位置信息,在 Level 类中的 create_map 函数添加如下代码:
level.py
...from weapon import Weaponfrom ui import UIfrom enemy import Enemy +...def create_map(self): layouts = {\'boundary\': import_csv_layout(\'../map/map_FloorBlocks.csv\'),\'grass\': import_csv_layout(\'../map/map_Grass.csv\'),\'object\': import_csv_layout(\'../map/map_Objects.csv\'),\'entities\': import_csv_layout(\'../map/map_Entities.csv\') +}graphics = {\'grass\': import_folder(\'../graphics/grass\'),\'objects\': import_folder(\'../graphics/objects\')} for style,layout in layouts.items(): ...if style == \'object\':surf = graphics[\'objects\'][int(col)]Tile((x,y),[self.visible_sprites,self.obstacle_sprites],\'object\',surf)if style == \'entities\': +if col == \'394\': +self.player = Player((x,y),[self.visible_sprites],self.obstacle_sprites,self.create_attack,self.destroy_attack,self.create_magic) +else: +if col == \'390\': monster_name = \'bamboo\' +elif col == \'391\': monster_name = \'spirit\' +elif col == \'392\': monster_name =\'raccoon\' +else: monster_name = \'squid\' +Enemy(monster_name,(x,y), [self.visible_sprites],self.obstacle_sprites) +
在 layouts 字典中 添加了新元素, entities 对应所有实体对象的绘制位置,其中 394 表示玩家位置,190 - 193 表示不同敌人的位置
下面的 if 条件中根据不同的数字绘制不同的实体,修改了 Player 的绘制坐标
在 YSortCameraGroup 类中添加调用每个敌人对象的 enemy_update 方法
level.py
class YSortCameraGroup(pygame.sprite.Group):...def enemy_update(self,player):enemy_sprites = [sprite for sprite in self.sprites() if hasattr(sprite,\'sprite_type\') and sprite.sprite_type == \'enemy\']for enemy in enemy_sprites:enemy.enemy_update(player)
该函数第一行是列表推导式的用法,用于从精灵组中筛选出所有的 敌人 精灵, hasattr 函数用于检查该对象是否有 sprite_type 属性,如果有,判断该属性值是否为 enemy,如果 sprite_type == enemy,就将该对象添加到 enemy_sprites 列表中
之后遍历所有敌人精灵,并调用该精灵的 enemy_update 函数
修改 Level 类中的 run 方法,调用自定义精灵组 YSortCameraGroup 的 enemy_update 函数
level.py
class Level: ... def run(self):#调用draw方法绘制可见的精灵组self.visible_sprites.custom_draw(self.player)self.visible_sprites.update()self.visible_sprites.enemy_update(self.player) + #绘制UIself.ui.display(self.player)
之前提到过精灵组的 update 方法会自动调用该精灵组中每一个精灵的 update 方法,但 enemy_update 方法是自定义的,所以需要手动调用
现在执行程序,当玩家靠近敌人时,敌人就会向玩家移动了
4.绘制敌人攻击动画
在 Enemy 类中修改 get_status 函数,添加攻击状态判断
enemy.py
def get_status(self, player):distance = self.get_player_distance_direction(player)[0]if distance <= self.attack_radius and self.can_attack: +if self.status != \'attack\': +self.frame_index = 0 +self.status = \'attack\' +elif distance <= self.notice_radius: +self.status = \'move\'else:self.status = \'idle\'
如果刚进入攻击范围,就将帧索引值重置为0,防止移动时用到的帧索引影响到攻击动画的绘制
在 Enemy 类中修改 actions 函数,保存攻击时间,用于判断攻击冷却
enemy.py
def actions(self,player):if self.status == \'attack\': +self.attack_time = pygame.time.get_ticks() +elif self.status == \'move\': +self.direction = self.get_player_distance_direction(player)[1]else:self.direction = pygame.math.Vector2()
在 Enemy 类中修改 animate 函数,这里新添加的判断条件,当敌人处于攻击状态并且动画帧已经播放完毕时,将 self.can_attack
设置为 False,用于后面的攻击冷却时间计算
enemy.py
def animate(self):animation = self.animations[self.status]self.frame_index += self.animation_speedif self.frame_index >= len(animation):if self.status == \'attack\': +self.can_attack = False +self.frame_index = 0self.image = animation[int(self.frame_index)]self.rect = self.image.get_rect(center = self.hitbox.center)
在 Enemy 类中添加 cooldown 函数,用于检查攻击冷却时间,并重置 can_attack 值
enemy.py
def cooldown(self):if not self.can_attack:current_time = pygame.time.get_ticks()if current_time - self.attack_time >= self.attack_cooldown:self.can_attack = True
当 animate 函数的攻击动画绘制完最后一帧时(if not self.can_attack:)才会开始计算敌人的攻击冷却时间,防止攻击冷却时间短 攻击动画时间长的敌人可以连续攻击
修改 update 函数,调用 cooldown 函数
enemy.py
def update(self):self.move(self.speed)self.animate()self.cooldown() +
十:处理敌人与玩家的交互
1.玩家普攻对敌人和小草造成伤害
修改 Weapon 的 init 函数,添加武器类精灵类型,后面会使用到
weapon.py
class Weapon(pygame.sprite.Sprite):def __init__(self,player,groups):super().__init__(groups)self.sprite_type = \'weapon\' +direction = player.status.split(\'_\')[0] ...
修改玩家攻击冷却时间判断,玩家攻击冷却时间 = 基础冷却时间 + 武器冷却时间
修改 Player 类的 cooldowns 函数
def cooldowns(self):current_time = pygame.time.get_ticks()if self.attacking:if current_time - self.attack_time >= self.attack_cooldown + weapon_data[self.weapon][\'cooldown\']: +self.attacking = Falseself.destroy_attack() ...
添加计算玩家伤害的函数,玩家伤害 = 基础伤害 + 武器伤害
先添加玩家基础伤害,默认为10,修改 Player 类的 init 函数
player.py
class Player(Entity):def __init__(...) ... self.stats = {\'health\':100, \'energy\':60, \'speed\':5, \'attack\':10} +self.health = self.stats[\'health\'] * 0.5 # 玩家当前的生命值self.energy = self.stats[\'energy\'] * 0.8 # 玩家当前的能量值 ...
在 Player 类中添加 get_full_weapon_damage 函数,用于计算当前使用武器普攻伤害
def get_full_weapon_damage(self):base_damage = self.stats[\'attack\']weapon_damage = weapon_data[self.weapon][\'damage\']return base_damage + weapon_damage
在 Enemy 中添加 get_damage 函数,用于计算敌人受到玩家的伤害,并扣除相应的生命值,参数 attack_type 是敌人受到攻击的类型,目前只有普攻
enemy.py
def get_damage(self,player,attack_type):if attack_type == \'weapon\':self.health -= player.get_full_weapon_damage()
在 Enemy 中添加 check_death 函数,用于判断敌人是否死亡
enemy.py
def check_death(self):if self.health <= 0:self.kill()
kill 函数之前提到过,会从所有继承 pygame.sprite.Sprite 时添加的精灵组中删除该精灵,这样敌人死亡后就不会继续绘制改精灵了
调用 check_death 函数,循环判断该敌人是否已经死亡
enemy.py
def update(self):self.move(self.speed)self.animate()self.cooldown()self.check_death() +
在 Level 类的 init 函数中添加两个精灵组:
attack_sprites 用于保存玩家可以对敌人和小草产生伤害的精灵,目前只有武器精灵
attackable_sprites 保存可以受到玩家攻击伤害的精灵,敌人和小草
level.py
class Level:def __init__(self): ...#武器精灵self.current_attack = None#攻击精灵组 +self.attack_sprites = pygame.sprite.Group() +#攻击目标精灵组 +self.attackable_sprites = pygame.sprite.Group() + ...
修改 Level 中的 create_map 函数,将小草和敌人添加到对应的精灵组中
level.py
def create_map(self): ... if style == \'grass\':random_grass_image = choice(graphics[\'grass\'])Tile((x,y),[self.visible_sprites, self.obstacle_sprites, self.attackable_sprites],\'grass\',random_grass_image) + ...else: monster_name = \'squid\' Enemy(monster_name,(x,y),[self.visible_sprites, self.attackable_sprites],self.obstacle_sprites) +
修改 create_attack 函数,将武器添加到对应的精灵组中
level.py
def create_attack(self):self.current_attack = Weapon(self.player,[self.visible_sprites, self.attack_sprites]) +
在 Level 类中添加 player_attack_logic 函数,用于检测 attack_sprites 精灵组和 attackable_sprites 精灵组中元素的碰撞
level.py
def player_attack_logic(self):if self.attack_sprites:for attack_sprite in self.attack_sprites:collision_sprites = pygame.sprite.spritecollide(attack_sprite,self.attackable_sprites,False)if collision_sprites:for target_sprite in collision_sprites:if target_sprite.sprite_type == \'grass\':target_sprite.kill()else:target_sprite.get_damage(self.player,attack_sprite.sprite_type)
spritecollide
函数用于检测两个精灵组(或精灵和精灵组)之间碰撞,遍历第一个参数指定的精灵,并检查它们是否与第二个参数指定的精灵组中的任何精灵发生碰撞。返回包含与第一个参数指定的精灵(或精灵组中的精灵)发生碰撞的所有精灵列表。
最后在run函数中调用 player_attack_logic 函数
level.py
def run(self):#调用draw方法绘制可见的精灵组self.visible_sprites.custom_draw(self.player)self.visible_sprites.update()self.visible_sprites.enemy_update(self.player) #方法处理玩家攻击逻辑 +self.player_attack_logic() + #绘制UIself.ui.display(self.player)
2.添加敌人的无敌时间和击退效果
首先添加无敌时间,在 Enemy 类的 init 函数添加敌人无敌时间相关变量
enemy.py
class Enemy(Entity):def __init__(self,monster_name,pos,groups,obstacle_sprites): ... #保存上一次攻击时间self.attack_time = None#攻击冷却时间self.attack_cooldown = 400#当前是否可以被击中 +self.vulnerable = True +#保存上一次被击中时间 +self.hit_time = None +#无敌时间 +self.invincibility_duration = 300 +
修改 Enemy 类的 get_damage 函数,当被攻击时修改 vulnerable 变量的状态,并记录被攻击时间
enemy.py
def get_damage(self,player,attack_type):if self.vulnerable:if attack_type == \'weapon\':self.health -= player.get_full_weapon_damage()self.hit_time = pygame.time.get_ticks()self.vulnerable = False
修改 Enemy 类的 cooldown 函数,添加敌人的无敌时间判断
enemy.py
def cooldown(self):current_time = pygame.time.get_ticks()if not self.can_attack:if current_time - self.attack_time >= self.attack_cooldown:self.can_attack = Trueif not self.vulnerable:if current_time - self.hit_time >= self.invincibility_duration:self.vulnerable = True
当敌人处于无敌状态时,还要添加一个闪烁状态,这个功能是通过改变敌人精灵的透明度实现的,设置透明度为 0 表示完全透明,透明度为 255 表示完全不透明
修改 Enemy 类的 animate 函数,根据当前敌人是否被攻击设置精灵的透明度
enemy.py
def animate(self): ...self.image = animation[int(self.frame_index)]self.rect = self.image.get_rect(center = self.hitbox.center)if not self.vulnerable: +alpha = self.wave_value() +self.image.set_alpha(alpha) +else: +self.image.set_alpha(255) +
当玩家处于无敌时间时,通过 wave_value 函数将透明度在 0 和 255 之间来回切换,达到闪烁效果。 set_alpha 函数用于设置透明度
添加 wave_value 函数,因为玩家受到攻击时也会闪烁,所有将该函数抽象到实体类中
entity.py
import pygamefrom math import sin +class Entity(pygame.sprite.Sprite): ...def wave_value(self): +value = sin(pygame.time.get_ticks())if value >= 0: return 255else: return 0
get_ticks
函数返回自 pygame 初始化以来经过的毫秒数,通过返回的时间计算正弦值。正弦函数的值域是 [-1, 1],随时间的推移而波动。
最后为敌人添加一个击退效果,当敌人受到伤害时,会向敌人移动相反的方向移动一定距离
修改 get_damage 函数,当敌人受到伤害时,先获取当前敌人的移动方向
def get_damage(self,player,attack_type):if self.vulnerable:self.direction = self.get_player_distance_direction(player)[1] +if attack_type == \'weapon\':self.health -= player.get_full_weapon_damage()self.hit_time = pygame.time.get_ticks()self.vulnerable = False
添加 hit_reaction 函数,将移动方向设置为当前敌人移动相反的方向,并在该方向添加移动距离
def hit_reaction(self):if not self.vulnerable:self.direction *= -self.resistance
修改 update 函数,添加 hit_reaction 函数,注意要添加到 move 函数之前
def update(self):self.hit_reaction() +self.move(self.speed)self.animate()self.cooldown()self.check_death()
3.敌人对玩家造成伤害
添加玩家无敌时间,在 Player 类的 init 函数添加无敌时间相关变量
player.py
class Player(Entity):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack,create_magic):super().__init__(groups) ...#设置移动速度self.speed = self.stats[\'speed\']#经验值self.exp = 123#玩家当前是否可以被击中 +self.vulnerable = True +#玩家上一次被击中时间 +self.hurt_time = None +#玩家无敌时间 +self.invulnerability_duration = 500 +
修改 Player 类中的 cooldowns 函数,对无敌时间冷却进行判断
player.py
def cooldowns(self):current_time = pygame.time.get_ticks() ...if not self.can_switch_magic:if current_time - self.magic_switch_time >= self.switch_duration_cooldown:self.can_switch_magic = Trueif not self.vulnerable: +if current_time - self.hurt_time >= self.invulnerability_duration: +self.vulnerable = True +
修改 Player 类的 animate 函数,同样添加闪烁
player.py
def animate(self): ...self.image = animation[int(self.frame_index)]self.rect = self.image.get_rect(center = self.hitbox.center)if not self.vulnerable: +alpha = self.wave_value() +self.image.set_alpha(alpha) +else: +self.image.set_alpha(255) +
再 Level 类中添加一个敌人对玩家造成伤害的函数 damage_player,参数 amount 表示当前敌人的伤害,attack_type 表示敌人的攻击类型(暂时没有用到)
level.py
class Level: ...def damage_player(self,amount,attack_type):if self.player.vulnerable:self.player.health -= amountself.player.vulnerable = Falseself.player.hurt_time = pygame.time.get_ticks()
修改 Level 类的 create_map 函数,将 damage_player 函数作为最后一个参数传递到 Enemy 类中
level.py
def create_map(self): ...Enemy(monster_name,(x,y),[self.visible_sprites, self.attackable_sprites],self.obstacle_sprites, self.damage_player) +
修改 Enemy 类的 init 函数形参,接收回到函数并保存
class Enemy(Entity):def __init__(self,monster_name,pos,groups,obstacle_sprites,damage_player): + ...#攻击冷却时间self.attack_cooldown = 400#保存攻击玩家回调函数 +self.damage_player = damage_player +#是否可以被击中self.vulnerable = True#保存上一次被击中时间 ...
修改 actions 函数,调用攻击玩家的函数,这里并没有检测碰撞,只是简单的判定当玩家和敌人的距离 <= 敌人的攻击距离 并且 敌人可以攻击时,玩家就会受到伤害
def actions(self,player):if self.status == \'attack\':self.attack_time = pygame.time.get_ticks()self.damage_player(self.attack_damage,self.attack_type) +elif self.status == \'move\': ...
十一:完善敌人的动画效果
这一章节将补充之前没有为敌人绘制的动画效果,包括敌人攻击动画、敌人死亡动画 和 小草被玩家攻击后打碎消失的动画
1.添加绘制动画功能
在 code 文件夹下添加 particles.py 文件,并添加 AnimationPlayer 类并添加如下代码
particles.py
import pygamefrom support import import_folderfrom random import choiceclass AnimationPlayer:def __init__(self):self.frames = {#加载敌人攻击动画 \'claw\': import_folder(\'../graphics/particles/claw\'),\'slash\': import_folder(\'../graphics/particles/slash\'),\'sparkle\': import_folder(\'../graphics/particles/sparkle\'),\'leaf_attack\': import_folder(\'../graphics/particles/leaf_attack\'),\'thunder\': import_folder(\'../graphics/particles/thunder\'),#加载敌人死亡动画\'squid\': import_folder(\'../graphics/particles/smoke_orange\'),\'raccoon\': import_folder(\'../graphics/particles/raccoon\'),\'spirit\': import_folder(\'../graphics/particles/nova\'),\'bamboo\': import_folder(\'../graphics/particles/bamboo\'),#加载小草消失动画\'leaf\': (import_folder(\'../graphics/particles/leaf1\'),import_folder(\'../graphics/particles/leaf2\'),import_folder(\'../graphics/particles/leaf3\'),import_folder(\'../graphics/particles/leaf4\'),import_folder(\'../graphics/particles/leaf5\'),import_folder(\'../graphics/particles/leaf6\'),self.reflect_images(import_folder(\'../graphics/particles/leaf1\')),self.reflect_images(import_folder(\'../graphics/particles/leaf2\')),self.reflect_images(import_folder(\'../graphics/particles/leaf3\')),self.reflect_images(import_folder(\'../graphics/particles/leaf4\')),self.reflect_images(import_folder(\'../graphics/particles/leaf5\')),self.reflect_images(import_folder(\'../graphics/particles/leaf6\')))}def reflect_images(self,frames):new_frames = []for frame in frames:flipped_frame = pygame.transform.flip(frame,True,False)new_frames.append(flipped_frame)return new_frames
AnimationPlayer 用于绘制指定的动画,目前只写了 init 函数,用于加载对应的动画图片列表,并存储到 frames 字典中
reflect_images 函数用于对叶片图像进行水平翻转(pygame.transform.flip(frame, True, False)
中的 True
表示水平翻转,False
表示不垂直翻转),翻转后返回图像列表,增加动画的多样性。
因为绘制这些动画的方法是相同的,所以将绘制方法单独抽象出一个类:
particles.py
class ParticleEffect(pygame.sprite.Sprite):def __init__(self,pos,animation_frames,groups):super().__init__(groups)#帧索引self.frame_index = 0#动画播放速度self.animation_speed = 0.15#要绘制的动画列表self.frames = animation_frames#初始化第一帧的图像self.image = self.frames[self.frame_index]#初始化第一帧的矩形self.rect = self.image.get_rect(center = pos)def animate(self):self.frame_index += self.animation_speedif self.frame_index >= len(self.frames):self.kill()else:self.image = self.frames[int(self.frame_index)]def update(self):self.animate()
init 函数参数分别为 绘制动画的 中心坐标,动画的图片列表 和 精灵组
绘制方式就不详细讲了,和之前绘制动画的方法相同。在 animate 函数中判断绘制动画完成后会将精灵从精灵组中删除,因为这些动画都是一次性的。
有了绘制动画的类,就可以绘制相应的动画了,在 AnimationPlayer 类中添加 create_grass_particles 函数,用于绘制小草的消失的动画
particles.py
def create_grass_particles(self,pos,groups):animation_frames = choice(self.frames[\'leaf\'])ParticleEffect(pos,animation_frames,groups)
参数为绘制动画的中心点坐标 和 精灵组。因为 self.frames[\'leaf\'] 是一个包含多个动画序列的列表,所以绘制小草的动画需要使用一个单独的函数,其中使用 choice 随机选择一个动画序列传作为参数创建 ParticleEffect 类绘制动画
在 AnimationPlayer 类中添加 create_particles 函数,用于绘制敌人攻击和死亡动画
particles.py
def create_particles(self,animation_type,pos,groups):animation_frames = self.frames[animation_type]ParticleEffect(pos,animation_frames,groups)
参数 animation_frames 是要绘制动画的名称,也就是 self.frames 字典的 key 值,通过 key 值获取要绘制动画的列表,并作为参数创建 ParticleEffect 类绘制动画
2.调用绘制动画的函数
修改 Level 类的 init 函数,创建 AnimationPlayer 对象用于后续动画的绘制
level.py
...from support import *from random import choice, randint +from weapon import Weaponfrom ui import UIfrom enemy import Enemyfrom particles import AnimationPlayer +class Level:def __init__(self): ...#创建UI对象self.ui = UI()#创建动画播放器对象 +self.animation_player = AnimationPlayer() +
修改 Level 类中的 player_attack_logic 函数,当小草消失时绘制消失动画
level.py
def player_attack_logic(self):if self.attack_sprites:for attack_sprite in self.attack_sprites:collision_sprites = pygame.sprite.spritecollide(attack_sprite,self.attackable_sprites,False)if collision_sprites:for target_sprite in collision_sprites:if target_sprite.sprite_type == \'grass\':pos = target_sprite.rect.center +offset = pygame.math.Vector2(0,75) +for leaf in range(randint(3,6)): +self.animation_player.create_grass_particles(pos - offset,[self.visible_sprites]) +target_sprite.kill()else:target_sprite.get_damage(self.player,attack_sprite.sprite_type)
因为之前的加载的每个叶片动画都只有一片叶子,所以这里用 randint 函数随机绘制 3 - 6 片叶子。其中 create_grass_particles 的参数 pos - offset 是为了让叶片动画在小草上面一定距离绘制,模拟小草被打飞了的感觉。参数 [self.visible_sprites] 表示将小草动画添加到可视精灵组中进行绘制
修改 Level 类中的 damage_player 函数绘制敌人攻击动画,同样添加到可以精灵组中
level.py
def damage_player(self,amount,attack_type):if self.player.vulnerable:self.player.health -= amountself.player.vulnerable = Falseself.player.hurt_time = pygame.time.get_ticks()self.animation_player.create_particles(attack_type,self.player.rect.center,[self.visible_sprites]) +
最后绘制敌人死亡动画,因为敌人死亡需要在 Enemy 类中判定,所以要在 Level 类中编写绘制敌人死亡动画的函数并传递到敌人类中,在判定敌人死亡时调用
添加绘制敌人死亡动画函数 trigger_death_particles
level.py
def trigger_death_particles(self,pos,particle_type):self.animation_player.create_particles(particle_type,pos,self.visible_sprites)
修改 Level 类的 create_map 函数,创建敌人类时传入 trigger_death_particles 函数
level.py
def create_map(self): ...Enemy(monster_name,(x,y),[self.visible_sprites, self.attackable_sprites],self.obstacle_sprites, self.damage_player, self.trigger_death_particles) +
修改 Enemy 类的 init 函数,接收并保存绘制敌人死亡的回调函数
enemy.py
def __init__(self,monster_name,pos,groups,obstacle_sprites,damage_player,trigger_death_particles): + ...#攻击冷却时间self.attack_cooldown = 400#保存攻击玩家回调函数self.damage_player = damage_player#保存绘制敌人死亡动画的回调函数 +self.trigger_death_particles = trigger_death_particles +#是否可以被击中self.vulnerable = True#保存上一次被击中时间self.hit_time = None#无敌时间self.invincibility_duration = 300
修改 Enemy 类的 check_death 函数在敌人死亡时调用
enemy.py
def check_death(self):if self.health <= 0:self.kill()self.trigger_death_particles(self.rect.center,self.monster_name) +
十二:完善玩家技能(魔法)释放
之间章节处理玩家释放技能时只是简单的打印,这一章节将补齐该功能
1.添加玩家技能类
在 AnimationPlayer 类的 init 函数中加载玩家释放技能的动画,方便后续需要使用 create_particles 函数绘制。其中包含一个火焰攻击技能 flame,和一个治疗技能,由 aura 和 heal 两个动画组成
particles.py
class AnimationPlayer:def __init__(self):self.frames = {#加载玩家技能动画 +\'flame\': import_folder(\'../graphics/particles/flame/frames\'), +\'aura\': import_folder(\'../graphics/particles/aura\'), +\'heal\': import_folder(\'../graphics/particles/heal/frames\'), +#加载敌人攻击动画 \'claw\': import_folder(\'../graphics/particles/claw\'),\'slash\': import_folder(\'../graphics/particles/slash\'), ...
在 code 文件夹下创建 magic.py ,并添加 MagicPlayer 类,用于处理玩家技能释放相关逻辑,如果后面要增加新技能也比较方便
首先添加 init 函数,参数为 AnimationPlayer 类的实例,用于后续绘制技能动画
magic.py
import pygamefrom settings import *from random import randintclass MagicPlayer:def __init__(self,animation_player):self.animation_player = animation_player
在 MagicPlayer 类中添加 heal 函数,用于处理治疗技能相关逻辑
magic.py
def heal(self,player,strength,cost,groups):if player.energy >= cost:player.health += strengthplayer.energy -= costif player.health >= player.stats[\'health\']:player.health = player.stats[\'health\']self.animation_player.create_particles(\'aura\',player.rect.center,groups)self.animation_player.create_particles(\'heal\',player.rect.center,groups)
参数: player 是 Player 类的实例,strength 是生命值回复值,cost 是蓝耗,groups 是精灵组
函数逻辑比较简单,如何蓝够的话就减少蓝量并回复生命值,并以玩家当前中心坐标绘制技能动画
在 MagicPlayer 类中添加 flame 函数,用于处理火焰攻击技能相当逻辑
magic.py
def flame(self,player,cost,groups):if player.energy >= cost:player.energy -= costif player.status.split(\'_\')[0] == \'right\': direction = pygame.math.Vector2(1,0)elif player.status.split(\'_\')[0] == \'left\': direction = pygame.math.Vector2(-1,0)elif player.status.split(\'_\')[0] == \'up\': direction = pygame.math.Vector2(0,-1)else: direction = pygame.math.Vector2(0,1)for i in range(1,6):if direction.x:offset_x = (direction.x * i) * TILESIZEx = player.rect.centerx + offset_x + randint(-TILESIZE // 3, TILESIZE // 3)y = player.rect.centery + randint(-TILESIZE // 3, TILESIZE // 3)self.animation_player.create_particles(\'flame\',(x,y),groups)else:offset_y = (direction.y * i) * TILESIZEx = player.rect.centerx + randint(-TILESIZE // 3, TILESIZE // 3)y = player.rect.centery + offset_y + randint(-TILESIZE // 3, TILESIZE // 3)self.animation_player.create_particles(\'flame\',(x,y),groups)
direction:根据玩家当前的方向确定确定技能的释放方向
for 循环会在技能释放的方向绘制5个火焰动画,首先判断技能释放方向,如果是水平方向(左或右),则计算 x
方向的偏移量;如果是垂直方向(上或下),则计算 y
方向的偏移量。
以水平方向为例,首先计算第 i 个火焰动画的水平偏移量,在计算出的偏移量基础上,对 x
和 y
位置分别加上一个小的随机偏移量,模拟火焰扩散效果。
2.调用玩家技能类
修改 Level 类的 init 函数,创建 MagicPlayer 类实例
level.py
...from enemy import Enemyfrom particles import AnimationPlayerfrom magic import MagicPlayer +class Level:def __init__(self): ...#创建动画播放器对象self.animation_player = AnimationPlayer()#创建玩家释放技能对象 +self.magic_player = MagicPlayer(self.animation_player) +
修改 Level 类的 create_magic 函数,调用对应的释放技能函数
level.py
def create_magic(self,style,strength,cost):if style == \'heal\':self.magic_player.heal(self.player,strength,cost,[self.visible_sprites])if style == \'flame\':self.magic_player.flame(self.player,cost,[self.visible_sprites,self.attack_sprites])
注意绘制 flame 技能时将火焰精灵添加到了 self.attack_sprites 精灵组中,这样该技能就可以通过 Level 类的 player_attack_logic 函数对敌人和小草产生攻击效果了,但是 player_attack_logic 对敌人产生攻击调用 get_damage 函数时,是通过 attack_type 变量来判断敌人受到伤害的类型的,而最终添加到 self.attack_sprites 精灵组中的 flame 精灵是 ParticleEffect 类型,所以要为 ParticleEffect 类型添加 attack_type ,否则火焰技能碰到敌人时程序会崩溃
修改 ParticleEffect 类的 init 函数
particles.py
class ParticleEffect(pygame.sprite.Sprite):def __init__(self,pos,animation_frames,groups):super().__init__(groups)#初始化精灵类型 +self.sprite_type = \'\' +#帧索引self.frame_index = 0
因为会通过 ParticleEffect 类创建多种不同类型的精灵,所以精灵类型最好通过函数参数设置,但不会影响到程序逻辑,我就懒得改了
3.设置技能伤害、蓝条自动回复
技能伤害和普攻伤害计算方式差不多,修改 Player 类的 init 函数,添加基础技能伤害
player.py
class Player(Entity):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack,create_magic): ...#保存上一次技能切换的时间self.magic_switch_time = None#初始化玩家的状态self.stats = {\'health\':100, \'energy\':60, \'speed\':5, \'attack\':10, \'magic\':4} +self.health = self.stats[\'health\'] * 0.5 # 玩家当前的生命值self.energy = self.stats[\'energy\'] * 0.8 # 玩家当前的能量值 ...
在 Player 中添加 get_full_magic_damage 函数,计算并返回当前使用技能的伤害
player.py
def get_full_magic_damage(self):base_damage = self.stats[\'magic\']spell_damage = magic_data[self.magic][\'strength\']return base_damage + spell_damage
修改 Enemy 类的 get_damage 函数,处理敌人受到玩家技能伤害的逻辑
enemy.py
def get_damage(self,player,attack_type):if self.vulnerable:self.direction = self.get_player_distance_direction(player)[1]if attack_type == \'weapon\':self.health -= player.get_full_weapon_damage()else: +self.health -= player.get_full_magic_damage() +self.hit_time = pygame.time.get_ticks()self.vulnerable = False
最后在 Player 类中添加 energy_recovery 函数,处理能量值回复的逻辑,并在 update 函数中调用。当前能量没有达到最大能量值时,缓慢回复
player.py
def energy_recovery(self):if self.energy < self.stats[\'energy\']:self.energy += 0.01 * self.stats[\'magic\']else:self.energy = self.stats[\'energy\']def update(self):self.input()self.cooldowns()self.get_status()self.animate()self.move(self.speed)self.energy_recovery() +
十三:升级
前面的章节在游戏画面的右下角绘制过经验值,也提到过击杀敌人可以获得经验值,这一章节将这部分内容补充完整,并且玩家可以通过花费经验值升级自身属性。
用户可以通过键盘按键来显示升级选项的菜单,可以为玩家的不同属性升级,每个选项都由一个背景色、边框颜色、最上方的属性名称、最下方的升级该属性所需经验值和中间的游标组成,用户可以通过键盘来切换菜单,背景为白色表示当前选中的菜单
1.击杀敌人获取经验
判断敌人死亡的逻辑和击杀敌人获得的经验值都在 Enemy 类中,需要将经验值添加到 Player 类中,还是可以用回调函数处理这个问题
在 Level 类中添加 add_exp 函数,增加玩家经验值
level.py
def add_exp(self,amount):self.player.exp += amount
修改 create_map 函数,创建敌人类时将 add_exp 函数传递到参数列表中
level.py
def create_map(self): ...Enemy(monster_name,(x,y),[self.visible_sprites,self.attackable_sprites],self.obstacle_sprites,self.damage_player,self.trigger_death_particles,self.add_exp) +
修改 Enemy 类的 init 函数形参列表,添加并保存 add_exp 函数
enemy.py
class Enemy(Entity):def __init__(self,monster_name,pos,groups,obstacle_sprites,damage_player,trigger_death_particles,add_exp): +...#保存绘制敌人死亡动画的回调函数self.trigger_death_particles = trigger_death_particles#保存添加经验的回调函数 +self.add_exp = add_exp +#是否可以被击中self.vulnerable = True#保存上一次被击中时间 ...
在敌人死亡时调用该函数
enemy.py
def check_death(self):if self.health <= 0:self.kill()self.trigger_death_particles(self.rect.center,self.monster_name)self.add_exp(self.exp) +
2.添加处理升级菜单的类
修改 Player 类的 init 函数,添加升级属性相关的值,后面要对玩家的这5个属性创建对应的升级选项进行升级
player.py
class Player(Entity):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack,create_magic): ...#保存上一次技能切换的时间self.magic_switch_time = None#初始化玩家的属性self.stats = {\'health\':100, \'energy\':60, \'speed\':5, \'attack\':10, \'magic\':4}#设置属性最大值 +self.max_stats = {\'health\': 300, \'energy\': 140, \'attack\': 20, \'magic\' : 10, \'speed\': 10} +#升级对应属性所需的经验值 +self.upgrade_cost = {\'health\': 100, \'energy\': 100, \'attack\': 100, \'magic\' : 100, \'speed\': 100} +self.health = self.stats[\'health\'] * 0.5 # 玩家当前的生命值self.energy = self.stats[\'energy\'] * 0.8 # 玩家当前的能量值
在 Player 类中添加对应列表值的获取函数
player.py
def get_value_by_index(self,index):return list(self.stats.values())[index]def get_cost_by_index(self,index):return list(self.upgrade_cost.values())[index]
在 code 文件夹下创建 upgrade.py 文件,并添加一个 Item 类,用于绘制每一个升级选项的 UI,并处理该选项对应的属性升级
在 settings.py 中添加绘制菜单选项用到的颜色
...HEALTH_COLOR = \'red\' #血条颜色ENERGY_COLOR = \'blue\' #蓝条颜色UI_BORDER_COLOR_ACTIVE = \'gold\' #切换物品时物品栏边框颜色 TEXT_COLOR_SELECTED = \'#111111\' #选中升级选项时的字体颜色 +BAR_COLOR = \'#EEEEEE\' #未选中升级选项时的游标颜色 +BAR_COLOR_SELECTED = \'#111111\' #选中升级选项时的游标颜色 +UPGRADE_BG_COLOR_SELECTED = \'#EEEEEE\' #选中升级选项时的背景色 +weapon_data = {...
upgrade.py
import pygamefrom settings import *class Item:def __init__(self,x,y,w,h,index,font):self.rect = pygame.Rect(l,t,w,h)self.index = indexself.font = font
init 函数的参数:要绘制选项 UI 的左上角 x y 坐标,选项 UI 的宽和高,index 表示该选项的序号(索引),font 表示选项上使用的字体和字体大小
在 Item 类上面添加一个 Upgrade 类,它使用 Item 类绘制升级选项的 UI 并且处理升级相关逻辑,
upgrade.py
class Upgrade:def __init__(self,player):# 获取当前显示窗口的表面对象 self.display_surface = pygame.display.get_surface()self.player = player# 获取玩家的属性数量self.attribute_nr = len(player.stats)# 获取玩家的属性名称self.attribute_names = list(player.stats.keys())# 获取玩家的属性最大值self.max_values = list(player.max_stats.values())# 初始化字体和字体大小self.font = pygame.font.Font(UI_FONT, UI_FONT_SIZE)# 初始化每个升级选项的高度和宽度self.height = self.display_surface.get_size()[1] * 0.8self.width = self.display_surface.get_size()[0] // 6# 创建升级选项self.create_items()# 初始化选项的索引self.selection_index = 0# 保存上一次切换选项或升级属性的时间self.selection_time = None# 是否可以切换选项或升级属性self.can_move = True# 冷却时间self.cool_down_time = 300
selection_index 变量表示当前选中的是第几个选项
为 Upgrade 类添加 create_items 函数,用于为每个属性创建升级选项
upgrade.py
def create_items(self):#保存升级选项的列表self.item_list = []for item, index in enumerate(range(self.attribute_nr)):# 计算每个升级选项的水平位置full_width = self.display_surface.get_size()[0]# 计算能够容纳每个升级选项的宽度increment = full_width // self.attribute_nr# 计算每个升级选项的左边缘位置left = (item * increment) + (increment - self.width) // 2# 计算每个升级选项的垂直位置top = self.display_surface.get_size()[1] * 0.1# 创建升级选项item = Item(left,top,self.width,self.height,index,self.font)self.item_list.append(item)
为 Upgrade 类添加 input 函数,用于处理用户切换选项和升级对应的属性
upgrade.py
def input(self):keys = pygame.key.get_pressed()if self.can_move:if (keys[pygame.K_RIGHT] or keys[pygame.K_d]) and self.selection_index = 1:self.selection_index -= 1self.can_move = Falseself.selection_time = pygame.time.get_ticks()if keys[pygame.K_SPACE] or keys[pygame.K_j]:self.can_move = Falseself.selection_time = pygame.time.get_ticks()self.item_list[self.selection_index].trigger(self.player)
为 Upgrade 类添加 selection_cooldown 函数,用于处理当前是否可以切换选项
upgrade.py
def selection_cooldown(self):if not self.can_move:current_time = pygame.time.get_ticks()if current_time - self.selection_time >= self.cool_down_time:self.can_move = True
为 Item 类添加 trigger 函数,用于处理指定属性的升级功能
upgrade.py
def trigger(self,player):# 获取玩家要的升级属性upgrade_attribute = list(player.stats.keys())[self.index]# 判断玩家是否有足够的经验值 和 升级属性是否已经达到最大值if player.exp >= player.upgrade_cost[upgrade_attribute] and player.stats[upgrade_attribute] player.max_stats[upgrade_attribute]:player.stats[upgrade_attribute] = player.max_stats[upgrade_attribute]
为 Upgrade 类添加 display 函数,循环检测用户输入和输入冷却时间,并调用 Item 类的 display 函数绘制对应选项 UI
upgrade.py
def display(self):self.input()self.selection_cooldown()for index, item in enumerate(self.item_list):# 获取属性名称、该属性的当前值、该属性的最大值和升级所需的经验值name = self.attribute_names[index]value = self.player.get_value_by_index(index)max_value = self.max_values[index]cost = self.player.get_cost_by_index(index)# 显示升级选项item.display(self.display_surface,self.selection_index,name,value,max_value,cost)
为 Item 类添加 display 函数,用于绘制对应的选项UI
upgrade.py
def display(self,surface,selection_num,name,value,max_value,cost):#如果是选中的索引,则背景为白色,否则为深灰色。 边框都为黑色if self.index == selection_num:pygame.draw.rect(surface,UPGRADE_BG_COLOR_SELECTED,self.rect)pygame.draw.rect(surface,UI_BORDER_COLOR,self.rect,4)else:pygame.draw.rect(surface,UI_BG_COLOR,self.rect)pygame.draw.rect(surface,UI_BORDER_COLOR,self.rect,4)
函数参数:surface 显示的表面、selection_num 选中的索引、name 属性名称、value 属性值、 max_value 属性最大值、cost 升级所需的经验值
为 Item 类添加 display_names 函数,用于绘制对应的选项的属性名称
upgrade.py
def display_names(self,surface,name,cost,selected):# 如果是选中的索引,则文字颜色为黑色,否则为白色color = TEXT_COLOR_SELECTED if selected else TEXT_COLOR# 将使用属性名称和颜色生成 Surface 对象title_surf = self.font.render(name,False,color)# 设置名称矩形位置title_rect = title_surf.get_rect(midtop = self.rect.midtop + pygame.math.Vector2(0,20))# 升级所需的经验同理cost_surf = self.font.render(f\'{int(cost)}\',False,color)cost_rect = cost_surf.get_rect(midbottom = self.rect.midbottom - pygame.math.Vector2(0,20))# 将Surface 对象绘制到屏幕上surface.blit(title_surf,title_rect)surface.blit(cost_surf,cost_rect)def display(self,surface,selection_num,name,value,max_value,cost): ...pygame.draw.rect(surface,UI_BORDER_COLOR,self.rect,4)self.display_names(surface,name,cost,self.index == selection_num) +
参数: surface 显示的表面、name是属性名称、cost是升级所需的经验值、selected是是否选中该属性
为 Item 类添加 display_bar 函数,用于绘制对应的选项的游标
def display_bar(self,surface,value,max_value,selected):# 获取游标范围顶部位置(选项顶部向中心位置下移动60像素)top = self.rect.midtop + pygame.math.Vector2(0,60)# 获取游标范围底部位置(选项底部中心位置向上移动60像素)bottom = self.rect.midbottom - pygame.math.Vector2(0,60)# 获取游标颜色,如果选中则为黑色,否则为白色color = BAR_COLOR_SELECTED if selected else BAR_COLOR# 计算游标范围高度full_height = bottom[1] - top[1]# 计算游标滑块相对高度relative_number = (value / max_value) * full_height# X 坐标 top[0] - 15 是中心位置向左偏移15像素# Y 坐标 bottom[1] - relative_number 是游标范围向下偏移相对高度value_rect = pygame.Rect(top[0] - 15,bottom[1] - relative_number,30,10)# 绘制游标范围和滑块pygame.draw.line(surface,color,top,bottom,5)pygame.draw.rect(surface,color,value_rect)def display(self,surface,selection_num,name,value,max_value,cost): ...self.display_names(surface,name,cost,self.index == selection_num)self.display_bar(surface,value,max_value,self.index == selection_num) +
参数: surface 显示的表面,value是属性的当前值,max_value是属性的最大值,selected是是否选中该属性
3.调用升级类
修改 Level 类的 init 函数创建 Upgrade 类并添加是否绘制升级界面的标志
level.py
...from particles import AnimationPlayerfrom magic import MagicPlayerfrom upgrade import Upgrade +class Level:def __init__(self): ...#创建UI对象self.ui = UI()#创建升级对象 +self.upgrade = Upgrade(self.player) +#是否显示升级界面 +self.game_paused = False +#创建动画播放器对象self.animation_player = AnimationPlayer()#创建玩家释放技能对象self.magic_player = MagicPlayer(self.animation_player)
在 Level 类中添加 toggle_menu 函数,用于修改 game_paused 标志
level.py
def toggle_menu(self):self.game_paused = not self.game_paused
修改 Level 类中 run 方法,根据标志位选择绘制升级菜单还是游戏画面
level.py
def run(self):self.visible_sprites.custom_draw(self.player)self.ui.display(self.player)if self.game_paused:#绘制升级菜单self.upgrade.display()else:#更新可见精灵组self.visible_sprites.update()self.visible_sprites.enemy_update(self.player)self.player_attack_logic()
修改 Game 类的 run 函数,用于判断用户输入是否打开升级菜单
main.py
def run(self):while True:#侦听键盘和⿏标事件for event in pygame.event.get():if event.type == pygame.QUIT:pygame.quit()sys.exit()if event.type == pygame.KEYDOWN: +if event.key == pygame.K_m: +self.level.toggle_menu() +# 让最近绘制的屏幕可⻅ 并将屏幕设置为黑色self.screen.fill(\'black\') ...
十四:添加音效并调整代码
1.添加游戏背景音乐
修改 Game 类的 init 函数,添加并播放游戏背景音乐
main.py
class Game:def __init__(self): ...#创建游戏关卡self.level = Level()#播放游戏背景音乐main_sound = pygame.mixer.Sound(\'../audio/main.ogg\') +main_sound.set_volume(0.5) +main_sound.play(loops = -1) +
pygame.mixer.Sound 是 Pygame 库中用于处理音频的一个类,可以使用音频路径初始化
set_volume 函数用于设置音频播放的音量,范围从 0.0(静音)到 1.0(最大音量)
play
函数用于开始播放音频,参数 loops
表示音频播放的次数。loops
默认值为 0,音频播放一次。loops
为 -1,音频会无限循环播放,loops
为正整数,音频会播放指定的次数后停止。
2、为玩家攻击添加音效
修改 Player 类的 init 函数,添加玩家普攻音效
player.py
class Player(Entity):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack,create_magic): ...self.hurt_time = None#玩家无敌时间self.invulnerability_duration = 500# 设置武器攻击音效 +self.weapon_attack_sound = pygame.mixer.Sound(\'../audio/sword.wav\') +self.weapon_attack_sound.set_volume(0.4) +
修改 Player 类的 input 函数,当玩家普攻时播放音效
player.py
def input(self): ... # 普攻if keys[pygame.K_SPACE] or keys[pygame.K_j]:self.attacking = Trueself.attack_time = pygame.time.get_ticks()self.create_attack()self.weapon_attack_sound.play() + ...
修改 MagicPlayer 类的 init 函数,添加玩家技能音效
magic.py
class MagicPlayer:def __init__(self,animation_player):self.animation_player = animation_playerself.sounds = { +\'heal\': pygame.mixer.Sound(\'../audio/heal.wav\'), +\'flame\':pygame.mixer.Sound(\'../audio/Fire.wav\') +} +
修改 MagicPlayer 类的 heal 和 flame 函数,播放对应的音效
def heal(self,player,strength,cost,groups):if player.energy >= cost:self.sounds[\'heal\'].play() +player.health += strength ...def flame(self,player,cost,groups):if player.energy >= cost:player.energy -= costself.sounds[\'flame\'].play() +if player.status.split(\'_\')[0] == \'right\': direction = pygame.math.Vector2(1,0) ...
3.为敌人添加音效
修改 Enemy 类的 init 函数,加载敌人音效,注意不同敌人的攻击音效是不同的
enemy.py
class Enemy(Entity):def __init__(self,monster_name,pos,groups,obstacle_sprites,damage_player,trigger_death_particles,add_exp): ...#无敌时间self.invincibility_duration = 300#敌人死亡音效 +self.death_sound = pygame.mixer.Sound(\'../audio/death.wav\') +#敌人别被击中音效 +self.hit_sound = pygame.mixer.Sound(\'../audio/hit.wav\') +#敌人别攻击音效 +self.attack_sound = pygame.mixer.Sound(monster_info[\'attack_sound\']) +self.death_sound.set_volume(0.6) +self.hit_sound.set_volume(0.6) +self.attack_sound.set_volume(0.6) +
修改 Enemy 类的 check_death 函数,添加敌人死亡音效
enemy.py
def check_death(self):if self.health <= 0:self.kill()self.trigger_death_particles(self.rect.center,self.monster_name)self.add_exp(self.exp)self.death_sound.play() +
修改 Enemy 类的 get_damage 函数,添加敌人被攻击时的音效
enemy.py
def get_damage(self,player,attack_type):if self.vulnerable:self.hit_sound.play() +self.direction = self.get_player_distance_direction(player)[1] ...
修改 Enemy 类的 actions 函数,添加敌人攻击音效
def actions(self,player):if self.status == \'attack\':self.attack_time = pygame.time.get_ticks()self.damage_player(self.attack_damage,self.attack_type)self.attack_sound.play() +elif self.status == \'move\':self.direction = self.get_player_distance_direction(player)[1]else:self.direction = pygame.math.Vector2()
4.调整碰撞体积
减小玩家和障碍物之间的碰撞体积,让玩家在一些狭窄的地方也可以正常通过
在 settings.py 文件中添加新的碰撞偏移量
FPS = 60 #帧率TILESIZE = 64HITBOX_OFFSET = { +\'player\': -26, #玩家垂直方向碰撞偏移量 +\'object\': -40, #障碍物垂直方向碰撞偏移量 +\'grass\': -10, #小草垂直方向碰撞偏移量 +\'invisible\': 0}#不看见障碍物垂直方向碰撞偏移量 +
修改 Player 类的 init 函数,修改玩家碰撞体积
player.py
class Player(Entity):def __init__(self,pos,groups,obstacle_sprites,create_attack,destroy_attack,create_magic):super().__init__(groups)#加载图片,并确保图像支持透明度self.image = pygame.image.load(\'../graphics/test/player.png\').convert_alpha() #获取图像的矩形区域self.rect = self.image.get_rect(topleft = pos)#创建一个矩形,比原始矩形小26像素,以便在碰撞检测中排除头部self.hitbox = self.rect.inflate(-6,HITBOX_OFFSET[\'player\']) + ...
修改 Tile 类的 init 函数,修改不同障碍物的碰撞体积
tile.py
class Tile(pygame.sprite.Sprite):def __init__(self,pos,groups,sprite_type,surface = pygame.Surface((TILESIZE,TILESIZE))):super().__init__(groups)self.sprite_type = sprite_typey_offset = HITBOX_OFFSET[sprite_type] +self.image = surfaceif sprite_type == \'object\':self.rect = self.image.get_rect(topleft = (pos[0],pos[1] - TILESIZE))else:self.rect = self.image.get_rect(topleft = pos)self.hitbox = self.rect.inflate(0,y_offset) +
还有一个小的修改,现在地图外围边界外是使用黑色填充的,为了让地图更美观,将填充色改为地图边缘水的颜色
在 settings.py 文件中添加新的颜色
...UI_FONT = \'../graphics/font/joystix.ttf\' #经验值字体路径UI_FONT_SIZE = 18 #经验值字体大小WATER_COLOR = \'#71ddee\' #水颜色 +UI_BG_COLOR = \'#222222\' #背景色UI_BORDER_COLOR = \'#111111\' #UI条框颜色...
修改 Game 类 run 函数,修改地图填充的颜色
def run(self):while True: ...# 让最近绘制的屏幕可⻅ 并将屏幕设置为黑色self.screen.fill(WATER_COLOR) +#运行游戏关卡self.level.run() ...
十五:处理玩家死亡
阅读完原作者的代码后发现并没有处理玩家死亡相关逻辑,如果不加总感觉这个游戏不完整,我就抄... 借鉴一下文章开头提到的“外星人入侵”项目重启游戏的方法简单处理一下
如果玩家死亡,就在屏幕中心显示一个按钮,用户点击按钮后从新开始游戏
因为 pygame 库没有提供按钮功能,所以我们要自己简单实现一下,在 code 文件夹下新建 button.py 文件,并创建 Button 类
button.py
import pygamefrom settings import *class Button: def __init__(self, text, width, height): # 获取显示表面 self.display_surface = pygame.display.get_surface() # 初始化按钮的宽度、高度和文本内容 self.width = width self.height = height self.text = text # 初始化字体和字体大小 self.font = pygame.font.Font(UI_FONT, UI_FONT_SIZE) # 创建按钮的矩形区域,并将其居中放置在屏幕中央 window_size = self.display_surface.get_size() window_center = (window_size[0] // 2, window_size[1] // 2) self.rect = pygame.Rect(0, 0, self.width, self.height) self.rect.center = window_center
init 函数的参数为 按钮上显示的内容、按钮的宽和高
为 Button 类添加 display 函数,用于绘制按钮
button.py
def display(self): # 设置按钮的背景色和边框颜色 pygame.draw.rect(self.display_surface,UI_BG_COLOR,self.rect) pygame.draw.rect(self.display_surface,UI_BORDER_COLOR,self.rect,4) # 设置按钮的文本内容和颜色 surface = self.font.render(self.text, False, TEXT_COLOR) surface_rect = surface.get_rect(center=self.rect.center) # 将按钮的文本内容绘制到屏幕上 self.display_surface.blit(surface, surface_rect)
修改 Level 类的 init 函数,初始化 Button 类,并添加一个游戏是是否结束的标志
level.py
...from upgrade import Upgradefrom button import Button +class Level:def __init__(self): ...#创建动画播放器对象self.animation_player = AnimationPlayer()#创建玩家释放技能对象self.magic_player = MagicPlayer(self.animation_player)#游戏是否结束 +self.game_over = False +#创建按钮对象 +self.button = Button(\"Click to start again\", TILESIZE * 5 , TILESIZE * 2) +
修改 Level 类的 damage_player 函数,判断玩家是否死亡,如果死亡,修改 game_over 变量状态
level.py
def damage_player(self,amount,attack_type):if self.player.vulnerable:self.player.health -= amountif self.player.health <= 0: +self.game_over = True +return +self.player.vulnerable = False ...
修改 Game 类的 run 方法,捕获用户鼠标点击的位置,其中 pygame.mouse.get_pos() 方法返回鼠标点击位置的坐标
main.py
def run(self):while True:#侦听键盘和⿏标事件for event in pygame.event.get():if event.type == pygame.QUIT:pygame.quit()sys.exit()if event.type == pygame.KEYDOWN:if event.key == pygame.K_m:self.level.toggle_menu()if event.type == pygame.MOUSEBUTTONDOWN: +mouse_pos = pygame.mouse.get_pos() +self.level.restart(mouse_pos) +# 让最近绘制的屏幕可⻅ 并将屏幕设置为水蓝色self.screen.fill(WATER_COLOR)
为 Level 类添加 restart 函数,用于重置游戏状态,其中 collidepoint 函数用于判断鼠标点击的位置有没有落在按钮矩形区域的内部
def restart(self, mouse_pos):#如果点击区域在按钮区域内if self.button.rect.collidepoint(mouse_pos):#清空可见精灵组self.visible_sprites.empty()#清空障碍物精灵组self.obstacle_sprites.empty()#清空攻击目标精灵组self.attackable_sprites.empty()#从新加载地图数据self.create_map()#从新创建升级对象self.upgrade = Upgrade(self.player)#重置游戏结束标志self.game_over = False
修改 Level 类的 run 方法,判断游戏是否结束,如果游戏结束,绘制按钮
def run(self):self.visible_sprites.custom_draw(self.player)self.ui.display(self.player)if self.game_over: +self.button.display() +elif self.game_paused: +#绘制升级菜单self.upgrade.display()else ...
到这里这个小游戏的开发就算全部完成了,如果你从头看到了这里,哪恭喜你超越了 97.648% 阅读这篇文章的读者。当然游戏还有不少需要补充的地方,比如击杀所有的敌人游戏并不会结束,或者随着玩家等级的提高增加敌人的强度,再或者可以持续刷新敌人等等,感兴趣的话大家可以自己实现
最后我在这里舔着脸要个赞,虽然文章质量很一般,但我也确实肝了很长时间,如果文章数据不错的话,我就给游戏再增加一个地图切换功能,提高游戏的可玩性~