[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

[57. HUD #7]

 

전력질주 시 스태미너가 감소하게 될텐데, 일반적인 게임에서는 스태미너를 사용하면 일정시간 회복되지 않다가 뒤늦게 회복을 시작하거나 스태미너의 잔여량이 특정 구간(ex:20%)을 지나면 게이지의 색상이 변한다던지 하는 경우가 많다.

지금은 특정 구간에 따른 색상 변화를 구현해 보도록 하자.

 

스태미너가 최소치를 초과한 상태, 최소치 이하인 상태, 0인 상태, 0에서 회복중인 상태 4개로 구분한다.

 

// Main.h
UENUM(BlueprintType)
enum class EStaminaStatus : uint8
{
    ESS_Normal UMETA(DisplayName="Normal"),
    ESS_BelowMinimum UMETA(DisplayName="BelowMinimum"),
    ESS_Exhausted UMETA(DisplayName="Exhausted"),
    ESS_ExhaustedRecovering UMETA(DisplayName="ExhaustedRecovering"),

    ESS_MAX UMETA(DisplayName="DefaultMax")
};

 

스태미너의 색상 구분을 위해 enum class를 새로 추가한다.

 

// Main.h
publuc:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Enums")
    EStaminaStatus StaminaStatus;
    
    FORCEINLINE void SetStaminaStatus(EStaminaStatus Status) { StaminaStatus = Status; }

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
    float StaminaDrainRate;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
    float MinSprintStamina;
    
// Main.cpp
AMain::AMain()
{
    //Initialize Enums
    MovementStatus = EMovementStatus::EMS_Normal;
    StaminaStatus = EStaminaStatus::ESS_Normal;

    StaminaDrainRate = 25.f;
    MinSprintStamina = 50.f;
}

void AMain::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    float DeltaStamina = StaminaDrainRate * DeltaTime;

    switch (StaminaStatus)
    {
    case EStaminaStatus::ESS_Normal:
        if (bShiftKeyDown)
        {
            if (Stamina - DeltaStamina <= MinSprintStamina)
            {
                SetStaminaStatus(EStaminaStatus::ESS_BelowMinimum);
                Stamina -= DeltaStamina;
            }
            else
            {
                Stamina -= DeltaStamina;
            }
            SetMovementStatus(EMovementStatus::EMS_Sprinting);
        }
        else
        {
            if (Stamina + DeltaStamina >= MaxStamina)
            {
                Stamina = MaxStamina;
            }
            else
            {
                Stamina += DeltaStamina;
            }
            SetMovementStatus(EMovementStatus::EMS_Normal);
        }
        break;
    case EStaminaStatus::ESS_BelowMinimum:
        if (bShiftKeyDown)
        {
            if (Stamina - DeltaStamina <= 0.f)
            {
                SetStaminaStatus(EStaminaStatus::ESS_Exhausted);
                Stamina -= 0.f;
                SetMovementStatus(EMovementStatus::EMS_Normal);
            }
            else
            {
                Stamina -= DeltaStamina;
                SetMovementStatus(EMovementStatus::EMS_Sprinting);
            }
        }
        else
        {
        if (Stamina + DeltaStamina >= MinSprintStamina)
        {
            SetStaminaStatus(EStaminaStatus::ESS_Normal);
            Stamina += DeltaStamina;
        }
        else
        {
            Stamina += DeltaStamina;
        }
        SetMovementStatus(EMovementStatus::EMS_Normal);
        }
        break;
    case EStaminaStatus::ESS_Exhausted:
        if (bShiftKeyDown)
        {
            Stamina = 0.f;
        }
        else
        {
            SetStaminaStatus(EStaminaStatus::ESS_ExhaustedRecovering);
            Stamina += DeltaStamina;
        }
        SetMovementStatus(EMovementStatus::EMS_Normal);
        break;
    case EStaminaStatus::ESS_ExhaustedRecovering:
        if (Stamina + DeltaStamina >= MinSprintStamina)
        {
            SetStaminaStatus(EStaminaStatus::ESS_Normal);
            Stamina += DeltaStamina;
        }
        else
        {
            Stamina += DeltaStamina;
        }
        SetMovementStatus(EMovementStatus::EMS_Normal);
        break;
    }
}

 

키 눌림과 스태미너의 잔여량에 따라 상태를 계속 변경해준다.

 

Progress bar의 색상까지 위와 같이 바인딩 해주면 색상 변화가 잘 된다.

 

[58. Array and Debug Spheres]

 

디버깅에 도움을 줄 수 있는 기능들이 있다.

먼저 알아볼 것은 특정 위치에 사각형이나 구체 등등의 기하학적인 물체를 띄우는 것이다.

 

#include "Kismet/KismetSystemLibrary"

//(오브젝트, 위치, 반지름, 세그먼트, 색상, 지속시간, 선의 두께)
UKismetSystemLibrary::DrawDebugSphere(this, GetActorLocation() + FVector(0.f, 0.f, 75.f), 25.f, 8, FLinearColor::Red, 5.f, .25f);

 

배열 대신 TArray를 사용하여 조금 더 많은 기능과 신속성, 메모리 효율성, 안정성을 챙길 수 있다.

 

 

[정리]

 

  • 디버그 함수를 이용해서 디버깅을 하는데 도움을 받을 수 있다.
  • TArray는 가장 많이 쓰이는 컨테이너 중 하나이다.

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

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

[55. HUD #5]

 

실제로 Item을 먹었을 때 캐릭터의 상태가 변경되도록 해보자.

 

// Main.h
public:
    void DecreamentHealth(float Amount);
    void Die();
    void IncreamentCoins(int32 Amount);
    
// Main.cpp
void AMain::DecreamentHealth(float Amount)
{
    if (Health - Amount <= 0.f)
    {
        Health -= Amount;
        Die();
    }
    else
    {
        Health -= Amount;
    }
}

void AMain::Die()
{
}

void AMain::IncreamentCoins(int32 Amount)
{
    Coins += Amount;
}

// Pickup.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Coins")
    int32 CoinCount;

// Pickup.cpp
APickup::APickup()
{
    CoinCount = 1;
}

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

    UE_LOG(LogTemp, Warning, TEXT("Pickup::OnOverlapBegin()"));

    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            Main->IncreamentCoins(CoinCount);
        }
    }
}

