HUD 就是叠在 z = 100 的一层,画分数、等级、下一个方块。 计分公式:1 行 100、2 行 300、3 行 500、4 行 800,再乘等级。 状态推进的顺序不能反:先按旧等级算分,再加行数,最后才升等级。
前两篇把场景、背景、菜单、参数都备齐了,这一篇进游戏场景本身。 教材 5.10 到 5.13 节:【游戏场景】【HUD】【记录状态】【参数设置与状态更新】。 四节讲的是同一件事:一局游戏里那些数字怎么变、什么时候变、变完画在哪。 计分表、等级曲线、顺序反了的差额,全是真算的。
一、HUD 是叠在最上面的一层
HUD 的全称是 Heads-Up Display,抬头显示。游戏里的分数、血量、小地图都算 HUD。 实现上它就是一层普通的层,只是 z 特别大。 演示列了四档:背景层 -10、游戏层 0、HUD 层 100、弹层 200。 为什么用 -10 和 100 这种整数,而不是 -1 和 1?因为留出间隔,以后想在中间插一层,改个数字就行, 不用动其他所有层的 z。
俄罗斯方块没有卷轴镜头,所以四档就够。 层数少是这类游戏的优势,换成马里奥那种带镜头的,层就要多好几档。
二、计分公式
教材用的是任天堂那套经典数值:一次消 1 行 100 分、2 行 300、3 行 500、4 行 800。
| 一次消几行 | 基础分 | 平均每行 |
|---|---|---|
| 1 行 | 100 | 100 |
| 2 行 | 300 | 150 |
| 3 行 | 500 | 167 |
| 4 行 | 800 | 200 |
看平均每行那一列:100、150、167、200。消得越多,每行越值钱。 这条曲线不是随便定的,它就是这个游戏核心策略的全部动力——玩家会刻意把一行留个缺口, 攒着等一个长条一次消四行。 如果四行给 400 分(四倍),就没人会攒了。数值设计就是这么起作用的。
三、乘上等级
基础分还要乘等级。演示算了 1、2、5、10 级:1 级消四行 800 分,10 级同样的四行 8000 分。 为什么等级越高给越多?因为等级高了方块掉得快,同样消四行难度大得多。分数是补偿,不是奖励。 这也是很多游戏的通用的做法:难度上去,单位操作的收益也跟着上去,否则玩家会觉得「越玩越亏」。
四、等级怎么算
每累计消 10 行升一级。演示列了 0、9、10、19、20、30 行对应的等级:1、1、2、2、3、4。
实现就一句 lines // 10 + 1。不用 if,不用查表。
少写一个分支就少一处可能写错的地方。
这种「能用算式表达的规则,就别用条件判断」的习惯,
在写游戏逻辑的时候特别值钱——因为游戏逻辑最容易加分支,加到后面没人看得懂。
五、状态推进的顺序
演示模拟了七次消除:1、2、4、3、4、4、2 行,一路把分数、累计行数、等级推到底。 关键在于顺序:先按当前等级算分,再把行数加上去,最后才重算等级。 第三步算出来的新等级,要到下一次消除才用得上。这是「这一局的这一手,按这一手开始时的等级结算」。
演示第六段故意把顺序反过来跑了一遍:先加行数、先升级,再按新等级算分。 同样七次消除,结果多算了 800 分。 注意它不报错。程序照跑,界面照显示,只是账不对。 这类 bug 最难查——没有异常,只有「感觉分数有点高」。
六、动手 Demo:计分、等级与状态推进的顺序
先看计分表和平均每行的收益,再乘上等级。 然后算等级怎么从行数推出来,最后模拟七次消除的完整推进。 末尾故意把顺序反过来跑一遍,看差多少分。
def info(s):
print(' ' + s)
def sec(t):
print(); print(' ' + t); print(' ' + '-' * 68)
base = {1: 100, 2: 300, 3: 500, 4: 800}
# ① 计分表
sec('① 一次消几行给多少分')
for n in (1, 2, 3, 4):
info('%d 行 %4d 分 平均每行 %.0f' % (n, base[n], base[n] / float(n)))
# ② 乘等级
sec('② 乘上等级')
for lv in (1, 2, 5, 10):
info('等级 %2d 1 行 %5d 4 行 %5d' % (lv, base[1] * lv, base[4] * lv))
# ③ 等级怎么算
sec('③ 等级 = 累计行数 // 10 + 1')
for lines in (0, 9, 10, 19, 20, 30):
info('%2d 行 -> 等级 %d' % (lines, lines // 10 + 1))
# ④ 正确顺序
sec('④ 先算分,再加行,最后升级')
st = {'score': 0, 'lines': 0, 'level': 1}
for n in (1, 2, 4, 3, 4, 4, 2):
gain = base[n] * st['level']
st['score'] += gain
st['lines'] += n
st['level'] = st['lines'] // 10 + 1
info('消 %d 行 +%5d 累计 %5d 分 %2d 行 等级 %d'
% (n, gain, st['score'], st['lines'], st['level']))
good = st['score']
# ⑤ 反过来的顺序
sec('⑤ 先升级,再算分')
st = {'score': 0, 'lines': 0, 'level': 1}
for n in (1, 2, 4, 3, 4, 4, 2):
st['lines'] += n
st['level'] = st['lines'] // 10 + 1
st['score'] += base[n] * st['level']
info('总分 %d,比正确顺序多 %d 分' % (st['score'], st['score'] - good))
实跑输出:
--------------------------------------------------------------------
游戏场景、HUD 与计分推进
--------------------------------------------------------------------
① HUD 是叠在最上面的一层
--------------------------------------------------------------------
| 层 | z | 放什么 |
|---|---:|---|
| 背景层 | -10 | 平铺的底图 |
| 游戏层 | 0 | 棋盘、正在落的方块 |
| HUD 层 | 100 | 分数、等级、下一个方块 |
| 弹层 | 200 | 暂停、结算、提示 |
俄罗斯方块没有卷轴镜头,所以 z 只有这四档就够。
! 四档之间留 10 的间隔,以后想在中间插一层,改个数字就行。
② 计分公式
--------------------------------------------------------------------
教材用的是任天堂那套:一次消 1 行 100、2 行 300、3 行 500、4 行 800,再乘等级。
| 一次消几行 | 基础分 | 平均每行 |
|---:|---:|---:|
| 1 行 | 100 | 100 |
| 2 行 | 300 | 150 |
| 3 行 | 500 | 167 |
| 4 行 | 800 | 200 |
! 四行不是四倍,是八倍。这条曲线就是「攒着一次消」这个玩法的全部动力。
③ 乘上等级
--------------------------------------------------------------------
| 等级 | 1 行 | 2 行 | 3 行 | 4 行 |
|---:|---:|---:|---:|---:|
| 1 | 100 | 300 | 500 | 800 |
| 2 | 200 | 600 | 1000 | 1600 |
| 5 | 500 | 1500 | 2500 | 4000 |
| 10 | 1000 | 3000 | 5000 | 8000 |
! 等级越高分越高,这是补偿——等级高了方块掉得快,难度上去了。
④ 等级怎么升:每 10 行一级
--------------------------------------------------------------------
| 累计消行 | 等级 |
|---:|---:|
| 0 | 1 |
| 9 | 1 |
| 10 | 2 |
| 19 | 2 |
| 20 | 3 |
| 30 | 4 |
! 用整除就行,不用 if 判断。少写一个分支,就少一处可能写错的地方。
⑤ 一次消除的完整状态推进
--------------------------------------------------------------------
开局 {'score': 0, 'lines': 0, 'level': 1}
第 1 次消 1 行 + 100 分 累计 100 分 1 行 等级 1
第 2 次消 2 行 + 300 分 累计 400 分 3 行 等级 1
第 3 次消 4 行 + 800 分 累计 1200 分 7 行 等级 1
第 4 次消 3 行 + 500 分 累计 1700 分 10 行 等级 2
第 5 次消 4 行 + 1600 分 累计 3300 分 14 行 等级 2
第 6 次消 4 行 + 1600 分 累计 4900 分 18 行 等级 2
第 7 次消 2 行 + 600 分 累计 5500 分 20 行 等级 3
! 顺序不能反:先按旧等级算分,再加行数,最后才升等级。
⑥ 反了会怎样
--------------------------------------------------------------------
先升级后算分,总分变成 6300 分
! 同一个玩家、同样七次消除,多算了 800 分。顺序错了不报错,只是账不对。
第一段那张表最值得看的是「平均每行」那一列。数值设计的意图全在这一列里:越攒越值钱。 第二段说明分数是补偿。等级高、掉得快、难度大,所以单位消除的收益也得往上走。 第三段一句话就完了。能用整除表达的规则,千万别写成一串 if。 第四段和第五段是这一篇的重点:同一个玩家、同样七次消除、只改了顺序,差了 800 分。 这类 bug 不报错,只让账不对。写完计分一定要拿手算的数对一遍。 最后提醒:这套数值是教材用的经典配置,不是什么标准。 你完全可以自己调,但要一次只调一个数,不然手感变了都不知道是谁变的。
30 秒实验
- 把四行的基础分从 800 改成 1200,重跑,看「平均每行」变成多少。
- 把升级门槛从 10 行改成 5 行,重跑第四段,看等级什么时候跳到 2。
- 把第四段的顺序换成「先加行数」,但保留「按旧等级算分」,看分数变不变。
常见误区
- 「HUD 单独开一个窗口画。」不行,得在同一块画布上叠一层。用 z 排序,不是用窗口。
- 「消四行给四倍分最公平。」公平但没意思。 没有超额收益,玩家就不会攒,游戏就变成了「见一行消一行」。
- 「计分顺序无所谓,反正最后分都加上了。」顺序错了不报错,只让账不对。 先算分再加行,这个顺序要写进注释。
自测三问(点开对答案)
1. 为什么消四行给 800 分而不是 400 分? —— 为了让「攒着一次消」有额外收益。平均每行从 100 涨到 200,玩家才有动力刻意留缺口等长条。 **这是数值驱动玩法的典型例子。** 2. 累计消了 23 行,等级是几? —— 3 级。23 // 10 + 1 = 2 + 1 = 3。**整除会自动把零头抹掉,不用额外判断。** 3. 为什么计算分数要用「消除之前」的等级? —— 因为这一手是按开始时的难度打的。 先升级再算分,等于用更高的倍率结算更简单的一手,分数会虚高——演示里七次消除就多算了 800 分。数字怎么变说清了,下一篇低头看棋盘本身:一张表、七种形状、一堆坐标换算。
- ☐ (本篇完,下一篇 5.4 游戏区域、棋盘与方块)