JS 游戏引擎 - 管理模型资源

本文给大家介绍笔者所在团队自研的 JS 游戏引擎是如何管理模型资源的,这里预告一波儿如何用 JS 游戏引擎实现 PID 算法,下面是基于 PID 实现的两轴飞行器定高效果:

在这里插入图片描述

需求描述

模型是什么?模型就是 ECSM 架构中的 Model,笔者在 JS 游戏引擎 - 物体编辑器 这篇文章里介绍了 ECSM 架构,提到将 Model 抽象为一种资源,独立于引擎进行管理,那么具体是怎么抽象和管理的呢?

需求分析

先来看看使用 Model 的基本流程:开发者制作 Model,游戏引擎加载 Model,游戏引擎使用 Model 创建 Entity。这里面涉及到几个关键问题:

  1. 开发者制作的是什么?也就是 Model 以什么形式存在?
  2. 游戏引擎从哪里加载 Model?也就是 Model 是怎么存储的?
  3. 游戏引擎怎样加载 Model?
  4. 游戏引擎怎么运行 Model?也就是 Model 在内存里的状态是什么?
  5. Model 需要版本管理吗?
  6. 需要设计 Model 的加载策略吗?

方案设计

定义形式

Model 用于描述 Entity 的具体内容,简单来说就是游戏引擎创建一个 Entity 时所依赖的一段 js 代码,通过这段代码,我们可以为 Entity 添加各种 Component。因此 Model 存在形式的核心就是 js 脚本。

这里借鉴 npm 包的概念,我们定义 Model 的实际存在形式为单文件夹,该文件夹下包含一个主文件和其它资源文件。主文件也是入口文件,通常为 index.js,其它资源文件可能是图片、JSON 文件等,为主文件所引用。

Car/
|--index.js
|--light.png
|--shine.json

明确存储

怎么存储 Model?通常的思路是使用一个存储服务,然而我们的产品是 小世界,这是一个基于 SolidJS 的纯前端应用,它的底层使用了团队自研的 JS 游戏引擎,现阶段我们还未计划引入后端服务。

其实 Model 本质上就是一种静态资源,在这个意义上,一个 Model 和一张图片没有区别,我们完全可以把 Model 放到静态资源服务器上,具体操作就是把 Model 放到项目工程的静态资源目录下,比如 public。

public/
|--models/
    |--Car/
        |--index.js
        |--light.png
        |--shine.json

加载方式

游戏引擎只需要加载 Model 的主文件 index.js,而加载 js 文件考虑两种方式:

  1. 使用 xhr 或 fetch API 获取 js 文本,然后通过 eval 或 Function 执行该文本;
const modelContent = await fetch('/public/models/Car/index.js');
const model = eval(modelContent); // 实际需要考虑 eval 的返回结果是否符合预期
  1. 使用 import 直接获取 js 模块。
