🎮 第5章 《俄罗斯方块》游戏
📄 一句话概括

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 秒实验

  1. 把四行的基础分从 800 改成 1200,重跑,看「平均每行」变成多少。
  2. 把升级门槛从 10 行改成 5 行,重跑第四段,看等级什么时候跳到 2。
  3. 把第四段的顺序换成「先加行数」,但保留「按旧等级算分」,看分数变不变。

常见误区

  1. 「HUD 单独开一个窗口画。」不行,得在同一块画布上叠一层。用 z 排序,不是用窗口。
  2. 「消四行给四倍分最公平。」公平但没意思。 没有超额收益,玩家就不会攒,游戏就变成了「见一行消一行」。
  3. 「计分顺序无所谓,反正最后分都加上了。」顺序错了不报错,只让账不对。 先算分再加行,这个顺序要写进注释。
自测三问(点开对答案) 1. 为什么消四行给 800 分而不是 400 分? —— 为了让「攒着一次消」有额外收益。平均每行从 100 涨到 200,玩家才有动力刻意留缺口等长条。 **这是数值驱动玩法的典型例子。** 2. 累计消了 23 行,等级是几? —— 3 级。23 // 10 + 1 = 2 + 1 = 3。**整除会自动把零头抹掉,不用额外判断。** 3. 为什么计算分数要用「消除之前」的等级? —— 因为这一手是按开始时的难度打的。 先升级再算分,等于用更高的倍率结算更简单的一手,分数会虚高——演示里七次消除就多算了 800 分。

数字怎么变说清了,下一篇低头看棋盘本身:一张表、七种形状、一堆坐标换算。

相关笔记