블랙보드와 비헤이비어 트리 둘다 필요하므로 만들어준다.

 

블랙보드는 애님 인스턴스를 먼저 만들고 애니메이션 재생과 관련된 데이터를 몰아넣고 매 프레임마다 해당 데이터들을 이용해서 애니메이션을 재생하는데 사용하던 개념과 비슷하다.

 

저번까지는 빙의 시 타이머를 등록하여 랜덤한 이동을 테스트 해봤는데 비헤이비어 트리를 이용하면 이런 부분이 필요가 없어진다.

 

// MyAIController.h
private:
    UPROPERTY()
    class UBehaviorTree* BehaviorTree;

    UPROPERTY()
    class UBlackboardData* BlackboardData;
    
// MyAIController.cpp
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"

AMyAIController::AMyAIController()
{
    static ConstructorHelpers::FObjectFinder<UBehaviorTree> BT(TEXT("BehaviorTree'/Game/AI/BT_MyCharacter.BT_MyCharacter'"));
    if (BT.Succeeded())
    {
        BehaviorTree = BT.Object;
    }

    static ConstructorHelpers::FObjectFinder<UBlackboardData> BD(TEXT("BlackboardData'/Game/AI/BB_MyCharacter.BB_MyCharacter'"));
    if (BD.Succeeded())
    {
        BlackboardData = BD.Object;
    }
}

void AMyAIController::OnPossess(APawn* InPawn)
{
    // 빙의 시
    Super::OnPossess(InPawn);

    // 블랙보드 사용
    if (UseBlackboard(BlackboardData, Blackboard))
    {
        // 비헤이비어 트리를 실행
        if (RunBehaviorTree(BehaviorTree))
        {
            // Log
        }
    }
}

 

아직까지는 비헤이비어 트리를 작성한게 딱히 없기때문에 아무런 행동도 하지 않지만 실제로는 적용되고 있다.

 

 

Task는 저것들이 전부가 아니라 기본적으로 제공되는 것들이고 당연히 커스텀으로 만들어서 사용할 수도 있다.

최상단의 Find Patrol Pos는 커스텀으로 만든 것이다.

 

BTTaskNode를 상속받은 클래스를 만들어서 필요한 기능을 안에 구현하면 된다.

 

// BTTask_FindPatrolPos.h
UCLASS()
class TESTUNREALENGINE_API UBTTask_FindPatrolPos : public UBTTaskNode
{
    GENERATED_BODY()

public:
    UBTTask_FindPatrolPos();

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

// BTTask_FindPatrolPos.cpp
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
    // 해당 이름으로 노드가 표시된다
    NodeName = TEXT("FindPatrolPos");
}

EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

    auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();

    if (CurrentPawn == nullptr)
        return EBTNodeResult::Failed;

    // 월드의 내비게이션 시스템을 가져온다
    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

    if (NavSystem == nullptr)
        return EBTNodeResult::Failed;

    FNavLocation RandomLocation;

    // 월드의 내비게이션 범위중 원점(첫번째 매개변수)을 기준으로 반지름 만큼의 범위를 한정지어
    // 랜덤한 좌표를 가져온다(RandomLocation에 저장됨)
    if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, OUT RandomLocation))
    {
        // 해당 액터를 RandomLocation으로 이동
        OwnerComp.GetBlackboardComponent()->SetValueAsVector(FName(TEXT("PatrolPos")), RandomLocation.Location);
        return EBTNodeResult::Succeeded;
    }

    return EBTNodeResult::Failed;
}

 

ExecuteTask는 딱 보기에도 태스크의 실행이다.

 

3초간 랜덤한 위치로 이동하는 구현부를 이쪽으로 옮겨오면서 코드 수정이 이루어졌다.

원래 직접 이동시키는 함수가 있었으나 시퀀스를 이용할 것이기 때문에 랜덤한 좌표를 계산하는것만 가져오도록 한다.

 

