雷火学堂

雷火学堂

游戏技能系统漫谈

游戏程序 2022.12.08

在游戏里你可以从大佛掌心飞跃到宗山之巅,从铁索悬棺瞬移到矿道纵横,飞檐走壁、破窗袭击、射人暗杀,连珠反弹的火炮、魂燃一线的快感.....游戏里激烈爽快的技能系统,可以让你沉浸式体验游戏的乐趣!

本文从程序的角度,围绕技能系统的设计,笔者尝试从几个核心概念出发,做一个游览式叙述。技能系统的很多设计思路适用于各种类型的游戏,限于笔者自身的经验局限,本文的很多例子和叙事将从 MMORPG 类游戏的角度来叙述。

本文对世界的基本理解,认为时间和空间是两个关键属性,万物只是空间中一组随时间发生变化的状态。

这篇文章是漫谈,便借用时间、空间、状态,这几个概念,来引出技能系统的一些关键点。时间、空间、状态间的关系,其实很紧密,没有其中一项,另外两项也难有意义,想要谈论时间的时候不涉及空间和状态是很难的,这篇文章也是如此,概念之间会有交错。

 

一、空间

 

任何一个技能,都需要借助某个实体施放出来,这个实体可以是玩家角色,可以是箭塔、小兵之类的怪物,也可以是不可见的隐形实体。不管是哪种情况,这个实体肯定存在于空间的某处,拥有位置属性,也就是坐标。
位于各自坐标的实体,相互间会产生一些空间关系,例如相距太远的两个玩家无法互相见到,再如下图玩家技能的障碍效果。

 

动图封面
玩家技能的障碍效果

这些空间关系,一者会影响到技能施放的条件,再者影响到技能施放的表现。

 

1.1条件

技能在施放前需要经过一系列的条件检查(或者说约束),技能发起者是否拥有这个技能,发起者的等级是不是足够;在技能施放过程中也会有一些条件检查,譬如筛选出距离跟自己足够接近的技能目标。

每个技能都会有一些类似的约束以及独特的约束,如果所有约束都由游戏研发工程师来实现,那么不仅会降低技能实现的灵活性,还会强化技能间的耦合,使得技能的维护越来越困难,滋生 BUG 。比较好的方法是,提供一些趁手的工具,将这些繁琐能提供大量灵活性的业务交给策划来设计实现。

那么我们该开发什么样的工具来供策划大佬发挥呢?

约束条件大多是一些等式或者不等式,可以使用声明式的语句来表示,例如 player.Grade > 50 。
大家平时应该都用过搜索引擎,进入搜索引擎的高级检索,可以看到一些逻辑操作的选项和筛选功能。我们可以借鉴这个思路,提供一种支持逻辑操作和筛选的声明式语法交给策划使用。

如果策划想要某技能总是对身边 5 米内且仇恨值大于 20 的怪物生效,那么对应的条件配置可能是这样 select * from monsters where dist < 5 and hate > 20 (这里借用了 SQL 形式的语法表述)。

守望先锋在 Networking Scripted Weapons and Abilities in Overwatch 中介绍自己的 Statescript 节点编辑器时,提到编辑器节点有两种形式,一种是声明式的节点,一种是指令式的节点。其在配置条件约束时,使用的便是声明式节点。

在一些技能密集的对战场合,复杂的条件配置和大量技能的重复条件检查可能会有不小的性能开销。“过早优化是万恶之源”,但是对于某些游戏来讲,例如有大型千人会战的 MMO,遭遇性能瓶颈几乎是必然的,因此在进行技能系统设计时就应该早做打算。

一种可行的方案是,对条件检查结果和筛选结果做缓存,用空间换时间。对于时变敏感的条件,例如实体可见关系可能上一帧的检查结果还是可见,下一帧就变为不可见了,可以通过给缓存标记版本(比如用帧号)来区分缓存的时效性。

 

1.2表现

