几十年来,平台游戏一直是深受玩家喜爱的游戏类型,它提供了令人兴奋的挑战和令人怀念的游戏玩法。在本教程中,我们将指导您使用 Python 中的 PyGame 库构建自己的平台游戏。
无论您是希望深入游戏开发的初学者,还是希望探索 Pygame 的资深程序员,本教程都将为您提供实现平台游戏创意所需的知识和技能。
在本教程中,我们将介绍平台游戏的基本组成部分,包括:
- 安装和设置
- 添加基本游戏组件
- 游戏循环
- 游戏世界
- 世界设置
- 添加世界功能
- 移动和处理碰撞
- 处理世界陷阱
- 更新世界变化
- 世界组件:瓦块、目标和陷阱
- 添加玩家
- 玩家动画
- 添加玩家能力
- 识别玩家活动
- 更新玩家
- 应用游戏目标
- 添加游戏指标
- 游戏检查
- 总结
我们将指导您逐步完成整个过程,并解释每个功能背后的代码和概念。
本教程结束时,您将拥有一个可以自豪地与他人分享的游戏,并可以根据自己的喜好进一步定制。话不多说,让我们开始吧。
安装和设置
要为名为 "Platformer-Game "的 Pygame 项目设置开发环境,请执行以下步骤:
- 确保在设备上安装了 Python 和 Pygame。您可以从它们的官方网站下载,或使用
pip install pygame
(Pygame)和sudo apt install python
(Linux 和 Mac)命令进行安装。 - 创建一个名为
Platformer-Game
的游戏目录。 - 在该目录下创建游戏开发所需的 Python 文件:
game.py
、goal.py
、main.py
、player.py
、settings.py
、support.py
、tile.py
、trap.py
和world.py
。
接下来我们需要的是游戏资产,也就是我们要用于游戏动画的媒体文件。在这款游戏中,我们将使用图片来制作目标点、生命(心脏)、玩家角色(运行、跳跃、下落、空闲和输赢状态)、地形(背景和地形块)和游戏陷阱的动画。以下是我们游戏资产的文件结构:
下面是游戏目录下的文件结构:
上图中的每个子文件夹(如goal
、life
、fall
、idle
等)都包含特定动画的精灵表或一系列图片。至于游戏的游戏资产,你可以从这里下载使用我的游戏资产。
添加基本游戏组件
现在我们有了游戏所需的软件包和文件,就可以开始编码了。首先,让我们在 settings.py
中添加游戏所需的基本配置,例如游戏窗口的宽度和高度,以及游戏中方块和其他对象的tile_size
。我们还要在 world_map
中配置游戏世界的地图或游戏障碍物。
有了这张地图,您就可以使用下面的相同图例来定制和创建不同的世界障碍。如果您感到困惑,以下是每个字符的定义: a. " "
- 空白 b. "X"
- 块 c. "s"
- 锯子/刀片(陷阱) d. "p"
- 玩家 e. "G"
- 为完成游戏任务而需要到达的目标/点:
python
# settings.py
world_map = [
' ',
' ',
' t t ',
' X XXXXXXXXXs XX X ',
' tXXXt XX XX XXXX tt XX ',
' XX XX XXXXX ',
' Xt t t t X G ',
' XXXXXX XXXXs XXXXXXXXXXX XX tt t XXX',
' P XX X XX X X XXXt X XX XX XXX XXXXXXXXs XXXXXX ',
'XXXXXXX X X X X XXXXXXXXX XX XX XXX XX XX XXXXXXX X ',
]
tile_size = 50
WIDTH, HEIGHT = 1000, len(world_map) * tile_size
我们还要创建一个函数,用于加载精灵或精灵表,为游戏中的对象制作动画。在 support.py
中创建一个函数并调用 import_sprite()
:
python
# support.py
from os import walk
import pygame
def import_sprite(path):
surface_list = []
for _, __, img_file in walk(path):
for image in img_file:
full_path = f"{path}/{image}"
img_surface = pygame.image.load(full_path).convert_alpha()
surface_list.append(img_surface)
return surface_list
import_sprite()
函数加载一个目录中的所有图像,并返回给定目录中的曲面列表,这些曲面可用于绘制游戏图形和动画。
该函数使用 os.walk()
函数遍历指定目录中的文件。对于每个文件,它会通过连接目录路径和文件名来创建完整路径。接下来,代码会使用 pygame.image.load()
加载图像,并使用 convert_alpha()
将其转换为 alpha 曲面。然后将生成的曲面追加到曲面列表中。目录中的每个文件都要重复这一过程。导入所有曲面后,函数将返回 surface_list
。
游戏循环
让我们在 main.py
中创建第一个类,并将其命名为 Platformer
类。Platformer
类负责运行游戏、管理游戏循环、处理事件和更新游戏世界:
python
# main.py
import pygame, sys
from settings import *
from world import World
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Platformer")
class Platformer:
def __init__(self, screen, width, height):
self.screen = screen
self.clock = pygame.time.Clock()
self.player_event = False
self.bg_img = pygame.image.load('assets/terrain/bg.jpg')
self.bg_img = pygame.transform.scale(self.bg_img, (width, height))
def main(self):
world = World(world_map, self.screen)
while True:
self.screen.blit(self.bg_img, (0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
self.player_event = "left"
if event.key == pygame.K_RIGHT:
self.player_event = "right"
if event.key == pygame.K_SPACE:
self.player_event = "space"
elif event.type == pygame.KEYUP:
self.player_event = False
world.update(self.player_event)
pygame.display.update()
self.clock.tick(60)
if __name__ == "__main__":
play = Platformer(screen, WIDTH, HEIGHT)
play.main()
使用 pygame.display.set_mode()
函数创建一个 Pygame 窗口,尺寸由 WIDTH
和 HEIGHT
常量指定,并使用 pygame.display.set_caption()
将窗口标题设置为 "Platformer"
。
接下来,Platformer
类有一个 __init__()
方法,用于初始化各种属性,并从 "assets/terrain/bg.jpg "
文件加载和缩放背景图片。
main()
方法是游戏循环所在。它会创建一个 World
类实例,并将 world_map
和 screen
作为参数传递。游戏循环无限期运行,直到窗口关闭。
在游戏循环中,使用 screen.blit()
将背景图片绘制到屏幕上。然后,循环遍历从 pygame.event.get()
中获取的事件。对于 KEYDOWN
事件,代码会检查按下的是哪个键。如果是左箭头键,player_event
就会被设置为 "left"
。如果是右方向键,player_event
将被设置为 "右"。如果是空格键,player_event
会被设置为 "空格"。对于 KEYUP
事件,player_event
会被设置为 "假",从而停止当前事件。
然后使用当前的 player_event
值调用 world.update()
方法,让游戏世界根据玩家的输入进行更新。然后,使用 pygame.display.update()
更新屏幕,绘制变化,并使用 self.clock.tick(60)
对游戏时钟进行计时,将帧速率调节为 60 FPS。
游戏世界
接下来,我们将创建之前在 Main
类中导入和使用的 World
类。World
类负责创建和管理游戏世界及游戏对象,还负责处理游戏对象与玩家之间的交互:
python
# world.py
import pygame
from settings import tile_size, WIDTH
from tile import Tile
from trap import Trap
from goal import Goal
from player import Player
from game import Game
class World:
def __init__(self, world_data, screen):
self.screen = screen
self.world_data = world_data
self._setup_world(world_data)
self.world_shift = 0
self.current_x = 0
self.gravity = 0.7
self.game = Game(self.screen)
我们首先要导入游戏中要用到的东西。我们将很快创建 Tile
、Trap
、Goal
、Player
和 Game
类。在 World
类中,我们将 world_data
和 screen
作为参数。world_data
是我们之前在 settings.py
中创建的 world_map
,screen
是我们的游戏窗口,因此我们可以直接在游戏窗口中绘制动画。world_shift
和 current_x
变量用于滚动背景,让玩家可以连续行走,而不会脱离游戏屏幕的视线。gravity
包含下拉力的强度,而 game
则包含游戏
的规则和目标。
世界设置
现在让我们添加一个新函数,从 world_data
中创建整个障碍物,我们将新函数命名为 _setup_world()
:
python
# world.py
# generates the world
def _setup_world(self, layout):
self.tiles = pygame.sprite.Group()
self.traps = pygame.sprite.Group()
self.player = pygame.sprite.GroupSingle()
self.goal = pygame.sprite.GroupSingle()
for row_index, row in enumerate(layout):
for col_index, cell in enumerate(row):
x, y = col_index * tile_size, row_index * tile_size
if cell == "X":
tile = Tile((x, y), tile_size)
self.tiles.add(tile)
elif cell == "t":
tile = Trap((x + (tile_size // 4), y + (tile_size // 4)), tile_size // 2)
self.traps.add(tile)
elif cell == "P":
player_sprite = Player((x, y))
self.player.add(player_sprite)
elif cell == "G":
goal_sprite = Goal((x, y), tile_size)
self.goal.add(goal_sprite)
_setup_world() 方法是在 init() 方法中调用的辅助函数,用于根据提供的布局参数配置游戏世界。该方法初始化多个精灵组,包括瓷砖、陷阱、玩家和目标,这些精灵组将保存不同游戏对象的实例。
接下来,该方法会遍历所提供的布局,遍历每一行和每一列。对于布局中的每个单元格,都会根据其值采取特定的操作。如果单元格为 "X",则会在相应位置创建一个瓷砖实例,并添加到瓷砖精灵组中。如果单元格为 "s",则会创建一个陷阱实例并添加到陷阱精灵组中。如果单元格为 "P",则会创建一个播放器实例并添加到播放器精灵组中。最后,如果单元格为 "G",则会创建一个目标实例并添加到目标精灵组中。
添加世界功能
让我们在 "世界 "类中再添加两个函数,一个用于在玩家行走时处理世界卷轴,另一个用于添加游戏重力:
python
# world.py
# world scroll when the player is walking towards left/right
def _scroll_x(self):
player = self.player.sprite
player_x = player.rect.centerx
direction_x = player.direction.x
if player_x < WIDTH // 3 and direction_x < 0:
self.world_shift = 8
player.speed = 0
elif player_x > WIDTH - (WIDTH // 3) and direction_x > 0:
self.world_shift = -8
player.speed = 0
else:
self.world_shift = 0
player.speed = 3
# add gravity for player to fall
def _apply_gravity(self, player):
player.direction.y += self.gravity
player.rect.y += player.direction.y
_scroll_x() 方法负责根据玩家的位置和方向控制游戏世界的水平滚动。为了确定是否需要滚动,该方法会检查玩家相对于屏幕宽度(WIDTH)的位置。如果玩家位于屏幕左侧(player_x < WIDTH // 3)并向左移动(direction_x < 0),则 world_shift 属性将被设置为正值(8)。此外,玩家的速度会被设置为 0,从而有效地暂停其移动。同样,如果玩家位于屏幕右侧(player_x > WIDTH - (WIDTH // 3))并向右移动(direction_x > 0),则 world_shift 属性将被设置为负值(-8)。这将导致游戏世界向相反方向移动,给人一种玩家在向前移动的感觉。同样,玩家的速度会被设置为 0 以停止移动。在所有其他情况下,当玩家处于屏幕中间部分时,既不会滚动也不会调整速度。world_shift 属性设置为 0,表示没有水平移动,玩家的速度设置为 3,允许他们在游戏世界中自由移动。
接下来是_apply_gravity()方法,其目的是为玩家的精灵施加重力。重力值(self.gravity)会增加玩家的 direction.y 属性,使玩家逐渐向下坠落。此外,玩家的 rect.y 属性也会根据 player.direction.y 的值进行更新,以确保玩家的视觉表现能反映重力导致的位置变化。
移动和处理碰撞
碰撞在这款游戏中非常重要,因为它们能使游戏对象(如玩家角色和障碍物或敌人)之间产生互动。它们能确保正确的游戏机制,允许避开障碍物、与敌人互动以及准确检测游戏进程或失败。让我们先创建一个处理水平碰撞的函数:
python
# world.py
# prevents player to pass through objects horizontally
def _horizontal_movement_collision(self):
player = self.player.sprite
player.rect.x += player.direction.x * player.speed
for sprite in self.tiles.sprites():
if sprite.rect.colliderect(player.rect):
# checks if moving towards left
if player.direction.x < 0:
player.rect.left = sprite.rect.right
player.on_left = True
self.current_x = player.rect.left
# checks if moving towards right
elif player.direction.x > 0:
player.rect.right = sprite.rect.left
player.on_right = True
self.current_x = player.rect.right
if player.on_left and (player.rect.left < self.current_x or player.direction.x >= 0):
player.on_left = False
if player.on_right and (player.rect.right > self.current_x or player.direction.x <= 0):
player.on_right = False
_horizontal_movement_collision() 方法用于处理水平移动时玩家与游戏世界中的地砖之间的碰撞。玩家的位置会根据其方向(player.direction.x)和速度(player.speed)进行水平更新。
接下来,循环遍历瓷砖精灵组,该组包含代表游戏环境的瓷砖对象。对于组中的每个精灵(瓷砖),该方法都会使用 colliderect() 方法检查其矩形(sprite.rect)是否与玩家的矩形(player.rect)相交。
如果检测到碰撞,该方法将确定玩家的移动方向。如果玩家向左移动(player.direction.x < 0),其左侧就会与瓷砖的右侧对齐(player.rect.left = sprite.rect.right)。此外,还会设置一个标志(player.on_left)来表示玩家正在接触左侧的瓷砖,并相应更新 current_x 位置。同样,如果玩家正在向右移动(player.direction.x > 0),其右侧就会与瓷砖的左侧对齐(player.rect.right = sprite.rect.left)。player.on_right 标志被设置,current_x 位置也会更新。
处理完碰撞后,还需要检查更新 player.on_left 和 player.on_right 标志。如果播放器之前位于磁贴的左侧(player.on_left 为 True),但其当前位置不再位于 current_x 位置的左侧,或者其移动方向不是朝左,则播放器.on_left 标志将设为 False。更新 player.on_right 标志的过程也是如此。
接下来,让我们添加另一个函数来处理垂直方向的碰撞。在处理水平碰撞的函数下面,我们再创建一个函数,并将其命名为 _vertical_movement_collision():
python
# world.py
# prevents player to pass through objects vertically
def _vertical_movement_collision(self):
player = self.player.sprite
self._apply_gravity(player)
for sprite in self.tiles.sprites():
if sprite.rect.colliderect(player.rect):
# checks if moving towards bottom
if player.direction.y > 0:
player.rect.bottom = sprite.rect.top
player.direction.y = 0
player.on_ground = True
# checks if moving towards up
elif player.direction.y < 0:
player.rect.top = sprite.rect.bottom
player.direction.y = 0
player.on_ceiling = True
if player.on_ground and player.direction.y < 0 or player.direction.y > 1:
player.on_ground = False
if player.on_ceiling and player.direction.y > 0:
player.on_ceiling = False
_vertical_movement_collision()方法用于处理垂直移动时玩家与游戏世界中的瓷砖之间的碰撞,它与前一个方法相同。
处理世界陷阱
为 "世界 "类添加另一个方法,并将其命名为 _handle_traps(),该函数将负责给出玩家穿过或触及陷阱的后果:
python
# world.py
# add consequences when player run through traps
def _handle_traps(self):
player = self.player.sprite
for sprite in self.traps.sprites():
if sprite.rect.colliderect(player.rect):
if player.direction.x < 0 or player.direction.y > 0:
player.rect.x += tile_size
elif player.direction.x > 0 or player.direction.y > 0:
player.rect.x -= tile_size
player.life -= 1
该方法首先遍历陷阱精灵组,该组包含游戏环境中的陷阱对象。对于组中的每个精灵(陷阱),该方法都会使用 colliderect() 方法检查其矩形(sprite.rect)是否与玩家的矩形(player.rect)相交。
如果检测到碰撞,该方法会检查玩家的移动方向。如果玩家向左移动(player.direction.x < 0)或向下移动(player.direction.y > 0),就会调整其位置,向右移动一个瓦片的宽度(player.rect.x += tile_size)。这样可以防止玩家不断与陷阱碰撞,从而可能被卡住。同样,右边的方向也是如此。这种调整有助于避免连续碰撞和潜在问题。
位置调整后,玩家的生命值会减少 1(player.life -=1)。这代表陷阱造成的惩罚或伤害,减少了玩家的生命值或健康值。
更新世界变化
现在我们已经准备好了所有的更改和操作,是时候添加一个方法来根据用户所做的更改更新整个游戏世界了:
python
# world.py
# updating the game world from all changes committed
def update(self, player_event):
# for tile
self.tiles.update(self.world_shift)
self.tiles.draw(self.screen)
# for trap
self.traps.update(self.world_shift)
self.traps.draw(self.screen)
# for goal
self.goal.update(self.world_shift)
self.goal.draw(self.screen)
self._scroll_x()
# for player
self._horizontal_movement_collision()
self._vertical_movement_collision()
self._handle_traps()
self.player.update(player_event)
self.game.show_life(self.player.sprite)
self.player.draw(self.screen)
self.game.game_state(self.player.sprite, self.goal.sprite)
_handle_traps()方法用于检测玩家与陷阱对象之间的碰撞并做出响应。它会调整玩家的位置,并在必要时施加惩罚或伤害。
玩家的 update() 方法会被调用,根据提供的输入处理玩家的移动和动画。调用 Game 类的 show_life() 方法,可以使用玩家精灵作为参照,在屏幕上显示玩家的剩余生命或健康状况。玩家精灵被绘制在屏幕上,以便在游戏世界中直观地显示出来。
最后,self.game.game_state() 方法会被调用,以根据玩家的位置和与目标对象的交互来确定和处理当前的游戏状态。
世界组件:瓦块、球门和陷阱
世界类完美地处理了游戏世界。现在,让我们为游戏中的每个游戏对象创建更多的类。
让我们在 tile.py 中创建表示游戏方块的 Tile 类:
python
# tile.py
import pygame
class Tile(pygame.sprite.Sprite):
def __init__(self, pos, size):
super().__init__()
img_path = 'assets/terrain/stone.jpg'
self.image = pygame.image.load(img_path)
self.image = pygame.transform.scale(self.image, (size, size))
self.rect = self.image.get_rect(topleft=pos)
# update object position due to world scroll
def update(self, x_shift):
self.rect.x += x_shift
瓦片类有一个 init() 方法,它接收两个参数:pos(位置)和 size(瓦片的大小)。我们将瓷砖的图像路径定义为 "assets/terrain/stone.jpg"。瓷砖的图像使用 pygame.image.load() 加载,并分配给图像属性。使用 pygame.transform.scale() 将加载的图像缩放到指定大小,并存储为 image 属性的新值。
使用 get_rect(),将 Tile.rect 属性设置为包围图像的矩形,并将 topleft 参数设置为 pos。
定义 update() 方法是为了在世界滚动时处理磁贴位置的更新。
让我们为游戏对象创建另一个类,在 goal.py 中创建一个名为 Goal 的新类。
python
# goal.py
import pygame
class Goal(pygame.sprite.Sprite):
def __init__(self, pos, size):
super().__init__()
img_path = 'assets/goal/gate.png'
self.image = pygame.image.load(img_path)
self.image = pygame.transform.scale(self.image, (size, size))
self.rect = self.image.get_rect(topleft = pos)
# update object position due to world scroll
def update(self, x_shift):
self.rect.x += x_shift
正如您所注意到的,我们与 Tile 类和 Goal 类的代码是相似的,图像是它们唯一不同的地方,这是因为它们在游戏中的行为相似。
在 trap.py 中创建另一个游戏对象类,并将其命名为 Trap 类。该类负责动画和处理游戏陷阱:
python
# trap.py
import pygame
from support import import_sprite
class Trap(pygame.sprite.Sprite):
def __init__(self, pos, size):
super().__init__()
self.blade_img = import_sprite("assets/trap/blade")
self.frame_index = 0
self.animation_delay = 3
self.image = self.blade_img[self.frame_index]
self.image = pygame.transform.scale(self.image, (size, size))
self.mask = pygame.mask.from_surface(self.image)
self.rect = self.image.get_rect(topleft = pos)
# adds the spinning effect to the Blade trap
def _animate(self):
sprites = self.blade_img
sprite_index = (self.frame_index // self.animation_delay) % len(sprites)
self.image = sprites[sprite_index]
self.frame_index += 1
self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y))
self.mask = pygame.mask.from_surface(self.image)
if self.frame_index // self.animation_delay > len(sprites):
self.frame_index = 0
# update object position due to world scroll
def update(self, x_shift):
self._animate()
self.rect.x += x_shift
Trap 类的代码与 Block 类和 Goal 类类似,只是它有一个 _animate() 方法。我们创建了 import_sprite()函数,该函数用于导入目录中的所有图像文件,并以图像表面列表的形式返回所有文件,我们将用它来加载 assets/trap/blade 目录中的所有图像。
定义 _animate() 方法是为了给刀片陷阱添加旋转效果。它根据 frame_index 循环播放 blade_img 中的精灵图像,并相应地更新图像、矩形和遮罩属性。如果动画已完成一个完整的循环,则 frame_index 将重置为 0。
感谢大家花时间阅读我的文章,你们的支持是我不断前进的动力。期望未来能为大家带来更多有价值的内容,请多多关注我的动态!