本节是为敌人增加简单的AI功能,并没有涉及UPawnSenseComponent,意思就是不可以用听觉或者视觉发现主角的存在。
也不会为主角增加UPawnNoiseEmitterComponent,来制造噪音,只是简单的走向主角并发动攻击。
第一步,我们先新建一个行为树和黑板。
在行为树中挂载黑板
在黑板中新建一个Object类型的变量,其父类选择为Actor。
第二步,在主角类中增加一个UBehaviorTree类型的变量,该变量不是给主角用的,因为敌人类继承自主角,所以为了让敌人类拥有行为树,在主角代码中需要增加。同时增加一个EnemyAttack函数(设置其在蓝图中完成),因为主角发动攻击是通过鼠标,敌人却不是EnemyAttack就是控制敌人如何发起攻击。
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shinbi | AI")
class UBehaviorTree* Trees;
UFUNCTION(BlueprintImplementableEvent)
void EnemyAttack();
第三步,新建一个C++控制器类,继承自AIController,该控制器的作用是控制敌人。
这是敌人控制器的头文件:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree\Blackboard\BlackboardKeyAllTypes.h"
#include "BehaviorTree/BlackboardData.h"
#include "EnemyController.generated.h"
/**
*
*/
UCLASS()
class SHIBI_API AEnemyController : public AAIController
{
GENERATED_BODY()
public:
AEnemyController();
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category = "Shinbe | AI")
class UBehaviorTreeComponent* AIBehaviorTree;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Shinbe | AI")
class UBlackboardComponent* AIBlackboard;
private:
virtual void OnPossess(APawn* InPawn) override;
};
这是敌人控制器的CPP文件:
// Fill out your copyright notice in the Description page of Project Settings.
#include "EnemyController.h"
#include "Shinbi.h"
#include "Engine.h"
AEnemyController::AEnemyController() {
AIBehaviorTree = CreateDefaultSubobject<UBehaviorTreeComponent>(TEXT("AIBehaviorTree"));
AIBlackboard = CreateDefaultSubobject<UBlackboardComponent>(TEXT("AIBlackboard"));
}
void AEnemyController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
AShinbi* enemy = Cast<AShinbi>(InPawn);
AIBlackboard->InitializeBlackboard(*enemy->Trees->BlackboardAsset);
AIBehaviorTree->StartTree(*enemy->Trees);
GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, TEXT("AI Initialized!"));
}
在CPP文件中,我们初始化了黑板,运行了行为树。
我们新建一个BTService进行行为树的刷新。
这是BTService的头文件:需要重写父类的TickNode方法。
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "EnemyController.h"
#include "MyBTService.generated.h"
/**
*
*/
UCLASS()
class SHIBI_API UMyBTService : public UBTService
{
GENERATED_BODY()
public:
UMyBTService();
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)override;
};
这是CPP文件:
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyBTService.h"
#include "Shinbi.h"
UMyBTService::UMyBTService() {
bCreateNodeInstance = true;
}
void UMyBTService::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
AEnemyController * EnemyController = Cast<AEnemyController>(OwnerComp.GetAIOwner());
AShinbi* character = Cast<AShinbi>(GetWorld()->GetFirstPlayerController()->GetPawn());
EnemyController->AIBlackboard->SetValueAsObject("Target", character);
UE_LOG(LogTemp, Warning, TEXT("target is set!"));
}
功能目的是不断的将主角信息作为参数设置到黑板中。
在行为树中增加一个Sequence,在下面点击反键加载刚才写好的BTService。
第四步,让敌人走向主角。
新建一个BTTask_BlackboardBase任务。
以下是头文件:要重写父类的ExecuteTask方法。
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTT_ChaseActor.generated.h"
/**
*
*/
UCLASS()
class SHIBI_API UBTT_ChaseActor : public UBTTask_BlackboardBase
{
GENERATED_BODY()
public:
UBTT_ChaseActor();
EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
以下是CPP文件:
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTT_ChaseActor.h"
#include "EnemyController.h"
#include "Shinbi.h"
#include "Engine.h"
UBTT_ChaseActor::UBTT_ChaseActor() {
bCreateNodeInstance = true;
}
EBTNodeResult::Type UBTT_ChaseActor::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AEnemyController* enemyController = Cast<AEnemyController>(OwnerComp.GetAIOwner());
if (enemyController) {
AShinbi* target = Cast<AShinbi>(enemyController->AIBlackboard->GetValueAsObject("Target"));
enemyController->MoveToActor(target,20);
return EBTNodeResult::Succeeded;
}
else {
return EBTNodeResult::Failed;
}
}
第五步,让敌人攻击主角。
再新建一个BTTask_BlackboardBase任务。
以下是头文件:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "AIAttack.generated.h"
/**
*
*/
UCLASS()
class SHIBI_API UAIAttack : public UBTTaskNode
{
GENERATED_BODY()
public:
UAIAttack();
EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
以下是CPP文件:
// Fill out your copyright notice in the Description page of Project Settings.
#include "AIAttack.h"
#include "Shinbi.h"
#include "EnemyController.h"
UAIAttack::UAIAttack() {
bCreateNodeInstance = true;
}
EBTNodeResult::Type UAIAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AEnemyController* enemyController = Cast<AEnemyController>(OwnerComp.GetAIOwner());
AShinbi* enemy = Cast<AShinbi>(enemyController->GetPawn());
if (enemy) {
enemy->EnemyAttack();
return EBTNodeResult::Succeeded;
}
else
{
return EBTNodeResult::Failed;
}
}
此时通过任务文件挂载敌人的攻击事件,之前说过敌人蓝图是继承自主角的,这就是为什么需要在主角中增加一个EnemyAttack方法。
第六步,回到行为树,挂载这些任务。
第七步,完善激活敌人的EnemyAttack功能。
进入敌人的蓝图,增加以下连接。
这里没有写WaitGameplayEvent,是因为我们用的招同样是主角的。我们将主角的招式复制一份,挂载到敌人的TryActivateAbilityByClass模块中。
这时候,敌人已经可以完成寻找主角到攻击主角的这一过程。
但是主角不会有任何损伤。
为了更加清晰标记主角与敌人,在主角中增加一个属性,TeamID,用于区别敌我身份。
将本地控制器控制的主角TeamID设置为0。其他的设置为255或者其他数值(该赋值过程在构造函数中完成,这里省略)。
void AShinbi::ArrangeIDbyController()
{
if (GetController() && GetController()->IsPlayerController()) {
TeamID = 0;
}
}
再创建一个方法,用于根据TeamID属性来判断敌我。
bool AShinbi::isOtherHostage(AActor* other)
{
AShinbi* others = Cast<AShinbi>(other);
return this->TeamID != others->TeamID;
}
当两者TeamID数值不同的时候,返回真值。意思就是不同的人物角色。
第八部,进入主角蓝图进行修改。
增加主角RootComponent碰撞体的Overlap事件。让主角受到攻击时候主角承担伤害。
此时主角已经可以承担伤害了。
但是又出现新问题,当主角被杀之后,抛出错误。是因为控制器丧失了要控制的角色造成的。
最后一步,在主角中增加一个方法。
void Dead();
该方法就是用于判断主角控制器与AI控制器在控制的角色死亡消失之后该怎么办的。
void AShinbi::Dead()
{
APlayerController* localController = Cast<APlayerController>(GetController());
if (localController) {
localController->DisableInput(localController);
}
AEnemyController* enmeyController = Cast<AEnemyController>(GetController());
if (enmeyController) {
enmeyController->GetBrainComponent()->StopLogic("Dead");
}
}
该方法中禁止了玩家的控制器,以及AI控制器。
这里需要导入相关头文件,例如BrainComponent.h等,可以根据自己情况进行添加。
void AShinbi::onDamage(float Health, float MaxHealth) {
float temp = Health / MaxHealth;
UUserWidget* tobeCast = widget->GetUserWidgetObject();
UHealthWidget* bp123 = Cast<UHealthWidget>(tobeCast);
bp123->healthBar->SetPercent(temp);
if (Health <= 0) {
isDead = true;
Dead();
onDie();
}
}
将Dead方法放置在执行死亡动画之前。
全部步骤完成,效果如下:
主角死亡,并未报错。