본격적인 근접 몬스터 Enemy_Melee의 Behavior Tree를 만들기 위해 BTService 클래스를 제작하였다.
🖥️ BTService_Melee 클래스 생성
BTService를 상속받는 CBTService_Melee 클래스를 제작하였다.
TickNode를 오버라이드하여 사용했는데, 여기서 UBehaviorTreeComponent의 OwnerComp 매개변수는 AIController가 된다. UBehaviorTreeComponent가 컨트롤러에 존재하기 때문에 해당 컴포넌트의 Owner는 AIController가 출력될 것이다.
AIController를 통해서는 GetPawn 함수를 통해 Enemy_AI 캐릭터를 가져올 수 있다.
Enemy_AI 캐릭터를 통해서는 캐릭터가 가지고 있는 State 컴포넌트와 BehaviorComponent(제작한거)를 참조하여 사용하였다.
나머지 Tick 함수 부분에서는 Enemy_AI의 Behavior 컴포넌트 상태 값에 따라 Behavior Tree를 수행할 수 있도록 순찰(Patrol), 공격(Action), 접근(Approach) 모드를 각각 설정해주었다.
Enemy의 타겟이 존재하지 않는다면 순찰 Patrol Mode를 시작하고, 타겟이 존재하는데, 해당 타겟이 공격 범위(Action Range) 내에 들어온다면 공격 Action Mode를 시작하도록 하였다.
타겟이 존재하지만, 공격 범위 내에는 안들어온다면 Approach Mode를 수행하여 타겟에게 접근하도록 구현하였다.
🖥️ Behavior Tree와 블랙보드 설정
컴파일 후, 만들어진 BTService_Melee를 비헤이비어 트리의 서비스로 등록시켜보았다.
이후, Task들을 수행하기 위한 데코레이터 노드를 설정해야 하므로 Behavior 컴포넌트에 생성해두었던 EAIStateType 열거형을 블랙보드의 키 값으로 추가해주었다.
Enum 형의 새 키를 추가하고 나서 Enum Type 목록에서 아무리 열거형을 찾아도 만들어둔 EAIStateType이 나오지 않았다. BlueprintType으로 UENUM의 속성값을 설정해도 보이지 않았다!
이때는 Enum Type의 목록에서 지정하는 것이 아니라, Enum Name에 만들어둔 열거형의 이름을 그대로 적어 찾아줘야 한다.
제대로 맞는 열거형 이름을 적고 엔터를 입력하면 EnumType에 해당 열거형이 등록된다. (잘못적으면 등록되지 않음)
이렇게 만들어진 블랙보드 키 값을 이용하여 데코레이터를 작성해보았다.
AIStateType이 Wait인 경우와, Patrol인 경우를 각각 나눠 노드를 추가하였다.
🚶🏻Patrol 1단계. 속도 초기화 - BTTaskNode_Speed 생성
순찰(Patrol)을 구현하는 첫번째 단계로, Enemy의 순찰 속도를 지정하는 Speed Task를 제작하였다.
Movement 컴포넌트에 존재하는 ESpeedType 열거형을 이용하여 Enemy의 이동 속도를 조정하였다.
Task가 실행될 때 호출되는 ExcuteTask를 오버라이드하여, Enemy_AI가 가지고 있는 Movement 컴포넌트의 SetSpeed 함수를 호출하였다.
💡EBTNodeResult
이후에는 EBTNodeResult 열거형을 반환하여 해당 Task가 성공적으로 종료되었는지, 실패되었는지를 전달해준다.
열거형의 종류로는 Succeeded(성공), Failed(실패), Aborted(취소), InProgress(대기) 가 있다.
블루프린트에서는 Task 노드의 반환값으로 성공이나 실패가 들어오지 않으면 대기를 했는데, C++ 프로젝트에서는 대기모드를 의미하는 InProgress가 따로 존재한다.
컴파일 후, 만들어진 Speed Task를 Walk로설정하여 Patrol 데코레이터 하위의 노드로 추가해주었다.
🚶🏻♂️Patrol 2단계. Patrol Path 액터 만들기 (Spline 순찰)
Enemy가 순찰을 수행할 때, Enemy_AI가 Patrol Path 액터를 소유하고 있다면 Spline 순찰을 수행하도록 하고, 가지고 있지 않다면 랜덤 지점을 순찰하도록 구현하였다.
PatrolPath 클래스에는 Spline Component를 Loop로 만들것인지를 설정할 bLoop와, Spline Component의 지점들을 나타낼 Point Index, 역방향으로 순찰할 것인지를 결정할 bReverse 변수들을 선언하였다.
또한 핵심 컴포넌트인 USplineComponent도 선언하였다.
PatrolPath 클래스의 생성자에서는 각 컴포넌트를 생성하고 초기화해주었다.
bRunConstructionScriptOnDrag 변수를 비활성화하여 드래그할때마다 Construction 함수가 호출되지 않도록 설정하였다.
TextRenderComponent 객체인 Text는 PatrolPath 액터 위에 해당 액터의 이름을 출력하기 위해 생성해두었다. (디버깅용이다)
OnConstruction 함수에서는 Text Render 컴포넌트를 해당 액터의 이름으로 초기화해주었다.
이때, 그냥 액터의 GetName을 사용하면 액터의 ID 이름이 출력되어 BP_CPatrolPath_2 와 같은 숫자가 붙기 때문에, 월드 아웃라이너에 뜨는 것과 같이 액터의 이름만 사용하기 위해 GetActorLabel 함수를 사용하였다.
이때 GetActorLabel 함수는 ActorEditor 클래스에 존재하는 함수인데, 해당 클래스의 함수들은 모두 #if WITH_EDITOR로 선언되어있어 에디터 모드에서만 사용 가능하다고 정의되어 있다.
따라서 GetActorLabel 함수를 사용할 때에도 #if WITH_EDITOR ~ #endif 명령어를 선언하였다.
GetMoveTo 함수에서는 Spline 컴포넌트의 GetLocationAtSplinePoint 함수를 통해 매개변수로 전달하는 Index의 위치를 반환받아주었다.
UpdateIndex 함수에서는 Spline 컴포넌트의 GetNumberOfSplinePoints 함수를 통해 전체 Spline 포인트 개수를 반환받아오고, 해당 개수를 토대로 Index를 증감시켜주었다.
역방향 순찰인지, 정방향 순찰인지에 따라 Index를 감소하거나 증가시켜주고, Loop 인지에 따라 인덱스를 처음부터 시작할 것인지, 이전부터 시작할 것인지를 결정해주었다.
만들어진 CPatrolPath 클래스는 Enemy_AI 클래스에서 직렬화하여 선언해주었다.
이렇게 만든 Patrol Path 액터를 블루프린트 기반 클래스로 생성하여, 월드에 배치하고 Spline Component를 형태에 맞게 만들어주었다.
🚶🏻♀️Patrol 3단계. Patrol Task 만들기
본격적인 Patrol 순찰 기능 구현을 위한 Patrol Task를 제작하였다.
PatrolTask에는 Enemy가 이동할 위치를 표시할 bDebugMode 변수와, 목표지점에 어느정도 까지 도달했을때 이동이 완료된 것인지를 설정하기 위한 수락 반경(AcceptanceDistance), 다음 목표지점을 선택하는 이동 거리(Distance)를 각각 선언하였다.
생성자에선 NodeName을 초기화하고, TickTask 함수를 사용하기 위한 bNorifyTick 변수를 활성화시켜주었다.
Task가 처음 실행될 때 호출되는 ExecuteTask에서는 Enemy_AI가 PatrolPath를 가지고 있는지 먼저 검사한다.
Enemy_AI가 PatrolPath를 가지고있다면, 해당 Spline 포인터의 위치값을 반환받아 Location 변수에 저장하였다.
이후, InProgress 상태를 반환하여 해당 Task가 종료되지 않았음을 알려주었다.
이 상태에서는 Location에 Spline의 Index 위치값을 계속해서 반환받기 때문에 TickTask에서 주기적인 Spline 순찰을 수행하며 Patrol Task가 작동된다.
반면, Enemy_AI가 PatrolPath를 가지고 있지 않는다면, FNavigationSystem의 GetCurrent 함수를 통해 현재 Enemy_AI가 올라가있는 네비 메시를 불러와 저장하였다.
UNavigationSystemV1 자료형에 네비게이션 메시를 저장해두고, GetRandomPointInNavigableRadius 함수를 통해 랜덤 위치를 반환받았다.
랜덤 위치는 FNavLocation 자료형으로 반환받아 구조체 안에 Location 값을 꺼내 사용할 수 있다.
반약 디버깅 모드 bool 변수가 켜져있다면 이동할 위치에 초록색 구체를 출력해주었고, Enemy_AI가 계속해서 순찰을 수행할 수 있도록 InProgress 상태를 반환해주었다.
TickTask 함수에서는 Location 변수로 Enemy_AI를 계속해서 이동하도록 만들어야 한다.
EPathFollowingRequestResult 열거형을 통해 Enemy가 성공적으로 이동을 완료했는지 검사하였다.
EPathFollowingRequestResult는 경로에 따라 이동 요청의 결과를 나타내는 열거형으로,
해당 경로로 이동 요청이 실패되는 Failed 와,
AI 캐릭터나 AI 컨트롤러가 이미 이동 목표 지점에 도달한 상태인 AlreadyAtGoal,
경로에 따른 이동 요청이 성공한 RequestSuccessful 까지 총 3개의 상태값을 가질 수 있다.
TickTask 함수에서는 이러한 Result 값을 비교하여 이동요청이 실패(Failed)한 경우 Patrol Task의 결과도 실패로 전달하였고,
이미 이동 목표에 도달한 AlreadyAtGoal 상태에서는 EnemyAI의 PatrolPath 경로를 가져와 UpdateIndex 함수로 다음 순찰 지점으로 업데이트한 다음 Patrol Task 작업도 성공으로 전달하였다.
FinishLatentTask 함수는 TickTask 함수에서 해당 Task의 결과를 반환해주기 위한 함수이다.
Execute 함수와 달리 Tick 함수는 반환값이 없는 void 자료형이기 때문에, 이러한 함수를 이용해 해당 Task를 끝맺을 수 있다. 이러한 FinishLatentTask 함수를 사용하지 않으면, Task를 빠져나오지 못해 오류가 발생하게 된다.
컴파일 후 만들어진 Patrol Task를 비헤이비어 트리에 추가해주었다.
[ 랜덤 순찰 결과 ]
먼저, Patrol Path를 지정하지 않아 랜덤 지점으로 순찰하는 Enemy를 확인해보았다.
[ Patrol Path - Spline 순찰 결과 ]
Patrol Path를 지정하여 Spline 지점 순찰의 결과도 확인해보았다.