간단한 AI 의사결정 모델 구현
지금까지 우리는 비헤이비어 트리, EQS, 내비게이션, 그리고 센서 시스템이라는 AI 구축의 핵심 도구들을 개별적으로 살펴보았습니다. 이제 이 모든 요소들을 결합하여, 플레이어를 감지하고, 추적하고, 공격하거나, 플레이어를 놓치면 순찰하는 간단한 AI 의사결정 모델을 구현해보겠습니다. 이는 AI가 '인지 → 판단 → 행동'의 순환을 통해 작동하는 기본적인 패턴을 보여줄 것입니다.
AI 의사결정 모델의 목표
우리가 구현할 AI는 다음과 같은 의사결정 과정을 따릅니다.
- 초기 상태: 순찰(Patrol) 또는 대기(Idle)
- 플레이어 감지: AI Perception을 통해 플레이어를 시야 또는 청각으로 감지합니다.
- 추적/전투: 플레이어가 감지되면 플레이어를 추적하고, 일정 거리 내에 들어오면 공격합니다.
- 플레이어 놓침: 플레이어를 시야/청각에서 놓치면, 마지막으로 감지했던 위치로 이동하여 탐색합니다.
- 탐색 실패: 마지막 감지 위치에서도 플레이어를 찾지 못하면 다시 순찰 상태로 돌아갑니다.
시스템 구현을 위한 준비물
- 플레이어 캐릭터:
BP_PlayerCharacter
(AI Perception Stimuli Source가 설정되어 있어야 함) - AI 폰/캐릭터:
BP_EnemyCharacter
(NavMesh 위에 배치될 것임) - AI 컨트롤러:
AIC_Enemy
(AI Perception Component 및Run Behavior Tree
설정) - 블랙보드:
BB_EnemyAI
(필요한 키:TargetActor
(Object),TargetLocation
(Vector),IsPlayerDetected
(Boolean),PatrolLocation
(Vector)) - 비헤이비어 트리:
BT_EnemyAI
(AI의 행동 로직) - 내비메시 볼륨: 레벨에 배치되어 AI가 이동할 수 있는 영역 설정
구현 단계별 가이드
이전 절에서 다룬 내용을 바탕으로 각 요소를 이미 설정했다고 가정하고, 이제 이들을 비헤이비어 트리에 결합하는 데 중점을 둡니다.
블랙보드 키 추가
BB_EnemyAI
블랙보드에 다음 키들을 추가합니다.
TargetActor
(Object, Base Class:Actor
)TargetLocation
(Vector)IsPlayerDetected
(Boolean) - AI가 플레이어를 감지했는지 여부PatrolLocation
(Vector) - AI가 순찰할 목표 위치
AI 컨트롤러 (AIC_Enemy
) 설정
AIC_Enemy
의 AI Perception
컴포넌트에서 On Target Perception Updated
이벤트를 처리하여 블랙보드를 업데이트합니다.
- 플레이어 감지 시 (
Successfully Sensed
가 True):TargetActor
키에 감지된 플레이어 액터(BP_PlayerCharacter
)를Set Blackboard Value as Object
로 저장.TargetLocation
키에 플레이어의 현재 위치(Get Actor Location
)를Set Blackboard Value as Vector
로 저장.IsPlayerDetected
키를Set Blackboard Value as Bool
로True
로 설정.
- 플레이어 놓침 시 (
Successfully Sensed
가 False):TargetActor
키를Clear Blackboard Value
로 지움.IsPlayerDetected
키를Set Blackboard Value as Bool
로False
로 설정.- 중요:
TargetLocation
은 플레이어를 놓친 마지막 위치이므로, 당장 지우지 않고 탐색에 활용합니다.
비헤이비어 트리 (BT_EnemyAI
) 구현
이제 BT_EnemyAI
를 열고 다음 구조로 AI의 의사결정 모델을 만듭니다.
Root
|
Selector (AI의 메인 의사결정, 우선순위: 전투/탐색 > 순찰)
|
+--- Sequence (플레이어 추적/공격)
| |
| +--- Decorator: Blackboard Based Condition (IsPlayerDetected == True)
| | (플레이어가 감지되었을 때만 이 브랜치를 실행)
| |
| +--- Task: Move To (Blackboard Key: TargetActor, Acceptable Radius: 150)
| | (플레이어에게 접근)
| |
| +--- Task: Attack Player (새로운 커스텀 태스크 블루프린트)
| | (공격 로직 - 애니메이션 재생, 데미지 적용 등)
| |
| +--- Service: Update Target Location (플레이어 위치를 지속적으로 TargetLocation에 업데이트)
| (부모인 Sequence 노드에 붙여, 해당 시퀀스가 실행되는 동안 주기적으로 실행되도록)
| (블랙보드 키: TargetActor에서 Get Actor Location을 가져와 TargetLocation에 저장)
|
+--- Sequence (플레이어 탐색)
| |
| +--- Decorator: Blackboard Based Condition (IsPlayerDetected == False AND TargetLocation != Empty)
| | (플레이어를 놓쳤지만 마지막 위치 정보가 있을 때)
| |
| +--- Task: Move To (Blackboard Key: TargetLocation, Acceptable Radius: 50)
| | (마지막 감지 위치로 이동)
| |
| +--- Task: Wait (3.0초 대기)
| | (해당 위치에서 잠시 탐색)
| |
| +--- Task: Clear Last Known Location (새로운 커스텀 태스크 블루프린트)
| (탐색을 마쳤으므로 TargetLocation 키를 Clear Blackboard Value로 지움)
|
+--- Sequence (순찰/대기)
|
+--- Task: Find Patrol Location (새로운 커스텀 태스크 블루프린트)
| (현재 위치 주변의 랜덤한 이동 가능한 지점을 찾아 PatrolLocation에 저장)
| (NavSystemLib의 Get Random Reachable Point in Radius 노드 활용)
|
+--- Task: Move To (Blackboard Key: PatrolLocation, Acceptable Radius: 100)
| (순찰 지점으로 이동)
|
+--- Task: Wait (5.0초 대기)
(잠시 대기 후 다음 순찰 지점 탐색)
새로운 태스크 블루프린트 생성
위 비헤이비어 트리 구조에 필요한 커스텀 태스크를 생성합니다.
-
BTTask_AttackPlayer
(공격 태스크):- 콘텐츠 브라우저에서 마우스 우클릭 >
블루프린트 클래스(Blueprint Class)
>All Classes
에서BTTask_BlueprintBase
를 검색하여 선택합니다. - 그래프 탭:
Receive Execute AI
이벤트를 오버라이드합니다. - 여기서 공격 애니메이션을 재생하고 (애님 몽타주 재생), 공격 타이밍에
Apply Damage
노드를 호출하는 등의 로직을 구현합니다. - 태스크 완료 시
Finish Execute
노드를 호출하고Success
를True
로 설정합니다.
- 콘텐츠 브라우저에서 마우스 우클릭 >
-
BTTask_ClearLastKnownLocation
(마지막 위치 지우기 태스크)BTTask_BlueprintBase
를 상속받아 생성합니다.- 그래프 탭:
Receive Execute AI
이벤트를 오버라이드합니다. Get Blackboard Component
노드를 호출하여 블랙보드 참조를 얻습니다.- 블랙보드 참조에서
Clear Value
노드를 호출하고Key Name
핀에TargetLocation
(키 이름)을 연결합니다. Finish Execute
노드를 호출하고Success
를True
로 설정합니다.
-
BTTask_FindPatrolLocation
(순찰 위치 찾기 태스크)BTTask_BlueprintBase
를 상속받아 생성합니다.- 그래프 탭:
Receive Execute AI
이벤트를 오버라이드합니다. Get AI Controller
>Get Controlled Pawn
노드를 통해 AI의 현재 위치를 얻습니다.Get Random Reachable Point in Radius
노드를 호출합니다.Origin
은 AI의 현재 위치.Radius
는 순찰 반경 (예: 1000.0).
- 찾아낸 랜덤 위치를
Get Blackboard Component
노드를 통해Set Value as Vector
노드로PatrolLocation
블랙보드 키에 저장합니다. Finish Execute
노드를 호출하고Success
를True
로 설정합니다.
BTService_UpdateTargetLocation
플레이어를 추적하는 동안 플레이어의 위치를 지속적으로 업데이트하는 서비스입니다.
- 서비스 블루프린트 생성
- 콘텐츠 브라우저에서 마우스 우클릭 >
블루프린트 클래스(Blueprint Class)
>All Classes
에서BTService_BlueprintBase
를 검색하여 선택합니다. - 이름을
BTS_UpdateTargetLocation
으로 지정합니다.
- 콘텐츠 브라우저에서 마우스 우클릭 >
- 그래프 탭
Receive Tick AI
이벤트를 오버라이드합니다.Receive Tick AI
이벤트 노드를 선택하고 디테일 패널에서Tick Interval
을0.5
초 정도로 설정합니다. (너무 자주 업데이트하면 성능 저하)Controlled Pawn
핀에서Cast To Character
(또는Cast To BP_EnemyCharacter
)를 연결합니다.Owner Controller
핀에서Get Blackboard Component
노드를 호출합니다.Blackboard Component
에서Get Value as Object
노드를 호출하고Key Name
핀에TargetActor
를 연결합니다.Get Value as Object
의Return Value
에서Cast To BP_PlayerCharacter
를 연결합니다.- 캐스팅 성공 시,
As BP Player Character
핀에서Get Actor Location
노드를 호출합니다. - 이 위치를
Blackboard Component
의Set Value as Vector
노드를 통해TargetLocation
블랙보드 키에 저장합니다. - 컴파일 및 저장합니다.
레벨 배치 및 테스트
- 레벨에
Nav Mesh Bounds Volume
이 AI 이동 영역을 포함하도록 배치하고P
키를 눌러 내비메시를 확인합니다. BP_EnemyCharacter
를 레벨에 배치합니다.- 게임을 플레이하고 AI의 행동을 관찰합니다.
- AI가 순찰하다가 플레이어를 발견하면 추적하고 공격합니다.
- 플레이어가 숨거나 AI의 시야/청각 반경을 벗어나면, AI는 마지막으로 플레이어를 본 위치로 이동하여 잠시 탐색합니다.
- 탐색에도 실패하면 다시 순찰을 시작합니다.
- 디버깅
- 게임 플레이 중
~
키를 눌러 콘솔을 열고ai.debugdrawpath 1
을 입력하여 AI의 이동 경로를 확인합니다. ai.blackboard
를 입력하여 AI의 블랙보드 값을 확인합니다.- 비헤이비어 트리 에디터에서
디버그(Debug)
드롭다운 메뉴를 통해 실행 중인 AI를 선택하고, 트리의 활성화된 노드를 실시간으로 확인합니다.
- 게임 플레이 중
추가적인 개선 사항
이 모델은 기본적인 의사결정의 예시이며, 실제 게임에서는 더욱 복잡한 로직이 추가됩니다.
- 다양한 공격 패턴: 근접 공격, 원거리 공격, 스킬 사용 등을
Attack Player
태스크 내부에서 또는 별도의 태스크로 분리하여 구현. - 회피 및 엄폐: EQS를 활용하여 엄폐 위치를 찾고 이동하는 로직 추가.
- 피해 반응: 데미지를 입었을 때 특정 애니메이션을 재생하거나, 후퇴하는 행동 추가.
- 그룹 AI: 여러 AI가 협동하거나 경쟁하는 로직 구현.
- 감지 수준: 플레이어의 행동(예: 웅크리기, 빠르게 달리기)에 따라 감지될 확률이나 거리가 달라지도록 설정.
이번 절에서는 언리얼 엔진의 비헤이비어 트리, 블랙보드, AI Perception, 내비게이션 시스템을 통합하여 간단하지만 기능적인 AI 의사결정 모델을 구현하는 방법에 대해 알아보았습니다. AI가 환경을 인지하고, 판단하며, 행동하는 이 기본적인 순환은 모든 복잡한 AI 시스템의 기반이 됩니다.
이로써 8장 'AI 시스템 구축'의 모든 내용을 마쳤습니다.