비헤이비어 트리에서 시퀀스에 등록된 노드들은 각 노드별로 성공시 순차적으로 실행되지만 중간에 실패하면 이후의 노드들의 실행이 중단된다.

그렇기 때문에 매번 중요한 곳에서 체크할 때마다 Failed를 반환해 주어야 한다.

 

블랙보드에 등록된 키에 값을 넣어주려면 SetValue~~ 함수를 이용해서 블랙보드에 만들어 준 키 이름과 동일한 것을 입력하고 어떤 값을 넘겨줄지를 두번째 인자로 넘겨주면 된다.

 

블랙보드에 키를 만들어주고

 

트리를 위와 같이 구성하면 5초 대기 후 좌표를 구해서 해당 좌표로 이동하는 것을 반복하게 된다.

 

[정리]

 

  • 컴포짓 : 셀렉터, 시퀀스, 심플 페러렐 노드가 있다.
  • 셀렉터 : 자식 노드중 하나라도 성공하면 나머지 자식 노드의 실행을 중단한다. 실패하면 다음 자식 노드를 실행한다.
  • 시퀀스 : 자식 노드중 하나라도 실패하면 나머지 자식 노드의 실행을 중단한다. 성공하면 다음 자식 노드를 실행한다.
  • 심플 페러렐 : 전체 노드 트리와 동시에 하나의 태스크를 실행할 수 있다.
  • BTTaskNode : 비헤이비어 트리에 커스텀 노드를 추가할 수 있다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] 샘플 분석  (0) 2022.09.12
[UE4 입문] Behavior Tree #2  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 스탯 매니저  (0) 2022.09.02

유니티의 경우 AI를 제어하려면 AIController같은 클래스를 만들어서 컴포넌트로 붙여서 제어하게 되는데 언리얼도 유사하게 되어있다.

언리얼은 이미 구조가 어느정도 잡혀있다는점이 조금 다르다.

AIController는 AI에 빙의 되었다가 풀렸다 하면서 제어가 된다.

 

AIController를 상속받는 클래스를 하나 만들어주도록 하자.

 

언리얼에서는 Behavior Tree라는것을 제공해주는데 항상 사용해야 되는것은 아니고 매우 간단한 AI같은 경우는 상태기계를 이용하는게 더 쉬울 수 있다.

Behavior Tree는 보통 서버쪽에서 만들어진다.

 

우선 모듈을 추가로 사용해야 하기 때문에 cs파일에 모듈을 추가해주자.

 

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "NavigationSystem", "AIModule", "GameplayTasks" });

 

NavigationSytstem은 단어 그대로 내비게이션 시스템을 사용하기 위해 필요하고 AIModule은 AIController를 사용하기 위해서 필요하다. GameplayTasks는 이번시간은 아니지만 차후 Behavior Tree를 사용할 때 필요하므로 미리 넣어준다.

 

 

// MyAIController.h
UCLASS()
class TESTUNREALENGINE_API AMyAIController : public AAIController
{
    GENERATED_BODY()

public:
    AMyAIController();

    virtual void OnPossess(APawn* InPawn) override;
    virtual void OnUnPossess() override;

private:
    void RandomMove();

private:
    // 타이머 매니저에 콜백을 등록하기 위해 필요한 핸들
    FTimerHandle TimerHandle;
};

// MyAIController.cpp
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"

AMyAIController::AMyAIController()
{
}

void AMyAIController::OnPossess(APawn* InPawn)
{
    // 빙의 시
    Super::OnPossess(InPawn);

    // 월드의 타이머 매니저에 콜백을 등록한다
    GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMyAIController::RandomMove, 3.f, true);
}

void AMyAIController::OnUnPossess()
{
    // 빙의 해제 시
    Super::OnUnPossess();

    // 등록된 콜백을 해제한다
    GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}

