news 2026/6/25 12:37:13

把 NES 模拟器搬到鸿蒙PC,再连个蓝牙手柄找回童年的感觉

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
把 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/?u=inv_faxm8m42tg11a06f&utm_source=invite

Taotoken 的方向很直白:把「多模型」收敛成「一条统一网关」。它是 CSDN 生态里的 AI 聚合与分发能力载体——面向开发者常见的调用路径,做网关侧的路由与协议适配,让你更少折腾基建,更多时间花在产品与效果上。谐音梗“掏token”,名字起的不错。以后AI时代,token就是食粮,越来越重要了。

详细移植过程参见猫哥的博客:把 FCEUX 移植到HarmonyOS鸿蒙PC:一个 NES 模拟器的移植笔记

使用atomcode +deepseek+devcli+鸿蒙知识库辅助。推荐atomcde,太强了!

关于AtomCode,参见:小模型也能写出大工程——AtomCode(ClaudeCode国产替代) 的介绍及使用

先看下最终效果:

左边方向键、中间游戏画面、右边 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.lastKeyText=event.keyText;this.lastKeyCode=event.keyCode;this.lastKeyType=event.type;letkt:string=event.keyText;letkc:number=event.keyCode;letisDown:boolean=(event.type===0);letbit:number=-1;// keyText detection (letters + arrow key names)if(kt&&kt.length>0){lett=kt.toUpperCase();if(t==='A'||t==='KEYCODE_A')bit=0;elseif(t==='B'||t==='KEYCODE_B')bit=1;elseif(t==='S'||t==='KEYCODE_S')bit=2;elseif(t==='T'||t==='KEYCODE_T')bit=3;elseif(t==='KEYCODE_DPAD_UP')bit=4;elseif(t==='KEYCODE_DPAD_DOWN')bit=5;elseif(t==='KEYCODE_DPAD_LEFT')bit=6;elseif(t==='KEYCODE_DPAD_RIGHT')bit=7;}// keyCode fallback (keyboard + PS4 gamepad)if(bit<0){if(kc===2012)bit=4;// Keyboard Upelseif(kc===2013)bit=5;// Keyboard Downelseif(kc===2014)bit=6;// Keyboard Leftelseif(kc===2015)bit=7;// Keyboard Right// PS4 gamepadelseif(kc===2301)bit=0;// × (Cross) → NES Aelseif(kc===2302)bit=0;// ○ (Circle) → NES Belseif(kc===2311)bit=2;// SHARE → NES Selectelseif(kc===2312)bit=3;// OPTIONS → NES Startelseif(kc===19)bit=4;// D-Pad Upelseif(kc===20)bit=5;// D-Pad Downelseif(kc===21)bit=6;// D-Pad Leftelseif(kc===22)bit=7;// D-Pad Rightelseif(kc===2303)bit=0;// □ (Square) → NES A (alt)elseif(kc===2304)bit=1;// △ (Triangle) → NES B (alt)elseif(kc===2307)bit=2;// L1 → NES Select (alt)elseif(kc===2308)bit=3;// R1 → NES Start (alt)}if(bit>=0){if(isDown)this.padState|=(1<<bit)elsethis.padState&=~(1<<bit)}})

从某多多上花三十块大洋就买到一个不错的蓝牙手柄。手柄首次蓝牙接入方法,参见你买的手柄提供的说明书。

怎么调试手柄键值?

我在底部加了一个调试显示条,格式是这样的:

Key: <按键名> [code=<键值> type=<0=按下/1=松开>]

打开游戏后连上蓝牙手柄,按右侧的功能键,底部的绿色文字会实时显示对应的 keyCode。

比如说你按了手柄的 A 键,底部显示Key: [code=2301 type=0],那 2301 就是这个手柄的 A 键码。

Type=0 表示按下,Type=1 表示松开。

怎么把自己的手柄键值加进去?

找到entry/src/main/ets/pages/Index.ets文件,在onKeyEvent处理函数里,有一段 keyCode 匹配的代码:

// keyCode fallbackif(bit<0){// ... 原有映射elseif(kc===2301)bit=0;// × (Cross) → NES Aelseif(kc===2302)bit=1;// ○ (Circle) → NES Belseif(kc===2311)bit=2;// SHARE → NES Selectelseif(kc===2312)bit=3;// OPTIONS → NES Start// ... 更多映射}

NES 手柄的 8 个键对应的比特位是:

BitNES 按键
0A
1B
2Select
3Start
4↑ (上)
5↓ (下)
6← (左)
7→ (右)

假设你的蓝牙手柄按 A 键显示 code=2301,那添加一行else if (kc === 2301) bit = 0;就能把那个键映射到 NES 的 A 键。同理,B 键是 bit=1,Select 是 bit=2,Start 是 bit=3。

加完之后重新编译安装,手柄的按键就能正常玩游戏啦。

踩过的几个坑

坑 1:顶部滤镜按钮拦截手柄事件

