KoreanFoodie's Study

이득우의 언리얼 C++ 10 : 아이템 상자와 무기 제작 본문

Game Dev/Unreal C++ : Tutorial

이득우의 언리얼 C++ 10 : 아이템 상자와 무기 제작

GoldGiver 2022. 3. 10. 20:32

이득우님의 "이득우의 언리얼 C++ 게임 개발의 정석" 책을 따라가며 실습한 내용을 정리한 포스팅입니다. 실습에 필요한 자료들은 이 링크에서, 제가 작업한 예제 소스 완성본은 여기에서 찾아보실 수 있습니다. (저는 언리얼 4.27.2 버전 기준으로 작업하였습니다)

캐릭터 소켓 설정 

무기는 메시에 착용해야 캐릭터 애니메이션에 따라 무기가 움직인다. 언리얼은 소켓이라는 시스템을 제공하는데, 우리가 사용하는 워리어 캐릭터의 스켈레탈 메시에는 이미 hand_rSocket 이라는 이름의 소켓이 생성돼 있다.

 

프리뷰 에셋을 추가해서, 소켓의 위치를 조정해보자. 무기 애셋은 InfinityBlade : Weapons

여기서는 소켓의 위치 값은 (-9.4, 3.0, 5.3), 회전 값은 (18.4, 23.2, 83.0) 으로 설정했다.

소켓의 설정을 완료하면 스켈레탈 메시 컴포넌트를 캐릭터 메시에 부착한다.

 

ABCharacter.h

...

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{

    ...
    
public:
    
    ...
    
    UPROPERTY(VisibleAnywhere, Category = Weapon)
    USkeletalMeshComponent* Weapon;
};

 

ABCharacter.cpp

...

AABCharacter::AABCharacter()
{
    ...
    
    FName WeaponSocket(TEXT("hand_rSocket"));
    if (GetMesh()->DoesSocketExist(WeaponSocket))
    {
        Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
        static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_WEAPON(TEXT("/Game/InfinityBladeWeapons/Weapons/Blade/Swords/Blade_BlackKnight/SK_Blade_BlackKnight.SK_Blade_BlackKnight"));
        if (SK_WEAPON.Succeeded())
        {
            Weapon->SetSkeletalMesh(SK_WEAPON.Object);
        }
        
        Weapon->SetupAttachment(GetMesh(), WeaponSocket);
    }
}

먼저 소켓을 찾은 후, 웨폰 애셋을 불러오고, 웨폰을 소켓에 붙인다. 이때 SetupAttachment 라는 함수를 사용한다.

 

게임을 시작하면 캐릭터에 자동으로 무기가 쥐어져 나온다.

 

 

무기 액터의 제작

액터에 고정시켜 무기를 장착하지 않고 때에 따라 무기를 바꿀 수 있도록 만들어 보자. 먼저, 무기를 액터로 분리해 만든다. ABWeapon 이라는 무기 액터 클래스(C++)를 생성한다.

 

ABWeapon.h

...

#include "ArenaBattle.h"

...

UCLASS()
class ARENABATTLE_API AAWeapon : public AActor
{
    ...
    
public:
    UPROPERTY(VisibleAnywhere, Category = Weapon)
    USkeletalMeshComponent* Weapon;
}

 

ABWeapon.cpp

...

AABWeapon::AAWeapon()
{
   ...
   
    Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
    RootComponent = Weapon;
   
    static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_WEAPON(TEXT("/Game/InfinityBladeWeapons/Weapons/Blade/Swords/Blade_BlackKnight/SK_Blade_BlackKnight.SK_Blade_BlackKnight"));
    if (SK_WEAPON.Succeeded())
    {
        Weapon->SetSkeletalMesh(SK_WEAPON.Object);
    }
    Weapon->SetCollisionProfileName(TEXT("NoCollision"));
}

...

여기서는 무기 액터의 루트 컴포넌트인 스켈레탈 메시 컴포넌트의 충돌 설정(프리셋)을 NoCollision 으로 지정했다. 액터를 월드에 배치한 후 콜리전을 확인해 보자.

NoCollision 으로 설정된 콜리전 프리셋

 

기존에 캐릭터가 무기를 장착했던 코드는 제거하고, 이제 ABCharacter.cpp 에서 무기를 장착하도록 만들어 보자.

 

ABCharacter.cpp

...

#include "ABWeapon.h"

...

void AABCharacter::BeginPlay()
{
   Super::BeginPlay();
   
   FName WeaponSocket(TEXT("hand_rSocket"));
   auto CurWeapon = GetWorld()->SpawnActor<AABWeapon>(FVector::ZeroVector, FRotator::ZeroRotator);
   if (nullptr != CurWeapon)
   {
       CurWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRule::SnapToTargetNotIncludingScale, WeaponSocket);
   }
}

