raygui 从 0 到 1:给 C 语言游戏装上一块能拖能点的设置面板
【免费下载链接】raylibA simple and easy-to-use library to enjoy videogames programming项目地址: https://gitcode.com/GitHub_Trending/ra/raylib
C 游戏缺什么最影响观感?答案通常是音量、画质、难度这些"设置",而 raylib 附带的单文件即时模式 GUI 模块 raygui 就是补上这块的 C 语言 GUI 控件方案:一个头文件拷进项目,十几行代码就能画出一个滑块加一个开关。这篇 raygui 教程按"接入 → 跑通 → 排错 → 进阶"的顺序走一遍,所有 API 签名均已对照仓库内的raygui.h(v5.0)源码核实。
给 C 游戏装面板,raygui 为什么合适
先说结论:你要的状态全在自己变量里,raygui 只负责每帧把变量画出来、再读一次输入写回去。
把 raygui 想成点餐小票:厨房不记单,每来一单都重打一张,照着做。即时模式 GUI 同理——控件没有实例,不注册、不销毁、不维护 ID,声明它的代码跑过去的那一刻,它就被画出来了。上一帧的控件和这一帧的控件是两个"不同的人",唯一连续存在的是你传给它的那几个变量。
这套模型带来两个直接好处:
- 不用管理控件生命周期,删除界面上的一块区域,删掉对应的声明即可;
- 布局没有自动排版(
raygui.h头注释里明确写了 "No auto-layout mechanism"),每个控件的位置尺寸都要你自己算,换来的是完全可控。
从仓库把 raygui.h 接进项目
raylib 仓库里raygui.h放在examples/的各模块目录下,每个模块各有一份内容相同的副本,实际用时拷一份到自己项目即可。
// panel.c —— 整个工程里只有这一处定义宏 #define RAYGUI_IMPLEMENTATION // 把 raygui 的实现编译进本翻译单元 #include "raygui.h"宏RAYGUI_IMPLEMENTATION必须在有且仅有一个.c文件里写,实现代码就从这个宏后面展开;其他源文件只写#include "raygui.h",拿到的是声明。编译时照常链接 raylib,例如gcc demo.c panel.c -lraylib -o demo。仓库地址是 https://gitcode.com/GitHub_Trending/ra/raylib ,clone 下来即可。
第一条滑块:最小可运行示例
单文件、能直接跑的版本长这样:
#include "raylib.h" #define RAYGUI_IMPLEMENTATION #include "raygui.h" int main(void) { InitWindow(520, 300, "raygui demo"); float g_vol = 0.35f; // 0~1 音量 bool g_speakerOn = true; // 喇叭开关 while (!WindowShouldClose()) { BeginDrawing(); ClearBackground(LIGHTGRAY); int pct = (int)(g_vol * 100); GuiSliderBar((Rectangle){ 48, 64, 240, 26 }, "VOL", TextFormat("%d%%", pct), &g_vol, 0.0f, 1.0f); GuiCheckBox((Rectangle){ 48, 118, 24, 24 }, "SPEAKER", &g_speakerOn); EndDrawing(); } CloseWindow(); return 0; }GuiSliderBar的四个参数依次是:位置尺寸(Rectangle)、左端标签、右端文本、数值指针加取值区间,返回值非 0 表示这一帧滑块被碰过。GuiCheckBox更简单,指针指向那个bool,复选框的勾选状态就存在变量里。跑起来后拖一下滑块:变量在变,画面就跟着变,没有其他任何"同步"代码。
控件隔帧就失忆:即时模式的记账规则
很多人第一次接触即时模式 GUI 时的疑问是:控件不存状态,那它凭什么"记得"我上次拖到哪?答案是:它不记,也不需要记,你每帧把变量的当前值递过去,画出来的就是这个值。
这也解释了三个容易踩的点:
- 想让下拉框展开、让文本框处于编辑态,开合状态得由你自己用一个
bool存着、每帧回传; - 界面上任何"记住上一步"的效果,实现方式都是"上一帧把结果写进变量,这一帧变量再画出来";
- 控件返回的永远是"本帧发生了什么",不是一段持续的历史,想触发一次性动作就用它的返回值判断。
按交互目的选控件:一张速查表
选型只看一件事:用户要做哪种动作。四种最常用的 C 语言 GUI 控件用法如下(均摘自各控件的函数签名):
| 交互目的 | 控件 | 一行调用 |
|---|---|---|
| 连续数值(0~1 音量、半径) | 滑块 | GuiSliderBar(rec, "VOL", TextFormat("%d%%", pct), &g_vol, 0.0f, 1.0f); |
| 开 / 关切换 | 复选框 | GuiCheckBox(rec, "SPEAKER", &g_speakerOn); |
| 按钮形态的开关,适合面板角落 | 开关按钮 | GuiToggle(rec, "MUTE", &g_mute); |
| 有限选项三选一 | 下拉框 | GuiDropdownBox(rec, "LOW;MID;HIGH", &g_quality, false); |
| 整数加减速(1~10 切片数) | 步进器 | GuiSpinner(rec, "SLICES", &g_slices, 1, 10, false); |
两个细节值得留意:滑块右端的数字显示不会自动生成,传NULL就不画,传文本就显示;下拉框的选项是用分号连起来的一个字符串,选中项是int下标,从 0 开始。
返回值语义各控件略有差别,但思路一致。GuiCheckBox在用户真正勾选/取消的那一帧才返回 1,所以if (GuiCheckBox(...)) PlaySoundClick();这种写法天然是"点击瞬间触发一次"。而GuiDropdownBox的第四参editMode语义不同——传false时它自己管理开合,点击展开、松开收起,调用方只管读指针里的选中项;传true则强制保持展开,配合手动翻转的bool可以做常驻展开的列表头。
实战样本:饼图示例右侧的整块控制台
仓库examples/shapes/shapes_pie_chart.c是 raygui 用法的集大成示例:右侧 270 像素宽的面板里,GuiSpinner调切片数、三个GuiCheckBox切换数值/百分比/环形显示、GuiSliderBar控制内环半径,下方GuiScrollPanel里每行还嵌了一个文本框加一个滑块。
读这个文件最大的收获是看清职责边界:切片扇形是用DrawCircleSector自己算角度画的,raygui 只管面板上那些"问用户要参数"的控件;而shapes_splines_drawing.c则展示了另一种混排——样条控制点靠GetMousePosition()自己做命中检测,和下拉框、滑块和平共处。记住这条线:输入与控件渲染归 raygui,业务绘制归你。
每行一个编辑器的列表:滚动面板加剪切区
控件多到放不下时,仓库示例给出了标准答案:GuiScrollPanel给出可视区,BeginScissorMode裁掉滚出屏幕的内容,内容按滚动偏移量平移:
int scroll = 0; // 当前滚动偏移 int h = rows * 34; // 内容总高 GuiScrollPanel((Rectangle){ 20, 40, 220, 200 }, NULL, (Rectangle){ 0, 0, 210, (float)h }, &scroll, &view); BeginScissorMode((int)view.x, (int)view.y, (int)view.width, (int)view.height); for (int i = 0; i < rows; i++) { int y = (int)(view.y + 5 + i * 34 + scroll); if (y > view.y && y < view.y + view.height) GuiSliderBar((Rectangle){ view.x + 10, (float)y, 190, 26 }, NULL, NULL, &values[i], 0.0f, 1.0f); } EndScissorMode();view是可视区在内容坐标下的窗口,scroll每帧由滚动条写回;行循环里那句if是滚动列表的通用优化:滚出视野的行直接跳过声明,反正即时模式没有"不画就不存在"的成本,跳过就是省。这套模式可以原样搬到参数编辑器、音量混音台这类"一行一个值"的界面上。
窗口拉大后控件不跟手的解法
硬编码坐标的面板一拉窗口就错位。修法是把"面板锚在哪条边"写成每帧重算的表达式:
int winW = GetScreenWidth(), winH = GetScreenHeight(); const int panelW = 240; float ax = winW - panelW - 14; // 右缘锚点 float ay = winH - 170 - 14; // 底缘锚点 GuiSliderBar((Rectangle){ ax + 18, ay, (float)(panelW - 36), 24 }, "LV", TextFormat("%d", g_level), &g_level, 1.0f, 5.0f);两个要点:所有控件坐标都从ax/ay派生,窗口尺寸一变,下一帧自动落回正确位置;开了高分屏缩放的平台上,GetScreenWidth/Height返回的已经是逻辑分辨率,别再手动乘 DPI 系数。如果面板外还有可拖拽的自定义实体,每帧拿GetMousePosition()减去实体自己的包围矩形做命中检测即可,raygui 不会挡住你自己写的输入逻辑。
文字超框、中文变方块、分配抖动:三个高频 raygui 踩坑
这三个坑几乎人人会遇到,对应的修复都是一行级:
- 文字贴边或画出框外 → 默认字号配 20 像素高的控件确实挤,
GuiSetStyle(DEFAULT, DEFAULT_TEXT_ALIGNMENT, TEXT_ALIGN_CENTER)调对齐,或把控件高度加大几像素; - 中文显示成方块 → 内置默认字体不含 CJK 字形,
GuiSetFont(LoadFontEx("cjk.ttf", 22, 0, 256));换一张带中文的字库,全局生效; - 每帧
TextFormat导致堆分配频繁 → 把结果缓存进静态char数组,只在数值真正变化时重算,面板帧率立刻稳下来。
收尾:变量为准,然后去改一套主题
raygui 的设计哲学一句话:控件不保存状态,每帧声明、变量为准——布局、主题、开合状态全部写在你自己的代码里,删了重写没有任何心理负担。接下来值得花一个下午的事:把GuiSetStyle的三个参数翻一遍,给滑块、复选框、下拉框各换一组颜色与字号,或者照着饼图示例的滚动列表,给自己的游戏做一个参数编辑器。
【免费下载链接】raylibA simple and easy-to-use library to enjoy videogames programming项目地址: https://gitcode.com/GitHub_Trending/ra/raylib
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考