SimpleShooter(5) - BT / EnemyAI

JUSTICE_DER·2023년 8월 15일
0

Simple Shooter

목록 보기
5/8

위의 기능들을 배우고자 하였는데,
항목으로만 보면, 반 이상을 진행하였다.

강의는 대략 27개정도 남았는데, 이번주 3일안에 끝낸다.

1. Enemy AI

1-1. 기본 설정

1 AI를 구현하기 위해선, 가장 먼저 AIController를 만든다.

2 AIController CPP를 BP로 생성한다.
(상황에 따라)

3 아래처럼 BP Character의 AI Controller Class를 방금 만든
BP로 설정한다.

이 설정이 있는걸보니, Character를 NPC로 만드는 것이 일반적인것 같다

게임을 시작했을 때, 설정한 AIController가 존재하면 준비는 완료되었다.

1-2. AI가 플레이어 보기

  • AI가 플레이어를 쳐다보는 기능을 구현하려고 한다.
    이를 위해선 아래의 SetFocus기능을 사용하면 된다.

매개변수로는

  • 쳐다보기 위한 목표 Actor
  • 우선순위
    이렇게 존재하는데, 우선순위는

Character의 Actor를 AIController에서 접근할 수 있는 방법은 없다.
따라서 Character Actor를 World에서 찾아야만하는데,

GameplayStatic에 관련 함수가 존재한다.
PlayerPawn을 가져오는데,
싱글게임이므로 하나밖에 존재하지 않고,
따라서 인덱스 0이 무조건 현재 PlayerCharacter이다.

지나가도 되는 내용

AIController를 처음 생성하면 아무것도 없다.
따라서 BeginPlay를 직접 생성해야 했는데,
AAcotr클래스를 보면, protected에 virtual선언되어있어서,

똑같이 protected로 override한다.

그런데 왜 virtual을 계속 붙이는 걸까..
상속받은 경우까지 생각하는 걸까?
그러면 super에 super를 하면 최상위 beginplay가 실행되고,
그 다음 beginplay가 실행되는 상황이 되겠다.

SetFocus의 매개변수 중, 우선위는 default값인 2가 최대이므로
굳이 따로 설정하지 않는다.
BeginPlay에서 SetFocus를 설정해도, 단 한번만 쳐다보는게 아니라.
플레이 중에 지속해서 쳐다보도록 설정된다고 한다.

정상동작하는 모습

F8로 3인칭으로 보았더니,
벽 뒤에 멀리 떨어진 경우에도 Player의 방향을 보고 있다.

1-3. AI 경로찾기

NPC가 경로를 찾게 하기 위해선 2가지가 필요하다.

  • 걸을 수 있는 공간을 알려주는 Mesh (=NavMesh)
  • 경로를 찾는 알고리즘

경로탐색에서 가장 많이 사용하는 알고리즘은 a Star 이다.

A. NavMesh

정확한 명칭은 NavMeshBoundsVolume이다.

배치하게 되면,
상자 모양의 공간을 생성하고, 공간 내에 갈 수 있는 바닥을 검사한다.
검사하며, 너무 가파르기 때문에 못가는 지형이나, 벽들을 제외한다.

단축키 p를 누르면 어디가 이동할 수 있는 구역인지
시각적으로 확인할 수 있게 된다.

적절히 크기를 수정한다.
그냥 박스로 덮어버리면 사실 끝난다.

알아서 이동할 수 있는 바닥이 매핑되기 때문이다.

B. Algorithm

AIController에 기본으로 PathFollowingComponent가 부착되어있다.
따라서 Player를 따라오는 알고리즘을 굳이 구현할 필요는 없다.
그냥 함수를 쓰면 알아서 계산하고 이동까지 한다.

AIController의 대표적인 Move관련 함수는 위의 2개라고 볼 수 있겠다.

MoveToLocation은 지정된 벡터의 위치로 이동하는 것이고,
MoveToActor는 말 그대로 Actor를 향해 이동하는 것이다.

따라서 현재 사용할 것은 MoveToActor이다.
아래 들어간 숫자는 해당 Actor반경 어디까지 따라가서 멈출지를 정하는 것이다.

