游戏里的GUI,说它是门面也好,说是骨架也罢,反正绕不开。我做了这么多年游戏相关的东西,从Web小游戏到桌面程序,从Unity到直接拿EasyX画界面,发现一个规律:凡是能让人一眼看懂、上手就玩的游戏,GUI一定不只是“画了几个按钮”那么简单。GUI承担的是信息呈现、交互反馈、氛围传达这三件事,缺一项玩家就会觉得别扭。这篇就围绕“游戏与图形界面”这个话题,把我踩过的坑、用过的方案、觉得值得记录的实现细节都摊开讲一讲。不管你是在用Unity做商业项目,还是用C++写个小游戏练手,这篇文章应该都有能直接拿走的东西。
1. 内容整体设计与思路拆解
1.1 游戏里的GUI到底在解决什么问题
先想清楚一个问题:游戏里的GUI和普通软件里的GUI有什么不一样。
普通软件重视的是“功能可达”,按钮在哪、菜单怎么排,核心是让用户快速完成任务。游戏GUI不一样,它首先要保证“沉浸感不能被打破”。血条不能只是数据,最好带一点动态反馈;商店界面不能像Excel表格,得有一层世界观包装;连按钮按下去那个抖动,都是在为“手感”服务。
我总结过游戏GUI的三层职责:
- 信息呈现:血量、分数、技能CD、背包物品、任务目标,第一时间让玩家看到该看的。
- 交互反馈:按钮悬停变色、点击压下、拖拽时的高亮,任何操作都不能让玩家怀疑“我点的到底有没有用”。
- 氛围传达:UI风格跟着游戏题材走,像素游戏用复古字体,科幻游戏用科技感边框,这个决定玩家第一印象。
所以做游戏GUI时,我从来不敢把“UI”和“玩法”拆成两个不相干的东西。一个按钮的位置、大小、响应区域,都会影响玩家的操作习惯和游戏节奏。比如手游上常见的“右手拇指热区”,如果按钮放得太靠屏幕中央,玩家够着就费劲,这个游戏就会莫名其妙被扣上“操作不跟手”的帽子。
1.2 为什么我做GUI时会先定输入路径,而不是先画界面
很多新手做游戏GUI,上来就拖控件、调颜色,然后才发现键盘操作逻辑、鼠标点击区域、手柄焦点切换全部乱成一团。我现在的习惯是反过来的:先确定玩家的输入方式,再设计界面布局。
举个具体的例子。假如做一个PC端的横版射击小游戏,玩家主要用键盘操作角色移动(WASD),只有菜单和结算界面用鼠标点击。这时候GUI设计的优先级就是:HUD(血条、弹药)放在屏幕边缘,不能阻挡主视野,按钮状态切换要考虑键盘焦点(方向键控制高亮还是Tab切换),而不是单纯依赖鼠标悬停。
如果做成手机游戏,输入路径会变成触控,这时候还要额外考虑手指遮挡问题。左下角和右下角天然是拇指控制区,但屏幕中央的UI如果和可拖拽区域重叠,玩家误触的概率会飙升。我早期做一款休闲游戏时,把“暂停按钮”放在了右上角,看似合理,结果玩家大拇指自然放置的位置正好压在上面,十分钟能误触三次,后来只好把按钮缩小并上移,才算解决。
这些本质上都是GUI设计的输入路径问题,比配色、字体优先级更高。
1.3 从热词里看到的几个典型GUI方向
这次整理相关热词,发现大家都在关注这几个方向的GUI问题:
- 引擎内置UI:Unity的UGUI、Godot的Control节点,这类方案和场景编辑器深度绑定,上手快,做界面效率高。
- 代码自绘UI:C/C++ + EasyX、MFC、Win32 GDI,这类更适合学习和复古游戏开发,适合把底层原理摸透。
- 工具链与优化:Unity游戏优化、游戏测试、界面布局适配等,讨论的是“怎么把GUI做得不卡、不崩、不乱跳”。
- 特定场景GUI:比如棋盘游戏、像素游戏、弹幕游戏,它们的GUI实现思路差异很大,像素游戏要搞图集和像素字体,弹幕游戏则要尽量精简UI避免遮挡弹幕。
这些方向的共性问题其实是同一个:怎么在“表达清楚”和“不干扰游戏”之间找到平衡。后面我会把每个方向的实操细节展开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 GUI的三大件:层级、布局、事件
不管用什么引擎还是自绘,游戏GUI做来做去就是这三件事:层级管理、布局计算、事件派发。这三件事处理好了,界面再丑都能用得住;处理不好,再花哨的皮也白扯。
**层级(Layer)**解决的是“谁盖住谁”的问题。Unity里有Sorting Layer和Order in Layer,Godot有Z Index,EasyX这种自绘方案就靠绘制顺序硬排。我习惯把游戏GUI归成几个固定层级:背景层(地图/场景)、世界空间UI(血条、名字,跟着角色走)、屏幕空间HUD(分数、背包、按钮)、弹窗层(商店、设置、对话框)、提示层(飘字、Toast)。弹窗层和提示层的层级必须在HUD之上,否则玩家打开背包,结果按钮被血条挡住,这种低级问题在自绘方案里尤其常见。
**布局(Layout)**解决的是“元素放哪”的问题。引擎里一般都有Anchor和Grid之类的布局组件,但游戏UI大部分用不上那种绝对的自适应流式布局。游戏UI通常要响应不同分辨率,所以锚点系统很重要。以屏幕中心为锚点的血量条,跟以左上角为锚点的Frame,逻辑完全不同。我自己的经验是:能固定就用固定位置,必须在不同分辨率下对齐的,优先锚定四角和中心点,少用百分比缩放的“万能方案”,因为极端尺寸下UI会被拉伸到没法看。
**事件(Event)**是GUI的灵魂。Unity的EventSystem、Godot的信号、自绘里的鼠标消息,本质都在做同一件事:把用户的物理操作翻译成游戏逻辑能理解的指令。事件系统里最容易出问题的是“事件穿透”。一个弹窗打开后,点击弹窗外的半透明遮罩,到底是关闭弹窗,还是让操作穿透到游戏世界?不同游戏有不同需求,但大多数情况下必须阻止穿透。Unity里用Image组件加Raycast Target属性来控制是否参与点击拾取,这个开关我每次做弹窗都会检查一遍。
2.2 引擎内置UI与自绘UI怎么选
选UI方案,其实就是选“效率”和“可控性”之间的权衡。
引擎内置UI(Unity UGUI、Godot Control)胜在效率。你做按钮、滑动条、输入框,直接拖拖拽拽就完了,还有可视化事件绑定。Unity的UGUI用RectTransform做布局,锚点系统很成熟,做多分辨率适配也比较省心。缺点是也被引擎套住了,想做一些特别动态的效果(比如纯代码生成大量动态列表项)时,UGUI的性能需要精细调,不然很容易在ScrollView里掉帧。
自绘UI(EasyX、SDL、OpenGL直接画)胜在可控。每个像素都是你自己画的,没有隐藏的布局开销,也没有黑盒行为。缺点也很明显:做一个按钮,你需要自己判断鼠标是否在按钮区域内、自己去监听点击事件、自己管理不同状态下的绘制内容。开发效率确实低,但理解深度完全不同。我从EasyX开始学游戏开发,做完几个小项目后,再回去用UGUI,终于明白底层的东西是什么样。
我给的选型建议很简单:
- 做商业项目、重界面、需要快速迭代:用引擎内置UI,别折腾自绘。
- 学习阶段、想弄明白GUI原理:先用自绘方案做一两个小游戏,再回到引擎。
- 复古像素风、性能敏感项目:自绘UI或轻量UI库更合适,引擎内置UI的默认风格改造反而费时间。
2.3 中文显示、字体与本地化的坑
游戏GUI里最容易翻车的就是中文显示。EasyX默认字体对中文支持不稳定,Unity旧版本也会有中文渲染问题,Godot的默认字体在某些平台会缺字形。遇到中文字体变成方框,十有八九是字体文件里没有对应字形,或者加载字体时没有正确指定字符集。
我的中文显示解决方案一般是三层:
- 选择开源可商用字体(思源黑体、HarmonyOS Sans之类),并明确字体授权。
- 在引擎里把字体文件打进包,设置字体Fallback,确保遇到生僻字时不崩。
- 自绘UI的话,更省心的方案是用位图字体,把需要的字符预先渲染到图集里,运行时按字符索引扣图。
另外提示一下:中文GUI显示性能比英文差一点,这是因为中文字形数量多,图集占空间更大,动态生成字形时会有额外开销。如果游戏里大量使用中文字体,建议把常用文字缓存好,尽量少在运行时动态加载字体。
本地化也不能忽视。游戏GUI里所有文案的宽度是变化的,英文一行,中文可能占更宽,某些语言甚至会出现单词长度溢出。我做本地化时会把所有文本区域都预留20%~30%的冗余宽度,并且对按钮测试不同语言下的换行情况。有些项目在中文环境一切正常,切到英文后按钮文字被截断,这种上线事故完全是可以避免的。
2.4 性能开销:合批、图集与脏矩形
游戏GUI的卡顿,绝大多数不是GPU不够强,而是“绘制次数”太多。
以Unity UGUI为例,它内部会把相邻的、用同一张图集的UI元素合并成一个批次(Batch),减少Draw Call。如果你的UI元素用的图集各不相同,并且交交错错排列,CPU就得频繁切换渲染状态,Draw Call飙升,帧率就下来了。优化手段不外乎这几个:
- 打图集(Atlas):把零散图标合并成一张或几张图集,减少纹理切换。
- 调整UI渲染顺序:让相同图集的元素尽量相邻,保证合批。
- 控制Overdraw:不要把多层半透明大图叠在一起,每层都写像素,开销极大。
- 动态元素独立处理:频繁变化的部分(玩家名字、飘字、进度条)单独放一个Canvas层,避免和其他静态UI一起触发Rebuild。
自绘UI里的优化思路对应着“脏矩形”技术。每一帧全屏重绘其实很浪费,尤其是棋盘游戏这种画面大部分静止的场景。做法是:标记发生变化的小区域,只重绘这些区域。EasyX里可以用BeginBatchDraw配合局部贴图实现,先把不变的背景渲染到内存DC,变化区域每次从背景上抠出来再画新的内容。一次实战里,我用脏矩形把纯自绘棋盘游戏的帧率从60帧稳到144帧,CPU占用还降了不少。
3. 实操过程与核心环节实现
3.1 用C++/EasyX写一个带完整GUI的小游戏
很多人觉得GUI难,其实是因为没有完成过一个完整的闭环。我现在拿C++和EasyX做一个简单但五脏俱全的“接金币”小游戏,完整展示GUI设计的思考过程。这个方案适合Windows环境下学习,代码我会尽量写得短,但核心逻辑不会缺。
游戏规则很简单:一个挡板在底部左右移动,金币从上方掉落,玩家用鼠标控制挡板接住金币,接住加分,漏掉扣命,生命归零弹出结束界面。
我先把GUI拆成几个部分:
- HUD区域:左上角分数、右上角剩余生命。
- 挡板:通过鼠标X坐标控制左右移动。
- 金币:圆形图案,内部画一点高光,模拟金币质感。
- 游戏结束弹窗:半透明遮罩、标题文字、重新开始按钮。
核心数据结构这样设计:
cpp复制struct Coin {
float x, y; // 位置
float vy; // 下落速度
bool active; // 是否存活
};
struct PlayerBoard {
float x; // 挡板中心x
float width; // 挡板宽度
};
struct GameUI {
int score;
int lives;
bool gameOver;
};
EasyX的坐标系统是以像素为单位的,默认原点在窗口左上角。在绘图前我会先初始化窗口,并设置好游戏循环的节奏。
cpp复制#include <graphics.h>
#include <conio.h>
#include <cmath>
#include <cstdlib>
int main() {
initgraph(800, 600); // 创建800x600窗口
setbkcolor(RGB(20, 24, 32));
cleardevice();
GameUI ui = {0, 3, false};
PlayerBoard board = {400.0f, 100.0f};
Coin coins[5];
// ... 初始化金币数组
BeginBatchDraw();
while (!ui.gameOver) {
cleardevice();
// 1. 处理输入:鼠标控制挡板
MOUSEMSG msg = GetMouseMsg(); // 非阻塞获取鼠标消息
board.x = msg.x;
// 2. 更新金币位置、碰撞检测
// 3. 绘制所有界面元素
DrawHUD(ui);
DrawBoard(board);
DrawCoins(coins);
// 4. 判断游戏结束
if (ui.lives <= 0) {
ui.gameOver = true;
break;
}
FlushBatchDraw();
Sleep(16); // 大约60帧
}
EndBatchDraw();
ShowGameOverScreen(ui.score);
getch();
closegraph();
return 0;
}
这段代码里有一个很关键的技巧:GetMouseMsg()在EasyX中默认是阻塞式的,如果当前消息队列没有鼠标消息,它会一直等待。所以游戏循环必须配合PeekMouseMsg()来非阻塞查询,或者直接处理WM_MOUSEMOVE对应的接口。我实际开发中常用的写法是:
cpp复制MOUSEMSG msg;
while (MouseHit()) { // 判断队列中是否有鼠标消息
msg = GetMouseMsg();
if (msg.uMsg == WM_MOUSEMOVE) {
board.x = msg.x;
}
if (msg.uMsg == WM_LBUTTONDOWN) {
// 点击按钮、发射子弹等逻辑
}
}
很多EasyX新手一上来就GetMouseMsg,结果游戏一旦没有鼠标输入就卡死,就是这个原因。
3.2 核心代码:按钮、碰撞、计分板
现在把GUI相关的核心函数拆出来看。
按钮实现。自绘方案里按钮没有现成控件,只有“矩形区域 + 鼠标事件 + 状态颜色”。我封装一个通用结构:
cpp复制struct Button {
int x, y, w, h;
const char* text;
bool hovered;
bool pressed;
bool Contains(int px, int py) {
return px >= x && px <= x + w && py >= y && py <= y + h;
}
};
void DrawButton(Button& btn) {
COLORREF fillColor = btn.hovered ? RGB(200, 170, 100) : RGB(150, 120, 70);
if (btn.pressed) fillColor = RGB(100, 80, 50);
setfillcolor(fillColor);
solidrectangle(btn.x, btn.y, btn.x + btn.w, btn.y + btn.h);
// 描边
setlinecolor(RGB(255, 220, 150));
rectangle(btn.x, btn.y, btn.x + btn.w, btn.y + btn.h);
// 文字居中
settextcolor(WHITE);
settextstyle(20, 0, "微软雅黑");
int textW = textwidth(btn.text);
int textH = textheight(btn.text);
outtextxy(btn.x + (btn.w - textW) / 2, btn.y + (btn.h - textH) / 2, btn.text);
}
按钮的“按下”状态不是鼠标在里面就行的,还要结合“在按钮内按下鼠标左键”。所以事件处理是这样的:
cpp复制void HandleButtonEvent(Button& btn, MOUSEMSG msg) {
if (msg.uMsg == WM_MOUSEMOVE) {
btn.hovered = btn.Contains(msg.x, msg.y);
} else if (msg.uMsg == WM_LBUTTONDOWN) {
if (btn.Contains(msg.x, msg.y)) btn.pressed = true;
} else if (msg.uMsg == WM_LBUTTONUP) {
if (btn.pressed && btn.Contains(msg.x, msg.y)) {
// 触发点击动作
OnButtonClicked(btn);
}
btn.pressed = false;
}
}
注意一个细节:WM_LBUTTONUP时也要检查鼠标是否还在按钮区域内,否则玩家按住鼠标拖出按钮再松手,也会触发点击,体验就很怪。这个“按下后移出再松手不触发”的行为,是GUI控件设计的默认规范,自绘时必须自己保证。
碰撞检测。金币和挡板之间,我直接用矩形相交检测。为了手感稍微宽松一点,我会把挡板的碰撞区域比绘制区域上下左右各扩大4像素,玩家会明显感觉到“明明看着没碰到,但被判定接到了”。这种视觉宽容度是游戏GUI交互的一部分,能让操作体验更好。
cpp复制bool CheckCollision(const Coin& coin, const PlayerBoard& board) {
int coinW = 30, coinH = 30;
int hitL = board.x - board.width / 2 - 4;
int hitR = board.x + board.width / 2 + 4;
int hitT = 560 - 4;
int hitB = 560 + 8 + 4;
return coin.x + coinW / 2 > hitL && coin.x - coinW / 2 < hitR
&& coin.y + coinH / 2 > hitT && coin.y - coinH / 2 < hitB;
}
计分板。分数不是每帧重算,而是在“接到金币”和“漏掉金币”这两个事件点更新。绘制的时候只需要outtextxy把数字拼成字符串,拼字符串用strcpy + _itoa_s就可以。我习惯在HUD显示上做一点视觉效果——分数增加时数字颜色闪白一下,这需要记录一个“得分时间戳”:
cpp复制void DrawScore(int score, int flashTimestamp) {
char buf[32];
sprintf_s(buf, "分数: %d", score);
settextcolor(flashTimestamp > GetTickCount() - 200 ? RGB(255, 255, 100) : WHITE);
outtextxy(20, 20, buf);
}
这整个项目跑下来,GUI相关代码大约是250行左右,但已经把层级、事件、碰撞、状态管理都过了一遍。做完这个再去碰Unreal或Unity的UI系统,很多术语一下子就能对上号。
3.3 Unity UGUI版本怎么对应改造
如果把这个接金币游戏搬到Unity UGUI里,同样是“挡板 + 金币 + HUD”,实现路径完全不同,但问题一致。
Unity的做法是:
- 用Canvas承载所有UI,Canvas的Render Mode设为Screen Space - Overlay。
- 挡板是一个位于Canvas底部的Image,直接用RectTransform的anchoredPosition控制位置。
- 金币可以用Image,也可以用SpriteRenderer放在世界空间,再通过WorldToScreenPoint把坐标转换到UI坐标系。
- HUD里的分数Text直接绑定一个C#脚本里的变量,每次变化时刷新text。
UGUI给我印象最深的一个坑是:如果是Screen Space - Overlay的Canvas,它的事件坐标是屏幕坐标,而你的游戏世界坐标可能和屏幕坐标不一致,需要转换。如果直接把绑定了RectTransform的UI元素放到世界坐标位置上,很可能会错位一大截。正确做法是:
csharp复制Vector2 screenPos = Camera.main.WorldToScreenPoint(worldPos);
scoreText.rectTransform.anchoredPosition = screenPos - canvasSize / 2f;
这里的canvasSize / 2f是因为anchoredPosition的原点默认在Canvas中心,而屏幕坐标原点在左下角。
另一个常见问题:UGUI里的CanvasScaler配置。做手机游戏时我习惯把UI Canvas的CanvasScaler设为“Scale With Screen Size”,参考分辨率写1280x720,这样在异形屏上UI整体缩放,不会出现元素超出画面边界的问题。但要注意,CanvasScaler会把UI坐标单位变成“参考像素”,和屏幕物理像素不再一一对应。如果需要把UI坐标和世界坐标互转,一定要先把坐标统一到一个坐标系,不然会有误差。
性能和GUI优化的角度,UGUI比自绘UI更讲究“动静分离”。我刚才说过Canvas Rebuild的概念,静态文本、静态图标放一个Canvas,动态计分板、飘字放另一个Canvas,这样动态元素变化时只Rebuild自己的部分,不会拖累整棵UI树。早期我做项目把所有UI都塞进同一个Canvas,一旦分数每帧刷新,整个Canvas的网格都会重建,掉帧掉到头皮发麻。
4. 常见问题与排查技巧实录
4.1 经典故障速查表
做GUI过程中我遇到过太多奇葩问题,大部分都能用一张表说清楚。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 按钮点击没反应 | 事件被上层UI拦截;按钮响应区域太小;坐标偏移 | 检查Raycast Target是否误开了;把按钮区域临时绘制出来;打印点击坐标和自己计算的坐标做对比 |
| 中文显示为方框 | 字体文件缺字形;字体没打进包;Fallback未设置 | 换用完整字体文件;检查Assets里的字体引用;统一用TextMesh/文本组件测试 |
| UI错位、锚点混乱 | 锚点设置不当;CanvasScaler缩放和实际分辨率不匹配 | 先固定参考分辨率;检查RectTransform的anchoredPosition和offsetMin/offsetMax |
| 点击时画面卡顿 | 高分辨率背景重绘;UI图集散乱导致Draw Call过高 | 自绘方案做局部刷新的脏矩形;UGUI方案合批并分离动态/静态Canvas |
| 游戏循环卡死 | 阻塞式等待鼠标消息;死循环里Sleep时间异常 | 用MouseHit()或轮询事件;打印日志定位循环卡住的迭代 |
| 自定义按钮在缩放后错位 | 按钮RectTransform比例缩放,但事件区域没同步 | 统一用RectTransform做区域判断,别用世界坐标和屏幕坐标混算 |
| UGUI文本模糊 | 字体尺寸过小且被CanvasScaler拉伸 | 字号建议接近参考分辨率,保证缩放比例尽量接近1 |
| 触摸屏误触 | 按钮与拇指热区重叠 | 调整UI布局,避开屏幕边缘和中央操作区域 |
这里补充说明一下“按钮点击没反应”这个头号问题。在自绘方案EasyX里,多半是鼠标消息处理的时序不对,比如在while (MouseHit())里处理了消息,但按钮绘制和事件检测用的坐标不一致。在Unity里,最阴间的坑是某个透明Image覆盖在按钮上,它的Raycast Target属性是true,把点击“吃掉”了。我看到这种问题时,第一反应都是检查所有和按钮重叠的UI元素,把不需要交互的Image的Raycast Target全部关掉。
4.2 一次UI点击无响应的定位过程
说一次真实的排查经历。我之前做一款Unity休闲小游戏,玩家点击“开始游戏”按钮时,有时候触发不了,而且不是每次都触发,像是随机性的问题。
一开始我以为是按钮位置有问题,打印点击坐标,发现坐标一直在按钮范围内,但事件系统不响应。打开EventSystem的EventSystem Debugger查看,发现有一个透明Image的Raycast Target覆盖在整个屏幕上,偶尔它的拾取顺序排在按钮前面,就拦截了点击。
到这里问题已经定位了:透明遮挡层。解决方法是把那个Image的Raycast Target关掉。但我没停下,继续往深处查为什么会“随机”。最终发现原因很蠢:遮挡层Image是“开始游戏”按钮点击后弹出的一个半透明过渡层的残留,它每次点击出现,但因为淡出动画没完全销毁,导致偶尔还会参与射线检测。
这类问题在GUI开发里特别典型。排查时一定要分三步走:
- 第一步,确认坐标。打印出鼠标/触摸坐标和控件区域,排除坐标系转换错误。
- 第二步,确认事件链。检查所有可能参与Raycast的UI元素,逐一关闭Raycast Target测试。
- 第三步,确认生命周期。检查动画回调、对象复用,是不是有隐藏对象残留并继续接收事件。
很多时候问题不是“画不出来”,而是“看不见的东西在捣乱”。
4.3 游戏GUI测试要盯住的几个点
游戏测试里,GUI是出问题的高发区,而且很多问题只在特定环境下暴露。我测试GUI时会盯住这么几个点:
- 点击区域与视觉区域一致性:按钮视觉上只有60x30,但实际可点击区域是原来的三倍。测试时要遍历所有控件,验证视觉范围和事件范围是否匹配。
- 界面切换的内存消耗:反复打开关闭商店页面,观察内存是否持续上涨。GUI对象没销毁会直接导致卡顿甚至崩溃。
- 多分辨率适配:在不同宽高比下检查UI是否溢出、是否错位,尤其是刘海屏和超宽屏。通常建议至少测16:9、18:9、19.5:9、4:3这几种典型比例。
- 输入法冲突:如果游戏有聊天输入框,中文输入法和游戏热键可能冲突,按键事件会被输入法吞掉。这种问题在PC端很常见。
- 中断恢复:游戏切后台再回来,GUI状态是否恢复。弹窗状态、按钮选中状态、文本输入焦点,都是易错点。
- 字体替换:如果玩家系统缺少某款字体,游戏GUI的布局会崩。测试时可以把默认字体改成不存在的字体,验证Fallback是否生效。
除了功能测试,GUI性能也要测。我自己常用的办法是开Unity Profiler,盯Canvas相关开销。如果UI Reconstruction次数异常高,说明动态元素和静态元素混在一个Canvas里了。阴影和描边特效会引入额外的绘制批次,能少用就少用,能用贴图实现的效果就不要挂实时光组件。
最后说个我自己踩过无数次坑的经验:GUI代码永远比游戏逻辑更容易产生“看起来没问题但就是不对”的bug。原因在于GUI是状态高度耦合的,按钮有正常/悬停/按下/禁用四种状态,弹窗有进入/停留/退出三种阶段,每个组合都要测到。所以我后来在写GUI代码时,强制自己把状态转移做成显式函数,禁止在绘图函数里偷偷改状态。比如按钮状态只允许在事件处理函数里更新,绘制函数只负责读状态。这样做以后,GUI相关的玄学bug少了很多,如果你现在正被莫名其妙的界面问题折磨,建议先看看自己的状态是不是也被改得乱七八糟了。
游戏GUI这条路不难,但很磨人。从自绘开始,搞懂事件、层级、重绘机制,再看引擎里那些“拖拖拽拽就出来”的界面,心里会踏实很多。真的上手做一两个小项目,比看十篇教程都有用。