SpawnActor 를 통해 새롭게 액터를 생성한다. 위치와 회전은 기본값으로 놓는다. 그 후, WeaponSocket 에 해당 웨폰 액터를 붙인다(AttachToComponent). 여기서 FAttachmentTransformRule 은 액터를 붙일때 어떤 식으로 붙일지 정하는 option 이다. 

KeepRelativeTransform 을 하게되면 변화가 없었지만, KeepWorldTransform 을 쓰게 되면 다음과 같이 부착 상태가 변했다.

 

 

아이템 상자의 제작

플레이어에게 무기를 공급해줄 아이템 상자를 제작한다. Actor 를 부모로 하는 ABItemBox 라는 C++ 클래스를 생성한다.

아이템 상자는 플레이어를 감지하는 콜리전 박스와 아이템 상자를 시각화해주는 스태틱메시로 나뉜다. 루트 컴포넌트에는 플레이어의 겹침을 감지할 박스 콜리전 컴포넌트를 사용하고, 자식에는 스태틱메시 컴포넌트를 추가한다.

아이템 상자에 사용할 스태틱메시는 InfinityBladeGrassLands 의 SM_Env_Breakables_Box1 애셋을 선택한다. 박스의 크기가 작으면 스태틱메시의 LOD 0 항목에서 빌드 스케일을 늘려준다. 여기서는 1.5 로 설정했다.

스태틱메시 설정을 완료하면 그 크기를 참고해 액터에 박스 콜리전 컴포넌트의 크기와 스태틱메시 컴포넌트의 애셋을 지정한다. 참고로 박스 콜리전 컴포넌트의 Extend 값은 전체 박스 영역 크기의 절반 값을 의미한다.

 

ABItemBox.h

...

#include "ArenaBattle.h"

...

UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
    ...
   
public:
    UPROPERTY(VisibleAnywhere, Category = Box)
    UBoxComponent* Trigger;
    
    UPROPERTY(VisibleAnywhere, Category = Box)
    UStaticMeshComponent* Box;
};

 

ABItemBox.cpp

...

AABItemBox::AABItemBox()
{
    PrimaryActorTick.bCanEverTick = false;
    
    Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
    Box = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BOX"));
    
    RootComponent = Trigger;
    Box->SetupAttachment(RootComponent);
    
    Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_BOX(TEXT("/Game/InfinityBladeGrassLands/Environments/Breakables/StaticMesh/Box/SM_Env_Breakables_Box1.SM_Env_Breakables_Box1"));
    if (SM_BOX.Succeeded())
    {
        Box->SetStaticMesh(SM_BOX.Object);
    }
    Box->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
}

...

Trigger 는 겹침을, Box 는 스켈레탈 메시 애셋을 통한 시각적 기능을 담당한다. SetBoxExtent 로 크기의 절반 값을 넣어준다.

 

이제 폰이 아이템을 획득하도록 아이템 상자에 오브젝트 채널을 추가한다. 프로젝트 설정의 콜리전 메뉴로 가서 ItemBox 라는 오브젝트 채널을 하나 생성하고 기본 반응은 무시로 지정한다.

 

그리고 ItemBox 라는 프리셋을 하나 추가한다. 해당 프리셋은 ABCharacter 에 대해서는 '겹침' 을 구현해야 한다.

 

ABCharacter 의 프리셋도 마찬가지로 겹침으로 바꾸어 준다.

새로운 프리셋을 박스 컴포넌트에 설정하고, 박스 컴포넌트에서 캐릭터를 감지할 때 관련된 행동을 구현한다. 박스 컴포넌트에는 Overlap 이벤트를 처리할 수 있게 OnComponentBeginOverlap 이라는 델리게이트가 선언돼 있다. 해당 델리게이트의 선언은 다음과 같다.

 

PrimitiveComponent.h

FComponentBeginOverlapSignature OnComponentBeginOverlap;

 

해당 델리게이트를 선언하는 데 사용한 FComponentBeginOverlapSignature 은 언리얼 소스 코드에 다음과 같이 선언돼 있다.

 

PrimitiveComponent.h

DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SixParams( FComponentBeginOverlapSignature, UPrimitiveComponent, OnComponentBeginOverlap, UPrimitiveComponent*, OverlappedComponent, AActor*, OtherActor, UPrimitiveComponent*, OtherComp, int32, OtherBodyIndex, bool, bFromSweep, const FHitResult &, SweepResult);

매크로를 보면 OnComponentBeginOverlap 델리게이트는 멀티캐스트 다이내믹 델리게이트임을 확인할 수 있다. 유형과 인자를 모두 복사해 매크로 설정과 동일한 멤버 함수를 선언하고 이를 해당 델리게이트에 바인딩하면 Overlap 이벤트가 발생할 때마다 바인딩한 멤버 함수가 호출된다.

 