void AMyAIController::RandomMove()
{
    auto CurrentPawn = GetPawn();

    // 월드의 내비게이션 시스템을 가져온다
    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

    if (NavSystem == nullptr)
        return;

    FNavLocation RandomLocation;

    // 월드의 내비게이션 범위중 원점(첫번째 매개변수)을 기준으로 반지름 만큼의 범위를 한정지어
    // 랜덤한 좌표를 가져온다(RandomLocation에 저장됨)
    if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, RandomLocation))
    {
        UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, RandomLocation);
    }
}

 

OnPossess와 OnUnPossess는 빙의 시, 빙의 해제 시 실행되는 함수이다. OnOverlapBegin/End같은 트리거 이벤트라고 보면 될 듯 하다.

 

빙의시 타이머 매니저에 의해 콜백이 등록되고 해제되는데 이와 같은 패턴이 서버를 만들때도 비슷하게 만들어진다.

 

 

AMyCharacter::AMyCharacter()
{
    AIControllerClass = AMyAIController::StaticClass();
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

 

AIControllerClass는 Pawn에 기본적으로 생성되어있는 AIController이다. 이것을 방금 만든 AIController로 바꿔준다.

AutoPossessAI는 자동 빙의 규칙인데 기본적으로 플레이어가 빙의된 경우는 무시된다.

월드에 배치되거나 스폰된 경우 AIController가 자동으로 빙의되도록 한다.

 

월드에 내비 메시 바운드 볼륨을 깔고 실행하면 플레이어가 아닌 맵에 배치된 캐릭터가 3초마다 랜덤한 곳으로 이동한다.

 

[정리]

 

  • AIController는 AI에 빙의와 해제를 반복하여 AI를 제어한다.
  • 비헤이비어 트리가 강력한 툴인것은 맞지만 구현이 복잡하기 때문에 매우 간단한 AI로직의 경우 꼭 비헤이비어 트리를 사용할 필요는 없다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] Behavior Tree #2  (0) 2022.09.12
[UE4 입문] Behavior Tree #1  (0) 2022.09.12
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 스탯 매니저  (0) 2022.09.02
[UE4 입문] 아이템 줍기  (0) 2022.09.02

[68. Enemy Combat #3]

 

MoveTo에 전달했던 NavPath의 정보와 DrawDebugSphere를 이용해서 경로를 시각적으로 확인해 보도록 한다.

 

// Enemy.cpp
#include "Kismet/KismetSystemLibrary.h"

void AEnemy::MoveToTarget(AMain* Target)
{
    if (NavPath) // 내비게이션 메시 밖으로 나가면 크래시가 나므로 null체크를 해줘야 한다
    {
        auto PathPoints = NavPath->GetPathPoints();

        for (const auto& Point : PathPoints)
        {
            auto Location = Point.Location;
            UKismetSystemLibrary::DrawDebugSphere(this, Location, 25.f, 8, FLinearColor::Red, 10.f, .5f);
        }
    }
}

 

장애물이 없는 직선상에서는 Enemy의 원점, Target의 원점 두개가 그려지고 Target의 원점으로 쭉 이동한다.

하지만 코너같이 장애물이 있는 경우 그것을 우회하는 경로 여러개가 그려진다.

 

 

MoveRequest의 Acceptance radius에 의한 허용 반경은 캡슐 콜리전 사이의 거리와 관련이 있다.

 

[69. Enemy Combat #4]

 

지금까지는 어그로 범위 안에 들어오면 추적하는 것을 구현해 보았고 이제 전투 범위에 들어오면 공격하는 것을 구현할 차례이다.

우선은 어그로 범위에 들어오면 MoveToTarget을 호출하고 MovementStatus를 변경했던 것처럼 공격도 마찬가지로 구현하고 애니메이션 블루프린트도 동일하게 만들어준다.

 

void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Attacking);
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super::CombatSphereOnOverlapBegin()"));
}

