JSNES游戏模拟器在 Node.js 环境下的测试使用及高清显示优化

随着技术的发展和设备生态的不断扩展,游戏模拟器作为一项经典的娱乐方式也逐渐在新的操作系统上得到实现。本文将介绍如何在 Node.js 环境下使用 JavaScript NES 模拟器(JSNES),并通过生成动态 GIF 文件来测试其运行效果。

JSNES 简介

JSNES 是一个用纯 JavaScript 编写的 NES(Nintendo Entertainment System)游戏模拟器。它可以在支持 JavaScript 的环境中运行,无需额外的插件或软件安装。JSNES 的主要特点包括:

  • 高兼容性:支持大量的 NES 游戏 ROM 文件,用户可以通过简单的 ROM 文件加载即可体验到经典游戏。
  • 可移植性:由于是基于 JavaScript 编写的,JSNES 可以很容易地移植到各种支持 JavaScript 的运行环境中,包括浏览器、Node.js 以及基于 JavaScript 的操作系统。
  • 开源许可:JSNES 使用 MIT 许可证分发,这意味着开发者可以自由地使用、修改和分发该模拟器,促进了游戏模拟技术的发展和社区共享。

完整工程源码下载地址(带资源可运行):
https://download.csdn.net/download/qq8864/92043021

安装依赖

要在 Node.js 环境下运行 JSNES,我们需要安装一些必要的依赖包,包括 jsnescanvasgifencoder

bash 复制代码
npm install jsnes canvas gifencoder

代码实现

下面我们将编写代码来测试 JSNES 在 Node.js 环境下的运行情况,并生成动态 GIF 文件。

1. 导入必要的模块

javascript 复制代码
const jsnes = require('jsnes');
const fs = require('fs');
const { createCanvas } = require('canvas');
const GIFEncoder = require('gifencoder');

2. 定义常量

javascript 复制代码
// 定义常量
const SCREEN_WIDTH = 256;
const SCREEN_HEIGHT = 240;
const FRAMEBUFFER_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT;

3. 创建 Canvas 和 GIFEncoder

javascript 复制代码
// 创建Canvas和GIFEncoder
const canvas = createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
const ctx = canvas.getContext('2d');
const encoder = new GIFEncoder(SCREEN_WIDTH, SCREEN_HEIGHT);

// 设置GIFEncoder参数
encoder.setRepeat(0); // 0 for repeat, -1 for no-repeat
encoder.setDelay(50); // frame delay in ms
encoder.setQuality(10); // image quality. 10 is default

4. 分配帧缓冲区数组

javascript 复制代码
// 分配帧缓冲区数组
let framebuffer_u8, framebuffer_u32;
let image = ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
let buffer = new ArrayBuffer(image.data.length);
framebuffer_u8 = new Uint8ClampedArray(buffer);
framebuffer_u32 = new Uint32Array(buffer);

5. 初始化画布背景

javascript 复制代码
// 初始化画布背景
ctx.fillStyle = "black";
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

6. 初始化 NES 模拟器

javascript 复制代码
// 初始化NES模拟器
const nes = new jsnes.NES({
  onFrame: function(framebuffer_24) {
    console.log('onFrame enter:');

    // 将帧缓冲区数据转换为Uint32Array
    for (let i = 0; i < FRAMEBUFFER_SIZE; i++) {
      framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i];
    }

    // 设置图像数据并绘制到Canvas
    image.data.set(framebuffer_u8);
    ctx.putImageData(image, 0, 0);

    // 添加当前帧到GIF
    encoder.addFrame(ctx);
  },
  onAudioSample: function(left, right) {
    // 处理音频样本,这里可以添加音频播放逻辑
    // console.log('onAudioSample:');
  }
});

7. 读取 ROM 文件

javascript 复制代码
// 读取ROM文件
try {
  const romData = fs.readFileSync('test1.nes', { encoding: 'binary' });
  nes.loadROM(romData);
} catch (error) {
  console.error('读取ROM文件失败:', error);
  process.exit(1);
}

8. 启动 GIF 编码

javascript 复制代码
// 启动GIF编码
encoder.start();

9. 运行 NES 帧

javascript 复制代码
// 运行NES帧
const frameTime = 1000 / 60; // 60 FPS 意味着每帧 16.67 毫秒
function runFrame() {
  nes.frame();
  // 可以在这里处理输入设备
  // nes.buttonDown(1, jsnes.Controller.BUTTON_A);
  // nes.frame();
  // nes.buttonUp(1, jsnes.Controller.BUTTON_A);
  // nes.frame();
}

