안녕하세요! '언리얼로 만들어보는 RPG'의 필자입니다. 이번 글에서는 전에 작성하던 Task노드의 나머지 두개를 정리하러 돌아왔습니다. 폭풍같은 회사일정이 지나고 나니까 정신이 없네요. 이전 글에서는 새로운 내용도 많았으나 이번에는 크게 새로울건 없어서 내용이 그렇게 길지 않을것 같습니다.

UCustomMove.h

1-1) 굉장히 간소하다

 당시 필자가 어떤 생각으로 작업을 했는지는 모르겠으나 엔진에서 기본제공하는 MoveTo Task노드를 상속받아서 구현을 했었습니다.

UCustomMove.cpp

1-2) 사실상 핵심은 마지막 if문입니다.

 지금 보면 정말 이해안가는 방식의 작업물이긴 합니다. 결국 하고싶었던건 AcceptableRadius 변수에 몬스터의 사거리를 대입하고 싶었던것 뿐이니까요. 이 외에는 부모클래스인 MoveTo 쪽에서 다 처리합니다. 이럴꺼면 차라리 다른 방법을 사용하는게 나았을 수도 있을것 같습니다.

 

UAttackSelector.h

2-1) 전 글과 다르게 이것도 별거 없습니다

UAttackSelector.cpp

2-2) 허무합니다

 공격을 선택하는부분이 상당히 빈약한데 이건 몬스터랑 애니메이션 클래스를 같이 설명해야 하는부분이라서 크게 적을게 없습니다. 하지만 역시나 여기서도 이상한 부분이 존재하는데 그걸 포함해서 앞으로 있을 작업을 미리보는 느낌으로 설명하겠습니다.

 

 우선 AttackType이라는 변수를 AttackAble이라는 bool배열의 크기의 범위로 랜덤한 값을 구해서 대입합니다. 이 글을 읽고 계신 여러분이라면 당연히 떠올릴법한 궁금증이 하나 있으실텐데요. '왜 랜덤값을 유지를 하는가?' 그건 필자의 잘못된 애니메이션 설계때문이라고 답해드릴수 있습니다.

 

 저번 AI(1) 글에서 유한상태기계에 대해서 이야기 한적이 있습니다. 그런 스타일로 작업을 하다보니까 거미줄로 가득한 아름다운 애니메이션BP를 만들게 되었고 그 덕에 저 값을 유지해야 한다는 사실입니다.

 

 AI의 마지막인데 이렇게 짧아서 너무 날로 먹는거 아닌가 싶긴 합니다. 하지만 격무에 치이는동안 글쓰는 감각을 잃은 필자의 재활기간이라 생각해주시면 좋을것 같습니다. 그럼 다음 글에서 뵙도록 하고 이만 글을 줄이도록 하겠습니다.

Posted by 별수집가
,

 안녕하세요. '언리얼로 만들어보는 RPG'의 필자입니다. 7~10일 정도의 텀으로 글을 작성하려고 했으나 그건 욕심이 좀 컸던거 같습니다. 필자가 너무 게을러서 그런거겠지만 거두절미하고 바로 본론으로 들어가서 이번 포스팅을 시작해 봅시다!

 

 저번 작성글에서는 AIController와 AnimInstance를 구성하려고 했었으나, AIController를 설명하다가 언리얼에서 AI를 구성할때 사용하는 시스템들에 대한 소개글로 마무리를 지었습니다. 원래 흐름대로라면 AnimInstance를 해야겠지만 노선을 좀 변경해서 행동트리를 먼저 하도록 하겠습니다.

 

 이번 글을 작성하기전에 간략한 견적을 잡고 가도록 하겠습니다.

1. 적대상대가 없을시 임의 위치 산정해내는 Task 작성

2. 몬스터의 상태변경을 관리하는 Task 작성

3. 몬스터의 이동에 관여하는 Task 작성

4. 공격방식을 선택하는 Task 작성

 

 Task 노드의 작업은 위 4가지 정도로 생각이 되고 행동트리에서 사용할 변수들은 블랙보드에 하나하나 담아가 보도록 하겠습니다.

