유니티의 경우 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

[59. Attaching to Sockets]

 

캐릭터의 손에 무기를 쥐어줄 시간이다.

 

스켈레톤에서 RightHand에 소켓을 추가하고 무기 하나를 프리뷰 해서 위치를 적절히 조절한다.

공격 애니메이션을 확인하면서 각도를 조정해주면 조금 더 자연스럽게 나온다.

 

무기도 아이템의 일종이므로 Item을 상속받는 C++ 클래스를 생성한다.

 

// Weapon.h
public:
    virtual void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
    virtual void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) override;
    void Equip(class AMain* Char);
    
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="SkeletalMesh")
    class USkeletalMeshComponent* SkeletalMesh; // Item의 StaticMesh는 사용하지 않는다
    
// Weapon.cpp
#include "Engine/SkeletalMeshSocket.h"

AWeapon::AWeapon()
{
    SkeletalMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalMesh"));
    SkeletalMesh->SetupAttachment(GetRootComponent());
}

void AWeapon::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    Super::OnOverlapBegin(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);

    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);

        if (Main)
        {
            Equip(Main);
        }
    }
}

void AWeapon::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    Super::OnOverlapEnd(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);
}

void AWeapon::Equip(AMain* Char)
{
    if (Char)
    {
        SkeletalMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
        SkeletalMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);

        SkeletalMesh->SetSimulatePhysics(false);

        const USkeletalMeshSocket* RightHandSocket = Char->GetMesh()->GetSocketByName(TEXT("RightHandSocket"));
        if (RightHandSocket)
        {
            RightHandSocket->AttachActor(this, Char->GetMesh());
            bRotate = false;
        }
    }
}

// Main.h
public:
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Items")
    class AWeapon* EquippedWeapon;

    FORCEINLINE void SetEquippedWeapon(AWeapon* WeaponToSet) { EquippedWeapon = WeaponToSet; }

 

추가로 Item에서 Destroy 되는 부분을 다른 클래스로 옮겨주어야 무기가 사라지지 않는다.

 

Weapon 클래스를 블루프린트 클래스로 만들고 메시를 설정해준 뒤 월드에 배치하면 무기 습득시 캐릭터의 손에 장착이 된다.

 

[60. Weapon Equipping]

 

바닥에 떨어진 무기를 획득했을 때 바로 장착하는게 아닌 선택권을 주려고 한다.

나중에 무기뿐만 아니라 방어구나 다른 습득 아이템도 장착의 선택권을 주기 위해 습득한 아이템을 Item의 포인터 타입으로 받아서 처리하도록 한다.

 

// Main.h
public:
    bool bLMBDown;
    void LMBDown();
    void LMBUp();
    
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Items")
    class AItem* ActiveOverlappingItem;

    FORCEINLINE void SetActiveOverlappingItem(AItem* Item) { ActiveOverlappingItem = Item; }

// Main.cpp
#include "Weapon.h"

void AMain::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    PlayerInputComponent->BindAction("LMB", IE_Pressed, this, &AMain::LMBDown);
    PlayerInputComponent->BindAction("LMB", IE_Released, this, &AMain::LMBUp);
}

void AMain::LMBDown()
{
    bLMBDown = true;

    if (ActiveOverlappingItem)
    {
        AWeapon* Weapon = Cast<AWeapon>(ActiveOverlappingItem);
        if (Weapon)
        {
            Weapon->Equip(this);
            SetActiveOverlappingItem(nullptr);
        }
    }
}

void AMain::LMBUp()
{
    bLMBDown = false;
}

 

// Weapon.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Item | Sound")
    class USoundCue* OnEquipSound;

// Weapon.cpp
#include "Kismet/GameplayStatics.h"
#include "Sound/SoundCue.h"

void AWeapon::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    Super::OnOverlapBegin(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);

    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);

        if (Main)
        {
            Main->SetActiveOverlappingItem(this);
        }
    }
}

void AWeapon::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    Super::OnOverlapEnd(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);

    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);

        if (Main)
        {
            Main->SetActiveOverlappingItem(nullptr);
        }
    }
}

void AWeapon::Equip(AMain* Char)
{
    if (Char)
    {
        SkeletalMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
        SkeletalMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);

        SkeletalMesh->SetSimulatePhysics(false);

        const USkeletalMeshSocket* RightHandSocket = Char->GetMesh()->GetSocketByName(TEXT("RightHandSocket"));
        if (RightHandSocket)
        {
            RightHandSocket->AttachActor(this, Char->GetMesh());
            bRotate = false;
            Char->SetEquippedWeapon(this);
            Char->SetActiveOverlappingItem(nullptr);
        }
        if (OnEquipSound) UGameplayStatics::PlaySound2D(this, OnEquipSound);
    }
}

 

