Notice
Recent Posts
Recent Comments
Link
«   2025/11   »
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
Tags
more
Archives
Today
Total
관리 메뉴

공부용 이모저모

UE4 - AI컨트롤러와 비헤이비어 트리 본문

UE4 - C++

UE4 - AI컨트롤러와 비헤이비어 트리

불타는버스 2021. 12. 2. 13:51

AIController와 내비게이션 시스템

 

NPC - 플레이어가 조종하지 않지만 스스로 행동하는 캐릭터를 의미

언리얼 엔진은 AI로 NPC를 조종하도록 AI 컨트롤러를 제공

ABAIController을 하나 만든다.

ABCharacter이 AI를 들고있도록 코드를 수정한다.

 

ABCharacter.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "ABCharacter.h"
...
#include "ABAIController.h"

// Sets default values
AABCharacter::AABCharacter()
{
	...
	AIControllerClass = AABAIController::StaticClass();
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

여기서 AIControllerClass와 AutoPossessAI는 Pawn이 기본으로 갖고있는 변수이다.

안에 AI관련이 추가되었다.변수로 추가하지 않았음에도 추가된것이 특이점.

AIController을 이용해 플레이어를 쫒아다니도록 수정한다.

주인공을 쫒아다니기 위해 내비게이션 메시를 깔기로 한다.

현재 레벨을 step3로 저장해둔다
볼륨 - 내비메시 바운드볼륨을 드래그해 월드에 배치한다.
0,0,0좌표에 10000/10000/500 사이즈로 설정한다.
뷰포트를 누르고 P를 누르면 내비메시가 표기된다.

ABAIController이 빙의한 폰에 목적지(컨트롤러가 조작하는 플레이어의 위치)를 알려줘 3초마다 이동하는 AI를 구현한다

우선 쫒아오기전에 랜덤으로 이동하는 캐릭터 AI 부터 구현하기로 한다

 

GetRandomPointInNavigableRadius : 이동가능한 내비매쉬 목적지를 랜덤으로 갖고온다

SimpleMoveToLocation : 목적지로 폰을 이동시킨다

 

UE4 4.20 버전 기준으로 NavigationSystem 클래스가 NavigationSystemV1로 바뀌었으므로 4.20이후 버전 기준으로

기재

 

ArenaBattle.Build.cs

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class ArenaBattle : ModuleRules
{
	public ArenaBattle(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "NavigationSystem"});

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

 

ABAIController.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "ABAIController.h"
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"

AABAIController::AABAIController()
{
	//3초마다 호출한다
	RepeatInterval = 3.0f;
}

void AABAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);
	//타이머,3초마다 OnRepeatTimer 호출
	GetWorld()->GetTimerManager().SetTimer(RepeatTimerHandle, this, &AABAIController::OnRepeatTimer, RepeatInterval, true);
}

void AABAIController::OnUnPossess()
{
	Super::OnUnPossess();
	GetWorld()->GetTimerManager().ClearTimer(RepeatTimerHandle);
}

void AABAIController::OnRepeatTimer()
{
	auto CurrentPawn = GetPawn();
	if (nullptr == CurrentPawn)
		return;

	//월드상에 배치된 내비메쉬를 얻어온다
	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
	if (nullptr == NavSystem)
		return;

	FNavLocation NextLocation;
	if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.0f, NextLocation))
	{
		//목표 위치 로그를 표기한다.
		UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, NextLocation.Location);
		UE_LOG(LogTemp, Warning, TEXT("Next Location: %s"), *NextLocation.Location.ToString());
	}
}

ABAIController.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "AIController.h"
#include "ABAIController.generated.h"

UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
	GENERATED_BODY()
	
public:
	AABAIController();
	virtual void OnPossess(APawn* InPawn) override;
	virtual void OnUnPossess() override;

private:
	void OnRepeatTimer();

	FTimerHandle RepeatTimerHandle;					//타이머
	float RepeatInterval;							//타이머 주기
};

3초마다 내비메시상에 있는 랜덤좌표로 이동하는 AI가 완성되었다

비헤이비어 트리 시스템

AI의 행동패턴을 보다 체계적으로 설계하기 위해 언리얼에서 제공하는 시스템

비헤이비어(NPC가 해야할 행동)를 분석하고 우선순위가 높은 행동부터 NPC가 실행하도록 해준다.

 

AI 폴더를 생성하고 인공지능 - 블랙보드로 BB_ABCharacter이라는 블랙보드 생성
비헤이비어 트린 BT로

 