1-1) 작업을 통해 완성된 행동트리

 앞으로 작업을 통해서 만들게 될 행동트리의 결과물이 바로 위 사진입니다. 보시면 CustomMove라는 Task노드가 존재합니다. 언리얼에서 기본적으로 MoveTo라는 노드를 제공해 주나 작업당시 필자는 변화를 좀 주고싶었습니다. 자세한 내용은 차근차근 알아가 보도록 하겠습니다.

EMonsterState

1-2) 몬스터의 상태를 결정해줄 ENUM

UChangeState.h

2-1) ChangeState 헤더파일

 몬스터의 상태를 변경하는 ChangeState 부터 시작해보도록 하겠습니다. 헤더파일에서는 변경될 상태를 설정할 State변수와 블랙보드의 변수이름을 지정하는 StateName 변수가 있습니다. StateName변수의 경우 필자가 처음 작업한것이다 보니 아쉬운 부분이 많은데 그 내용은 CPP쪽에서 풀겠습니다.

UChangeState.cpp

2-2) ChangeState CPP파일

 ExecuteTask 함수에서는 컨트롤러, 블랙보드, 몬스터클래스 여부를 확인한 다음에 하나라도 통과를 못하면 즉시 결과를 실패처리 합니다. 필자도 약간 과하지 않나 싶기는 하지만 나중에 제거하자는 마음으로 남겨두고 그대로 잊었습니다. 사람인데 어떻게 하겠습니까?

 

 아무튼 블랙보드에 있는 변수설정은 타입별로 함수를 불러서 해줘야 하는데 이름과 변수를 인자로 받아들입니다. 아까 언급한 StateName이 여기서 불편함을 유발하게 되는것이죠. StateName은 에디터에서 작업자가 설정해 주어야 하는데 원하는 변수의 이름을 정확히 적지 않는다면 문제가 발생하기 마련입니다. 이런 문제는 블랙보드에 있는 변수를 직접 선택할 수 있도록 해줌으로써 해결이 가능한데 당시의 필자는 거기까진 찾아보지 않았다는게 안타까운 점이죠.

UBTTask_BlackboardBase.h

2-3) KeySelector의 존재!

UBTTask_MoveTo.cpp

2-4) KeySelector에서 선택 가능한 변수 필터추가

 2-3 사진을 보시면 FBlackboardKeySelector 라는 자료형을 볼 수 있습니다. 해당 자료형은 아까 언급한대로 블랙보드내에 있는 변수를 선택할 수 있도록 해주는 자료형입니다. 이 자료형을 이용했었다면 작업자가 일일히 블랙보드 변수의 이름을 적는 불필요한 작업을 하지 않을 수 있었겠죠.

 

 그리고 이 자료형을 쓰는게 좋은 이유는 2-4 사진에 나와있는것처럼 해당 KeySelector가 어떤 유형의 변수를 선택 할 수 있는지에 대해서 지정해 줄 수 있기때문 입니다. 필터 기능까지 이용한다면 더더욱 작업자의 실수로 인한 문제발생의 여지가 줄어드리라 믿습니다. 이 글을 보시는 여러분도 필자와 같이 이름을 입력하는게 아닌 KeySelector를 사용하면 좋겠습니다.

USetDestination.h

3-1) 뭔가 이상하다

 필자는 헤더파일을 보고 정신이 혼미해질 수 밖에 없었습니다. 몬스터의 상태를 바꾸는 Task를 작성해 두고서 임의위치를 산정하는 Task에서 상태를 바꾸려고 블랙보드에서 가져올 변수 이름을 받으려고 저러고 있습니다. 아 정말 아찔하네요 StateName 변수는 실제로 쓰지도 않고있으니 무시하시면 됩니다.

USetDestination.cpp

3-2) 굉장히 비효율적입니다.

 현재 사용하고 있는 방식은 0~360 사이의 각을 구해서 해당 방향벡터를 만들고 '정찰 범위/2 ~ 정찰 범위' 만큼의 값을 곱해서 해당 위치로 가는 그런 형태를 취하고 있습니다. 작동이야 하겠지만 좀 불필요한 내용이 많은것 같기도 합니다. 도달가능한 위치를 구하는건 GetRandomReachablePointInRadius라는 함수를 통해서 얻을 수 있기도 하고 말이죠

 

 그래서 이왕 마음에 안드는거 위에서 언급한 FBlackboardKeySelector를 이용해서 필자가 생각하기에 흡족한 방식으로 다시 작성해보기로 했습니다.

