아이템을 획득 할 수 있는 박스를 생성하고 어떤 종류의 박스인지에 따라 획득하는 아이템이 다르도록 만들어보자.
- 캐릭터에 다양한 종류의 아이템을 제공하는 시스템을 만들어 본다.
- 이번에 다루는 게임 프레임워크는 기믹의 트리거를 맡게 될 예정.
- 트리거 박스를 활용하여 아이템 상자를 구현한다
- 다양한 종류의 아이템에 대한 개별적인 습득 처리에 대한 구현
- 소프트오브젝트 레퍼런스와 하드 오브젝트 레퍼런스의 차이를 이해한다.
아이템 박스
아이템이 있다하더라도 충돌을 감지 해야만 습득을 할 수 있기 때문에 아이템 박스를 만들고 트리거를 설정해줘야한다.
ABItemBox라고 하는 액터를 생성하고
각 요소로 트리거와 static mesh, ParticleSystem을 추가한다.
루트 컴포넌트는 트리거로 설정한다.
protected:
UPROPERTY(VisibleAnywhere, Category = Box)
TObjectPtr<class UBoxComponent> Trigger;
UPROPERTY(VisibleAnywhere, Category = Box)
TObjectPtr<class UStaticMeshComponent> Mesh;
UPROPERTY(VisibleAnywhere, Category = Effect)
TObjectPtr<class UParticleSystemComponent> Effect;
};
AABItemBox::AABItemBox()
{
//일단 가장 기본적인 CDO로 만들어 놓은 다음에,,
Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerBox"));
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Effect"));
//루트 컴포넌트로 트리거를 설정하고 나머지는 Attachment를 진행하는 형태
RootComponent = Trigger;
Mesh->SetupAttachment(Trigger);
Effect->SetupAttachment(Trigger);
//그 다음에 다 좋은데 트리거 같은 경우에는 우리가 만들어 놓은 콜리전 채널을 사용하게 한다.
Trigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
//ConstructorHelpers를 이용하여 박스 메시를 가져오는 작업을 수행한다.
static ConstructorHelpers::FObjectFinder<UStaticMesh> BoxMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Env_Breakables_Box1.SM_Env_Breakables_Box1'"));
if (BoxMeshRef.Object) {
Mesh->SetStaticMesh(BoxMeshRef.Object);
}
//메시의 위치에 대한 설정을 진행하고 콜리전 채널 또한 맞춰주는데 이거 같은 경우에는
//아까 지정해줬던 콜리전 채널과는 구별이 되어야 하므로 다음과 같이 지정한다.
Mesh->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
Mesh->SetCollisionProfileName(TEXT("NoCollision"));
//이펙트의 경우에는 직관적으로 다음과 같이 놓아두되, 처음부터 뻥뻥터지는건 옳지 않으므로,
//다음과 같이 지정을 해두면 된다.
static ConstructorHelpers::FObjectFinder<UParticleSystem> EffectRef(TEXT("/Script/Engine.ParticleSystem'/Game/ArenaBattle/Effect/P_TreasureChest_Open_Mesh.P_TreasureChest_Open_Mesh'"));
if (EffectRef.Object) {
Effect->SetTemplate(EffectRef.Object);
Effect->bAutoActivate = false;
}
}
따로 액터를 스폰하지 않고 레벨 자체적으로 배치한다.
저기 있는 트리거와 충돌 한다는 것은 코드상에서 감지를 해줘야만 한다. 즉, 어떠한 이벤트가 수행되어야 한다.
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);
매개 변수가 이렇게 긴 이유는 우리가 지정한 Trigger에 델리게이트가 이미 존재하는데 이에 연결을 해주기 위해서는 다음과 같은 매개변수가 있기 때문이다.
- OverlappedComponent : 해당 액터에서 겹쳐진 부분에 대해서 가져옴.
- OtherActor : 이제 충돌한 액터에 관한 정보를 가져옴.
- OtherComp : 이 액터의 어떤 부분과 충돌했는지
- OtherBodyIndex : 몇번 째 인덱스인지
- bFromSweep : Sweep으로부터 일어난 일인지
- FHitResult : 이제 Hit된 결과들
따라서 이렇게 사용할 수 있다.
Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnOverlapBegin);
그래서 트리거 overlap 되면 OnOverlapBegin 함수가 실행될 것이다.
void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
Effect->Activate(true);
Mesh->SetHiddenInGame(true);
SetActorEnableCollision(false);
Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
void AABItemBox::OnEffectFinished(UParticleSystemComponent* ParticleSystem)
{
Destroy();
}
이렇게 하면 캐릭터가 상자에 닿으면 이펙트가 틀어지고 상자의 모습은 사라지게 된다. 그리고 이펙트가 모두 종료되면 box actor가 사라지게 된다.
아이템
아이템에 관한 정보를 전부 Data Asset으로 관리할 것이다.
UENUM(BlueprintType)
enum class EItemType : uint8
{
Weapon = 0,
Potion,
Scroll
};
/**
*
*/
UCLASS()
class ARENABATTLE_API UABItemData : public UPrimaryDataAsset
{
GENERATED_BODY()
public :
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Type)
EItemType Type;
};
여러가지의 아이템을 만들 수 있게 만들 것이다.
UCLASS()
class ARENABATTLE_API UABWeaponItemData : public UABItemData
{
GENERATED_BODY()
public :
UPROPERTY(EditAnywhere, Category = Weapon)
TObjectPtr<USkeletalMesh> WeaponMesh;
};
그리고 Content단으로 돌아가 UABItemData를 이용해 DataAsset을 만들어주고 다음과 같이 설정한다.
이제 이를통해 아이템을 얻는 코드를 작성해줘야하는데
문제는 의존성이다.
코드를 작성하면서 많이들 느끼지만 구조를 아름답게 작성하는 것이 매우 중요하다. 추후의 설계가 변경되거나 새로운 아이템이 추가되었을 때, 빠른 대처와 유연성이 중요하기 때문이다.
따라서 레이어를 3개로 나눈다.
- 데이터 레이어 : 게임을 구성하는 기본 데이터 (스탯정보, 캐릭터 레벨 테이블 등등)
- 미들웨어 레이어 : 게임에 사용되는 독립적 모듈 (UI, 아이템, 애니메이션, AI 등등)
- 게임 레이어 : 게임 로직을 구체적으로 구현하는데 사용한다 (캐릭터, 게임 모드 등등)
- 위에서 아래로는 직접 참조하되, 아래에서 위로는 인터페이스를 통해 접근하도록 설정하게 한다. 상단에 있는건 하단에 있는 것을 헤더로써 참조가 가능하지만, 하단에 있는 놈은 상단에 있는 것을 참조할 수 없다.
위에서 아래로 명령을 내릴때는 직접잠초하면 되지만 그 반대의 경우는 인터페이스를 통해 접근하도록 한다.
게임 레이어의 경우에는, 게임 모드, 캐릭터 이런 것들을 의미한다.
이 외의 아이템, 스탯, UI 같은 것들은 애초에 캐릭터에 서비스를 제공하는 구조이기 때문에, 미들웨어의 개념으로 바라보는 것이 좋아보임.
그렇기 때문에 얘는 캐릭터에 붙을 수는 있어도, 캐릭터를 알고 있으면 안된다.
(조금은 독립을 할 필요는 있다는 말임)
이렇게 만들어야 기능 확장에 대해서 유연하게 대처가 가능하다.
코드 구현
class ARENABATTLE_API IABCharacterItemInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
virtual void TakeItem(class UABItemData* InItemData) = 0;
};
아이템 획득할 경우에 대한 interface를 만들고 이를 Character가 상속받아 구현하도록 한다.
class ARENABATTLE_API AABCharacterBase : public ACharacter, public IABAnimationAttackInterface, public IABCharacterWidgetInterface, public IABCharacterItemInterface
{
...
//Item Section
protected :
virtual void TakeItem(class UABItemData* InItemData);
};
이제 이를 위에서 만든 BoxTrigger에서 처리하면 된다.
void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
IABCharacterItemInterface* OverlappingPawn = Cast<IABCharacterItemInterface>(OtherActor);
if (OverlappingPawn) {
OverlappingPawn->TakeItem(Item);
}
Effect->Activate(true);
Mesh->SetHiddenInGame(true);
SetActorEnableCollision(false);
Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
TakeItem에서는 델리게이트를 이용할 것이다.
가령 무기가 들어오면 지정된 함수가 작동되고 포션이 들어왔을땐 DrinkPotion함수가 작동되는 식이다. 물론 이를 Swtich로 관리해도 되지만 그것보단 델리게이트를 사용해보겠다.
그런데 TArray를 이용해서 여러개의 델리게이트를 이용할 수 없다. 언리얼엔진에서 UPROPERTY 때문에 델리게이트는 TArry의 요소로 사용할 수 없다. 따라서 이를 허용하기 위한 구조체를 만들고 이 구조체 안에 델리게이트를 넣는 식으로 해보겠다.
DECLARE_DELEGATE_OneParam(FOnTakeItemDelegate, class UABItemData* /*InItemData*/);
USTRUCT(BlueprintType)
struct FTakeItemDelegateWrapper {
GENERATED_BODY()
FTakeItemDelegateWrapper() {}
FTakeItemDelegateWrapper(const FOnTakeItemDelegate& InItemDelegate) : ItemDeleagate(InItemDelegate) {}
FOnTakeItemDelegate ItemDeleagate;
};
virtual void DrinkPotion(class UABItemData* InItemData);
virtual void EquipWeapon(class UABItemData* InItemData);
virtual void ReadScroll(class UABItemData* InItemData);
이렇게 생성과 동시에 구조체에 넣을 수 있다.
//Item Action
TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::EquipWeapon)));
TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::DrinkPotion)));
TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::ReadScroll)));
void AABCharacterBase::TakeItem(UABItemData* InItemData)
{
if (InItemData) {
TakeItemActions[(uint8)InItemData->Type].ItemDeleagate.ExecuteIfBound(InItemData);
}
}
void AABCharacterBase::DrinkPotion(UABItemData* InItemData)
{
UE_LOG(LogABCharacter, Log, TEXT("Drink Potion"));
}
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}
void AABCharacterBase::ReadScroll(UABItemData* InItemData)
{
UE_LOG(LogABCharacter, Log, TEXT("Read Scroll"));
}
무기 획득
지금은 로그만 작성해서 아무런 행동의 변화가 없지만 무기를 손에 쥐게 만들어보자.
먼저 우리가 착용할 무기를 선언하자.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Equipment, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USkeletalMeshComponent> Weapon;
//Weapon Component
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Weapon"));
Weapon->SetupAttachment(GetMesh(), TEXT("hand_rSocket"));
여기서 hand_rSocket은 SkeletalMesh에서 이미 만들어진 소켓중 하나이다.
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
//어쨌던 지금 들어온 것은 아이템 데이터고 이 아이템 데이터의 경우에는
UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
if (InItemData) {
Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh);
}
}
이렇게 하면 정상적으로 작동을 하게된다.
소프트 레퍼런싱
에셋을 다룸에 있어서 레퍼런싱은 매우 중요하다. 즉, 최적화가 매우 중요하다는 얘기이다.
일반적으로 액터를 로딩할 시, TObjectPtr로 선언을 한 언리얼 오브젝트도 따라서 메모리에 로딩이 되었음.
⇒ 이를 하드 레퍼런싱이라고 함.
일단 게임 진행을 할 때 필수적인 건 저렇게 하드하게 가지고 오는 건 Ok인데, 아이템 같은 경우에 우르르르르 끌고 오면 어떻게 될까?
그러면 문제가 발생한다.
선언을 이제 하는 건 좋은데 로딩할 때 필요할 때만 로딩하는 것이 좋기 때문에,
TSoftObjectPtr을 사용해서 애셋을 로딩하는데 시간을 줄일 수 있게 될 것이다.
따라서
이렇게 바꿔줄 수 있다.
UPROPERTY(EditAnywhere, Category = Weapon)
TSoftObjectPtr<USkeletalMesh> WeaponMesh;
바꾸고 빌드하면 에러가 나는데 기존에 사용했던 코드에서 문제가 발생한다. 왜냐면 소프트 레퍼런싱은 아직 로드가 됐는지 안됐는지 모르기 때문이다.
따라서 이런 코드로 수정해준다.
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
if (InItemData) {
//로드가 아직 안되었다면?
if (WeaponItemData->WeaponMesh.IsPending()) {
//동기적인 로드를 진행하고,
WeaponItemData->WeaponMesh.LoadSynchronous();
}
//로드가 된 데이터를 가져오는 작업을 수행한다.
Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh.Get());
}
}
'Unreal Engine 5 > 강의' 카테고리의 다른 글
[UE5] 데이터 - 게임 데이터 관리하기 (0) | 2024.12.12 |
---|---|
[UE5] 스폰 - 무한 맵 제작 (0) | 2024.12.10 |
[UE5] 애니메이션 - 캐릭터 공격 판정 (0) | 2024.12.04 |
[UE5] 애니메이션 - 캐릭터 애니메이션과 몽타주를 이용한 연속 공격 (0) | 2024.12.03 |
[UE5] 캐릭터 움직임 - 캐릭터 컨트롤에 대해 (0) | 2024.11.29 |