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

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

时间:2023-03-15 20:44:06浏览次数:50  
标签:PawnSensingComponent 04 Pawn AI void C++ ASurAiCharacter include

斯坦福课程 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

标签:PawnSensingComponent,04,Pawn,AI,void,C++,ASurAiCharacter,include
From: https://www.cnblogs.com/Qiu-Bai/p/17219944.html

相关文章

  • 004-OpenFOAM的场
    004-OpenFOAM的场 场操作,讲到继承的Foam::vector和对应的mag方法/*---------------------------------------------------------------------------*\=========......
  • C++学习记录
    C++recordnotebook基础导论C++特性具有c访问硬件的能力和面向对象程序的属性,以及更具有泛型编程的功能(使用模板进行编程)。OOP(面向对象编程)其中的方法有:自顶向下和......
  • day1 | 704. 二分查找、27.移除元素
     704.二分查找题目简述一个有序数组中找到目标值,返回其位置 思路确定左右指针,一个指向最左边,一个指向最右边  1.闭区间确定中间值,如果中间值大于目标值,......
  • C++ 构造函数和析构函数
    构造函数和析构函数目录页面问题构造函数与析构函数初始化列表转换构造拷贝构造(这种都是浅拷贝,每一项成员依次拷贝过去)默认的赋值运算符小的总结页面构造/......
  • C++ 常用语法
    1.定义一个字符串常量staticconststd::stringversion("0.0.1");staticconststd::stringname("Car-"+version);2.定义size大小staticconstexpruint64_tsh......
  • C++风格 字符串操作
    获取字符串长度              str.size();或者str.length();连接字符串                     str=str+"world";删除字符串......
  • [计算机基础笔记] C/C++
    C语言面向过程,C++面向对象。面相过程的思维方式,它更加注重这个事情的每一个步骤以及顺序。他比较直接高效,需要做什么可以直接开始干。程序=算法+数据面向对象的思维方式......
  • C++学习笔记3
    18.虚析构问题提出:在继承关系中构造和析构什么时候被调用?假如当前有类CSon继承CFather构造:当newCSon的时候,就会调用CSon(),程序跳进CSon(),在CSon()里会先调用CFather(......
  • C++学习笔记4
    C++=C+面向对象+泛型编程+STL26.STL容器STL(标准模板库),它其中包含了:容器、迭代器、算法、空间配置器、配接器、仿函数六个部分,这里介绍一些容器以及几个简单算法......
  • 一些不常遇到的C++知识总结
    explicit防止隐式转换C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。C++中,一个......