블랙 보드 : 인공지능의 판단에 사용하는 데이터의 집합.NPC의 의사 결정은 블랙보드에 있는 데이터를 기반으로 진행.

비헤이비어 트리 : 블랙보드 데이터에 기반해 설계한 비헤이비어 트리의 정보를 저장한 애셋.

 

비헤이비어 트리를 더블클릭후 Wait 생성

Wait는 말그대로, 폰에게 지정 시간동안 대기하라는 명령을 내린다.

 

반드시 Composite  node를 통해서만 AI가 실행되는데,  대표적으로 Selector,Sequence 두 종류가 존재한다.

Sequence는 연결된 태스크가 False가 나올때까지 계속 실행시킨다.

 

계속 Wait를 지시하는 트리

ArenaBattle.Build.cs

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class ArenaBattle : ModuleRules
{
	public ArenaBattle(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "NavigationSystem", "AIModule"});

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

ABAIController.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "AIController.h"
#include "ABAIController.generated.h"

UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
	GENERATED_BODY()
	
public:
	AABAIController();
	virtual void OnPossess(APawn* InPawn) override;
private:
	UPROPERTY()
		class UBehaviorTree* BTAsset;
	UPROPERTY()
		class UBlackboardData* BBAsset;
};

AABAIController.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "ABAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"

AABAIController::AABAIController()
{
	static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject(TEXT("/Game/Book/AI/BB_ABCharacter.BB_ABCharacter"));
	if (BBObject.Succeeded())
		BBAsset = BBObject.Object;

	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject(TEXT("/Game/Book/AI/BT_ABCharacter.BT_ABCharacter"));
	if (BTObject.Succeeded())
		BTAsset = BTObject.Object;
}

void AABAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);
	if (UseBlackboard(BBAsset, Blackboard))
	{
		if (!RunBehaviorTree(BTAsset))
			UE_LOG(LogTemp, Warning, TEXT("AIController couldn't run behavior tree!"));
	}
}

비헤이비어 트리를 누르고 ALT + P로 게임을 실행시키면.비헤이비어 트리의 진행도를 볼 수 있다.
G블랙보드에 Vector 형태로 HomePos, PatrolPos를 만든다

HomePos : 캐릭터 생성위치

PatrolPos : 매번 이동할 위치

 

ABAIController.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "ABAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"

const FName AABAIController::HomePosKey(TEXT("HomePos"));
const FName AABAIController::PatrolPosKey(TEXT("PatrolPos"));

AABAIController::AABAIController()
{
	static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject(TEXT("/Game/Book/AI/BB_ABCharacter.BB_ABCharacter"));
	if (BBObject.Succeeded())
		BBAsset = BBObject.Object;

	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject(TEXT("/Game/Book/AI/BT_ABCharacter.BT_ABCharacter"));
	if (BTObject.Succeeded())
		BTAsset = BTObject.Object;
}

void AABAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);
	if (UseBlackboard(BBAsset, Blackboard))
	{
		//현재 로케이션 값을 블랙보드의 HpomePosKey에 저장시킨다
		Blackboard->SetValueAsVector(HomePosKey, InPawn->GetActorLocation());
		if (!RunBehaviorTree(BTAsset))
			UE_LOG(LogTemp, Warning, TEXT("AIController couldn't run behavior tree!"));
	}
}

ABAIController.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "AIController.h"
#include "ABAIController.generated.h"

UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
	GENERATED_BODY()
	
public:
	AABAIController();
	virtual void OnPossess(APawn* InPawn) override;
	
	static const FName HomePosKey;
	static const FName PatrolPosKey;
private:
	UPROPERTY()
		class UBehaviorTree* BTAsset;
	UPROPERTY()
		class UBlackboardData* BBAsset;
};

시뮬레이션 중에 HomePos가 바뀐걸 확인 할 수 있다.

 

ArenaBattle.Build.cs

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class ArenaBattle : ModuleRules
{
	public ArenaBattle(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "NavigationSystem", "AIModule", "GamePlayTasks"});

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, dadd it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

GameplayTasks를 선언해야한다.

 

BTTaskNode 상속으로 C++을 하나 만든다.이름은 BTTask_FindPatrolPos.

비헤이비어트리에서 블랙보드에 직접 값을 쓰도록 설계하기 위해 TaskNode를 만든다.

 

비헤이비어트리는 태스크를 실행할때마다 ExecuteTask라는 함수를 실행시킨다.