// Explosive.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
    float Damage;

// Explosive.cpp
AExplosive::AExplosive()
{
    Damage = 15.f;
}

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

    UE_LOG(LogTemp, Warning, TEXT("Explosive::OnOverlapBegin()"));

    if (OtherActor)
    {
        // 캐스팅 실패시 nullptr 반환
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            Main->DecreamentHealth(Damage);
        }
    }
}

 

에디터에서 별다른 작업 없이 코드만 작성해주면 잘 작동된다.

 

[56. HUD #6]

 

전력질주를 구현해보자.

Shift키의 Up/Down 여부에 따라 전력질주 상태 여부를 결정할 것이다.

 

// Main.h
UENUM(BlueprintType)
enum class EMovementStatus : uint8
{
    EMS_Normal UMETA(DisplayName="Normal"),
    EMS_Sprinting UMETA(DisplayName = "Sprinting"),
    EMS_MAX UMETA(DisplayName = "DefaultMAX") // 실제로 보여지지는 않고 열거형의 끝을 알려줌
};

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Enums")
    EMovementStatus MovementStatus;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Running")
    float RunningSpeed;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Running")
    float SprintingSpeed;

    bool bShiftKeyDown;

    /* Pressed down to enable sprinting */
    void ShiftKeyDown();

    /* Released to stop sprinting */
    void ShiftKeyUp();
    
    /* Set movement status and running speed */
    void SetMovementStatus(EMovementStatus Status);
    
// Main.cpp
AMain::AMain()
{
    RunningSpeed = 650.f;
    SprintingSpeed = 950.f;

    bShiftKeyDown = false;
}

void AMain::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
    PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
}

void AMain::SetMovementStatus(EMovementStatus Status)
{
    MovementStatus = Status;

    if (MovementStatus == EMovementStatus::EMS_Sprinting)
    {
        GetCharacterMovement()->MaxWalkSpeed = SprintingSpeed;
    }
    else
    {
        GetCharacterMovement()->MaxWalkSpeed = RunningSpeed;
    }
}

void AMain::ShiftKeyDown()
{
    bShiftKeyDown = true;
}

void AMain::ShiftKeyUp()
{
    bShiftKeyDown = false;
}

// MainAnimInstance.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    class AMain* Main;
    
// MainAnimInstance.cpp
void UMainAnimInstance::NativeInitializeAnimation()
{
    if (Pawn == nullptr)
    {
        // 이 애니메이션 인스턴스의 소유자를 가져옴
        Pawn = TryGetPawnOwner();
        if (Pawn)
        {
            Main = Cast<AMain>(Pawn);
        }
    }
}

void UMainAnimInstance::UpdateAnimationProperties()
{
    if (Pawn == nullptr)
    {
        Pawn = TryGetPawnOwner();
    }

    if (Pawn)
    {
        FVector Speed = Pawn->GetVelocity();
        FVector LateralSpeed = FVector(Speed.X, Speed.Y, 0.f);

        MovementSpeed = LateralSpeed.Size();

        bIsInAir = Pawn->GetMovementComponent()->IsFalling();

        if (Main == nullptr)
        {
            Main = Cast<AMain>(Pawn);
        }
    }
}

 

Shift키의 Up/Down 액션에 대해 바인딩을 해준다. 애니메이션의 상태 변환을 하려면 블루프린트에서 값에 접근해야 하므로 AMain을 캐스팅해서 가지고 있는다.

 

 

상태 머신의 구조는 위와 같이 변한다.

 

아직 전력질주 상태를 지정하지 못해서 애니메이션의 변화는 없다.

 

[정리]

 

  • 없음

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

[UE C++] Combat #1  (0) 2022.09.10
[UE C++] Gameplay Mechanics #7  (0) 2022.09.09
[UE C++] Gameplay Mechanics #5  (0) 2022.09.08
[UE C++] Gameplay Mechanics #4  (0) 2022.09.08
[UE C++] Gameplay Mechanics #3  (0) 2022.09.07

