캐릭터와 무기에 대한 기능 구현이 어느 정도 완료되었으니 이제는 적군 AI를 만들어 볼 차례이다. 적군 AI을 만든다는 의미는 캐릭터가 [어떤 상태]에 [어떻게 행동] 할지를 규정한다는 것인데 바로 이 작업을 BT를 통해서 해결할 수 있다.

간단하게 BT를 설명하자면 트리 구조로 이루어져 있는 행동결정 알고리즘인데, 이 BT를 따르는 오브젝트가 주변 환경에 의해 하나의 상태로 진입하게 되면 그 상태에 해당하는 노드의 행동을 하도록 만드는 형식이다.

글을 더 읽기 전에, 아래 링크를 통해서 BT에 대한 기초 개념을 잡고 가면 좋을 것이다:)

기본적인 BT 설명

BT 심화

기본적인 BT 설명

재미를 위해 적군의 종류를 3가지(Rifle, Shotgun, Rocket Launcher)로 만들었는데 데미지 방식과 사정거리 정도가 다르고 모두 같은 BT 로직을 따른다.

전체 BT(크게 Patrol, Attack, Death 총 3가지 State)

Patrol 상태

적군은 Perception AI(시각을 주 감각기관으로 설정)에 자기와 다른 Tag를 가진 Character가 들어오면 이를 BlackBoard에 Target으로 결정하게 된다. 이 Target의 설정 여부를 체크하는 Decorator가 있는데 여기서 false값이 return되면 Patrol, true가 나오면 공격 State에 진입하도록 만든다.

Level에 Nav Mesh를 설치하게 되면, GetRandomLocationNavigableRadius라는 노드를 통해서 일정 반경 안에 있는 지점을 Random하게 정할 수 있다. 이를 적극 활용해서 <Random한 지점 설정> -> <그쪽으로 이동(AI Move To라는 노드를 언리얼에서 지원해준다. )> -> <대기> 의 사이클을 반복한다.

Attack 상태

Target이 설정되었다면 이제 공격 상태로 진입한다. Target 설정 이외에 2개의 Decorator를 추가로 더 넣었는데, 둘 다 캐릭터가 죽는 모션 뒤에 바로 Destroy 안돼서 들어가게 되었다(이는 의도한 것인데, 캐릭터가 죽은 다음에 바로 없어지면 부자연스럽기 때문이다).

Self Not Dead decorator는 자신의 생사 여부를 체크한다. 이를 통해 캐릭터가 죽으면 공격 모션을 멈출 수 있다. EnemyAliveStatus decorator는 죽은 적을 계속 공격하는 행동을 멈추기 위해 넣었다.

사실 이 부분에 핵심은 Parallel subtree에 있다. 여기서는 자신의 Target을 공격과 동시에 회전하는 부분이다. Parallel 노드의 왼쪽 자식을 메인 Task, 오른쪽 자식을 Background Task라 하는데 노드의 설정을 Immediate로 하면 메인 Task가 종료되자마자 Parallel을 빠져나오고 Delayed로 하면 Background Task가 끝날때까지 기다리게 된다.

왼쪽에 위치한 공격 Task에서는 무기 타입별로 다른 로직을 구현한다. 이에 대해서는 밑에서 더 설명하도록 하겠다.

Rotating Node에서는 적군 Actor의 회전 방향이 Target을 향하도록 설정한다. 여기에는 아주 유용한 노드들이 있는데 자신과 타겟의 Location Vector만 가지고 있으면 Find Look at Rotation을 통해 어디로 돌지 알 수 있고, RInterp To를 통해 부드럽게 회전, 최종적으로 Set Actor Rotation로 자신의 Rotation을 정한다.

사실상 Target의 위치는 계속 변하므로 Tick에서 이 로직을 처리해 주어야 하는데 이를 해결하는 방법은 아직 못찾아서 노드에 진입할시 실행되는 Event Receive Execute AI를 사용하였다. 내부 로직으로는 일정한 시간마다 event를 발동시키는 timer를 만든 다음에 custom event를 여기에 delegate로 등록시켜서 tick에서 처리하는 것처럼 만들어 보았다.

Death 상태

자신이 받은 데미지를 Event Any Damage를 통해서 감지하고 있다가 일정량이 넘어가게 되면 자신의 상태를 죽었다라고 BlackBoard에 기록한다. Self Dead Decorator로 이를 확인하고 맞으면 RagDoll 상태로 만들고 Level에서 Destroy한다.


캐릭터 종류

Shotgun

Shotgun은 무기 특성상 거리가 짧은 대신 한번 발사하면 넓은 범위를 타격하기 때문에 Capsule Trace By Channel로 collision 체크를 하고 맞은 대상에 대해서는 높은 데미지를 가하도록 하였다.

Rifle

Rifle은 Line Trace By Channel로 collision 체크를 하였다. 불쌍한 AI가 허겁지겁 달려와 캐릭터를 향해 몇발 쏘고 사망하는 모습이다.

