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

수민·2023년 3월 27일
0
post-thumbnail

이득우의 언리얼 C++ 게임 개발의 정석을 읽고 개인 공부 목적으로 요약 정리한 글입니다!


👀 무기 장착하기

캐릭터 소켓 설정

캐릭터 스켈레탈 메시를 더블클릭해서 설정으로 들어가면
스켈레톤 트리가 있다
거기에 무기를 붙일 오른쪽 손을 보면
hand_rSocket이 있다.

우리는 여기다가 무기를 붙여줄거기 때문에

우클릭 > 프리뷰 에셋 추가 > 원하는 무기 선택
을 해준당.

요렇게

그러면 이렇게 귀여운 애가 귀여운걸 손에 잡는다


프리뷰 애니메이션을 선택해서 애니메이션을 재생할 때 무기가 자연스러운 위치에 붙도록
소켓 위치를 조정해주면 된다


난 요렇게 했음.

이건 프리뷰니까
실제로 달아줘야 한다

캐릭터에 무기 장착

무기를 캐릭터가 장착하는 걸로 할거니까,
Actor를 부모 클래스로 하는 MyWeapon 클래스를 생성한다.

MyWeapon.h

UCLASS()
class HUNT_PROTOTYPE_API AMyWeapon : public AActor
{
	GENERATED_BODY()
	
public:	
	AMyWeapon();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

public:
	UPROPERTY(VisibleAnywhere, Category = Weapon)
		USkeletalMeshComponent* Weapon;							// 설정해줄 Skeletal Mesh
};

MyWeapon.cpp

AMyWeapon::AMyWeapon()
{
 	PrimaryActorTick.bCanEverTick = false;						// Tick이 호출될 필요 없음

	Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
	RootComponent = Weapon;

	static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_WEAPON(TEXT("SkeletalMesh'/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"));		// 충돌처리 안해줄거임
}

무기 레퍼런스 복사한 뒤에 무기를 생성해줬다.

그리고 이제 캐릭터에 붙여줘야 한다

MyCharacter.cpp

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

SpawnActor() 함수

SpawnActor 함수를 활용해서 월드에 새롭게 액터를 생성해줄 수 있다.
액터(Actor)는 월드에 존재하는 물체이므로, GetWorld()로 월드의 포인터를 가져온 뒤에 함수를 호출해준당.

결과

그러면 이렇게 무기가 착 붙여져서 나오고,


아웃라이너에서
캐릭터의 하위 멤버로 무기 액터가 분리되어 있다.


👀 무기 붙여주는 아이템 상자 만들기

콜리전 설정

충돌해서 무기를 뿅! 하고 장착시켜줄거니까 콜리전 설정을 해야 한다!!

프로젝트 설정 > 콜리전 들어가서
오브젝트 채널에 ItemBox 추가하고 기본 무시.

캐릭터랑 부딪혔을 때 Overlap으로 이벤트 발생시켜서 충돌처리 해줄거니까

프리셋 만들어준다.
그리고 캐릭터와의 관계에만 Overlap으로 체크해준다

아이템박스 클래스 만들기

Actor 클래스를 부모 클래스로 하는 MyItemBox 클래스를 만들자

아이템박스는
플레이어를 감지하는 CollisionBox
아이템 상자를 시각화해주는 StaticMesh로 구성하장.

MyItemBox.h

UCLASS()
class HUNT_PROTOTYPE_API AMyItemBox : public AActor
{
	GENERATED_BODY()
	...
public:
	UPROPERTY(VisibleAnywhere, Category = Box)
		UBoxComponent* Trigger;							// (CollisionBox : 충돌 감지)

	UPROPERTY(VisibleAnywhere, Category = Box)
		UStaticMeshComponent* Box;						// StaticMesh : 시각적 효과
	...
};

MyItemBox.cpp

