NESBox 是一个红白机联机平台(详细介绍),现在他已经是一个通用的小游戏联机平台了,任何人都可以在上面发布自己开发的游戏,本文我将介绍他的工作方式以及开发过程。
背景
NESBox 最初作为一个红白机联机平台,背后使用的是一个 Rust 语言编写的模拟器,前端通过一套完整的输入/输出接口和 Rust 代码进行交互,如果想要 NESBox 支持其他游戏只需要对其他游戏做简单的适配即可,让该游戏立刻具备联机、同步设置、好友系统等功能。于是我尝试自己开发游戏并发布在 NESBox 上,首先开发了 JavaScript 游戏俄罗斯方块 - MT,之后又用 Rust 开发了游戏五子棋 - MT。
安全性
在开发游戏前,先要保证游戏中的代码能安全的运行在 NESBox 上,也就是说不允许游戏的代码非法读取 NESBox 中的数据和操作 NESBox 的界面。于是我开发了一个 JavaScript 沙箱,他在一个隔离环境中运行 JavaScript 代码,并且导入了一个全局对象 nesbox ,他让游戏更简单的接入 NESBox API,这样使用他:
nesbox.init({
width: 256,
height: 240,
getVideoFrame: () => {
// 一帧视频,RGBA 格式
return getVideoFrame();
},
getAudioFrame: () => {
// 一帧的音频数据,目前只支持 44100Hz 的音频
return getAudioFrame();
},
getState: () => {
// 获取游戏状态,NESBox 将状态保存在本地
return getState();
},
setState: (state) => {
// 恢复游戏状态
setState(state);
},
});
只需要在游戏中实现这几个函数即可。而读取输入则这样使用:
// 玩家 1 是否刚刚按下开始键
nesbox.isTap(nesbox.buttons1.Start);
// 玩家 1 是否按下开始键
nesbox.isPressed(nesbox.buttons1.Start);
// 获取鼠标位置
nesbox.cursorPosition(Player.One);
如果使用其他语言,WASM 则是一个天然的沙箱环境,限制则是只能使用 NESBox 指定的导入 WebAPIs,如 console.log。
ECS 库
并非任意游戏框架开发的游戏都可以移植到 NESBox,由于沙箱的作用,绝大多数的 WebAPIs 将不能访问。出于实践的目的,我开发了一个基于 ECS 构架的库,这样使用他:
const helloWorld = (world) => {
console.log("hello");
for (const entity of world.getEntitiesIter()) {
if (entity.hasComponent(SizeComponent)) {
// 针对这些实体做一些事情
}
}
}
const world = new World(WIDTH, HEIGHT).addSystem(helloWorld);
world.addEntity(new BasicEntity().addComponent(new SizeComponent(10, 10)));
并且有 getVideoFrame getAudioFrame 等接口无缝接入 NESBox。
资源管理
游戏一般需要渲染图片、文本,还要播放音效,沙箱没有导出网络接口,另外我希望 NESBox 上的游戏足够小,所以游戏中不会包含各种图片、音频格式的解码器,所以我选择一种统一的、简单的资源压缩方案 - QOI 压缩,并提供了一个 Web 界面:
只需要下载压缩的资源并复制代码到项目中即可。
现在,已经可以利用上述工具开发 JavaScript 游戏了。
让 Rust 游戏快速接入 NESBox
Bevy 是一个优秀的游戏引擎,并且有较为完善的生态,可惜的是,由于安全性的问题不能将 Bevy 的游戏直接移植到 NESBox 中。于是,我结合 bevy_ecs 和自己写的渲染插件、音频插件、资源管理插件开发了一个工具集 nesbox-utils,他导出了一个宏 nesbox_bevy,让你直接开始编写游戏逻辑:
use nesbox_utils::prelude::*;
#[nesbox_bevy]
pub fn create_app() -> App {
let mut app = create_bevy_app(256, 240, Color::default());
// 用来加载图片、字体和音频
let mut assets_resource = app.world.get_resource_mut::<AssetsResource>().unwrap();
let sound = decode_qoi_frame(include_bytes!("../assets/select-an-item.mp3.data"));
assets_resource.load_audio("select", sound);
// 添加游戏的逻辑
app.add_state::<AppState>().add_system(global_handle);
// 函数返回 app,但不能调用 `run()`,因为 app 将通过 NESBox 来调度更新
app
}
结语
不是任何游戏多能在 NESBox 中运行,但是可以用任何语言开发游戏发布到 NESBox。目前开发的工具不能满足各式各样的游戏,仍然要很多问题需要解决,例如
- 碰撞检查
- 矢量文本渲染
- ...
最后
如果你也想发布自己的小游戏,你可以 Fork NESBox,然后在 games 文件夹中参考已有游戏创建自己的项目,测试通过后通过 NESBox 发布自己的游戏,你有任何问题也可以联系我。