Skip to content

@nesjs/vue3 - Vue 3 NES 模拟器组件

npm version

License: MIT

基于 @nesjs/native 的 Vue 3 组件封装,提供开箱即用的 NES 模拟器 Vue 组件。

特性

  • 🎮 基于 @nesjs/native 的完整 NES 模拟功能
  • ⚡ Vue 3 Composition API 支持
  • 📦 开箱即用,一个组件搞定
  • 🎯 TypeScript 完整支持
  • 🎨 响应式配置和状态管理
  • 🔧 丰富的 API 和事件回调
  • 📱 移动端适配支持
  • 🎵 自动音频激活处理

安装

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

快速开始

基础使用示例

vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { NesVue } from '@nesjs/vue3'
import type { NESComponentExpose } from '@nesjs/vue3'

const nesRef = ref<NESComponentExpose>()
const romUrl = '/path/to/your/game.nes'

// 模拟器配置
const emulatorConfig = {
  scale: 2,
  smoothing: false,
  clip8px: true,
  audioBufferSize: 1024,
  audioSampleRate: 44100
}

const isPlaying = computed(() => nesRef.value?.isPlaying || false)

const togglePlay = async() => {
    await nesRef.value?.togglePlay()
}

const reset = () => {
    nesRef.value?.reset()
}

const screenshot = () => {
    nesRef.value?.screenshot(true) // true = 自动下载
}

const downloadSave = () => {
    nesRef.value?.downloadSaveState()
}
</script>

<template>
  <div class="nes-container">
    <NesVue 
      ref="nesRef"
      :rom="romUrl" 
      :volume="80"
      :auto-start="false"
      :emulator-config="emulatorConfig"
      class="nes-emulator"
    />
    <div class="controls">
      <button @click="togglePlay">{{ isPlaying ? '暂停' : '开始' }}</button>
      <button @click="reset">重置</button>
      <button @click="screenshot">截图</button>
      <button @click="downloadSave">下载存档</button>
    </div>
  </div>
</template>

<style scoped>
.nes-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}

.nes-emulator {
  border: 2px solid #333;
  border-radius: 8px;
}

.controls {
  display: flex;
  gap: 10px;
}

button {
  padding: 8px 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: #f0f0f0;
  cursor: pointer;
  color: #000;
}

button:hover {
  background: #e0e0e0;
}
</style>

全局注册

typescript
// main.ts
import { createApp } from 'vue'
import NesVuePlugin from '@nesjs/vue3'
import App from './App.vue'

const app = createApp(App)

// 全局注册组件
app.use(NesVuePlugin)

app.mount('#app')
vue
<!-- 在任意组件中直接使用 -->
<template>
  <NesVue :rom="romData" :scale="3" />
</template>

API 参考

Props (配置选项)

属性类型默认值描述
romstring | Uint8Array | ArrayBuffer | Blob-ROM 数据源(必需)
autoStartbooleanfalse是否自动开始游戏
volumenumber50音量大小 (0-100)
debugModebooleanfalse是否开启调试模式
mashingSpeednumber16连发速度
emulatorConfigEmulatorConfigOptions见下方模拟器配置对象

模拟器配置选项 (EmulatorConfig)

emulatorConfig 属性接受一个包含以下属性的对象:

属性类型默认值描述
scalenumber2画面缩放倍数
smoothingbooleanfalse是否启用图像平滑
clip8pxbooleantrue是否裁剪边框8像素
fillColor`string[number, number, number, number]`-
audioBufferSizenumber1024音频缓冲区大小
audioSampleRatenumber44100音频采样率
autoSaveIntervalnumber-SRAM存档自动保存间隔(帧数)
enableCheatboolean-是否启用金手指
player1KeyMapRecord<string, string>-玩家1键位映射
player2KeyMapRecord<string, string>-玩家2键位映射

注意事项:autoStart 与音频播放

如果你在配置中启用了 autoStart,请注意:

在用户与页面进行交互(如点击、按键、触摸)之前,浏览器不会允许音频播放,因此在此之前不会有声音输出。

这是浏览器的安全策略,旨在防止自动播放音频。模拟器画面和游戏逻辑会正常运行,但声音会在用户首次交互后才激活。

建议在界面上适当提示用户需要操作页面以启用声音。

方法 (通过 ref 调用)

游戏控制

typescript
// 开始游戏
await nesRef.value?.start()