一开始发现手柄的方向键能用,但功能键老是触发顶部的滤镜切换。查了一下,原来是顶部按钮获得了焦点,手柄按键激活了按钮的onClick

解决:给所有顶栏按钮加.focusable(false),让它们不参与焦点导航,手柄事件直接穿透到游戏的onKeyEvent处理器。

坑 2:按键响应慢

每次按键都通过 NAPI(JS ↔ C++ 桥)调用一次setPadState,频繁的跨语言调用开销不小。

解决:改成帧循环模式——所有按键只更新 ArkTS 侧的位掩码(纯 JS 操作),帧循环(每 16ms 一次)统一把状态同步到 C++ 层。NAPI 调用从"每次按键都触发"变成"每帧最多一次"。

坑 3:音频没声音

FCEUX 的音频数据是 int32 格式,但值域其实在 int16 范围内。直接(int16_t)sample转就行,但我不小心多写了个>> 8,结果声音衰减到 1/256,差不多静音。查了大半天 hilog 日志才发现。

性能表现

在 MateBook Pro 上实测:

  • 帧率稳定 60fps
  • 内存 ~50MB
  • CPU 占用 ~15%(单核)
  • HAP 包 6.6MB

后续想加的功能

  • 存档 / 读档(FCEUX 核心支持完整,缺个 UI)
  • 自定义按键映射(在界面上可视化配置,不用改代码)
  • 金手指 Cheat 码输入

开源

项目代码在 gitcode上,有兴趣的朋友可以直接拿去编译玩玩:

https://gitcode.com/qq8864/ohos_nes_fceux

欢迎 PR,尤其是各种蓝牙手柄的按键码——收集齐了就能做一个通用的手柄映射库,大家都不用重复踩坑了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/25 12:36:37

大模型入门必看:小白程序员轻松掌握Agent与AI核心技能(收藏版)

本文深入浅出地介绍了大模型的核心概念与技术&#xff0c;包括LLM的预训练、微调、幻觉现象&#xff0c;以及Agent的规划、记忆和工具调用能力。文章还详细解析了MCP协议、Token、RAG检索增强生成、记忆模块、Skill、ReAct反应机制等关键要素&#xff0c;并探讨了Agent的自我反…

作者头像 李华
网站建设 2026/6/25 12:33:21

有限元静力学计算验证-有理论计算结果对比——网格对弧形结构影响较大,矩形影响不大。——采用了一维线体梁单元-横截面矩形和圆形对比-三维计算结果对比-矩形表面和圆柱形表面!

网格对弧形结构影响较大,矩形影响不大 “悬臂问题描述:长为 2000 mm、横截面为 50 mm 的梁,在 100 N 作用下的最大挠度,材料默认为结构钢。” w计算的结果:横截面50*50mm,采用线体一维单元计算,2.5612mm,和chat计算结果一致。发现ds计算过程都会出错,ds专业性还是不够…

作者头像 李华
网站建设 2026/6/25 12:33:15

栖影 AI 落地实践:中小电商团队零代码搭建自动化视觉生产流水线

摘要 针对中小电商团队 AI 视觉生产门槛高、流程碎片化、输出标准不统一的普遍问题&#xff0c;本文提出一套零代码、低成本的自动化视觉生产流水线方案。方案基于多模态生成工具的参数复用、批量处理与跨模块联动能力&#xff0c;从模板标准化、批量生成、多平台适配、资产沉…

作者头像 李华
网站建设 2026/6/25 12:31:12

告别LLM能力边界!30分钟掌握AI Tools调用核心逻辑

作为开发者&#xff0c;你是否曾困惑&#xff1a; 为什么大模型能查股票价格、查天气&#xff1f; 一个只会“词语接龙”的概率模型&#xff0c;怎么突破虚拟限制调用外部工具&#xff1f; 明明训练数据里没有实时数据&#xff0c;却能返回精准的股票收盘价&#xff1f; 这篇文…

作者头像 李华
网站建设 2026/6/25 12:29:03

04-性能优化与最佳实践——06. React Compiler - 自动记忆化

06. React Compiler - 自动记忆化 一、5W1H 概述维度内容WhatReact 团队的自动记忆化编译器&#xff0c;自动优化组件渲染Why无需手动编写 useMemo、useCallback、React.memoWhenReact 19 项目WhereBabel/Vite 配置中启用Who希望简化性能优化的开发者How配置 babel-plugin-reac…

作者头像 李华
网站建设 2026/6/25 12:27:55

企业官网的长期持有成本:年费构成、隐性收费与TCO测算

企业官网的长期持有成本&#xff1a;年费构成、隐性收费与TCO测算 "官网每年交多少钱"看似是报价问题&#xff0c;工程上其实是 TCO&#xff08;Total Cost of Ownership&#xff0c;总拥有成本&#xff09; 问题&#xff1a;一次性投入 逐年运行成本 潜在的锁定成…

作者头像 李华