人靠衣装,马靠鞍,技能的动画和特效表现在技能系统中是浓墨重彩的一个环节。

游戏中的实体,在没有施放技能的时候,就有不少表现。角色行走、跳跃、下蹲都有各自的动画,这些动画往往由一个状态机来控制。当角色施放技能时,会有额外的施法动作、或者是武器的挥砍动作。这些动作并不一定都是在角色站立不动时出现,比如角色可能边行走边施法,或者骑在马上做挥砍动作。这种复合形态的动作可以通过分离动画来实现,这种复合的状态关系则可以使用多个状态机组合或是分层状态机的方式来实现,如果在施法结束后还需要恢复到原来的动作状态,则可以进一步使用下推状态机

角色的位置和动作,一般为了解耦,是互相正交的。即角色动作播放过程中,其位置不会发生移动。举个最简单的例子,角色行走。角色的移动由移动管理器控制,角色的行走动作由动作管理器控制,如果移动管理器正常运行,动作管理器停止播放,我们将看到角色在地表平移。理想情况下,移动管理器和动作管理器默契配合,角色的行走看起来十分自然。当角色移动加速时,动作管理器也给行走动作加速。

在某些情况下,角色的动作可能比较复杂,比如下图中角色突然加速跑,还做一个华丽的凌空跳。这些情况下,动画的位移变化和速度的关系无法用简单的线性表达式得到,这使得移动和动作管理器间的配合变得困难。

动图
角色突然加速跑,还做一个华丽的凌空跳

UE使用根运动( RootMotion )来解决这个问题,从 UE 的叙事角度,通常的实体运动是碰撞胶囊体驱动动画。而某些复杂运动需要动画驱动碰撞胶囊体。比如这个向前猛冲并砸下锤子的例子。

动图封面
向前猛冲并砸下锤子

雷火的一些 MMORPG 项目中,以《新倩女幽魂》为代表,通过锁定来实现类似的效果。在倩女中,一个实体在客户端是一体两面的,作为引擎对象,有移动的属性;作为渲染对象,有动作的属性。通常情况下,引擎对象和渲染对象互相锁定;遇到复杂运动时,临时解除锁定增加自由度。比如偃师的幻形引路技能(见下图),会先往目标投掷一个引线傀儡,等傀儡落地后,再由引线牵引将偃师拉扯过去。幻形引路技能施放时,偃师引擎对象和渲染对象解锁,各自按自己的节奏步进,由于引擎对象不可见,从玩家角度只看到渲染对象在顺畅播放动作。

动图封面

 

二、状态

 

从计算机的角度看,状态就是一堆数据,所有状态都可以通过数据来进行表示。

游戏世界中对象繁多,对象间关系错综复杂,在时间和空间的作用下更是使得状态变化万千。我们希望游戏程序就像是一个织布机,游戏的状态则是送入织布机的经纬,只要不断提供新鲜的数据,游戏就能产生丰富的内容。譬如大家津津乐道的 DotA 系列玩法,最初由 Eul 通过《魔兽争霸III》自带的地图编辑器创作。而《Dota 2》的地图编辑器又孕育出了自走棋类玩法。

在技能系统中,同样涉及到许许多多的状态,好的技能系统应该允许游戏设计师可以设计出无穷无尽的有趣的技能。技能系统用到的大部分数据,应当都是预先配置的,譬如技能的各种属性、技能的施放流程、技能的应用效果、技能的表现方式等等。

好的配置系统应当具备如下特点:

  • 可复用
  • 规则统一
  • 集合简练
  • 机制容错

可复用,意味着同样的技能,策划不需要配置两遍,有些复杂的技能,包含许多子技能,而有些不同技能间的子技能是完全一致的,那么策划可以通过只配置一次子技能,其他技能只要引用这个子技能就可以;规则统一,意味着技能的设计有明确的方法可循,每一步设计都可以有对应的配置方法;集合简练,意味着游戏设计师没有太多的记忆负担,如果配置项又多又杂,配置的难度甚至可能超过编程实现;机制容错,意味着在配置时,降低游戏设计师手抖填错的机会,规避常见的拼写错误及明显的不合理数值。