// 暂停游戏  
nesRef.value?.pause()

// 继续游戏
nesRef.value?.play()

// 切换播放状态
await nesRef.value?.togglePlay()

// 重置游戏
nesRef.value?.reset()

// 停止游戏
nesRef.value?.stop()

// 添加金手指
nesRef.value?.addCheat('07FA-01-01')

// 移除金手指
nesRef.value?.removeCheat('07FA-01-01')

// 切换金手指状态
nesRef.value?.toggleCheat('07FA-01-01')

// 移除所有金手指
nesRef.value?.clearAllCheats()

存档系统

typescript
// 创建存档数据
const saveData = nesRef.value?.save() // Uint8Array

// 你可以将其保存到任意地方,例如 localStorage 
localStorage.setItem('nes-save', JSON.stringify(Array.from(saveData)))

// 加载存档数据
const data = localStorage.getItem('nes-save')
const success = nesRef.value?.load(new Uint8Array(JSON.parse(data)))

// 下载存档文件
nesRef.value?.downloadSaveState()

// 上传存档文件
await nesRef.value?.uploadSaveState()

截图功能

typescript
// 获取截图数据URL
const dataUrl = nesRef.value?.screenshot()

// 自动下载截图
nesRef.value?.screenshot(true)

信息获取

typescript
// 获取ROM信息
const romInfo = nesRef.value?.getROMInfo()
console.log(romInfo?.mapperNumber) // Mapper编号

// 获取调试信息
const debug = nesRef.value?.getDebugInfo()
console.log(debug?.frameCount) // 帧数

// 获取游戏状态
const isPlaying = nesRef.value?.isPlaying // 是否在游戏中
const isLoading = nesRef.value?.isLoading // 是否在加载中

高级用法

自定义键位映射

vue
<script setup>
// 自定义玩家1键位
const customKeyMap = {
  UP: 'ArrowUp',
  DOWN: 'ArrowDown', 
  LEFT: 'ArrowLeft',
  RIGHT: 'ArrowRight',
  A: 'Space',
  B: 'ShiftLeft',
  SELECT: 'KeyQ',
  START: 'KeyE'
}
</script>

<template>
  <NesVue 
    :rom="romUrl"
    :emulator-config="{ player1KeyMap: customKeyMap }"
  />
</template>

响应式配置

vue
<script setup>
import { reactive, ref } from 'vue'

const volume = ref(70)
const emulatorConfig = reactive({
  scale: 2,
  smoothing: false,
  clip8px: true,
  audioBufferSize: 1024,
  audioSampleRate: 44100
})
const romUrl = '/games/your-rom.nes'
</script>

<template>
  <div>
    <!-- 配置面板 -->
    <div class="config-panel">
      <label>
        音量: {{ volume }}
        <input v-model.number="volume" type="range" min="0" max="100">
      </label>
      
      <label>
        缩放: {{ emulatorConfig.scale }}x
        <input v-model.number="emulatorConfig.scale" type="range" min="1" max="5">
      </label>
      
      <label>
        <input v-model="emulatorConfig.smoothing" type="checkbox"> 图像平滑
      </label>
    </div>
    
    <!-- 模拟器组件 -->
    <NesVue 
      :rom="romUrl"
      :volume="volume"
      :emulator-config="emulatorConfig"
    />
  </div>
</template>

多种 ROM 数据源支持

vue
<script setup>
import { ref } from 'vue'

const romData = ref(null)
const romUrl = ref('')

// 文件上传
const handleFileUpload = async (event) => {
  const file = event.target.files[0]
  if (file) {
    romData.value = await file.arrayBuffer()
  }
}

// URL加载
const loadFromUrl = () => {
  if (romUrl.value) {
    romData.value = romUrl.value
  }
}
</script>

<template>
  <div>
    <!-- 文件上传 -->
    <input type="file" @change="handleFileUpload" accept=".nes">
    
    <!-- 从URL加载 -->
    <input v-model="romUrl" placeholder="输入ROM URL">
    <button @click="loadFromUrl">从URL加载</button>
    
    <!-- 模拟器 -->
    <NesVue v-if="romData" :rom="romData" />
  </div>
</template>

游戏状态管理示例

vue
<script setup>
import { ref, computed } from 'vue'

const nesRef = ref()
const debugMode = ref(false)