AMyItemBox::AMyItemBox()
{
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
	Box = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BOX"));

	RootComponent = Trigger;
	Box->SetupAttachment(RootComponent);

	Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));
    // Extent는 박스 크기의 절반씩이다. 콜리전 박스의 크기를 설정해준다고 생각하면 될듯.
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_BOX(TEXT("StaticMesh'/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));
}

충돌 처리

위 프리셋을 아이템 박스에 넣어주면, 충돌이 먹힐거다.
박스 컴포넌트와 캐릭터가 충돌되는지 볼건데,
박스 컴포넌트에는 Overlap 이벤트를 처리할 수 있도록 선언된 델리게이트가 있다.
이름은 OnComponentBeginOverlap.

이 델리게이트는 멀티캐스트 다이내믹 델리게이트이다
매크로의 유형, 인자를 복사해서 설정과 동일한 멤버 함수를 선언할거다.
이걸 해당 델리게이트에 바인딩하면 Overlap 이벤트가 발생할 때 바인딩한 멤버함수가 호출될거당.

MyItemBox.h

UCLASS()
class HUNT_PROTOTYPE_API AMyItemBox : public AActor
{
	GENERATED_BODY()
	...
public:
	...
	UPROPERTY(EditInstanceOnly, Category = Box)
		TSubclassOf<class AMyWeapon> WeaponItemClass;	// 충돌하면 플레이어에 장착시켜줄 무기 클래스
        
protected:
	virtual void PostInitializeComponents() override;

private:
	UFUNCTION()
		void OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};

MyItemBox.cpp

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

void AMyItemBox::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AMyItemBox::OnCharacterOverlap);
}

void AMyItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	HUNT_LOG_S(Warning);
}

이렇게 하면,
Trigger에서 충돌이 감지되면 바인딩한 멤버함수 OnCharacterOverlap이 호출되어서
로그가 뜰거다.

아이템박스와 충돌하면 무기 장착시켜주기

무기를 캐릭터 손에 붙이려면
캐릭터가 무기를 붙일 수 있는 상태인지 체크해야 하고,
캐릭터에게 무기를 붙여줘야 하니까

두 개의 멤버함수를 캐릭터에다가 만든다.

MyCharacter.h

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

MyCharacter.cpp


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

void AMyCharacter::SetWeapon(AMyWeapon* NewWeapon)
{
	HUNT_CHECK(nullptr != NewWeapon && nullptr == CurrentWeapon);

	FName WeaponSocket(TEXT("hand_rSocket"));
	if (nullptr != NewWeapon) {
		NewWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, WeaponSocket);
		NewWeapon->SetOwner(this);
		CurrentWeapon = NewWeapon;
	}
}

시작할 때는 무기 없다가, 아이템상자와 충돌하면 무기를 장착시켜줄거니까
BeginPlay()에 넣어놨던 부분은 삭제하면 된당.


그리고 이제

아이템 상자에 무기 클래스의 정보를 저장하면 된다.

TSubclassof 키워드를 사용해서 필요한 클래스 정보를 받아온다.

TSubclassof 키워드

특정 클래스와 상속받은 클래스들로 목록을 한정하게 하는 키워드.
이를 사용하면 특정 오브젝트의 선언들만 볼 수 있다.

MyItemBox.h

UCLASS()
class HUNT_PROTOTYPE_API AMyItemBox : public AActor
{
	GENERATED_BODY()
	
public:
	...
	UPROPERTY(EditInstanceOnly, Category = Box)
		TSubclassOf<class AMyWeapon> WeaponItemClass;
};

MyItemBox.cpp

AMyItemBox::AMyItemBox()
{
 	...
	WeaponItemClass = AMyWeapon::StaticClass();
}

이렇게 하고
에디터로 돌아가서
레벨에서 상자를 선택하면
디테일 창에 Weapon Item Class를 설정하는 메뉴가 나타난다.

이 상태에서
Overlap 이벤트가 발생할 때 무기 생성하고 장착시켜주면 된당.

MyItemBox.cpp

void AMyItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	HUNT_LOG_S(Warning);

	auto MyCharacter = Cast<AMyCharacter>(OtherActor);
	HUNT_CHECK(nullptr != MyCharacter);

	if (nullptr != MyCharacter && nullptr != WeaponItemClass) {
		if (MyCharacter->CanSetWeapon()) {
			auto NewWeapon = GetWorld()->SpawnActor<AMyWeapon>(WeaponItemClass, FVector::ZeroVector, FRotator::ZeroRotator);
			MyCharacter->SetWeapon(NewWeapon);

			Effect->Activate(true);
			Box->SetHiddenInGame(true, true);
			SetActorEnableCollision(false);
			Effect->OnSystemFinished.AddDynamic(this, &AMyItemBox::OnEffectFinished);
		}
		else {
			HUNT_LOG(Warning, TEXT("%s can't equip weapon currently"), *MyCharacter->GetName());
		}
	}
}