setInterval(runFrame, frameTime);

10. 保持应用程序活动状态

javascript 复制代码
// 保持应用程序活动状态
process.stdin.resume();

11. 处理 Ctrl+C 退出

javascript 复制代码
// 处理 Ctrl+C 退出
process.on('SIGINT', function() {
  console.log('Exiting...');
  encoder.finish();
  const buffer = encoder.out.getData();
  fs.writeFileSync('animation.gif', buffer);
  console.log('动态GIF已生成: animation.gif');
  process.exit();
});

注意事项

  1. 性能优化

    • NES 模拟器对性能要求较高,考虑使用多线程来提高整体性能。
    • 如果需要,可以关闭音频模拟以提高渲染效率。
  2. 音频适配

    • Node.js 环境下没有浏览器的 Web Audio API,需要特别适配音频处理。
  3. 内存管理

    • 确保及时释放不再使用的资源。
    • 注意 Node.js 的内存使用限制。
  4. 兼容性

    • 测试不同的 ROM 文件以确保兼容性。

完整测试代码

javascript 复制代码
const jsnes = require('jsnes');
const fs = require('fs');
const { createCanvas } = require('canvas');
const GIFEncoder = require('gifencoder');

console.log('Hello World!');

// 定义常量
const SCREEN_WIDTH = 256;
const SCREEN_HEIGHT = 240;
const FRAMEBUFFER_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT;

// 创建Canvas和GIFEncoder
const canvas = createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
const ctx = canvas.getContext('2d');
const encoder = new GIFEncoder(SCREEN_WIDTH, SCREEN_HEIGHT);

// 设置GIFEncoder参数
encoder.setRepeat(0); // 0 for repeat, -1 for no-repeat
encoder.setDelay(50); // frame delay in ms
encoder.setQuality(10); // image quality. 10 is default

// 分配帧缓冲区数组
let framebuffer_u8, framebuffer_u32;
let image = ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
let buffer = new ArrayBuffer(image.data.length);
framebuffer_u8 = new Uint8ClampedArray(buffer);
framebuffer_u32 = new Uint32Array(buffer);

// 初始化画布背景
ctx.fillStyle = "black";
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

// 初始化NES模拟器
const nes = new jsnes.NES({
  onFrame: function(framebuffer_24) {
    console.log('onFrame enter:');
    // 将帧缓冲区数据转换为Uint32Array
    for (let i = 0; i < FRAMEBUFFER_SIZE; i++) {
      framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i];
    }

    // 设置图像数据并绘制到Canvas
    image.data.set(framebuffer_u8);
    ctx.putImageData(image, 0, 0);

    // 添加当前帧到GIF
    encoder.addFrame(ctx);
  },
  onAudioSample: function(left, right) {
    // 处理音频样本,这里可以添加音频播放逻辑
    // console.log('onAudioSample:');
  }
});

// 读取ROM文件
try {
  const romData = fs.readFileSync('test1.nes', { encoding: 'binary' });
  nes.loadROM(romData);
} catch (error) {
  console.error('读取ROM文件失败:', error);
  process.exit(1);
}

// 启动GIF编码
encoder.start();

// 运行NES帧
const frameTime = 1000 / 60; // 60 FPS 意味着每帧 16.67 毫秒
function runFrame() {
  nes.frame();
  // 可以在这里处理输入设备
  // nes.buttonDown(1, jsnes.Controller.BUTTON_A);
  // nes.frame();
  // nes.buttonUp(1, jsnes.Controller.BUTTON_A);
  // nes.frame();
}

setInterval(runFrame, frameTime);

// 保持应用程序活动状态
process.stdin.resume();

// 处理 Ctrl+C 退出
process.on('SIGINT', function() {
  console.log('Exiting...');
  encoder.finish();
  const buffer = encoder.out.getData();
  fs.writeFileSync('animation.gif', buffer);
  console.log('动态GIF已生成: animation.gif');
  process.exit();
});

高清显示优化算法

默认的游戏画质太差了,可以使用优化算法提高显示分辨率,增强在手机或平板等大屏设备上的使用体验。

这是一个基于jsnes的NES游戏模拟器,支持将原始的256x240分辨率游戏画面放大到高分辨率,适配现代手机屏幕。

