Skip to content

@nesjs/native - 浏览器原生 NES 模拟器

npm version

License: MIT

基于 @nesjs/core 的浏览器原生实现,提供开箱即用的 Canvas 渲染、Web Audio API 音频输出和键盘/手柄控制支持。

特性

  • 🎮 基于 @nesjs/core 的完整 NES 模拟
  • 🖼️ Canvas 2D 渲染器,支持缩放和图像抗锯齿
  • 🎵 Web Audio API 音频输出,低延迟播放
  • ⌨️ 可自定义的键盘控制映射
  • 🎯 手柄 (Gamepad API) 支持,包括连发功能
  • 🔧 金手指功能支持
  • 🎨 可配置的视觉效果(缩放、抗锯齿、边框裁剪等)
  • 📱 响应式设计,适配不同屏幕尺寸

安装

bash
npm install @nesjs/native
bash
yarn add @nesjs/native
bash
pnpm add @nesjs/native

快速开始

基础使用

typescript
import { NESEmulator } from '@nesjs/native'

// 获取 Canvas 元素
const canvas = document.getElementById('nes-canvas') as HTMLCanvasElement

// 创建模拟器实例
const emulator = new NESEmulator(canvas, {
    scale: 2,                    // 2x 缩放
    smoothing: false,            // 关闭抗锯齿
    audioSampleRate: 44100,      // 音频采样率
    enableCheat: true            // 启用金手指
})

// 加载 ROM 文件
const response = await fetch('path/to/game.nes')
const romData = new Uint8Array(await response.arrayBuffer())

await emulator.loadROM(romData)

// 启动模拟器
await emulator.start()

HTML 示例

html
<!DOCTYPE html>
<html>
<head>
    <title>NES Emulator</title>
    <style>
        #nes-canvas {
            border: 2px solid #333;
            image-rendering: pixelated; /* 保持像素完美 */
        }
        .controls {
            margin-top: 10px;
        }
        button {
            margin: 5px;
            padding: 10px 15px;
        }
    </style>
</head>
<body>
    <canvas id="nes-canvas"></canvas>
    <div class="controls">
        <button onclick="emulator.start()">开始</button>
        <button onclick="emulator.pause()">暂停</button>
        <button onclick="emulator.reset()">重置</button>
        <input type="file" id="rom-input" accept=".nes">
    </div>

    <script type="module">
        import { NESEmulator } from '@nesjs/native'
        
        const canvas = document.getElementById('nes-canvas')
        const emulator = new NESEmulator(canvas)
        
        // ROM 文件加载
        document.getElementById('rom-input').addEventListener('change', async (e) => {
            const file = e.target.files[0]
            if (file) {
                const romData = new Uint8Array(await file.arrayBuffer())
                await emulator.loadROM(romData)
            }
        })
        
        // 暴露到全局作用域供按钮使用
        window.emulator = emulator
    </script>
</body>
</html>

API 参考

NESEmulator 类

构造函数

typescript
new NESEmulator(canvas: HTMLCanvasElement, options?: NESEmulatorOptions)

配置选项 (NESEmulatorOptions):

typescript
interface NESEmulatorOptions {
    // 渲染选项
    scale?: number                           // 缩放倍数,默认 2
    clip8px?: boolean                        // 裁剪边框 8 像素,默认 false
    fillColor?: string | [number, number, number, number]  // 裁剪区域的填充颜色
    smoothing?: boolean                      // 图像抗锯齿,默认 false
    
    // 音频选项
    audioBufferSize?: number                 // 音频缓冲区大小,默认 1024
    audioSampleRate?: number                 // 音频采样率,默认 44100
    ringCapacity?: number                   // 环形缓冲区容量,默认 8192
    enableSAB?: boolean                     // 启用 SharedArrayBuffer,默认 false,开始需跨域时配置,否则回退使用普通缓冲区。
    sabCapacity?: number                    // SharedArrayBuffer 容量,默认 65536

    // 核心模拟器选项
    autoSaveInterval?: number                // SRAM存档自动保存间隔,默认 3600
    enableCheat?: boolean                    // 启用金手指,默认 true
    
    // 控制器键位映射
    player1KeyMap?: Record<string, string>   // 玩家1键位映射
    player2KeyMap?: Record<string, string>   // 玩家2键位映射
}

核心方法

loadROM(romData: Uint8Array): Promise<void>

加载 ROM 文件数据。

start(): Promise<void>

启动模拟器。如果已暂停,则恢复运行。

pause(): void

暂停模拟器。

resume(): void

恢复暂停的模拟器。

stop(): void

完全停止模拟器并清理资源。

reset(): void

重置游戏到初始状态。

音频控制

enableAudio(): Promise<boolean>

启用音频输出。

disableAudio(): void

禁用音频输出。

setVolume(volume: number): void

设置音量 (0.0 - 1.0)。

视觉设置

setScale(scale: number): void

设置画面缩放倍数。

setSmoothing(smoothing: boolean): void

设置图像抗锯齿开关。

金手指功能

addCheat(code: string): boolean

添加金手指代码。

toggleCheat(code: string): void

切换金手指启用状态。

removeCheat(code: string): void

移除金手指。

clearAllCheats(): void

清除所有金手指。

控制器配置

setupKeyboadController(player: 1 | 2, keyMap: Record<string, string>): void

