AI Controller/Blackboard/BehaviorTree 이 셋은 적을 실제 게임처럼 움직이도록 구현하기 위해 필요한 것들이다.
기본적으로 Character 클래스는 AI Controller Class 를 설정할 수 있다.
BP_EnemyBase 에서 AI Controller 를 검색하면 확인해볼 수 있다.

생성경로: public/AI



추가
AI관련된 기능 사용을 위해서는AIModule를Build.cs파일에 추가해야함PrivateDependencyModuleNames.AddRange(new string[] { "...", "AIModule" });
AuraAIController 가 Blackboard , BehaviorTree 를 사용할 수 있도록 추가한다.
#include "..."
class UBlackboardComponent;
class UBehaviorTreeComponent;
UCLASS()
class AURA_API AAuraAIController : public AAIController
{
GENERATED_BODY()
public:
AAuraAIController();
protected:
UPROPERTY()
TObjectPtr<UBlackboardComponent> BlackboardComponent;
UPROPERTY()
TObjectPtr<UBehaviorTreeComponent> BehaviorTreeComponent;
};
#include "..."
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
AAuraAIController::AAuraAIController()
{
BlackboardComponent = CreateDefaultSubobject<UBlackboardComponent>("BlackboardComponent");
check(BlackboardComponent);
BehaviorTreeComponent = CreateDefaultSubobject<UBehaviorTreeComponent>("BehaviorTreeComponent");
check(BehaviorTreeComponent);
}
AuraAIController 를 사용할 수 있도록 AuraEnemy 에 코드를 추가해준다.
#include "..."
class ...;
class UBehaviorTree;
class AAuraAIController;
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacterBase, public IEnemyInterface
{
GENERATED_BODY()
public:
AAuraEnemy();
/** 코드 추가 : 생성자 바로 아래 추가 */
virtual void PossessedBy(AController* NewController) override;
/** 코드 추가 */
...
protected:
...
UPROPERTY(EditAnywhere, Category = "AI")
TObjectPtr<UBehaviorTree> BehaviorTree;
UPROPERTY()
TObjectPtr<AAuraAIController> AuraAIController;
};
#include "..."
#include "AI/AuraAIController.h"
...
void AAuraEnemy::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
AuraAIController = Cast<AAuraAIController>(NewController);
}
...
컴파일 후 에디터에서 AuraAIController 기반 BP_AuraAIController 블루프린트 를 생성하고
생성경로: Blueprints/AI


BP_EnemyBase 에 생성한 BP_AuraAIController 를 적용시킨다.

이제 AuraAIController 가 사용할 BlackBoard 와 BehaviorTree 블루프린트를 생성한다.
생성경로: Blueprints/AI
먼저 Blackboard 블루프린트인 BB_EnemyBlackboard 를 생성하고


동일한 경로에 동일한 방식으로 BehaviorTree 블루프린트인 BT_EnemyBehaviorTree 를 생성한다.

BP_EnemyBase 에서 BT_EnemyBehaviorTree 를 사용하도록 설정한다.

적이 BehaviorTree 를 실행시키기 위한 코드를 작성한다.
#include "..."
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
...
void AAuraEnemy::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
/** 코드 추가 */
// 클라이언트 컨트롤러는 서버측에서 소유 및 관리하므로
// 클라이언트측일 경우 return;
if(!HasAuthority()) return;
/** 코드 추가 */
AuraAIController = Cast<AAuraAIController>(NewController);
/** 코드 추가 */
AuraAIController->GetBlackboardComponent()->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
AuraAIController->RunBehaviorTree(BehaviorTree);
/** 코드 추가 */
}
...
추가
GetBlackboardComponent()의 정의를AIController.h에서 확인할 수 있는데, 해당 함수는UBlackboardComponent포인터BlackBoard를 반환한다.
즉AuraAIController에서 굳이Blackboard를 선언할 필요가 없으므로 해당 코드를 삭제해도 된다.
- AuraAIController.h
#include "..." UCLASS() class AURA_API AAuraAIController : public AAIController { GENERATED_BODY() public: ... protected: /** 코드 삭제 */ UPROPERTY() TObjectPtr<UBlackboardComponent> BlackboardComponent; /** 코드 삭제 */ ... };
- AuraAIController.cpp
#include "..." AAuraAIController::AAuraAIController() { /** 코드 수정 : BlackboardComponent -> Blackboard */ Blackboard = CreateDefaultSubobject<UBlackboardComponent>("BlackboardComponent"); check(BlackboardComponent); /** 코드 수정 */ ... }
컴파일 후 BT_EnemyBehaviorTree 에서 아래 그림과 같이 트리를 구성해준다.

재생할 애니메이션을 추가하고

서버측 에서 실행하면 애니메이션이 재생되는 것을 확인할 수 있다.

(클라이언트측에서는 애니메이션 재생 안됨)

BehaviorTree 기본적으로 Root Node 가 있고, 해당 노드에 여러 노드를 부착할 수 있다.
Select Node 는 왼쪽에서 오른쪽으로 자식 노드를 실행하는 특성을 가지고 있다.Ex) 첫번째 자식노드 실패 -> 부모인 Selector에게 전달
-> 두번째 자식노드 성공 -> 부모인 Selector에게 전달 -> 세번째 자식노드 실행xSelector 는 Composite Node 라고도 할 수 있으므로 특정 타입( Decorator , Service )의 노드를 부착할 수 있다.

Service 는 Selector와 함께 작동하여 특정 역할을 수행하게 된다. Service는 주기적으로 또는 조건이 충족될 때마다 Behavior Tree의 상태를 검사하고 업데이트하는 노드Service 를 Selector 에 부착시켜 동작시키는 방법도 있지만, 직접 고유의 Service 를 생성하는 것도 가능하다.
BTService_BlueprintBase 기반 BTx_FindNearestPlayer 블루프린트를 생성한다.
생성경로: Blueprints/AI


BTS_FindNearestPlayer 를 살펴보면 특정 액션을 기본으로 0.5초의 Interval 을 두고 0.1초의 Random Deviation 차를 두고 실행하도록 되어 있는 것을 확인할 수 있다.
즉 0.4~0.6 의 랜덤한 간격을 두고 실행됨

Custom Description 에 설명과 노드명을 추가하고

BT_EnemyBehaviorTree 에 커스텀으로 생성한 BTS_FindNearestPlayer 를 부착시킨다.


BTS_FindNearestPlayer 의 Function 탭에서 Override 클릭 후 Receive Tick AI 를 선택하고, Owner Controller 를 뷰포트에 출력하도록 노드를 구성하여 실행하면 현재 적용중인 Controller 를 확인할 수 있고


Controlled Pawn 을 뷰포트에 출력하도록 노드를 구성하면 AIController 를 소유하고 있는 폰을 확인할 수 있다.

