第四次引擎编程尝试

ronald1年前职场5120

本节我们将尝试在游戏中创建AI,并针根据游戏内的事件做出对应的反应。

创建自己的AI类

· 按照介绍的,先创建一个我们的AI类,因为AI可以移动、碰撞等,所以我们选择其继承自Character

image-20220221145525472.png

· 创建好AI类之后,我们创建其蓝图类

image-20220221150158783.png

· 创建好蓝图我们进入AI的编辑界面

image-20220221155850140.png

从界面中,我们可以看到:

    我们的FPSTpl2AIGuard继承自Character,拥有Arrow ComponentSkeletal MeshCharacterMovementCapsuleComponent三个组件,其中:

        ①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或者其发出的声响:

感官设置

    如下图:

image-20220222111847214.png

点击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模块

componenttick间隔

感官事件处理

    我们可以通过在蓝图里面绑定事件处理接口,当Actor看到或者听到什么时,可以调用对应的处理逻辑。当然我们可以通过接口去做相关的事情,如下:

AFPSTpl2AIGaurd::AFPSTpl2AIGaurd()
{
	PawnSensingComponent = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensingComp"));
	PawnSensingComponent->OnSeePawn.AddDynamic(this, &AFPSTpl2AIGaurd::OnSeenCharacter);
	PawnSensingComponent->OnHearNoise.AddDynamic(this, &AFPSTpl2AIGaurd::AIGuardOnHeardNoise);
}

自定义接口AFPSTpl2AIGaurd::OnSeenCharacterAFPSTpl2AIGaurd::AIGuardOnHeardNoise,通过PawnSensingComponentOnSeePawnOnHearNoise进行绑定,需要注意的就是,接口的签名,这个可以其委托的定义

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看到玩家之后会在玩家出现的位置绘制一个黑色球,效果图如下:

image-20220222141342575.png

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状态

image-20220222202159064.png

image-20220224095709538.png

在介绍OnAIGuardStateChange之前,我们先编辑AI状态展示控件,之前已经介绍不在赘述,需要注意的是:

· 我们创建WP_GuardState,本质上是创建了一个蓝图类,界面编辑本质上是为其添加各种成员变量并设置属性,只不过这些成员变量是控件类型

· 除了编辑成员变量之外,我们还可以定义成员方法,如本例中的AIGuardStateTip,此方法里面调用了SetText方法,修改成员变量TextBlock_0的值

AIGuard绑定UI

image-20220224101043178.png

在前面,我们为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为空

    如下图所示:

image-20220221201414187.png    我们无法对组件的相关参数进行编辑,经排查是我们的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);
};

    修改后,界面状态如下:

image-20220221201757388.png

定义声音事件的时候编译错误

image-20220222130432074.png

    这是因为Instigator已经是父类的成员变量,所以在形参列表应该重新定义,让编译器知道最后用哪个,修改如下:

void AFPSTpl2AIGaurd::TestAIGuardOnHeardNoise(APawn* MyPawn, const FVector& Location, float Volume)
{
	... ...
}

断点调试及接口不执行

    我们在实际编码的过程中,遇到接口不执行的情况,这是因为我们在定义AIGuardStateTip的时候设置为pure表示我们告诉UE编译系统此接口为纯执行流

image-20220224114756543.png

    此时在调用流里面又没有接收返回值,则就被编译系统优化不执行了

image-20220224115107749.png

参考资料

    UPawnSensingComponent | Unreal Engine Documentation


相关文章

    栈(stack)一种操作受限的线性结构,限定仅能在尾部进行插入和删除,能进行插入和删除操作的一端叫做栈顶,而另一端叫做栈底;我们将插入栈的操作叫做入栈,从栈中删除元素的操作叫做出栈,栈是一个先入后出(FILO)的数据结构,即先入栈的元素会后出栈。    栈最常被我们接触的场景就是函数调用了,在进程地址空间中,有...

Lua的Upvalue和闭包(二)

Lua的Upvalue和闭包(二)

Lua闭包和Upvalue的实现    前面文章介绍了Lua闭包和upvalue的概念,本文简单过一下Lua对于闭包和upvalue的实现以加深理解。Lua闭包结构    Lua在内存的结构如下所示:#define ClosureHeader \ CommonHeader; lu_by...

几种Lua和C交叉编程的程序写法

Lua程序调用C接口//另一个待Lua调用的C注册函数。 static int sub2(lua_State* L) {     double op1 = luaL_checknumber(L,1);     double op2 ...

第三次引擎编程尝试

第三次引擎编程尝试

    本文用来介绍如何实现同步游戏状态给玩家,这里用到了前面介绍的HUD。基本概念HUD    游戏过程中,游戏系统和玩家交互是非常有必要的,UE4提供了HUD来实现将游戏系统状态同步给玩家的方式,也就是说HUD对于玩家来说是只读的;UI    不同于HUD是将游戏系统状态同步...

K8S背景

    在深入了解K8S之前,我们先了解一下K8S产生的背景,看下是为了解决怎样的问题一步步衍生出K8S这样一套系统。一. 微服务化    随着需求的发展,单体应用的复杂度越来越高,大大增加了系统现网的运维成本,主要包括以下几个方面。    1. 模块耦合度提升,维护成本高&nb...

第五次引擎编程尝试

第五次引擎编程尝试

在前面的章节中,我们已经尝试了制作特效、为游戏添加AI并设置AI的UI,到现在这一步我们希望游戏可以多人联网,我们一步步来。开启多人游戏通过这个我们可以看到角色移动在CS之间同步,但是真正运行的时候发现,距离真正的多人联网游戏还很遥远:1)射击的时候CS端的弹道不同步2)NPC的状态CS端不同步3)游戏状态CS端不同步... ...针对上述问题,我们逐步来看:让发射物在CS之间同步原先的实现流程如...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。