본문 바로가기

게임 개발/이득우의 언리얼 C++

[이득우의 언리얼 C++ 게임개발의 정석] #10. 아이템 상자와 무기 제작

반응형

이득우의-언리얼-C++-게임개발의-정석-표지

언리얼 엔진의 '소켓 시스템'을 활용해 
캐릭터 에셋에 액새서리를 부착하는 방법을 학습한다.

물리 엔진 기능을 활용해 플레이어 캐릭터만 감지하는 아이템 상자를 제작하고

상자를 먹으면 아이템을 생성한 후 캐릭터에 부착시키는 기능을 구현해 본다.
 

<1>. 캐릭터 소켓 설정

캐릭터 오른손에 무기를 장착해 보자.

 

무기는 캐릭터에 트랜스폼으로 배치하는 것이 아니라 메시에 착용해야 한다.

언리얼 엔진은 캐릭터에 무기, 액세서리를 부착하기 위한 용도로 '소켓'이라는 시스템을 제공한다.

※예제에는 'hand_rSocket' 이름의 소켓이 오른쪽 손에 있다.

 

무기는 스켈레탈 메시이기 때문에 스켈레탈 메시 컴포넌트를 캐릭터 메시에 부착한다.

//ABCharacter.h

UPROPERTY(VisibleAnywhere, Category = Weapon)
USkeletalMeshComponent* Weapon;

//ABCharacter.cpp

FName WeaponSocket(TEXT("hand_rSocket"));
if(GetMesh()->DoesSocketExist(WeaponSocket))
{
	Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
    static ConstructorHelpers::FObjectFinder<USkeletalMesh>SK_WEAPON(TEXT("(경로)"));
    if(SK_WEAPON.Succeeded())
    {
    	Weapon->SetSkeletalMesh(SK_WEAPON.Object);
    }
    Weapon->SetupAttachment(GetMesh(), WeaponSocket);
}

 

<2>. 무기 액터의 제작

필요에 따라 무기를 바꿀 수 있게 하려면, 무기를 액터로 분리해 만드는 것이 좋다. 

→ "ABWeapon"이라는 이름의 액터 생성 

 

예제에서 무기는 실제로 충돌을 발생시키지 않는다. 그러므로 액터의 루트 컴포넌트인 스켈레탈 메시 컴포넌트의 충돌 설정을 NoCollision으로 지정한다.

//ABWeapon.h

public:
	UPROPERTY(VisibleAnywhere, Category=Weapon)
    USkeletalMeshComponent* Weapon;
    
//ABWeapon.cpp

AABWeapon::AABWeapon()
{
	PrimaryActorTick.bCanEverTick = false;
    
    Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
    RootComponent = Weapon;
    
    static ConstructorHelpers::FObjectFinder<USkeletalMesh>SK_WEAPON(TEXT("(경로)"));
    if(SK_WEAPON.Succeeded())
    {
    	Weapon->SetSkeletalMesh(SK_WEAPON.Object);
    }
    Weapon->SetCollisionProfileName(TEXT("NoCollision"));
}

 

이제 무기 액터를 따로 만들었으니, 캐릭터의 BeginPlay에서 무기 액터를 생성하고, 이를 캐릭터에 부착시켜 보자.

 

월드에서 새롭게 액터를 생성하는 명령은 'SpawnActor'이다. SpawnActor의 인자에는 '생성할 액터의 클래스'와 액터가 

'앞으로 생성할 위치 및 회전'을 지정한다.

→ 액터는 월드에 존재하므로 이는 월드의 명령어다. GetWorld 함수로 월드의  포인터를 가져와서 함수를 실행한다.

//ABCharacter.cpp

#include "ABWeapon.h"

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

 

<3>. 아이템 상자의 제작

플레이어에게 무기를 공급해 줄 아이템 상자를 제작해 보자.

→ Actor를 부모로 하는 "ABItemBox" 클래스를 생성한다.

 

아이템 상자는 플레이어를 감지하는 '콜리전 박스'와 아이템 상자를 시각화해 주는 '스태틱메시'로 나뉜다.

→ 루트 컴포넌트에는 박스 콜리전 컴포넌트를, 자식에는 스태틱메시 컴포넌트를 추가한다.

 ※박스 콜리전 컴포넌트의 Extent 값은 전체 박스 영역 크기의 절반 값을 의미한다.