정상적으로 컨트롤러를 소유하는 것을 확인했으니, 근처의 플레이어를 찾는 기능을 BTS_FindNearestPlayer 에 구현해야 한다.
플레이어의 위치를 찾고, 거리를 확인하려면 위치와 거리에 대한 변수가 필요하다.
BehaviorTree 는 변수를 따로 저장하지 않고, Blackboard 가 해당 역할을 하게 된다.
즉 Blackboard 에서 추가한 키를 BehaviorTree 에서 접근해 사용 가능하다.
Blackboard 에 선언된 키
BehaviorTree 에서 접근해 사용 가능해진 키
키 관련 간략한 설명
1.Key라는 이름처럼 특정 타입의 키를 생성하면 해당 키에 대한 동일한 타입의Value를 소유
2.Blackboard에 특정 타입의 키를 생성기본적으로 SelfActor 라는 키를 가지고 있음
- 직접 생성한
Behavior Tree Service에서 동일하게 변수 생성Blockboard Key Selector 타입의 SelfActorKey- 해당 변수를 public화
Behavior Tree의 키를 직접 생성한Service에서 만든 변수에 설정함으로써Service에서 키에 대한 접근 가능
Print String노드를 통해 뷰포트에서Self Actor가 무엇을 가리키는지 확인 가능하다.
추가
확인용으로 생성한 변수이므로BTS_FindNearestPlayer에서SelectActorKey삭제
Event Recieve Tick AI노드 제외 삭제
먼저 Player 를 찾기 위해 BP_EnemyBlackboard 에서 Object 타입의 TargetToFollow 키를 추가한다.


추가로 Key Type -> Base Class : Actor 로 지정하여 해당 키가 Actor 클래스만 지정할 수 있도록 한다.

추가
Instance Synced는Blackboard Key값을 AI 캐릭터 인스턴스 간에 동기화가 필요할 때 사용한다. 활성화시, 같은Blackboard Key를 사용하는 모든 AI 인스턴스가 해당 Key값을 공유하거나 일관되게 유지하려고 한다.
이어서 타겟과의 거리를 확인하기 위한 float 타입의 DistanceToTarget 키도 추가한다.

키를 추가하였으므로 BTS_FindNearestPlayer 에서 키에 대한 접근이 가능해졌다.
키를 생성하였으므로 FindNearestPlayer 를 구현할 차례이다.
우선 모든 플레이어에 대한 정보를 가져와야 하는데, 태그를 통해 가져올 수 있도록 한다.
( Enemy 태그를 가진 경우 플레이어가 아니므로 정보를 가져오지 않게 하기 위함)
BP_EnemyBase 에서 Enemy 태그를 추가한다.

BP_AuraCharacter 에는 Player 태그를 추가한다.

BT_EnemyBehaviorTree 에서 생성한 키를 BTS_FindNearestPlayer 에서 접근할 수 있도록 Blackboard Key Selector 타입 변수를 2개 추가하고

태그명 확인을 위한 Name 타입 변수를 하나 추가한다.

컨트롤러를 소유하는 폰의 Tag 가 Player 인지 확인하고, Branch 를 통해 참일 경우
Player 의 타겟인 Enmey 가 TargetTag 가 되도록 하고 거짓일 경우
Enemy 의 타겟인 Player 가 TargetTag 가 되도록 한다.
위 과정을 통해 TargetTag 를 통해 적을 특정할 수 있게 됬다.
노드를 묶어 SetTargetTag 함수로 리팩토링하고, Inputs 의 파라미터명을 Controlled Pawn 으로 변경한다.



TargetTag 설정이 완료됬다면, 해당 태그를 소유한 모든 Actor 를 가져와 루프를 통해 뷰포트에 출력하도록 노드를 구성한다.

컴파일 후 실행시 정상적으로 플레이어가 출력된다.

Player 수가 2이므로 0과 1 표시, 실행할 때 플레이어수 1명으로 줄이면 0만 표시된다.

노드를 묶어 FindNearestPlayer 함수로 리팩토링한다.
TODO: 여기서부터 재정리 필요


FindNearestPlayer 함수에서 가장 가까운 플레이어 발견시, 대상을 할당할 Actor : Object Reference 타입 지역변수를 하나 생성하고


ClosestActor 에 null값을 할당하도록 노드를 추가해 연결한다.

대상과의 거리를 할당하기 위한 float 타입 지역변수 ClosestDistance 도 생성하고

동일하게 ClosestDistance 에 높은 값을 할당하도록 노드를 추가해 연결한다.

첫비교시 999999999와 비교하면 무조건 작으므로 `ClosestDistance` 가 변경됨
루프를 통해 다시 비교하여 더 작은 값이 있으면 갱신
타겟(플레이어)과 적( Controlled Pawn )의 거리를 구하기 위해 FindNearestPlayer 함수에 Pawn : Object Reference 타입의 OwningPawn input 을 추가해주고 Event Recieve Tick AI 노드의 Controlled Pawn 핀과 연결시킨다.



다시 FindNearestPlayer 함수로 돌아와, 각 요소에 대한 유효성을 확인하도록 IsValid 노드를 추가하고

ControlledPawn 에 대한 유효성도 확인하도록 노드를 생성한다.

ControlledPawn(적) 과 ArrayElement(플레이어)의 유효성 검사를 진행한 후 둘 사이의 거리를 구하기 위함
둘 사이의 거리를 확인하기 위해 Get Distance To 노드를 생성하여 연결하고

둘 사이의 거리를 ClosestDistance 와 비교하여 더 작을 경우 해당 값으로 변경하도록 노드를 구성한다.

ClosestActor 를 가장 가까운 대상으로 변경하도록 동일하게 노드를 구성한다.
(선이 너무 많아지므로 Actor 로 지역변수 승격후 사용)



ClosestDistance 와 ClosesetActor 값을 Blackboard 로 넘겨야 한다.
Set Blackboard Value as Object 노드를 통해 대상 관련 Key 와 Value 를 할당하고

동일하게 Set Blackboard Value as Float 노드를 통해 거리 관련 Key 와 Value 를 연결한다.

TargetToFollow_Selector 와 DistanceToTarget_Selector 를 public화하여 접근가능하게 해주고

BT_EnemyBlackboard 의 Find Nearest Player 를 선택하고 링크를 생성할 수 있도록 설정해준다.
( Find Nearest Player 를 선택해야 설정 가능)


이제 Service 를 통해 매 인터벌마다 TargetToFollow 와 DistanceToTarget 이 갱신되고, 이를 이용할 수 있다.
Selector 노드에서 Move To 노드를 생성하여 연결하고, Blackboard Key : TargetToFollow 로 설정한다.



컴파일 후 실행하면 정상적으로 캐릭터를 향해 움직이는 것을 확인할 수 있다.