以下是显示算法优化效果:

  • 🎮 支持NES游戏ROM文件
  • 📱 多种手机屏幕分辨率支持 (720p, 1080p, 1440p, 4K)
  • 🎨 两种图像放大算法:
    • 最近邻插值:保持像素艺术风格
    • 双线性插值:平滑效果
  • 🎬 生成高质量GIF动画
  • ⚙️ 灵活的配置系统
javascript 复制代码
const jsnes = require('jsnes');
const fs = require('fs');
const { createCanvas } = require('canvas');
const GIFEncoder = require('gifencoder');
const config = require('./config');

// 定义常量
const NES_WIDTH = 256;
const NES_HEIGHT = 240;
const NES_FRAMEBUFFER_SIZE = NES_WIDTH * NES_HEIGHT;

// 从配置文件加载设置
const DISPLAY_CONFIGS = config.DISPLAY_CONFIGS;
const DISPLAY_CONFIG = DISPLAY_CONFIGS[config.CURRENT_CONFIG];
const SCREEN_WIDTH = DISPLAY_CONFIG.width;
const SCREEN_HEIGHT = DISPLAY_CONFIG.height;
const SCALE_FACTOR = DISPLAY_CONFIG.scale;

console.log('=== NES游戏模拟器 - 高分辨率版本 ===');
console.log(`当前配置: ${Object.keys(DISPLAY_CONFIGS).find(key => DISPLAY_CONFIGS[key] === DISPLAY_CONFIG)}`);
console.log(`输出分辨率: ${SCREEN_WIDTH}x${SCREEN_HEIGHT}`);
console.log(`放大倍数: ${SCALE_FACTOR}x`);
console.log(`插值算法: ${config.USE_NEAREST_NEIGHBOR ? '最近邻插值(保持像素艺术风格)' : '双线性插值(平滑效果)'}`);
console.log('按 Ctrl+C 停止录制并生成GIF文件');
console.log('=====================================');

// 图像放大算法
function nearestNeighborUpscale(sourceData, sourceWidth, sourceHeight, targetWidth, targetHeight) {
  const targetData = new Uint8ClampedArray(targetWidth * targetHeight * 4);
  const scaleX = sourceWidth / targetWidth;
  const scaleY = sourceHeight / targetHeight;
  
  for (let y = 0; y < targetHeight; y++) {
    for (let x = 0; x < targetWidth; x++) {
      const sourceX = Math.floor(x * scaleX);
      const sourceY = Math.floor(y * scaleY);
      const sourceIndex = (sourceY * sourceWidth + sourceX) * 4;
      const targetIndex = (y * targetWidth + x) * 4;
      
      targetData[targetIndex] = sourceData[sourceIndex];     // R
      targetData[targetIndex + 1] = sourceData[sourceIndex + 1]; // G
      targetData[targetIndex + 2] = sourceData[sourceIndex + 2]; // B
      targetData[targetIndex + 3] = sourceData[sourceIndex + 3]; // A
    }
  }
  
  return targetData;
}

// 双线性插值放大算法(更平滑但可能模糊像素艺术)
function bilinearUpscale(sourceData, sourceWidth, sourceHeight, targetWidth, targetHeight) {
  const targetData = new Uint8ClampedArray(targetWidth * targetHeight * 4);
  const scaleX = (sourceWidth - 1) / targetWidth;
  const scaleY = (sourceHeight - 1) / targetHeight;
  
  for (let y = 0; y < targetHeight; y++) {
    for (let x = 0; x < targetWidth; x++) {
      const gx = x * scaleX;
      const gy = y * scaleY;
      const gxi = Math.floor(gx);
      const gyi = Math.floor(gy);
      const gxw = gx - gxi;
      const gyw = gy - gyi;
      
      const c00 = (gyi * sourceWidth + gxi) * 4;
      const c10 = (gyi * sourceWidth + Math.min(gxi + 1, sourceWidth - 1)) * 4;
      const c01 = (Math.min(gyi + 1, sourceHeight - 1) * sourceWidth + gxi) * 4;
      const c11 = (Math.min(gyi + 1, sourceHeight - 1) * sourceWidth + Math.min(gxi + 1, sourceWidth - 1)) * 4;
      
      const targetIndex = (y * targetWidth + x) * 4;
      
      for (let i = 0; i < 4; i++) {
        const c0 = sourceData[c00 + i] * (1 - gxw) + sourceData[c10 + i] * gxw;
        const c1 = sourceData[c01 + i] * (1 - gxw) + sourceData[c11 + i] * gxw;
        targetData[targetIndex + i] = Math.round(c0 * (1 - gyw) + c1 * gyw);
      }
    }
  }
  
  return targetData;
}

