TomLooman_ActionRoguelike_第十二章环境查询再生机器人
该专栏用于保存对TomLooman的ActionRoguelike项目的学习笔记,学习过程中的思考与记录不一定准确。
教程参考:https://github.com/tomlooman/ActionRoguelike
基于UE5.0的项目实现:https://github.com/CarolBaggins2023/TomLooman_ActionRoguelike_Tutorial

2023_08_11
EnvironmentContexQuery和生成机器人:EQS寻找机器人生成点,自定义的GameMode
我们想要在世界中生成一些机器人,这些机器人不是在编辑器中拖到世界中的,而是在世界中Spawn的,就像刷新的怪物那样。这类涉及到游戏机制的内容通常由GameMode决定,所以我们要自定义一个GameMode。
我们自定义的GameMode继承自GameModeBase类,而不是GameMode类,两者的区别是GameMode范围更小,趋向于类似虚幻争霸的游戏。
我们希望在游戏开始后,在玩家周围生成bot,一般来说与“游戏开始”有关的功能会写在BeginPlay中,但是因为GameMode本身就要调用其他对象的BeginPlay,所以GameMode本身没有BeginPlay,但有StartPlay,所以我们要在自定义GameMode的StartPlay中实现功能。
首先要保证通过Super::StartPlay()正常执行StartPlay该执行的工作,然后我们通过Timer句柄,以一定的时间间隔,循环调用生成bot的函数。SetTimer的最后一个参数就表示是否循环。
这里我们要创建时间句柄成员变量和控制循环间隔的成员变量。


Timer句柄中循环调用的SpawnBotTimerElapsed进行环境查询,并生成查询实例。其中UEnvQueryManager::RunEQSQuery的第一个参数表示在哪个世界中,第二个参数是查询模板(用之后会讲的我们在编辑器中自定义的环境查询赋值),第三个参数是查询者指针,第四个参数是在编辑器中见过的,在环境查询生成的点中,是选择最优点,前5%的点,还是怎么选,第五个参数与蓝图使用有关(没弄明白)。
在查询返回的UEnvQueryInstanceBlueprintWrapper类的查询实例中有一个委托GetOnQueryFinishedEvent,当查询结束后进行广播。因为我们是要在玩家周围生成机器人,应当在环境查询结束后,执行在查询点上生成bot的操作,所以我们让生成bot的函数监听环境查询结束委托。
这里我们要创建环境查询的成员变量,之后在编辑器中要创建一个EQS并赋值。


委托的两个参数是得到的查询实例和查询状态。当查询状态为成功时,我们通过GetResultsAsLocations得到表示为Location的查询结果,并用类似于子弹生成的方法,用GetWorld()->SpawnActor生成bot。
注意,这里要处理生成时发生碰撞的情况,否则bot会因为与floor发生碰撞而无法正常生成(教程中是可以的)。默认状态下发生碰撞时bot不生成,所以我们改为始终生成。
这里我们要创建生成bot的类别的成员变量,和之前生成子弹类似,表示某个类别时,我们创建TSubclassOf<>类的成员变量,并在编辑器中将我们机器人的蓝图赋给它。



综上所述,游戏开始后在玩家周围生成bot的逻辑是,游戏开始后通过GameMode在世界中间隔地进行环境查询,在玩家周围生成环境查询点。当环境查询结束后,广播环境查询结束的委托,从而执行生成bot的函数。在生成bot的函数中,在环境查询结果对应的位置上生成bot。
现在我们还缺少查询玩家周围点的EQS。我们新建如下的EQS,仍然是DonutPoints,但查询点生成的中心是我们自定义的包括所有玩家的查询情景,而不是只有目标玩家的查询情景。

自定义的包括所有玩家的查询情景如下,返回的不是一个Actor,而是Actor向量。

那么相应的,DistanceTest中也做对应改变,

这里我们还加了一个PathFindingTest,因为对于在有些点生成的bot,不存在通向玩家的路径,比如下面的区域,这是在一个中空方块中,一旦bot在这里生成,就被卡住出不去了。解决这一问题的方法有两种,一种就是用PathFindingTest节点,但是这样消耗比较大。另一种是用NavModifierVolume,用切除的方式将不合理的导航网格去掉。



从这个EQS的使用我们也知道了,EQS不一定用在行为树上,也不一定用在蓝图中。
上面的bot生成方法会一直生成bot,我们希望能随着游戏时间增加世界中bot的数量,但又不是无限增加。所以我们在生成bot时需要,设定某一时刻世界中bot的最大数量,如果当前bot的数量等于设定的最大数量,则不再生成bot(统计世界中bot数量的方法之后讲)。因此我们在GameMode的OnQueryCompleted中,在SpawnActor之前增加了如下语句,

其中的MaxBotCount是表示最大bot数量的成员变量,当前世界中bot数量超过它时就退出函数,不执行下面SpawnActor的语句。而MaxBotCount又受CurveFloat类的DifficultyCurve控制,就是我们常见的x-y函数,我们用时间作为x轴,用bot数量作为y轴。

我们要在编辑器中对难度曲线进行赋值,创建蓝图实例的过程如下


添加点的过程与蓝图中的TimeLine类似。

我们在控制世界中bot数量时,需要统计当前存在的bot数量。因为bot肯定有出生,被玩家攻击,死亡的过程,所以我们需要统计的是当前存活的bot数量,因此我们的bot也要像玩家控制的角色一样拥有血量等属性,也就是说AICharacter类需要有AttributeComponent。因此,类似对Character的操作,我们在AICharacter中加入AttributeComponent类的成员变量,并创建一个成员函数与属性组件变量中的OnHealthChanged委托绑定(绑定在PostInitializeComponents进行)。


当子弹与bot发生overlap时,子弹类检测到bot带有属性组件,所以触发bot的属性组件的ApplyHealthChange函数,在ApplyHealthChange函数中对OnHealthChanged委托进行广播,从而执行监听的OnHealthChanged成员变量,判断bot当前的血量,执行死亡相关操作(死亡相关操作后面讲)。

bot带有属性组件类的成员变量后,我们还可以修改bot的Mesh,实现类似玩家Character的受击发光效果,只要将之前创建的MaterialFunction与bot材料的EmissiveColor连接即可。

还可以实现在bot身上显示伤害数字的效果,在bot的蓝图中创建OnHealthChanged委托的执行流,其中创建我们之前做的显示伤害的UI即可。这里我们还实现了加血时显示不同的动画(其实只改了颜色)。