除了静态的配置数据,技能系统还涉及一些运行时数据,可分为实体数据上下文数据。(如何对数据做科学合理的分类,仁者见仁,智者见智,这里的分类来自本人一些浅薄的工作经验)

实体数据指那些施放技能的实体所拥有的属性,实体数据包括战斗属性非战斗属性。像血量、体力、攻击力、躲避、抗性等等都属于战斗属性,这些属性都可能成为技能结算公式中的参数影响结算结果。非战斗属性中,有些是作为施放技能的条件,譬如角色的各种状态(冰冻、混乱、眩晕等)、视野大小等;有些是技能施放的结果,譬如角色仇恨、PK值等。条件或者结果数据并没有严格区分,前一个技能施放的结果可能会称为另一个技能施放的条件。

实体数据按照有效时间长短可分为临时属性时限属性永久属性。对于同一个属性,在玩家看来可能是同一个东西,在实现上可能是多个数据的组合。比如血量,就可以是下面这样的公式:

玩家看到血量 = 临时血量 + 时限血量 + 永久血量

临时血量只作用于当前战局,当角色离开战局,则临时血量归零;时限血量具有一定的有效期,在有效期内,角色即使离开游戏,重新上线也还是存在;永久血量则长期存在于角色身上,往往跟角色的等级和装备情况有关。

上下文数据又可分为事件上下文瞬发上下文持续上下文,这里其实扩展了上下文这个词的含义,可能更合适的词是语境。这些上下文可能是简单的一个数值,也可能是一些状态的组合,也可能是一个包含数据和行为的上下文对象。

事件上下文,其实是一些事件,没有实际的数值,可以理解为状态的跳变,譬如玩家请求施放一个技能、请求中断施放、或者是发起一个蓄力事件。当然这个事件不一定是由玩家发起,也可以是由某些条件的满足触发,比如玩家血量低于 30% 时自动回血。

瞬发上下文,指的是该上下文只在当前帧的当前代码段使用,比如复制对方攻击力,从获取对方攻击力到设置到自己身上,是瞬间完成的。

持续上下文,指的是该上下文有时间属性,可以短期或者长期发挥作用。当数据有了时间属性,情况就会立马变得复杂起来。持续上下文又可以进一步划分,譬如有些像增益效果,如每秒回 10 点血,持续 30 秒;有些则是需要持续收集的数据,如统计闪电链技能攻击到的目标数量;有些则是收集数据的时间和使用数据的时间不一致,如弹道技能在施放者下线后还能发挥作用,但是弹道技能在击中目标时可能还需要用到施放者的一些信息。

了解了状态的各个类别和特点,我们得以进一步对状态进行组织,添加操作它们的行为。这里我们只关注基本的组织框架,一个好的框架,能与系统的其他部分顺畅协作,能够满足项目独特的开发需求。

本节介绍两种比较主流的技能组织框架,分别以虚幻引擎的玩法技能系统和《守望先锋》(后面简称 OW )的实体组件系统为代表。

 

2.1玩法技能系统

虚幻引擎遵循面向对象的设计思路,将技能的不同方面划分为若干抽象概念,通过对这些抽象进行组合,便能创造出各种技能。
玩家技能系统(Gameplay Ability System)的主要抽象:

  • 技能系统组件 AbilitySystenComponent
  • 玩法技能 GameplayAbility
  • 技能任务 AbilityTask
  • 玩法效果 GameplayEffect
  • 玩法属性 GamePlayAttribute
  • 玩法标签 GameplayTag
  • 玩法提示 GameplayCue
  • 玩法事件 GameplayEvent

拥有技能组件系统的角色可以使用玩法技能,玩法技能反过来影响角色的玩法属性

