池塘边的榕树上,知了在声声叫着夏天。操场边的秋千上,只有蝴蝶停在上面。而我闭上眼,仿佛回到了那个无忧无虑的童年时光,心中充满了对经典游戏的怀念。那些年,NES游戏机陪伴我度过了数不清的日日夜夜。
如今,随着技术的发展和设备生态的不断扩展,我决定将这个记忆中的伙伴--NES 模拟器(JSNES)移植到 HarmonyOS 鸿蒙系统上,让更多的用户能够在 HarmonyOS 设备上重温那些难忘的时光。
本指南将介绍如何将 JavaScript NES 模拟器(JSNES)移植到 HarmonyOS 系统,让更多用户能够在 HarmonyOS 设备上体验 NES 游戏的乐趣。

JSNES 简介
JSNES是一款使用JavaScript编写的模拟器,能够完美重现小霸王游戏机的经典体验。这款模拟器不仅展示了JavaScript语言的强大功能,同时也揭示了不同浏览器JavaScript引擎之间的性能差异。
JSNES 是一个用纯 JavaScript 编写的 NES(Nintendo Entertainment System)游戏模拟器。它可以在支持 JavaScript 的环境中运行,无需额外的插件或软件安装。JSNES 的主要特点包括:
- 高兼容性:支持大量的 NES 游戏 ROM 文件,用户可以通过简单的 ROM 文件加载即可体验到经典游戏。
- 可移植性:由于是基于 JavaScript 编写的,JSNES 可以很容易地移植到各种支持 JavaScript 的运行环境中,包括浏览器、Node.js 以及基于 JavaScript 的操作系统。
- 开源许可:JSNES 使用 MIT 许可证分发,这意味着开发者可以自由地使用、修改和分发该模拟器,促进了游戏模拟技术的发展和社区共享。
https://github.com/bfirsh/jsnes
移植的意义
将 JSNES 移植到 HarmonyOS 系统,具有以下重要意义:
- 丰富应用生态:为 HarmonyOS 应用商店引入更多的游戏内容,满足用户多样化的娱乐需求。
- 促进技术交流:通过移植的过程,可以加深开发者对 HarmonyOS 和 JavaScript 应用开发的理解,促进技术交流和创新。
- 增强用户粘性:提供独特的游戏体验,增加用户对 HarmonyOS 设备的兴趣和粘性,有助于提高 HarmonyOS 的市场份额和品牌影响力。
已完成的修改
为了使 JSNES 更好地适应 HarmonyOS 环境,我们对其源代码进行了相应的调整和转换,主要更改包括:
-
模块导入导出适配:
- 使用
import
替代require()
- 使用
export default
和export
替代module.exports
- 保持了对 CommonJS 环境的向后兼容性
- 使用
-
核心文件修改:
src/index.js
- 主入口文件src/controller.js
- 控制器模块src/nes.js
- NES 核心模块src/ppu.js
- 图形处理单元模块src/papu.js
- 音频处理单元模块src/cpu.js
- CPU 模拟模块src/rom.js
- ROM 加载模块src/mappers.js
- 内存映射模块src/tile.js
- 图形瓦片模块src/utils.js
- 工具函数模块
在 HarmonyOS 中使用 JSNES
移植完成后,我们可以在 HarmonyOS 应用中使用 JSNES。下面将具体介绍如何操作。
1. 创建 HarmonyOS 项目
首先,打开 DevEco Studio 创建一个新的 HarmonyOS 应用项目。
2. 添加 JSNES 源码
将修改后的 JSNES 源码放置到 HarmonyOS 项目的 src/main/ets
目录中。
3. 创建 NES 播放器组件
在 src/main/ets
目录下创建一个 ArkTS 文件,例如 NESPlayer.ets
,该文件将用于实现 NES 播放器组件。
typescript
// NESPlayer.ets
import { Canvas, CanvasRenderingContext2D, ImageData } from '@ohos.canvas';
import { ResourceManager } from '@ohos.resourceManager';
import { KeyEvent, KeyCode } from '@ohos.multimodalInput.keyEvent';
import JSNES from './jsnes/src/index.js';
// 定义常量
const SCREEN_WIDTH: number = 256;
const SCREEN_HEIGHT: number = 240;
const FRAMEBUFFER_SIZE: number = SCREEN_WIDTH * SCREEN_HEIGHT;
export class NESPlayer {
private canvas: Canvas;
private canvasCtx: CanvasRenderingContext2D;
private image: ImageData;
private framebufferU8: Uint8ClampedArray;
private framebufferU32: Uint32Array;
private nes: any;
private animationId: number = 0;
constructor(canvas: Canvas) {
this.canvas = canvas;
this.initialize();
}
private initialize(): void {
// 初始化Canvas和JSNES
this.canvas.width = SCREEN_WIDTH;
this.canvas.height = SCREEN_HEIGHT;
this.canvasCtx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
this.image = this.canvasCtx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT);
const buffer = new ArrayBuffer(this.image.data.length);
this.framebufferU8 = new Uint8ClampedArray(buffer);
this.framebufferU32 = new Uint32Array(buffer);
// 初始化JSNES
this.nes = new JSNES.NES({
onFrame: (framebuffer24: Uint8Array) => {
// 处理帧数据
for (let i = 0; i < FRAMEBUFFER_SIZE; i++) {
const pixel = framebuffer24[i];
const r = (pixel >> 16) & 0xFF;
const g = (pixel >> 8) & 0xFF;
const b = pixel & 0xFF;
this.framebufferU32[i] = 0xFF000000 | (r << 16) | (g << 8) | b;
}
// 绘制到Canvas
this.image.data.set(this.framebufferU8);
this.canvasCtx.putImageData(this.image, 0, 0);
},
onStatusUpdate: (status: string) => {
console.log(`NES 状态:${status}`);
}
});
}
// 加载ROM
async loadRomFromResource(resourceManager: ResourceManager, resourceId: number): Promise<void> {
try {
const romData: Uint8Array = await resourceManager.getRawFileContent(resourceId);
// 将Uint8Array转换为字符串
let romString: string = '';
for (let i = 0; i < romData.length; i++) {
romString += String.fromCharCode(romData[i]);
}
this.nes.loadROM(romString);
this.startRender();
} catch (error) {
console.error(`加载 ROM 失败: ${error}`);
}
}
// 开始渲染
private startRender(): void {
const renderLoop = (): void => {
this.nes.frame();
this.animationId = requestAnimationFrame(renderLoop);
};
renderLoop();
}
// 停止渲染
stopRender(): void {
if (this.animationId > 0) {
cancelAnimationFrame(this.animationId);
this.animationId = 0;
}
}
// 处理按键输入
handleKeyEvent(event: KeyEvent): void {
const keyCode = event.keyCode;
const isPressed = event.type === 'keydown';
// 映射键盘按键到NES控制器
switch (keyCode) {
case KeyCode.KEY_UP:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_UP);
break;
case KeyCode.KEY_DOWN:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_DOWN);
break;
case KeyCode.KEY_LEFT:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_LEFT);
break;
case KeyCode.KEY_RIGHT:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_RIGHT);
break;
case KeyCode.KEY_X:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_A);
break;
case KeyCode.KEY_Z:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_B);
break;
case KeyCode.KEY_ENTER:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_START);
break;
case KeyCode.KEY_SHIFT_LEFT:
this.nes.buttonUp(isPressed ? 0 : 1, JSNES.Controller.BUTTON_SELECT);
break;
}
}
}
4. 配置页面使用 NES 播放器
在主页面中添加 Canvas 组件,并使用 NESPlayer 类。
typescript
// MainPage.ets
import { NESPlayer } from './NESPlayer';
import { Canvas, CanvasController } from '@ohos.canvas';
import { resourceManager } from '@ohos.application';
import { KeyEvent } from '@ohos.multimodalInput.keyEvent';
@Entry
@Component
struct MainPage {
private nesPlayer: NESPlayer | null = null;
private canvasController: CanvasController = new CanvasController();
build() {
Column() {
Canvas(this.canvasController)
.width('100%')
.height('80%')
.onReady((canvas: Canvas) => {
this.nesPlayer = new NESPlayer(canvas);
// 加载ROM
this.loadRom();
})
.onKeyEvent((event: KeyEvent) => {
if (this.nesPlayer) {
this.nesPlayer.handleKeyEvent(event);
}
})
}
.width('100%')
.height('100%')
}
async loadRom(): Promise<void> {
try {
const rm = await resourceManager.getResourceManager();
// 假设ROM文件ID为$rawfile.test_rom
await this.nesPlayer?.loadRomFromResource(rm, $rawfile.test_rom);
} catch (error) {
console.error(`加载 ROM 失败: ${error}`);
}
}
aboutToDisappear(): void {
// 停止渲染
this.nesPlayer?.stopRender();
}
}
5. 添加 ROM 文件
将 NES ROM 文件放入项目的 resources/rawfile
目录中。
6. 配置文件更新
在 config.json
中添加必要的权限,以确保应用可以访问 ROM 文件。
json
{
"module": {
"reqPermissions": [
{
"name": "ohos.permission.READ_MEDIA"
}
]
}
}
注意事项
-
性能优化:
- NES 模拟器对性能要求较高,考虑使用多线程来提高整体性能
- 如果需要,可以关闭音频模拟以提高渲染效率
-
音频适配:
- HarmonyOS 的音频 API 与 Web Audio API 不同,需要特别适配
-
内存管理:
- 确保及时释放不再使用的资源
- 注意 HarmonyOS 的内存使用限制
-
兼容性:
- 虽然代码已经转换为 ES6 模块语法,但 HarmonyOS 可能还需要额外的适配
- 测试不同的 ROM 文件以确保兼容性
调试技巧
在开发过程中,使用 DevEco Studio 的日志功能查看运行时信息,使用 HarmonyOS 的性能分析工具监控性能,逐步调试,确保每个模块正常工作。
后续优化方向
- 实现 ROM 管理功能
- 添加游戏存档/读档功能
- 优化渲染性能
- 添加音频支持
- 实现虚拟手柄UI
通过以上步骤和注意事项,您可以成功地将 JSNES 移植到 HarmonyOS 系统,并享受 NES 游戏带来的乐趣。希望本指南对您有所帮助!
参考链接
https://www.showapi.com/news/article/66cec77a4ddd79f11a14363c