const model = (await import('/public/models/Car/index.js').default;

我们选择了第二种方式,因为我们希望得到的 Model 是一个符合 ES6 模块规范的 Module,以方便引擎使用。

编写规范

Model 被游戏引擎加载后,在内存里就是一个符合 ES6 模块规范的 Module,这也就决定了应当按照 ES6 模块规范来编写一个 Model:

// public/models/Car/index.js

export default { 
    name: '小车',
    propsSchema: { },
    onCreate,
    onUpdate,
}

async function onCreate() {
    // 添加各种 Component
}

async function onUpdate() {
    // 更新各种 Component
}

版本管理

Model 需要版本管理吗?

当一个 Model 的内容发生变化,那么所有使用(引用)它的地方都应该关注并处理这种变化;同样地,当一个 Model 依赖(引用)的内容有变化,那么它也要关注并处理这种变化。

比如,游戏引擎加载运行 Model,Model 代码里会使用引擎提供的 API,此时 Model 就依赖了引擎,那么当引擎发生变化,Model 就应当关注这种变化,要么打补丁,要重新制作。

又比如,一个 Model 依赖另一个 Model。在一个 Model 的代码逻辑里,我们不仅可以添加各种 Component,还可以加载使用另外一个 Model,比如基于这个 Model 创建 Entity,再调用它的方法:

// public/models/Car/index.js

export default { 
    name: '小车',
    propsSchema: { },
    onCreate,
    onUpdate,
}

async function onCreate() {
    // 添加各种 Component
    
    // 添加子 Entity
    const model = await director.loadModel('/public/models/Box/index.js');
    const box = director.createEntity(model);
    this.addChild(box);
    
    box.getMehtod('doSomething')();
}

async function onUpdate() {
    // 更新各种 Component
}

类似地,当更新了 Box Model,原有的 Car Model 可能无法处理新的 Box Model,此时需要给原有的 Car Model 打补丁或者再制作一个新的 Car Model。

其实只要引用方与被引用方都能总是同步(不论是自动还是手动)使用最新的内容,就不需要版本管理,但现实是我们无法保证这种同步,不论是在需求层面还是在手段层面,所以我们还是需要对 Model 做版本管理。

语义化版本

我们借鉴了 npm 包的版本管理,选择 semver 对 Model 做语义化版本管理,制定了 Model 命名规范,将 Model 名作为 Model 的唯一标识,并支持了单级的命名空间。

public/
|--models/
    |--base/
        |--Box
            |--v1.0.0
                |--index.js
    |--Car/
        |--v1.0.0/
            |--index.js
            |--light.png
            |--shine.json
        |--v1.5.0/
            |--index.js
            |--light.png
        

上面的目录结构对应了 3 个 Model:@base/Box@1.0.0、Car@1.0.0、Car@1.5.0。

接着我们就可以使用 Model 名来加载 Model:

const model = await director.loadModel('@base/Box@1.0.0');

当然在 loadModel 里我们需要将 ‘@base/Box@1.0.0’ 解析成 ‘/public/models/base/Box/v1.0.0/index.js’,这里面会涉及到版本解析,用到 semver 相关 API ,在此不赘述。

另外顺便提一个使用语义化版本的好处,就是获取依赖内容的指定版本范围内的最新版本:

// public/models/Car/index.js

export default { 
    name: '小车',
    propsSchema: { },
    onCreate,
    onUpdate,
}

async function onCreate() {
    // 添加各种 Component
    
    // 添加子 Entity
    const model = await director.loadModel('@base/Box@^1.5.0');
    const box = director.createEntity(model);
    this.addChild(box);
}

async function onUpdate() {
    // 更新各种 Component
}

上面的代码保证 Car 在不修改代码的情况下总能使用到 Box 的 >=1.5.0 < 2.0.0 范围内的最新版本。

加载策略

我们的游戏引擎在运行一个游戏场景前,需要先加载该场景对应的 Model,然后在运行该场景时,又可能会动态加载一些资源,通常是一些动态引用的资源,比如用到的一些图片、JSON 文件、音频等,又比如动态创建子 Entity 前,需要先加载子 Entity 对应的 Model。现在我们希望能够尽可能早地加载那些动态引用的资源,该怎么做?

我们可以将那些动态引用的资源声明在 Model 的导出,具体来说就是将动态引用的 Model 声明在 dependencies 上,将其它动态引用的如图片声明在 assets 上:

export default {
  name: 'dp1',
  dependencies: {
    model1: '@testDeepDependence/dp1_1@1.0.0',
  },
  assets: {
    asset1: './dp1.jpg',
  },
  onCreate,
}

async function onCreate() {
    // ...
}

在通过 loadModel 得到一个 Model 时,我们分析其导出,收集它的资源声明,接着使用 requestIdleCallback 在浏览器空闲时去加载并存储这些资源,若遇到一个 Model 资源时,就继续使用 loadModel 加载,这是一个递归的过程。

总结

本文介绍了 JS 游戏引擎是如何管理模型资源的,先讲述模型的含义,然后提出模型管理的关键问题,接着从定义形式、明确存储、加载方式、编写规范、版本管理、加载策略几个方面,详细地阐述整个模型管理的方案设计,欢迎大家交流意见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值