第四次引擎编程尝试
本节我们将尝试在游戏中创建AI,并针根据游戏内的事件做出对应的反应。
创建自己的AI类
· 按照介绍的,先创建一个我们的AI类,因为AI可以移动、碰撞等,所以我们选择其继承自Character
· 创建好AI类之后,我们创建其蓝图类
· 创建好蓝图我们进入AI的编辑界面
从界面中,我们可以看到:
我们的FPSTpl2AIGuard
继承自Character
,拥有Arrow Component
、Skeletal Mesh
、CharacterMovement
、CapsuleComponent
三个组件,其中:
①arrow component
,箭头组件,用于指示对象应当遵循的方向
②capsule component
,胶囊体组件,用于进行简单碰撞或者充当触发器
③CharacterMovementComponent
,角色移动组件,专用于Characters
,无法由其他类实现;允许非刚体类的角色移动、走、游泳等
④Skeletal Mesh
,骨骼网格体组件,用于表现人物、生物或者机械等具备复杂运动的对象
让我们的AI拥有听力和视力
源码实现
这一节,我们在AI周围行走,当AIGuard“看到”我们的时候,在场景内绘制出我们的位置;
(1)我们为FPSTpl2AIGuard
创建成员变量PawnSensingComponent
(2)当AIGuard出现看见事件时有事件抛出,注册相关事件UPawnSensingComponent::OnSeePawn.AddDynamic()
(3)在事件回调函数进行相关的处理逻辑AFPSTpl2AIGaurd::OnSeenCharacter(APawn* MyCharater)
源码逻辑如下:
//FPSTpl2AIGaurd.h class UPawnSensingComponent; UCLASS() class FPSTPL2_API AFPSTpl2AIGaurd : public ACharacter { ... ... public: UPROPERTY(VisibleAnywhere, BlueprintReadWrite) //UPROPERTY() UPawnSensingComponent* PawnSensingComponent; UFUNCTION() void OnSeenCharacter(APawn* MyCharater); }; //FPSTpl2AIGaurd.cpp AFPSTpl2AIGaurd::AFPSTpl2AIGaurd() { ... ... PawnSensingComponent = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensingComp")); PawnSensingComponent->OnSeePawn.AddDynamic(this, &AFPSTpl2AIGaurd::OnSeenCharacter); } void AFPSTpl2AIGaurd::OnSeenCharacter(APawn* MyCharater) { DrawDebugSphere(GetWorld(), MyCharater->GetTargetLocation(), 5, 0, FColor::Black, false, 10.0f); }
关于PawnSensingComponent
PawnSensingComponent
封装了Actor
的感官的设置和感应功能,使得actor
可以听到或者看到游戏内其他pawn
或者其发出的声响:
感官设置
如下图:
点击PawnSensingComponent
,可以看到右边的details界面,里面主要包括以下几个模块:
(1)AI模块
AI模块主要是用于设置一些感官参数,如:
Hearing Threshold:Actor的听力范围,以听到1分贝大小音量为准,且不考虑阻挡
LOSHearingThreshold:无遮挡的情况下,Actor能听到的最大距离,LOSHearingThreshold需要大于Hearing Threshold
Sight Radius:Actor能看到的最大距离
Sensing Interval:感知事件的刷新间隔,决定了AI感知的灵敏度
SeePawns和Hear Noises:感知功能开关,表示是否开启听力和视觉功能
Hearing Max Sound Age:能听到的最大持续时间的声音,若是声音持续时间过短,小于sensing interval
,我们会丢一部分信息
(2)Events模块
Events
模块主要用于绑定当指定事件发生时的处理接口
(3)Tick模块
component
的tick
间隔
感官事件处理
我们可以通过在蓝图里面绑定事件处理接口,当Actor
看到或者听到什么时,可以调用对应的处理逻辑。当然我们可以通过接口去做相关的事情,如下:
AFPSTpl2AIGaurd::AFPSTpl2AIGaurd() { PawnSensingComponent = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensingComp")); PawnSensingComponent->OnSeePawn.AddDynamic(this, &AFPSTpl2AIGaurd::OnSeenCharacter); PawnSensingComponent->OnHearNoise.AddDynamic(this, &AFPSTpl2AIGaurd::AIGuardOnHeardNoise); }
自定义接口AFPSTpl2AIGaurd::OnSeenCharacter
和AFPSTpl2AIGaurd::AIGuardOnHeardNoise
,通过PawnSensingComponent
的OnSeePawn
和OnHearNoise
进行绑定,需要注意的就是,接口的签名,这个可以其委托的定义
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FSeePawnDelegate, APawn*, Pawn );
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams( FHearNoiseDelegate, APawn*, Instigator, const FVector&, Location, float, Volume);
· 委托到FSeePawnDelegate
的接口,需要形参格式为APawn* Pawn
· 委托到FHearNoiseDelegate
的接口,形参格式为APawn* Instigator, const FVector& Location, float Volume
关于.build.cs
我们的PawnSensingComponent
是非引擎模块,而是AIModule
的,所以我们在编译之前需要将AIModule
添加到我们的编译列表中,如下:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "AIModule" });
运行结果
在上面的流程中,我们编译代码后运行,我们AIGuard看到玩家之后会在玩家出现的位置绘制一个黑色球,效果图如下:
AI的状态同步
添加枚举类型
UENUM(BlueprintType) enum class EnumAIGuardState : uint8 { Enum_AIState_Idle UMETA(DisplayName = "Idle"), //空闲状态 Enum_AIState_Search UMETA(DisplayName = "Search"), //搜索状态 Enum_AIState_Find UMETA(DisplayName = "Find"), //锁定状态 };
在UE中使用枚举我们有:
(1)可以仅定义枚举类型,和原C++的定义方式一致,此时使用范围仅限于UE的C++代码,且没有反射等特性
(2)还有一种就是枚举类,用UENUM
修饰,再下面enum class ENUMNAME
实际是定义了一个枚举类,在原有C++枚举的基础功能上,新增了反射功能,开发者可以实现根据下标获取枚举名,根据枚举名查对应下标等功能
(3)枚举类默认是不暴露给蓝图的,若是我们希望暴露给蓝图,我们需要加入修饰符BlueprintType
(4)我们还可以通过UMETA
设置枚举项的一些熟悉,如displayname
达到设置蓝图中展示的名字的目的
(5)enum class ENUMNAME : int8
,这是C++枚举类的使用机制,且uint8
指定了枚举类型的范围,设置存储类型为uint8
事件发生修改AI状态
class FPSTPL2_API AFPSTpl2AIGaurd : public ACharacter { public: UFUNCTION() void OnSeenCharacter(APawn* MyCharater); UFUNCTION() void TestAIGuardOnHeardNoise(APawn* MyPawn, const FVector& Location, float Volume); UFUNCTION() void SetAIGuardState(EnumAIGuardState NewAIGuardState); UFUNCTION(BlueprintImplementableEvent) void OnAIGuardStateChange(EnumAIGuardState CurrAIGuardState); UPROPERTY(VisibleAnywhere, BlueprintReadWrite) EnumAIGuardState AIGuardState; };
void AFPSTpl2AIGaurd::OnSeenCharacter(APawn* MyCharater) { DrawDebugSphere(GetWorld(), MyCharater->GetTargetLocation(), 5, 0, FColor::Black, false, 10.0f); SetAIGuardState(EnumAIGuardState::Enum_AIState_Find); } void AFPSTpl2AIGaurd::TestAIGuardOnHeardNoise(APawn* MyPawn, const FVector& Location, float Volume) { //DrawDebugSphere(GetWorld(), Instigator->GetTargetLocation(), 5, 0, FColor::Cyan, false, 10.0f); SetAIGuardState(EnumAIGuardState::Enum_AIState_Search); } void AFPSTpl2AIGaurd::SetAIGuardState(EnumAIGuardState NewAIGuardState) { if (AIGuardState == NewAIGuardState) { //return; } AIGuardState = NewAIGuardState; OnAIGuardStateChange(NewAIGuardState); }
这一步,我们希望当事件发生时可以修改AI的状态,并修改控件的状态:
· 这里,首先是在AIGuard里面定义AIGuardState
记录AI的状态,添加对应的接口SetAIGuardState
,并在对应位置调用
· 需要关注的是接口OnAIGuardStateChange
,注意到此接口的修饰符为BlueprintImplementableEvent
,表示此接口是蓝图实现的
UMG制作UI展示AI状态
在介绍OnAIGuardStateChange
之前,我们先编辑AI状态展示控件,之前已经介绍不在赘述,需要注意的是:
· 我们创建WP_GuardState,本质上是创建了一个蓝图类,界面编辑本质上是为其添加各种成员变量并设置属性,只不过这些成员变量是控件类型
· 除了编辑成员变量之外,我们还可以定义成员方法,如本例中的AIGuardStateTip
,此方法里面调用了SetText
方法,修改成员变量TextBlock_0
的值
AIGuard绑定UI
在前面,我们为AIGuard添加了AI的状态,并设计了展示AI状态的UI,在这里我们需要讲AI和AIGuard进行绑定,并且和C++代码的调用流程组合起来,需要做下面几步:
(1)为AIGuard的蓝图类BP_FPSTplAIGuard
增加组件Widgt
(2)编辑Widgt
使其绑定我们编辑的UI控件WP_AIGuardState
,在Widgt
里面,我们定义了供C++调用的接口OnAIGuardStateChange
:这里,我们获取Widgt
的实例,转为WP_AIGuardState
类型,调用接口AIGuardStateTip
(3)在蓝图流程编辑框里面target
可以理解调用此方法的主体,即C++的this
指针
(4)需要注意就是一个pure
属性的问题,pure function
是纯执行流,默认不会改变状态,若是没有返回值的话,则实际在蓝图中是会被优化掉,不会执行
AI的定时器
启动程序我们发现,当AI状态转变之后,一直不能恢复;这不是我们希望看见的,我们希望当AI状态停留若干秒之后,若无事件触发,则把AI状态重置,这里我们引入定时器,代码如下:
UCLASS() class FPSTPL2_API AFPSTpl2AIGaurd : public ACharacter { ... ... FTimerHandle AIGuardTimerHandle; }; void AFPSTpl2AIGaurd::OnSeenCharacter(APawn* MyCharater) { GetWorld()->GetTimerManager().ClearTimer(AIGuardTimerHandle); GetWorld()->GetTimerManager().SetTimer(AIGuardTimerHandle,this,&AFPSTpl2AIGaurd::ResetAIGuardState, 1.0f,false,5.0f); } void AFPSTpl2AIGaurd::ResetAIGuardState() { AIGuardState = EnumAIGuardState::Enum_AIState_Idle; OnAIGuardStateChange(AIGuardState); }
· UE提供定时器结构体FTimerHandle
· 我们通过接口GetWorld()->GetTimerManager().ClearTimer
清除定时器&GetWorld()->GetTimerManager().SetTimer
设置定时器
· 需要注意的是,在SetTimer
的时候,若是设置InRate
参数为0,则表示清除已有定时器,会把当前定时器也清掉(不再执行)
遇到的问题及解决
PawnSensingComponent创建后detail为空
如下图所示:
我们无法对组件的相关参数进行编辑,经排查是我们的
UPROPERTY
设置有问题,改成VisiableAnyWhere就好了
,如下代码段所示
class UPawnSensingComponent; UCLASS() class FPSTPL2_API AFPSTpl2AIGaurd : public ACharacter { GENERATED_BODY() public: // Sets default values for this character's properties AFPSTpl2AIGaurd(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; public: // Called every frame virtual void Tick(float DeltaTime) override; UPROPERTY(VisibleAnywhere, BlueprintReadWrite) UPawnSensingComponent* PawnSensingComponent; UFUNCTION() void OnSeenCharacter(APawn* MyCharater); };
修改后,界面状态如下:
这是因为Instigator
已经是父类的成员变量,所以在形参列表应该重新定义,让编译器知道最后用哪个,修改如下:
void AFPSTpl2AIGaurd::TestAIGuardOnHeardNoise(APawn* MyPawn, const FVector& Location, float Volume) { ... ... }
断点调试及接口不执行
我们在实际编码的过程中,遇到接口不执行的情况,这是因为我们在定义AIGuardStateTip的时候设置为pure
表示我们告诉UE编译系统此接口为纯执行流
此时在调用流里面又没有接收返回值,则就被编译系统优化不执行了