本文给大家介绍笔者所在团队自研的 JS 游戏引擎是如何管理模型资源的,这里预告一波儿如何用 JS 游戏引擎实现 PID 算法,下面是基于 PID 实现的两轴飞行器定高效果:
需求描述
模型是什么?模型就是 ECSM 架构中的 Model,笔者在 JS 游戏引擎 - 物体编辑器 这篇文章里介绍了 ECSM 架构,提到将 Model 抽象为一种资源,独立于引擎进行管理,那么具体是怎么抽象和管理的呢?
需求分析
先来看看使用 Model 的基本流程:开发者制作 Model,游戏引擎加载 Model,游戏引擎使用 Model 创建 Entity。这里面涉及到几个关键问题:
- 开发者制作的是什么?也就是 Model 以什么形式存在?
- 游戏引擎从哪里加载 Model?也就是 Model 是怎么存储的?
- 游戏引擎怎样加载 Model?
- 游戏引擎怎么运行 Model?也就是 Model 在内存里的状态是什么?
- Model 需要版本管理吗?
- 需要设计 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 文件考虑两种方式:
- 使用 xhr 或 fetch API 获取 js 文本,然后通过 eval 或 Function 执行该文本;
const modelContent = await fetch('/public/models/Car/index.js');
const model = eval(modelContent); // 实际需要考虑 eval 的返回结果是否符合预期
- 使用 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 游戏引擎是如何管理模型资源的,先讲述模型的含义,然后提出模型管理的关键问题,接着从定义形式、明确存储、加载方式、编写规范、版本管理、加载策略几个方面,详细地阐述整个模型管理的方案设计,欢迎大家交流意见。