void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_MoveToTarget);
            MoveToTarget(Main); // 공격 범위에서 벗어나도 다시 추적하도록 해야한다
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super:CombatSphereOnOverlapEnd()"));
}

 

IdleWalk로 돌아오는것은 Not Equal로 해준다.

 

어그로 범위에 들어가면 추적하고 공격 범위에 들어오면 공격 모션을 취한다. 하지만 공격 모션 도중에 공격 범위를 벗어나면 애니메이션이 중단되고 다시 추적하는 문제가 있지만 잠시 뒤에 수정하도록 한다.

 

어느정도 의도한 대로 작동하고 있지만 몬스터가 영원히 쫓아오는것은 원치 않는다. 어느정도 범위를 벗어나면 몬스터가 추적을 멈추고 도망갈수 있기를 원한다.

 

 

// Enemy.cpp
void AEnemy::AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Idle);
            if (AIController)
            {
                AIController->StopMovement();
            }
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super:AgroSphereOnOverlapEnd()"));
}

 

StopMovement()는 현재 무엇을 하고 있던간에 행동을 중지시킨다.

 

 

공격 도중 타겟이 범위를 벗어나면 모션이 끊기는 것을 수정해 보도록 한다.

애니메이션 노티파이를 이용해서 공격 모션이 재생되면 중단 없이 쭉 재생하고 모션의 끝에서 공격이 끝났다는 노티파이를 발생시킨 뒤 그것을 통해 다시 타겟을 추적하도록 만들 것이다.

몽타주를 따로 만들지 않았으므로 공격 애니메이션 애셋에서 노티파이를 직접 추가해주면 된다.

 

블루프린트로 처리할것이기 때문에 코드를 조금 수정하도록 한다.

 

// Enemy.h
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="AI")
    bool bOverlappingCombatSphere; // 전투 영역에 겹침 상태인지 확인하기 위함

    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "AI")
    AMain* CombatTarget; // 블루프린트에서 전투중인 타겟을 알기 위함

// Enemy.cpp

void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            CombatTarget = Main; // 전투 영역에 들어오면 타겟을 설정한다
            bOverlappingCombatSphere = true;
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Attacking);
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super::CombatSphereOnOverlapBegin()"));
}

void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            bOverlappingCombatSphere = false;
            // 아래 조건문이 실행될수가 없음
            if (EnemyMovementStatus != EEnemyMovementStatus::EMS_Attacking)
            {
                MoveToTarget(Main);
                CombatTarget = nullptr; // 전투 영역을 벗어나면 타겟을 해제한다
            }
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super::CombatSphereOnOverlapEnd()"));
}

 

EndAttack 노티파이가 발생하면(모션이 끝나면) MoveToTarget을 호출한다.

여기서 문제점이 다시 발생한다. 타겟이 움직이지 않고 여전히 전투 영역에 있는 경우에도 EndAttack이 발생하면 전투상태가 풀리고 MoveToTarget이 무조건 호출이 되기때문에 연속적으로 공격을 하지 않는다.

 

전투 영역과 겹침 상태가 아닐때만 MoveToTarget을 호출하도록 노드의 흐름을 바꿔주면 된다.

 

 

[정리]

 

  • MoveTo에 전달되는 FNavPathSharedPtr에 경로와 관련된 정보를 돌려받는다.
  • AIController의 StopMovement 함수를 통해 행동을 중단시킬수 있다.
  • 공격 모션이 도중에 끊기는 것은 애니메이션 노티파이를 통해 해결할 수 있다. (코드만으로도 가능한지 확인 필요)

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #6  (0) 2022.09.13
[UE C++] Combat #5  (0) 2022.09.12
[UE C++] Combat #3  (0) 2022.09.11
[UE C++] Combat #2  (0) 2022.09.11
[UE C++] Combat #1  (0) 2022.09.10

