斯坦福 UE4 C++ ActionRoguelike游戏实例教程 04.角色感知组件PawnSensingComponent和更平滑的转身
斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论
概述
本文章对应课程第十一章 43、44节。本文讲述PawnSensingComponent中的视觉感知的使用,以及对AI角色平滑转身进行一点小优化。
目录
- 添加PawnSensingComponent
- 平滑转身
添加PawnSensingComponent
修改代码
在之前的课程中,若要让AI选取目标,都是预先在程序里获取目标的对象。要做到更真实的AI,我们想让AI在“看到”玩家后才获取玩家控制的角色对象,甚至可以自由转换目标。本小节的主要内容就是实现这个目标。
课程提到,要想让AI角色拥有感知世界的能力,通常有两种做法,一种是使用AI感知系统,还有一种就是本文要讲述的PawnSensingComponent组件。相较于前者,后者显得十分原始,但是优点在于易于理解和拓展性强。本节课实现的功能并不复杂,使用PawnSensingComponent组件完全可以胜任本节课的任务。
PawnSensingComponent顾名思义,可以让拥有该组件的Actor获得感知Pawn的能力。常用的有视觉感知和听觉感知,本节重点使用视觉感知。具体的做法,我们边做边说。
阅读源码可以发现,视觉感知使用的是射线检测获取的目标对象,感兴趣的读者也可以自己试着实现一下。
和添加其他组件一样,PawnSensingComponent需要引入头文件#include "Perception/PawnSensingComponent.h",并在构造函数里进行创建。
//.h
UPROPERTY(VisibleAnywhere, Category = "AI")
UPawnSensingComponent* PawnSensingComp;
//.cpp
PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");
翻阅UPawnSensingComponent源码,我们注意到PawnSensingComponent定义了一个FSeePawnDelegate OnSeePawn
委托。熟悉C#或JAVA的读者可能会知道,委托实际上就是绑定了一系列的回调函数,在需要的时候可以一起调用。当AI角色看到Pawn类型的对象后,就会调用OnSeePawn
里绑定的函数。我们所需要做的,就是按照委托定义的函数签名,将我们自定义的函数绑定到委托里。这里我们依葫芦画瓢,创建一个void OnPawnSeen(APawn* Pawn)
函数。
//.h
UPROPERTY(EditDefaultsOnly, Category = "AI")
FName TargetActorKey;
UFUNCTION()
void OnPawnSeen(APawn* Pawn);
//.cpp
void ASurAiCharacter::OnPawnSeen(APawn* Pawn)
{
AAIController* AIC = Cast<AAIController>(GetController());
if(AIC)
{
UBlackboardComponent* BBComp = AIC->GetBlackboardComponent();
BBComp->SetValueAsObject(TargetActorKey, Pawn);
DrawDebugString(GetWorld(), GetActorLocation(), "PLAYER SPOTTED", nullptr, FColor::White);
}
}
文章末我会放出完整代码。
OnPawnSeen的逻辑也很简单,参数Pawn指的是看到的Pawn类型对象,当调用该函数时,看到的Pawn类型对象会作为参数传进来,我们将其存储到黑板里,之后行为树就可以根据黑板里的Pawn对象来执行判断距离等一系列逻辑了。
与课程中不一样的是,我将TargetActorKey暴露给蓝图,这样我们就可以在UE编辑器里修改要绑定的黑板键了。
然后别忘了将自定义的函数绑定到委托里。
void ASurAiCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
PawnSensingComp->OnSeePawn.AddDynamic(this, &ASurAiCharacter::OnPawnSeen);
}
还有一件事,我们不希望之前的代码影响到这次的实验。把之前在代码里获取玩家对象的相关代码删掉或者注释掉。
//SurAiController.cpp
void ASurAIController::BeginPlay()
{
Super::BeginPlay();
if(ensure(BehaviorTree))
{
RunBehaviorTree(BehaviorTree);
}
//GetPlayerPawn可以是这个关卡的任意对象,这里传入this就行
// APawn* MyPawn = UGameplayStatics::GetPlayerPawn(this, 0);
// if(MyPawn)
// {
// GetBlackboardComponent()->SetValueAsVector("MoveToLocation", MyPawn->GetActorLocation());
// GetBlackboardComponent()->SetValueAsObject("TargetActor", MyPawn);
// }
}
修改蓝图
编译代码,进入蓝图。首先把我们暴露在蓝图的TargetActorKey赋为我们的黑板键。
点击PawnSensingComp,如图所示,从胶囊体放射出去的圆锥状线条是视野范围,外圈的圆环是听觉范围。从细节面板中可以看到感知组件的一系列参数,我们可以根据自己的需要进行修改。这里仅修改了视觉角度。注意到到视点比人物要高点儿,可以在自身(self)属性栏里修改基础眼高度,这里就不展示了。
修改完成,运行游戏,AI在看不见我们的时候,黑板键TargetActor是空指针,从上节课定义的蓝图可以知道,这里执行的是Cast Failed,不会选取任何目标。
因此不会进行任何实质性的寻路行为。别忘了我们自定义的SBTService_CheckAttackRange
里也有相关逻辑,由于TargetActor为空指针,因此不会执行后面的逻辑。
PS:课程中这里在Failed时添加了一个默认值,默认返回玩家对象。出于笔者自认为的合理性,这里就不添加了。
看到玩家后,TargetActor被赋值,输出Debug文字,AI开始自动寻路。
优化角色旋转
剩下一些小细节。在观察AI角色移动时,我们注意到AI角色在转向时是一帧转向,期间没有任何过渡,显得十分突兀。为了优化这一点,我们可以在MovementComponent组件里勾选使用控制器所需的旋转
。该选项将使角色按照旋转速率平滑地旋转到目标角度。
要想使上述选项生效,我们还需要取消勾选自身细节面板里的使用控制器旋转Yaw
,这样AI控制器不再强制设置角色当前的Yaw,实现Movement组件完全控制角色的旋转。
最后,课程还提到使用ensureMsgf宏来生成更详细的错误日志,这对于若干年后的debug可以起到提高效率的作用。读者可以自行了解使用
完整代码
//SurAiCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SurAiCharacter.generated.h"
class UPawnSensingComponent;
UCLASS()
class FPSPROJECT_API ASurAiCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASurAiCharacter();
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, Category = "AI")
UPawnSensingComponent* PawnSensingComp;
UPROPERTY(EditDefaultsOnly, Category = "AI")
FName TargetActorKey;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UFUNCTION()
void OnPawnSeen(APawn* Pawn);
void PostInitializeComponents() override;
};
//SurAIController.cpp
#include "Ai/SurAiCharacter.h"
#include "AIController.h"
#include "DrawDebugHelpers.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/PawnSensingComponent.h"
// Sets default values
ASurAiCharacter::ASurAiCharacter()
{
PrimaryActorTick.bCanEverTick = true;
PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");
}
// Called when the game starts or when spawned
void ASurAiCharacter::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void ASurAiCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void ASurAiCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
void ASurAiCharacter::OnPawnSeen(APawn* Pawn)
{
AAIController* AIC = Cast<AAIController>(GetController());
if(AIC)
{
UBlackboardComponent* BBComp = AIC->GetBlackboardComponent();
BBComp->SetValueAsObject(TargetActorKey, Pawn);
DrawDebugString(GetWorld(), GetActorLocation(), "PLAYER SPOTTED", nullptr, FColor::White);
}
}
void ASurAiCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
PawnSensingComp->OnSeePawn.AddDynamic(this, &ASurAiCharacter::OnPawnSeen);
}
//SurAIController.cpp 这里是将不需要的代码删除掉,这里以注释作为标识
#include "Ai/SurAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
void ASurAIController::BeginPlay()
{
Super::BeginPlay();
if(ensure(BehaviorTree))
{
RunBehaviorTree(BehaviorTree);
}
//GetPlayerPawn可以是这个关卡的任意对象,这里传入this就行
// APawn* MyPawn = UGameplayStatics::GetPlayerPawn(this, 0);
// if(MyPawn)
// {
// GetBlackboardComponent()->SetValueAsVector("MoveToLocation", MyPawn->GetActorLocation());
// GetBlackboardComponent()->SetValueAsObject("TargetActor", MyPawn);
// }
}
参考链接
基于C++代码的UE4学习(三十九)——为AI增加感官(视觉与听觉) https://blog.csdn.net/weixin_43654485/article/details/108152408