// 创建Canvas和GIFEncoder
const canvas = createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
const ctx = canvas.getContext('2d');
const encoder = new GIFEncoder(SCREEN_WIDTH, SCREEN_HEIGHT);

// 设置GIFEncoder参数
encoder.setRepeat(config.GIF_SETTINGS.repeat);
encoder.setDelay(config.GIF_SETTINGS.delay);
encoder.setQuality(config.GIF_SETTINGS.quality);

// 分配帧缓冲区数组
let nes_framebuffer_u8, nes_framebuffer_u32;
let nes_image = ctx.getImageData(0, 0, NES_WIDTH, NES_HEIGHT);
let nes_buffer = new ArrayBuffer(nes_image.data.length);
nes_framebuffer_u8 = new Uint8ClampedArray(nes_buffer);
nes_framebuffer_u32 = new Uint32Array(nes_buffer);

// 创建高分辨率图像数据
let display_image = ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

// 初始化画布背景
ctx.fillStyle = "black";
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

// 配置参数
const USE_NEAREST_NEIGHBOR = config.USE_NEAREST_NEIGHBOR;

// 初始化NES模拟器
const nes = new jsnes.NES({
  onFrame: function(framebuffer_24) {
    console.log('onFrame enter:');
    
    // 将NES帧缓冲区数据转换为Uint32Array
    for (let i = 0; i < NES_FRAMEBUFFER_SIZE; i++) {
      nes_framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i];
    }

    // 设置NES原始图像数据
    nes_image.data.set(nes_framebuffer_u8);
    
    // 使用选择的算法进行图像放大
    let upscaledData;
    if (USE_NEAREST_NEIGHBOR) {
      upscaledData = nearestNeighborUpscale(
        nes_framebuffer_u8, 
        NES_WIDTH, 
        NES_HEIGHT, 
        SCREEN_WIDTH, 
        SCREEN_HEIGHT
      );
    } else {
      upscaledData = bilinearUpscale(
        nes_framebuffer_u8, 
        NES_WIDTH, 
        NES_HEIGHT, 
        SCREEN_WIDTH, 
        SCREEN_HEIGHT
      );
    }
    
    // 设置高分辨率图像数据
    display_image.data.set(upscaledData);
    ctx.putImageData(display_image, 0, 0);

    // 添加当前帧到GIF
    encoder.addFrame(ctx);
  },
  onAudioSample: function(left, right) {
    // 处理音频样本,这里可以添加音频播放逻辑
    // console.log('onAudioSample:');
  }
});

// 读取ROM文件
try {
  const romData = fs.readFileSync('test1.nes', { encoding: 'binary' });
  nes.loadROM(romData);
} catch (error) {
  console.error('读取ROM文件失败:', error);
  process.exit(1);
}

// 启动GIF编码
encoder.start();

// 运行NES帧
const frameTime = 1000 / 60; // 60 FPS 意味着每帧 16.67 毫秒
function runFrame() {
  nes.frame();
  // 可以在这里处理输入设备
  // nes.buttonDown(1, jsnes.Controller.BUTTON_A);
  // nes.frame();
  // nes.buttonUp(1, jsnes.Controller.BUTTON_A);
  // nes.frame();
}

setInterval(runFrame, frameTime);

// 保持应用程序活动状态
process.stdin.resume();

// 处理 Ctrl+C 退出
process.on('SIGINT', function() {
  console.log('Exiting...');
  encoder.finish();
  const buffer = encoder.out.getData();
  
  // 根据配置生成文件名
  const configName = Object.keys(DISPLAY_CONFIGS).find(key => 
    DISPLAY_CONFIGS[key] === DISPLAY_CONFIG
  );
  const algorithmName = USE_NEAREST_NEIGHBOR ? 'nearest' : 'bilinear';
  const filename = `nes_${configName}_${algorithmName}_${SCREEN_WIDTH}x${SCREEN_HEIGHT}.gif`;
  
  fs.writeFileSync(filename, buffer);
  console.log(`高分辨率动态GIF已生成: ${filename}`);
  console.log(`分辨率: ${SCREEN_WIDTH}x${SCREEN_HEIGHT}`);
  console.log(`放大倍数: ${SCALE_FACTOR}x`);
  console.log(`插值算法: ${USE_NEAREST_NEIGHBOR ? '最近邻插值(像素艺术风格)' : '双线性插值(平滑)'}`);
  process.exit();
});
javascript 复制代码
在这里插入代码片