ExecuteTask 함수는 아래중 하나의 값을 반환한다.

 

Aborted : 태스크 실행중 중단됨(실패)

Failed : 수행했으나 실패함

Succeded : 수행하고 성공했다

InProgress : 태스크를 수행하고있다.실행 결과는 추후 알려줆.

 

함수의 실행결과에 따라, 다음 태스크로 수행할지, 중단할지가 결정됨.

시퀸스 컴포짓의 경우 태스크가 실패할때까지 계속하는 성질을 지니고있음.

 

ExecuteTask 함수에서 다음 정찰지점을 찾는 로직을 구현 하고, 실행결과를 반환하도록 만들어본다.

*태스크의 이름을 다른 이름으로 표시하고 싶으면 NodeName속성을 다른값으로 바꾸면 됨

 

BTTask_FindPatrolPos.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_FindPatrolPos.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTTask_FindPatrolPos : public UBTTaskNode
{
	GENERATED_BODY()
	
public:
	UBTTask_FindPatrolPos();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

BTTask_FindPatrolPos.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTTask_FindPatrolPos.h"
#include "ABAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "NavigationSystem.h"

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
	NodeName = TEXT("FindPatrolPos");
}

EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	auto ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn)
	{
		return EBTNodeResult::Failed;
	}

	//4.20 버전부터 UNavigationSystem은 UNavigationSystemV1로 바뀌었다.
	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(ControllingPawn->GetWorld());
	//내비 매쉬 못찾으면 False
	if (nullptr == NavSystem)
		return EBTNodeResult::Failed;

	FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(AABAIController::HomePosKey);
	FNavLocation NextPatrol;

	if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.0f, NextPatrol))
	{
		OwnerComp.GetBlackboardComponent()->SetValueAsVector(AABAIController::PatrolPosKey, NextPatrol.Location);
		return EBTNodeResult::Succeeded;
	}

	return EBTNodeResult::Failed;
}

비헤이비어 트리에 FindPatrolPos를 추가
PatrolPos로 MoveTo 설정.
5초단위로 FindPatrolPos를 거쳐서 정해진 값으로 MoveTo한다.HomePos는 그대로, 매번 Moveto 할때마다 PatrolPos가 바뀐다.

이번엔 플레이어를 쫒아다니도록 메커니즘을 조정한다.

Object 타입으로 Target을 추가하고, Key Type의 Base Class를 ABCharacter로 한다

주인공을 발견시 추격하고, 아닐시 정찰을 시켜야하므로 Selector 컴포짓을 사용해 로직을 확장한다.

추격을 더 우선권을 주고, 블랙보드의 Target을 향해 이동하도록 한다.

Sequence를 여러개로 쪼갠다. Move To에 블랙보드의 Target을 키로 삼는다.
C++ 클래스로 BTService를 부모로 하여 BTService_Detect를 생성

BTService_Detect.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "BehaviorTree/BTService.h"
#include "BTService_Detect.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTService_Detect : public UBTService
{
	GENERATED_BODY()
	
public:
	UBTService_Detect();

protected:
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};

BTService_Detect.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTService_Detect.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"

UBTService_Detect::UBTService_Detect()
{
	NodeName = TEXT("Detect");
	Interval = 1.0f;
}

void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn) return;

	UWorld* World = ControllingPawn->GetWorld();
	FVector Center = ControllingPawn->GetActorLocation();
	float DetectRadius = 600.0f;

	if (nullptr == World) return;
	TArray<FOverlapResult> OverlapResults;
	FCollisionQueryParams CollisionQueryParam(NAME_None, false, ControllingPawn);
	bool bResult = World->OverlapMultiByChannel(
		OverlapResults,
		Center,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(DetectRadius),
		CollisionQueryParam
	);

	DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}

비헤이비어 트리에 마우스 오른쪽, 서비스 추가 - Detect를 추가한다.

AI가 캐릭터를 포착할경우, 그게 우리가 조종하는 캐릭터인지 확인하기위해 컨트롤러를 체크할 필요가 있다.

플레이어를 찾을경우 Target값을 플레이어로, 없으면 null을 넣는다.

 

ABAIController.h

static const FName TargetKey; 추가

 

ABAIController.cpp

const FName AABAIController::TargetKey(TEXT("Target")); 추가

 

BTService_Detect.cpp