[66. Enemy Combat #1]

 

폰은 기능이 많지 않은 차량이나 기본적인 입력만을 받고 돌아다니는 것들에 적합하다.

하지만 캐릭터에 기능들이 좀 더 다양하므로 해당 실습에서는 폰이 아닌 캐릭터 클래스를 기반으로 적을 구현하도록 한다.

 

캐릭터를 상속받는 Enemy 클래스를 만든다.

해당 C++ 클래스를 블루프린트 클래스로 만들고 프로토타입을 작성해보도록 한다.

 

Sphere Collision을 하나 추가해서 반경을 600으로 크게 설정한다.

 

OtherActor를 Main캐릭터로 형변환 후 TargetActor로 둔다.

Acceptance Radius만큼의 Target 반경에 접근하면 이동이 완료된 것으로 간주한다.

다만 이상태로는 실행을 해도 추적하지 않는다. 추적하는 경로를 모르기 때문이다.

그래서 내비게이션 메시를 깔아주면 경로 계산이 가능해져서 추적을 하게 된다.

 

 

스케일을 50, 50, 50 정도로 설정하면 매우 넓게 설정이 된다. 캐릭터와 적이 내비게이션 메시 위에 동시에 존재하면서 겹침 이벤트가 발생 시 추적하게 된다.

 

이제 C++에서 구현할 것이기 때문에 프로토타입으로 작성한 블루프린트 이벤트는 지워주도록 한다.

 

 

 

// Enemy.h
UENUM(BlueprintType)
enum class EEnemyMovementStatus : uint8
{
    EMS_Idle			UMETA(DisplayName="Idle"),
    EMS_MoveToTarget	UMETA(DisplayName="MoveToTarget"),
    EMS_Attacking		UMETA(DisplayName="Attacking"),

    EMS_MAX				UMETA(DisplayName="DefaultMAX")
};

UCLASS()
class FIRSTPROJECT_API AEnemy : public ACharacter
{
    GENERATED_BODY()

public:
    AEnemy();

protected:
    virtual void BeginPlay() override;

public:	
    virtual void Tick(float DeltaTime) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement")
    EEnemyMovementStatus EnemyMovementStatus;

    FORCEINLINE void SetEnemyMovementStatus(EEnemyMovementStatus Status) { EnemyMovementStatus = Status; }
};

 

// EnemyAnimInstance.h
public:
    virtual void NativeInitializeAnimation() override;

public:
    UFUNCTION(BlueprintCallable, Category = AnimationProperties)
    void UpdateAnimationProperties();

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    float MovementSpeed;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    class APawn* Pawn;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    class AEnemy* Enemy;
};

// EnemyAnimInstance.cpp
#include "Enemy.h"

void UEnemyAnimInstance::NativeInitializeAnimation()
{
    if (Pawn == nullptr)
    {
        Pawn = TryGetPawnOwner();
        if (Pawn)
        {
            Enemy = Cast<AEnemy>(Pawn);
        }
    }
}

void UEnemyAnimInstance::UpdateAnimationProperties()
{
    if (Pawn == nullptr)
    {
        Pawn = TryGetPawnOwner();
        if (Pawn)
        {
            Enemy = Cast<AEnemy>(Pawn);
        }
    }
    if (Pawn)
    {
        FVector Speed = Pawn->GetVelocity();
        FVector LateralSpeed = FVector(Speed.X, Speed.Y, 0.f);

        MovementSpeed = LateralSpeed.Size();
    }
}

 

애니메이션도 추가해 주도록 한다.

 

.

 

블렌드 스페이스에서 최대값은 150으로 설정한다.

 