[프로젝트 세팅 - 입력] 에서 마우스 좌클릭을 액션 매핑 LMB로 설정한다.

이제 더이상 무기와 겹침판정 즉시 획득되지 않고 무기와 겹친 상태에서 좌클릭을 눌러야 획득이 된다.

 

 

땅에 떨어진 장비에 가시성을 더하기 위해 파티클이나 아웃라인이 그려진 경우들이 많다. 파티클로 가시성을 더해주고 습득 시 파티클이 비활성화 되는 기능을 추가해 보도록 한다.

 

// Weapon.h

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item | Particles")
    bool bWeaponParticles;
    
// Weapon.cpp
#include "Particles/ParticleSystemComponent.h"

AWeapon::AWeapon()
{
    bWeaponParticles = false;
}
void AWeapon::Equip(AMain* Char)
{
    if (Char)
    {
        if (!bWeaponParticles)
        {
            IdleParticlesComponent->Deactivate();
        }
    }
}

 

파티클을 설정해주고 무기 습득시 파티클 재생이 중지된다. 

 

[61. Weapon Equipping #2]

 

무기를 습득한 상태에서 아무것도 하지 않아도 오브젝트에 피해를 주고 다니는 것은 의도한 사항이 아니다.

오직 공격할 때만 피해를 주고 싶기 때문에 무기에 상태를 설정하여 관리한다.

 

// Weapon.h

UENUM(BlueprintType)
enum class EWeaponState : uint8
{
    EWS_Pickup		UMETA(DisplayName="Pickup"),
    EWS_Equipped	UMETA(DisplayName="Equipped"),

    EWS_MAX			UMETA(DisplayName="DefaultMax")
};

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item")
    EWeaponState WeaponState;

    FORCEINLINE void SetWeaponState(EWeaponState State) { WeaponState = State; }
    FORCEINLINE EWeaponState GetWeaponState() { return WeaponState; }
    
// Weapon.cpp

AWeapon::AWeapon()
{
    WeaponState = EWeaponState::EWS_Pickup;
}

void AWeapon::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    Super::OnOverlapBegin(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
    
    // 무기가 픽업 가능한 상태일때 && 겹침 대상이 있을 때
    if (WeaponState == EWeaponState::EWS_Pickup && OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);

        if (Main)
        {
            Main->SetActiveOverlappingItem(this);
        }
    }
}

 

차후 구현을 위해 우선 선언 및 정의를 해둔다.

 

우선 한 가지 구현해야 할 사항은 무기를 습득하여 들고 있는 상태에서 새로운 무기를 습득 시 기존 무기는 파괴시키는 방식을 선택할 것이다.

 

 

// Main.h
public:
    void SetEquippedWeapon(AWeapon* WeaponToSet);

// Main.cpp
void AMain::SetEquippedWeapon(AWeapon* WeaponToSet)
{
    // 장착중인 무기가 있다면
    if (EquippedWeapon)
    {
        // 해당 무기를 파괴한다
        EquippedWeapon->Destroy();
    }

    EquippedWeapon = WeaponToSet;
}

 

무기 몇개를 월드에 더 배치하고 하나를 습득한 상태에서 다른 무기를 습득하여 장착 시, 기존 무기는 파괴되고 새로운 무기로 변경이 된다.

여기서 유의할 점은 파괴가 일어난다고 해도 항상 즉시 소멸되는것은 아니고 삭제된 오브젝트로써 남아있다가 GC에 의해 특정 순간 완전히 소멸된다.

 

[정리]

 

  • 스켈레톤에 소켓을 추가하여 액터를 붙일 수 있다.
  • 오브젝트의 소멸을 호출하더라도 즉시 소멸되지 않고 GC에 의해 특정 시간마다 완전히 소멸된다.

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

[UE C++] Combat #3  (0) 2022.09.11
[UE C++] Combat #2  (0) 2022.09.11
[UE C++] Gameplay Mechanics #7  (0) 2022.09.09
[UE C++] Gameplay Mechanics #6  (0) 2022.09.09
[UE C++] Gameplay Mechanics #5  (0) 2022.09.08

+ Recent posts