梦回童年,将JSNES 游戏模拟器移植到 HarmonyOS 移植指南
池塘边的榕树上,知了在声声叫着夏天。操场边的秋千上,只有蝴蝶停在上面。而我闭上眼,仿佛回到了那个无忧无虑的童年时光,心中充满了对经典游戏的怀念。那些年,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 播放器组件。
// 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和JSNESthis.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);// 初始化JSNESthis.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;}// 绘制到Canvasthis.image.data.set(this.framebufferU8);this.canvasCtx.putImageData(this.image, 0, 0);},onStatusUpdate: (status: string) => {console.log(`NES 状态:${status}`);}});}// 加载ROMasync 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 类。
// 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);// 加载ROMthis.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_romawait 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 文件。
{"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
https://blog.csdn.net/yyz_1987/article/details/136037394
https://pineight.com/jsnes/
https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/quick-start/typescript-to-arkts-migration-guide.md#%E4%BB%8Etypescript%E5%88%B0arkts%E7%9A%84%E9%80%82%E9%85%8D%E8%A7%84%E5%88%99