HUD 摆在四个角,右对齐的那两个要减去文字宽度。 半透明遮罩每个通道各自算:原值乘(1 减透明度)再加上遮罩值乘透明度。 遮罩和提示文字要分两层,否则字也会被压暗。
上一篇定完了结构,这一篇把「画在最上面」的三样东西摆出来:菜单、HUD、提示层。 教材 7.3 到 7.8 节:【菜单背景】【菜单】【记录状态】【HUD】【提示场景】【提示层】。 六节听着多,其实都在回答一个问题:东西画在哪个位置、哪一层、什么顺序。 HUD 坐标、遮罩混色、文字居中,都是真算的。
一、HUD 摆在四个角
分数左上、金币左上偏下、时间右上、命数右上偏下——这是老游戏的标准布局。 演示里四个位置是:(20, 20)、(20, 60)、(780, 20)、(780, 60)。那个 780 是右边缘,不是起点。 右对齐的项目,画的时候要减去文字宽度。这一步太容易忘了,而忘了的后果很直观: 分数一到五位数就顶出屏幕。
演示算了三种宽度的数字:1 位宽 16 像素,起点 764;3 位宽 48,起点 732;5 位宽 80,起点 700。 排得整整齐齐,右边永远留 20 像素。
二、半透明遮罩
暂停的时候整个画面压暗一层,做法是盖一块半透明的黑。 算法每个通道各自算:新值 = 原值 × (1 - 透明度) + 遮罩值 × 透明度。遮罩是黑的,遮罩值就是 0, 所以只剩乘法。 演示里底色 (100, 150, 200),盖 60% 的黑,结果 (40, 60, 80)。心算一下:100 × 0.4 = 40,对得上。
透明度 0.3 时是 (70, 105, 140),0.9 时是 (9, 14, 19)。 0.9 已经几乎全黑了——超过 0.8 就看不清底下了。 这条算式在游戏里到处都用:阴影、天气、血条底衬、场景切换的淡入淡出。认一次,每处都能用。
三、遮罩和文字的顺序
顺序必须是:先画游戏 → 再画遮罩 → 最后画提示文字。 反过来先画文字再盖遮罩,字也被压暗了。新手最常见的做法就是「画完所有东西,最后统一盖一层」。 解决办法有两个:一是严格按顺序画,二是把提示文字放到更高的 z 层。
演示的 z 表就是这样:遮罩 200,提示文字 210。差 10 就够,不用差 100。 这也解释了为什么 z 要用 10、100、200 这种整数:留出间隔,中间想插东西随时插。
四、提示文字居中
居中就是「窗口宽减文字宽,再除二」。演示算了三个:「暂停」宽 48,起点 376; 「游戏结束」宽 96,起点 352;「第 1 关完成」宽 168,起点 316。 这里用的是等宽估算(每个字 24 像素)。 真做的时候,中文和数字宽度不同,得用字体对象量一下真实宽度。 不讲这个细节的后果:中英混排的提示语会明显偏左或偏右。肉眼一看就觉得别扭,但说不上哪里不对。
竖着居中同理,只是把宽度换成高度。别忘了字体本身有上行和下行留白,直接拿字号当高度会偏。
五、菜单背景
菜单背景用平铺:一张 256×256 的图铺 800×600 的窗口,需要 4 列 × 3 行 = 12 张。 算法跟俄罗斯方块那边一模一样,只是尺寸不同。同一套代码直接用,这就是「抽出来」的好处。 菜单背景要动的话,就用取模滚动。也是同一个套路。
三个项目写下来,你会发现平铺、取模、居中、遮罩这些「小算法」反复出现。 把它们写成函数放进一个工具文件,下次直接用。
六、菜单本身
菜单就是一堆按钮加一条排布规则,第五篇已经算过了竖排和横排。 这里多一件事:选中项要高亮。做法是把当前项的图片换成另一张,或者加一个框。 高亮的状态要跟着输入走:上键减一、下键加一、到头循环。三行,但要防越界。
演示里没跑菜单交互(那要真按键),但排布和高亮的原理跟 HUD 一样:都是算位置、按状态选图。
七、动手 Demo:菜单、HUD 与提示层的摆法
先把 HUD 摆到四个角,看右对齐为什么要减文字宽。 再算半透明遮罩的混色,看 0.3、0.6、0.9 三种透明度各是什么颜色。 最后算提示文字的居中位置,把 z 层排一遍。
import math
W, H = 800, 600
def info(s):
print(' ' + s)
def sec(t):
print(); print(' ' + t); print(' ' + '-' * 68)
# ① HUD 四角
sec('① HUD 摆四角')
for name, x, y, al in (('分数', 20, 20, '左'), ('金币', 20, 60, '左'),
('时间', W - 20, 20, '右'), ('命数', W - 20, 60, '右')):
info('%-4s x = %3d y = %d %s对齐' % (name, x, y, al))
# ② 右对齐要减宽度
sec('② 右对齐减多少')
for text in ('0', '300', '12500'):
w = len(text) * 16
info('%-6s 宽 %2d,起始 x = %d' % (text, w, W - 20 - w))
# ③ 遮罩混色
sec('③ 半透明遮罩')
base = (100, 150, 200)
for a in (0.3, 0.6, 0.9):
out = tuple(int(base[i] * (1 - a)) for i in range(3))
info('透明度 %.1f -> %s' % (a, out))
# ④ 居中
sec('④ 文字居中')
for text in ('暂停', '游戏结束', '第 1 关完成'):
w = len(text) * 24
info('「%s」宽 %d,起始 x = %d' % (text, w, (W - w) // 2))
# ⑤ 平铺张数
sec('⑤ 菜单背景铺几张')
nx = int(math.ceil(W / 256.0))
ny = int(math.ceil(H / 256.0))
info('256 的图铺 %dx%d 的窗口:横 %d 张纵 %d 张,共 %d 张'
% (W, H, nx, ny, nx * ny))
实跑输出:
--------------------------------------------------------------------
菜单、HUD 与提示层
--------------------------------------------------------------------
① HUD 摆在四个角
--------------------------------------------------------------------
| 项目 | x | y | 对齐 |
|---|---:|---:|---|
| 分数 | 20 | 20 | 左对齐 |
| 金币 | 20 | 60 | 左对齐 |
| 时间 | 780 | 20 | 右对齐 |
| 命数 | 780 | 60 | 右对齐 |
! 右对齐的项目,x 写的是「右边缘」,画的时候要减去文字宽度。这一步很容易忘。
② 右对齐要减多少
--------------------------------------------------------------------
假设每个数字占 16 像素宽,字号固定
| 文字 | 宽度 | 实际起始 x |
|---:|---:|---:|
| 0 | 16 | 764 |
| 300 | 48 | 732 |
| 12500 | 80 | 700 |
! 数字变长的时候起点要往左挪——不减的话,分数一大就顶出屏幕。
③ 半透明遮罩怎么算
--------------------------------------------------------------------
游戏画面是 (100, 150, 200),暂停时盖一层黑,透明度 0.6
透明度 0.3 -> (70, 105, 140)
透明度 0.6 -> (40, 60, 80)
透明度 0.9 -> (9, 14, 19)
! 每个通道各自算:新值 = 原值 × (1 - 透明度) + 遮罩值 × 透明度。黑就是 0,所以只剩乘法。
④ 遮罩别盖住提示文字
--------------------------------------------------------------------
顺序要这样:先画游戏 → 再画遮罩 → 最后画提示文字。
反过来先画文字再盖遮罩,字也被压暗了。
! 或者把提示文字放到更高的 z 层。两种做法都行,混着用就会出「字看不见」的怪事。
⑤ 提示文字居中
--------------------------------------------------------------------
「暂停」宽 48,起始 x = (800 - 48) // 2 = 376
「游戏结束」宽 96,起始 x = (800 - 96) // 2 = 352
「第 1 关完成」宽 168,起始 x = (800 - 168) // 2 = 316
! 居中就是「窗口宽减文字宽,再除二」。中文字和英文数字宽度不同,混排时得逐字算。
⑥ z 层怎么排
--------------------------------------------------------------------
| 层 | z | 放什么 |
|---|---:|---|
| 背景 | -10 | 远景、天空 |
| 地图 | 0 | 砖块、地面 |
| 角色 | 10 | 马里奥、敌人、金币 |
| HUD | 100 | 分数、金币、时间 |
| 遮罩 | 200 | 暂停的半透明黑 |
| 提示文字 | 210 | 暂停、结算的字 |
! 遮罩和文字分开两层,就是为了解决第 ④ 段那个顺序问题。
⑦ 菜单背景
--------------------------------------------------------------------
菜单背景用平铺:一张 256×256 的图铺 800×600 的窗口,需要 12 张
跟俄罗斯方块那边一模一样的算法,只是尺寸不同。
! 同一套平铺代码,在两个项目里直接用。这就是「抽出来」的好处。
第一段那四个坐标,右对齐的两个写的是「右边缘 780」。
这个写法很多教程会搞混——写成起始点,右边就永远差一截。
第二段那个变化很直观:数字从 1 位变 5 位,起点从 764 挪到 700,右边始终留 20 像素。
第三段那个算式记住就行:新值 = 原值 × (1 - 透明度) + 遮罩值 × 透明度。遮罩是黑的就只剩乘法,
遮罩是白的就变成「往白里提」。
第四段用的是等宽估算。
真要精确得量字体,但做原型阶段用估算够了——偏几个像素看不出来,偏一半就明显了。
第五段那个 ceil 跟俄罗斯方块那边一字不差。这就是「小算法反复出现」的证据。
30 秒实验
- 把窗口宽从 800 改成 1024,重跑第二段,看三行起始 x 怎么变。
- 把
base换成 (255, 255, 255),重跑第三段,看遮罩变成「往黑里压」还是「往白里提」。 - 在第五段里把图从 256 改成 300,重跑,看张数怎么变。
常见误区
- 「右对齐就把文字往右边一贴。」不减去宽度,数字一长就出屏。x 存右边缘,画的时候减宽度。
- 「画完所有东西最后盖一层遮罩最省事。」字也会被压暗。遮罩和文字分两层。
- 「居中除以二就行,不用管字宽。」会明显偏。中英混排尤其明显,得按实际宽度算。
自测三问(点开对答案)
1. 半透明的黑色遮罩盖在 (100, 150, 200) 上,透明度 0.6,结果是多少? —— (40, 60, 80)。每个通道乘 (1 - 0.6) = 0.4:100 × 0.4 = 40,150 × 0.4 = 60,200 × 0.4 = 80。 2. 为什么提示文字要和遮罩分两层? —— 因为画是有顺序的。先画文字再盖遮罩,字也被压暗了。 把文字放到更高的 z 层(比如 210),就不会被遮罩影响。 3. 分辨率 800 宽,一个 5 位数字每个数字 16 像素,右对齐留 20 像素边距,起始 x 是多少? —— 700。5 × 16 = 80,800 - 20 - 80 = 700。界面摆好了,下一篇进游戏本身:一张地图、一堆砖块、和会闪的那种。
- ☐ (本篇完,下一篇 7.3 地图、游戏场景与闪烁砖块)