斯坦福 UE4 C++ ActionRoguelike游戏实例教程 02.AI自定义任务和观察器中断

斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论

概述

本文章对应课程第十一章 42节。这篇文章会进一步地为AI添加新功能,创建自定义任务,允许AI发射子弹,并且讲解观察器中断的用法。

最终效果:

目录

  1. 创建自定义任务节点
  2. 观察器中止是什么
  3. 优化AI

创建自定义任务节点 BTTask

这节课我们要为AI小兵添加进入攻击范围后进行射击的功能。正如上节课看到的那样,行为树支持各种各样的任务,包括朝目标前进、Wait等我们已经使用过的UE自带Task。这节课的目标是自定义Task节点,实现开火的功能。具体的细节,我们边做边说。

让我们像以前做的一样,右键编辑器,勾选搜索全部,找到BTTasksNode类,将其作为父类。

image-20230306155020469

创建BTTaskNode的子类

接着修改代码如下,对于BTTaskNode的子类,我们目前只需要重写ExecuteTask函数即可。返回值为EBTNodeResult::Failed和EBTNodeResult::Successd,对应行为树的节点返回失败或者成功。

逻辑也很简单,就是获取对象等一系列API操作,作为初学者熟悉使用即可。

除此以外,我们还添加了一个自定义子弹类型的对象,这些操作在大家学习编写人物的时候已经相当熟悉了。

//SBTTask_RangeAttack.h
UCLASS()
class FPSPROJECT_API USBTTask_RangeAttack : public UBTTaskNode
{
   GENERATED_BODY()
protected:
   virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
   
   UPROPERTY(EditAnywhere, Category = "AI")
   TSubclassOf<AActor> ProjectileClass;
};


//SBTTask_RangeAttack.cpp
EBTNodeResult::Type USBTTask_RangeAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	AAIController* MyController = OwnerComp.GetAIOwner();
	if(ensure(MyController))
	{
		ACharacter* MyPawn = Cast<ACharacter>(MyController->GetPawn());
		if(MyPawn == nullptr)
		{
			return EBTNodeResult::Failed;
		}

		AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor"));
		if(TargetActor == nullptr)
		{
			return EBTNodeResult::Failed;
		}

		FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");
		//方向向量=目标位置-当前位置
		FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
		FRotator MuzzleRotation = Direction.Rotation();

		FActorSpawnParameters params;
		params.Instigator = MyPawn;
		params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

		ensure(ProjectileClass);
		AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass, MuzzleLocation, MuzzleRotation);
		return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
	}

	return EBTNodeResult::Failed;
}

接下来验证我们的代码。修改行为树如下,相对于上节课添加了左边两个节点,并添加了一个装饰器,用于检测是否已进入攻击范围。

图中的RangeAttack则是我们刚才编写的自定义任务,添加方式和上节课的MoveTo一样,搜索即可找到。

image-20230306170830964

修改行为树,别忘了设置子弹的类型

PS:笔者在实验的时候,经常贪图方便使用热重载。这次出现了子弹类型没有暴露在蓝图中的问题。关闭UE重新编译才顺利出现,果然不能信任热重载。

被炸烂惹

上图是运行游戏的结果。可以看到,当敌对小兵进入设定的攻击范围后,执行了后面的Wait节点,等待一段时间后,重新回到最左边,执行了RangeAttack任务,由于Selector节点的特性,反复执行了最左边的节点,连续发射了大量子弹进行了狂轰滥炸。到目前为止,我们的初期目标已经达成了。

因为真正的游戏中不会出现这么蠢的AI,我们还需要对这个AI进行一些优化,例如发射子弹有冷却时间,能够三连发,能够在进入范围后立刻开火等功能。

观察器中断的概念

我们首先要实现的是进入攻击范围后能立刻开火。在原来的设计中,当AI跑入攻击范围后,会先执行行为树最右边Wait任务,等待上一段时间。现在我们希望马上执行RangeAttack任务,除了调整节点的顺序以外,行为树还给我们提供了一个很有用的功能,就是观察器中止(Observer Aborts )。由于上节课没有说清楚,我这里详细的说一下。

点击装饰器,可以看到细节面板有观察者中止的选项。

image-20230306172711451

观察器中止

官方文档的解释十分的晦涩难懂,这里我仅以实用的角度简单描述一下观察器中止的用法,实际效果还需要读者自行进行测试。