玩法效果提供修改玩法属性的方法。由于属性可以分为临时属性、时限属性和永久属性,因此玩法效果提供了修改属性的三种方式,包括即刻(intant)、持续(duration)、无限(infinite)。

即刻,例如满血药可以立即将血量回满;持续,例如持续回血药,每秒恢复一定血量,持续一段时间后失效;无限,例如自动回血,只要角色不是满血,每秒恢复一定血量。

如果说玩法效果用于处理瞬发上下文和持续上下文,那么技能任务可用于处理事件上下文,包括可以处理时序相关的延时事件、重复事件,也可以处理来自玩法技能、玩法效果、玩法属性、玩法标签、玩法事件等等的各类事件。

技能任务在处理完事件上下文后,技能的施放就转换成了普通上下文处理,因此最终修改玩法属性还是通过玩法效果来实现的。下图是 UE 关于技能任务的一个官方示例,其中的 “Play Montage and Wait for Event” 节点就是一个技能任务节点。

玩法标签这个概念比较有趣,UE 在很多地方都用到了它,玩法标签具有层次,形如Family.Genus.Species。而且一个角色可以拥有多个标签,这样不仅实现了对角色的分类,还能够实现同对象在不同分类体系下的不同类别。UE 允许开发者根据标签识别、匹配、分类或过滤对象。简单来说,玩法标签为前面提到的声明式条件语句提供了便利。

举例来说,可以将角色划分为不同的类别:玩家、灵兽、怪物,怪物可进一步划分为天道怪物、人道怪物、建筑道怪物,那么建筑到怪物可以表示为Actor.Monster.Building,如果某个技能只能攻击到建筑到怪物那么只需要添加条件 target.ActorTag == Actor.Monster.Building 即可。
玩法标签还可以用来标识冲突状态,例如角色观战模式下不允许进行攻击,条件可以表示为 target.PkTag == Pk.Watch 。

对于体量较大的游戏,比如 MMO,角色可能有非常多的状态,角色可做的事件也非常丰富。这种情况下,如何对游戏标签进行维护,是一个值得思考的问题。

玩法提示用来处理非游戏逻辑相关的功能,也就是主要用于处理各类表现。
玩法事件是一个数据结构,承载了施放技能所需要的上下文信息,通过玩法事件可以直接施放一个技能而无需再提供其他附加信息和条件。按照前面的分类,玩法事件和技能任务一样,也是一种事件上下文。

优点:这种设计的优点在于其概念通俗易懂,开发者想要实现一个技能,很容易想到实现的方案,并且同一个问题可以有多种实现方式。就 UE 而言,玩法技能系统隐藏了底层的网络同步细节,使得开发者在实现技能逻辑时,无需关心同步细节。UE 的蓝图编辑器,使得没有编程经验的设计师也能实现复杂炫酷的技能。

缺点:首先是不同抽象间的互相调用、组合的自由度很高,如果在玩法技能系统这一层上直接进行所有技能的开发,可能会使得最终技能的调用链路错综复杂,给后期的维护和扩展留下隐患。另一方面,在这个层次上进行技能开发,即使是使用蓝图,其难度也并不会比直接编写代码低,甚至可能还是编写代码更方便快捷。

从 UE 官方 Demo 《Lyra新手教程》也能看出一些端倪:
某些Lyra技能具有C++实现,可以强制执行特定的激活条件、执行复杂的数学逻辑(在蓝图中实现会很麻烦)
更推荐的方式是在此基础上,根据自己游戏的特点,再做一次抽象,简化技能设计链路,所有技能都采用通用的链路来实现。

另一个问题是这类抽象没有考虑某些游戏对暂停、重播、预测等方面的需求,如果在采用玩法技能系统实现了所有的技能之后,突然发现有这样的需求,再想做重构调整,会比较困难。

 

2.2 实体组件系统