(New)USetDestination.h

3-3) 기존 헤더와는 다르게 뭔가 많아졌습니다.

 우선 DestKey는 블랙보드에서 목적지의 용도로 사용할 FVector형 변수를 선택하는데 쓸것입니다. 밑의 OriginKey의 경우 기준점을 스폰 당시의 위치로 하거나 현재 몬스터의 위치로 하거나 두가지 선택권을 가지게 할 것입니다. 마지막 변수는 정찰 범위를 의미합니다.

 

 그럼 이제 흔하지 않은 함수 InitializeFromAsset가 있습니다. 이친구의 역할은 필자가 블랙보드로부터 변수를 가져다 쓸것이기 때문에 행동트리에 블랙보드가 설정이 안돼있는 만일의 경우 생기는 문제를 막기위해서 사용합니다. 자세한 내용은 밑의 사진에서 확인해보겠습니다.

(New)USetDestination.cpp

3-4) 주석이 뭔가 짤린거 같은건 기분탓입니다.

 아까 언급한 함수는 노드를 초기화 하는과정에서 블랙보드가 없다면 경고 로그를 찍게됩니다. 이런식으로 블랙보드의 변수를 가져다 써야하는 노드이니 잊지말고 넣으라고 알려주는 작업이 되는것입니다.

 

 이 Task 노드의 가장 핵심부분인 실행 함수를 한번 살펴보도록 합시다. 여기서도 도움이 될만한 부분이 있는데요 빨간색으로 밑줄이 쳐있는 if문을 보시면 블랙보드 셀렉터가 선택한 변수가 어떤 타입인지 비교를 할 수 있습니다.

 

 이를 통해서 필자는 FVector형일때는 Origin변수에 블랙보드로부터 해당키 이름을 통해 값을 가져와서 대입하고 만약 아니라면 위에서 설정한것처럼 ACharacter형태의 Object만 설정이 가능하니 해당 액터로부터 위치를 가져와 대입합니다.

 

 여기서도 필자가 미흡하게 넘어간 부분이 존재하는데 만약 도달가능한 임의위치를 찾지 못했다면 현재 몬스터의 위치를 목적지로 설정해주는게 생길지 모르는 문제들을 막아줄 것 같습니다.

 

 이번 글은 여기까지만 작성하고 마무리를 하기전에 실제로 작성한 Task 노드를 사용하려고 빌드를 하면 성공이 뜨는걸 확인하실 겁니다. 하지만 에디터에서 확인해보면 노드는 보이지도 않고 혹시몰라 컴파일을 하면 LNK 2001, 2019 등의 링크에러가 뜨는걸 확인하실텐데요. 이유는 간단하게도 모듈을 추가해주지 않아서 입니다.

 

 UNavigationSystemV1클래스는 "NavigationSystem"모듈을 추가해주어야 하고 BTTaskNode를 상속받아 쓰려면 "AIModule"을 추가해야합니다. 다음글에는 남은 두개의 Task노드를 정리해보도록 하겠습니다.

Posted by 별수집가
,

 안녕하세요! '언리얼로 만들어보는 RPG'의 필자입니다. 정말 부끄러우나 마지막 글로부터 1년 3개월 정도만에 나머지 내용을 정리하려고 돌아왔네요. 변명을 조금 해보자면 회사에서 폐 안끼치려고 부족한 실력 끌어모아 몇달을 달려왔더니 그럴 여유가 전혀 없었습니다. 지금은 이미 시간이 너무 지나버려서 계속 보시는 분들이 없겠지만 그분들에게 심심한 사죄의 말씀을 올립니다.

 

<대충 지친사회인 짤>

 

 너무 긴시간 글을 안썼더니 어떻게 시작해야할지 모르겠어서 잠깐 제 작업물을 확인하고 올 필요성이 있습니다.

 

 피자도 좀 먹고 그간의 게시글도 확인좀 하느라 늦었습니다. 예전(약 1년전)의 방식대로 저희는 간략한 견적을 작성해보고 시작하도록 하겠습니다.

1. 몬스터의 행동을 컨트롤할 AIContoller 작성