设置键盘控制映射。

控制器配置

默认键位映射

玩家1 (WASD + KJ):

typescript
const P1_DEFAULT = {
    UP: 'KeyW',      // W - 上
    DOWN: 'KeyS',    // S - 下  
    LEFT: 'KeyA',    // A - 左
    RIGHT: 'KeyD',   // D - 右
    A: 'KeyK',       // K - A键
    B: 'KeyJ',       // J - B键
    C: 'KeyI',       // I - A连发
    D: 'KeyU',       // U - B连发
    SELECT: 'Digit2', // 2 - SELECT
    START: 'Digit1'   // 1 - START
}

玩家2 (方向键 + 小键盘):

typescript
const P2_DEFAULT = {
    UP: 'ArrowUp',       // ↑ - 上
    DOWN: 'ArrowDown',   // ↓ - 下
    LEFT: 'ArrowLeft',   // ← - 左  
    RIGHT: 'ArrowRight', // → - 右
    A: 'Numpad2',        // 小键盘2 - A键
    B: 'Numpad1',        // 小键盘1 - B键
    C: 'Numpad5',        // 小键盘5 - A连发
    D: 'Numpad4',        // 小键盘4 - B连发
    SELECT: 'NumpadDecimal', // 小键盘. - SELECT
    START: 'NumpadEnter'     // 小键盘Enter - START
}

自定义键位映射

typescript
// 自定义玩家1键位
emulator.setupKeyboadController(1, {
    UP: 'KeyI',
    DOWN: 'KeyK', 
    LEFT: 'KeyJ',
    RIGHT: 'KeyL',
    A: 'Space',
    B: 'ShiftLeft',
    SELECT: 'KeyQ',
    START: 'KeyE'
})

手柄支持

模拟器自动检测并支持标准游戏手柄:

  • 按键映射: 自动映射 Xbox/PlayStation 控制器按键
  • 连发功能: B 和 Y 按键默认支持连发
  • 摇杆支持: 左摇杆控制方向
  • 即插即用: 无需额外配置,连接后自动识别

高级用法

画面设置

typescript
// 创建像素完美的显示
const emulator = new NESEmulator(canvas, {
    scale: 3,              // 3倍缩放
    smoothing: false,      // 关闭抗锯齿
    clip8px: true,         // 裁剪边缘
    fillColor: '#000000'   // 黑色填充
})

// 运行时调整
emulator.setScale(4)
emulator.setSmoothing(true)

音频配置

typescript
const emulator = new NESEmulator(canvas, {
    audioSampleRate: 48000,  // 高质量音频
    audioBufferSize: 512     // 低延迟缓冲区
})

// 控制音频
await emulator.enableAudio()
emulator.setVolume(0.8)       // 80% 音量
emulator.disableAudio()       // 静音

金手指使用

typescript
// 添加金手指代码
emulator.addCheat('079F-01-01')

// 管理金手指
emulator.toggleCheat('079F-01-01')  // 切换启用状态
emulator.removeCheat('079F-01-01')  // 移除
emulator.clearAllCheats()         // 清除全部

状态管理

typescript
// 创建存档
const saveState = emulator.saveState()

// 加载存档
const savedState = emulator.loadState(savedState)

// 获取调试信息
const debug = nes.getDebugInfo()
console.log(`帧数: ${debug.frameCount}, CPU: ${debug.cpuCycles}`)

浏览器兼容性

  • Chrome 66+ - 完全支持
  • Firefox 60+ - 完全支持
  • Safari 11.1+ - 完全支持
  • Edge 79+ - 完全支持

所需 API:

  • Canvas 2D Context
  • Web Audio API
  • Gamepad API (可选)
  • ArrayBuffer/Uint8Array

性能优化

建议配置

typescript
// 性能优先
const emulator = new NESEmulator(canvas, {
    scale: 2,
    smoothing: false,        // 关闭抗锯齿减少 GPU 负载
    audioBufferSize: 2048,   // 增大缓冲区减少音频卡顿
})

// 质量优先  
const emulator = new NESEmulator(canvas, {
    scale: 4,
    smoothing: true,         // 抗锯齿
    audioSampleRate: 48000,  // 高音质
    audioBufferSize: 512,    // 低延迟
})

故障排除

常见问题

音频无法播放:

typescript
// 确保在用户交互后启用音频
const enableAudioOnInteraction = async() => {
    await emulator.enableAudio()

    // Remove event listeners
    document.removeEventListener('click', enableAudioOnInteraction)
    document.removeEventListener('keydown', enableAudioOnInteraction)
    document.removeEventListener('touchstart', enableAudioOnInteraction)
}

document.addEventListener('click', enableAudioOnInteraction)
document.addEventListener('keydown', enableAudioOnInteraction)
document.addEventListener('touchstart', enableAudioOnInteraction)

画面模糊:

typescript
// 关闭抗锯齿并设置 CSS
emulator.setSmoothing(false)
canvas.style.imageRendering = 'pixelated'

性能问题:

typescript
// 检查帧率
let frameCount = 0
setInterval(() => {
    const debug = emulator.nes.getDebugInfo()
    console.log(`FPS: ${debug.frameCount - frameCount}`)
    frameCount = debug.frameCount
}, 1000)

Released under the MIT License.