【UE4C++-ActionRougelike-13】AI优化
一、AI角色再生
1、创建生成点EQS
新建EQS文件Query_FindBotSpawn,该EQS的目的是从所有的玩家身边的环形区域随机生成AI机器人。(具体节点设置有些还是不太清楚为什么,以后系统学习AI部分后再回来看看是否能够理解吧)
Donut节点设置:中心点设置为一个自定义情景(找到所有的玩家)
Distance节点设置:
PathFinding节点设置:
QueryContext_FindAllPlayers蓝图:找到所有的玩家类
2、自定义GameMode
以GameModeBase为父类新建一个GameMode类
class ACTIONROGUELIKE_API AMyGameModeBase : public AGameModeBase
{
GENERATED_BODY()
protected:
//指定要生成的AI角色的类
UPROPERTY(EditDefaultsOnly, Category = "AI")
TSubclassOf<AActor> MinionClass;
//指定用于生成AI角色的环境查询系统(EQS)查询
UPROPERTY(EditDefaultsOnly, Category = "AI")
UEnvQuery* SpawnBotQuery;
//指定游戏的难度曲线(AI角色生成数量)
UPROPERTY(EditDefaultsOnly, Category = "AI")
UCurveFloat* DifficultyCurve;
//管理定时生成AI角色的计时器
FTimerHandle TimerHandle_SpawnBots;
//指定生成AI角色的时间间隔
UPROPERTY(EditDefaultsOnly, Category = "AI")
float SpawnTimerInterval;
//计时器回调函数,用于生成AI角色
UFUNCTION()
void SpawnBotTimerElapsed();
//EQS查询完成的回调函数,用于处理查询结果
UFUNCTION()
void OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus);
public:
AMyGameModeBase();
virtual void StartPlay() override;
};
在MyGameModeBase.cpp中给出GameMode类的实现,主要思路是:
- 游戏开始后,设置定时器来循环执行AI机器人生成函数
- 生成函数会首先检测当前游戏中AI机器人数量是否达到上限,如果达到上限则进入下一次定时器。
- 如果没有则会运行先前创建的EQS来寻找生成位置
- 在EQS查询结束后执行与查询结束实践绑定的 OnQueryCompleted() 函数
AMyGameModeBase::AMyGameModeBase()
{
SpawnTimerInterval = 2.0f;
}
void AMyGameModeBase::StartPlay()
{
Super::StartPlay();
//设置了一个定时器TimerHandle_SpawnBots,定时调用SpawnBotTimerElapsed()函数,时间间隔为SpawnTimerInterval秒,循环执行
GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &AMyGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
}
void AMyGameModeBase::SpawnBotTimerElapsed()
{
int32 NrOfAliveBots = 0;
//统计当前世界中存活的 AMyAICharacter 类型的角色数量
for (TActorIterator<AMyAICharacter> It(GetWorld()); It; ++It)
{
AMyAICharacter* Bot = *It;
UMyAttributeComponent* AttributeComp = Cast<UMyAttributeComponent>(Bot->GetComponentByClass(UMyAttributeComponent::StaticClass()));
//这里目前会报Bug,因为还没有给MyAICharacter添加MyAttributeComponent组件
if (ensure(AttributeComp) && AttributeComp->IsAlive())
{
NrOfAliveBots++;
}
}
UE_LOG(LogTemp, Log, TEXT("Found %i alive bots"), NrOfAliveBots);
float MaxBotCount = 10.0f;
//根据 DifficultyCurve 曲线的值计算出最大的 bot 数量
if (DifficultyCurve)
{
MaxBotCount = DifficultyCurve->GetFloatValue(GetWorld()->TimeSeconds);
}
//与当前存活的 bot 数量进行比较。如果存活的 bot 数量小于最大数量,则从查询结果中获取一个位置
if (NrOfAliveBots >= MaxBotCount)
{
UE_LOG(LogTemp, Log, TEXT("At maximum bot capacity. Skipping bot spawn."));
return;
}
//运行一个环境查询(Environment Query System, EQS)并获取查询实例 QueryInstance。
UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, SpawnBotQuery, this, EEnvQueryRunMode::RandomBest5Pct, nullptr);
if (ensure(QueryInstance))
{
//将当前对象绑定到查询完成事件,查询完成后会调用 OnQueryCompleted() 函数。
QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &AMyGameModeBase::OnQueryCompleted);
}
}
void AMyGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
if (QueryStatus != EEnvQueryStatus::Success)
{
UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
return;
}
TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
//通过 GetWorld()->SpawnActor<AActor>() 函数在该位置生成一个 MinionClass 类型的角色
if (Locations.IsValidIndex(0))
{
GetWorld()->SpawnActor<AActor>(MinionClass, Locations[0], FRotator::ZeroRotator);
}
}
3、创建Curve & 分配资产
创建Curve可以自定义随时间变化的最大AI机器人数量,以MyGameMode为父类创建蓝图类GameMode_BP,并分配资产。最后要在WorldSetting里的GameMode选用GameMode_BP。
如果生成的AI机器人不会动,要在MyAICharacter.cpp的构造函数中添加下面代码,表示生成的AI也可以执行各种行为。
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
4、效果演示
效果演示这里已经为MyAICharacter添加了Attribute组件,不添加的话会在保错。
二、AI角色增加血量属性
1、添加属性组件
在MyAICharacter中添加属性组件
class ACTIONROGUELIKE_API AMyAICharacter : public ACharacter
{
protected:
//属性组件
UPROPERTY(VisibleAnywhere,BlueprintReadOnly, Category = "Components")
UMyAttributeComponent* AttributeComp;
UFUNCTION()
void OnHealthChanged(AActor* InstigatorActor, UMyAttributeComponent* OwningComp, float NewHealth, float Delta);
在MyAICharacter.cpp中初始化属性组件,并绑定血量变化事件函数
AMyAICharacter::AMyAICharacter()
{
AttributeComp = CreateDefaultSubobject<UMyAttributeComponent>("AttributeComp");
}
void AMyAICharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
//血量变化事件触发时执行OnHealthChanged函数
AttributeComp->OnHealthChange.AddDynamic(this, &AMyAICharacter::OnHealthChanged);
}
2、设置AI死亡
在MyAICharacter.cpp的OnHealthChanged中可以设置AI何时死亡,主要思路:
- 当AI生命值变化时,根据Delta判断是否收到伤害
- 如果收到伤害且来源不是自身,则将伤害来源角色设为TargetActor
- 如果生命值降至0以下,则AI角色死亡并执行死亡后的逻辑
- 停止角色的行为树逻辑
- 设置角色Mesh的所有物体模拟物理效果
- 设置角色的寿命,定时销毁角色
void AMyAICharacter::OnHealthChanged(AActor* InstigatorActor, UMyAttributeComponent* OwningComp, float NewHealth, float Delta)
{
if (Delta < 0.0f)
{
if (InstigatorActor != this)
{
SetTargetActor(InstigatorActor);
}
GetMesh()->SetScalarParameterValueOnMaterials(TimeToHitParamName, GetWorld()->TimeSeconds);
//AI角色死亡
if (NewHealth <= 0.0f)
{
//停止行为树
AAIController* AIC = Cast<AAIController>(GetController());
if (AIC)
{
AIC->GetBrainComponent()->StopLogic("Killed");
}
//设置模拟物理效果
GetMesh()->SetAllBodiesSimulatePhysics(true);
GetMesh()->SetCollisionProfileName("Ragdoll");
//设置尸体存留时间
SetLifeSpan(10.0f);
}
}
}
void AMyAICharacter::SetTargetActor(AActor* NewTarget)
{
AAIController* AIC = Cast<AAIController>(GetController());
if (AIC)
{
AIC->GetBlackboardComponent()->SetValueAsObject("TargetActor", NewTarget);
}
}
3、效果演示
三、射击逻辑优化
1、静态函数
在MyAttributeComponent中声明两个静态函数,分别用于获取某个对象的属性组件和判断对象是否存活
class ACTIONROGUELIKE_API UMyAttributeComponent : public UActorComponent
{
public:
UFUNCTION(BlueprintCallable, Category = "Attributes")
static UMyAttributeComponent* GetAttributes(AActor* FromActor);
UFUNCTION(BlueprintCallable, Category = "Attributes", meta = (DisplayName = "IsAlive"))
static bool IsActorAlive(AActor* Actor);
}
MyAttributeComponent.cpp中给出函数的实现
UMyAttributeComponent* UMyAttributeComponent::GetAttributes(AActor* FromActor)
{
if (FromActor)
{
return Cast<UMyAttributeComponent>(FromActor->GetComponentByClass(UMyAttributeComponent::StaticClass()));
}
return nullptr;
}
bool UMyAttributeComponent::IsActorAlive(AActor* Actor)
{
UMyAttributeComponent* AttributeComp = GetAttributes(Actor);
if (AttributeComp)
{
return AttributeComp->IsAlive();
}
return false;
}
这种静态函数的设计模式可以提供一些全局的工具函数或实用功能,并且不需要创建类的实例就可以调用这些函数。静态函数在这种情况下可以作为一个工具函数库,提供方便的方法来处理属性组件和判断 Actor 存活状态
//UMyAttributeComponent* AttributeComp = Cast<UMyAttributeComponent>(Bot->GetComponentByClass(UMyAttributeComponent::StaticClass()));
UMyAttributeComponent* AttributeComp = UMyAttributeComponent::GetAttributes(Bot);
2、玩家死亡后停止射击 & 射击弹道偏移
修改MyBTTask_RangedAttack类,添加弹道偏移范围属性
class ACTIONROGUELIKE_API UMyBTTask_RangedAttack : public UBTTaskNode
{
public:
UMyBTTask_RangedAttack();
protected:
//弹道波动范围
UPROPERTY(EditAnywhere, Category = "AI")
float MaxBulletSpread;
};
在MyBTTask_RangedAttack.cpp中添加玩家死亡后停止射击逻辑和弹道偏移
EBTNodeResult::Type UMyBTTask_RangedAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
//获取当前任务节点所属的 AI 控制器
AAIController* MyAIController = OwnerComp.GetAIOwner();
if (ensure(MyAIController))
{
//获取 AI 控制器的控制角色
ACharacter* MyPawn = Cast<ACharacter>(MyAIController->GetPawn());
//如果控制角色为空,则返回 EBTNodeResult::Failed 表示任务失败。
if (MyPawn == nullptr)
{
return EBTNodeResult::Failed;
}
//获取枪口位置(MuzzleLocation)
FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");
//获取黑板中保存的目标角色(TargetActor)
AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor"));
//如果目标角色为空,则返回 EBTNodeResult::Failed 表示任务失败
if (TargetActor == nullptr)
{
return EBTNodeResult::Failed;
}
//如果角色死亡,停止射击
if (!UMyAttributeComponent::IsActorAlive(TargetActor))
{
return EBTNodeResult::Failed;
}
//计算从枪口位置到目标角色位置的方向向量(Direction),并将其转换为旋转
FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
FRotator MuzzleRotation = Direction.Rotation();
//弹道波动范围
MuzzleRotation.Pitch += FMath::RandRange(0.0f, MaxBulletSpread);
MuzzleRotation.Yaw += FMath::RandRange(-MaxBulletSpread, MaxBulletSpread);
FActorSpawnParameters Params;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
Params.Instigator = MyPawn;
AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, Params);
//如果新生成的投射物实例不为空,则返回 EBTNodeResult::Succeeded 表示任务成功,否则返回 EBTNodeResult::Failed 表示任务失败
return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}
return EBTNodeResult::Failed;
}