跳过正文

CoopGame06-AI基础

··3931 字·8 分钟
coopgame - 这篇文章属于一个选集。
§ 6: 本文

CoopGame06-AI基础
#

type: Post status: Published date: 2022/10/10 slug: CoopGame06 summary: UE4 C++多人游戏入门 category: Unreal

跟随B站up主“技术宅阿棍儿”的教程制作的笔记。教程链接

#AI导航

资源准备:

导入最新工程的TrackerBot文件夹。工程地址

###1.创建继承Pawn类的AI角色球STracerBot的C++类,。

STracerBot.h

UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Components")
class UStaticMeshComponent* StaticMeshComp;

STracerBot.cpp

ASTracerBot::ASTracerBot()
{
//...
	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComp"));
	RootComponent = StaticMeshComp;

        StaticMeshComp->SetCanEverAffectNavigation(false);//自身不影响导航
//...
}

###2.创建继承STracerBotC++类的BP_TracerBot蓝图类。 ###3.给蓝图类设置静态网格体:复制默认的球体网格体到新建文件夹Meshes中,重命名为SM_TracerBot,打开静态网格体设置其编译设置,构建比例为0.2。 ###4.创建材质M_TracerBot,先给个白色基础颜色,并设置网格体SM_TracerBot的材质为这个。 ###5.放大场地,摆放一些简单的阻挡物,做一个简单的场景,拖入BP_TracerBot. ###6.在放置Actor窗口,选择体积,选择导航网格体边界体积拖入场景内,并调节大小覆盖场景,按p可显示导航面积。 ###7.Build.cs文件中添加导航系统

项目名.Build.cs

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" ,"NavigationSystem"});

###8.获取导航路径的下一个点

STracerBot.h

FVector GetNextPathPoint();

STracerBot.cpp

FVector ASTracerBot::GetNextPathPoint()
{
	//拿到0号玩家对象
	APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(this, 0);
	//立即找到下一个路径(上下文,路径开始点,目标Actor)
	UNavigationPath* NavPath = UNavigationSystemV1::FindPathToActorSynchronously(this,GetActorLocation(),PlayerPawn);
	//如果路径点数量大于1返回下一个位置点
	if (NavPath->PathPoints.Num()>1)
	{
		return NavPath->PathPoints[1];
	}
	//否则返回初始位置
	return GetActorLocation();
}

###9.用作用力移动AI角色球

STracerBot.h

    //添加力的大小
	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	float MovementForce;
	//是否使用改变速度
	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	bool bUseVelocityChange;
	//距离目标多少时判定为到达
	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	float RequiredDistanceToTarget;
	//下一个点的变量
	UPROPERTY()
	FVector NextPathPoint;

STracerBot.cpp

ASTracerBot::ASTracerBot()
{
//...
	//设置AI球网格体启用物理模拟
	StaticMeshComp->SetSimulatePhysics(true);
	//启用力改变速度
	bUseVelocityChange = true;
	//作用力大小
	MovementForce = 200;
	//判定到达目标的距离
	RequiredDistanceToTarget = 100;
//...
}

void ASTracerBot::BeginPlay()
{
	Super::BeginPlay();
	//获取到下一个导航点并赋值
	NextPathPoint = GetNextPathPoint();
}

void ASTracerBot::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	//获得两点向量差的大小,既是距离下一个点的距离
	float DistanceToTarget = (GetActorLocation() - NextPathPoint).Size();

	//距离下一个点的距离小于阈值100则继续获取下一个点,如果还没到则推进。
	if (DistanceToTarget <= RequiredDistanceToTarget)
	{
		NextPathPoint = GetNextPathPoint();
		DrawDebugSphere(GetWorld(), NextPathPoint, 20, 12, FColor::Yellow, 0.0f, 0, 1.0f);
	}
	else
	{
		//获得从AI球指向下一个点的向量。
		FVector ForceDirection = NextPathPoint - GetActorLocation();
		//获取方向,不受大小的影响。
		ForceDirection.Normalize();
		//方向向量 * 力 = 有方向的力,用来推动小球。
		ForceDirection *= MovementForce;
		//添加推力使小球朝目标滚动。同时改变小球的速度。
		StaticMeshComp->AddForce(ForceDirection, NAME_None, bUseVelocityChange);
        //画出AI球的运动方向
		DrawDebugDirectionalArrow(GetWorld(), GetActorLocation(), GetActorLocation() + ForceDirection, 32, FColor::Red,
								  false, 0.0f,
								  0, 1.0f);
	}
    ////画出下一个目标点位置
    DrawDebugSphere(GetWorld(), NextPathPoint, 20, 12, FColor::Yellow, false, 0.0f, 1.0f);
}

