728x90
반응형
- 정리
- 무한 맵 기믹의 제작
- 기믹 구현을 위한 다양한 상태 설계
- 상태 변경 시 에디터에서 관련 로직을 수행하는 OnConstruction 함수의 활용
- 약한 참조를 사용하는 약포인터의 선언과 활용
- 에셋 매니저를 활용한 특정 에셋을 로딩하기
- 무한 맵 기믹의 제작
- 스테이지 기믹 기획
- 스테이지는 플레이어와 NPC가 1:1로 겨루는 장소로 정의
- 스테이지는 총 4개의 상태 → 순서대로 진행하도록
- READY : 플레이어의 입장을 처리
- FIGHT : 플레이어와 NPC가 대전
- REWARD : 플레이어가 보상을 선택
- NEXT : 다음 스테이지로 이동을 처리
- 무한히 순환
- 스테이지 준비 단계
- 스테이지 중앙에 위치한 트리거 볼륨 준비
- 플레이어가 트리거 볼륨에 진입하면 대전 단계로 이동
- 스테이지 대전 단계
- 플레이어가 못 나가게 스테이지의 모든 문을 닫고 NPC 스폰
- NPC가 없어지면 보상 단계로 이동
- 스테이지 보상 선택 단계
- 정해진 위치의 4개의 상자에서 아이템을 랜덤하게 생성
- 상자 중 하나를 선택하면 다음 스테이지 단계로 이동
- 다음 스테이지 선택 단계
- 스테이지의 문을 개방
- 문에 설치된 트리거 볼륨을 활용해 통과하는 문에 새로운 스테이지를 스폰
- 스테이지 기믹의 설계와 구현
- 스테이지에 설치한 트리거 볼륨의 감지 처리
- 각 문에 설치한 네 개의 트리거 볼륨의 감지 처리
- 상태별로 설정할 문의 회전 설정
- 대전할 NPC의 스폰 기능
- 아이템 상자의 스폰 기능
- 다음 스테이지의 스폰 기능
- NPC의 죽음 감지 기능
- 아이템 상자의 오버랩 감지
- 아이템의 랜덤 보상을 위해서 애셋 매니저라는 기능을 활용할 필요가 있다.
- 애셋 매니저
- 언리얼 엔진이 제공하는 애셋을 관리하는 싱글톤 클래스
- 엔진이 초기화될 때 제공되며, 애셋 정보를 요청해 받을 수 있음
- PrimaryAssetId를 사용해 프로젝트 내 애셋의 주소를 얻어올 수 있음
- PrimaryAssetId는 태그와 이름의 두 가지 키 조합으로 구성되어 있음
- 특정 태그를 가진 모든 애셋 목록을 가져올 수 있음
- 랜덤 보상 설정
- 아이템 데이터에 ABItemData라는 애셋 태그를 설정
- 프로젝트 설정에서 해당 애셋들이 담긴 폴더를 지정
- 전체 애셋 목록 중에서 하나를 랜덤으로 선택, 이를 로딩해 보상으로 할당 (Weapon, Potion, Scroll)
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ABStageGimmick.generated.h"
UCLASS()
class ARENABATTLE_API AABStageGimmick : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AABStageGimmick();
// Stage Section
protected:
UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UStaticMeshComponent> Stage;
UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UBoxComponent> StageTrigger;
UFUNCTION()
void OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
// Gate Section
protected:
UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
TMap<FName, TObjectPtr<class UStaticMeshComponent>> Gates;
UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
TArray<TObjectPtr<class UBoxComponent>> GateTriggers;
UFUNCTION()
void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
- ABStageGimmick.h
- Stage로 쓸 StaticMesh와 Trigger로 사용할 StageTrigger를 선언해주고, Overlap되었을때 발동시킬 함수인 OnStageTriggerBeginOverlap()을 선언해준다.
- Map으로 된 FName과 StaticMesh로 구성된 Gates 컨테이너와 배열로 된 GateTriggers를 선언해주고, Overlap되었을 때 발동시킬 함수인 OnGateTriggerBeginOverlap()을 선언해준다.
#include "Gimmick/ABStageGimmick.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Physics/ABCollision.h"
// Sets default values
AABStageGimmick::AABStageGimmick()
{
// Stage Section
Stage = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Stage"));
RootComponent = Stage;
static ConstructorHelpers::FObjectFinder<UStaticMesh> StageMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Stages/SM_SQUARE.SM_SQUARE'"));
if (StageMeshRef.Object)
{
Stage->SetStaticMesh(StageMeshRef.Object);
}
// Trigger를 만든 뒤, 영역 범위와 위치를 지정, TriggerProfileName 설정, Trigger와 Overlap되었을때 발생할 함수를 바인딩
StageTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("StageTrigger"));
StageTrigger->SetBoxExtent(FVector(775.0f, 775.0f, 300.0f));
StageTrigger->SetupAttachment(Stage);
StageTrigger->SetRelativeLocation(FVector(0.0f, 0.0f, 250.0f));
StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
StageTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnStageTriggerBeginOverlap);
// Gate Section
static FName GateSockets[] = { TEXT("+XGate"), TEXT("-XGate"), TEXT("+YGate"),TEXT("-YGate") };
static ConstructorHelpers::FObjectFinder<UStaticMesh> GateMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_GATE.SM_GATE'"));
for (FName GateSocket : GateSockets)
{
UStaticMeshComponent* Gate = CreateDefaultSubobject<UStaticMeshComponent>(GateSocket);
Gate->SetStaticMesh(GateMeshRef.Object);
// SetupAttachment()에서 특이하게 소켓을 지정하고 있는데, 소켓으로 위치를 조정하면 살짝만 조정해서 문의 위치를 정확하게 배치할 수 있다.
Gate->SetupAttachment(Stage, GateSocket);
Gate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
Gate->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
// TMap에 Gates를 추가
Gates.Add(GateSocket, Gate);
// 객체를 생성할 때 다른 이름을 가져와야 되기 때문에 GateSocket을 String으로 변환 후 Trigger라는 표현 추가하기
FName TriggerName = *GateSocket.ToString().Append(TEXT("Trigger"));
UBoxComponent* GateTrigger = CreateDefaultSubobject<UBoxComponent>(TriggerName);
GateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.0f));
GateTrigger->SetupAttachment(Stage, GateSocket);
GateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
GateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnGateTriggerBeginOverlap);
// 태그 추가
GateTrigger->ComponentTags.Add(GateSocket);
GateTriggers.Add(GateTrigger);
}
}
void AABStageGimmick::OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}
void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}
- ABStageGimmick.cpp
- 생성자에서 초기화를 진행해준다.
- Gate의 경우에는 소켓을 지정하여 위치 조정에 용이하게 해주고, Tag를 달아 관리하기 쉽게 해준다.
- 스태틱 메쉬에 Socket Manager에 소켓이 달린걸 확인할 수 있다.
- 이후 방금 만든 ABStageGimmick을 상속받은 BP를 생성하고 맵을 구성해준다.
// 원래는 상태 변경 함수등을 Switch문으로 구성하기도 하지만, 상태가 점점 늘어날 수록 굉장히 복잡해 보인다.
// 그래서 델리게이트 어레이로 함수 포인터를 사용해서 구성하는 방법도 있다.
// 델리게이트 선언
// 델리게이트를 감싸는 Wrapper구조체 선언
DECLARE_DELEGATE(FOnStageChangedDelegate)
USTRUCT(BlueprintType)
struct FStageChangedDelegateWrapper
{
GENERATED_BODY()
FStageChangedDelegateWrapper() {}
FStageChangedDelegateWrapper(const FOnStageChangedDelegate& InDelegate) : StageDelegate(InDelegate) {}
FOnStageChangedDelegate StageDelegate;
};
UENUM(BlueprintType)
enum class EStageState : uint8
{
READY = 0,
FIGHT,
REWARD,
NEXT
};
UCLASS()
class ARENABATTLE_API AABStageGimmick : public AActor
{
// ...
protected:
// 에디터에서 값을 변경하면 실행되는 함수
virtual void OnConstruction(const FTransform& Transform) override;
// Gate 모든 문 열기/닫기 함수
void OpenAllGates();
void CloseAllGates();
// ...
// State Section
protected:
UPROPERTY(EditAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
EStageState CurrentState;
// 상태 변경은 언제나 SetState 함수 호출을 통해서 이루어지도록 규칙 설정
void SetState(EStageState InNewState);
UPROPERTY()
TMap<EStageState, FStageChangedDelegateWrapper> StateChangeActions;
// 상태를 바인딩할 함수들
// 이런식으로 상태를 변화시키는 함수들을 별도로 선언하면 스위치문을 사용하지않고 각각의 함수에서 로직 전개 가능
void SetReady();
void SetFight();
void SetChooseReward();
void SetChooseNext();
};
- ABStageGimmick.h
- 에디터에서 값을 변경하면 실행되는 함수인 OnConstruction을 오버라이드해 C++클래스의 테스트가 용이하도록 설정해준다.
- Gate에 대한 로직을 추가해준다.
- OpenAllGates()
- CloseAllGates()
- 상태 변경은 SetState()를 통해서 이루어지도록 내부적으로 규칙을 만들어준다.
- 델리게이트를 사용하여 상태 변경에 따른 로직을 구현해주기 위해 FStageChangedDelegateWrapper 구조체를 만들어주고, 상태와 구조체를 받는 Map 컨테이너, 상태를 바인딩할 함수들을 선언해준다.
- void SetReady();
- void SetFight();
- void SetChooseReward();
- void SetChooseNext();
// Sets default values
AABStageGimmick::AABStageGimmick()
{
// ...
// State Section
// 생성자에서 초기화
CurrentState = EStageState::READY;
// 열거형 값에 따라 연동
StateChangeActions.Add(EStageState::READY, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetReady)));
StateChangeActions.Add(EStageState::FIGHT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetFight)));
StateChangeActions.Add(EStageState::REWARD, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseReward)));
StateChangeActions.Add(EStageState::NEXT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseNext)));
}
// 에디터에서 값을 변경하면 실행되는 함수
void AABStageGimmick::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
SetState(CurrentState);
}
void AABStageGimmick::OpenAllGates()
{
for (auto Gate : Gates)
{
(Gate.Value)->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
}
}
void AABStageGimmick::CloseAllGates()
{
for (auto Gate : Gates)
{
(Gate.Value)->SetRelativeRotation(FRotator::ZeroRotator);
}
}
void AABStageGimmick::SetState(EStageState InNewState)
{
// 원래는 Switch문으로 구성하기도 하지만, 상태가 점점 늘어날 수록 굉장히 복잡해 보인다.
// 그래서 델리게이트 어레이로 함수 포인터를 사용해서 구성하는 방법도 있다.
// 바뀐 상태로 업데이트
CurrentState = InNewState;
// InNewState라는 상태가 자료구조 맵 구조안에 있다면~
if (StateChangeActions.Contains(InNewState))
{
// 해당 델리게이트가 바인딩되어 있으면 실행하도록
// 각 열거형 값에 따라 함수들이 호출된다.
StateChangeActions[CurrentState].StageDelegate.ExecuteIfBound();
}
}
void AABStageGimmick::SetReady()
{
// Player와의 충돌을 위한 Trigger 콜리전 활성화
StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
// Gate는 Trigger 콜리전이 활성화될 필요가 없다.
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
}
OpenAllGates();
}
void AABStageGimmick::SetFight()
{
// StageTrigger는 콜리전이 활성화될 필요가 없다.
StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
// Gate는 Trigger 콜리전이 활성화될 필요가 없다.
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
}
CloseAllGates();
}
void AABStageGimmick::SetChooseReward()
{
// StageTrigger는 콜리전이 활성화될 필요가 없다.
StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
// Gate는 Trigger 콜리전이 활성화될 필요가 없다.
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
}
CloseAllGates();
}
void AABStageGimmick::SetChooseNext()
{
// StageTrigger는 콜리전이 활성화될 필요가 없다.
StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
// Gate는 Trigger 콜리전이 활성화될 필요가 없다.
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
}
OpenAllGates();
}
- ABStageGimmick.cpp
- 상태를 변경하는 함수에 Switch문을 사용해서 로직을 구성해도 되지만, 적당한 양의 상태가 존재할 때는 괜찮을지 몰라도 점점 많아질수록 코드의 형태가 복잡하게 된다.
- 그래서 이전에 사용한 델리게이트 어레이로 함수 포인터를 사용하는 방법도 있다.
- 생성자에서는 상태를 초기화해주고, 열거형 값에 따라 함수가 연동될 수 있도록 바인딩해준다. 이렇게 구성하게 되면 각 상태에 따라 함수가 호출된다.
- Gate Open, Close에 대한 로직을 만들어주고 상태에 따라 로직을 구성해준다.
class ARENABATTLE_API AABStageGimmick : public AActor
{
// Fight Section
protected:
// TSubclassOf - 언리얼에서 제공하는 템플릿 클래스, 지정한 클래스로부터 상속받은 클래스 목록만 표시되도록 한정해서 지정할 수 있게 기능을 제공
// 클래스 정보를 한정시켜주는 기능을 사용해서 보다 편리하게 스폰시킬 NPC를 지정할 수 있음
UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AABCharacterNonPlayer> OpponentClass;
// 스폰에 딜레이를 주기위한 변수
UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
float OpponentSpawnTime;
// NPC가 죽으면 다음 보상단계로 진행하기 위한 함수
UFUNCTION()
void OnOpponentDestroyed(AActor* DestoryedActor);
FTimerHandle OpponentTimerHandle;
void OnOpponentSpawn();
}
- AABStageGimmick.h
#include "Character/ABCharacterNonPlayer.h"
// Sets default values
AABStageGimmick::AABStageGimmick()
{
// ...
// Fight Section
// 스폰에 딜레이를 줌
OpponentSpawnTime = 2.0f;
OpponentClass = AABCharacterNonPlayer::StaticClass();
}
void AABStageGimmick::SetFight()
{
// ...
// 문이 모두 닫히고 나면~
// 딜레이 후 NPC 소환
GetWorld()->GetTimerManager().SetTimer(OpponentTimerHandle, this, &AABStageGimmick::OnOpponentSpawn, OpponentSpawnTime, false);
}
void AABStageGimmick::OnOpponentDestroyed(AActor* DestoryedActor)
{
SetState(EStageState::REWARD);
}
void AABStageGimmick::OnOpponentSpawn()
{
const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f;
// OpponentClass를 TSubclassOf로 한정시켰기 때문에 캐릭터에 있는 ABCharacterNonPlayer를 상속받은 캐릭터에 한정해서만 액터를 스폰시키게 된다.
AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator);
AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor);
// 캐스팅이 정상적으로 된다면, OnDestroyed 델리게이트에 함수를 바인드
if (ABOpponentCharacter)
{
ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
}
}
- AABStageGimmick.cpp
- NEXT상태에서 열린 문쪽으로 가까이 가면 GateTrigger발동과 함께 해당 공간이 빈 공간이라면 맵을 생성하고, READY상태가 된다.
- READY 상태에서 다음 공간으로 가면 StageTrigger발동과 함께 Gate가 닫히게 되며 FIGHT 상태로 전이되며, 일정 시간 후 (2sec) NPC가 생성된다.
- FIGHT상태에서 NPC를 처치하게 되면 REWARD 상태로 전이된다.
- NPC를 처치하고 나면 REWARD 상태에서의 아이템 박스가 4방향에서 나오도록 로직을 구성해주고, 아이템 박스를 획득하고 나면 필요없는 다른 아이템 박스들은 소멸시켜주고 다음 상태인 NEXT 상태로 전이시켜 준다.
UCLASS()
class ARENABATTLE_API AABStageGimmick : public AActor
{
GENERATED_BODY()
// ...
// Reward Section
protected:
// AABItemBox에 상속받은 클래스들을 대상으로 한정시키기 위해 TSubclassOf 템플릿 클래스로 타입 지정
UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AABItemBox> RewardBoxClass;
// TWeakObjectPtr(약참조)
// RewardBoxes의 경우 스폰된 상자를 관리하기 위해서 선언한 것. 이 스폰된 상자들은 사실상 작업하고 있는 스테이지 기믹 액터와는 무관하게 자기 스스로 동작하게 된다.
// 그래서 외부의 영향 or 내부의 로직에 의해 스스로 소멸될 수도 있다. 이 경우 TObjectPtr(강참조)로 걸게 되면 언리얼은 메모리에서 소멸시키지 않을수도 있다.
// 액터 소멸과 함께 메모리에서 소멸되야한다면 강참조가 맞지만, 이렇게 액터와 무관하게 동작해야되는 다른 액터들의 경우 가급적 약참조를 걸어 관리하는게 좋다.
UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
TArray<TWeakObjectPtr<class AABItemBox>> RewardBoxes;
// Key로 관리할 수 있도록 Map 사용
TMap<FName, FVector> RewardBoxLocations;
UFUNCTION()
void OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
void SpawnRewardBoxes();
};
- ABStageGimmick.h
// Sets default values
AABStageGimmick::AABStageGimmick()
{
// ...
// Reward Section
RewardBoxClass = AABItemBox::StaticClass();
for (FName GateSocket : GateSockets)
{
FVector BoxLocation = Stage->GetSocketLocation(GateSocket) / 2;
RewardBoxLocations.Add(GateSocket, BoxLocation);
}
}
// Reward와 충돌했을 때
// 상자를 획득했을 때 실행됨
void AABStageGimmick::OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// 약참조 된 RewardBoxes 순회
for (const auto& RewardBox : RewardBoxes)
{
// IsValid()함수를 통해 진짜 존재하는지 검사
if (RewardBox.IsValid())
{
// 살아있다면 Get()함수를 통해 포인터 얻어오기
AABItemBox* ValidItemBox = RewardBox.Get();
AActor* OverlappedBox = OverlappedComponent->GetOwner();
// 플레이어가 획득한 상자는 남기고 나머지는 다 Desroty()
if (OverlappedBox != ValidItemBox)
{
ValidItemBox->Destroy();
}
}
}
SetState(EStageState::NEXT);
}
// 상자 스폰 로직
void AABStageGimmick::SpawnRewardBoxes()
{
for (const auto& RewardBoxLocation : RewardBoxLocations)
{
// RewardBox를 스폰해준다. Location, Actor, Casting, Overlap시 발동시킬 함수 추가, 약참조 RewardBoxes에 추가
FVector WorldSpawnLocation = GetActorLocation() + RewardBoxLocation.Value + FVector(0.0f, 0.0f, 30.0f);
AActor* ItemActor = GetWorld()->SpawnActor(RewardBoxClass, &WorldSpawnLocation, &FRotator::ZeroRotator);
AABItemBox* RewardBoxActor = Cast<AABItemBox>(ItemActor);
if (RewardBoxActor)
{
RewardBoxActor->Tags.Add(RewardBoxLocation.Key);
RewardBoxActor->GetTrigger()->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnRewardTriggerBeginOverlap);
RewardBoxes.Add(RewardBoxActor);
}
}
}
- ABStageGimmick.cpp
- 보상 아이템상자들의 경우 약참조로 선언해준다. 이 상자들은 사실상 작업하고 있는 스테이지 기믹 액터와는 무관하게 자기 스스로 동작하게 된다.
- 그래서 외부의 영향 or 내부의 로직에 의해 스스로 소멸될 수도 있다. 이 경우 TObjectPtr(강참조)로 걸게 되면 언리얼은 메모리에서 소멸시키지 않을수도 있다.
- 액터 소멸과 함께 메모리에서 소멸되야한다면 강참조가 맞지만, 이렇게 액터와 무관하게 동작해야되는 다른 액터들의 경우 가급적 약참조를 걸어 관리하는게 좋다.
- OnRewardTriggerBeginOverlap()함수를 통해 플레이어가 획득한 상자 빼고 다른 상자들은 Destroy()로 제거후 NEXT 상태로 전이, 무한맵 로직 완성
- 현재 아이템 박스의 경우 ABItemData라고 하는 Data Asset을 부모로 가지는 에셋들을 활용하여, 랜덤이라고는 하지만 하나씩 에셋 정보를 넣어줬지만, 모두 불러와 랜덤하게 Data Asset들을 적용시켜준다.
- Data Asset들을 모두 불러올 수 있는 효과적인 방법은 Project Settings에 있는 Asset Manager를 활용하는 것
- Asset Manager는 엔진이 초기화될 때 반드시 활성화되는 단 하나의 싱글턴 클래스다.
- Primary Asset Types to Scan에서 ABItemData를 관리하도록 지정해주고 이 에셋 매니저로 부터 모든 에셋의 목록을 불러오도록 설정해준다.
- 설정후, 제작한 클래스의 선언에서 PrimaryAssetId라고 하는 값을 ABItemData라는 것으로 태그를 달아주면 된다.
UCLASS()
class ARENABATTLE_API UABItemData : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
// UPrimaryDataAsset 내부의 생성될 에셋의 아이디를 직접 지정해줄 수 있는 GetPrimaryAssetId() 함수를 오버라이드
FPrimaryAssetId GetPrimaryAssetId() const override
{
// 두가지 정보를 바탕으로 유일한 식별자 아이디값을 만들어 낼 수 있다.
return FPrimaryAssetId("ABItemData", GetFName());
}
// ...
};
- ABItemData.h
- ABItemData.h와 이를 상속받은 클래스인 ABWeaponItemData.h 에 GetPrimaryAssetId()함수를 override 시켜준다.
UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
GENERATED_BODY()
// ...
protected:
// ItemBox의 액터 셋팅이 마무리되는 시점에 랜덤하게 보상을 넣을 수 있도록 PostInitializeComponents()함수를 사용
virtual void PostInitializeComponents() override;
// ...
}
- ABItemBox.h
#include "Engine/AssetManager.h" // 에셋 매니저 헤더 추가
#include "ABItemData.h"
// ...
// ItemBox가 초기화가 된 이후
void AABItemBox::PostInitializeComponents()
{
Super::PostInitializeComponents();
// 이 에셋 매니저의 경우 엔진이 초기화될 때 언제나 로딩을 보장해준다라고 보면 된다.
UAssetManager& Manager = UAssetManager::Get();
// Manager의 GetPrimaryAssetIdList()함수에 태그 정보를 넘겨주면 지정한 폴더 내에 있는 모든 에셋에 대해서 태그 아이디를 가지고 있는 에셋들의 목록을 배열로 반환해준다.
TArray<FPrimaryAssetId> Assets;
Manager.GetPrimaryAssetIdList(TEXT("ABItemData"), Assets);
// 잘 동작하는가?
ensure(0 < Assets.Num());
// 약참조 후, RandomIndex를 통해 랜덤하게 AssetData를 지정
int32 RandomIndex = FMath::RandRange(0, Assets.Num() - 1);
FSoftObjectPtr AssetPtr(Manager.GetPrimaryAssetPath(Assets[RandomIndex]));
if (AssetPtr.IsPending())
{
AssetPtr.LoadSynchronous();
}
Item = Cast<UABItemData>(AssetPtr.Get());
ensure(Item);
}
- ABItemBox.cpp
- ItemBox 액터 셋팅이 마무리되는 시점에 랜덤하게 보상을 넣을 수 있도록 마지막 호출 함수인 PostInitializeComponents()에서 GetPrimaryAssetIdList()의 태그정보로 가져온 에셋 데이터들을 RandomIndex를 통해 랜덤하게 약참조하여 설정해준다.
해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런
청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래
www.inflearn.com
728x90
'공부 > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
[Study] Part 2 - 행동 트리 모델의 이해 (11/15) (0) | 2024.06.19 |
---|---|
[Study] Part 2 - 게임데이터 관리 (10/15) (0) | 2024.06.18 |
[Study] Part 2 - 아이템 시스템 (8/15) (0) | 2024.06.13 |
[Study] Part 2 - 캐릭터 스탯과 위젯 (7/15) (0) | 2024.06.04 |
[Study] Part 2 - 캐릭터 공격 판정 (6/15) (0) | 2024.06.03 |