正文
做了个迷宫游戏,本来只是控制台里打印字符画来着。某天觉得不过瘾,干脆一路做到了炸弹人------吃宝箱得炸弹、2 秒引信、十字爆炸、炸怪物,没躲开连自己一起送走 。最终产物是一个 666KB 的单文件 exe,双击即玩,没有任何外部依赖。

这篇聊聊它是怎么搭起来的。
一句话架构
C++ 负责规则,Web 负责好看。
- C++ 后端(MSVC 编译):迷宫生成、BFS 最短路径、炸弹引信、怪物游走、碰撞判定,跑一个 120ms 的主循环,每帧把全量状态推给前端;
- Web 前端(Canvas 2D):墙体、金币、宝箱、怪物、炸弹、爆炸火焰全部手绘,帧间插值动画,移动丝滑;
- 中间是自研框架 TauriCPP(C++/Win32/WebView2):
Bridge双向通信(invoke命令 +Emit事件),前端资源编译期打包进 exe 的 rc,运行时从内存 VirtualFS 加载------不落临时文件,用户看到的只有一个 exe。
几个做得挺舒服的点
1. 游戏逻辑全部在 C++,且可单测。
移动、放弹、爆炸杀伤、误伤、复活这些规则都写成了纯函数(DoMove / TryPlaceBomb / GameTick),配了一个 game_test,连跑多种子回归。改玩法先写测试,心里不慌。
2. 前端不存权威状态。
所有规则以后端为准,前端只做渲染和输入。每次 move 拿回全量快照直接覆盖渲染目标------状态同步想出错都难。
3. 怪物移动"不瞬移"。
后端 0.8 秒走一格,前端拿到新坐标后按 70ms 时间常数做指数插值,看起来就是在"游荡"。怪物还有稳定 id,被炸死/复活后动画表自动增删。炸弹引信越短,脉冲抖动越快------压迫感是渲染出来的,不是规则。
4. 音效也是程序生成的。
没有引入任何音频文件:背景伴奏是 Am-F-C-G 四小节循环(三角波琶音 + 高通噪声打击),吃币、开箱、放弹、爆炸、阵亡全部用 Web Audio Oscillator 现场合成。exe 体积零增长。
踩过的坑(最有价值的部分)
坑一:吃个金币,整个游戏白了。
上线"金币"功能后,一吃金币画面永久空白。最后的现场证据是一行报错:
Failed to execute 'arc' on 'CanvasRenderingContext2D':
The radius provided (-14.292) is negative.
两个错误叠在一起:拾取特效用了两套时钟(performance.now() 绝对时间 vs rAF 的相对时间),算出的特效半径为负;而 WebView2 对负半径会抛异常,普通 Chromium 只会静默忽略 ------所以我在 Chrome 里怎么点都复现不出来。异常跳出 rAF 回调后 requestAnimationFrame(loop) 再也不会执行,画布停在 clear 之后的纯色上。
修复:统一时钟基准 + 半径 clamp + 每帧快照兜底(draw 抛异常就恢复上一帧)。结论:WebView2 和 Chrome 的容错不一样,别拿 Chrome 当免测金牌。
坑二:缺失一个 DOM 元素,游戏从开局就白屏。
改版时删了个统计节点,忘了删 JS 里的引用,null.textContent 抛错 → newGame 的 Promise reject → init() 提前终止 → 渲染循环从未启动。一帧都没画过。
坑三:SendKeys 自动化把测试输出发到了我的微信里。
一开始想用 SendKeys 驱动 GUI 测试,结果游戏窗口被微信盖住,按键全发进了聊天框......羞愧。后来换成 CDP 直连 :WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222 一指,playwright connectOverCDP 上来就能截图、派发真实键盘事件、读页面状态------不抢焦点、不打扰桌面、还能抓 console。上面两个白屏 bug 都是这么逮到的。
数据
| 指标 | 值 |
|---|---|
| exe 体积 | 666 KB(图标占了 93KB) |
| 难度 | 21×11 ~ 51×29 四档 |
| 怪物 | 1~4 只 / 局 |
| 主题 | 7 套,CSS 变量 + Canvas 双路切换 |
| 游戏循环 | 120ms tick |
| 音频文件 | 0 |
仓库
Windows + VS2022 一把梭:git clone 下来跑 .\build.ps1。踩坑经验都写在 README 和提交历史里了,欢迎来交流。
#C++ #WebView2 #桌面开发 #游戏开发 #TauriCPP