ABItemBox.h

...

UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
    ...
    
protected:
    
    ...
    
    virtual void PostInitializationComponents() override;
    
    ...
    
private:
    UFUNCTION()
    void OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const HitResult& SweepResult);
};

 

ABItemBox.cpp

...

AABItemBox::AABItemBox()
{
    ...
    
    Trigger->SetCollisionProfileName(TEXT("ItemBox"));
    Box->SetCollisionProfileName(TEXT("NoCollision"));
}

...

void AABItemBox::PostInitializeComponents()
{
    Super::PostInitializeComponents();
    Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnCharacterOverlap);
}

void AABItemBox::OnCharacterOverlap(FComponentBeginOverlapSignature, UPrimitiveComponent, OnComponentBeginOverlap, UPrimitiveComponent*, OverlappedComponent, AActor*, OtherActor, UPrimitiveComponent*, OtherComp, int32, OtherBodyIndex, bool, bFromSweep, const FHitResult &, SweepResult)
{
    ABLOG_S(Warning);
}

이제 캐릭터가 아이템 박스를 통과할 때마다 오버랩 델리게이트와 바인딩된 함수가 실행돼 로그가 출력된다.

 

플레이어가 박스와 겹쳤을 경우 왼쪽 아래에 로그가 출력되는 것을 확인해 볼 수 있다.

 

 

아이템 습득

아이템 상자를 통과하면 빈손의 플레이어에게 아이템을 쥐어주는 기능을 구현해 보자. 배치한 아이템 상자에 클래스 정보를 저장할 속성을 추가하고, 이 값을 기반으로 플레이어가 아이템 상자의 영역에 들어왔을 때 아이템을 생성하도록 기능을 구현해본다.

클래스 정보를 저장하는 변수를 선언할 때 UClass 의 포인터를 사용할 수 있지만, 이를 사용하면 현재 프로젝트에 사용하는 모든 언리얼 오브젝트의 선언이 보이게 된다. 언리얼 엔진은 특정 클래스와 상속받은 클래스들로 목록을 한정하도록 TSubclassof 라는 키워드를 제공한다. 이를 사용하면 목록에서 아이템 상자와 이를 선언한 클래스 목록만 볼 수 있다.

 

ABItemBox.h

...

UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
    ...
    
public:
    
    UPROPERTY(EditInstanceOnly, Category = Box)
    TSubclassof<class AABWeapon> WeaponItemClass;
};

 

ABItemBox.cpp

...

#include "ABWeapon.h"

AABItemBox::AABItemBox()
{
    ...
    
    WeaponItemClass = AABWeapon::StaticClass();
}

 

아이템 박스에 Weapon Item Class 를 선택할 수 있게 메뉴가 나타난다.

 

이제 캐릭터에 무기를 장착시키는 SetWeapon 이라는 멤버 함수를 선언한다. 여기에는 현재 캐릭터에 무기가 없으면 hand_rSocket 에 무기를 장착시키고 무기 액터의 소유자를 캐릭터로 변경하는 로직을 넣는다. 그리고 기존에 BeginPlay 에서 시작할 때 무기 액터를 장착시킨 로직은 삭제한다.

 

ABCharacter.h

...

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
    ...
    
    bool CanSetWeapon();
    void SetWeapon(class AABWeapon* NewWeapon);
    
    UPROPERTY(VisibleAnywhere, Category = Weapon);
    class AABWeapon* CurrentWeapon;
    
    ...
};

 

ABCharacter.cpp

...

bool AABCharacter::CanSetWeapon()
{
    return (nullptr == CurrentWeapon);
}

void AABCharacter::SetWeapon(AABWeapon* NewWeapon)
{
    ABCHECK(nullptr != NewWeapon && nullptr == CurrentWeapon);
    FName WeaponSocket(TEXT("hand_rSocket"));
    if (nullptr != NewWeapon)
    {
        NewWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, WeaponSocket);
        NewWeapon->SetOwner(this);
        CurrentWeapon = NewWeapon;
    }
}

...

원리는 이전과 비슷하다. 이제 배치한 상자에 Overlap 이벤트가 발생할 때 아이템 상자에 설정된 클래스 정보로부터 무기를 생성하고 이를 캐릭터에게 장착시키는 기능을 구현한다.

 

ABItemBox.cpp

...
#include "ABCharacter.h"
...

void AABItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    ABLOG_S(Warning);
    
    auto ABCharacter = Cast<AABCharacter>(OtherActor);
    ABCHECK(nullptr != ABCharacter);
    
    if (nullptr != ABCharacter && nullptr != WeaponItemClass)
    {
        if (ABCharacter->CanSetWeapon())
        {
            auto NewWeapon = GetWorld()->SpawnActor<AABWeapon>(WeaponItemClass, FVector::ZeroVector, FRotator::ZeroRotator);
            ABCharacter->SetWeapon(NewWeapon);
        }
        else
        {
            ABLOG(Warning, TEXT("%s can't equip weapon currently."), *ABCharacter->GetName());
        }
    } 
}