[67. Ememy Combat #2]

 

블루프린트에서 프로토타입으로 확인했던 내용을 C++로 구현해볼 차례이다.

일정 범위 안에 들어오면 캐릭터를 추적하고 공격하게 할 것이다.

 

 

// Enemy.h
UENUM(BlueprintType)
enum class EEnemyMovementStatus : uint8
{
    EMS_Idle            UMETA(DisplayName="Idle"),
    EMS_MoveToTarget    UMETA(DisplayName="MoveToTarget"),
    EMS_Attacking       UMETA(DisplayName="Attacking"),

    EMS_MAX             UMETA(DisplayName="DefaultMAX")
};

UCLASS()
class FIRSTPROJECT_API AEnemy : public ACharacter
{
    GENERATED_BODY()

public:
    AEnemy();

protected:
    virtual void BeginPlay() override;

public:	
    virtual void Tick(float DeltaTime) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
    // 어그로 범위, 공격 범위를 따로 구분한다.
    UFUNCTION()
    virtual void AgroSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    UFUNCTION()
    virtual void AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

    UFUNCTION()
    virtual void CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    UFUNCTION()
    virtual void CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

    // 타겟으로 이동
    void MoveToTarget(class AMain* Target);

    // getter, setter
    FORCEINLINE void SetEnemyMovementStatus(EEnemyMovementStatus Status) { EnemyMovementStatus = Status; }
    FORCEINLINE EEnemyMovementStatus GetEnemyMovementStatus() { return EnemyMovementStatus; }

public:
    // 행동 상태
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement")
    EEnemyMovementStatus EnemyMovementStatus;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="AI")
    class USphereComponent* AgroSphere;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    USphereComponent* CombatSphere;

    // AI를 조종하기 위한 컨트롤러
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    class AAIController* AIController;
};

#include "Components/SphereComponent.h"
#include "AIController.h"
#include "Main.h"

// Sets default values
AEnemy::AEnemy()
{
    // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    AgroSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AgroSphere"));
    AgroSphere->SetupAttachment(GetRootComponent());
    AgroSphere->InitSphereRadius(600.f);

    CombatSphere = CreateDefaultSubobject<USphereComponent>(TEXT("CombatSphere"));
    CombatSphere->SetupAttachment(GetRootComponent());
    CombatSphere->InitSphereRadius(75.f);
}

void AEnemy::BeginPlay()
{
    Super::BeginPlay();

    AIController = Cast<AAIController>(GetController());

    AgroSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::AgroSphereOnOverlapBegin);
    AgroSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemy::AgroSphereOnOverlapEnd);

    CombatSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::CombatSphereOnOverlapBegin);
    CombatSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemy::CombatSphereOnOverlapEnd);
}

void AEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
}

void AEnemy::AgroSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            MoveToTarget(Main);
        }
    }
}

void AEnemy::AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}

void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}

void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}

void AEnemy::MoveToTarget(AMain* Target)
{
    SetEnemyMovementStatus(EEnemyMovementStatus::EMS_MoveToTarget);

    if (AIController)
    {
        UE_LOG(LogTemp, Warning, TEXT("MoveToTarget"));
    }
}

 

AI와 관련된 모듈을 이용하려면 프로젝트의 cs파일에서 공개 의존성 모듈에 "AIModule" 을 추가해 주어야 컴파일 오류가 발생하지 않는다.

 

우선 어그로 범위 감지용 구체와 전투 범위 감지용 구체 2개를 만들어준다.

각각의 겹침 판정에 대해 함수도 바인딩 해준다.

 

우선 테스트를 위해 어그로 구체와 겹칠 시 로그를 띄우도록 했고 실행 시 잘 작동하게 된다.

 

이제 프로토타입에서 사용했던 MoveTo를 직접 코드로 작성해 본다.

 

virtual FPathFollowingRequestResult MoveTo
(
    const FAIMoveRequest & MoveRequest,
    FNavPathSharedPtr * OutPath
)

 

두개의 인자를 받는데, 첫번째는 목표에 대한 구조체이다. 접근 반경이나 길찾기 여부 등의 정보가 담겨있다. 두번째는 경로와 관련이 있는 구조체이다. OutPath라는것을 보면 알수 있듯이 경로가 OutPath에 담겨진다.

 