BeginPlay에서 사용하면, 시작하고 단 1번만 따라오게 된다.
따라서 Tick에 넣어서 지속적으로 따라오게 한다.
하지만 이 방법은 권장하는 방법이 아니고, 정석은 아니다.

일반적으로 AI NPC주변의 탐지영역을 만들고,
그 영역 내에 Player가 존재할 시에만 호출하는 방식을 사용할 수 있다.

따라온다.
하지만 끝까지 따라온다. 어디에 있어도 언젠가는 따라온다.
왜냐면 그렇게 설정했기 때문이다?

탐색 범위가 없다.

1-4. 경로 찾기 수정

탐색 범위를 사용해도 되지만, AI Contorller에 이미 내장된 다른 기능도 있다.

LineOfSightTo이고, 매개변수는 아래와 같다.

그렇게 수정한 코드.
Pawn을 가져오는 것 빼고, 모두 AIController의 기본 메서드를 사용하였다.

else부분은 빼도 크게 차이가 없다고 생각을 했는데, 아니었다.
그 이유는 MoveToActor에 의해 끝까지 쫓아오기 때문이다.
따라서 StopMovement로 적절히 멈춰주어야만 한다.

LineOfSightTo에 모든 매개변수를 채우지 않고, target인 Actor를 주기만 하면 된다.
원리는 ViewPoint부터 targetActor까지 LineTrace를 사용하여 확인하는 건데,
Viewpoint를 설정하지 않아도 알아서

위의 과정을 거쳐서 기본값에 바탕한 위치로부터
TargetActor까지의 Linetrace를 진행한다.

벽에 시야가 가로막혀서 LineTrace가 진행되지 않았고,
Focus초기화, Movement멈춤 된 모습

1-5. BehaviorTree / BlackBoard

해당 항목을 추가하기 전에, 기존 Tick함수에 구현한 내용을 모두 삭제한다.
그 이유는, BT와 BB로 AI의 모든 움직임을 관리할 것이기 때문이다.

BT와 BB를 생성한다.
같은 이름으로 생성하면 자동으로 위처럼
BehaviorTree에 BlackBoard가 매핑된다.

BT와 BB는 AnimGraph와 AnimInstance의 관계와 동일하다고 볼 수 있다.
BB의 변수값에 따라서 BT의 행동이 결정된다.

AIController에 BT 변수를 생성한다.
AIController에 방금 생성한 BT를 매핑한다.

이것으로 AIController와 BT-BB가 연결되었다.

GetBlackboardComponent를 하면 쉽게 BB를 가져올 수 있다.
해당 BB는 AIController와 연결된 BT로부터 가져와지는 것이다.

BB에 AnimInstance처럼 Vector변수를 생성한다.

TEXT값에 해당하는 이름의 BB 변수에 특정 Vector값을 세팅하는 코드이다.
여기서 Vector는 Player의 Location으로 두었다.

따라서 게임중에 BB의 변수에 접근하고 초기화할 수 있게 된다.

BehaviorTree를 위처럼 설정한다.
Sequence의 Decorator로 BlackBoard를 넣었다.

Decorator를 하나에만 넣은 이유는 Selector가 if문이기 때문이다.
Decorator가 붙은쪽이 참이면 실행, 아니면 오른쪽걸 실행한다.

Decorator의 ObserverAborts 설정으로 Both를 넣었다.
이렇게 되면, 해당 값을 감지하고, 값이 변경이 되었을 때,
조건을 다시 검사하고 조건에 맞지 않는 노드를 중지시킨다.
Self로 한다면, 본인것만 중지시키는 것이고,
LowerPriority면, 본인의 형제노드만 중지시키는 것이다.
따라서 Both를 사용한다.

다시보면,
PlayerLocation의 값이 IsSet되어있다면,
ChaseSequence를 실행한다.
그리고 PlayerLocation으로 이동한다.
그렇지 않다면, 같은 Selector에 연결된 다른 노드인
InvestigationSequence를 실행하고,
LastPlayerLocation으로 이동한다.

BlackBoard에 맞게 LineOfSight문에 작성한다.

