0%

利用pygame开发一款游戏:「跳跳兔」(三)

简介

pygame如何通过键盘来控制游戏中的元素呢?元素之间是如何进行碰撞检测的呢?

阅读完本节你就很清晰了,此外本文还会整理出pygame开发游戏的通用整体结构,该系列后续的内容都以这个结构来编写。

整体结构

随着游戏项目的复杂化,有必要整理一下代码,形成一个统一的风格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import pygame as pg

class Game:
# 初始化窗口、计数器
def __init__(self):
pass

# 开启新的游戏
def new(self):
pass

# 游戏中的大循环
def run(self):
pass

# 更新
def update(self):
pass

# 事件
def events(self):
pass

# 绘制界面
def draw(self):
pass

# 游戏启动 / 开始画面的钩子函数
def show_start_screen(self):
# game splash/start screen
pass

# 游戏结束 / 继续的钩子函数
def show_go_screen(self):
pass

# 入口逻辑
g = Game()
g.show_start_screen()
while g.running:
g.new()
g.show_go_screen()

pg.quit()

将不同的步骤封成不同的方法,这样让Game类会简单直观很多,不同方法的细节如下

__init__中,初始化各种基本对象,如pygame对象、计数器对象等待

1
2
3
4
5
6
7
8
9
10
# main.py/Game

def __init__(self):
# 初始化窗口、计数器
pg.init()
pg.mixer.init()
self.screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption(TITLE)
self.clock = pg.time.Clock()
self.running = True

在new()方法中,新游戏开始时,进行元素的初始化,并调用run()方法,运行游戏的主循环。

1
2
3
4
5
6
7
8
# main.py/Game

# 开启新的游戏
def new(self):
self.all_sprites = pg.sprite.Group()
self.player = Player()
self.all_sprites.add(self.player)
self.run()

run()方法就很简单了,主要就是调用相应的方法设置帧率、处理事件输入、更新状态与绘制渲染游戏等,代码如下

1
2
3
4
5
6
7
8
9
10
# main.py/Game

# 游戏中的大循环
def run(self):
self.playing = True
while self.playing:
self.clock.tick(FPS) # 设置帧率
self.events() # 事件输入处理
self.update() # 状态更新
self.draw() # 绘制

对应方法代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def update(self):
# 更新所有元素的状态
self.all_sprites.update()

def events(self):
# 获得事件输入
for event in pg.event.get():
# 判断是否是退出事件
if event.type == pg.QUIT:
if self.playing:
self.playing = False
self.running = False

def draw(self):
# 绘制与渲染游戏背景
self.screen.fill(BLACK)
self.all_sprites.draw(self.screen) # 绘制所有元素到界面中
pg.display.flip()

代码内容在前面文章中介绍过,不再赘述。

控制玩家元素

控制玩家元素其实非常简单,核心就是利用pg.key.get_pressed()方法获得键盘的输入,然后判断敲击的键位是不是需要的键位,如果是,则移动元素。

但还需要考虑的是如何移动元素?想此前简单的对元素x轴或y轴方向添加位移距离是不适合的,比如我就敲了一下左键,元素就一直往左移动,不会停下来,这不符合常规,应该要实现,只有一直按着左键,元素才能一直向左移动。

为了实现上述的效果,就需要引入摩擦系数,模仿现实世界,我们之所以走一步不会一直滑下去就是因为存在摩擦力,这里我们使用摩擦系数来模拟这个过程。

下面通过代码来实现一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import pygame as pg
from settings import *
vec = pg.math.Vector2 # 二维变量

# 在settings.py中,为了方便理解,贴一下
# PLAYER_ACC = 0.5 # 加速度
# PLAYER_FRICTION = -0.12 # 摩擦系数

class Player(pg.sprite.Sprite):
def __init__(self):
pg.sprite.Sprite.__init__(self)
# 创建一个黄色方块作为玩家元素
self.image = pg.Surface((30, 40))
self.image.fill(YELLOW)
self.rect = self.image.get_rect()
# 初始化在游戏框中心
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.pos = vec(WIDTH / 2, HEIGHT / 2) # 创建一个二维变量并将其初始化为游戏框的中心位置
self.vel = vec(0, 0) # 记录速度
self.acc = vec(0, 0) # 记录加速度

def update(self):
self.acc = vec(0, 0)
keys = pg.key.get_pressed() # 获得键盘输入
if keys[pg.K_LEFT]: # 按左
self.acc.x = -PLAYER_ACC # 加速度,正负号表示方向
if keys[pg.K_RIGHT]: # 按右
self.acc.x = PLAYER_ACC

# 计算加速度
# 当前加速度 = (上一帧的速度 * 摩擦系数) + 上一帧加速度
self.acc += self.vel * PLAYER_FRICTION
# 当前速度 = 当前加速度 + 上一帧的速度
self.vel += self.acc
# 有了当前速度与加速度,就可以计算移动距离了
self.pos += self.vel + 0.5 * self.acc
# 跑马灯效果
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH
# 移动玩家元素
self.rect.center = self.pos