void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	...
	if (bResult)
	{
		for (auto OverlapResult : OverlapResults)
		{
			AABCharacter* ABCharacter = Cast<AABCharacter>(OverlapResult.GetActor());
			//컨트롤러가 플레이어의 컨트롤러인지
			if (ABCharacter && ABCharacter->GetController()->IsPlayerController())
			{
				OwnerComp.GetBlackboardComponent()->SetValueAsObject(AABAIController::TargetKey, ABCharacter);

				//반경을 보여준다
				DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Green, false, 0.2f);

				//
				DrawDebugPoint(World, ABCharacter->GetActorLocation(), 10.0f, FColor::Blue, false, 0.2f);
				DrawDebugLine(World, ControllingPawn->GetActorLocation(), ABCharacter->GetActorLocation(), FColor::Blue, false, 0.2f);
				return;
			} 
		}
	}
	else
	{
		//null로 설정해준다.
		OwnerComp.GetBlackboardComponent()->SetValueAsObject(AABAIController::TargetKey, nullptr);
	}

	DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}

범위안이 아니면 빨강,범위이면 초록으로 표기한다.

다음은 이동방향에 따라 회전도 자연스럽게 바뀌도록 코드를 좀 더 추가한다

 

ABCharacter.h

enum class EControlMode
{
	GTA,
	DIABLO,
	NPC			//NPC용 모드를 추가
};
	...
public:
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	virtual void PostInitializeComponents() override;
	virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;

	virtual void PossessedBy(AController* NewController) override;
    ...

ABCharacter.cpp

void AABCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	CurveTimeLine.TickTimeline(DeltaTime);
	SpringArm->TargetArmLength = FMath::FInterpTo(SpringArm->TargetArmLength, ArmLengthTo, DeltaTime, ArmLengthSpeed);

	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		break;
	case EControlMode::DIABLO:
		SpringArm->SetRelativeRotation(FMath::RInterpTo(SpringArm->GetRelativeRotation(), ArmRotationTo, DeltaTime, ArmRotationSpeed));
		if (DirectionToMove.SizeSquared() > 0.0f)
		{
			GetController()->SetControlRotation(FRotationMatrix::MakeFromX(DirectionToMove).Rotator());
			AddMovementInput(DirectionToMove);
		}
		break;
	case EControlMode::NPC:				//NPC모드 추가
		bUseControllerRotationYaw = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		GetCharacterMovement()->bOrientRotationToMovement = true;
		GetCharacterMovement()->RotationRate = FRotator(0.0f, 480.0f, 0.0f);
		break;
 	}
 }
 ...
 
 
void AABCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	if (IsPlayerControlled())
	{
		//플레이어는 디아블로 시점
		CurrentControlMode = EControlMode::DIABLO;
		SetControlMode();
		GetCharacterMovement()->MaxWalkSpeed = 600.0f;
	}
	else
	{
		CurrentControlMode = EControlMode::NPC;
		SetControlMode();
		//NPC를 주인공이 떨칠수 있게 속도를 조절한다
		GetCharacterMovement()->MaxWalkSpeed = 300.0f
	}
}

적당히 따돌릴수 있는 속도로 열심히 쫒아온다
블랙보드 데코레이터 추가
이쪽은 타겟이 있을경우에 오는 블랙보드이다. Value값, Target이 있는지 체크하여 쫒아간다.
반대쪽은 Key값이 Not Set 상태일때 들어오는것으로 조건을 건다.

NPC 공격기능 추가 

 

왼쪽 추격로직을 발전시켜서, 플레이어를 따라잡으면 공격하는 가능을 추가한다.

첫번째 행동은 거리로 구분하기 때문에, 한번더 분기가 필요하다.

 

시퀸스 2개를 만들어서 떼어놓는다.
BTDecorator 클래스 추가.이름은 BTDecorator_IsInAttackRange

BTDecorator_IsInAttackRange.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_IsInAttackRange.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTDecorator_IsInAttackRange : public UBTDecorator
{
	GENERATED_BODY()
	
public:
	UBTDecorator_IsInAttackRange();
protected:
	virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
		const override;
};

BTDecorator_IsInAttackRange.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTDecorator_IsInAttackRange.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTDecorator_IsInAttackRange::UBTDecorator_IsInAttackRange()
{
	NodeName = TEXT("CanAttack");
}

bool UBTDecorator_IsInAttackRange::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
	bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);

	auto ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();

	if (nullptr == ControllingPawn)
		return false;

	auto Target = Cast<AABCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AABAIController::TargetKey));
	if (nullptr == Target)
		return false;
	bResult = (Target->GetDistanceTo(ControllingPawn) <= 200.0f);
	return bResult;
}

 