[51. HUD (Heads Up Display) #1]

 

HUD는 화면에 표시되는 정보의 모든 것이다. (체력바, 보유재화 등등)

HUD의 변수들을 넣을 클래스는 개발에 따라 다르다. 그 중 한가지 방법은 모든 HUD의 항목을 플레이어 컨트롤러에 두는 것이다.

 

게임 도중 캐릭터가 죽으면 캐릭터가 사라졌다가 다시 생성될수는 있겠지만 플레이어 컨트롤러는 그대로 유지된다.

만약 정보가 캐릭터에 귀속 되어있다면, 캐릭터가 사망시 모든 정보가 초기화되기 때문에 캐릭터에서 관리할 수 없다.

 

UMG를 사용하여 HUD를 구현할 것이기 때문에 UMG(Unreal Motion Graphic) 모듈을 프로젝트에 포함시켜야 한다.

 

'프로젝트명.cs' 에서 추가시켜주면 된다.

 

 

이제 위젯 블루프린트를 이용해서 UI를 만들어 보려고 한다.

다른 모든 위젯들을 포함하는 전체 위젯, 즉 HUD 오버레이가 될 것이기 때문에 HUDOverlay로 이름을 짓는다.

 

 

Box들을 이용해서 각 영역의 비율을 정해줄 수 있다. 디테일 탭의 채우기와 비율을 이용하면 된다.

계층구조에 따라 상대적으로 영역을 분할한다.

 

Padding값 설정으로 간격을 줄 수 있다.

 

체력바를 만들어 보자.

 

 

Style은 Progress의 Percent가 감소되었을 때 표시되는 영역이다. 이미지나 틴트로 변화를 줄 수 있다.

Appearance는 Percent의 비율만큼 채워진 영역이다.

 

다 만들었으므로 HUDOverlay에 배치해볼 시간이다.

 

사용자 생성(User Created) 탭에 방금 만든 위젯이 나온다.

HealthBarBox의 하위 계층에 넣어주면 된다.

 

[52. HUD #2]

 

C++ 코드와 HUD를 연동시켜보자.

 

PlayerController를 상속받는 MainPlayerController C++ 클래스를 만든다.

 

// MainPlayerController.h
public:
    /* Reference to the UMG asset in the editor */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Widgets")
    TSubclassOf<class UUserWidget> HUDOverlayAsset;

    /* Variable to hold the widget after creating it */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Widgets")
    UUserWidget* HUDOverlay;

protected:
	virtual void BeginPlay() override;
    
// MainPlayerController.cpp
#include "Blueprint/UserWidget.h"

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

    if (HUDOverlayAsset)
    {
        HUDOverlay = CreateWidget<UUserWidget>(this, HUDOverlayAsset);
    }
    HUDOverlay->AddToViewport(); // 뷰포트에 생성
    HUDOverlay->SetVisibility(ESlateVisibility::Visible); // HUD Hiding?
}

 

조그맣지만 좌상단에 잘 표시된다. 

 

[53. HUD #3]

 

우선은 블루프린트를 이용해서 체력을 연동시켜 보자.

 

HealthBar의 Percent를 바인딩 해준다.

 

// Main.h
public:
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Player Stats")
    float MaxHealth;			
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player Stats")
    float Health;				
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Player Stats")
    float MaxStamina;			 
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player Stats")
    float Stamina;				 
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player Stats")
    int32 Coins;
    
// Main.cpp
AMain::AMain()
{
    MaxHealth = 100.f;
    Health = 65.f;
    MaxStamina = 350.f;
    Stamina = 120.f;
    Coins = 0;
}

 

코드까지 작성해주면 바인딩은 끝난다. 하지만 실시간으로 체력이 변화하는걸 확인할 수단이 없으므로 프로토 타입으로 간단히 만들어서 확인해본다.

 

N키를 누르면 10씩 감소된다. 실행해보면 실제로 잘 적용된다.

스태미너도 똑같이 만들어주면 동일하게 잘 적용된다.

 

[54. HUD #4]

 

 

위젯 블루프린트를 만들고 Text에 기존과 같이 바인딩을 해주면 된다.

 

코인까지 연결시켜주면 N키 한번에 값이 변경되고 UI에도 잘 적용된다.

 

[정리]

 

  • UI를 구성하기 위한 프레임 워크에는 UMG, HUD, Slate가 있다. 보통 UMG를 이용한다. 슬레이트를 감싼 프레임워크가 UMG라고 보면 된다. 조금 더 로우레벨이기 때문에 오로지 코드만으로 UI을 구현하고 싶다면 슬레이트로 만들 수 있다.

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

[UE C++] Gameplay Mechanics #7  (0) 2022.09.09
[UE C++] Gameplay Mechanics #6  (0) 2022.09.09
[UE C++] Gameplay Mechanics #4  (0) 2022.09.08
[UE C++] Gameplay Mechanics #3  (0) 2022.09.07
[UE C++] Gameplay Mechanics #2  (0) 2022.09.07

+ Recent posts