
最近想试试做独立游戏开发,由于自己对C++比较熟悉所以选择了ue5(对蓝图一窍不通,感觉没有时间和兴趣去学蓝图),上手之后感觉也不怎么需要看教程,遇到啥问题问ai基本上都能解决,可以说相当友好了。由于我特别喜欢玩博德之门3,想带入其他背景(比如葬送的芙莉莲)做一个类似的rpg游戏,在开发技能系统的时候感觉挺有意思,所以对一些比较关键的实现做一个记录。
一、技能类自身相关
1.1 技能基类
技能本身实现一个技能基类给各种具体的技能去继承,提供多个虚函数接口给外面调用。接口一开始的设想比较简单,但是随着后面需求越来越复杂也有所变化,所以先放到后面再讨论。对于初次接触ue5的人来说,有一点需要特别注意,就是自己创建类的时候就算不使用什么ue5的功能,最好也要继承自ue5中的UObject类,否则这个类就不会被反射系统识别到,ue5很多东西都依赖这个反射系统,比如UFUNCTION(),UPROPERTY()这些宏定义,如果这个类不在反射系统里面,他不会为这个类生成.generated.h的头文件,在这样的文件里面定义的东西都不能进反射系统,可能在其他地方会有困扰(例如我有个函数需要绑定委托,但是这个函数里面的参数不在反射系统里面,这样就绑定不了,因为委托绑定的函数是强制要有UFUNCTION()宏的)。
此外,虽然技能种类繁多,功能各异,但是仍有很多共通之处,这共通的部分很大程度上占据了技能的主要功能,因此,这里使用了ue5的数据资产来对技能进行统一的配置,当技能类被创建后,可以通过技能类型在数据资产中找到对应的参数,来对技能进行初始化,既减轻了开发负担(不再需要在代码中手动调整每个技能的参数),也方便在编辑器中直接调整技能数值。数据资产的具体内容包括了技能图标、技能名称、技能消耗、技能伤害等等信息,示例如下:

1.2 技能释放管理器
下一个问题是,技能要怎么去释放,由谁来调用?这里放弃了由角色类自己内部释放技能的方式。可以设想一下:如果世界里面有非常多的角色,每个角色都有自己的技能,那在每个角色的对象里面维护自己的技能类,感觉会很浪费资源。尤其是很多技能(如普通攻击、跳跃等)是几乎所有角色都会的。因此这里使用了一个统一的技能管理器来专门负责技能的释放,这个管理器继承自ue5中的UGameInstanceSubsystem,可以理解为软件开发中的单例设计模式。这里本来想自己实现,问了ai才知道ue5有现成的可以直接用。这样做有一个明显的好处,任何外部类只要能获取到世界(GetWorld()),就能获取到这个技能释放管理器的指针,然后调用其接口进行技能释放,能够保证全局唯一并且节约资源。比如下面这样:

下一步需要实现技能管理器内部的技能释放处理。最开始的方法非常简单,就是传入一个技能类的枚举类型,然后创建对应的技能类对象去处理这个释放。在之后引入其他功能后认为这样的处理方式存在明显问题,除了技能释放之外,很多其他地方也需要对应的技能类来获取信息(例如:ui上面鼠标悬停在技能图标上时需要显示技能信息,技能选中后待释放阶段需要根据鼠标悬停的目标实时展示技能准确率等),这样做会导致技能类被频繁创建和销毁,是很不合理的做法。在问过ai后,ai给的建议是使用对象池来管理,对象池内部主要维护下面这样一个数据结构:

通过一个技能类到技能对象数组的映射(这里的TMap和std::unordered_map是一样的,就是一个哈希表),可以在外部从技能类对应的数组里面取出/放回技能类的对象,这样可以在技能管理器中需要的时候,从中取出一个,用完之后再放回去,这样就可以避免技能对象频繁的创建和销毁,感觉是一个很合理的解决方案。
1.3 技能释放流程
有了上述基础后,技能管理器可以对外提供两种接口,首先是用于给npc释放技能的接口,因为npc释放技能一旦确定了是立即释放的,没有中间步骤。但是玩家在释放技能时存在 默认状态-》选择技能-》选择目标-》释放技能并回到默认状态 这样一个流程,因此需要至少提供两个接口给玩家释放技能时调用,分别是技能被选择和技能被释放/被取消,在ui中点击技能图标后,此时进入技能已选择状态,需要执行通知ui展示技能释放范围、技能释放信息等操作,直到玩家选择目标,或玩家主动取消技能回到默认状态。
二、角色类相关
2.1 角色buff
现在已经完成了简单的技能释放逻辑。熟悉博德之门3的读者应该了解,技能最终产生的效果与施法者、目标角色都有关系,并不是只和技能本身有关。如果目标角色身上有祝福术,则针对豁免类型的技能会有1d4的加成,或者施法者身上有触发的巢穴之母的复仇,攻击时会附加额外1d6毒素伤害,如果目标身上有穿刺易伤,则其受到的穿刺伤害会翻倍等等。这些附加在施法者和目标角色上面的buff效果,是技能释放时技能本身无法考虑的。当然,可以在释放时通过遍历角色身上的buff效果,根据buff的类型手动调整技能释放结果,但是这样显然不是一个合理的解决方案,在buff效果种类多了之后会非常抽象。目前的实现思路是:由一个buff的基类派生出不同的buff类型,在角色类内部维护一个角色buff的列表,和对应对象的指针,记录当前角色身上的buff。在技能释放的时候,先由技能管理器内部从技能池中获取一个技能对象,然后让技能对象计算一个基础结果(例如攻击投掷1d20+属性调整值,或者豁免检定结果等等),根据此结果得到一个技能上下文,将其广播给施法者/目标角色身上的buff对象,然后由施法者/目标角色身上的buff对象对这个技能上下文进行修正,最后得到一个修正后的技能上下文,并以此为依据来得到最终的技能执行结果。例如,技能上下文可以包含以下信息:

(上图只是示例,没有包括伤害类型信息,实际实现时应使用类似TMap<EDamageType, int32>的结构进一步细分伤害类型)由技能对象本身得到BaseDamage,然后将这个技能上下文扔给角色中生效的buff对象进行处理,例如如果目标角色有易伤的buff,则向DamageRatioFix中Add一个2,如果释放者有巢穴之母的复仇buff,则向DamageBaseFix中Add一个1d6的结果,这样在考虑完角色身上的buff之后可以得到一个最终的释放结果进行结算。
三、反应相关
最后还有一项功能希望实现,类似博德之门3中的反应机制。我一直认为反应是一项非常重要的资源,但是在博德之门3中没有特别好的体现,有很大的扩展空间。可以设想一下葬送的芙莉莲背景下两个法师对战的战斗体验,一方使用攻击魔法,另一方使用防御魔法,两边可以打的有来有回,也许能呈现出剧中提到的“消耗战”的效果。博德之门3其实有类似的机制,就是法师的护盾术,但是这部分做的比较单一,几乎有且只有这一个护盾术,其他类似的比如法术反制、预言法的预言骰子有一定类似的作用。我希望实现更复杂的反应技能,也许能够达到更好玩的战斗体验,毕竟博德之门3中玩家在回合外能做的事情太少了,甚至可以参考万智牌中的堆叠的概念,多设计一些能消耗反应资源释放的攻击性/防御性技能,让反应技能也能够被反应,达到类似“我法术反制你的法术反制”这种效果。总之,由于这个步骤需要玩家参与,即玩家需要决定是否使用反应,来干涉技能的释放结果,这就要求技能释放能够异步进行,因此需要对现有的技能框架做出很大调整。
在现有基础上,引入一个新的响应管理器(也是继承自UGameInstanceSubsystem的单例模式),专门负责第二节中提到的buff计算和此节的反应计算。将技能释放分为两个阶段:pre_excecute和post_excecute,分别代表技能开始释放(还没生效)和技能结算完成。当技能被试图释放时,由技能管理器调用pre_excecute,并将对应的技能上下文信息抛给响应管理器。响应管理器主要做两件事:第一,根据抛过来的技能上下文,计算角色buff结果,并将其附加到技能上下文中;第二,根据修正后的技能上下文,计算一个预期的释放结果(比如是否命中),判断角色身上的反应技能能否被触发(例如护盾术的触发条件是攻击投掷-目标AC<5点,因为护盾术只能给AC+5),这部分还需要分为两种情况,即npc的反应和玩家的反应。npc的反应需要根据npc自身的控制逻辑决定是否触发反应,而玩家则需要将对应的反应技能显示到ui上,由玩家决定是否触发反应(这也是为什么技能释放需要异步进行的直接原因)。此步骤结束后,再调用post_excecute进行最终的技能结算,并将取出的技能对象返回技能池,此时技能释放才算最终结束。一个典型的完整流程如下所示:

至此,算是比较完善地实现了技能系统释放技能的完整流程。至于前面提到的“反应触发反应”之类的复杂功能,在现有框架上应该不难实现。
最后叠个甲,由于我没啥游戏开发经验,ue5的很多东西更是完全不了解,后面还需要去研究一下怎么做技能释放的动画,以及控制npc的逻辑,还有ue5自带的GAS和行为树什么的。上述内容可能有写错或者可以改进的地方,如果你觉得哪里不对或者可以改进,非常非常欢迎和我交流,我也想把这个项目越做越好!