UE AI学习之路
行为树学习
行为树简介
行为树控制着游戏中AI的行为,可以看成AI的大脑。它是一个自上而下运行的结构,从根节点开始向下一直到达任务节点。中间有一些改变执行路径的分支节点.
Bots行为与移动
- 创建新的类SAICharacter,SAIController
- 创建AI文件夹,新建SAICharacter蓝图
- 为蓝图选择Mesh,设置动画
- 修改蓝图,使动画正常运行(因为我们是从别的地方拿的资产,缺少相关的类)
- 将资产放入场景
- 在Volumes里面选择Nav Mesh Bounds Volume
- 在AI中选择Behavior Tree 和 Blackboard
8.在Behavior Tree中为ROOT选择Blackboard Asset,与我们建立的Blackboard绑定
- 在Blackboard中创建Key-选择Vector
- 完善Behavior Tree
- 修改C++代码,以SAIController为基类创建蓝图
//SAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "SAIController.generated.h"
class UBehaviorTree;
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API ASAIController : public AAIController
{
GENERATED_BODY()
protected:
UPROPERTY(EditDefaultsOnly,Category = "AI")
UBehaviorTree* BehaviorTree;
virtual void BeginPlay() override;
};
//SAIController.cpp
#include "AI/SAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
void ASAIController::BeginPlay()
{
Super::BeginPlay();
RunBehaviorTree(BehaviorTree);
APawn* MyPawn = UGameplayStatics::GetPlayerPawn(this,0);
if(MyPawn != nullptr)
{
//使AI移动至角色初始位置
GetBlackboardComponent()->SetValueAsVector("MoveToLocation",MyPawn->GetActorLocation());
//使AI移动至角色当前位置
GetBlackboardComponent()->SetValueAsObject("TargetActor",MyPawn);
}
}
- 将蓝图与Behavior Tree绑定,修改AI Controller Class
攻击范围检测
- 创建新C++类,以BTService为基类
//SBTService_CheckAttackRange.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "SBTService_CheckAttackRange.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API USBTService_CheckAttackRange : public UBTService
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere,Category="AI")
FBlackboardKeySelector AttackRangeKey;
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
//SBTService_CheckAttackRange.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/SBTService_CheckAttackRange.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
void USBTService_CheckAttackRange::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
//Check distance between ai pawn and target actor
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if(ensure(BlackboardComp))
{
AActor* TargetActor = Cast<AActor>(BlackboardComp->GetValueAsObject("TargetActor"));
if(TargetActor)
{
AAIController* MyController = OwnerComp.GetAIOwner();
if(ensure(MyController))
{
APawn* AIPawn = MyController->GetPawn();
if(ensure(AIPawn))
{
float DistanceTo = FVector::Distance(TargetActor->GetActorLocation(),AIPawn->GetActorLocation());
bool bWithinRange = DistanceTo < 2000.0f;
/*bool bHasLOS = false;
if(bWithinRange)
{
bHasLOS = MyController->LineOfSightTo(TargetActor);
}*/
BlackboardComp->SetValueAsBool(AttackRangeKey.SelectedKeyName,(bWithinRange /*&& bHasLOS*/));
}
}
}
}
}
//未去掉注释的代码实现,NPC自动寻找玩家,如果范围小于2000.0f则继续寻找,否则原地停止
//去掉上述代码中的注释,上述代码实现Bot如果无法看到玩家,就自动寻找路径直到能够发现玩家
- 打开Blackboard,创建新Key,类型为bool,
- 打开Behavior Tree,修改Sequence,以及Move To Player
- 修改添加的Decorator,图中例子意味着当不在攻击范围时,会执行情况
- 添加Selector
6.最终成果如下
使Bot根据范围进行攻击
- 创建新的类
- 在C++中完成下列代码
//SBTTask_RangedTask.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/SBTTask_RangedTask.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "GameFramework/Character.h"
EBTNodeResult::Type USBTTask_RangedTask::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* MyController = OwnerComp.GetAIOwner();
if(ensure(MyController))
{
ACharacter* MyPawn = Cast<ACharacter>(MyController->GetPawn());
if(MyPawn == nullptr)
{
return EBTNodeResult::Failed;
}
FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");
AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor"));
if(TargetActor == nullptr)
{
return EBTNodeResult::Failed;
}
FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
FRotator MuzzleRotation = Direction.Rotation();
FActorSpawnParameters Params;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass,MuzzleLocation,MuzzleRotation,Params);
return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}
return EBTNodeResult::Failed;
}
//SBTTask_RangedTask.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "SBTTask_RangedTask.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API USBTTask_RangedTask : public UBTTaskNode
{
GENERATED_BODY()
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
protected:
UPROPERTY(EditAnywhere,Category="AI")
TSubclassOf<AActor> ProjectileClass;
};
- 在BT Tree中如下勾选,发现Bot攻击没有间隔
- 添加Cooldown添加攻击间隔,设置Loop实现三连击
- 行为树观察中断操作(没有成功实现,目前不知道原因)
2024/5/9更新 将CoolDown的Observer aborts设置为Lower Priority即可解决
2024/5/14更新 ,其实根据下面的图片中的蓝图重新修改后就可完成效果,这里不成功是因为和教程中选择Blackboard Key还是不一样
- 使得Bot在攻击时焦点聚集于玩家身上,我们的Default Focus选择的TargetActor,也就是Bot瞄准的位置
让AI更聪明的移动
- 创建Environment Query
按照下图添加
- 在Behavior Tree中添加RunEQSQuery
- 修改Filter 确定最小范围
- 创建蓝图
在蓝图中创建函数 ProvideSingleActor
- 通过修改如图所指,可以实现影响ai的路径
- 继续修改
- 使用开发者工具中的 Visual Logger
- ai判断得分的标准
- 修改一下
为AI加上感测系统
- 修改下列代码
// SAICharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SAICharacter.generated.h"
class UPawnSensingComponent;
UCLASS()
class ACTIONROGUELIKE_API ASAICharacter : public ACharacter
{
GENERATED_BODY()
public:
ASAICharacter();
protected:
virtual void PostInitializeComponents() override;
UPROPERTY(VisibleAnywhere,Category="Components")
UPawnSensingComponent* PawnSensingComp;
UFUNCTION()
void OnPawnSeen(APawn* Pawn);
};
// SAICharacter.cpp
#include "AI/SAICharacter.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/PawnSensingComponent.h"
// Sets default values
ASAICharacter::ASAICharacter()
{
PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");
}
void ASAICharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
PawnSensingComp->OnSeePawn.AddDynamic(this,&ASAICharacter::OnPawnSeen);
}
void ASAICharacter::OnPawnSeen(APawn* Pawn)
{
AAIController* AIC = Cast<AAIController>(GetController());
if(AIC)
{
UBlackboardComponent* BBComp = AIC->GetBlackboardComponent();
BBComp->SetValueAsObject("TargetActor",Pawn);
DrawDebugString(GetWorld(),GetActorLocation(),"PlayerSpotted",nullptr,FColor::White,4.0f,true);
}
}
// 注释掉SAIController.cpp中的部分代码,这里原本是为之前ai寻找目标实现的,现在要让ai感知player的位置,这部分不需要了
#include "AI/SAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
void ASAIController::BeginPlay()
{
Super::BeginPlay();
RunBehaviorTree(BehaviorTree);
/*APawn* MyPawn = UGameplayStatics::GetPlayerPawn(this,0);
if(MyPawn != nullptr)
{
//使AI移动至角色初始位置
GetBlackboardComponent()->SetValueAsVector("MoveToLocation",MyPawn->GetActorLocation());
//使AI移动至角色当前位置
GetBlackboardComponent()->SetValueAsObject("TargetActor",MyPawn);
}*/
//上面注释的代码已经不需要了
}
- 查看我们设计的Character,可以看到绿线是ai的检测范围
- 修改蓝图,告知ai角色的位置
改善Bots动画
通过上一步,我们仍然发现bots的步伐在会在到达指定位置时卡顿,现在我们要使动画更加顺滑
添加代码
//SAIController.cpp
if(ensureMsgf(BehaviorTree,TEXT("Behavior Tree is a nullptr! Please assign BehaviorTree in Your AI Controller.")))
{
RunBehaviorTree(BehaviorTree);
}
当行为树未部署时,会主动提示,但是在完成过程中并未发现提示
寻找机器人出生点
新建eqs
选择基础蓝图
在overvide函数中选择Actor Set重构
修改中心center
新建蓝图,进行eqs Testing
显示bot 生成位置
添加Distance Test检测距离上是否存在不能通过的位置
在这里我们很幸运,没有遇到点的位置出现在掩体内,不过我们还是要考虑这个问题
显示Navigation
我们在Volume中找到这个Nav Modifier Volume
然后在WireFrame模式下,将Nav部分消除
添加PathFinding Test,检测道路是否存在,这样做会增大开销,但是能避免路径出现在死点的情况。
自定义GameMode及实现Bots生成
创建新的类GameModeBase,在这里我们可以定义属于自己的游戏规则
//SAICharacter.cpp
ASAICharacter::ASAICharacter()
{
PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
//SGameModeBase.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "SGameModeBase.generated.h"
class UEnvQuery;
class UEnvQueryInstanceBlueprintWrapper;
class UCurveFloat;
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API ASGameModeBase : public AGameModeBase
{
GENERATED_BODY()
protected:
UPROPERTY(EditDefaultsOnly, Category = "AI")
TSubclassOf<AActor> MinionClass;
UPROPERTY(EditDefaultsOnly, Category = "AI")
UEnvQuery* SpawnBotQuery;
UPROPERTY(EditDefaultsOnly, Category = "AI")
UCurveFloat* DifficultyCurve;
FTimerHandle TimerHandle_SpawnBots;
UPROPERTY(EditDefaultsOnly, Category = "AI")
float SpawnTimerInterval;
UFUNCTION()
void SpawnBotTimerElapsed();
UFUNCTION()
void OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus);
public:
ASGameModeBase();
virtual void StartPlay() override;
};
////SGameModeBase.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "SGameModeBase.h"
#include "EnvironmentQuery/EnvQueryManager.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "EnvironmentQuery/EnvQueryInstanceBlueprintWrapper.h"
#include "AI/SAICharacter.h"
#include "SAttributeComponent.h"
#include "EngineUtils.h"
ASGameModeBase::ASGameModeBase()
{
SpawnTimerInterval = 2.0f;
}
void ASGameModeBase::StartPlay()
{
Super::StartPlay();
// Continuous timer to spawn in more bots.
// Actual amount of bots and whether its allowed to spawn determined by spawn logic later in the chain...
GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &ASGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
}
void ASGameModeBase::SpawnBotTimerElapsed()
{
UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, SpawnBotQuery, this, EEnvQueryRunMode::RandomBest5Pct, nullptr);
if (ensure(QueryInstance))
{
QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &ASGameModeBase::OnQueryCompleted);
}
}
void ASGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
if (QueryStatus != EEnvQueryStatus::Success)
{
UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
return;
}
int32 NrOfAliveBots = 0;
for (TActorIterator<ASAICharacter> It(GetWorld()); It; ++It)
{
ASAICharacter* Bot = *It;
USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(Bot->GetComponentByClass(USAttributeComponent::StaticClass()));
if (ensure(AttributeComp) && AttributeComp->IsAlive())
{
NrOfAliveBots++;
}
}
float MaxBotCount = 10.0f;
if (DifficultyCurve)
{
MaxBotCount = DifficultyCurve->GetFloatValue(GetWorld()->TimeSeconds);
}
if (NrOfAliveBots >= MaxBotCount)
{
return;
}
TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
if (Locations.IsValidIndex(0))
{
GetWorld()->SpawnActor<AActor>(MinionClass, Locations[0], FRotator::ZeroRotator);
}
}
新建一个以SGameMode为基类的蓝图,设置MinionClass和eqs
在世界设计GameMode为GameModeBP
创建难度曲线,选择CurveFloat
在Curve中 在0s时,只允许存在一个敌人,到60秒后,最多存在10个
增加伤害,死亡,布娃娃功能
我们要新增三个新功能,对Bots造成伤害,使bots死亡并且呈现布娃娃的状态
首先新增代码
//SAICharacter.h
protected:
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Components")
USAttributeComponent* AttributeComp;
UFUNCTION()
void OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,float Delta);
//SAICharacter.cpp
ASAICharacter::ASAICharacter()
{
AttributeComp = CreateDefaultSubobject<USAttributeComponent>("AttributeComp");
}
void ASAICharacter::PostInitializeComponents()
{
AttributeComp->OnHealthChanged.AddDynamic(this,&ASAICharacter::OnHealthChanged);
}
void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
float Delta)
{
if(Delta <0.0f)
{
if(NewHealth <= 0.0f)
{
//stop BT
AAIController* AIC = Cast<AAIController>(GetController());
if(AIC )
{
AIC->GetBrainComponent()->StopLogic("Killed");
}
//ragdoll
GetMesh()->SetAllBodiesSimulatePhysics(true);
//set lifespan
SetLifeSpan(10.0f);
}
}
}
//SGameModeBase.cpp
//仅仅添加一些调试的内容
void ASGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
if (QueryStatus != EEnvQueryStatus::Success)
{
UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
return;
}
UE_LOG(LogTemp,Log,TEXT("Found %i alive Bots"),NrOfAliveBots);
if (NrOfAliveBots >= MaxBotCount)
{
UE_LOG(LogTemp,Log,TEXT("At maximum bot capacity.Skipping bot spawn"));
return;
}
TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
if (Locations.IsValidIndex(0))
{
GetWorld()->SpawnActor<AActor>(MinionClass, Locations[0], FRotator::ZeroRotator);
DrawDebugSphere(GetWorld(),Locations[0],50.0f,20,FColor::Blue,false,60.0f);
}
}
修改魔法子弹的伤害后,射击机器人,发生机器人会停止行为,掉入虚空
为魔法子弹建立子蓝图,因为我们不需要bot能够打出那么多伤害,然后在子蓝图中我们修改伤害为5点
我们中RangedTask的ProjectileClass设置为刚刚的子蓝图
新增ragdoll的代码
////SAICharacter.cpp
//ragdoll
GetMesh()->SetAllBodiesSimulatePhysics(true);
GetMesh()->SetCollisionProfileName("Ragdoll");
现在击中后,bot不会直接掉入虚空,而是Ragdoll的碰撞条件的效果
这是因为我们bots的Mesh是由胶囊和球型组合在一起的,他们之间的空隙存在一定的约束
为AI添加更多感觉
我们希望当我们攻击ai时,他们不会傻傻的仍然往前走,而是会立刻反击
//SAICharacter.h
void SetTargetActor(AActor* NewTarget);
UFUNCTION()
void OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,float Delta);
//SAICharacter.cpp
void ASAICharacter::OnPawnSeen(APawn* Pawn)
{
SetTargetActor(Pawn);
DrawDebugString(GetWorld(),GetActorLocation(),"PlayerSpotted",nullptr,FColor::White,4.0f,true);
}
void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
float Delta)
{
if(Delta <0.0f)
{
if(InstigatorActor != this)
{
SetTargetActor(InstigatorActor);
}
if(NewHealth <= 0.0f)
{
//stop BT
AAIController* AIC = Cast<AAIController>(GetController());
if(AIC )
{
AIC->GetBrainComponent()->StopLogic("Killed");
}
//ragdoll
GetMesh()->SetAllBodiesSimulatePhysics(true);
GetMesh()->SetCollisionProfileName("Ragdoll");
//set lifespan
SetLifeSpan(10.0f);
}
}
}
//SAttributeComponent.h
UFUNCTION(BlueprintCallable,Category="Attributes")
bool ApplyHealthChange(AActor* InstigatorActor,float Delta);
//SAttributeComponent.cpp
bool USAttributeComponent::ApplyHealthChange(AActor* InstigatorActor,float Delta)
{
float OldHealth = Health;
Health = FMath::Clamp(Health+Delta,0.0f,HealthMax);
float ActualDelta = Health - OldHealth;
OnHealthChanged.Broadcast(InstigatorActor,this,Health,ActualDelta);
return ActualDelta != 0;
}
//SMagicProjectile.cpp
void ASMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if(OtherActor && OtherActor != GetInstigator())
{
USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(OtherActor->GetComponentByClass(USAttributeComponent::StaticClass()));
if(AttributeComp)
{
AttributeComp->ApplyHealthChange(GetInstigator(),-DamageAmount);
Destroy();
}
}
}
//SPowerup_HealthPotion.cpp
if (ensure(AttributeComp) && !AttributeComp->IsFullHealth())
{
// Only activate if healed successfully
if (AttributeComp->ApplyHealthChange(this,AttributeComp->GetHealthMax()))
{
HideAndCooldownPowerup();
}
}
一些对建设游戏框架有用的技巧(Static Function)
//SAttributeComponent.h
public:
UFUNCTION(BlueprintCallable,Category="Attributes")
static USAttributeComponent* GetAttributes(AActor* FromActor);
UFUNCTION(BlueprintCallable, Category = "Attributes",meta = (DisplayName = "Alive"))
static bool IsActorAlive(AActor* Actor);
//SAttributeComponent.cpp
USAttributeComponent* USAttributeComponent::GetAttributes(AActor* FromActor)
{
if (FromActor)
{
return Cast<USAttributeComponent>(FromActor->GetComponentByClass(USAttributeComponent::StaticClass()));
}
return nullptr;
}
bool USAttributeComponent::IsActorAlive(AActor* Actor)
{
USAttributeComponent* AttributeComp = GetAttributes((Actor));
if(AttributeComp)
{
return AttributeComp->IsAlive();
}
return false;
}
改善Bots的开火逻辑
//SBTTask_RangedTask.cpp
USBTTask_RangedTask::USBTTask_RangedTask()
{
MaxBulletSpread = 2.0f;
}
EBTNodeResult::Type USBTTask_RangedTask::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* MyController = OwnerComp.GetAIOwner();
if(ensure(MyController))
{
ACharacter* MyPawn = Cast<ACharacter>(MyController->GetPawn());
if(MyPawn == nullptr)
{
return EBTNodeResult::Failed;
}
FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");
AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor"));
if(TargetActor == nullptr)
{
return EBTNodeResult::Failed;
}
if(!USAttributeComponent::IsActorAlive(TargetActor))
{
return EBTNodeResult::Failed;
}
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);
return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}
return EBTNodeResult::Failed;
}
//SBTTask_RangedTask.h
protected:
UPROPERTY(EditAnywhere,Category="AI")
float MaxBulletSpread;
public:
USBTTask_RangedTask();
如果我们将弹丸的尺寸变大,在发生时会直接与bots发生碰撞,这时候我们在//SBTTask_RangedTask.cpp 中的
Params.Instigator = MyPawn;
就起到了作用,防止与bots自身碰撞
控制台
slomo 5
顿帧,可以加速运行时的速度
在行为树中,将最大偏移再次修改为8
为Bots添加被击效果(失败)
在之前的一次为角色添加被击效果时我就没搞定
//SAICharacter.cpp
ASAICharacter::ASAICharacter()
{
TimeToHitParamName = "TimeOfHit";
}
void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
float Delta)
{
GetMesh()->SetScalarParameterValueOnMaterials(TimeToHitParamName, GetWorld()->TimeSeconds);
}
//SAICharacter.h
protected:
UPROPERTY(VisibleAnywhere,Category="Effects")
FName TimeToHitParamName;
进入到Minion的材质中
更改连线如下图
Bots残血逃跑寻找掩体回血
//SBTService_CheckAttackRange.h
UPROPERTY(EditAnywhere, Category = "AI")
float MaxAttackRange;
//SBTService_CheckAttackRange.cpp
void USBTService_CheckAttackRange::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
//Check distance between ai pawn and target actor
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if(ensure(BlackboardComp))
{
AActor* TargetActor = Cast<AActor>(BlackboardComp->GetValueAsObject("TargetActor"));
if(TargetActor)
{
AAIController* MyController = OwnerComp.GetAIOwner();
APawn* AIPawn = MyController->GetPawn();
if (ensure(AIPawn))
{
float DistanceTo = FVector::Distance(TargetActor->GetActorLocation(), AIPawn->GetActorLocation());
bool bWithinRange = DistanceTo < MaxAttackRange;
bool bHasLOS = false;
if (bWithinRange)
{
bHasLOS = MyController->LineOfSightTo(TargetActor);
}
BlackboardComp->SetValueAsBool(AttackRangeKey.SelectedKeyName, (bWithinRange && bHasLOS));
}
}
}
}
USBTService_CheckAttackRange::USBTService_CheckAttackRange()
{
MaxAttackRange = 2000.f;
}
//SBTService_CheckHealth.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "SBTService_CheckHealth.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API USBTService_CheckHealth : public UBTService
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere,Category="AI")
FBlackboardKeySelector LowHealthKey;
UPROPERTY(EditAnywhere, Category = "AI", meta = (ClampMin="0.0", ClampMax="1.0"))
float LowHealthFraction;
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
public:
USBTService_CheckHealth();
};
//SBTService_CheckHealth.cpp
#include "AI/SBTService_CheckHealth.h"
#include "SAttributeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
USBTService_CheckHealth::USBTService_CheckHealth()
{
LowHealthFraction = 0.3f;
}
void USBTService_CheckHealth::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APawn* AIPawn = OwnerComp.GetAIOwner()->GetPawn();
if (ensure(AIPawn))
{
USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(AIPawn);
if (ensure(AttributeComp))
{
bool bLowHealth = (AttributeComp->GetHealth() / AttributeComp->GetHealthMax()) < LowHealthFraction;
UBlackboardComponent* BlackBoardComp = OwnerComp.GetBlackboardComponent();
BlackBoardComp->SetValueAsBool(LowHealthKey.SelectedKeyName, bLowHealth);
}
}
}
//SBTTask_HealSelf.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "SBTTask_HealSelf.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API USBTTask_HealSelf : public UBTTaskNode
{
GENERATED_BODY()
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
//SBTTask_HealSelf.cpp
#include "AI/SBTTask_HealSelf.h"
#include "AIController.h"
#include "SAttributeComponent.h"
EBTNodeResult::Type USBTTask_HealSelf::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
APawn* MyPawn = Cast<APawn>(OwnerComp.GetAIOwner()->GetPawn());
if(MyPawn == nullptr)
{
return EBTNodeResult::Failed;
}
USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(MyPawn);
if(ensure((AttributeComp)))
{
AttributeComp->ApplyHealthChange(MyPawn,AttributeComp->GetHealthMax());
}
return EBTNodeResult::Succeeded;
}
安装图下内容添加EQS 以及修改BTtree
优化Bots死亡布娃娃效果
//SGameplayFunctionLibrary.cpp
bool USGameplayFunctionLibrary::ApplyDirctionalDamage(AActor* DamageCauser, AActor* TargetActor, int DamageAmount,
const FHitResult& HitResult)
{
if(ApplyDamage(DamageCauser,TargetActor,DamageAmount))
{
UPrimitiveComponent* HitComp = HitResult.GetComponent();
if(HitComp && HitComp->IsSimulatingPhysics(HitResult.BoneName))
{
// Direction = Target - Origin
FVector Direction = HitResult.TraceEnd - HitResult.TraceStart;
Direction.Normalize();
HitComp->AddImpulseAtLocation(Direction * 300000.f, HitResult.ImpactPoint, HitResult.BoneName);
}
return true;
}
return false;
}