PlayerLocation과 LastPlayerLocation은 모두 Player의 Location값을 저장한다.
하지만 PlayerLocation은 시야에 없다면 clear한다.
그러면 IsSet조건에 부합하지 않게 된다.

그러면 Investigation이 실행되고, LastPlayerLocation으로 이동하고 끝날 것이다.
왜냐면 LastPlayerLocation은 더이상 갱신이 되지 않는다.

BehaviorTree를 보면 예상한대로 실행된다.

1-6. BT / BB 응용

Task Node 클래스를 생성한다.
종류가 여러가지인데, 그 중 BlackBoard용을 고른다.

모듈을 추가해야만 TaskNode를 사용할 수 있다.

UBTTaskNode 헤더에 들어가보았다.

  • Execute - 해당 노드가 실행될 때 호출된다.
  • Abort - 조건이 거짓이 되어서 노드가 종료하는 경우에 호출된다.
  • TickTask - 노드가 실행중에 계속 호출되지만, 첫번째 tick은 실행x
    그 이유는 execute가 가장 첫번째 tick이기 때문이라고 함.
  • OnMessage - 메세지를 통해 노드를 종료하는 경우? 호출

TaskNode는 위의 4가지 기능을 주로 사용한다.
actor의 beginplay, tick 같은 존재라고 보면 되겠다

추가로 UBTTask_BlackboardBase 헤더에도 들어가 보았다.

해당 클래스에만 존재하는 기능이 있다.
GetSelectedBlackboardKey
이 기능때문에 이 클래스를 생성한 것이다.
해당 기능을 사용하면, 해당 클래스의 노드에 설정된 키값을 바로 접근할 수 있다.

위의 코드만 이해한다면 끝이다.

OwnerComp로 해당 노드를 가지고 있는 TreeComponent에 접근하고,
Tree로부터 BlackBoard Component에 접근한다.
그리고 BlackBoard의 특정 키의 값을 Clear할 것인데,
그 키는 아까 보았던 GetSelectedBlackboardKey를 통해 가져온다.

리턴값은 총 4가지인데, 사실 3가지로 봐도 된다.
Succeeded / Failed-Aborted / InProgress
성공끝 / 실패-중단 끝 / 실행중
이 중에서 InProgress는 노드가 TickTask에 의해 계속 실행되어야하는 경우 사용된다.
(그렇게 반복하다가 특정 조건되면 Succeeded로 나가겠지..)

BT는 위처럼 설정하였다. 정확히 이해하진 못해서 후에 정리한다.

2. BT 정리

Behavior Tree를 구성하면서 어떤 과정으로 동작하는지
헷갈리는 부분이 다수 존재했다.

우선 BT의 구조는 위와 같다.
Slector에 2개의 Sequence노드와 1개의 일반 TaskNode가 연결되어있다.

Selector에 의해 일단 첫번째 SequenceNode부터 실행된다.
Decorator가 걸려있고,

  • Notify는 Result
  • Abort는 Both

다음 노드로 Decorator를 걸었고,

  • Notify는 Result
  • Abort는 None

Sequence노드에 붙은 사용자정의 TaskNode로,
부착된 Key값을 Clear하는 역할을 한다.


위의 내용만 보면 사실 이해가 되지 않았다.
특히 이해가 되지 않았던 부분은 Decorator의 설정부분이다.

https://m.blog.naver.com/cloud2ind/220876361058
검색을 해봤고, 위에서 해설을 찾을 수 있었지만,
정확한 해설은 없었다.

노드에 있어서 Result가 뭔지, Value가 뭔지 모르겠다.

조건이 뭐고 블랙보드 키가 뭔데..
음.. 위에서 내가 유추한게 맞는 것 같다.
보통 블랙보드 키가 변경되면,
조건도 변경되기 때문에 당장은 상관이 없다만..
특히 조건이 범위가 아닌 IsSet이면 더더욱 그렇다.

Observer Aborts는 Decorator 조건에 따라 실행된다.
Self면 조건이 true였다가 false가 된 경우, 본인을 중지시킨다.
LowerPriority면 조건이 false여서 현재 노드를 건너뛰고,
다음 순위인 오른쪽 형제 노드가 실행되고 있다가,
조건이 true가 된 것을 감지했을 때, 현재 진행중인 형제노드를 중단시킨다.
Both는 두 경우 모두를 의미한다.