이렇게 하면
한 번 충돌하면 자동으로 무기를 장착하고,
두 번 충돌하면 워닝 로고가 뜬다.

이펙트 효과 넣기 (+끝나면 삭제)

박스 액터에 ParticleSystemComponent를 추가하고
원하는 이펙트 에셋의 레퍼런스를 파티클 컴포넌트의 템플릿으로 지정하면 된당.

ParticleSystemComponent에서는 OnSystemFinished 델리게이트를 제공한다
아이템상자를 삭제하는 멤버함수를 만들고,
이 델리게이트에 이 멤버함수를 바인딩해주자

OnSystemFinished 델리게이트는 다이내믹 델리게이트이다
다이내믹 델리게이트는 UFUNCTION 매크로가 있는 함수를 바인딩해야 하므로
C++ 람다식으로 표현함 함수는 바인딩할 수 없다

근데,
충돌이 끝나면 액터를 없애긴 하는데

이펙트가 재생될 동안 충돌 기능을 제거해서 여러 번 충돌되지 않도록 하고,
박스의 StaticMesh의 모습을 숨긴다.

액터의 시각적인 기능을 끌 때 사용할 수 있는 함수

SetVisibility

컴포넌트의 시각적인 기능을 아예 없애준다.
에디터 화면과 게임플레이 화면에서 모두 사라짐

HiddenInGame

에디터 레벨 작업을 할 때는 보이고, 게임플레이 중에는 사라진다.

MyItemBox.h

UCLASS()
class HUNT_PROTOTYPE_API AMyItemBox : public AActor
{
	GENERATED_BODY()
	
public:
	...
	UPROPERTY(VisibleAnywhere, Category = Effect)
		UParticleSystemComponent* Effect;

private:
	UFUNCTION()
		void OnEffectFinished(class UParticleSystemComponent* PSystem);
};

MyItemBox.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "MyItemBox.h"
#include "MyWeapon.h"
#include "MyCharacter.h"

// Sets default values
AMyItemBox::AMyItemBox()
{
 	...
	Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("EFFECT"));

	...
	Effect->SetupAttachment(RootComponent);

	...
    
	static ConstructorHelpers::FObjectFinder<UParticleSystem> P_CHESTOPEN(TEXT("ParticleSystem'/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 AMyItemBox::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	HUNT_LOG_S(Warning);

	auto MyCharacter = Cast<AMyCharacter>(OtherActor);
	HUNT_CHECK(nullptr != MyCharacter);

	if (nullptr != MyCharacter && nullptr != WeaponItemClass) {
		if (MyCharacter->CanSetWeapon()) {
			auto NewWeapon = GetWorld()->SpawnActor<AMyWeapon>(WeaponItemClass, FVector::ZeroVector, FRotator::ZeroRotator);
			MyCharacter->SetWeapon(NewWeapon);

			Effect->Activate(true);
			Box->SetHiddenInGame(true, true);
			SetActorEnableCollision(false);
			Effect->OnSystemFinished.AddDynamic(this, &AMyItemBox::OnEffectFinished);
		}
		else {
			HUNT_LOG(Warning, TEXT("%s can't equip weapon currently"), *MyCharacter->GetName());
		}
	}
}

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

요렇게 해주면
파티클 효과가 잘 나오고,
재생이 끝나면 액터가 사라진다.

무기 변경 쉽게 하는 법

은 당연히 블루프린트 ㅎㅎ

이렇게 MyWeapon을 부모로 하는 블루프린트를 생성해주고

스켈레탈 메시만 바꿔주면 된다
너무쉽죵? ㅎ.ㅎ


👀 결과

이미지 설명

사진 클릭 시 영상 링크로 이어집니다

profile
우하하

0개의 댓글