아이템 시스템
Unreal 이론2024. 10. 30. 16:21
[이득우의 언리얼 프로그래밍 Part2 수업의 정리]
트리거 박스의 구현
- 루트에 트리거를 설정하고 자식에 메시 컴포넌트 부착
- 이펙트는 기본 값으로 비활성화 상태로 두고 오버랩 이벤트 발생시 발동되도록 설정
- 이펙트 종료시 액터가 제거되도록 설정
// ABItemBox.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ABItemBox.generated.h"
UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AABItemBox();
protected:
UPROPERTY(VisibleAnywhere, Category = Box)
TObjectPtr<class UBoxComponent> Trigger; // Loot Component
UPROPERTY(VisibleAnywhere, Category = Box)
TObjectPtr<class UStaticMeshComponent> Mesh;
UPROPERTY(VisibleAnywhere, Category = Effect)
TObjectPtr<class UParticleSystemComponent> Effect;
// 캐릭터가 박스를 밟았을 때 이펙트
UFUNCTION() // Trigger에 원래 있는 델리게이트에 연결할 함수
void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);
UFUNCTION()
void OnEffectFinished(class UParticleSystemComponent* ParticleSystem);
};
- 헤더에 먼저 Loot Component인 Trigger를 선언하고, Mesh와 Effect도 선언해준다.
- BoxComponent에 존재하는 델리게이트 OnComponentBeginOverlap과 PatricleSystemComponent의 델리게이트 OnSystemFinished에 연결할 함수를 각각 선언해준다.
// ABItemBox.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Item/ABItemBox.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Particles/ParticleSystemComponent.h"
#include "Physics/ABCollision.h"
// Sets default values
AABItemBox::AABItemBox()
{
// CreateDefaultSubobject 함수로 객체 생성
Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerBox"));
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Effect"));
// 액터 내부에 Scene Setup
RootComponent = Trigger;
Mesh->SetupAttachment(Trigger);
Effect->SetupAttachment(Trigger);
Trigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));
Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnOverlapBegin);
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;
}
}
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 끝난 뒤 델리게이트 사용
Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
void AABItemBox::OnEffectFinished(UParticleSystemComponent* ParticleSystem)
{
Destroy();
}
- 액터의 RootComponent를 Trigger로 설정하고, Trigger에 Mesh와 Effect를 Setup해준다.
- Trigger의 충돌은 CPROFILE_ABTRIGGER로 ABCollision.h에 선언했던걸 사용
- OnComponentBeginOverlap 델리게이트는 컴포넌트간에 겹칠때 호출되는 이벤트. 워프포인트, 캐릭터의 히트판정시 데미지 구현 등등 많이 쓴다고 하니 잘 기억해두자.
- OnComponentBeginOverlap에 바인딩한 OnOverlapBegin 함수에서는 Effect를 true로 설정해 재생하고, 발동한 상자 메시를 지우고 Collision을 없앤다. Effect 끝난 뒤 OnSystemFinished 델리게이트를 다시 바인딩
- OnSystemFinished 델리게이트가 실행되면 최종적으로 OnEffectFinished 함수에서 이펙트를 없앤다.
이 아이템 상자에 아이템 정보를 넣어 무기를 습득하도록 구현
// ABItemData.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ABItemData.generated.h"
/**
*
*/
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;
};
- 아이템 데이터를 에셋으로 관리할 수 있도록 PrimaryDataAsset 상속받은 ItemData 클래스 생성
// ABWeaponItemData.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Item/ABItemData.h"
#include "ABWeaponItemData.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API UABWeaponItemData : public UABItemData
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = Weapon)
TObjectPtr<USkeletalMesh> WeaponMesh;
};
- ItemData를 상속받은 WeaponItemData 클래스 생성해 확장 구현
- 별도로 프로젝트 폴더에 Item 클래스들을 관리하기 위한 폴더 생성, 각각 DataAsset 생성
프로젝트에서 사용할 아이템 에셋
- 총 3가지 종류의 아이템 타입 지정
- 무기 타입: 캐릭터에 무기 부착(무기에 의한 부가 스탯 강화)
- 포션 타입: 캐릭터의 HP 회복
- 스크롤 타입: 캐릭터의 기본 스탯 상승
- 실제 스탯 구현은 차후 강좌
프로젝트의 주요 레이어
- 데이터 레이어: 게임을 구성하는 기본 데이터 (스탯 정보, 캐릭터 레벨 테이블 등)
- 미들웨어 레이어: 게임에 사용되는 미들웨어 모듈 (UI, 아이템, 애니메이션, AI 등)
- 게임 레이어: 게임 로직을 구체적으로 구현하는데 사용 (캐릭터, 게임모드 등)
- 위에서 아래로는 직접 참조하되, 아래에서 위로는 인터페이스를 통해 접근하도록 설정
- ex) 아이템을 먹어 캐릭터가 행동을 수행하도록 아래에서 위로 명령을 내리는 상황 -> 인터페이스 사용
- ABCharacterItemInterface 클래스를 생성해 TakeItem 가상함수 생성
// ABCharacterItemInterface.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ABCharacterItemInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UABCharacterItemInterface : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
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;
};
- ItemBox 클래스에 Item 언리얼 오브젝트 포인터 변수 추가
- OnOverlapBegin 함수에 아이템 습득 추가
void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
if (nullptr == Item) {
Destroy();
return;
}
IABCharacterItemInterface* OverlappingPawn = Cast<IABCharacterItemInterface>(OtherActor);
if (OverlappingPawn) {
OverlappingPawn->TakeItem(Item);
}
Effect->Activate(true);
Mesh->SetHiddenInGame(true);
SetActorEnableCollision(false);
// Effect 끝난 뒤 델리게이트 사용
Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
- CharacterBase 클래스에 TakeItem 가상함수 구현 및 아이템 종류별 행동 구현할 Delegate 및 구조체 선언, Delegate에 바인딩 할 가상함수 3개 선언
DECLARE_DELEGATE_OneParam(FOnTakeItemDelegate, class UABItemData* /*InItemData*/);
USTRUCT(BlueprintType)
struct FTakeItemDelegateWrapper {
GENERATED_BODY()
FTakeItemDelegateWrapper() {}
FTakeItemDelegateWrapper(const FOnTakeItemDelegate& InItemDelegate) : ItemDelegate(InItemDelegate) {}
FOnTakeItemDelegate ItemDelegate;
};
protected:
UPROPERTY()
TArray<FTakeItemDelegateWrapper> TakeItemActions;
virtual void TakeItem(class UABItemData* InItemData) override;
virtual void DrinkPotion(class UABItemData* InItemData);
virtual void EquipWeapon(class UABItemData* InItemData);
virtual void ReadScroll(class UABItemData* InItemData);
- cpp 파일에서 구체적인 동작 구현
AABCharacterBase::AABCharacterBase()
{
// ItemActions
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)
{
// Item 종류 따라 캐릭터의 다른 동작 구현
if (InItemData) {
TakeItemActions[(uint8)InItemData->Type].ItemDelegate.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"));
}
- 생성자에 무기 스켈레탈 메시 부여, EquipWeapon 함수에서 적용
AABCharacterBase::AABCharacterBase()
{
// Weapon Component
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Weapon"));
// 그냥 트랜스폼 부여x, 캐릭터의 특정 본에 무기가 항상 부착되도록 소켓 이름 지정
// hand_rSocket은 Infinity Warrior Character에 원래 존재하는 소켓 명
Weapon->SetupAttachment(GetMesh(), TEXT("hand_rSocket"));
}
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
if (WeaponItemData) {
Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh);
}
UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}
소프트 레퍼런싱 vs 하드 레퍼런싱
- 액터 로딩시 TObjectPtr로 선언한 언리얼 오브젝트도 따라서 메모리에 로딩됨
- 이를 하드 레퍼런싱이라 함
- 게임 진행에 필수적인 언리얼 오브젝트는 이렇게 선언 가능. 근데 아이템은?
- 데이터 라이브러리에 1000종의 아이템 목록이 있으면 이걸 다 로딩할것인지? -> 메모리 부담 1000배
- 필요한 데이터만 로딩하도록 TSoftObjectPtr로 선언하고 대신 에셋 주소 문자열 지정
- 필요시 에셋 로딩하도록 구현 변경할 수 있으나 에셋 로딩 시간 소요됨
- 현재 게임에서 로딩되어 있는 스켈레탈 메시 목록 살펴보기 -> 콘솔 명령어 [Obj List Class=SkeletalMesh]
- 그럼 WeaponMesh를 SoftObjectPtr로 바꿔서 확인
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
if (WeaponItemData) {
if (WeaponItemData->WeaponMesh.IsPending()) {
WeaponItemData->WeaponMesh.LoadSynchronous();
}
Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh.Get());
}
UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}
- 이렇게 바꾸면 처음 엔진 로딩시에는 스켈레탈 메시가 로딩되지 않고, 아이템을 먹었을 때 로딩된다.
- 언리얼 엔진 최적화의 중요한 요소 [초기의 게임이 로드될 때 메모리 양 최소화]
'Unreal 이론' 카테고리의 다른 글
게임데이터 관리 (0) | 2024.11.06 |
---|---|
무한 맵의 제작 (0) | 2024.10.31 |
캐릭터 스탯과 위젯 (1) | 2024.10.27 |
캐릭터 콤보 액션 (0) | 2024.10.22 |
캐릭터 애니메이션 설정 (0) | 2024.10.21 |
댓글()