//ABItemBox.h

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)); //박스 메시 크기가 80cmx82cmx41cm
    static ConstructorHelpers::FObjectFinder<UStaticMesh>SM_BOX(TEXT("(경로)"));
    if(SM_BOX.Succeeded())
    {
    	Box->SetStaticMesh(SM_BOX.Object);
    }
    Box->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
}

 

이제 폰이 아이템을 획득하도록 아이템 상자에 '오브젝트 채널'을 추가한다.→ 콜리전 메뉴에서 ItemBox라는 오브젝트 채널을 생성한다(기본반응 = 무시).→ ABCharacter 오브젝트 채널에만 반응하도록 '겹침' 세팅을 지정한다. (양방향 둘 다)

→ ItemBox라는 프리셋을 추가한다.

 

새로운 프리셋을 박스 컴포넌트에 설정하고, 박스 컴포넌트에서 캐릭터를 감지할 때 관련된 행동을 구현한다.

박스 컴포넌트에는 Overlap 이벤트를 처리할 수 있게 'OnComponentBeginOverlap'라는 델리게이트가 선언돼 있다.

→ 해당 델리게이트 선언 : FComponentBeginOverlapSignature OnComponentBeginOverlap;

→ FComponentBeginOverlapSignature는 델리게이트 유형이고, 다음과 같이 선언돼 있다.

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

 

→ 이 매크로를 통해, 'OnComponentBeginOverlap'는 멀티캐스트 델리게이트임을 확인할 수 있다.

→ 유형, 인자를 모두 복사해 매크로 설정과 동일한 멤버 함수를 선언하고 이를 해당 델리게이트에 바인딩하면

    Overlap 이벤트가 발생할 때마다 멤버 함수가 호출된다.

//ABItemBox.h

class ARENABATTLE_API AABItemBox : public AActor
{
protected:
	virtual void PostInitializeComponents() override;
    
private:
	UFUNCTION()
    void OnCharacterOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, 
    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult);
    
//ABItemBox.cpp

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

void AABItemBox::PostInitializeComponents()
{
	Super::PostInitializeComponents(); //자식 클래스에서 추가된 작업 전에 부모 클래스에서 정의된 초기화 작업.
    //override 했기 때문에 해주는 초기화
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnCharacterOverlap);
    //자식 클래스의 추가 초기화 작업
}

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

 

<4>. 아이템의 습득

아이템 상자를 통과하면 빈손의 플레이어에게 아이템을 쥐어주는 기능을 구현해 보자

→ 아이템 상자에 클래스 정보를 저장할 속성을 추가하고, 이를 기반으로 아이템을 생성하도록 기능을 구현한다. 

 

→ 클래스 정보를 저장하는 변수를 선언할 때, UClass 포인터를 사용할 수 있지만, 이를 사용하면 현재 프로젝트에 사용하는 모든 언리얼 오브젝트의 선언이 보이게 됨.

→ 특정/상속받은 클래스들로 목록을 한정하는 'TSubclassof'라는 키워드를 사용하면 목록에서 아이템 상자, 이를 선언한 클래스 목록만 볼 수 있다.

//ABItemBox.h

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(); //WeaponItemClass는 AABWeapon 클래스의 타입 정보를 가진다.
}

 

캐릭터에 무기를 장착시키는 SetWeapon이라는 멤버 함수를 선언한다.

→ 현재 무기가 없으면, hand_rSocket에 무기를 장착시키고 → 무기 액터의 소유자를 캐릭터로 변경하는 로직을 넣는다.

//ABCharacter.h

class ARENABATTLE_API AABCharacter:public ACharacter
{
public:
	bool CanSetWeapon(); //캐릭터에게 무기가 있는지 없는지
    void SetWeapon(class AABWeapon* NewWeapon); //무기 장착
    
    UPROPERTY(VisibleAnywhere, Category = Weapon)
    class AABWeapon* CurrentWeapon;
}

//ABCharacter.cpp

bool AABCharacter::CanSetWeapon() //현재무기가 없을 때(null일 때), true를 반환
{
	return (nullptr == CurrentWeapon);
}