OW 的开发者设计了一种实体组件系统( Entity Component System ,简称 ECS )来解决 FPS 类型游戏中预测和回滚的问题。这种设计模式对于技能系统的实现模式也具有一定的参考价值。

OW 的开发者认为,现有的面向对象系统存在一些局限性,Tim Ford 举了个例子 :想象一下你家前院盛开的樱桃树吧,从主观上讲,这些树对于你、你们小区业委会主席、园丁、一只鸟、房产税官员和白蚁而言都是完全不同的。从描述这些树的状态上,不同的观察者会看见不同的行为。树是一个被不同的观察者区别对待的主体(Subject)。

类比来说,玩家实体,或者更准确地说,Connection 组件,就是一个被不同 System 区别对待的主体。我们之前讨论过的管理玩家连接的 System ,把 Connection 组件视为 AFK 踢下线的主体;连接实用程序(ConnectUtility)则把 Connection 组件看作是广播玩家网络消息的主体;在客户端上,用户界面 System 则把 Connection 组件当做记分板上带有玩家名字的弹出式 UI 元素主体。

既然不同的观察者对同一个对象会有不同的理解,那么对象应该只是一堆数据,而不同观察者对同一堆数据可以有不同的操作视角。这里作为对象的数据就是组件( Component ),操作视角就是系统( System ),而观察者就是实体( Entity )。

实体组件系统核心的三个概念很容易理解,他们之间的关系也很简单,这里借用 Unity 手册的例子再说明一下:

在此图中,系统读取 Translation 和 Rotation 组件,将它们相乘,然后更新相应的 LocalToWorld 组件。
实体 A 和 B 具有 Renderer 组件而实体 C 没有,但系统对 A 、B 、C 都一视同仁,因为系统不关心 Renderer 组件。 如果我们将系统设置为需要 Renderer 组件,那么实体 C 会被系统忽视;如果我们将系统设置为排除具有 Renderer 组件的实体,则实体 A 和 B 会被系统忽视。可以看出系统在筛选实体和关联组件这两件事上具有一定的自由度。

此外,OW 开发者为了提高实体组件系统的适用性,降低代码复杂度,以应对游戏项目工程中的复杂情况,还提出了 Singleton Component 、 Utility 两个概念。

单例组件( Singleton Component ),相当于是一堆全局数据,这类组件归属于唯一的匿名实体,可以由不同的 System 进行访问。这种组件应用面很广,在 OW 有 40 % 都是单例组件。

工具函数( Utility ),用来解决 System 之间相互调用以及 System 内部包含多个层级的问题,例如处理敌友关系的逻辑,在许多 System 中都会用到,如果说把敌友关系的逻辑也包装成 System ,那么会出现 System 互相调用,一旦开了这样的先例,最后可能会导致各个 System 之间的调用关系像一团乱麻,不容易维护。这种处理敌友关系的逻辑可以实现为工具函数。一方面,尽量避免在工具函数内部使用 System (因为如果使用System,那么其实并没有把问题解决);另一方面,如果实在需要使用 System ,那么做好访问限制,尽量把副作用局限到可控范围内。

此外 OW 开发者还提出可以通过延迟技术来减少耦合的情况,有很多工作并不需要立马完成,对于这些工作,可以放到队列里面,等到一帧结束,或者合适的时机再一并处理。

 

2.2.1 ECS框架下的技能系统

如果使用 ECS 方式来设计技能系统,可以把技能系统涉及到的多种不同类型的数据拆分到不同的组件中,每个实体根据需要来挂接不同的组件。譬如:

  • 移动组件
  • 事件组件
  • 战斗组件
  • 状态组件
  • 同步组件
  • 玩法组件

移动组件维护移动相关的数据,例如移动速度、移动方向等。

事件组件维护实体的事件列表,技能相关包括技能事件和技能时序事件,比如技能施放时触发、实体死亡时触发的事件或是间隔触发的效果事件。

战斗组件维护实体的战斗属性,例如红蓝体力、攻击力防御力等属性。