在行为树中,观察器中断(Observer Aborts)是我们控制行为树任务(BehaviorTreeTask,BTT)执行的最重要的设置之一,其能在满足行为树装饰器(BehaviorTreeDecorator,BTD)临界条件后有条件地中断被观察者中断标记为中断范围的所有节点的执行。

说人话就是,如果你设置了观察器,当装饰器观测到自己负责的这个黑板键值变化时, 这个装饰器就有权利中断后面正在运行的节点,转而从装饰器这个节点开始运行。

其中下拉框有四个选项,其作用分别是:

  • None:不终止执行。
  • Self:终止自己及节点以下的所有子树。
  • Lower Priorit:终止此节点右方(具体的说是拥有最深共同父级)的所有节点。
  • Both:终止自己及节点以下的所有子树及右方所有节点。

如下图所示,当Within Attack Range节点设置了Lower Priorit时,右边的的节点全部变成了蓝色,当Within Attack Range的条件达成后,不管蓝色的节点在运行什么任务,都会被直接中断,转而从这个节点开始运行。

image-20230306173539640

设置为Lower Priorit

当设置为self时,发现只有子树节点变成了绿色。当观测到条件变化时,会中断子树的任务,从装饰器的节点开始运行。

image-20230306173905140

设置为self

both则是两种设置的集合,不再赘述。

image-20230306174226293

设置为Both

另外,通知观察者有两种方式, 用于控制行为树如何通知观察者中断:

一种是结果改变时(On Result Change):对应装饰器的Bool结果发生变化时通知中断

一种是值改变时(On Value Change):对应装饰器的值(一般是行为树对应的黑板中此装饰器使用的黑板键的值)变化时通知中断。

由于我们这里使用的都是Bool,所以都默认On Result Change即可。

image-20230306194747710

通知观察者

优化AI

子弹发射冷却和循环

回到正题来,大家可以慢慢咀嚼一下刚才讲的东西,我们先把子弹发射冷却和循环给做出来。
将行为树修改成这样子。相比于之前,添加了两个装饰器节点和一个Sequence节点。斯坦福课程里使用的是Selector节点,而这里使用的是Sequence节点。因为只有一个子节点,在这里两者是没有区别的,请读者充分理解两者的区别后再提出疑问。

image-20230306181005200

CoolDown和Loop都是装饰器节点

顾名思义,Cooldown节点会控制节点执行的间隔,当节点处于冷却时间时,会直接返回false。Loop装饰器则会循环执行下面的节点,具体情况读者可以自行实验。

到现在为止,这个AI的行为模式为:如果目标在攻击范围外,会向目标移动。进入攻击范围后,会等待一段时间(这里设置为1s)。然后开枪向角色以0.2s为间隔时间射击三次,冷却2s。在此期间,行为树会运行后面两个节点,直到wait结束后重新判断是否结束冷却。

设置优先级

当这个wait设置的时间很长(例如5s)时,有时即使冷却时间结束了他也不会立即攻击,会傻傻地等待wait结束。这时候就需要用到之前提到的观察器中断。

将Within Attack Range的观察器中断设置为Both,Cooldown的观察器中断设置为Lower Priorit,根据之前的理论,当角色进入攻击范围时或者冷却时间结束都会立刻执行当节点。由于Within Attack Range设置为Both,当角色离开攻击范围时也会立刻停止执行后面的节点并返回false。

另外,和上节课一样,将Out of Attack Range节点设置为Both,这样角色在进入攻击范围时就会立刻停下来了。这部分可以实验的东西相当多,请读者自行实验。

最后的效果如下:

这里用走位很细节的在AI射出两发子弹的时候离开了他的攻击范围,这时候他会立即停止射击,执行中间的节点。

设置攻击的时候面朝玩家

这个实际上就是一个小细节。要实现这个功能,我们通常会想到可以在执行攻击动作的时候使用SetActorRotation函数修改AI小兵的朝向,实际上行为树为我们自带了这样一个服务(BTService)。这给我们带来了思路,我们可以使用BTService,在节点激活的情况下定时调整角色的状态,当然这里我们直接使用即可。

image-20230306195205220

设置朝向,你也可以将它放在selector节点

参考链接

https://www.bilibili.com/read/cv13400311

https://www.cnblogs.com/timy/p/9048267.html

posted @ 2023-03-09 17:52  仇白  阅读(243)  评论(0编辑  收藏  举报