斯坦福 UE4 C++ ActionRoguelike游戏实例教程 04.角色感知组件PawnSensingComponent和更平滑的转身

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

概述

本文章对应课程第十一章 43、44节。本文讲述PawnSensingComponent中的视觉感知的使用,以及对AI角色平滑转身进行一点小优化。

目录

  1. 添加PawnSensingComponent
  2. 平滑转身

添加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赋为我们的黑板键。

image-20230310114044092

记得修改TargetActorKey

点击PawnSensingComp,如图所示,从胶囊体放射出去的圆锥状线条是视野范围,外圈的圆环是听觉范围。从细节面板中可以看到感知组件的一系列参数,我们可以根据自己的需要进行修改。这里仅修改了视觉角度。注意到到视点比人物要高点儿,可以在自身(self)属性栏里修改基础眼高度,这里就不展示了。

image-20230310114142586

根据自身需要修改参数

修改完成,运行游戏,AI在看不见我们的时候,黑板键TargetActor是空指针,从上节课定义的蓝图可以知道,这里执行的是Cast Failed,不会选取任何目标。

image-20230310124513010

重温蓝图

因此不会进行任何实质性的寻路行为。别忘了我们自定义的SBTService_CheckAttackRange里也有相关逻辑,由于TargetActor为空指针,因此不会执行后面的逻辑。

PS:课程中这里在Failed时添加了一个默认值,默认返回玩家对象。出于笔者自认为的合理性,这里就不添加了。

image-20230310115205088

在看到主角之前是处于一个傻站着的状态

看到玩家后,TargetActor被赋值,输出Debug文字,AI开始自动寻路。

image-20230310115258179

看到玩家后输出Debug信息,开始攻击玩家

优化角色旋转

剩下一些小细节。在观察AI角色移动时,我们注意到AI角色在转向时是一帧转向,期间没有任何过渡,显得十分突兀。为了优化这一点,我们可以在MovementComponent组件里勾选使用控制器所需的旋转。该选项将使角色按照旋转速率平滑地旋转到目标角度。

image-20230310195529563

勾选选项

要想使上述选项生效,我们还需要取消勾选自身细节面板里的使用控制器旋转Yaw,这样AI控制器不再强制设置角色当前的Yaw,实现Movement组件完全控制角色的旋转。

image-20230310195640902

取消勾选

最后,课程还提到使用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

posted @ 2023-03-15 20:40  仇白  阅读(313)  评论(0编辑  收藏  举报