状态组件维护实体的各种状态属性,包括各种增减益状态、受控状态、条件状态等。

同步组件维护实体关注的其他实体的信息,包括关注的实体类型信息,实体的位置外观及状态信息等。

玩法组件维护实体在特定玩法下的一些数据,比如某些玩法下实体只能使用指定的技能。

动图封面

比如实现如上的火焰陷阱效果,对范围内所有敌方单位造成多次伤害。可以把陷阱作为一个实体,挂接事件组件和战斗组件。事件组件注册一个间隔触发的时间事件,战斗组件记录陷阱的伤害次数、单次伤害量、最后一击伤害等战斗属性。由于没有移动组件,位置更新系统不需要更新该陷阱;事件系统遍历到该陷阱时,若满足触发条件,则找出陷阱范围内的敌方单位,并由技能结算系统根据陷阱战斗组件中的战斗属性结合英雄的战斗属性和状态属性确定扣除的血量,作用到敌方单位的战斗组件,最后同步系统根据实体的同步组件对敌方单位的战斗属性变化进行同步。

优点:

很明显,在 ECS 的框架下,数据与操作基本解耦,数据由谁操作一目了然。这使得数据管理十分可控。由于数据的读写方是明确的,并且数据自然的以实体为单位拆分,那么容易设计出一套机制将数据的计算分摊到多个计算节点上进行处理。这对于 CPU 密集型的技能系统来说是一个很大的优势。

同样,由于数据的读写可控,容易设计出一套基于组件的命令模式,比如数据库都支持回滚操作,在 ECS 系统中,也容易实现对组件操作的回滚。这个特性是 OW 实现网络同步预测,实现游戏回放的一个基础。

缺点

ECS 框架数据和操作解耦的形式,对开发人员的设计能力提出了更高的要求,传统方式下,开发人员可以在不了解其他机制的情况下直接完成自己的实现,ECS 更要求全局性的设计考量。

ECS 的 System 设定是只能拥有行为,不能有数据。需要的数据都得包装到单例组件中。并且需要尽量减少 System 之间的互相调用。对于技能系统来说,技能行为非常多样,每个技能涉及到的各个 System 难以安排出明确的先后处理时序,即使在项目初期精心设计出一套 ECS 技能系统,随着差异化技能的增加,仍然可能导致 System 的复杂度失控,滋生出许多难以查验调试的技能Bug。

 

三、时间

 

时间的概念理解起来有一定难度,本节尝试选几个角度来做探讨。

3.1 事件

前文介绍玩法技能系统时提到技能任务可用于处理时序相关的延时事件、重复事件。在这些情况下,时间是一种事件上下文,从游戏设计师的角度看,时间事件是通过条件语句来进行配置的,包括 trigger,trigger at "2022-22-22 22:22:22" ,trigger after 22 sec,every 22 sec等,分别表示即刻触发,定点触发,延迟触发,间隔触发。基于这些配置可以实现任意时间事件。比如一个持续 22 分钟的事件可以由即刻触发和 22 分钟的延迟触发组合实现。

 

3.2 数据新鲜度

数据新鲜度表示获取到的数据与数据当前的实际值之间的差距,取到的数据与当前值越接近,数据就越新鲜。
数据在节点之间传输,会导致传输过程中数据新鲜度改变。

玩家 A 和 B 互相对战,t1 时刻,A 向命中区域内的 B 施放了一个技能,服务端在 t3 收到技能请求,此时 B 还在 A 的命中范围内(服务器要等 t4 才处理 B 离开 A 的事件),服务端判定命中。在 t1 与 t3 之间的 t2,B 离开了 A 的命中区域,并在 t5 看到自己被 A 命中了, 从 B 的视角看自己明明已经远离了 A 但还是被命中了。从 A 的视角看,B 是在 t6 才离开自己的命中区域。

可以看到对于任何一个节点来说,从其他节点传来的数据都是不新鲜的。