데코레이터 추가에 IsInAttackRange를 검색하면 우리가 만든 클래스가 표시된다.
아직 공격하는건 만들지 않았으므로 Wait를 임의로 넣어둔다.
반대편에다가는 inversed 옵션으로 집어넣어놓고 CanNotAttack으로 수정
와서 공격하는 대신 대기한다
BTTaskNode로 BTTask_Attack을 추가

BTTask_Attack.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_Attack.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTTask_Attack : public UBTTaskNode
{
	GENERATED_BODY()
public:
	UBTTask_Attack();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;

protected:
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

private:
	bool IsAttacking = false;
};

BTTask_Attack.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTTask_Attack.h"
#include "ABAIController.h"
#include "ABCharacter.h"

UBTTask_Attack::UBTTask_Attack()
{
	//틱기능 활성화. Finish를 계속 체크한다.
	bNotifyTick = true;
	//아직 공격중이 아니다
	IsAttacking = false;
}

EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	auto ABCharacter = Cast<AABCharacter>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == ABCharacter)
		return EBTNodeResult::Failed;

	ABCharacter->Attack();
	IsAttacking = true;
	//람다식.ABCharacter이 AttackEnd Delegate를 호출하면 IsAttacking을 false로
	ABCharacter->OnAttackEnd.AddLambda([this]()->void {
		IsAttacking = false;
	});

	//일단 InProgress에서 머물게한다.공격이 끝나기 전까지 계속 지연시켜준다.
	return EBTNodeResult::InProgress;
}

//매 틱마다 공격태스크가 끝났는지를 체크한다.
void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
	//OnAttackEnd Delegate가 호출되면 Attacking이 false 되므로
	if (!IsAttacking)
	{
		//공격 태스크가 끝났음을 알려준다
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
	}
}

 

ABCharacter.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Components/TimelineComponent.h"
#include "ArenaBattle.h"
#include "GameFramework/Character.h"
#include "ABCharacter.generated.h"
enum class EControlMode
{
	GTA,
	DIABLO,
	NPC			//NPC용 모드를 추가
};

DECLARE_MULTICAST_DELEGATE(FOnAttackEndDelegate);

...
public:
//Called every frame
...
void Attack();				//public으로 바꿔준다
FOnAttackEndDelegate OnAttackEnd;
...

ABCharacter.cpp

void AABCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterruped)
{
	ABCHECK(IsAttacking);
	IsAttacking = false;
	AttackEndComboState(); 
	OnAttackEnd.Broadcast();		//AttackEnd의 BroadCast호출
}

기존의 Wait Task를 지우고 Attack Task를 추가한다.
다가와서 공격하는 AI

공격 할 때, 타겟을 향해 회전하면서 공격하도록 하고싶다.

BTTaskNode로 TurnToTarget을 추가한다

BTTask_Node로 BTTask_TurnToTarget 추가

BTTask_TurnToTarget.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_TurnToTarget.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTTask_TurnToTarget : public UBTTaskNode
{
	GENERATED_BODY()
	
public:
	UBTTask_TurnToTarget();
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

BTTask_TurnToTarget.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTTask_TurnToTarget.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_TurnToTarget::UBTTask_TurnToTarget()
{
	NodeName = TEXT("Turn");
}

EBTNodeResult::Type UBTTask_TurnToTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	auto ABCharacter = Cast<AABCharacter>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == ABCharacter)
		return EBTNodeResult::Failed;

	auto Target = Cast<AABCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AABAIController::TargetKey));
	if (nullptr == Target)
		return EBTNodeResult::Failed;

	//타겟의 위치와 현재위치를 빼서 방향벡터를 만든다
	FVector LockVector = Target->GetActorLocation() - ABCharacter->GetActorLocation();
	LockVector.Z = 0.0f;
	FRotator TargetRot = FRotationMatrix::MakeFromX(LockVector).Rotator(); //방향벡터로부터 각도를 얻어낸다(내적?)
	//각도만큼 2초에 걸쳐서 회전시킨다.
	ABCharacter->SetActorRotation(FMath::RInterpTo(ABCharacter->GetActorRotation(), TargetRot, GetWorld()->GetDeltaSeconds(), 2.0f));

	return EBTNodeResult::Succeeded;
}

기존에 시퀸스대신 Simple Parallel을 생성한다.
Simple Parallel 로 바꾸고 Attack와 Turn을 동시에 실행시키도록 세팅한다.
살짝이지만 공격하기전에 각도를 쫒아온다.