추가
이슈 해결
1. 적이 아이템을 획득하는 문제 해결
GameplayEffect가 적에게도 적용되는 문제때문에 발생.
다만 단순히 적용시키지 않도록 하기에는FireArea와 같이 게임상 적에게 데미지를 줘도 무방한GameplayEffect까지 미적용될 수 있으므로 이점을 생각해서 수정이 필요.
BP_EnemyBase를 확인해보면BP_EnemyBase기반 적들은 모두Enemy태그를 소유하고 있음.
해당 태그 보유시 특정GameplayEffect가 적용되지 않도록 코드를 수정
- AuraEffectActor.h
#include "..." ... UCLASS() class AURA_API AAuraEffectActor : public AActor { GENERATED_BODY() public: ... protected: ... /** 코드 수정 : 변수명 수정 */ /** bDestroyOnEffectApplication으로 변경 */ /** "Effect가 적용되면 파괴" 라고 해석하면 될듯 함 */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects") bool bDestroyOnEffectApplication = false; /** 코드 수정 */ /** 코드 추가 */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects") bool bApplyEffectsToEnemies = false; /** 코드 추가 */ ... };
- AuraEffectActor.cpp
#include "..." ... // 추가수정 : Parameter가 그냥 Actor일 경우 TargetActor로 수정 void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass) { // 태그를 통해 TargetActor가 적이고, bApplyEffectsToEnemies 비활성화시 리턴 // Ex) 적이 FireArea(적에게 적용시키기 위해 활성화)위에 있을 경우 데미지받아야되므로 if문 실행x if(TargetActor->ActorHasTag(FName("Enemy")) && !bApplyEffectsToEnemies) return; ... // bDestroyOnEffectApplication활성화시키면, Effect 적용시 삭제 // bDestroyOnEffectApplication && EffectSpecHandle.Data.Get()->Def.Get()->DuratinoPolicy == EGameplayEffectDurationType::Instant 조건문은 instant인 경우만 삭제 // 왜 기껏 변수 민들어놓고 bDestoryOnEffectApplication 안쓰는지 모르겠음 // 쓸거면 bDestoryOnEffectApplication && !bIsInfinite해야되는거 아닌가? if(!bIsInfinite) { Destroy(); } } ... void AAuraEffectActor::OnOverlap(AActor* TargetActor) { if(TargetActor->ActorHasTag(FName("Enemy")) && !bApplyEffectsToEnemies) return; ... } ... void AAuraEffectActor::OnEndOverlap(AActor* TargetActor) { if(TargetActor->ActorHasTag(FName("Enemy")) && !bApplyEffectsToEnemies) return; ... } ...컴파일 후
BP_FireArea에서ApplyEffectsToEnemies를 활성화시키고
포션과 크리스탈 전부DestroyOnEffectApplication를 활성화시킨 다음Event Graph에서Destroy노드를 삭제
(파라미터 수정시Apply Effect To Target노드 refresh후Other Actor재연결 필요)
실행시 더이상 적이 아이템을 획득하지 못함
!!!테스트용으로 설치해둔 콜리젼 박스의 경우 태그관련 안되있어서 오류발생!!!
2. 적이 움직일 때 스무스한 회전이 아닌 스냅핑(회전을 각지게 하는)을 하는 문제 해결
BP_EnemyBase에서UseControllerRotationYaw비활성화 후
Components패널의CharacterMovement선택하고UseControllerDesiredRoataion활성화 및RotationRate.Z : 360으로 설정
코드도 수정
- AuraEnemy.cpp
#include "..." AAuraEnemy::AAuraEnemy() { ... AbilitySystemComponent->...; /** 코드 추가 */ bUseControllerRotationPitch = false; bUseControllerRotationYaw = false; bUseControllerRotationRoll = false; GetCharacterMovement()->bUseControllerDesiredRotation = true; /** 코드 추가 */ ... }
Decorator 는 Service 와 동일하게 노드에 부착할 수 있고, 조건문의 역할을 한다.
우선 기존의 Move To 노드를 삭제하고 새로운 Selector 를 생성하여 연결한다.

Selector 우클릭 -> Add Decorator -> Blackboard 를 선택하여 Blackboard 기반 조건문을 이용할 수 있도록 한다.

Details 탭에서 여러 조건을 설정할 수 있다.
Decorator 의 결과가 변경된 경우Decorator 결과와 상관없이 키값이 변경된 경우Decorator 에 의해 중단될 때 중단 대상을 지정Decorator 를 소유한 노드만 중단 Decorator 를 소유한 노드의 자식 노드만 중단Decorator 를 소유한 노드 및 자식 노드 전부 중단적이 원거리타입( Goblin_Range )인지 근거리타입( Goblin_Spear )인지 확인을 위한 노드를 구성하고 타입에 따라 일정거리만큼 이동하도록 트리를 구성할 것이다.
첫번째 조건은 추적 대상( TargetToFollow )에 대해 설정( Is Set ) 되어있는지 확인하는 Decorator 의 결과가 변경될 때 트리거되도록 한다.


두번째 조건은 피격 중일 때 움직이면 안되므로 이에 대한 체크가 필요하다.
Blackboard 에서 bool 타입 HitReacting 키를 생성하고

기본값으로 false 를 가지도록 코드를 추가한다.
그리고 HitReact 발생시 해당 값을 변경하도록 콜백함수를 추가한다.
#include "..."
...
void AAuraEnemy::PossessedBy(AController* NewController)
{
...
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), false);
}
...
void AAuraEnemy::HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
...
// HitReacting 값이 변경될 때 마다 해당 값으로 변경
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), bHitReacting);
}
...
키의 기본값을 설정하였으므로 Blackboard Decorator 를 추가한다.

HitReacting 이 Is Not Set 인 경우에 노드가 실행되도록 조건을 수정한다.


현재 적은 두 종류가 있는데, 첫번째는 가까이에서 공격하는 Goblin_Spear 이고, 두번째는 멀리서 공격하는 Goblin_Ranger 다.
구분하기 위해 bool 타입 RangedAttacker 키를 추가한다.

Selector 노드에서 Sequnce 노드 3개를 자식노드로 생성한다.
Sequence와 Selector의 차이
* Selector : 자식 노드중 하나라도 성공하면 성공으로 평가
* Sequence : 모든 자식노드가 성공해야 성공으로 평가

RangedAttacker 설정을 위한 코드를 작성한다.
#include "..."
...
void AAuraEnemy::PossessedBy(AController* NewController)
{
...
// Elementalist와 Ranger 둘다 원기리이므로 Warrior가 아닐 경우 원거리
AuraAIController->GetBlackboardComponent()->SetValueAsBool(FName("RangedAttacker"), CharacterClass != ECharacterClass::Warrior);
}
...
Ranged Attacker Sequence 에 Blackboard Decorator 를 추가하고, RangedAttacker 가 Is Set 인 경우를 조건으로 추가한다.


RangedAttacker 가 Is Set 되어 있는 상태이므로 조건을 만족하여 추후에 추가할 첫번째 Sequence 의 하위 노드를 진행하게 된다.
원거리일 경우 RangedAttacker 는 true 라는 값으로 변경되고(기본적으로 false)
중단 조건에 상관없이 중단 대상이 None 이므로 자식 노드를 실행한다.
또한 첫번재 Sequence 의 성공 평가가 부모 Selector 에게 전달되고, Selector 의 특성상 자식 노드중 하나라도 성공이면 성공 평가를 내려 나머지 자식 노드를 실행하지 않게 된다.
실패시 두번째 Sequence 로 넘어가게 된다.
Melee Attacker Sequence 에도 Blackboard Decorater 를 추가하고, DistanceToTarger 이 Is Less Then 인 경우를 조건을 추가하여 거리차가 지정한 조건보다 크지 않을 경우 실패하게 된다.


실패시 세번째 Sequence 로 넘어가게 된다.
두번재 Sequence 실패는 플레이어와 멀리 떨어져있다는 것을 의미하므로 약간의 딜레이 이후 플레이어를 향해 이동하도록 Move To 노드를 추가한다.



동일하게 Decorator 를 추가하는데, 두가지 조건이 필요하다.
첫번째로 플레이어에게 접근하기 위해 충분히 가까이 있는지 확인해야 하고
(너무 멀 경우 추적을 하지 않도록 하기 위함)