결국 최종적으로 플레이어가 무기를 장착하게 하는 코드는 ABItemBox.cpp 를 통해 이루어진다.

 

플레이어가 박스 가까이로 가면 무기가 장착된다.

 

또한 이미 무기가 있을 경우 무기를 장착할 수 없다는 로그가 뜬다.

 

이제는 이펙트를 상자에 부착해 아이템을 습득하면 이펙트를 재생하고, 이펙트 재생이 완료되면 상자가 사라지는 기능을 구현해 보자. InfinityGrassLands 패키지의 T_TreasureChest_Open_Mesh 라는 이펙트를 사용할 것이다.

상자 액터에 파티클 컴포넌트를 추가한 후, 해당 이펙트 애셋의 레퍼런스를 복사하고 파티클 컴포넌트의 템플릿으로 이를 지정한다. 멤버 함수를 하나 추가하고 파티클 컴포넌트 시스템에서 제공하는 OnSystemFinished 델리게이트에 이를 연결해 이펙트 재생이 종료되면 아이템 상자가 제거되도록 로직을 구성한다. 이때 OnSystemFinished 델리게이트는 다이내믹 형식이므로 바인딩할 대상 멤버 함수에 UFUNCTION 매크로를 선언해 주어야 한다.

 

ABItemBox.h

...

UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
    ...

public:
    UPROPERTY(VisibleAnywhere, Category = Effect)
    UParticleSystemComponent* Effect;
   
    ...
    
private:
    UFUCNTION()
    void OnEffectFinished(class UParticleSystemComponent* PSystem);
};

 

ABItemBox.cpp

...

AABItemBox::AABItemBox()
{
    ...
    
    Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("EFFECT"));
    
    ...
    
    Effect->SetupAttachment(RootComponent);
    
    ...
    
    static ConstructorHelpers::FObjectFinder<UParticleSystem> P_CHESTOPEN(TEXT("/Game/InfinityBladeGrassLands/Effects/FX_Treasure/Chest/P_TreasureChest_Open_Mesh.P_TreasureChest_Open_Mesh"));
    if (P_CHESTOPEN.Succeeded())
    {
        Effect->SetTemplate(P_CHESTOPEN.Object);
        Effect->bAutoActivate = false;
    }
}

...

void AABItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    ...
    
    if (nullptr != ABCharacter && nullptr != WeaponItemClass)
    {
        if (ABCharacter->CanSetWeapon())
        {
            auto NewWeapon = GetWorld()->SpawnActor<AAWeapon>(WeaponItemClass, FVector::ZeroVector, FRotator::ZeroRotator);
            ABCharacter->SetWeapon(NewWeapon);
            Effect->Activate(true);
            Box->SetHiddenInGame(true, true);
            SetActorEnableCollision(false);
            Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
        }
        ...
    }
}

void AABItemBox::OnEffectFinished(UParticleSystemComponent* PSystem)
{
    Destroy();
}

먼저 ABItemBox 의 생성자에 이펙트 컴포넌트를 붙이고, 비활성화를 해 놓는다. 그 후, OnCharacterOverlap 에서 무기를 습득할 때 이펙트를 키고, 박스를 숨기고, 충돌 설정을 끈다. 이펙트가 끝나면 OnSystemFinished 델리게이트에 등록한 OnEffectFinished 함수를 호출해 아이템 박스 액터를 파괴한다.

이펙트가 잘 재생된다. 참고로, 액터를 숨기는 방식은 다음과 같은 두 가지 방식이 자주 사용된다.

SetVisibility SetHiddenInGame
컴포넌트의 시각적인 기능을 아예 없앰
(에디터 화면과 게임플레이 화면에서 모두 사라짐)
에디터 레벨 작업에서는 보여줌
(게임플레이 화면에는 사라짐)

 

이제 아이템 상자에서 다른 무기를 생성할 수 있도록 만들자. 블루프린트를 사용해 ABWeapon 을 상속받은 객체를 다수 생성해 본다. Blueprints 폴더에 ABWeapon 을 상속받은 블루프린트 클래스를 생성한다. 이름은 BP_WeaponSucker 로 짓는다.

기본적으로 BlackKnight 검이 생성됨

 

블루프린트 스켈레탈 메시에서 SK_Blade_Sucker 를 선택한다.

 

새 아이템 박스 액터를 배치하고 Weapon Item Class 를 BP_WeaponSucker 로 변경한다.

 

Sucker 가 잘 장착된 것을 확인할 수 있다!

 

Comments