void AEnemy::MoveToTarget(AMain* Target)
{
    SetEnemyMovementStatus(EEnemyMovementStatus::EMS_MoveToTarget);

    if (AIController)
    {
        FAIMoveRequest MoveRequest;
        MoveRequest.SetGoalActor(Target); // 목표 액터 설정
        MoveRequest.SetAcceptanceRadius(5.f); // 수용(접근) 범위

        FNavPathSharedPtr NavPath;

        AIController->MoveTo(MoveRequest, &NavPath);
    }
}

 

애님 블루프린트에서 UpdateAnimationProperties까지 연결시켜주면 거미들이 쫓아올 때 애니메이션이 재생 된다.

 

 

[정리]

 

  • AIController를 사용하려면 cs파일의 공개 의존성 모듈에 "AIModule"을 추가하여야 한다.
  • AI가 이동하는데 사용하는 함수들은 여러종류가 있다.
    https://blog.naver.com/raveneer/220796853517

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #5  (0) 2022.09.12
[UE C++] Combat #4  (0) 2022.09.11
[UE C++] Combat #2  (0) 2022.09.11
[UE C++] Combat #1  (0) 2022.09.10
[UE C++] Gameplay Mechanics #7  (0) 2022.09.09

[62. Switching Blendspaces]

 

무기를 들었을 때의 새로운 애니메이션이 필요한 때이다.

 

애셋들을 다운로드하고 무기를 든 상태의 Idle/Walk/Run 블렌드 스페이스를 만들어 준다.

기존에 Idle/Walk/Run에 만들어 뒀던 내용은 모두 삭제하고 새로운 스테이트 머신을 추가할 것이다.

 

 

 

평상시->무기 습득시 (nulltpr이 아니라면)

무기 장착 여부에 관계없이 Idle/Walk/Run은 동일하기 때문에 스테이트 머신 안의 스테이트 머신이 필요한 것이다.

 

Sprint도 똑같이 만들어주면 된다.

 

