前言

要准备开始把状态帧同步方案接入Moba项目了,其中比较重要的两部分是战斗系统的重构以及技能编辑器的适配,前者先不谈,内容多而细碎,后面会单独出文章整理说明。技能编辑器目前使用了是基于节点的行为树编辑器,说实话对于技能的配置与效果的预览并不友好,但不可否认的是其逻辑组织能力雀食优秀,为了弥补其短板,准备接入一个Timeline编辑器,我的选择是ParadoxNotion-Slate,理由是界面美观,可拓展性较好,源码注释详细简洁。

这篇文章就是梳理下Slate的架构以及技能编辑器的适配思路。

Slate版本:2.0.2

正文

基础架构

由表及里主要有CutsceneGroup,CutsceneTrack,ActionClip三层结构,三者都实现IDirectable接口

  • CutsceneGroup:包含多个CutsceneTrack与多个Section,其中Section被用于划分CutsceneTrack区域和快捷编辑
  • CutsceneTrack:包含多个ActionClip
  • ActionClip:一个具体的行为片段,例如播放一段动画,播放一个特效,移动一段距离

具体层级关系如下

image-20210907010528531

底层驱动

Slate默认提供了三种驱动模式对CutScene进行更新

  • Normal:每次LateUpdate更新
  • Animate Physics:每次FixedUpdate更新
  • Unscaled Time:每次LateUpdate更新,但间隔为Time.unscaledDeltaTime,即不受时间缩放影响

Slate默认提供了四种终止模式,只要CutScene终止了,就会执行相应的终止模式,假设起点为A,终止点为B,终点为C

  • Skip:将会从B跳转到C,然后停在C,保留A到B区间任何修改
  • Rewind:将会从B跳转到A,然后停在A,撤销A到B之间任何修改
  • Hold:可以看作暂停
  • Skip Rewind No Undo:将会从B跳转到C,然后折返并停在A,保留A到B区间的任何修改

Slate默认提供了三种播放模式

  • Once:只播放一次
  • Loop:循环播放
  • Ping Pong:从起点播放到终点,再从终点播放到起点

Slate默认的更新顺序:

Group Enter -> Track Enter -> Clip Enter | Clip Exit -> Track Exit -> Group Exit

流程分析

需求分析

通用性相关

因为Slate本身提供的功能十分丰富,例如动画过渡,镜头控制,路径编辑,渲染相关等,可以用于项目的其他模块,例如CG编辑,剧情推演等模块,所以我们需要在不影响Slate原本功能基础上对其进行拓展。

配置相关

由于我们使用行为树对技能进行流程控制,每个Slate都是一段线性的技能逻辑,也就是说,一个技能的配置,由一个行为树和一个(或多个)Slate组成。

对于行为树,由于其职责为流程控制,所以服务端和客户端差异巨大,并且复杂多变,如果共用一颗行为树会导致太过冗杂和巨大,难以拓展和维护,所以对于客户端与服务端来说,需要各自配置一颗行为树来处理技能逻辑。

对于Slate,由于都是线性逻辑,并且相对来说比较直观和简单,所以可以共用同一配置,唯一需要注意的是具体Action所对应的行为可能需要做处理,例如一个Slate技能在第50帧会产生一个碰撞体,做碰撞检测,这个行为只会在服务端出现,所以当客户端运行这个Action时,会跑空函数,可以在脚本中通过预编译宏来控制。

时间帧率规范

考虑游戏可能会有逻辑帧Tick频率变化的需求,例如公测项目为30fps,电竞比赛时会上调到90fps来获得更精准的逻辑判定,所以在Slate配置技能时统一使用现实中的时间规范,这样一是更加符合第一时间直觉,二是可以根据游戏需求进行时间-目标逻辑帧的换算,而不必重新制作技能配置。当然了,也可以根据之前技能配置逻辑帧进行换算,得到在新的逻辑帧率下的目标触发帧,不过显得有些画蛇添足就是了。

综上,Slate配置技能时一律使用现实时间,而不是帧数,在战斗进行时提供一个实用函数将技能配置的时间点换算成目标逻辑帧。

举个例子,考虑帧同步方案中的逻辑帧概念,底层是由一个计时器进行控制帧驱动的,例如目标帧率为15fps,也就是每隔66.666…ms驱动一次逻辑帧

考虑一个技能A,会在释放技能后0.5s产生一个碰撞体,我们需要在技能配置加载时就将这个0.5s转变为一个具体的触发帧数,假设释放技能时为第600帧

  • 如果使用15fps的逻辑帧率,那么将会在第608(500/66.666 = 7.5舍入为8)帧触发这个碰撞体创建的事件
  • 如果使用20fps的逻辑帧率,那么将会在第610(500/50 = 10)帧触发这个碰撞体创建的事件

跨平台相关

由于需要在服务端执行技能逻辑,所以需要在服务端构建一套Timeline的运行时,照抄Slate的核心逻辑就可以了

由于服务端运行时不需要进行技能预览(因为我们在编辑时就所见即所得预览好了),所以可以简化一些Slate为了保证时间轴快进,回退时依旧预览正常的多余代码

序列化相关

客户端

对于常规的需求,例如CG编辑,剧情编辑等,在Slate中,每个完整的Slate配置会作为一个CutScene挂载到一个GameObject上,所以我们可以直接将Timeline自身的序列化反序列化工作交予Unity来完成(将Scene中的Gameobject持久化为Prefab即可),Slate由行为树拉起执行时,加载Prefab进行播放即可,对于Slate的中断,暂停也可以取得其CutScene组件进行控制。

但对于将其作为技能编辑器进行拓展来说,仍旧需要在客户端跑我们自己写的Slate运行时,因为Slate官方源码本身并没有“帧”这个概念,全是真实时间。

服务端

对于数据配置的导出需要自己处理,由于上面提及的跨平台相关,我们已经做了一套运行在服务器上的Slate运行时,所以我们只需要抽象出每个ActionClip的数据进行序列化,然后运行时反序列化重建Slate即可