网页版对比优化算法后的效果

nes-embed.js文件

javascript 复制代码
const NES_WIDTH = 256;
const NES_HEIGHT = 240;
const NES_FRAMEBUFFER_SIZE = NES_WIDTH * NES_HEIGHT;


var canvas_ctx, image,display_image;
var framebuffer_u8, framebuffer_u32;

var AUDIO_BUFFERING = 512;
var SAMPLE_COUNT = 4*1024;
var SAMPLE_MASK = SAMPLE_COUNT - 1;
var audio_samples_L = new Float32Array(SAMPLE_COUNT);
var audio_samples_R = new Float32Array(SAMPLE_COUNT);
var audio_write_cursor = 0, audio_read_cursor = 0;

const SCREEN_WIDTH = 2560;
const SCREEN_HEIGHT = 1440;
var FRAMEBUFFER_SIZE = SCREEN_WIDTH*SCREEN_HEIGHT;


var nes = new jsnes.NES({
	onFrame: function(framebuffer_24){
		for(var i = 0; i < NES_FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i];
			
		// 截取前100字节
    var frameBufferSlice = framebuffer_u8.slice(0, 100);
    var hexString = Array.prototype.map.call(frameBufferSlice, function(byte) {
      return ('0' + (byte & 0xFF).toString(16)).slice(-2);
    }).join('');
    // 打印十六进制字符串
    console.log('onFrames: ' + hexString);
	},
	onAudioSample: function(l, r){
		audio_samples_L[audio_write_cursor] = l;
		audio_samples_R[audio_write_cursor] = r;
		audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK;
	},
});

function onAnimationFrame(){
	window.requestAnimationFrame(onAnimationFrame);
	
	image.data.set(framebuffer_u8);
	//canvas_ctx.putImageData(image, 0, 0);
	
	// 使用选择的算法进行图像放大
    let upscaledData;
    
    upscaledData = nearestNeighborUpscale(
        framebuffer_u8, 
        NES_WIDTH, 
        NES_HEIGHT, 
        SCREEN_WIDTH, 
        SCREEN_HEIGHT
      );
    
    // 设置高分辨率图像数据
    display_image.data.set(upscaledData);
    canvas_ctx.putImageData(display_image, 0, 0);
}

function audio_remain(){
	return (audio_write_cursor - audio_read_cursor) & SAMPLE_MASK;
}

function audio_callback(event){
	var dst = event.outputBuffer;
	var len = dst.length;
	
	// Attempt to avoid buffer underruns.
	if(audio_remain() < AUDIO_BUFFERING) nes.frame();
	
	var dst_l = dst.getChannelData(0);
	var dst_r = dst.getChannelData(1);
	for(var i = 0; i < len; i++){
		var src_idx = (audio_read_cursor + i) & SAMPLE_MASK;
		dst_l[i] = audio_samples_L[src_idx];
		dst_r[i] = audio_samples_R[src_idx];
	}
	
	audio_read_cursor = (audio_read_cursor + len) & SAMPLE_MASK;
}

function keyboard(callback, event){
	var player = 1;
	switch(event.keyCode){
		case 38: // UP
			callback(player, jsnes.Controller.BUTTON_UP); break;
		case 40: // Down
			callback(player, jsnes.Controller.BUTTON_DOWN); break;
		case 37: // Left
			callback(player, jsnes.Controller.BUTTON_LEFT); break;
		case 39: // Right
			callback(player, jsnes.Controller.BUTTON_RIGHT); break;
		case 65: // 'a' - qwerty, dvorak
		case 81: // 'q' - azerty
			callback(player, jsnes.Controller.BUTTON_A); break;
		case 83: // 's' - qwerty, azerty
		case 79: // 'o' - dvorak
			callback(player, jsnes.Controller.BUTTON_B); break;
		case 9: // Tab
			callback(player, jsnes.Controller.BUTTON_SELECT); break;
		case 13: // Return
			callback(player, jsnes.Controller.BUTTON_START); break;
		default: break;
	}
}

function nes_init(canvas_id){
	var canvas = document.getElementById(canvas_id);
	canvas_ctx = canvas.getContext("2d");
	image = canvas_ctx.getImageData(0, 0, NES_WIDTH, NES_HEIGHT);
	
	// 创建高分辨率图像数据
    display_image = canvas_ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
	
	canvas_ctx.fillStyle = "black";
	canvas_ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
	
	// Allocate framebuffer array.
	var buffer = new ArrayBuffer(image.data.length);
	framebuffer_u8 = new Uint8ClampedArray(buffer);
	framebuffer_u32 = new Uint32Array(buffer);
	
	// Setup audio.
	var audio_ctx = new window.AudioContext();
	var script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2);
	script_processor.onaudioprocess = audio_callback;
	script_processor.connect(audio_ctx.destination);
}