[63. Anim Montages (Attack!) #1]

 

애니메이션들을 합쳐서 선택적으로 재생할 수 있도록 해주는 유연한 툴이다.

 

결과적으로 이런 식으로 사용하는 것인데 슬롯에 대한 개념이 잡히지 않아서 글을 작성하기가 어렵다.

추후 슬롯에 대한 개념이 잡히면 다시 작성하도록 하고 해당 실습을 따라가기만 하는것으로 둔다.

 

[64. Anim Montages (Attack!) #2]

 

마우스 좌클릭시 공격 애니메이션이 실행되는 것 까지 잘 되었다.

하지만 공격모션 도중에도 이동이나 점프가 자유롭게 된다는 문제가 있다. 이것을 수정해야 한다.

 

이동을 막는것은 간단하다.

 

// Main.cpp
void AMain::MoveForward(float Value)
{
    if ((Controller != nullptr) && (Value != 0.0f) && (!bAttacking))
    // ...
}

/* Called for side to side input */
void AMain::MoveRight(float Value)
{
    if ((Controller != nullptr) && (Value != 0.0f) && (!bAttacking))
    // ...
}

 

공격중이라면 이동 자체가 일어나지 않도록 해주면 된다.

다만 이러면 또 문제가 발생한다. 현재까지의 구현 내용으로는 bAttacking이 false로 돌아오는 일이 없기때문에 한번 공격시 영원히 움직일 수 없게된다.

이 부분은 애니메이션 노티파이를 통해 공격 모션이 끝날 때 특정 함수를 호출하는 것으로 해결하도록 한다.

 

EndAttacking을 배치할 때 위치를 잘 봐야한다. 섹션 이후가 아닌 섹션 이전에 와야 의도한 대로 작동할 것이다.

 

// Main.h
public:
    UFUNCTION(BlueprintCallable)
    void AttackEnd();
    
// Main.cpp
void AMain::AttackEnd()
{
    bAttacking = false;
}

 

노티파이가 호출할 함수이다. bAttacking을 false로 만들어주면 다시 이동이 가능해질 것이다.

 

노티파이를 설정했다면 이벤트그래프에서 표시가 될 것이다.

 

AnimNotify_ 라는 접두어가 붙은 이벤트가 새로 생성된다. 애니메이션 재생 도중에 노티파이가 설정된 구간을 지나면 이벤트를 호출하는 개념이다.

 

이제 공격 도중 움직일 수 없고 공격 모션이 끝나면 다시 이동할 수 있게 잘 수정되었다.

하지만 문제가 한가지 또 있다. 좌클릭을 연속으로 입력하거나 입력을 유지하고 있으면 공격 모션이 계속 초기화 되어 재생되는 것이다.

 

 

// Main.cpp
void AMain::Attack()
{
    // 1회성에 한해 공격하도록 함
    if (!bAttacking)
    {
        bAttacking = true;

        UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
        if (AnimInstance && CombatMontage)
        {
            // 몽타주 재생. 1.35배만큼 빠르게 재생
            AnimInstance->Montage_Play(CombatMontage, 1.35f);
            // 섹션으로 건너 뜀
            AnimInstance->Montage_JumpToSection(FName("Attack_1"), CombatMontage);
        }
    }
}

void AMain::AttackEnd()
{
    bAttacking = false;

    // 공격이 끝났음에도 마우스를 계속 누르고 있다면 다시 공격 실행
    if (bLMBDown)
    {
        Attack();
    }
}

 

이것 또한 간단하게 수정이 가능하다. Attack의 구현부를 bAttacking으로 모두 감싸면 한번 실행될 때 true가 되므로 이후로는 실행이 되지 않는다. 추가로 마우스를 계속 누르고 있을 때 공격모션이 계속 이루어지는것이 편리하므로 해당 부분도 겸사겸사 간단하게 처리한다.

 

[65. Anim Montages (Attack!) #3]

 

공격 모션을 추가하고 공격시 매번 같은 모션이 나오는것이 아닌, 무작위로 재생하게끔 하면 게임이 단조롭지 않을 것이다.

 

void AMain::Attack()
{
    if (!bAttacking)
    {
        bAttacking = true;

        UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
        if (AnimInstance && CombatMontage)
        {
            // 0-1 사이의 난수 발생
            int32 Section = FMath::RandRange(0, 1);
            switch (Section)
            {
            case 0:
                AnimInstance->Montage_Play(CombatMontage, 2.2f);
                AnimInstance->Montage_JumpToSection(FName("Attack_1"), CombatMontage);
                break;
            case 1:
                AnimInstance->Montage_Play(CombatMontage, 1.8f);
                AnimInstance->Montage_JumpToSection(FName("Attack_2"), CombatMontage);
                break;
            default:
                ;
            }
        }
    }
}

 

몽타주 섹션에서 이어진 프리뷰까지 끊어주면 공격 모션이 랜덤하게 재생된다.

 

[정리]

 

  • 애니메이션 몽타주는 여러개의 애니메이션을 엮어서 만들 수 있다. 섹션으로 나누어서 특정 섹션만 반복하거나 섹션끼리 연달아서 재생할 수도 있다.
  • 애니메이션 몽타주의 슬롯에 대한 명확한 설명을 찾기가 어렵다. 사용 예를 정확히 체감하지 못해서 그런 것 같다.
    관련 링크: https://docs.unrealengine.com/5.0/ko/animation-slots-in-unreal-engine/
  •  애니메이션 몽타주에서 노티파이를 이용해서 특정 모션에 이벤트를 발생시킬 수 있다.

 

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #4  (0) 2022.09.11
[UE C++] Combat #3  (0) 2022.09.11
[UE C++] Combat #1  (0) 2022.09.10
[UE C++] Gameplay Mechanics #7  (0) 2022.09.09
[UE C++] Gameplay Mechanics #6  (0) 2022.09.09

+ Recent posts