Post List

2017/11/29

Unreal4__4_몬스터AI__따라오기_공격하기_죽음

오늘은  아주아주x100 간단한 몬스터 AI관련해서 만들어 보았습니다.
몬스터는 가장 가까운 거리에 있는 플레이어를 탐색하고 접근을 하게 됩니다! 

그리고 일정 거리내로 좁혀지면 공격을 하게 되는 AI랍니다.

언리얼 엔진에서 인공지능 제작용으로 제공되는 BehaivorTree몬스터가 플레이어를 인식하고 움직일  있도록 해주는 AIController 클래스를 가지고 AI 구현해보겠습니다.

에디터에서 AAIController 부모로하는 클래스를 하나 추가하겠습니다클래스 이름은 JAIController 하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
class UBehaviorTreeComponent;
class UBlackboardComponent;    
UCLASS()
class JOYLAND_API AJAIController : public AAIController
{
private:
    UPROPERTY(transient)
        UBlackboardComponent* BlackboardComp;
    UPROPERTY(transient)
        UBehaviorTreeComponent* BehaviorComp;
cs
AIController는 BlackboardComponent와 BehaviorTreeComponent를 멤버변수로 가지고 있습니다.
여기서 BehaviorTreeComponent는 AI의 프로세서이며, 의사결정을 내리고 이를 토대로 실행에 옮기기 위한 컴포넌트이며, BlackboardComponent는 AI의 기억장소로 BehaviorTree가 사용할 데이터값들을 저장하고 있습니다.

에디터로 돌아와서  블랙보드를 생성합니다. 마우스 우클릭하면 인공지능>블랙보드가 있습니다!. 생성한 블랙보드를 열어주면!

블랙보드 창이 하나 뜨고 몬스터의 타겟을 지정해줄 키(Key)를 추가해줍니다

오른쪽에 블랙보드 디테일에서 Key Type을 JPlayer 지정해줍니다몬스터의 타겟이 플레이어가 되게 하기위해서이죠. 

블랙보드를 저장하고 블랙보드를 생성한 처럼 비헤이비어트리도 생성해 줍니다. 생성한 비헤이비어트리 이름을 'Monster_BHT'로 변경해줍니다.
비헤이비어트리를 작업하기전에 JAIController 클래스에서 먼저 해야할 작업이 있습니다. 

비쥬얼스튜디오로 돌아와 JAIController.h을 열어줍니다.
1
2
3
4
5
6
7
// AJAIController
public:
    virtual void Possess(APawn* InPawn) override;
protected:
// 에디터의 블랙보드에서 설정한 key id를 받아와 저장한다.
    int32 iEnemyKeyID;

cs
코드를 보시면 함수 하나, 변수 하나가 추가되었습니다.
Possess()함수는 몬스터로 사용할 폰에 이 컨트롤러를 붙여주는 함수입니다.
iEnemyKeyID는 블랙보드에 지정한 Key ID를 가져와 저장하게 되는 변수입니다. 

그리고 몬스터 클래스에 비헤이비어 트리 변수를 추가해주어야 합니다.
여기서 추가된 비헤이비어 트리 변수가 가질 데이터는 에디터에서 추가한 Monster_BHT가 되겠습니다.
1
2
3
4
//JMonster.h
public:
    UPROPERTY(EditAnywhere, Category = Behavior)
    class UBehaviorTree* MonBehavior;
cs
몬스터클래스에 추가도 했으니 다시 JAIController로 돌아와 Possess함수를 만들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void AJAIController::Possess(APawn* InPawn)
{
    Super::Possess(InPawn);
    AJMonster* pMon = Cast<AJMonster>(InPawn);
    if (pMon && pMon->MonBehavior)
    {
        if (pMon->MonBehavior->BlackboardAsset)
        {
            BlackboardComp->InitializeBlackboard(*pMon->MonBehavior->BlackboardAsset);
        }
        // 블랙보드에 지정한 Key ID를 가져와 저장한다.
        iEnemyKeyID = BlackboardComp->GetKeyID("Monster");
        // BehaviorTree를 실행시킨다.
        BehaviorComp->StartTree(*(pMon->MonBehavior));
    }
}
cs
아까도 말했다시피 Possess함수는 몬스터에 이 컨트롤러를 붙여주는 함수입니다. 여기서 인자로 몬스터의 주소값을 넘겨받습니다.
아까 몬스터 클래스에서 추가한 BehaviorTree변수를 가져와 BlackboardComponent와 연결시켜주고, 몬스터의 블랙보드에서 지정했던 KeyID를 가져와 iEnemyKeyID에 저장을 합니다. 이 값으로 이따가 적을 탐색할때 사용되는 변수입니다.
그리고 마지막으로 BehaviorComponent로 StartTree를 호출해줌으로써 몬스터의 BehaviorTree를 실행시킵니다.

이제 AIController와 몬스터의 BehaviorTree를 연결하였으니, 몬스터의 BehaviorTree에서 사용될 AI기능을 만들어보겠습니다.
AI기능은 2가지이며, 첫번째는 적을 탐색하고 가까이 다가가기. 두번째는 일정범위내에 있는 적을 공격하기 입니다.

기능에 대한 구현은 C++에서 구현하고 해당함수를 비헤이비어 트리에서 호출하는 식으로 만들어 보겠습니다.

AIController.h에 두개의 함수를 추가해줍니다.
1
2
3
4
5
6
7
//AJAIController.h
    UFUNCTION(BlueprintCallable, Category = Behavior)
        void FindClosetEnemy();
    UFUNCTION(BlueprintCallable, Category = Behavior)
        void AttackEnemy();
cs
UFUNCTION()메크로를 추가하고, 에디터에서 호출될 수 있도록 설정해줍니다.
FindClosetEnemy()함수를 먼저 살펴보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void AJAIController::FindClosetEnemy()
{
    const FVector myLocation = myMon->GetActorLocation();
    float bestDistSq = 25600.0f;
    AJPlayer* bestPawn = nullptr;
    for (FConstPawnIterator it = GetWorld()->GetPawnIterator(); it; ++it)
    {
        AJPlayer* enemyPawn = Cast<AJPlayer>(*it);
        if (testPawn != nullptr)
        {
            const float DistSq = (enemyPawn ->GetActorLocation() - myLocal).SizeSquared();
            if (DistSq < bestDistSq)
            {
                bestDistSq = DistSq;
                bestPawn = enemyPawn ;
            }
        }
    }
    if (bestPawn)
    {
        SetEnemy(bestPawn);
    }
}
cs
여기서는 간단한 수학지식이 필요합니다. 몬스터가 탐색할 수 있는 최대 거리(bestDistSq)를 지정한 후, 몬스터와 적(플레이어)의 거리를 계산합니다. 거리계산은 적의 위치벡터와 몬스터의 위치벡트를 뺀 후 SizeSquared()함수를 호출하여 길이로 변환을 합니다. 
그리고 bestDistSq와 비교 후 bestDistSq보다 짧을경우 현재 탐색된 적과의 거리로 변경 후 다른 적을 또 탐색합니다. 그리고 그 중 제일 짧은 거리에 위치한 적을 타겟으로 
지정하게 되며, 적에 다가가기 위해 움직이기 시작합니다.

1
2
3
4
5
6
    if (BlackboardComp)
    {
        BlackboardComp->SetValue<UBlackboardKeyType_Object>(iEnemyKeyID, InPawn);
        SetFocus(InPawn);
    }
cs
그리고 여기서 구현부의 마지막을 보면 SetEnemy(bestPawn); 구문이 있습니다.
이 부분은 몬스터에게 공격할 대상을 지정해줄 수 있는 함수로, 블랙보드컴포넌트의SetValue()함수를 통해 아까 얻어온 iEnemyKeyID값과, 타겟된 적의 주소값과 같이 넘겨줍니다. 적의 값을 넘겨줌으로써 몬스터 비헤이비어트리가 동작하게끔 값을 넣어주는것이지요.

그리고 AttackEnemy()함수도 타겟된 적과의 공격할 수 있는 거리로 좁혀지게 되면
해당 몬스터는 적을 향해 공격을 시작합니다. 내부 구현은 설명만으로도
쉽게 구현 가능하니 생략하겠습니다.

이제 C++에서 추가한 기능들을 에디터에서 비헤이비어 트리에 추가해보겠습니다.
위에서 생성한 비헤이비어 트리를 열어주면 '루트'라는 노드가 있습니다. 노드의 네모난 검정부분을 드레그하면 Composites에 Selector를 추가해줍니다.

C++ 클래스인 JAIControll에서 추가한 함수들을 사용할 수 있도록 추가해주어야 하는데, 상단에 새 서비스를 클릭하면 BTService_BlueprintBase를 클릭해줍니다. (위 사진에는 이미 블루프린트베이스를 추가하고 C++함수를 호출한 서비스를 추가했기때문에 보여질뿐입니다.) 먼저 플레이어를 찾는 메소드를 연결해보겠습니다.

그럼 BTService_BlueprintBase의 이벤트 그래프 창이 뜨게됩니다.
오른쪽 마우스를 클릭하여 Receive Tick Event 노드를 만들고 생성된 노드를 JAIController 형변환 노드를 생성해줍니다. 그리고 FindClosetEnemy를 찾아 노드를 연결해줍니다.

저장을 하고 브라우저를 보면 Monster_BHT옆에 BTService_BlueprintBaseNew가 생성되어있습니다. 저희가 방금 추가한 서비스죠. 이름을 TService_FindClosetEnemy로 바꿔주도록 합니다.
다시 BHT 돌아와서 Selector에 FindClosetEnemy서비스를 추가해줍니다. Selector 우클릭한 서비스 추가를 누르면 변경된 이름의 서비스가 나옵니다.


그리고 게임을 실행하면 몬스터는 플레이어를 탐색하고, 탐색된 플레이어의 방향으로 바라보게 됩니다( 이부분은 c++ 클래스에서 구현되어 있습니다. )
여기서 몬스터가 플레이어를 따라오는 AI 추가할 경우 Tasks에서 Move To 추가해주시면 따라옵니다.
그리고 마지막으로 남은 AttackEnemy도 연결해주겠습니다. 방식은 위와 동일합니다.
그럼 위와같이 됩니다. 두개의 AI기능을 C++에서 만들고, 비헤이비어트리로 호출하여 AI프로세스에 맞춰 동작하게끔 되었습니다.

브라우저창들을 다 저장한 후 꺼주고, 레벨에 몬스터가 돌아다닐 수 있는 영역을 지정해주면 끝이랍니다.!

화면의 왼쪽보시면 모드 패널에 볼륨>Nav Mesh Bounds Volume 끌어다가 레벨에 배치해줍니다. 크기 위치 등 레벨에서 설정이 가능하는 원하는데로 배치하시면 됩니다.


ps.
레벨에 몬스터 블루프린트를 드래그해서 몬스터 한마리를 바로 배치했을 경우, AI작동에 문제가 없었는데
새로 추가한 스폰클래스를 이용해 런타임중 다수의 몬스터를 스폰할 경우 AI가 작동되지 않는 문제가 발생하였다.
AJMonster* const SpawnMon = World->SpawnActor<AJMonster>(WhatToSpawn, SpawnLocation, SpawnRotation, SpawnParams);
SpawnMon->SpawnDefaultController();
호출해줌으로써 해결됬다....



마지막으로, 몬스터의 죽음 처리도 진행해보겠습니다.
우선 몬스터가 죽게되면 BehaviorTree 로직을 멈추어야 합니다.

먼저 위에서 설명했던 것처럼, 몬스터의 상태를 실시간으로 체크할 수 있는 서비스를 하나 더 추가해줍니다. 이름은 UpdateState로 해주었습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void AJAIController::UpdateState()
{
    AJMonster* myMon = Cast<AJMonster>(GetPawn());
    if(myMon == nullptr)
        return;
    if(myMon->isDie())
    {
        m_eState = AI_STATE::DIE;
        BehaviorComp->StopTree();
        return;
    }
}
cs
AI Control에서 몬스터가 죽은 상태인지 체크하여, 만약 상태가 Die일 경우 비헤이비어트리의 로직을 멈추도록 StopTree()를 호출해줍니다.



몬스터 작업을 하면서 추가적으로 설정해주었던 내용들인데 그래도 남겨놓기위해..

* 먼저 애니메이션을 1번만 플레이되게 하기위해서는 아래 사진의 빨간네모박스에 있는 Loop Animation을 체크 해제하시면 애니메이션이 1번만 플레이됩니다. 


* 위에서 몬스터가 공격하는 서비스인데, 노드에서 몬스터가 2초간격으로 공격을 할 수 있도록 노드를 추가해준 내용입니다. Wait State 노드를 생성해 Delay를 지정해줌으로 몬스터가 공격을 시도하면서 2초간의 딜레이가 발생하게 됩니다.