비헤이비어 트리 모델을 사용해 컴퓨터 인공지능을 설계하고
인공지능에 의해 스스로 정찰하고 플레이어를 쫓아와 공격하는 NPC 제작
<1>. AIController와 내비게이션 시스템
언리얼 엔진에는 컴퓨터가 인공지능으로 NPC를 제어하도록 'AI 컨트롤러'를 제공한다.
→ AIController를 상속받는 "ABAIController" 클래스 생성
//ABCharacter.cpp
#include "ABAIController.h"
AABCharacter::AABCharacter()
{
AIControllerClass = AABAIController::StaticClass();
//AIControllerClass의 클래스 속성을 ABAIController 의 클래스로 정한다.
AutoPossesAI = EAutoPossessAI::PlacedInWorldOrSpawned;
//플레이어 조종 캐릭터 외 모든 캐릭터는 ABAIController의 지배를 받는다.
}
※ 내비게이션 메시 : NPC가 스스로 움직이게 할 수 있게끔 해주는 장치
프로젝트 추가 → 볼륨 → NavMesh Bounds Volume 배치. (10000 x 10000 x 500cm) → P키를 눌러 NavMesh 시각화
내비 메시 영역을 생성하여 NPC가 목적지까지 3초마다 스스로 움직이도록.
※ 내비게이션 시스템 제공 함수
- "GetRandomPointInNavigableRadius" : 이동 가능한 목적지를 랜덤으로 가져오기
- "SimpleMoveToLocation" : 목적지로 폰을 이동시키기
<2>. 비헤이비어 트리 시스템
좀 더 복잡한 NPC의 행동 패턴을 구현하려면 비헤이비어 트리 모델을 써야 한다.
※ 비헤이비어 트리 : NPC가 해야 할 행동을 분석하고, 우선순위가 높은 행동부터 실행할 수 있게 하는 트리 구조 설계기법.
비헤이비어 트리를 제작하기 위해서는 '비헤이비어 트리 에셋'과 '블랙보드 에셋'을 생성해야 한다.
- 블랙보드 에셋 : 인공지능의 판단에 사용하는 데이터 집합. 이 데이터를 기반으로 NPC가 의사결정을 함.
- 비헤이비어 트리 에셋 : 블랙보드 데이터에 기반해 설계한 비헤이비어 트리의 정보를 저장한 에셋.
- Wait 태스크를 하나 생성한다 → 폰에게 지정한 시간 동안 대기하라는 명령을 내림
- 태스크는 독립적으로 실행될 수 없고 반드시 '컴포짓 노드'를 거쳐 실행돼야 한다. 컴포짓 노드에는 렉터, 시퀀스가 있는데 시퀀스 컴포짓을 루트에 연결한다.
※ 시퀀스 컴포짓 : 연결된 태스크들이 False가 나올 때까지 왼쪽에서 오른쪽으로 태스크를 계속 실행한다.
우리가 생성한 블랙보드, 비헤이비어 트리 에셋을 ABAIController가 사용하도록 코드를 추가한다.
→ ABAIController를 가동하면 비헤이비어 트리 에셋과 같은 폴더에 위치한 블랙보드 에셋, 비헤이비어 트리도 함께 동작한다. 비헤이비어 트리 에셋을 더블 클릭 하면 로직을 디버그 할 수 있다.
※ NPC의 순찰 기능을 구현하려면 2가지 데이터가 필요하다.
- NPC가 생성됐을 때의 위치 값. (블랙보드에서 이를 Vector 타입으로 키를 생성하고 HomePos라는 이름을 부여한다.)
- NPC가 순찰할 위치 정보를 보관할 블랙보드 키. (Vector로 키를 생성한 후, PatrolPos라는 이름을 부여한다.)
AI컨트롤러에서 블랙보드의 'HomePos' 키 값을 지정하도록 C++ 로직을 구현한다.
//ABAIController.h
class ARENABATTLE_API AABAIController:public AAIController
{
public:
static const FName HomePosKey;
static const FName PatrolPoskey;
}
//ABAIController.cpp
#include "BehaviorTree/BlackboardComponent.h"
const FName AABAIController::HomePosKey(TEXT("HomePos"));
const FName AABAIController::PatrolPosKey(TEXT("PatrolPos"));
void AABAIController::Possess(APawn* InPawn)
{
Super::Possess(InPawn);
if(UseBlackboard(BBAsset, Blackboard))
{
Blackboard->SetValueAsVector(HomePosKey, InPawn->GetActorLocation());
if(!RunBehaviorTree(BTAsset))
{
ABLOG(Error);
}
}
}
HomePos 키 값이 블랙보드에 잘 전달된다면, 다음으로 'PatrolPos' 데이터를 생성해야 한다.
→ 이는 순찰할 때마다 바뀌므로 태스크를 제작해 블랙보드에 값을 쓰도록 설계하자.
※ 비헤이비어 트리는 태스크를 실행할 때 태스크 클래스의 'ExecuteTask'라는 멤버 함수를 실행한다. 이 함수는 다음 4가지 중 하나의 값을 반환해야 한다.
- Aborted : 태스크 실행 중에 중단됐다. 결과적으로 실패했다.
- Failed : 태스크를 수행했지만 실패했다.
- Succeeded : 태스크를 성공적으로 수행했다.
- InProgress : 태스크를 계속 수행하고 있다. 결과는 향후 알려줄 예정이다.
이 결과에 따라, 컴포짓 내에 있는 다음 태스크를 계속 수행할지, 중단할지가 결정된다.
→ 다음 정찰 지점을 찾는 로직을 구현하고 바로 결과를 반환하도록 구현한다 (FindPatrolPos 태스크)
<3>. NPC의 추격 기능 구현
이번에는 NPC가 정찰 중에 플레이어를 발견하면 플레이어를 추격하도록 기능을 추가한다.
※ NPC가 플레이어를 발견할 때 플레이어 정보를 블랙보드에 저장하도록 Object 타입으로 Target 변수를 생성한다. 기반 클래스는 ABCharacter로 지정한다.
※ NPC는 행동을 추격, 정찰 중 하나를 선택해 행동하기 때문에 '셀렉터' 컴포짓을 사용해 로직을 확장하자.→ 추격에 더 우선권을 주고, 추격 로직은 Target을 향해 이동하도록 비헤이비어 트리 설계를 한다.
!!(왼쪽에 둘 수록, 순서가 빨라지고 이는 즉 우선순위가 높다는 뜻임.)
플레이어가 일정 반경 내에 있으면 이를 감지해 추격하는 기능을 넣어보자.
→ 플레이어를 감지하는 서비스 노드를 생성하고, 이를 셀렉터 컴포짓에 추가하면 비헤이비어 트리는 플레이어를 감지하는 루틴을 계속 반복한다.
※ 서비스 노드 : 독립적으로 동작하지 않고 컴포짓 노드에 부착되는 노드
→ 서비스 노드는 자신이 속한 컴포짓 노드가 활성화될 경우 주기적으로 TickNode 함수를 호출한다. 그 주기는 Interval 속성 값으로 지정할 수 있다.
//BTService_Detect.h
#include "ArenaBattle.h"
class ARENABATTLE_API UBTService_Detect : public UBTService
{
public:
UBTService_Detect();
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
//BTService_Detect.cpp
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"
UBTService_Detect::UBTService_Detect()
{
NodeName = TEXT("Detect");
Interval = 1.0f;
}
void UBTService_Detect::TickNode(UBehaviorTreeComponents OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if(nullptr == ControllingPawn) return ;
UWorld* World = ControllingPawn->GetWorld();
FVector Center = ControllingPawn->GetActorLocation();
float DetectRadius = 600.0f;
if(nullptr == World) return ;
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParam(NAME_None, false, ControllingPawn);
bool bResult = World->OverlapMultiByChannel(
OverlapResults,
Center,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel2,
FCollisionShape::MakeSphere(DetectRadius),
CollisionQueryParam
);
DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}
이렇게 서비스가 만들어지면 컴포짓을 우클릭하고 Detect를 선택해 컴포짓에 부착한다.
이렇게 해서 NPC가 탐지하다 캐릭터를 감지하면, 우리가 조종하는 캐릭터만 추려내야 한다.
→ 우리가 조종하는 건지 파악하기 위해 IsPlayerController 함수를 사용.
→ 플레이어를 감지하면 블랙보드의 Target 값을 플레이어 캐릭터로 지정하고, 그렇지 않으면 nullptr 지정.
→ 플레이어를 감지하면 녹색 구체를 그리고, NPC와 캐릭터까지 연결된 선을 추가로 그려준다.
이제 블랙보드의 Target 키에 값이 있는지 없는지에 따라 셀렉터 데코레이터 왼쪽의 추격과 오른쪽의 정찰 로직이 나뉜다.
→ 왼쪽 시퀀스 컴포짓에 블랙보드 값을 기반으로 실행 여부를 결정하는 데코레이터 노드를 삽입한다.
→ 이 데코레이터에서 노티파이 옵서버 값 변경 / 관찰자 중단 옵션 설정.
→ 오른쪽 시퀀스 컴포짓도 똑같이!
<4>. NPC의 공격
왼쪽의 추격 로직을 발전시켜 플레이어를 따라잡으면 공격하는 기능을 추가해 본다.
왼쪽에서 추격 / 공격으로 나뉘기 때문에 왼쪽 로직을 다시 두 갈래로 확장해 준다.
→ 왼쪽 시퀀서 컴포짓을 셀렉터 컴포짓으로 바꿔준다. → 기존 왼쪽 시퀀스 컴포짓에 설정했던 데코레이터를 셀렉터 컴포짓에 드래그해 옮긴다.
※ 이번엔 블랙보드의 값을 참조하지 않고 플레이어가 공격 범위 내에 있는지 판단하는 데코레이터를 하나 생성한다.
데코레이터 클래스는 'CalculateRawConditionValue' 함수를 상속받아 원하는 조건이 달성됐는지를 파악할 수 있다.
이 함수는 const로 선언돼 데코레이터 클래스의 멤버 변수 값은 변경할 수없다.
왼쪽을 끝냈으니, 오른쪽도 동일한 데코레이터를 추가한 후, Inverse Condition(반대로) 속성 체크해 준다.
※ 공격 태스크를 생성하는 법
공격 태스크는 공격 애니메이션이 끝날 때까지 대기해야 하는 지연 태스크이므로 ExecuteTask의 결과 값을 InProgress로 일단 반환하고, 공격이 끝났을 때 태스크가 끝났다고 알려줘야 한다. 이를 알려주는 함수가 'FinishLatentTask'이다.
→ 이 함수를 호출할 수 있도록 노드의 Tick 기능을 활성화하자.
문제가 발생한다. NPC가 플레이어를 제자리에서 공격하기 때문에, 플레이어가 NPC 뒤로 돌아가도 계속 같은 곳을 공격.
→ 이를 보완하기 위해 공격과 동시에 플레이어를 향해 회전하는 기능을 추가.
회전 태스크를 완성하면 공격 로직에서 사용한 시퀀스 컴포짓을 '심플 패러럴 컴포짓'으로 대체한 후, 캐릭터의 공격을 메인 태스크로, 회전을 보조 태스크로 지정한다. 심플 패러럴 컴포짓에 의해 캐릭터는 공격과 캐릭터를 향해 회전하는 태스크를 동시에 실행한다.
'게임 개발 > 이득우의 언리얼 C++' 카테고리의 다른 글
[이득우의 언리얼 C++ 게임개발의 정석] #14. 게임플레이의 제작 (0) | 2024.07.03 |
---|---|
[이득우의 언리얼 C++ 게임개발의 정석] #13. 프로젝트의 설정과 무한 맵의 제작 (0) | 2024.07.02 |
[이득우의 언리얼 C++ 게임개발의 정석] #11. 게임 데이터와 UI 위젯 (0) | 2024.06.26 |
[이득우의 언리얼 C++ 게임개발의 정석] #10. 아이템 상자와 무기 제작 (0) | 2024.06.25 |
[이득우의 언리얼 C++ 게임개발의 정석] #9. 충돌 설정과 대미지 전달 (0) | 2024.06.22 |