두번째로 플레이어와 일정 거리보다 멀리 있어야 한다.
(원거리의 경우 사거리 내에 플레이어가 있으면 이동하는 것이 아닌 공격을 해야함, 근거리의 경우 이동 관련 노드를 Melee Attacker Sequence 에 추가 예정)


Decorator 명을 이해하기 쉽게 수정한다.

- 추가1
나머지Sequence의Decorator명도 변경
첫번째Sequence: Am I a Ranged Attacker?
두번째Sequence: Am I Close Enough to Attack?
- 추가2
첫번째Sequence에Decorator하나 더 추가
Am I Close Enough to Attack?
DistanceToTarget
600보다 작거나 같을 경우
요약
1. Root의 자식 Selector 추가
조건1: 타겟(플레이어 설정시), 조건2: 피격도중이 아닐 경우
2. 조건이 참일 경우 첫번째 자식 Sequence(Ranged Attacker) 진행
조건1: RangedAttacker 의 Is Set 여부, 조건2: 공격하기 충분히 가까운 거리(600)내에 있을 경우
600내에 있을 경우 공격 관련 노드를 실행하므로 원거리 공격이 됨
3. 첫번째 자식 Sequence(Ranged Attacker) 실패시 두번째 Sequence(Melee Attacker) 진행
조건: 타겟(플레이어)와의 거리가 500미만인 경우
500내에 있을 경우 공격 관련 노드를 실행하므로 근거리 공격이 됨
4. 조건이 참일 경우 두번째 자식 Sequence(Melee Attacker) 진행
5. 두번째 자식 Sequence(Melee Attacker) 실패시 세번재 Sequence(Move to Target) 진행
조건1: 타겟(플레이어가) 특정 거리 내에 있을 경우, 조건2: 타겟(플레이어)가 공격사거리 밖에 있을 경우
Behavior Tree 에는 노드 추가를 통한 AI 구현뿐만이 아닌, 신규 Behavior Tree Task 를 생성하여 적용시킬 수도 있다.
BTTask_BlueprintBase 기반 BTT_Attack 블루프린트를 생성한다.
생성경로: Blueprints/AI


BTT_Attack 에서 함수탭의 Override 를 클릭하고 Event Recieve Execute AI 노드를 생성한다.


Finish Execute 노드를 추가하고 Success 값을 참으로 기본설정한 다음 연결시킨다.
( Task 내에서 성공/실패 아무 값이나 설정해주지 않으면 생성한 Task 를 벗어날 수 없으므로 임시로 설정)

이제 BT_EnemyBehaviorTree 에서 Melee Attack 관련 Sequence 에 BTT Attack 을 자식 노드로 추가해줄 수 있다.

BTT_Attack 이 BT_EnemyBehavoirTree 에서 정상적으로 작동하는지 확인할 수 있다.
BTT_Attack 으로 돌아와 Get Object Name 노드를 통해 문자열을 뷰포트에 출력하게 하고 플레이어와 접촉하게 되면 정상적으로 Goblin_Spear 가 출력되는 것을 확인할 수 있다.
TODO; 캐릭터 안움직임오류

동일하게 Owner Controller 도 확인해볼 수 있다.
공격시 시각적 피드백을 위한 DebugSpehre 을 뷰포트에 나타내기 위해 기존의 노드를 삭제하고 액터의 위치를 구해 DebugSphere 를 나타내도록 노드를 수정한다.
TODO: 캡쳐후추가(Controlled Pawn -> Get Actor Location -> Draw Debug Sphere)
실행시 정상적으로 DebugSphere 가 나타나는 것을 확인할 수 있다.
TODO: 캡쳐후추가
공격을 하기 위해 이동을 중단시켜야 한다.
세번째 Sequence 의 Am I Far Enough to Approach? 를 살펴보면 접근할 만큼 멀리 떨어져있다면, 거리가 500이상인지 확인하고 있다.
이동 중단을 위해 Observer aborts : Self 로 설정해준다.
이렇게 설정하면 500보다 크거나 동일한 경우 중단하는데 대상이 self가 된다.
TODO: 캡쳐후추가
Melee Attacker 의 경우 500보다 더 짧은 거리로 접근해야 한다.
Melee Attacker Sequence 에서 가장 왼쪽에 Move To 자식 노드를 생성하고, Blackboard Key : TargetToFollow , Acceptable Radius : 20 으로, 노드병을 Get Closer 로 설정한다.
TODO: 캡쳐후추가
추가
BTT_Attack->Attack으로 노드명 변경
컴파일 후 실행시 Goblin_Slingshot 은 500 거리에 멈춰서있고, Goblin_Spear 는 플레이어에게 접근하는 것을 확인할 수 있다.
TODO: 캡쳐후추가
추가
Goblin_Spear가 500지점에서 이동도중 움찔거리는 이슈
플레이어에게 500보다 멀리 있을 경우 접근하는 것과 근접관련 접근이 동시에 있어서 발생하는 문제
ABP_Goblin_Spear에서 적용중인 애니메이션 경로를 확인
TODO: 캡쳐후추가(AssetOverride)
BP_GoblinSpear_IdleWalkRun->Weight Speed : 4로 설정
TODO: 캡쳐후추가
실행시 더이상 움찔거리는게 많이 완화됨
동일하게Goblin_Slinghot에도 적용시킨다.
TODO: 캡쳐후추가
추가
BTService_FindNearestPlayer.cpp뷰포트 디버그메세지 출력 코드 삭제
Goblin_Spear 가 근접 공격 이후 게릴라 공격을 하듯이 잠시 멀리 벗어났다 다시 접근해서 공격하도록 구현할 수 있다.
Vector 타입의 MoveToLocation 키를 생성한다.
TODO: 캡쳐후추가
새로운 Behavior Tree Task 를 생성하기 위해 New Task -> BTTast_BlueprintBase 를 선택하고 BTT_GoAroundTarget 을 생성한다.
생성경로: Blueprints/AI
TODO: 캡쳐후추가
함수탭 옆의 Override 를 클릭해 Event Recieve Execute AI 를 생성하고, 해당 Taks가 성공/실패로 끝났는지를 반환하도록 Finish Execute 노드를 추가한다.
TODO: 캡쳐후추가
MoveToLocation 키에 접근하기 위해 Blackboard Key Selector 타입 NewLocation 을 생성하고, public화 한다.
TODO: 캡쳐후추가
BT_EnemyBehaviorTree 에서 공격 후 주변 랜덤 지점으로 이동하도록 BTT_GoAroundTarget 노드를 자식노드로 가지도록 생성하고
TODO: 캡쳐후추가
New Location 에 MoveToLocation 을 할당해 BTT_GoAroundTarget 에서 접근가능하도록 한다.
TODO: 캡쳐후추가
또한 적이 Target 인 플레이어 주변으로 이동하는 것이므로 플레이어에 대한 정보도 필요하다.
BTT_GoAroundTarget 에서 Blackboard Key Selector 타입 Target 을 생성하고, public화한 다음
TODO: 캡쳐후추가
BT_EnemuBehaviorTree 에서 Target 에 TargetToFollow 를 할당한다.
BTT_GoAroundTarget 에서 Target 노드를 Get Blackboard Value as Actor 노드와 연결하여 Value 에 접근가능한도록 한 뒤, IsValid 노드를 통해 유효성 검사를 한다.
TODO: 캡쳐후추가(Target -> Get Blackboard Value as Actor -> IsValid, Isvalid는 Event 노드랑 연결)
Get Actor Location 노드를 통해 플레이어의 위치를 파악하고
TODO: 캡쳐후추가
주변 지점이 NavMesh 내의 도달 가능한 지점인지 판단할 수 있도록 GetRandomLocationInNavigableRadius 노드를 생성하여 연결한다.
TODO: 캡쳐후추가(GetActorLocation 노드와 Origin 핀 연결, Radius : 300(변수 승격화, public화(BT_EnemyBehaviorTree에서 필요하면 편하게 수정하기 위함)))
해당 랜덤 지점을 NewLocation 노드를 통해 생성한 Set Blackboard Value As Vector 노드에 연결시키고
TODO: 캡쳐후추가(NewLocation 노드에서 Set Blackboard Value As Vector 노드 생성하여 연결, GetRandomLocationInNavigableRadius 노드의 리턴 핀 Random Location 은 Set Blackboard Value as Vector 의 Value 핀과 연결)
마지막으로 Set Blackboard Value As Vector 노드를 Finish Execute 노드와 연결하고 GetRAndomLocationInNavigableRadius 의 Return Value 를 Finish Execute 노드의 Success 핀과 연결한다.
TODO: 캡쳐후추가
랜덤 지점으로 이동하도록 BT_EnemyBehaviorTree 에서 Move To 노드를 Melee Attacker Sequence 가장 오른쪽에 추가하고, Blackboard Key : MoveToLocation 으로 설정한다.
그리고 공격 후 바로 랜덤지점으로 이동하는 것이 아닌, 약간의 딜레이를 가지도록 Wait 노드를 추가하고 Wait Time : 1 , Random Deviation : 0.5 로 설정한다.
TODO: 캡쳐후추가
마지막으로 이동시 다른 적에 끼여 이동이 불가능할 경우 해당 동작을 중단하도록 TimeLimit 이라는 Decorator 를 추가한다.
TODO: 캡쳐후추가
중단 대상은 자신이 되도록 Observer aborts : Self 로 설정하고, Time Limit : 2 로 설정하여 2초후에 동작을 중단하게 하도록 한다.
추가
노드명 변경
BTT_GoAroundTarget->Find New Location Around Target
TODO: 캡쳐후 추가
원거리 적의 공격을 구현할 때 고려해야 할 점은 플레이어와의 거리뿐만이 아니다.
만약 적과 플레이어 사이에 벽이 있다면, 적의 공격은 아래와 같이 벽에 막히게 된다.

