从零打造一个小游戏平台

532 阅读4分钟

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 发布自己的游戏,你有任何问题也可以联系我。