一帧六件事的顺序:收输入、移动、判纵向、判横向、镜头、绘制。 落地和撞墙都要「把越界的量抹掉、把速度清零」,光判不抹会陷进去。 踩敌人还是被敌人撞,判据只有一个:脚底在不在头顶附近,还要留一点容差。
这是全书的最后一篇正文。角色的零件都齐了,这一篇把它们串成一台能玩的机器。 教材 7.15 到 7.18 节:【游戏主体控制】【音乐与音效】【异常处理】【小结】。 四节都在回答一个问题:每帧到底按什么顺序做什么。 落地的帧数、越界的像素、贴墙的位置,全是真跑的。
一、一帧里六件事
| 次序 | 做什么 | 乱了会怎样 |
|---|---|---|
| 一 | 收输入,算出想怎么动 | 按键慢一帧 |
| 二 | 按速度更新位置 | 位置和画面不同步 |
| 三 | 先判纵向,再判横向 | 会卡进墙里,或者穿过地面 |
| 四 | 处理「撞到的是什么」 | 踩敌人变成被敌人撞 |
| 五 | 镜头跟随 | 画面一顿一顿 |
| 六 | 绘制 | 还没算完就画,画面闪 |
这六步跟第三章飞机大战那四步是一回事,只是把「碰撞」拆成了「判纵向」和「判横向」两步。 因为马里奥会跳、会贴着墙,两个方向必须分开判。 第三第四步最容易写反。先移动后判定,判的是这一帧的新位置,才对得上画面。 反过来的话,玩家看到的是「隔一帧才撞上」。
二、为什么先判纵向
角色同时往右和往下动,一帧里可能同时撞墙和落地。两个方向挤在一起,就得分个先后。 先判纵向的意思是: 先看这一帧的垂直移动有没有撞到东西,有就把 y 拉回来、垂直速度清零;再看水平移动。 反过来(先横后纵)的典型症状是「从墙边跳上去,卡进墙里」——因为横向先被墙挡住, 纵向的修正就基于一个错误的位置算了。
两种顺序都有人用,关键是整个项目统一。混着用必出「有时候能穿墙」这种偶发 bug——最难查的那种。
三、落地:判到之后要拉回来
演示里角色从 y = 100 自由下落,平台顶在 192,角色高 30。第 17 帧落地,y = 163.8。
注意这时候脚底在 193.8,已经越过平台顶 1.8 像素了。
这 1.8 像素必须抹掉:把 y 拉回 192 - 30 = 162,垂直速度清零。
不拉回来会怎样?角色会稳定陷进地面一两像素。
看起来只是「站得低了一点」。但下一个平台的判定会因此算错,
最终表现是「走到某个地方突然掉下去」。
这条规律对撞墙同样成立:演示里角色往右走到 x = 426.7,右边缘 450.7,墙在 448,越过 2.7 像素,
钳到 448 - 24 = 424。
记住这一句:判到之后,越界的量要抹掉,速度要清零。光判不抹,就是「一点点蹭进去」。
四、踩敌人还是被敌人撞
同一帧里,马里奥和板栗仔的框重叠了。到底算踩还是算撞? 判据只有一个:马里奥的脚底在板栗仔的头顶附近,就是踩中。演示里写的是「脚底 < 头顶 + 8」。 那个 +8 是容差。 没有它,玩家会觉得「明明踩上了却死了」——因为判定要精确到一像素,而人眼分辨不了那么细。
容差给多少?大约角色高度的四分之一。太小了踩不中,太大了会「隔空踩」。 踩中之后还有两件事:板栗仔死掉、马里奥往上弹一下。 弹那一下很重要——没有它,踩完会继续往下掉,落到第二个敌人头上就变成撞了。
五、通关
通关判定最简单:马里奥的框碰到旗杆的框。就一句,放在主循环第四步里。 碰到之后做什么?停掉所有输入、播通关音乐、把状态切到结算场景。三件事,一件都不能少。 少停输入的表现是:通关动画播着,玩家还能控制角色往右走,走出地图。 这种 bug 在测试时很容易漏,因为没人会在通关动画里乱按键。
切场景用 replace 不用 push——通关之后不需要回到游戏场景。
六、异常处理
这一章要兜住三处:素材缺失、地图越界、声音加载失败。
兜住的意思是「记一条日志,然后跳过这个对象」,不是「整个程序崩掉」。一个金币的图没找到,
不该让整关玩不了。
地图越界最典型:角色走到关卡最右边,board[row][col] 直接 IndexError。
越界一律当墙,这一条第三篇就说过,这里是它真正救命的地方。
声音加载失败也常见——玩家把音量设备拔了、系统没有音频驱动。加载失败就当静音玩,别让游戏起不来。
顺带说一句:异常处理不是「到处 try」。只在「可能失败、失败了也能继续」的地方加,
而且要写清失败了怎么办。空着的 except: pass 比不写还危险。
七、动手 Demo:主循环:落地、撞墙、踩敌人
先看一帧六件事的顺序表,再真跑一次落地,看第几帧、越界了多少像素。 然后跑一次撞墙,同样看越界量怎么抹掉。 最后把「踩中」和「撞上」的判据摊开。
TILE, W = 32, 800
PLATFORM = [(10, 6), (11, 6), (12, 6), (13, 6)]
WALL = [(14, 5), (14, 6), (14, 7)]
def solid(tx, ty):
return (tx, ty) in PLATFORM or (tx, ty) in WALL
def info(s):
print(' ' + s)
def sec(t):
print(); print(' ' + t); print(' ' + '-' * 68)
dt = 1 / 60.0
# ① 落地
sec('① 自由下落,第几帧落地')
y, vy, g, foot = 100.0, 0.0, 1500.0, 30
top = 6 * TILE
frame = 0
while y + foot < top:
vy += g * dt
y += vy * dt
frame += 1
info('第 %d 帧落地:y = %.1f,脚底 %.1f,平台顶 %d'
% (frame, y, y + foot, top))
info('越界了 %.1f 像素,要拉回到 y = %d' % (y + foot - top, top - foot))
# ② 撞墙
sec('② 往右撞墙')
x, vx, w = 300.0, 200.0, 24
wall_left = 14 * TILE
step = 0
while x + w < wall_left:
x += vx * dt
step += 1
info('第 %d 帧贴上墙:x = %.1f,右边缘 %.1f,墙在 %d'
% (step, x, x + w, wall_left))
info('越界 %.1f 像素,钳到 x = %d' % (x + w - wall_left, wall_left - w))
# ③ 踩还是撞
sec('③ 踩中还是撞上')
top_c = 180
for foot_y in (178, 182, 186, 190, 200):
stomp = foot_y < top_c + 8
info('脚底在 %3d -> %s' % (foot_y, '踩中' if stomp else '撞上'))
实跑输出:
--------------------------------------------------------------------
主循环、碰撞与通关
--------------------------------------------------------------------
① 一帧里做的事,顺序不能乱
--------------------------------------------------------------------
| 次序 | 做什么 | 乱了会怎样 |
|---|---|---|
| 一 | 收输入,算出想怎么动 | 按键慢一帧 |
| 二 | 按速度更新位置 | 位置和画面不同步 |
| 三 | 先判纵向,再判横向 | 会卡进墙里,或者穿过地面 |
| 四 | 处理「撞到的是什么」 | 踩敌人变成被敌人撞 |
| 五 | 镜头跟随 | 画面一顿一顿 |
| 六 | 绘制 | 还没算完就画,画面闪 |
! 第三第四步最容易写反。先移动后判定,判的是这一帧的新位置,才对得上画面。
② 为什么先判纵向
--------------------------------------------------------------------
角色同时往右和往下动,一帧里可能同时撞墙和落地。
先判横向的话,角色会先被墙挡住、再判落地,表现是「贴着墙也能落地」——
听起来没问题,但反过来(先纵后横)就会出现「从墙边跳上去卡进墙里」。
! 两种顺序都有人用,关键是整个项目统一。混着用必出「有时候能穿墙」这种偶发 bug。
③ 真跑一次落地
--------------------------------------------------------------------
角色从 y = 100 自由下落,脚底离平台顶还差 62 像素
第 17 帧落地,y = 163.8,脚底在 193.8,平台顶在 192
落地那帧向下的速度 425 像素每秒
! 落地就是「脚底越过了平台顶」。越过的那一帧要把 y 拉回来,不然角色会陷进去一点点。
④ 落地之后把 y 拉回来
--------------------------------------------------------------------
越过的量 = 193.8 - 192 = 1.8 像素
拉回到 y = 192 - 30 = 162
! 不拉回来,角色会稳定陷进地面一两像素。看不出来,但下一个平台会因此判错。
⑤ 往右撞墙
--------------------------------------------------------------------
角色从 x = 300 往右走,墙在 x = 448
第 38 帧贴上墙,x = 426.7,右边缘 450.7
钳到 x = 448 - 24 = 424,速度清零
! 跟落地一样:越界的量要抹掉,速度要清零。只判不抹,角色会一点点蹭进墙里。
⑥ 踩敌人还是被敌人撞
--------------------------------------------------------------------
同一帧里,马里奥和板栗仔的框重叠了。判什么?
判据只有一个:马里奥的脚底在板栗仔的头顶附近,就是踩中。
| 情况 | 判据 | 结果 |
|---|---|---|
| 踩中 | 马里奥脚底 < 板栗仔头顶 + 8 | 板栗仔死,马里奥弹一下 |
| 撞上 | 其余情况 | 马里奥掉一条命 |
! 那个「+8」是容差。没有容差,玩家会觉得「明明踩上了却死了」。
⑦ 通关和出错
--------------------------------------------------------------------
通关判定:马里奥的框碰到旗杆的框。就一句,放在主循环第四步里。
异常处理:素材缺失、地图越界、声音加载失败,三处都要兜住。
! 兜住的意思是「记一条日志,然后跳过这个对象」,不是「整个程序崩掉」。
一个金币图没找到,不该让整关玩不了。
第一段那个 1.8 像素是这一篇的核心。 落地那一帧一定会越界,越界就得抹掉。不抹的话角色稳定陷进地面,下一个平台就会判错。 第二段一样:越过 2.7 像素,钳回 424。判到之后不做修正,角色就会一点点蹭进墙里。 第三段那五个值把容差的作用说清了:178 和 182 都在容差内算踩中,186 开始算撞上。 把 8 改成 2,就只有 178 算踩中了——玩家会觉得「明明踩上了却死」。 这三段都不用 Cocos2d。主循环的顺序、越界的修正、踩撞的判据,全是逻辑,跟引擎无关。 到这里这本书的三个项目都过完了。骨架从头到尾没变过:状态、场景、循环、碰撞、绘制。
30 秒实验
- 把
foot从 30 改成 48,重跑第一段,看第几帧落地、越界多少。 - 把
vx从 200 改成 600,重跑第二段,看越界量变成多少——想想为什么越快越容易穿墙。 - 把踩中判据里的
+ 8改成+ 2,重跑第三段,看哪几行结果变了。
常见误区
- 「落地判到就行,不用拉回来。」角色会稳定陷进地面一两像素。看起来没事,但下一个平台会判错。
- 「碰撞和踩敌人一起判。」先判重叠、再判方向,顺序不能反。混在一起写,就成了「碰到就死」。
- 「异常处理就是到处 try。
」空着的
except: pass比不写还危险——它把「哪里出了问题」这个信息也一起吞了。
自测三问(点开对答案)
1. 为什么落地那一帧要把 y 拉回来? —— 因为每帧位移是一个固定步长,落地那一帧脚底一定越过了平台顶。 不拉回来角色会陷进去,而「陷进去」会让下一个平台的判定算错。 **判到之后,越界的量要抹掉、速度要清零。** 2. 踩中敌人的判据是什么?为什么要留容差? —— 马里奥脚底在敌人头顶附近(比如 +8 像素内)就算踩中。 留容差是因为玩家分辨不了一像素,**没有容差会觉得「明明踩上了却死了」。** 3. 为什么通关之后切场景用 replace 而不是 push? —— 因为通关之后不需要回到游戏场景。 push 会把游戏场景留在栈里,白占内存, 而且「返回」会退回去——**通关了还能退回去接着玩,是明显的 bug。**正文到这里就完了。最后一篇是附录:示例代码在哪、用什么工具、素材的许可证怎么看。
- ☐ (本篇完,下一篇 A.1 示例代码、工具与延伸阅读)