이런 경우에 사용하는 것이 Environment Query System 이다.

Environment Query System 을 이용하면 Enemy 주변의 Location 을 Item 으로 가지고, 공격 가능한 지점이 여러 곳이라면 가장 효율적인 위치로 움직여 공격을 진행하게 된다.
추가
파일 정리경로: Blueprints/AI/BehaviorTree 옮길 파일: BB_EnemyBlackboard, BT_EnemyBehaviorTree 경로: Blueprints/AI/Tasks 옮길 파일: BTT_Attack, BTT_GoAroundTarget 경로: Blueprints/AI/Tasks 옮길 파일: BTS_FindNearestPlayer 경로: Blueprints/AI/AIController 옮길 파일: BP_AuraAIControllerTODO: 옮긴거 캡쳐후추가
12.2.1 Environment Query 블루프린트 생성 및 동작방식 확인
생성경로: Blueprints/AI/EQS
Environment Query블루프린트인EQ_FindRangedAttackPosition을 생성한다.
TODO: 캡쳐후추가(우클릭->Artificial Intelligence -> Environment Query 클릭)
우선 시각적으로 어떻게 동작하는지 나타낼EQSTestingPawn기반BP_EQSTesting블루프린트를 하나 생성한다.
TODO: 캡쳐후추가
테스트용 레벨을 생성한다.File -> New Level -> Basic 선택 -> Create 레벨 생성후 File -> Save Current Level As 저장경로: All/Content/Maps 파일명: EQS_TestingMapTODO: 캡쳐후추가
레벨에BP_EQSTesting을 배치하고
TODO: 캡쳐후추가
뷰포트의Outliner에서Query Template : EQ_FindRAngedAttackPosition으로 설정한다.
TODO: 캡쳐후추가
EQ_FindRangedAttackPosition를 열고Root에Generator : Pathing Grid를 추가하여 주변에Location을 생성한다.
Grid라는 단어에서 예상되듯이, 격자 형태의 포인트가 있고 해당 지점을 토대로 계산을 진행한다.
TODO: 캡쳐후추가
Details패널에서는GridHalfSize를 통해 반지름과 같이 격자의 크기를 지정할 수 있고,Space Between을 통해 격자 포인트간 간격을 지정할 수 있다.
TODO: 캡쳐후추가
뷰포트로 돌아가면BP_EQTesting주변에 격자 형태의 포인트가 생성된 것을 확인할 수 있다.
TODO: 캡쳐후추가다른
Generator적용시 모습
TODO: 캡쳐후추가, 싹다추가
Generator 우클릭후 Add Test 를 통해 테스트 조건을 추가할 수 있다.
먼저 Trace 에 대해 설명하자면
TODO: 캡쳐후추가
Trace 는 선택한 특정 목적지로 Trace 를 진행하는데, 기본적으로 Visibility 채널을 기준으로 진행한다.
Trace 는 Yes 또는 No 질문의 정렬과 같다.
특정 대상을 추적하는데 있어서 <타격 가능한가?> 와 <경로상에 무엇이 공격을 방해하는가?> 질문이 연속으로 등장하고, 이에 대해 답을 내린다.
추가한 Trace 를 클릭하면 우측의 Details 패널에서 다양한 설정들을 확인할 수 있다.
적합한 경로 추적만 필요하므로 Filter Only 로 지정한다.
TODO: 캡쳐후추가
Trace 를 수행할 수 있는데, 이 대상을 Context 라고 부르고, Context 에서 각 지점으로 트레이스를 수행할 수도 있음. EnvQueryContext 는 직접 만들어 사용 가능함.Trace 를 진행할 대상을 지정하기 위한 EnvQueryContext_BlueprintBase 타입 EQS_PlayerContext 블루프린트를 생성한다.
생성경로: Blueprints/AI/EQS
TODO: 캡쳐후추가
EQS_PlayerContext 에서 함수탭의 Override 를 클릭해 Provide Actor Set 을 추가하여 Trace 대상 액터를 리턴할 수 있도록 한다.
TODO: 캡쳐후추가
GetAllActorsOfClass 노드를 생성하고, BP_AuraCharacter 를 선택한 후, 액터를 리턴하도록 노드를 구성한다.
TODO: 캡쳐후추가
EQ_FindRangedAttackPosition 으로 돌아와 Context 를 방금 생성한 EQS_PlayerContext 로 지정해주면, EQS_Testing 은 BP_AuraCharcter 를 대상으로 Trace 를 진행하게 된다.
TODO: 캡쳐후추가
컴파일하고 뷰포트로 돌아오면 모든 포인트가 붉은색으로 표시된다.
Trace 할 대상인 BP_AuraCharacter 가 없기 때문에 포인트가 0이 되기 때문이다.
TODO: 캡쳐후추가
BP_AuraCharacter 를 뷰포트에 추가해주고 EQS_Testing 을 선택하면 색이 바뀌는 것을 확인할 수 있다.
( BP_AuraCharacter 배치 이후에도 색이 변하지 않는다면 EQS_Testing 이 지면에 박혀있어서 그럴 수 있으므로 z축 위로 살짝 이동)
TODO: 캡쳐후추가
Trace 결과를 어느 관점에서 보느냐의 차이Trace 가 벽과 충돌할 경우 해당 지점이 붉은색으로 표시Context 를 Trace 가능한 지점이 파란색으로 표시됨Trace 가 벽과 충돌할 경우 해당 지점이 파란색으로 표시Context 를 Trace 가능한 지점이 붉은색으로 표시됨False 로 설정을 유지한다.
Match 를 확인해보기 위해 스태틱 메시를 하나 추가한다.
경로: Content/Assets/Dungeon
파일명: SM_Tile_3x3x3
TODO: 캡쳐후추가
Match 활성화시
TODO: 캡쳐후추가
Match 비활성화시
TODO: 캡쳐후추가
스태틱 메시 내부를 보면 내부는 Context 를 Trace 하는 것을 확인할 수 있다.
TODO: 캡쳐후추가
해당 스태틱 메시는 내부에서는 blocking 을 하지 않기 때문인데
해결하기 위해서 해당 메시를 우클릭 -> Edit SM_Tile_3x3x3
TODO: 캡쳐후추가
Show -> Simple Collision 선택시 초록색의 콜리전 볼륨이 없는 것을 확인할 수 있다.
TODO: 캡쳐후추가
상단의 Collision -> Add Box Simpleified Collision 을 선택하면 박스 형태의 콜리전이 생성된다.
TODO: 캡쳐후 추가
정상적으로 적용되었는지 확인하기 위해 Nav Mesh Bounds Volume 을 추가한다.
TODO: 캡쳐후추가
EQS_Testing 을 클릭한 채로 스태틱 메시 내부를 확인해보면 더이상 포인트가 생기지 않는 것을 확인할 수 있다.
TODO: 캡쳐후추가
PathingGrid 에 Add Test 를 통해 Distance 를 추가한다.
TODO: 캡쳐후추가
Test Purpose : Scored Only 로 설정하고
TODO: 캡쳐후추가
뷰포트를 확인하면 Context 를 확인할 수 없는 지점은 파란색, 점수가 0인 지점은 붉은색, 점수가 높아질수록 붉은색에서 초록색으로 변하는 것을 확인할 수 있다. TODO: 캡쳐후추가
Scoring Factor 의 값을 -1.0으로 설정할 경우 역으로 표시된다.
TODO: 캡쳐후추가
Context 를 EQS_PlayerContext 로 변경하고 뷰포트를 확인하면 EQS_Player 기준으로 거리에 따라 점수가 표시되는 것을 확인할 수 있다.
TODO: 캡쳐후추가
Scoring Factor 의 값을 1.0으로 변경하면 Context 주변이 붉게 표시된다.
TODO: 캡쳐후추가
원거리 적의 경우 플레이어가 보이는 가장 가까운, 즉 점수가 높은 지점으로 이동하는 것이 목적이므로 EnvQueryContext_Querier 로 설정하고, Scoring Factor 를 -1로 설정한 다음 EQS_Testing 을 벽 뒤로 옮기면 EAS_PlayerContext 가 보이는 메시의 모서리가 가장 점수가 높은 것을 확인할 수 있다.
TODO: 캡쳐후추가
EQS_PlayerContext 를 완전히 반대편에 두면, 거의 근접해야지 공격가능한 지점이 설정되는 것을 확인할 수 있다.
TODO: 캡쳐후추가
이제 EQS 를 이용하도록 노드를 구성해야 한다.
Ranged Attacker 에서 Run EQSQuery 를 선택하여 노드를 생성할 수 있다.
TODO: 캡쳐후추가
Query Template 에서 적용시킬 EQS 를 선택할 수 있다.
TODO: 캡쳐후추가
노드명도 변경시키고
TODO: 캡쳐후추가(Calculating Fire Position)
Blackboard Key : MoveToLocation 로 설정하여 대상을 지정해준다.
TODO: 캡쳐후추가
이후 Move To 노드로 MoveToLocation 으로 이동하도록 노드를 생성하고 노드명도 수정한다.
TODO: 캡쳐후추가(Get in Firing Position)
공격 가능 지점에 도달하였으니 공격을 하도록 BTT_Attack 노드를 생성하고, 다음 행동까지 약간의 딜레이를 가지도록 Wait 노드를 추가한다.
TODO 캡쳐후추가(1초에 랜덤0.5초)
추가
EQ_FindRangedAttackPosition에서GridHalfSize : 500수정
컴파일 후 원래 레벨에서 실행시 Goblin_Ranger 가 500거리에 멈춰서서 공격을 시도하고, 공격을 시도했기해 DebugSphere 가 나타나는 것을 확인할 수 있다.
TODO: 캡쳐후추가
레벨에 벽을 하나 추가하고 실행해도 정상적으로 동작하는 것을 확인할 수 있다.
TODO: 캡쳐후추가(벽sm 위치:contest->assets->dungeon->SM_Wall_4x2)
추가
벽뒤 클릭시 오류 발생
- AuraPlayerController.cpp
... /** 코드 수정 : if문으로 check 진행 */ if(NavPath->PathPoints.Num() > 0) { CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1]; bAutoRunning = true; } ... /** 코드 수정 */
뭐 더있는데 알아서 추가하시오나는모르겠다
근접 적 전용 GameplayAbility 를 적용시키기 위한 AuraDamageGameplayAbility 기반 c++ 클래스 AuraMeleeAttack 을 생성한다.
생성경로: public/AbilitySystem/Abilites
GameplayAbility 를 적용시키기 전에, 공격 관련된 태그를 생성한다.
#include "..."
struct FAuraGameplayTags
{
public:
...
/*
* Damage Types
*/
...
FGameplayTag Abilities_Attack;
...
protected:
private:
...
};
#include "..."
FAuraGameplayTags FAuraGameplayTags::GameplayTags;
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
...
/*
* Abilities
*/
GameplayTags.Abilities_Attack = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Abilities.Attack"), FString("Attack Ability Tag"));
/*
* Effects
*/
...
CharacterClassInfo 클래스는 캐릭터 생성시 기본적으로 가져야 할 것들을 정의해둔 클래스이다.
해당 클래스에서 생성한 적이 기본적으로 근접인지 원거리인지 확인할 수 있도록 코드 수정이 필요하다.
#include "..."
...
USTRUCT(BlueprintType)
struct FCharacterClassDefaultInfo
{
GENERATED_BODY()
...
// 적이 시작시 가지고 시작할 Abilities를 지정할 수 있도록 하기 위함
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
};
...
Ability 를 사용 가능하도록 승인하는 코드는 AuraAbilitySystemLibrary 클래스의 GiveStartupAbilities() 함수가 그 역할을 한다.
해당 함수를 수정하여 ㅁㅁㅁ할 수 있도록 한다.
TODO: 뭘 할수있게 하는지 모르겠음
#include "..."
#include "Data/CharacterClassInfo.h"
UCLASS()
class AURA_API UAuraAbilitySystemLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
...
/** 코드 수정 */
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|CharacterClassDefaults")
static void GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC, ECharacterClass CharacterClass);
/** 코드 수정 */
...
};
추가
해당 함수 사용하는 곳에도 파라미터를 추가해줘야 하므로 단순 빌드 진행후 오류 발생지점에 파라미터 추가
- AuraEnemy.cpp
#include "..." ... void AAuraEnemy::BeginPlay() { ... if(HasAuthority()) { /** 코드 수정 */ UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComopnent, CharacterClass) /** 코드 수정 */ } } ...
#include "..."
...
void UAuraAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC, ECharacterClass CharacterClass)
{
...
/** 코드 수정 */
if (CharacterClassInfo == nullptr) return;
/** 코드 수정 */
...
/** 코드 추가 */
const FCharacterClassDefaultinfo& DefaultInfo = CharacterClassInfo->GetClassDefaultInfo(CharacterClass);
for(TSubclassOf<UGameplayAbility> AbilityClass : DefaultInfo.StartupAbilities)
{
// 레벨을 확인하고, 레벨에 따라 더 강한 공격을 하기 위함
if(ICombatInterface* CombatInterface = Cast<ICombatInterface>(ASC->GetAvatarACtor()))
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, CombatInterface->GetPlayerLevel());
ASC->GiveAbility(AbilitySpec);
}
}
/** 코드 추기 */
}
...
컴파일 후 AuraMeleeAttack 기반 GA_MeleeAttack 블루프린트를 생성한다.
생성경로: Blueprints/AbilitySystem/GameplayAbilities
TODO: 캡쳐후추가
GA_MeleeAttack 에서 Abilities.Attack 태그를 소유하도록 설정한다.
TODO: 캡쳐후추가
DA_CharacterClassInfo 에서 Warrior 에게 추가해둔 Startup Abilities : GA_MeleeAttack 를 설정해주면 해당 Ability를 보유하게 된다.
GA_MeleeAttack 을 하도록 BTT_Attack 에서 DebugSphere 를 생성하는 노드를 삭제하고
TODO: 캡쳐후추가
Get Ability System Component 노드를 통해 Activate Ability By Tag 노드를 생성하여 태그를 통해 Ability 를 활성화할 수 있도록 한다.
TODO: 캡쳐후추가
GameplayTag 타입 AttackTag 를 생성하여 Abilities.Attack 태그를 선택한 뒤, Make Gameplay Tag Container from Tag 노드를 생성하여 연결한 뒤
TODO: 캡쳐후추가
Make Gameplay Tag Container from Tag 노드를 Activate Ability By Tag 노드의 Gameplay Tag Container 핀과 연결시킨다.
TODO: 캡쳐후추가
Finish Execute 노드와 연결하여 노드 구성을 끝마친다.
TODO: 캡쳐후추가
정상적으로 동작하는지 확인하기 위해 GA_MeleeAttack 에서 Event ActivateAbility 노드에 DrawDebugSphere 노드를 연결시킨다.
TODO: 캡쳐후추가(Radius:40, duration:0
.5, GetAvatarActorFromActorInfo->GetActorLocation->Center핀연결, DrawDebugSphere->End Ability)
마지막으로 Class Defaults -> Instancing Policy : Instance Per Actor 로 설정한다.
TODO: 캡쳐후추가
실행시 BehaviorTree 에서 Ability 를 활성화시키고, GameplayAbility 를 통해 Warrior 타입의 적이 공격을 시도하는 것을 확인할 수 있다.
TODO: 캡쳐후추가
GA_MeleeAttack 에서 GameAbility 발동시 몽타주를 플레이하도록 노드를 구성한다.
기존의 DebugSphere 관련 노드를 전부 삭제하고 PLay Montage And Wait 노드를 생성한다.
공격 애니메이션의 몽타주 AM_Attack_GoblinSpear 를 생성하고
생성경로: Content/Assets/Enemies/Goblin/Animations/Spear
TODO: 캡쳐후추가(Attack_Spear 애니메이션으로 몽타주 생성)
Play Montage And Wait 방금 생성한 몽타주를 생성하도록 추가한다.
TODO: 캡쳐후추가
몽타주 재생이 끝나면 End Ability 를 통해 능력을 비활성화시킨다.
컴파일 후 실행하면 Goblin_Spear 가 근접 공격하는 것을 확인할 수 있다.
TODO: 캡쳐후추가
실행시 자세히 살펴보면 고블린의 전방방향으로 공격방향이 고정되어 있다는 점이다.
운좋게 플레이어를 바라보고 멈춘 다음 공격을 하면 다행이지만, 플레이어를 넘어간 위치에서 공격을 시도할 경우 플레이어가 아닌 전방을 공격하는 문제가 발생한다.
TODO: 캡쳐후추가
플레이어와 동일하게 Motion Warping 을 이용하여 해결 가능하다.
추가
CombatInterface클래스를 살펴보면 공격방향으로 회전하도록 구현된UpdateFacingTarget함수가 있다.
BP_EnemyBase 에서 Event Update Facing Target 노드를 생성하고
TODO: 캡쳐후추가
컴포넌트 탭에서 Motion Warping 을 생성하고
TODO: 캡쳐후추가
Add or Update Warp Target from Location 노드를 생성하여 Target 핀과 연결한다.
AM_Attack_GoblinSpear 에서 생성할 Motion Warping 은 동일하게 FacingTarget 으로 할것이므로 Warp Target Name : FacingTarget 으로 설정해주고, Location 핀과 TargetLocation 핀을 연결한다.
TODO: 추가
AM_Attack_GoblinSpear 에서는 Motion Warping 할 구간을 지정해주어야 한다.
먼저 트랙명을 Motion Warping 으로 변경하고
TODO: 캡쳐후추가
Add NOtify State -> Motion Warping 을 선택하여 구간을 지정해준다.
TODO: 캡쳐후추가(거의 시작지점~0.23 18.96%)
Warp Target Name : FacingTarget 으로 설정하고, Warp Translation 을 비활성화한 뒤, Rotation Type : Facing 으로 설정한다.
TODO: 캡쳐후추가
마지막으로 AM_Attack_GoblinSpear 에서 EnableRootMotion 이 활성화되어 있는지 체크한다.
TODO: 캡쳐후추가
이제 대상의 위치를 확인할 CombatTarget 이 필요하다.
#include "...
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacterBase, public IEnemyInterface
{
GENERATED_BODY()
public:
...
UPROPERTY(BlueprintReadWrite, Category = "Combat")
TObjectPtr<AActor> CombatTarget;
prptected:
...
private:
...
};
EnemyInterface 에서 CombatTarget 의 호출(Getter)과 설정(Setter) 를 위한 코드를 추가한다.
#incluude "..."
UINTERFAC(MinimalAPI)
...
class AURA_API IEnemyInterface
{
GENERATED_BODY()
public:
...
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void SetCombatTarget(AActor* InCombatTarget);
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
AActor* GetCombatTarget() const;
};
AuraEnemy 클래스에서 함수를 override해서 사용하도록 코드를 추가한다.
#include "..."
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacterBase, public IEnemyInterface
{
GENERATED_BODY()
public:
...
/** Combat Interface */
...
virtual void SetCombatTarget_Implementation(AActor* InCombatTarget) override;
virtual AActor* GetCombatTarget_Implementation() const override;
/** Combat Interface */
...
}
#include "..."
...
void AAuraEnemy::SetCombatTarget_Implementation(AActor* InCombatTarget)
{
CombatTarget = InCombatTarget;
}
void AAuraEnemy::GetCombatTarget_Implementation() const
{
return CombatTarget;
}
...
컴파일 후 CombatTarget 을 지정해주기 위해 BTT_Attack 에서 Blackboard Key Selector 타입의 CombatTargetSelector 를 생성하고 public화한다.
TODO: 캡쳐후추가
BT_EnemyBehaviorTree 의 Ranged Attack Sequence -> BTT_Attack 에서 Combat Target Selector 를 TargetToFollow 로 설정하고, 노드명을 Ranged Attack 으로 변경한다.
TODO: 캡쳐후추가
동일하게 Melee Attack Sequence -> Attack 에서 Combat Target Selector 를 TargetToFollow 로 설정하고, 노드명을 Melee Attack 으로 수정한다.
BTT_Attack 에서 Combat Target Selector 노드에서 키의 밸류를 얻기 위해 Get Blackboard Value as Actor 와 연결시키고, 값의 유효성을 검사한 다음 Try Activate Abilities by Tag 노드와 연결한다.
TODO: 캡쳐후추가
만약 유효하지 않을 경우 Finish Execute 노드와 연결한다.
TODO: 캡쳐후추가
Controlled Pawn 으로부터 Set Combat Target 노드를 생성하여 연결하고, BT_EnemyBehaviorTree 로부터 얻어낸 값을 In Combat Target 핀과 연결하여 CombatTarget 을 설정해주고, 해당 과정을 유효성 검사 이후에 진행하도록 한다.
TODO 캡쳐후추가
CombatTarget 이 정해졌으므로 GA_MeleeAttack 에서 Get Avatar Actor From Actor Info 노드를 통해 Get Combat Target 노드를 호출하고
TODO: 캡쳐후추가
테스트를 위해 Return Value 에 Get Object Name 노드를 연결하여 뷰포트에 출력하도록 노드를 구성한다.
TODO: 켑챠(Event Activate Ability -> Get Combat Target -> PrintString)
실행시, Ability 가 활성화되면 CombatTarget 이 플레이어로 설정되어 뷰포트에 출력되는 것을 확인할 수 있다.
TODO: 캡쳐후추가
Get Avatar Actor From Actor Info 노드에서 Cast To CombatInterface 노드를 통해 캐스팅을 진행한 뒤, Update Facing Target 노드를 생성한다.
TODO: 캡쳐후추가
Get Combat Target 에서는 Get Actor Location 을 통해 플레이어의 위치를 확인하여 Update Facing Target 과 연결한다.
최종적으로 Play Montage And Wait 노드와 연결하면 적이 Motion Warping 하는 것을 확인할 수 있다.
제대로
Motion Wapring하는지 확인하는 방법
Motion Warping노티파이의 길이를 늘려 모션 워핑 타이밍을 길게 잡은 상태에서 플레이어 캐릭터를 움직이면 확인 가능
TODO: 캡쳐후추가
복구: 0.26 21.74%
몽타주 관련 이벤트도 추가한다.
플레이어의 경우 AN_MontageEvent 라는 애님 노티파이를 만들어 태그를 부여하고 그와 관련된 이벤트가 발생하도록 구현했다.
TODO: 캡쳐후추가(blueprints->Animnotify)
AM_Attack_GoblinSpear 에도 동일하게 Events 노티파이 트랙을 생성하고, AN_MontageEvent 노티파이를 추가한다.
TODO: 캡쳐후추가(0.31 25.97%)
태그를 추가하기 위해 Project Settings -> GameplayTags 에서 Event.Montage.Attack.Melee 태그를 추가한다.
TODO: 캡쳐후추가
AM_Attack_GoblinSpear 로 돌아와 방금 생성한 태그로 설정해준다.
TODO: 캡쳐후추가
GA_MeleeAttack 에서 몽타주 재생후 Wait Gameplay Event 노드를 추가하여 Event.Montage.Attack.Melee 태그 발생까지 대기하도록 하고
( Only Match Exact 활성화 확인 필수)
TODO: 캡쳐후 추가
확인을 위해 이벤트 발생이 확인되면( Event Recieved ) DebugSphere 를 무기의 끝부분에 표시하려고 한다.
CombatInterface 클래스를 살펴보면 GetCombatSocketLocation 이라는 소켓 위치를 반환하는 함수가 있다.
TODO: 캡쳐후추가
블루프린트에서 호출 가능하도록 UFUNCTION 매크로를 추가한다.
// virtual 삭제
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
FVector GetCombatSocketLocation();
// 함수 정의 삭제
FVector ICombatInterface::GetCombatInterface()
{
...
}
UFUNCTION 매크로를 이용하도록 수정하였으므로 GetCombatSocketLocation() 을 호출하는 곳 전부 _Implementatino 을 추가해주어야 한다.
// _Implementation 추가
virtual FVector GetCombatSocketLocation_Implementation() override;
// _Implmentation 추가
FVector AAuraCharacterBase::GetCombatSocketLocation_Implementation()
{
...
}
이제 GA_MeleeAttack 에 GetCombatSocketLocation 을 추가할 수 있다.
GetAvatarActorfromActorInfo 노드를 생성하여 Target 핀과 연결하고
TODO: 캡쳐후추가
Event Recieve 핀과 GetCombatSocketLocation 노드를 연결한 다음
TODO: 캡쳐후추가
DebugSphere 를 CombatSocket 에 출력하도록 노드를 구성한다.
TODO: 캡쳐후추가(radius:15, color:red, duration:3, endability노드와 연결 마무리까지)
BP_Goblin_Spear 의 컴포넌트에서 Weapon 선택후 스켈레탈 메시 파일을 열고 소켓명을 복사한다음
TODO: 캡쳐후추가
BP_Goblin_Spear -> Weapon Tip Socket Name 에 붙여넣기한다.
TODO: 캡쳐후추가
컴파일 후 실행시 무기 끝부분에 DebugSphere가 생성되는 것을 확인할 수 있다.
TODO: 캡쳐후추가
플레이어로 파이어볼트 발사시 오류가 발생한다.
GetCombatSocketLocation 함수를 UFUNCTION 매크로를 통해 BlueprintNativeEvent 지정해 주었는데
AuraProjectileSpell.cpp 에서 직접 호출하기 때문에 문제가 생기는 것이다.
해당 부분의 수정이 필요하다.
#include "..."
...
void UAuraProejctileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
...
if(CombatInterface)
{
/** 코드 추가 */
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocadtion(GetAvatarActorFromActorInfo());
/** 코드 추가 */
/** 코드 삭제 */
const FVector SocketLocation = CombatInterface-GetCombatSocketLocaiton();
/** 코드 삭제 */
...
...
}
}
...
컴파일 후 실행하면 정상적으로 파이어볼트가 발사된다.
TODO: 캡쳐후추가
추가
추후 플레이어에게 데미지를 입히기 위한 기능 구현시, 범위 내에 있으면 피해를 입게 구현할 것이므로GA_MeleeAttack에서DebugSphere의 radius 45로 수정
TODO: 캡쳐후추가