CoopGame09-进阶AI
type: Post
status: Published
date: 2022/10/25
slug: CoopGame09
summary: UE4 C++多人游戏入门
category: Unreal
跟随B站up主“技术宅阿棍儿”的教程制作的笔记。教程链接
0.内容
- 人形敌人,远程武器
- AI行为树
- 使用EQS系统帮助AI掩护或寻找攻击点
- AI的视觉
- 挑战:AI低血量自动逃跑和回复
1.综合使用黑板,行为树,环境查询
1.创建AdvancedAI文件夹放高级AI相关文件,创建BP_AdavncedAI蓝图类,继承SCharacter C++类。
2.设置BP_AdvancedAI蓝图类
设置自身
设置网格体
3.创建黑板BB_AdvancedAI,新建向量类型变量TragetDestination,Object类型变量TargetActor,其基类为Actor。
4.创建行为树BT_AdvancedAI,点击根,绑定黑板BB_AdvancedAI,并编写
5.编写BP_AdavncedAI蓝图类,实现AI朝一个点前进。
6.使用EQS决定AI攻击的位置
新建环境查询EQS_FindMoveTo,根下添加Point:Donut(环),设置如图,字面意思,找点模式为根据场景上的导航体。
测试1距离测试:离AI自己越近的分数越高,权重为2
测试2追踪测试:
测试3追踪测试:看不到玩家的点都是无效的
7.新建装饰器Decorator_DistanceTo,继承BTDecorator_BlueprintBase BT装饰器蓝图基础。
重载PerformConditionCheckAI 执行条件检查AI函数,新建黑板键选择器结构变量BlackboardKey和浮点变量Distance,两个变量都设置为可编辑实例,编写蓝图。
8.装饰器的作用是,当AI的环境查询设置了看不到玩家的点时无效点时,不能依靠环境查询来靠近玩家,需要另一个分支来使AI靠近玩家
9.修改之前蓝图类BP_AdvancedAI逻辑,改为向目标Actor移动
10.新建蓝图类EnvQueryContext_TargetActor,继承EvnQueryContext_Blueprintbase,重载ProvideSingleActor 提供单个Actor。
10.修改行为树BT_AdvancedAI
11.修改环境查询
2.AI感知和行为树服务
1.修改蓝图类BP_AdvancedAI,将之前获取玩家Pawn然后设置黑板的TargetActor的步骤删除,改为下面在服务里用视觉组件,看到谁就找谁,而不是自己指定Actor。
添加AI感知组件(AIPerception),并设置添加感官配置AI视力配置
创建服务蓝图Service_SelectTargetActor ,继承BTService_BlueprintBase类,编写蓝图。注意新建的黑板键选择器变量设置为可编辑实例。
修改行为树BT_AdvancedAI,服务下面的selector是添加Blackboard装饰器,判断TargetActor已设置
3.新环境查询和装饰器的终止机制
实现多人游戏情况下,AI找的是离自己最近的玩家
1.新建环境查询EQS_FindNearesPlayer,根拉出Actor of class,选择我们的玩家蓝图类,取消勾选仅生成半径中的Actor
2.修改行为树,设置黑板条件中的观察期终止,终止低优先级。意思Blackboard Based Condition条件为假,既没有看到玩家的时候,是走右边的查询最近的玩家并移动过去,但在移动的过程中看到玩家了,此时条件为真,会终止比他优先级低的节点,既是Blackboard Based Condition右边的会终止。
4.进击的AI
1.修改角色代码
SCharacter.h
public:
UFUNCTION(BlueprintCallable,Category="Player")
void StartFire();
UFUNCTION(BlueprintCallable,Category="Player")
void StopFire();
2.新建任务蓝图Task_AttackTarget,继承BTTask_Blueprint BaseBT任务蓝图基础,并编写
3.修改行为树BT_AdvancedAI
4.修改AI的攻击伤害。
1.SWeapon.h
/* 子弹散射角度 */
UPROPERTY(EditDefaultsOnly, Category = "Weapon", meta = (ClampMin=0.0f))
float BulletSpread;
2.SWeapon.cpp
ASWeapon::ASWeapon()
{
//...
BulletSpread = 2.0f;
}
void ASWeapon::Fire()
{
//...
if (MyOwner)
{
//...
// 子弹散射角,以射线为中心的锥形随机向量
float HalfRad = FMath::DegreesToRadians(BulletSpread);
ShotDirection = FMath::VRandCone(ShotDirection, HalfRad, HalfRad);
FVector TraceEnd = EyeLocation + (ShotDirection * 10000);
}
}
3.把BP_Rifle复制到AdvancedAI目录,更名为BP_Rifle_AI,修改类默认值减小攻击伤害,增大散射角。将BP_AdvancedAI蓝图类的累默认值的武器修改为修改后的枪。
4.随机生成两种AI敌人,修改BP_TestGameMode蓝图类。
5.别开枪,是友军。
1.人形AI不打友军。
SHealthComponent.h
//队伍编号
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "HealthComponent")
uint8 TeamNum;
//判断两个Actor是否为友军
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "HealthComponent")
static bool IsFriendly(AActor* ActorA, AActor* ActorB);
SHealthComponent.cpp
USHealthComponent::USHealthComponent()
{
//默认队伍编号为255
TeamNum = 255;
}
void USHealthComponent::HandleTakeAnyDamage(AActor* DamagedActor, float Damage, const class UDamageType* DamageType, class AController* InstigatedBy,
AActor* DamageCauser)
{
if (Damage <= 0.0f || bIsDead)
{
return;
}
//在伤害发生时进行一个友军判断
if (DamageCauser != DamagedActor && IsFriendly(DamagedActor, DamageCauser))
{
return;
}
}
bool USHealthComponent::IsFriendly(AActor* ActorA, AActor* ActorB)
{
if (ActorA == nullptr || ActorB == nullptr)
{
return true;
}
USHealthComponent* HealthCompA = Cast<USHealthComponent>(ActorA->GetComponentByClass(USHealthComponent::StaticClass()));
USHealthComponent* HealthCompB = Cast<USHealthComponent>(ActorB->GetComponentByClass(USHealthComponent::StaticClass()));
//没有健康值的物体也算友军,不伤害
if (HealthCompA == nullptr || HealthCompB == nullptr)
{
return true;
}
//返回两个Actor的队伍编号是否相等
return HealthCompA->TeamNum == HealthCompB->TeamNum;
}
SWeapon.cpp
void ASWeapon::Fire()
{
// Trace the world, from pawn eyes to crosshair location
if (GetLocalRole() < ROLE_Authority)
{
ServerFire();
}
AActor* MyOwner = GetOwner();
if (MyOwner)
{
//...
if (GetWorld()->LineTraceSingleByChannel(Hit, EyeLocation, TraceEnd, COLLISION_WEAPON, QueryParams))
{
//...
//这里把伤害者改为武器拥有者,武器是没有生命值组件的
UGameplayStatics::ApplyPointDamage(HitActor, ActualDamage, ShotDirection, Hit, MyOwner->GetInstigatorController(), MyOwner, DamageType);
}
}
}
调整人形AI的相机,修改BP_AdvancedAI蓝图类中的弹簧臂,使相机在人物眼睛位置。
修改玩家BP_SCharacter中的生命值组件的队伍编号为0,这样就和其他人形AI的255分开了。2.实时更新选择离自己最近的目标攻击
Service_SelectTargetActor
3.优化球形AI,并让球形AI不伤害友军。
STracerBot.h
protected:
FTimerHandle TimerHandle_RefreshPath;
void RefreshPath();
STracerBot.cpp
//调试用
static int32 DebugTrackerBotDrawing = 0;
FAutoConsoleVariableRef CVARDebugTrackerBotDrawing(
TEXT("COOP.DebugTrackerBot"),
DebugTrackerBotDrawing,
TEXT("Draw Debug Lines for TrackerBot"),
ECVF_Cheat);
ASTrackerBot::ASTrackerBot()
{
//修改伤害和范围
ExplosionDamage = 60;
ExplosionRadius = 350;
}
FVector ASTrackerBot::GetNextPathPoint()
{
AActor* BestTarget = nullptr;
float NearestTargetDistance = FLT_MAX;
//使用Pawn迭代器,拿到每一个Pawn的生命值组件,比较他们的队伍编号,找到生命值大于0的非友军,设置离自己最近的敌人的距离。
for (FConstPawnIterator It = GetWorld()->GetPawnIterator(); It; ++It)
{
APawn* TestPawn = It->Get();
if (TestPawn == nullptr || USHealthComponent::IsFriendly(TestPawn, this))
{
continue;
}
USHealthComponent* TestPawnHealthComp = Cast<USHealthComponent>(TestPawn->GetComponentByClass(USHealthComponent::StaticClass()));
if (TestPawnHealthComp && TestPawnHealthComp->GetHealth() > 0.0f)
{
float Distance = (TestPawn->GetActorLocation() - GetActorLocation()).Size();
if (Distance < NearestTargetDistance)
{
BestTarget = TestPawn;
NearestTargetDistance = Distance;
}
}
}
if (BestTarget)
{
UNavigationPath* NavPath = UNavigationSystemV1::FindPathToActorSynchronously(this, GetActorLocation(), BestTarget);
GetWorldTimerManager().ClearTimer(TimerHandle_RefreshPath);
GetWorldTimerManager().SetTimer(TimerHandle_RefreshPath, this, &ASTrackerBot::RefreshPath , 5.0f, false);
if (NavPath && NavPath->PathPoints.Num() > 1)
{
// 返回下一个点
return NavPath->PathPoints[1];
}
}
// 寻路失败
return GetActorLocation();
}
void ASTrackerBot::RefreshPath()
{
NextPathPoint = GetNextPathPoint();
}
6.挑战:狡猾AI
1.实现人形AI血量降低到40时,逃跑并回血
AI行为树的修改,一个装饰器用来检查血量,一个查询用来查找逃离地点,一个任务用来回血
1.新建检查血量的装饰器Decorator_CheckBelowHealth,继承装饰器基类,重载PerformConditionCheckAI执行条件检查AI函数,新建血量阈值LowHealthTreshold设为公开。
2.新建查询逃离点的环境查询EQS_FindCover1.生成环
2.测试1,找建筑物高于100的掩体,来躲开玩家。
3.测试2,找距离玩家至少300的距离最远的点。
4.测试3,找直线距离最近的点,不要绕道的。
3.新建回血的任务Task_HealSelf,记得变量公开