const isLoading = computed(() => nesRef.value?.isLoading)
const isPlaying = computed(() => nesRef.value?.isPlaying) 
const romInfo = computed(() => nesRef.value?.getROMInfo())
const debugInfo = computed(() => nesRef.value?.getDebugInfo())
</script>

<template>
  <div>
    <div class="status-bar">
      <span v-if="isLoading">加载中...</span>
      <span v-else-if="isPlaying">游戏运行中</span>
      <span v-else>游戏已暂停</span>
      
      <span v-if="romInfo">
        | Mapper: {{ romInfo.mapperNumber }}
        | PRG: {{ romInfo.prgSize }}KB
        | CHR: {{ romInfo.chrSize }}KB
      </span>
    </div>
    
    <NesVue ref="nesRef" :rom="romUrl" />
    
    <div class="debug-panel" v-if="debugMode">
      <h3>调试信息</h3>
      <pre>{{ JSON.stringify(debugInfo, null, 2) }}</pre>
    </div>
  </div>
</template>

移动端适配示例

vue
<script setup>
import { ref, computed } from 'vue'
import { NESControllerButton } from '@nesjs/core'

const nesRef = ref()

const isMobile = computed(() => {
  return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
})

const pressButton = (button) => {
  const gamepad = nesRef.value?.emulator?.getGamepad(1)
  gamepad?.setButton(NESControllerButton[button], 1)
}

const releaseButton = (button) => {
  const gamepad = nesRef.value?.emulator?.getGamepad(1)  
  gamepad?.setButton(NESControllerButton[button], 0)
}
</script>

<template>
  <div class="mobile-container">
    <NesVue 
      ref="nesRef"
      :rom="romUrl"
      :scale="isMobile ? 1 : 2"
      class="mobile-emulator"
    />
    
    <!-- 虚拟按键 -->
    <div v-if="isMobile" class="virtual-controls">
      <div class="dpad">
        <button @touchstart="pressButton('UP')" @touchend="releaseButton('UP')"></button>
        <div class="dpad-middle">
          <button @touchstart="pressButton('LEFT')" @touchend="releaseButton('LEFT')"></button>
          <button @touchstart="pressButton('RIGHT')" @touchend="releaseButton('RIGHT')"></button>
        </div>
        <button @touchstart="pressButton('DOWN')" @touchend="releaseButton('DOWN')"></button>
      </div>
      
      <div class="action-buttons">
        <button @touchstart="pressButton('B')" @touchend="releaseButton('B')">B</button>
        <button @touchstart="pressButton('A')" @touchend="releaseButton('A')">A</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.mobile-container {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.virtual-controls {
  display: flex;
  justify-content: space-between;
  width: 100%;
  max-width: 400px;
  margin-top: 20px;
}

.dpad {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-rows: 1fr 1fr 1fr;
  gap: 2px;
}

.dpad button {
  width: 50px;
  height: 50px;
  border: 2px solid #333;
  background: #f0f0f0;
  font-size: 16px;
}

.action-buttons {
  display: flex;
  gap: 10px;
}

.action-buttons button {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  border: 2px solid #333;
  background: #f0f0f0;
  font-weight: bold;
}
</style>

常见问题

音频无法播放

由于浏览器安全策略,音频需要用户交互后才能激活。组件已自动处理这个问题,但如果仍有问题,可以手动处理:

vue
<script setup>
const audioEnabled = ref(false)

const enableAudio = async () => {
  await nesRef.value?.emulator?.enableAudio()
  audioEnabled.value = true
}
</script>

<template>
  <div>
    <button v-if="!audioEnabled" @click="enableAudio">启用音频</button>
    <NesVue ref="nesRef" :rom="romUrl" />
  </div>
</template>

ROM 文件加载失败

确保 ROM 文件路径正确,且服务器支持相应的 MIME 类型:

javascript
// vite.config.js 或 webpack 配置
export default {
  server: {
    // 添加 .nes 文件的 MIME 类型支持
    mimeTypes: {
      'application/octet-stream': ['nes']
    }
  }
}

性能优化

对于低端设备,可以调整配置以提升性能:

vue
<NesVue 
  :rom="romUrl"
  :emulator-config="{
    scale: 1,
    smoothing: false,
    audioBufferSize: 2048
  }"
/>

浏览器支持

  • Chrome 66+ ✅
  • Firefox 60+ ✅
  • Safari 11.1+ ✅
  • Edge 79+ ✅

Released under the MIT License.