function nes_boot(rom_data){
	nes.loadROM(rom_data);
	window.requestAnimationFrame(onAnimationFrame);
}

function nes_load_data(canvas_id, rom_data){
	nes_init(canvas_id);
	nes_boot(rom_data);
}

function nes_load_url(canvas_id, path){
	nes_init(canvas_id);
	
	var req = new XMLHttpRequest();
	req.open("GET", path);
	req.overrideMimeType("text/plain; charset=x-user-defined");
	req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`);
	
	req.onload = function() {
		if (this.status === 200) {
		nes_boot(this.responseText);
		} else if (this.status === 0) {
			// Aborted, so ignore error
		} else {
			req.onerror();
		}
	};
	
	req.send();
}

// 图像放大算法
function nearestNeighborUpscale(sourceData, sourceWidth, sourceHeight, targetWidth, targetHeight) {
  const targetData = new Uint8ClampedArray(targetWidth * targetHeight * 4);
  const scaleX = sourceWidth / targetWidth;
  const scaleY = sourceHeight / targetHeight;
  
  for (let y = 0; y < targetHeight; y++) {
    for (let x = 0; x < targetWidth; x++) {
      const sourceX = Math.floor(x * scaleX);
      const sourceY = Math.floor(y * scaleY);
      const sourceIndex = (sourceY * sourceWidth + sourceX) * 4;
      const targetIndex = (y * targetWidth + x) * 4;
      
      targetData[targetIndex] = sourceData[sourceIndex];     // R
      targetData[targetIndex + 1] = sourceData[sourceIndex + 1]; // G
      targetData[targetIndex + 2] = sourceData[sourceIndex + 2]; // B
      targetData[targetIndex + 3] = sourceData[sourceIndex + 3]; // A
    }
  }
  
  return targetData;
}

document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)});
document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)});

网页文件:

html 复制代码
<!DOCTYPE html>

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
		<title>Embedding Example</title>
		
		<script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script>
		<script type="text/javascript" src="nes-embed.js"></script>
		<script>window.onload = function(){nes_load_url("nes-canvas", "test1.nes");}</script>
	</head>
	<body>
		<div style="margin: auto; width: 75%;">
			<canvas id="nes-canvas" width="2560" height="1440" style="width: 100%"/>
		</div>
		<p>DPad: Arrow keys<br/>Start: Return, Select: Tab<br/>A Button: A, B Button: S</p>
	</body>
</html>

运行命令:

bash 复制代码
python -m http.server

效果明显增强不少,显示清晰,没有模糊感啦。

结论

通过上述步骤,我们成功地将 JSNES 移植到 Node.js 环境下,并生成了一个动态 GIF 文件来测试其运行效果。JSNES 的高兼容性和可移植性使其成为在不同环境中运行 NES 游戏的理想选择。希望本文对您有所帮助!

参考资料

通过这些资源,您可以进一步了解和扩展 JSNES 在 Node.js 环境下的功能。

相关推荐
土丁爱吃大米饭3 小时前
千年游戏智慧:传承的密码
游戏·传承·游戏设计·游戏历史·古老游戏·古代游戏
huangql5204 小时前
基于前端+Node.js 的 Markdown 笔记 PDF 导出系统完整实战
前端·笔记·node.js
大Mod_abfun7 小时前
Unity游戏基础-4(人物移动、相机移动、UI事件处理 代码详解)
游戏·ui·unity·游戏引擎
Jiezcode8 小时前
LeetCode 55.跳跃游戏
c++·算法·leetcode·游戏
大Mod_abfun9 小时前
Unity游戏基础-3(UI层)
游戏·ui·unity·游戏引擎
雪下的新火20 小时前
Unity+Blender-03-输出制作Flipbook
游戏·unity·游戏引擎·blender·资源·笔记分享
m0_5522008221 小时前
《UE5_C++多人TPS完整教程》学习笔记60 ——《P61 开火蒙太奇(Fire Montage)》
c++·游戏·ue5
谢尔登21 小时前
【Nest】基本概念
javascript·node.js·express
linux修理工21 小时前
node.js 二进制安装
node.js