void AABCharacter::SetWeapon(AABWeapon* NewWeapon)
{
	FName WeaponSocket(TEXT("hand_rSocket")); //FName타입의 WeaponSocket을 "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* OverlappedComponent, AActor* OtherActor, 
    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult);
{
	ABLOG_S(Warning);
    
    auto ABCharacter = Cast<AABCharacter>(OtherActor); //OtherActor를 AABCharacter타입으로 변환
    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());
        }
    }
}

 

이번엔 상자에 이펙트를 부착해 아이템을 습득하면 이펙트를 재생하고, 재생이 끝나면 상자가 사라지는 기능을 구현한다.

→ 상자 액터에 파티클 컴포넌트를 추가한 후, 이펙트 에셋을 파티클 컴포넌트의 템플릿으로 지정한다.

→ 멤버 함수를 하나 추가하고 OnSystemFinished(파티클 컴포넌트 시스템에서 제공하는 델리게이트)에 이를 연결해 이펙트 재생이 종료되면 상자가 제거되는 로직을 구성하자.

→ 이펙트가 재생될 때는 액터의 충돌 기능을 제거해 아이템을 두 번 습득하지 못하도록 방지 / 박스 스태틱메시도 액터가 제거될 때까지 모습을 숨기게끔 하자.

 

※OnSystemFinished 델리게이트는 '다이내믹 형식'이므로 바인딩할 대상 멤버 함수에 UFUNCTION 매크로를 선언해 준다.

  • 정적 바인딩 : 성능이 중요하고 호출 대상이 고정적일 때 사용. 컴파일 시간에 결정되어 있으므로 실행 시점에서 추가적인 비용 없이 빠르게 동작함. 블루프린트에서 사용 불가능
  • 동적 바인딩 : 객체의 실제 동작이 런타임에 결정되어야 하거나, 상속 관계에서 다양한 타입의 객체를 일관된 인터페이스로 다룰 때 유용함. 블루프린트에서 사용 가능.
//ABItemBox.h

public:
	UPROPERTY(VisibleAnywhere, Category=Effect)
    UParticleSystemComponent* Effect;
private:
	UFUNCTION()
    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에 사용할 파티클 시스템 템플릿 설정
		Effect->bAutoActivate = false;  //이 컴포넌트가 생성되었을 때, 자동으로 활성화될지 여부 -> 명시적으로 활성화될 때까지 비활성 상태로 유지
	}
}
...
	if (ABCharacter->CanSetWeapon())
	{
		auto NewWeapon = GetWorld()->SpawnActor<AABWeapon>(WeaponItemClass, FVector::ZeroVector, FRotator::ZeroRotator);
		ABCharacter->SetWeapon(NewWeapon);
		Effect->Activate(true); //파티클 효과 발생시키기
		Box->SetHiddenInGame(true, true);  //Box 컴포넌트를 게임에서 숨길지, 두번째 인수는 하위 컴포넌트도 함께 숨길지
		SetActorEnableCollision(false); //두번 습득 방지
		Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
	}
    
void AABItemBox::OnEffectFinished(UParticleSystemComponent* PSystem)
{
	Destroy();
}

 

이번에는 아이템 상자에서 다른 무기가 나오게끔 새로운 무기를 추가해 본다.

→ 이번엔 C++가 아닌, 블루프린트를 사용해 ABWeapon을 상속받은 객체를 다수 생성해 본다.

→ 블루프린트 클래스 생성 → ABWeapon을 부모 클래스로 설정 → 해당 블루프린트에서 스켈레탈 메시 컴포넌트의 스켈레탈 메시에서 새로운 무기로 바꿔줌 → 상자의 WeaponClass 설정을 방금 생성한 블루프린트로 변경 → 상자 먹으면 새로운 무기 획득

 

 

 

[이득우의 언리얼 C++ 게임개발의 정석] #11. 게임 데이터와 UI 위젯

엑셀 데이터로부터 캐릭터의 능력치를 관리할 수 있도록테이블 데이터를 언리얼 엔진에 불러들이는 방법을 알아본다.게임 데이터를 효과적으로 관리하는 게임 인스턴스 클래스,방대한 로직을

lbto.tistory.com

 

반응형