此外,代码的编写方式也可能导致数据不新鲜的问题。

 

bool bEnemy = CheckEnemy(playerA,playerB);
 
// 代码段 1 ...
 
if (bEnemy)
{
    // 代码段 2 ...
}

如上面这段代码,将玩家 A 和 B 的敌对状态临时存在 bEnemy 中,如果在代码段 1 中存在改变敌对关系的逻辑,那么等到后面再使用 bEnemy 来判断时,这个数据已经不新鲜了。

 

3.3 时序依赖

时序依赖意味着某事件的执行依赖于另一个事件的完成。如果调换事件发生的顺序,便会出现异常。
在技能系统中,模块或者对象的初始化与销毁阶段,常会遇到一些时序相关的问题,例如在技能效果执行过程中杀死了玩家,如果此时直接把玩家对象销毁,则玩家对象在销毁时可能会执行一些增益减益的清理操作,而当前的技能效果可能就来自于玩家的某个减益,导致技能效果执行异常。

对于上述情况,可以通过延迟处理玩家销毁来避免。延迟技术还可以用来优化技能系统的性能,比如角色状态发生改变时会触发同步事件,将角色的状态信息同步给客户端,而一次技能事件可能会频繁修改角色的状态,如果每次状态变更都触发同步,那么同步的开销就很客观。此时可以通过延迟技术,在一次技能结束之后再触发同步事件,减小同步开销。

生产实践中还有一些其他的技术手段来处理时序依赖,例如将生命周期划分为多个不同的阶段,各个阶段的执行顺序确定,只要不同时序的业务逻辑安排到相应的阶段,便可以保证时序;对于非线性的时序关系,还可以配置依赖关系表,在执行具体的业务逻辑前先根据依赖表生成一个依赖图,再依据依赖图逐一执行。

时序依赖的另一个例子是技能统计信息的使用,例如角色的某技能伤害依赖于技能前一次在完整技能链中的命中目标数量。在单节点单线程的线性执行流程下,后施放的技能肯定在前一次技能结算完成后,不会有什么问题。如果角色的技能为并行处理,尤其是一个技能链上的多个子技能并非在同一个节点上执行时,前后两个技能的时序关系是不自然保证的。

此时可以采用通道队列的方式来进行处理,即给存在依赖关系的技能设定通道,对于同一个通道的技能,塞入 FIFO 队列,只有当队列当前执行完成后,才能继续执行下一个队列中的技能。而对于不同通道的技能,则可以并行处理,如下图。

 

3.4 虚拟时间

现实世界的时间单向流动不可逆转,游戏世界的时间是虚拟的,我们可以根据需要进行时间逆行、跳变、静止。
虚拟时间给游戏开发以及游戏测试带来很多便利。我们可以实现类似于编译器调试的断点机制,来控制技能施放过程,比如给某个技能节点打上断点,查看技能的状态信息,或者是临时修改技能的某些属性来观察技能效果。
对于某些以时间作为事件上下文的技能,游戏测试工程师可以直接修改时间来触发事件,而不需要进行漫长的等待,提高工作效率。

在一些赛事环境中,如果能够在机制上实现对技能系统的暂停和回退,那么赛事主办方可以更好地把控赛事进程,比如在遇到一些突发情况时,及时暂停比赛,等问题处理完毕后再继续进行比赛。

 

参考素材

  1. 《新倩女幽魂》金刚甲技能轩辕·守带镇关山·通附体下真机实录. 
  2. 《永劫无间》双截棍连招教程视频. 
  3. Root Motion - Unreal Engine Documentation 演示动画. 
  4. Ability Tasks - Unreal Engine Documentation 示意图. 
  5. Lyra 新手教程
  6. 摘自 《守望先锋》架构设计和网络同步
  7. 《哈利波特·魔法觉醒》(https://wwwharrypottermagicawakened.com)火焰熊熊教学视频.