Rocket Launcher

Rocket Launcher는 수류탄과 비슷하게 디자인했다. Rocket에 대한 별도의 BP를 만들어 공격시 Launcher muzzle 앞에 spawn이 된다. Projectile Movement, Cascade Particle, Radial Force 를 달아서 각각 spawn될때 초기 속도를 가지는 포물선 움직임, 연기 효과, 땅에 부딪혔을때 주변의 사물을 밀어내는 기능을 넣었다.

Rocket Launcehr 무기 특성상 가장 긴 사거리를 가지게 만들었다. Move To 노드에는 Acceptance Radius라는 필드가 있는데 Target에게 얼마나 가까이 접근할지에 대한 값이다. 위에서 설정한 Projectile Movement의 초기 속도 값과 Acceptance Radius 값을 서로 조절해가면서 어느 거리에서 어느 속도로 발사해야 Target이 피격될지에 대한 값을 구했다.

Rocket 발사체 BP


BT를 만들면서 해결했던 문제들

1. 타겟이 자신으로부터 일정 거리이상 떨어졌어도 계속 추격하는 문제

2. 자신이 공격한 타겟이 죽었음에도 해당 방향으로 계속 공격 모션이 되는 문제

3. 가장 멀리 있는 적군부터 타겟팅되는 문제

1. 타겟이 자신으로부터 일정 거리 이상 떨어졌어도 계속 추격하는 문제

언리얼의 Perception 시각 관련 AI에는 자신에 가시거리 안에 정보가 업데이트 될때 발생하는 event인 On Target Perception Updated와 반대로 가시거리 안에 있던 타겟이 없어지면 발생하는 이벤트인 On Target Perception Forgotten이 있다. 각각이 trigger 되는 거리는 AI Perception 디테일에서 조정 가능하다.

다만 한가지 주의할 점은 블루프린트에서 이 On Target Perception Forgotten을 그냥 쓸 수 없다! 프로젝트 폴더 > Config에는 defaultengine.ini라는 파일이 있는데 여기서 다음과 같은 텍스트를 추가해주어야 한다.

[/Script/AIModule.AISystem]
bForgetStaleActors=True

추가했다면, 타겟이 시야 반경에서 없어질때 On Target Perception Forgotten event가 들어오게 된다. Clear Value라는 노드로 BlackBoard에 존재했던 Target 값을 다시 초기화시켜준다.

사정거리를 벗어나면 다시 Patrol로 전환

2. 자신이 공격한 타겟이 죽었음에도 해당 방향으로 계속 공격 모션이 되는 문제

자연스러움을 위해서 캐릭터가 죽었을때 바로 없애지 않고 5초 정도 RagDoll 상태로 방치시킨 다음에 Level에서 Destroy하게 만들었다. 하지만 이 때문에 공격을 가하는 캐릭터 입장에서는 상대방이 죽어도 Target이 자신의 BlackBoard에 계속 남게 되어 공격 모션을 멈추지 않는 문제가 생겼다.

이를 위해서는 자신이 때리고 있은 Target의 BlackBoard에 접근해서 일정 시간마다 이 Target이 아직 살아있는지 확인하는 작업이 필요하다. 처음에는 Service 노드를 따로 만들어서 이를 해결하려고 했으나 특정 상황에서 모든 노드에 갈 수 없게 되면 BT자체가 멈춰 버린다는 상황에 직면하게 되었다(아래 그림에서 빨간색 줄이 그어진 박스들이 조건에 부합하지 않는 Decorator들이다.)

Deadlock에 걸려서 BT가 멈춰버린 모습

그래서 정보를 얻어오는 로직을 캐릭터 BP에 구현하기로 하였다. Tick을 사용하면 너무 로드가 쎌 것 같아서 캐릭터의 BeginPlay에 Set Timer By Event 노드를 만들고 일정 시간마다 Delegate에 의해 발동되는 custom event를 만들었다.

3. 가장 멀리 있는 적군부터 타겟팅되는 문제

기존에 Target 세팅을 할때 On Target Perception Updated로 인식되는 Actor를 계속 새로운 Target으로 설정하였다. 하지만, 자신에게 가까운 적을 우선적으로 때려야지 먼 적을 우선적으로 타겟팅하는 것은 말도 안됨으로 이를 수정하는 작업을 하였다.

수정 작업은 매우 간단했다. Target 값이 없으면 On Target Perception Updated로 인지된 값을 Target으로 설정하고 이미 값이 있었으면 그냥 무시한다.


결과

Alpha Team VS Bravo Team

캐릭터의 TeamType을 결정하는 Enum을 하나 만들고 Alpha 혹은 Bravo 중 하나의 팀에 속하게 만들었다.

Fight Scene 1

Fight Scene 2

이 실험을 하면서 젤리처럼 생긴 우리 귀여운 캐릭터들을 너무 많은 죽여서 미안하다...

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글