2. 생명을 불어넣어주는 AnimInstance 작성

 

 우선적으로 쓸데없는 이야기인데 제목과 넘버링을 어떻게 해야할지 조금 고민했습니다. 몬스터(1)로 갈지 AI(1)로 갈지 말이죠 어짜피 생각해보면 몬스터를 구성하기 위한 모든 과정이라 몬스터를 넘버링으로 사용해도 될거 같았는데 나중에 확인하기 쉽도록 분할해서 넘버링을 메기는게 나을것 같아서 AI로 가기로 했습니다.

 정말 쓸데없는 이야기는 이정도로 끝내도록 하고 몬스터 클래스를 만들기도 전에 AIController를 만들러 출발하도록 하겠습니다. 

AEnemyAI.h

1-1) EnemyAI의 헤더

 이것이 앞으로 몬스터를 조종하게될 Controller입니다. 지금 보니까 전방선언 해두고 쓰지도 않는게 두개나 있네요. 거기에 더해서 사실 저 AIOwner는 따로 선언해서 사용할 필요는 없었습니다. AIController의 2계층 위에 있는 Controller에 빙의할때 대상을 Pawn이라는 변수에 저장하거든요. 거기까지 확인하지 않았던 필자의 잘못이니 지금은 그러려니 하고 넘어가도록 합시다.

 

 사실 필자 작업한 AEnemyAI 자체에서 하는일은 크게 없습니다. 그걸 왜 이야기 하냐면 BTData라는 변수때문에 나온 이야기입니다. 해당 BehaviorTree는 실제 AI의 행동을 관리하고 상태를 변경시키는데 활용하는 객체입니다. 물론 AIController가 아무것도 하지 않는다는것은 아닙니다.

 

 어째서냐? 하면, 플레이어의 눈을 Camera가 대신 하는것처럼 몬스터 같은 NPC에게도 눈과 같은 역할을 할 수 있는게 있습니다. 해당 친구는 PerceptionComponent 인데 시각, 청각 등의 감각에 대한 반응을 설정할 수 있습니다. 좀더 디테일하게 파고들어가면 좋겠지만 필자 본인도 거기까지는 하지 않아서 말을 줄이겠습니다.

 

 함수가 두가지 구현되어있는데 Possess는 몬스터에 빙의할때 실행되고 나머지 하나인 UpdatePerception 함수가 중요하다고 볼 수 있습니다.

AEnemyAI.cpp

1-2) EnemyAI의 CPP

 몬스터에게 빙의를 하면 BTData를 이용해 RunBehaviorTree함수를 실행합니다. 해당 함수는 AIController에 선언 및 정의되어 있으며 인자로 들어온 BehaviorTree객체를 BrainComponent로 사용하도록 합니다. 그리고 PerceptionComponent가 Null이 아니라면 자극이 들어올때에 대한 처리를 하기위해 작성한 UpdatePerception 함수를 AddDynamic으로 델리게이트에 바인딩 시켜줍니다.

 

 필자가 바인딩 시킨 함수를 보시면 새로운 자극을 가한 객체들이 TArray<AActor*> 형태로 전달됩니다. 몬스터는 싸워야할 대상인 Player인지 확인해보고 맞다면 BlackBoard에서 해당 변수를 가져와 NULL인지 아닌지에 따라서 처리가 나뉘게 됩니다. 이러한 작업에는 이유가 있는데 이 Perception이 자극을 받는 경우가 시각을 예로들자면 범위 안에 들어올때와 나갈때 둘다 반응하기 때문입니다. 시야 밖으로 나가는 경우에는 BlackBoard에 있는 Player변수를 NULL로 변경해야 그에 따른 행동을 취할 수 있기 때문이죠

 

 이쯤 오니까 마구잡이로 글을 쓰기전에 몬스터의 AI를 구성하기 위해서 필자가 사용했던 엔진 기능들에 대해서 먼저 정리를 할 필요성이 있다는걸 느꼈습니다. 이번 첫글에서는 AIController, BehaviorTree, BlackBoard, AIPerception에 대해서 필자 나름대로 이해한 내용들을 적어내려가겠습니다.

AIController

 크게 설명할것은 없습니다. 플레이어블 캐릭터를 조종할때 사용하는 컨트롤러가 PlayerController라면 반대로 NPC를 조종하기 위해서 사용하는 컨트롤러는 AIController입니다. 지금까지 글을 작성하며 언급한 BehaviorTree나 AIPerception등 상태의 변경이나 관리, 행동을 변경시키는 객체들을 담아두고 관리하는 용도로 사용하면 된다고 생각합니다.(?)

 

 문서의 링크를 남겨두기는 하지만 문서에도 딱히 적힌 내용은 별로 없어서 실망하실 수 있습니다.

