给 Rust 游戏加暂停菜单:egui 接入 Bevy 与 Miniquad 的完整指南
【免费下载链接】eguiegui: an easy-to-use immediate mode GUI in Rust that runs on both web and native项目地址: https://gitcode.com/GitHub_Trending/eg/egui
egui 是一个纯 Rust 的即时模式 GUI 库,把它的 egui 集成做进游戏引擎,核心工作量只有两件事:每帧把输入喂给它、把它输出的三角形画出来。这篇 Rust 游戏 GUI 教程以"给游戏加一个暂停菜单"为任务推进,讲清 egui 怎么接进 Bevy 和 Miniquad、为什么能跑通、每帧成本有多高、常见坑怎么修。读完你能拿到一个可运行的接入路径,和一份对照排查的坑位清单。
3 步跑通第一个暂停菜单 🎮
先不管引擎细节,目标是:游戏按 Esc 后弹出一个带"继续游戏"按钮的窗口。
- 建一个独立的 UI 状态。egui 是即时模式——每帧把界面重画一遍,好处是界面永远反映最新状态,所以你不需要"创建窗口、持有句柄、销毁窗口"这一套,只需要在每帧的回调里声明它。
- 把输入喂给 egui,让它算出该画什么、该响应什么点击。
- 把 egui 输出的网格交给引擎的渲染器画上去。
以 Bevy 为例,前两步被bevy_egui插件包掉了,你只写第 2 步里的 UI 代码:
[dependencies] bevy = "0.11" bevy_egui = "0.22"fn pause_menu(mut egui_ctx: ResMut<EguiContext>) { egui::Window::new("暂停").show(egui_ctx.ctx_mut(), |ui| { if ui.button("继续游戏").clicked() { // 把游戏状态切回 Running } }); }窗口、拖动、失焦、点击判定都是窗口本身处理的,你只关心按钮是否被点。这一步跑通后,菜单已经能盖在 3D 场景上了——因为 egui 本身不管窗口也不管显卡,这两件事都归引擎管。
即时模式:为什么它和游戏循环合拍
保留模式 GUI(传统方式)先构建一棵持久的控件树,靠回调通知状态变化;即时模式每帧从零声明一遍界面,if ui.button("继续").clicked()这种写法里,"是否被点"是当场算出来的,没有任何回调要接。
这对游戏是天然契合的:你的游戏循环本来就是每帧跑一次,UI 逻辑和游戏逻辑在同一个节拍上,不存在"游戏改了血量、血条还显示旧值"的同步问题,也不用维护控件的生命周期。代价是每帧都做一次完整布局,所以下一节的性能话题绕不开它。官方 README 里 crates/egui/README.md 的 "Why immediate mode" 一节把这个取舍讲得很透,值得一读。
选型问答:Bevy 和 Miniquad 各走哪条桥
egui 的官方集成只负责"纯 Rust 环境"(crates/eframe、crates/egui-winit、crates/egui-wgpu),接进第三方引擎靠的是社区桥接库,官方 README 在 Integrations 一节也明确把 Bevy 和 Miniquad 归为第三方集成。两条路怎么选,问自己三个问题:
- 谁管窗口和输入?如果你的引擎自己管(Bevy 的
Input插件、miniquad 的事件回调),egui 只需要被转发事件,不需要再建一套窗口系统。两者都符合,此项打平。 - 你的渲染器是什么?Bevy 默认 wgpu,Miniquad 给你裸 OpenGL。egui 官方提供 crates/egui_glow 和 crates/egui-wgpu 两套绘制器,桥接库各自封装其一——选引擎渲染栈匹配的那条,别自己混搭。
- 你想自己控制每帧时序吗?Miniquad 风格更底层:输入回调、
run、paint三步都摆在你手里(流程示意):
fn render() { forward_input_to_egui(); // 1. 转发 miniquad 输入 let out = egui_ctx.run(raw, |ctx| pause_menu_ui(ctx)); // 2. 跑 UI egui_miniquad::paint(out); // 3. 提交三角形 }Bevy 路线把这三步藏进插件,你只写 UI 代码,适合 UI 是"附属品"的项目;Miniquad 路线每步可见,适合 UI 与游戏逻辑深度耦合(比如自定义光标、逐帧拦截按键)的项目。不确定就先走 Bevy,跑通后再看要不要降层。
每帧成本:性能注意什么
即时模式的账单是每帧一次完整布局,但官方 README 给出的经验值是有参考的:典型应用 egui 每帧占 1~2 ms,并且只在有交互或动画时重绘,界面静止时基本不花 CPU。真正会咬人的是内容规模:
- 超大滚动区。滚动列表里塞上万行,每帧都要给它们算布局。做法是只布局可见范围附近的内容,或把长历史裁掉。
- 纹理碎片。文字和图片走纹理图集(crates/epaint/src/texture_atlas.rs),频繁上传零散小图会拖慢绘制,尽量让图标、贴图复用同一批纹理。
- 字体别在运行时现装。中文等字体在启动阶段一次性装好,参考 examples/custom_font/src/main.rs 的
set_fonts写法,避免游戏中途切场景时卡顿。 - 3D 画面叠 UI 的顺序。游戏先画场景、egui 后画网格,顺序反了菜单会被场景盖住;反向需求(在 UI 里嵌 3D)可以看 examples/custom_3d_glow/src/main.rs,它演示了用
PaintCallback在 egui 区域内画 3D。
三个高频坑:现象、原因、解法 ⚠️
按"现象→原因→解法"排查:
- 输入被抢走或双发现象:菜单开着时按方向键,角色也在动;或按键进了 UI 却进不了游戏。 原因:引擎和 egui 各自消费了一遍原始事件,没人"先问对方要不要"。 解法:固定一个优先级——本帧事件先交给 egui,
ctx.input里能看到它实际消费了哪些,剩下的再发给游戏逻辑;暂停时干脆不往游戏层转发。事件结构定义在 crates/egui/src/data/input/,对照它写过滤逻辑。 - 高分屏上文字发虚现象:同一套 UI,普通显示器清晰,Retina 屏模糊。 原因:egui 用"逻辑点"计算布局,物理像素密度没传对,网格被拉伸了。 解法:把真实像素密度喂进去,入口是 crates/egui/src/context.rs 里的
Context::set_pixels_per_point(它最终换算为 zoom factor),确保引擎报告的缩放系数和视口尺寸一致。 - 中文显示成方块现象:英文正常,中文全是豆腐块。 原因:默认字体(crates/epaint_default_fonts/fonts)只覆盖拉丁字符和 emoji,不含 CJK。 解法:自备
.ttf/.otf中文字体,用Context::set_fonts装进去并调到Proportional字体族首位,完整写法在 examples/custom_font。
还有一个小坑值得记一下:egui 默认拿窗口标题当 ID。两个动态标题的窗口重名时,位置、展开状态会互相串。给动态窗口显式指定独立 ID 即可。
下一步:跑一遍再往下学
现在就执行:
cargo run --release -p egui_demo_app这个演示应用里每个窗口的源码都能在 crates/egui_demo_lib/src/demo 找到对应文件,看到什么就去读对应文件,比通读文档快得多。想深入原理,读 crates/egui/README.md 的 "Why immediate mode" 与 "Integrations" 两节;想再动手,examples/ 目录里hello_world、custom_keypad、multiple_viewports是从简单到进阶的一条完整路径。
【免费下载链接】eguiegui: an easy-to-use immediate mode GUI in Rust that runs on both web and native项目地址: https://gitcode.com/GitHub_Trending/eg/egui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考