#与玩家互动 ###1.添加造成伤害支持

STracerBot.h

1
2
3
4
5
6
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Conmponents")
class USHealthComponent* HealthComp;

//生命值委托函数?
UFUNCTION()
void HandleTakeDamage(class USHealthComponent* OwningHealthComp,float Health,float HealthDelta,const class UDamageType* DamageType, class AController*InstigatedBy, AActor* DamageCauser);

STracerBot.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ASTracerBot::ASTracerBot()
{
//...
	HealthComp = CreateDefaultSubobject<USHealthComponent>(TEXT("HealthComp"));
	HealthComp->OnHealthChanged.AddDynamic(this,&ASTracerBot::HandleTakeDamage);
//...
}

void ASTracerBot::HandleTakeDamage(USHealthComponent* OwningHealthComp, float Health, float HealthDelta,
	const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
	UE_LOG(LogTemp,Log,TEXT("Health %s of %s"),*FString::SanitizeFloat(Health),*GetName);
}

###2.受伤闪烁,通过控制材质参数来改变材质。

M_TracerBot材质文件修改。

Untitled.webp

STracerBot.h https://s3.zmingu.com/imagesic* MatInstance;


> STracerBot.cpp
> 

```c
void ASTracerBot::HandleTakeDamage(USHealthComponent* OwningHealthComp, float Health, float HealthDelta,
	const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
	if (MatInstance ==nullptr)
	{
                //拿到材质实例
		MatInstance = StaticMeshComp->CreateAndSetMaterialInstanceDynamicFromMaterial(0,StaticMeshComp->GetMaterial(0));
	}
	if (MatInstance)
	{
                //受伤时,通过改变材质中的参数,这时这个参数几乎等于输入Timer,所以会亮一下。
		MatInstance->SetScalarParameterValue("LastTimeDamageTaken",GetWorld()->TimeSeconds);
	}

	UE_LOG(LogTemp,Log,TEXT("Health %s of %s"),*FString::SanitizeFloat(Health),*GetName());
}

###3.AI球生命值为0时解体爆炸,记得给BP_TracerBot蓝图类的类默认值指定爆炸特效。

STracerBot.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	void SelfDestruct();//自毁函数,需要生成爆炸特效,造成范围伤害。

	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	class UParticleSystem* ExplosionEffect;//爆炸特效

	bool bExploded;//是否爆炸

	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	float ExplosionRadius;//爆炸范围

	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	float ExplosionDamage;//爆炸伤害

STracerBot.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ASTracerBot::ASTracerBot()
{
//...
	ExplosionRadius = 200;
	ExplosionDamage = 100;
//...
}

void ASTracerBot::HandleTakeDamage(USHealthComponent* OwningHealthComp, float Health, float HealthDelta,
	const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
//...
	if (Health <= 0)
	{
		SelfDestruct();
	}
//...
}

void ASTracerBot::SelfDestruct()
{
	//如果爆炸过直接返回
	if (bExploded) return;
	//设置爆炸
	bExploded = true;
	//播放爆炸特效
	UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionEffect, GetActorLocation());
	//添加忽略的Actor,自身
	TArray<AActor*> IgnoredActors;
	IgnoredActors.Add(this);
	//造成范围伤害ApplyRadialDamage(上下文,原点的基础伤害,原点位置,伤害半径,伤害类型类,要忽略的Actor列表,造成伤害的人,负责造成伤害的控制器,伤害是否根据原点缩放)
	UGameplayStatics::ApplyRadialDamage(this, ExplosionDamage, GetActorLocation(), ExplosionRadius, nullptr,
	                                    IgnoredActors, this, GetInstigatorController(), true);
	DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 12, FColor::Green, false, 2.0f, 0, 1.0f);
	//设置这个Actor的寿命。当寿命结束就会销毁。
	SetLifeSpan(2.0f);
}

###4.靠近玩家时倒计时自爆。

STracerBot.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    //球形组件,用来判断和玩家重叠
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
	class USphereComponent* SphereComp;
	//时间句柄
	FTimerHandle TimerHandle_SelfDamage;
	//伤害自己的函数
	void DamageSelf();
	//是否开始伤害自己
	bool bStartSelfDestruction;
	//重叠事件
	virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;

STracerBot.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ASTracerBot::ASTracerBot()
{
//...
	SphereComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
	//设置半径
	SphereComp->SetSphereRadius(200);
	//设置碰撞类型为只查询
	SphereComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	//设置所有碰撞通道为忽略
	SphereComp->SetCollisionResponseToChannels(ECR_Ignore);
	//只开启和Pawn类型的重叠
	SphereComp->SetCollisionResponseToChannel(ECC_Pawn,ECR_Overlap);
	SphereComp->SetupAttachment(StaticMeshComp);
}

void ASTracerBot::DamageSelf()
{
    //对自己造成20点伤害,ApplyDamage(被伤害的Actor,基础伤害,造成伤害的控制器,造成伤害的Actor,描述造成伤害的类)
	UGameplayStatics::ApplyDamage(this,20,GetInstigatorController(),this,nullptr);
}

void ASTracerBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
	Super::NotifyActorBeginOverlap(OtherActor);
	if (!bStartSelfDestruction)
	{
		ASCharacter* MyCharacter = Cast<ASCharacter>(OtherActor);
		//如果碰到的是玩家,就用定时器每0.5秒伤害自己一次,一次20滴血
		if (MyCharacter)
		{
			//SetTimer(时间句柄,调用者,调用的方法,调用间隔,是否循环,离第一次调用的延迟)
			GetWorldTimerManager().SetTimer(TimerHandle_SelfDamage,this,&ASTracerBot::DamageSelf,0.5f,true,0.0f);
			//设置为开始自爆
			bStartSelfDestruction = true;
		}
	}
}

###5.AI球的音效。(滚动,警告,爆炸),记得在蓝图类选择音效。

STracerBot.h

1
2
3
4
5
6
    //倒计时自爆音效
	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	class USoundBase *SelfDestructSound;
	//爆炸音效
	UPROPERTY(EditDefaultsOnly,Category="TracerBot")
	class USoundBase* ExplodeSound;

STracerBot.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   void ASTracerBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
	Super::NotifyActorBeginOverlap(OtherActor);
	if (!bStartSelfDestruction)
	{
	//...
		if (MyCharacter)
		{
		//...
		//播放爆炸特效
	    UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionEffect, GetActorLocation());
	    //播放爆炸音效
	    UGameplayStatics::SpawnSoundAtLocation(this, ExplodeSound, GetActorLocation());
		}

	}
}

void ASTracerBot::SelfDestruct()
{
//...
	UGameplayStatics::SpawnSoundAtLocation(this,ExplodeSound,GetActorLocation());//播放爆炸音效
	Destroy();
}

###6.两种设置声音衰减的方法。

1.直接在声音Cue上设置,如图:

Untitled.webp

2.创建声音衰减文件,如图:

Untitled 1.webp

###7.设置滚动的声音随速度变化。

1.蓝图BP_TracerBot添加音频组件,选择滚动音效。 2.写蓝图,如图:

Untitled 2.webp

#联机AI ###1.只在服务端执行寻路逻辑。

TracerBot.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void ASTracerBot::BeginPlay()
{
	Super::BeginPlay();
    //只在服务端执行寻路逻辑
	if (GetLocalRole() == ROLE_Authority)
	{
	    NextPathPoint = GetNextPathPoint(); //获取到下一个导航点
	}

}

void ASTracerBot::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
        //只在服务端执行寻路逻辑,且AI球没爆炸
	if (GetLocalRole() == ROLE_Authority && !bExploded )
	{
	    //...
	}
}

###2.修改生命值组件和AI球联机逻辑。

SHealthComponent.h

1
2
3
4
5
    UPROPERTY(ReplicatedUsing=OnRep_Health,BlueprintReadOnly,Category="HealthComponent")
	float Health;

	UFUNCTION()
	void OnRep_Health(float OldHealth);

SHealthComponent.cpp

1
2
3
4
5

void USHealthComponent::OnRep_Health(float OldHealth)
{
	OnHealthChanged.Broadcast(this,Health,Health - OldHealth,nullptr,nullptr,nullptr);
}

STracerBot.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void ASTracerBot::SelfDestruct()
{
	//...
    //静态网格体设置可见性和关闭碰撞
	StaticMeshComp->SetVisibility(false, true);
	StaticMeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	//AI球造成范围伤害也在服务端上做
	if (GetLocalRole() == ROLE_Authority)
	{
	//添加忽略的Actor,自身
		TArray<AActor*> IgnoredActors;
		IgnoredActors.Add(this);
		//造成范围伤害ApplyRadialDamage(上下文,原点的基础伤害,原点位置,伤害半径,伤害类型类,要忽略的Actor列表,造成伤害的人,负责造成伤害的控制器,伤害是否根据原点缩放)
		UGameplayStatics::ApplyRadialDamage(this, ExplosionDamage, GetActorLocation(), ExplosionRadius, nullptr,
		                                    IgnoredActors, this, GetInstigatorController(), true);
		DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 11, FColor::Green, false, 2.0f, 0, 1.0f);
		//设置这个Actor的寿命。当寿命结束就会销毁。
		SetLifeSpan(1.0f);
	}
}

void ASTracerBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
	Super::NotifyActorBeginOverlap(OtherActor);
	//如果没有开始倒计时自爆,且没有爆炸
	if (!bStartSelfDestruction && !bExploded)
	{
        //...
		//如果碰到的是玩家,就用定时器每0.5秒伤害自己一次,一次20滴血
		if (MyCharacter)
		{
            //设置自爆的定时器也要在服务器上
			if (GetLocalRole() == ROLE_Authority)
			{
				//SetTimer(时间句柄,调用者,调用的方法,调用间隔,是否循环,离第一次调用的延迟)
				GetWorldTimerManager().SetTimer(TimerHandle_SelfDamage, this, &ASTracerBot::DamageSelf, 0.5f, true, 0.0f);
			}
            //...
		}
	}
}

###3.运行结果:两端的角色都能被AI球炸死,特效音效正常。 #挑战:AI群体Buff ###1.AI球附近有多个同类时,闪烁并增加伤害。

StracerBot.h

1
2
3
4
5
    //检测附近同类的函数
	UFUNCTION()
	void OnCheckNearbyBots();
	//伤害等级
	int32 PowerLevel;

StracerBot.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
void ASTracerBot::BeginPlay()
{
	Super::BeginPlay();
	//只有在服务端上的AI球才寻路
	if (GetLocalRole() == ROLE_Authority)
	{
		//获取到下一个导航点并赋值
		NextPathPoint = GetNextPathPoint();
		//设置每秒调用检测附近AI球同类的OnCheckNearbyBots函数
		FTimerHandle TimerHandle;
		GetWorldTimerManager().SetTimer(TimerHandle, this, &ASTracerBot::OnCheckNearbyBots, 1.0f, true);
	}
}

void ASTracerBot::SelfDestruct()
{
//...
	if (GetLocalRole() == ROLE_Authority)
	{
	//...
		//照成的伤害翻倍取决于附近的同类数量
		float Damage = ExplosionDamage + (ExplosionDamage * PowerLevel);
		//造成范围伤害ApplyRadialDamage(上下文,原点的基础伤害,原点位置,伤害半径,伤害类型类,要忽略的Actor列表,造成伤害的人,负责造成伤害的控制器,伤害是否根据原点缩放)
		UGameplayStatics::ApplyRadialDamage(this, Damage, GetActorLocation(), ExplosionRadius, nullptr,IgnoredActors, this, GetInstigatorController(), true);
		DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 11, FColor::Green, false, 2.0f, 0, 1.0f);
    //...
	}
//...
}

FVector ASTracerBot::GetNextPathPoint()
{
	//拿到0号玩家对象
	APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(this, 0);
	//拿到0号玩家才继续寻路
	if (PlayerPawn)
	{
		//立即找到下一个路径(上下文,路径开始点,目标Actor)
		UNavigationPath* NavPath = UNavigationSystemV1::FindPathToActorSynchronously(this, GetActorLocation(), PlayerPawn);
		//如果路径点数量大于1返回下一个位置点,且路径存在
		if (NavPath && NavPath->PathPoints.Num() > 1)
		{
			return NavPath->PathPoints[1];
		}
	}
//否则返回初始位置
		return GetActorLocation();
}

void ASTracerBot::OnCheckNearbyBots()
{
	//声明碰撞球,用于检测球体内有多少AI同类。
	FCollisionShape CollisionShape;
	//设置碰撞球半径。
	CollisionShape.SetSphere(600);
	//重叠结果的数组,用来存放所有重叠到的东西
	TArray<FOverlapResult> OverlapResults;
	//碰撞对象查询参数
	FCollisionObjectQueryParams QueryParams;
	//添加需要查询的两种碰撞通道类型
	QueryParams.AddObjectTypesToQuery(ECC_PhysicsBody);
	QueryParams.AddObjectTypesToQuery(ECC_Pawn);
	//按对象类型的重叠检测,将检测到的东西放进OverlapResults数组中
	GetWorld()->OverlapMultiByObjectType(OverlapResults, GetActorLocation(), FQuat::Identity, QueryParams,
	                                     CollisionShape);
	//画出用于检测的碰撞球
	DrawDebugSphere(GetWorld(), GetActorLocation(), CollisionShape.GetSphereRadius(), 12, FColor::Blue, false, 1.0f);
	//声明其他同类AI球的数量
	int32 NrOfBots = 0;
	//遍历所有重叠检测到的东西,如果是同类就把同类数量+1
	for (FOverlapResult Result : OverlapResults)
	{
		ASTracerBot* Bot = Cast<ASTracerBot>(Result.GetActor());
		//重叠检测到的是AI球,且不是自己,就计数
		if (Bot && Bot != this)
		{
			NrOfBots++;
		}
	}
	//定义常量最大伤害等级
	const int32 MaxPowerLevel = 4;
	//限制伤害等级范围0-4
	PowerLevel = FMath::Clamp(NrOfBots, 0, MaxPowerLevel);
	if (MatInst == nullptr)
	{
		//拿到AI球的材质实例
		MatInst = StaticMeshComp->
			CreateAndSetMaterialInstanceDynamicFromMaterial(0, StaticMeshComp->GetMaterial(0));
	}
	if (MatInst)
	{
		//通过设置参数来改变材质闪烁,更详细的材质介绍请看视频
		float Alpha = PowerLevel / (float)MaxPowerLevel;
		MatInst->SetScalarParameterValue("PowerLevelAlpha", Alpha);
	}
	//打印伤害等级
	DrawDebugString(GetWorld(), FVector(), FString::FromInt(PowerLevel), this, FColor::White, 1.0f, true);
}

###2.材质修改,参数PowerLevelAlpha控制材质闪烁。

蓝图如图:

Untitled 3.webp

coopgame - 这篇文章属于一个选集。
§ 6: 本文