중단시킨다는 것의 의미는, 부모로 간다는 의미이고,
자식노드의 왼쪽부터 다시 진행한다는 의미이다.


BT의 동작하는 과정부터 살펴본다.

Selector에 의해 처음 노드가 실행된다.
하지만 Decorator가 걸려있어서 조건을 만족해야만 실행된다.
PlayerLocation이 IsSet인지가 조건이고,

PlayerLocation은 AIController에서 플레이어를 LineTrace하면
Set되게 된다.
따라서 Player가 시야에 존재하면 해당 노드는 무조건 실행된다.

실행된다면, 아래의 MoveTo TaskNode에 의해
PlayerLocation으로 이동하게 된다.

해당 Decorator를 Both로 설정한 이유는,
플레이어가 이동하고 Aborts가 없다면 MoveTo가 끝날 수 없기 때문이다.
그리고, 다른 형제 노드 진행중에도 조건만 맞으면
무조건 MoveTo를 진행하기 위해서이다.

하지만 시야에서 없다면?
AIController상에서 PlayerLocation의 값을 Clear한다.
그러면 해당 노드의 조건은 맞지 않게 되고,
첫노드의 Decorator의 ObserverAborts조건으로 Both가 걸려있으므로,
본인 노드를 중지하고 다시 Selector를 진행한다.
그러면 조건에 맞지 않아서 이번에는 2번째 노드가 실행된다.

Decorator는 처음에만 조건을 확인하게 되는 것이다..
만약에 Aborts조건이 None이었다면, 일단 한번 통과했으므로
조건을 이제 만족하던 만족하지 않던 계속 MoveTo를 실행하는 것이다.

2번째 노드는 Decorator 조건은 LastPlayerLocation IsSet.
항상 설정되어있는 값으로,
아래의 3개의 노드가 순차적으로 모두 진행되고서 상위노드로 올라간다.

올라가고선, 이제 Decorator를 만족하지 못하게 되므로,
우측의 형제노드인 MoveTo가 실행되게 된다.

큰 틀을 봐야한다.
Sequence는 왼쪽부터 진행하고, 모두 맞아야 완료된다.
Selector는 왼쪽부터 진행하고, 하나만 맞아도 완료된다.
완료된다면 부모노드로 간다.


그림으로 내가 이해한 내용을 정리해보았다.

  • Root - Selector - Sequence1
    조건이 true여서 Sequence를 진행하는데,
    진행하다가 조건이 false가 되면, abort로 중단시키고 나온다.
    나와서 형제노드로 가는 것이 아닌,
    Abort기 때문에 부모노드로 가서 Selector를 다시 진행한다.

  • Selector - Sequence1 - Sequence2 - TaskNode
    1번 노드의 조건이 false여서 다음 노드를 진행한다.
    2번 노드의 조건이 true여서 진행한다.
    진행하면서 2번 노드의 조건이 false가 되지만,
    abort설정이 None이기 때문에 Sequence를 따라서 계속 진행한다.
    Sequence를 정상적으로 마쳤으니 부모 노드로 돌아간다.
    그리고 다음 노드로 순서를 이동한다. (Selector기 때문)
    TaskNode를 실행한다.

  • 3번 노드를 마치고 Selector로 돌아간다.
    Selector는 다시 Root로 돌아간다. 모든 Tree가 끝났다.
    다시 진행한다.
    Root - Selector - TaskNode
    1번 노드의 조건이 false여서 다음 노드를 진행한다.
    2번 노드의 조건이 false여서 다음 노드를 진행한다.
    TaskNode를 실행한다.
    1번이나 2번의 조건이 영원히 갱신되지 않는다면,
    영원히 TaskNode만 반복해서 실행한다.

한 번 정리해두니 복잡한 AI Tree도 구현할 수 있을 것 같다.
Decorator부분의 정확한 해설없이 내 해석이 맞다는 전제하에

profile
Time Waits for No One

0개의 댓글