본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 2 - 무한맵의 제작 (9/15)

728x90
반응형

 

 

 

  • 정리
    • 무한 맵 기믹의 제작
      1. 기믹 구현을 위한 다양한 상태 설계
      2. 상태 변경 시 에디터에서 관련 로직을 수행하는 OnConstruction 함수의 활용
      3. 약한 참조를 사용하는 약포인터의 선언과 활용
      4. 에셋 매니저를 활용한 특정 에셋을 로딩하기

 

 

  • 스테이지 기믹 기획
    • 스테이지는 플레이어와 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로 쓸 StaticMeshTrigger로 사용할 StageTrigger를 선언해주고, Overlap되었을때 발동시킬 함수인 OnStageTriggerBeginOverlap()을 선언해준다.
  • Map으로 된 FNameStaticMesh로 구성된 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를 달아 관리하기 쉽게 해준다.

 

StaticMesh>Socket Manager

  • 스태틱 메쉬에 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 > READY > FIGHT > REWARD
  • 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들을 적용시켜준다.

Edit>Project Settings>Game>Asset Manager
Add, ABItemData Class 추가, DataAsset들이 모여있는 Directiories 설정

  • 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.hGetPrimaryAssetId()함수를 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를 통해 랜덤하게 약참조하여 설정해준다.

 

 

 

NPC를 처치하고 나면 4방향에서 아이템박스가 스폰되고, 획득하면 다른 아이템박스들은 소멸
랜덤하게 데이터가 셋팅되는 걸 확인할 수 있다.

 

 

 

 

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런

청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래

www.inflearn.com

 

 

 

 

728x90