__init__()方法中,除了初始化玩家元素为一个黄色长方形外,还利用pg.math.Vector2初始化了多个二维变量,pygame的math模块还支持三维变量的初始化。

初始化多个二维变量的目的在于计算当前帧的移动距离,其基本物理公式可以参考下图。

再看到update()方法,计算移动距离其实就是套用了上图红色框中的公式,只是时间t为1。时间t之所以可以为1,是因为我们每一帧都在这部分逻辑,将每一帧时间作为1则可。

其余的具体逻辑,请阅读详细的注释,注释比文字好理解。

最后,将计算出来的新位置self.pos赋值给玩家元素的中心点,从而实现玩家元素的移动,玩家中心点的理解可以参考下图。

碰撞检测

通过上两节的代码,已经可以实现通过键盘空中游戏框中黄色方块的效果,但感觉有点单调,是否可以让元素在某个平台上运行呢?

具体而言就是实现下面的效果,黄色的玩家元素在绿色的平台上站立移动,如果超出绿色小平台的范围,会自动的落下到下面的绿色大平台。

wow,跟「跳跳兔」游戏有点关系了。

首先,绿色平台本身也是一个元素,所以在开始新游戏时,需要初始化好这个元素,具体逻辑写到new()方法中则可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# main.py/Game

# 开始新的游戏
def new(self):
self.all_sprites = pg.sprite.Group() # 所有元素组
self.platforms = pg.sprite.Group() # 平台元素组
self.player = Player() # 实例化玩家对象
self.all_sprites.add(self.player) # 添加到所有元素组
p1 = Platform(0, HEIGHT - 40, WIDTH, 40) # 实例化平台元素, 绿色小平台
self.all_sprites.add(p1) # 添加到所有元素组
self.platforms.add(p1) # 添加到平台元素组
p2 = Platform(WIDTH / 2 - 50, HEIGHT * 3 / 4, 100, 20) # 绿色大平台
self.all_sprites.add(p2)
self.platforms.add(p2)
self.run() # 运行

# 为了方便,展示一下Platform类对应的代码
# sprites.py

class Platform(pg.sprite.Sprite):
def __init__(self, x, y, w, h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w, h))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y

代码中有详细的注释,你可能对创建一个平台元素组的做法带有疑惑,平台对象为什么既要添加到所有元素组中又有添加到平台元素组中?

这是为了方便做碰撞检测。

通常,碰撞检测的逻辑会写到游戏中元素状态更新后,判断更新后的状态是否发生了碰撞,所谓碰撞就是不同的两个元素某部分接触到了,在pygame中使用pg.sprite.spritecollide()方法就可以判断出来了。

看一下具体的代码

1
2
3
4
5
6
7
8
9
10
11
# main.py/Game

def update(self):
self.all_sprites.update()
# 碰撞检测,检测玩家对象self.player 与 平台元素组中的所有平台对象是否发生碰撞
hits = pg.sprite.spritecollide(self.player, self.platforms, False)
if hits: # 发生碰撞
# 玩家对象的y轴位置等于碰撞对象的顶部
self.player.pos.y = hits[0].rect.top
# 玩家对象y轴速度为0,即不再移动
self.player.vel.y = 0

看到碰撞检测的相关逻辑,玩家对象要与所有的平台对象进行碰撞检测,利用平台元素组就可以轻松搞定,这也是new()中平台对象既要添加到所有元素组中又有添加到平台元素组中的原因。

如果发生了碰撞,则玩家对象的y轴位置等于碰撞对象的顶部,玩家对象y轴速度为0,即不再移动。

为了让效果正常,我们还需要修改一下玩家类的update()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# spirtes.py/Player

def update(self):
self.acc = vec(0, 0.5) # x轴没有加速度,而y轴有初始加速度,让物体可以自由落下
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.acc.x = -PLAYER_ACC
if keys[pg.K_RIGHT]:
self.acc.x = PLAYER_ACC

self.acc.x += self.vel.x * PLAYER_FRICTION
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH
# 移动时,以元素底部为基准 - 与碰撞检测那里相互呼应
self.rect.midbottom = self.pos

通过下面的图来辅助理解。

速度分为x轴方向与y轴方向,因为y轴方向需要做碰撞检测,如果self.pos赋值给玩家元素的中心,即center,那么就会出现穿透现象。(在碰撞检测时,操作的是pos与val属性,而此时又将pos赋值给玩家元素的center,必然会出现穿透现象。)

本节给出了比较多的内容,讨论元素的控制,基本运动的实现逻辑以及碰撞检测等,至此已经构建出了「跳跳兔」最基本的原型。

如果你觉得文章还不错,点击「在看」支持一下作者。

今天一日一题不弄了,花点时间准备录「动态规划」的视频。