H5 游戏引擎的 Entity 层级设计

大家好,我是 AlphaLu。

这次给大家介绍笔者所在团队自研的 H5 游戏引擎(小世界,一个非常有意思的项目!)的 Entity 层级设计方案,基于层级机制,我们就能灵活调整 Entity 的层级:

需求描述

我们说的 Entity 层级是指视觉层面上的,当两个 Entity 出现在画布的同一个位置,就必然要确定它们间的覆盖关系,而我们希望这种覆盖关系是由我们确定的,并且是可调整的,具体来说我们想要对 Entity 做如下操作: 上移一层、下移一层、置于顶层、置于底层、锁到顶层、锁到底层、释放锁层

方案设计

PIXI 层级机制

PIXI 绘制特点

我们的游戏引擎使用 PIXI 作为渲染引擎,它的绘制特点如下:

  1. 以 Container 作为绘制的单位元素,先绘制的 Container 会被后绘制的 Container 覆盖;
  2. Container 的绘制顺序和其在父 Container 的 children 里的位置顺序一致,因为渲染过程是深度优先遍历 Container 树的;
  3. Container 在其在父 Container 的 children 里的位置由其 zIndex 属性和添加顺序确定,Container 的 sortChildren 方法会将它的子 Container 按照 zIndex 递增的顺序排列,若 zIndex 相同,则默认使用添加的顺序;
  4. PIXI 在渲染过程中,会深度优先遍历 Container 树进行绘制,当发现某个 Container 的 sortDirty 属性为 true,就执行其 sortChildren 方法,再接着遍历绘制;
  5. Container 的 sortDirty 变为 true 通常是因为发生了 addChild 或者更新了其 zIndex。

使用 PIXI

ECSM 架构中,Entity 由 PIXI 绘制,一个 Entity 对应一个 PIXI Container,具体来说就是:

  1. 在创建 Entity 时,引擎会给每个 Entity 内置一个 Transform Component;
  2. 当 Entity 被添加到场景时,引擎将 Entity 的 Transform Component 收集到 Render System,在完成添加后,我们会得到一棵 Entity 树;
  3. 在每帧都执行的 tick 函数里,Render System 会统一处理所有的 Transform Component:先统一给每个 Entity 创建一个 PIXI Container,再统一将这些 Container 添加到对应的父 Container 下,这样我们就得到一棵 Container 树。
  4. PIXI 渲染 3 中的 Container 树,绘制出整个游戏场景。

这个过程有点问题:因为 Render System 是通过列表遍历来处理 Transform Component的,导致 Container 的添加顺序取决于 Transform Component 在 Render System 里的存储顺序,这不是我们所希望的。我们希望 Container 的添加顺序和 Entity 的添加顺序保持一致,因为比如我们想要后添加的 Entity 覆盖先添加的 Entity

Entity 层级机制

我们发现此时的 Entity 树和 Container 树上的节点并不是一一对应的,其实 Entity 树是基于游戏场景逻辑构建的,它的结构才是我们应该关心和维护的,也就是我们应当把 Entity 树的结构信息同步给 Container 树,而所谓的结构信息,就是指各节点在其父节点 children 里的位置顺序。

我们借鉴 PIXI Container 的层级概念,实现 Entity 的层级机制:

  1. Entity 有 zIndex 属性,表征它的视觉层级;
  2. Entity 有 sortChildren 方法,会将它的子 Entity 按照 zIndex 递增的顺序排列,若 zIndex 相同,则默认使用添加的顺序;
  3. Entity 有 sortDirty 属性,表征它的 children 是否发生变化,比如当 Entity 添加了子 Entity,意味着它的 children 发生变化,需要重新排序,那么标记其 sortDirty 为 true;
  4. 引擎会在每一帧内收集 sortDirty 变为 true(通常是发生了 addChild、更新了 zIndex) 的 Entity到一个队列中,然后在下一帧渲染前清空该队列;
  5. 引擎清空该队列的过程就是把队列中的 Entity 逐个取出,执行其 sortChildren 方法,然后再将其 children 顺序同步给其 Container 的 children,这样就能保证 Entity 树和 Container 树的一致性。

需要注意的是,在 5 中同步后,记得把 PIXI Container 的 sortDirty 置为 false,避免 PIXI 在渲染时又重复执行 sortChildren。

Entity 层级移动

基于 Entity 层级机制,我们来实现将 Entity 上移一层、下移一层、置于顶层、置于底层。

现在有一个 Entity 的 children 如下:

js 复制代码
[et1, et2, et3, et4, et5] // children
[  0,   0,   1,   3,   3] // 对应的 zIndex

上 | 下移一层

假设需要将 et2 上移一层,我们就把 et2 往后移一个位置:

js 复制代码
[et1, et3, et2, et4, et5] // children
[  0,   1,   0,   3,   3] // 对应的 zIndex

我们发现此时的 zIndex 并不是递增的,为了保证 sortChildren 后 et2 还是紧随 et3 后,我们需要修改 et2 的 zIndex:

js 复制代码
[et1, et3, et2, et4, et5] // children
[  0,   1,   1,   3,   3] // 对应的 zIndex

置于顶 | 底层

类似地,如果我们想把 et2 置于顶层,就先把 et2 移动到最后面,然后修改其 zIndex 为最大值即可:

js 复制代码
[et1, et3, et4, et5, et2] // children
[  0,   1,   3,   3,   3] // 对应的 zIndex

Entity 层级锁定

基于 Entity 层级机制,我们来实现将 Entity 锁到顶层、锁到底层、释放锁层,这里锁到顶层的含义是该 Entity 在被释放锁层前将永远处于顶层,并且它的层级不会被改变。

现在有一个 Entity 的 children 如下:

js 复制代码
[et1, et2, et3, et4, et5] // children
[  0,   0,   1,   3,   3] // 对应的 zIndex

锁到顶 | 底层

假设需要将 et2 锁到顶层,我们就先把 et2 移动到最后面,然后修改其 zIndex 为 Infinity 即可:

js 复制代码
[et1, et3, et4, et5, et2] // children
[  0,   1,   3,   3, Infinity] // 对应的 zIndex

Infinity 能保证在 sortChildren 后,et2 都是排在最后一个位置。

另外,我们需要在 Entity 层级移动功能的实现中排除被锁到顶层或底层的 Entity。

释放锁层

当我们想要 et2 不被锁到顶层时,就修改其 zIndex 为最大值即可:

js 复制代码
[et1, et3, et4, et5, et2] // children
[  0,   1,   3,   3,   3] // 对应的 zIndex

这样 et2 被释放锁层时,还是处于最顶层,可以避免非预期的视觉突变效果。

总结

本文给大家介绍了 H5 游戏引擎的 Entity 层级设计方案,先描述我们需要的 Entity 层级调整功能,然后分析在 ECSM 架构中单纯使用 PIXI 层级机制的问题,接着设计了 Entity 层级机制,最后讲解基于该机制实现 Entity 层级移动、Entity 层级锁定,欢迎大家交流意见。

相关推荐
天下无贼!40 分钟前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr40 分钟前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林43 分钟前
npm发布插件超级简单版
前端·npm·node.js
罔闻_spider1 小时前
爬虫----webpack
前端·爬虫·webpack
吱吱鼠叔1 小时前
MATLAB数据文件读写:1.格式化读写文件
前端·数据库·matlab
爱喝水的小鼠2 小时前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
WeiShuai2 小时前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
Wandra2 小时前
很全但是超级易懂的border-radius讲解,让你快速回忆和上手
前端
ice___Cpu2 小时前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill2 小时前
nestjs使用ESM模块化
前端