https://docs.unrealengine.com/ko/Gameplay/Framework/Controller/AIController/index.html

 

AI 컨트롤러

AIController, AI 컨트롤러는 사람 플레이어의 입력 없이 주변 월드를 관찰하고 의사를 결정한 뒤 알맞게 반응합니다.

docs.unrealengine.com

BehaviorTree(행동트리)

 NPC에게 행동을 지시하는 두뇌와 같은 역할을 하는 객체입니다. 언리얼에서의 행동트리는 Task, Decorator, Service 세가지의 실행 노드가 존재하며 Task는 가장 마지막인 잎 위치에 있어야하고 Decorator나 Service 노드의 경우 Composite라고 부르는 흐름제어 노드 (Selector등)와 Task에 함께 사용할 수 있습니다. Decorator는 일종의 조건문이라고 생각하시면 편하고 Service는 필자가 사용은 하지 않았으나 상당히 중요한 기능인 EQS를 실행시키거나 BlackBoard에 있는 정보의 최신화를 하는등에 사용가능합니다.

 

 행동트리 문서는 안타깝게도 한글로 번역이 안되어 있는데 필자가 설명한 정도만 알고 있어도 크게 어렵지는 않으리라 예상합니다. 또한 퀵 스타트 가이드를 따라가다 보면 금새 감을 잡으실꺼라 믿습니다.

https://docs.unrealengine.com/ko/Engine/ArtificialIntelligence/BehaviorTrees/BehaviorTreeQuickStart/index.html

 

Behavior Tree Quick Start Guide

This guide shows how to use Behaviour Trees to set up an AI character that will patrol or chase a player.

docs.unrealengine.com

 잠깐 옆길로 빠져서 한창 필자가 공부할때 (물론 지금도 공부를 안할 수 없지만) FSM이라 하는 유한상태기계를 통한 AI를 구성하는 방식을 사용하기도 했었습니다. 그게 정말 FSM인지 아닌지는 잘 모르겠으나 AI 구성에 관해서 관심이 있다면 이거저거 찾아보시는것도 나쁘지 않으리라 생각합니다. 물론 언리얼에서는 행동트리를 쓰는게 좋지 않을까요?

BlackBoard

 행동트리를 설명하며 두뇌와 같다는 이야기를 했었습니다. 아쉽게도 행동트리 혼자서는 온전한 두뇌는 어렵고 신경망정도라고 생각하면 이해가 쉽지 않을까 생각합니다. 그 이유라하면 BlackBoard는 두뇌의 저장공간과도 같은 역할로써 자신의 현재 상태와 기억하고 있어야할 다양한 정보를 관리하는 객체이기 때문입니다. BlackBoard에서 적에 대한 정보가 변경이 되면 그걸 기반으로 행동트리는 필요한 행동을 하도록 지시를 내리는 것이 되는것이죠

AIPerception

 몬스터의 상태에 따른 행동을 관리하는게 BehaviorTree였다면 상태의 변경에 관여를 하는건 방금 언급한 PerceptionComponent입니다. 이 Perception에 새로운 자극 예를들어 설정한 시야범위 안에 어느 객체가 등장했다거나 청각범위안에서 소리가 들리는등의 일들이 벌어지면 OnPerceptionUpdate라는 델리게이트를 호출하게 됩니다.

 

 특별한 내용이 있지는 않으나 더 많은 정보를 알고 싶으시다면 문서를 적극 추천드립니다.

https://docs.unrealengine.com/ko/Engine/ArtificialIntelligence/AIPerception/index.html

 

AI Perception

Documents the AI Perception Component and how it is used to generate awareness for AI.

docs.unrealengine.com

 뭐 얼마 적은거 같지도 않은데 시간은 시간대로 지나고 허리는 허리대로 아프군요. 예전처럼 2~3일에 한개씩 올리는건 좀 어려울거 같고 7~10일 정도에 한번씩 올릴거 같습니다. 시간을 내어서 이 글을 읽어주신 분들에게 감사의 말씀을 남기며 필자는 다음에 다시 돌아오겠습니다!

Posted by 별수집가
,