把 NES 模拟器搬到鸿蒙PC,再连个蓝牙手柄找回童年的感觉
最近晚上闲来无事把手头的 MateBook Pro 翻出来折腾。HarmonyOS 的应用市场逛了一圈没找到好用的 NES 模拟器。想连个蓝牙手柄找回童年的感觉。为什么要干这事事情是这样的翻了翻应用市场现成的 NES 模拟器不好用且无论是虚拟按键还是键盘体验都是不好想支持下接入蓝牙手柄可定制放大屏幕没有源码就没法搞。那就自己搞一个吧。FCEUX 是我比较熟悉的模拟器代码质量高核心部分极其纯净——几乎不依赖操作系统 API纯标准 C 就能编译。于是就有了这个项目ohos_nes_fceux一个跑在 HarmonyOS 上的红白机模拟器基于经典的 FCEUX 的老牌NES模拟器核心。更多交流学习欢迎加入开源鸿蒙PC社区https://harmonypc.csdn.net/欢迎在PC社区平台申请新建项目https://atomgit.com/OpenHarmonyPCDeveloper项目开源地址https://gitcode.com/qq8864/ohos_nes_fceux当然这个移植借助了AI的能力如果你做过 AI 应用或自动化脚本多半遇到过同一种疲惫每家厂商一套账号、一套密钥、一套计费口径想在项目里换个模型常常不是「改一行参数」这么简单而是「再集成一遍」。如果你想体验国外厉害的大模型能力却总是被禁或者服务不稳定。推荐下taotoken这个是csdn官方推出的产品速度流畅稳定可靠。 关键是很便宜性价比不错。taotoken尝鲜入口https://taotoken.net/?uinv_faxm8m42tg11a06futm_sourceinviteTaotoken 的方向很直白把「多模型」收敛成「一条统一网关」。它是 CSDN 生态里的 AI 聚合与分发能力载体——面向开发者常见的调用路径做网关侧的路由与协议适配让你更少折腾基建更多时间花在产品与效果上。谐音梗“掏token”名字起的不错。以后AI时代token就是食粮越来越重要了。详细移植过程参见猫哥的博客把 FCEUX 移植到HarmonyOS鸿蒙PC一个 NES 模拟器的移植笔记使用atomcode deepseekdevcli鸿蒙知识库辅助。推荐atomcde太强了关于AtomCode参见小模型也能写出大工程——AtomCodeClaudeCode国产替代 的介绍及使用先看下最终效果左边方向键、中间游戏画面、右边 AB 键布局参考了横版红白机手柄的样式。顶部一排可以切换 xBRZ / HQ2x / HQ3x 等像素缩放滤镜。娃玩的不亦乐乎眼神里似乎想童年的自己两眼发光的感觉太好玩了。这项目是怎么一回事简单说就是把有 20 多年历史的 FCEUX 模拟器移植到了 HarmonyOS 上。FCEUX 是目前最活跃的 NES 模拟器之一代码质量很高核心部分6502 CPU、PPU 渲染、APU 音频、230 多种卡带映射器几乎不依赖操作系统 API。移植的核心思路是模拟器核心基本不动只重写驱动层和 UI。整体架构分四层ArkTS UI (Index.ets) ↓ NAPI 桥 C Native Layer (NAPI Module) ↓ 函数调用 HarmonyOS 驱动层 (ohos_driver) ↓ FCEUX 核心 API FCEUX 核心 (6502/PPU/APU/230 Mapper)视频用 XComponent Surface OH_NativeWindow 原生渲染像素数据直接从 C 写入缓冲区不走 Canvas音频OH_AudioRenderer NDK 原生播放APU 生成的 PCM 数据通过环形缓冲消费输入ArkTS 的onKeyEvent捕获按键 虚拟手柄触摸事件转成 8 位掩码传给核心蓝牙手柄到底能不能用这是个好问题。项目原本只写了键盘映射A/B/S/T 方向键和触屏虚拟手柄蓝牙手柄的支持其实是个半成品——D-Pad 方向键能用但右侧的 A/B/X/Y 功能键一律没反应。原因是不同的蓝牙手柄发送的按键码keyCode不一样代码里只硬编码了 PS4 手柄的几个按键码其他手柄比如 Switch Pro、Xbox、各种杂牌蓝牙手柄的按键码都没有匹配。手柄的接入很简单其实还是监听的onKeyEvent.onKeyEvent((event:KeyEvent){this.lastKeyTextevent.keyText;this.lastKeyCodeevent.keyCode;this.lastKeyTypeevent.type;letkt:stringevent.keyText;letkc:numberevent.keyCode;letisDown:boolean(event.type0);letbit:number-1;// keyText detection (letters arrow key names)if(ktkt.length0){lettkt.toUpperCase();if(tA||tKEYCODE_A)bit0;elseif(tB||tKEYCODE_B)bit1;elseif(tS||tKEYCODE_S)bit2;elseif(tT||tKEYCODE_T)bit3;elseif(tKEYCODE_DPAD_UP)bit4;elseif(tKEYCODE_DPAD_DOWN)bit5;elseif(tKEYCODE_DPAD_LEFT)bit6;elseif(tKEYCODE_DPAD_RIGHT)bit7;}// keyCode fallback (keyboard PS4 gamepad)if(bit0){if(kc2012)bit4;// Keyboard Upelseif(kc2013)bit5;// Keyboard Downelseif(kc2014)bit6;// Keyboard Leftelseif(kc2015)bit7;// Keyboard Right// PS4 gamepadelseif(kc2301)bit0;// × (Cross) → NES Aelseif(kc2302)bit0;// ○ (Circle) → NES Belseif(kc2311)bit2;// SHARE → NES Selectelseif(kc2312)bit3;// OPTIONS → NES Startelseif(kc19)bit4;// D-Pad Upelseif(kc20)bit5;// D-Pad Downelseif(kc21)bit6;// D-Pad Leftelseif(kc22)bit7;// D-Pad Rightelseif(kc2303)bit0;// □ (Square) → NES A (alt)elseif(kc2304)bit1;// △ (Triangle) → NES B (alt)elseif(kc2307)bit2;// L1 → NES Select (alt)elseif(kc2308)bit3;// R1 → NES Start (alt)}if(bit0){if(isDown)this.padState|(1bit)elsethis.padState~(1bit)}})从某多多上花三十块大洋就买到一个不错的蓝牙手柄。手柄首次蓝牙接入方法参见你买的手柄提供的说明书。怎么调试手柄键值我在底部加了一个调试显示条格式是这样的Key: 按键名 [code键值 type0按下/1松开]打开游戏后连上蓝牙手柄按右侧的功能键底部的绿色文字会实时显示对应的 keyCode。比如说你按了手柄的 A 键底部显示Key: [code2301 type0]那 2301 就是这个手柄的 A 键码。Type0 表示按下Type1 表示松开。怎么把自己的手柄键值加进去找到entry/src/main/ets/pages/Index.ets文件在onKeyEvent处理函数里有一段 keyCode 匹配的代码// keyCode fallbackif(bit0){// ... 原有映射elseif(kc2301)bit0;// × (Cross) → NES Aelseif(kc2302)bit1;// ○ (Circle) → NES Belseif(kc2311)bit2;// SHARE → NES Selectelseif(kc2312)bit3;// OPTIONS → NES Start// ... 更多映射}NES 手柄的 8 个键对应的比特位是BitNES 按键0A1B2Select3Start4↑ (上)5↓ (下)6← (左)7→ (右)假设你的蓝牙手柄按 A 键显示 code2301那添加一行else if (kc 2301) bit 0;就能把那个键映射到 NES 的 A 键。同理B 键是 bit1Select 是 bit2Start 是 bit3。加完之后重新编译安装手柄的按键就能正常玩游戏啦。踩过的几个坑坑 1顶部滤镜按钮拦截手柄事件一开始发现手柄的方向键能用但功能键老是触发顶部的滤镜切换。查了一下原来是顶部按钮获得了焦点手柄按键激活了按钮的onClick。解决给所有顶栏按钮加.focusable(false)让它们不参与焦点导航手柄事件直接穿透到游戏的onKeyEvent处理器。坑 2按键响应慢每次按键都通过 NAPIJS ↔ C 桥调用一次setPadState频繁的跨语言调用开销不小。解决改成帧循环模式——所有按键只更新 ArkTS 侧的位掩码纯 JS 操作帧循环每 16ms 一次统一把状态同步到 C 层。NAPI 调用从每次按键都触发变成每帧最多一次。坑 3音频没声音FCEUX 的音频数据是 int32 格式但值域其实在 int16 范围内。直接(int16_t)sample转就行但我不小心多写了个 8结果声音衰减到 1/256差不多静音。查了大半天 hilog 日志才发现。性能表现在 MateBook Pro 上实测帧率稳定 60fps内存 ~50MBCPU 占用 ~15%单核HAP 包 6.6MB后续想加的功能存档 / 读档FCEUX 核心支持完整缺个 UI自定义按键映射在界面上可视化配置不用改代码金手指 Cheat 码输入开源项目代码在 gitcode上有兴趣的朋友可以直接拿去编译玩玩https://gitcode.com/qq8864/ohos_nes_fceux欢迎 PR尤其是各种蓝牙手柄的按键码——收集齐了就能做一个通用的手柄映射库大家都不用重复踩坑了。