다양한 기록

[UE Game Framework] #9 Infinity Map 본문

언리얼 엔진/Unreal Game Framework

[UE Game Framework] #9 Infinity Map

라구넹 2025. 1. 6. 15:55

스테이지 기믹 기획

- 스테이지는 플레이어와 NPC가 1:1로 겨루는 장소

- 스테이지는 총 4개의 상태를 가지고 있으며 순서대로 진행

    - READY : 플레이어의 입장을 처리하는 단계

    - FIGRT : 플레이어와 NPC가 대전하는 단계

    - REWARD : 플레이어가 보상을 선택하는 단계

    - NEXT : 다음 스테이지로 이동을 처리하는 단계

- 무한히 순환 ...

 

스테이지 준비 단계

- 스테이지 중앙에 위치한 트리거 볼륨을 준비

- 플레이어가 트리거 볼륨에 진입하면 대전 단계로 이동

스테이지 대전 단계

- 플레이어가 못나가게 스테이지의 모든 문을 닫고 대전할 NPC를 스폰

- NPC가 없어지면 보상 단계로 이동

스테이지 보상 선택 단계

- 정해진 위치의 4개의 상자에서 아이템을 랜덤하게 생성

- 상자 중 하나를 선택하면 다음 스테이지 단계로 이동

다음 스테이지 선택 단계

- 스테이지의 문을 개방

- 문에 설치된 트리거 볼륨을 활용해 통과하는 문에 새로운 스테이지를 스폰 

 

애셋 매니저

- 언리얼 엔진이 제공하는 애셋을 관리하는 싱글톤 클래스

- 엔진이 초기화될 때 제공되며 애셋 정보를 요청해 받을 수 있음

- PrimaryAssetId를 사용해 프로젝트 내 애셋의 주소를 얻어올 수 있음

- PrimaryAssetId는 태그와 이름의 두가지 키 조합으로 구성됨 (PrimaryAssetTag / PrimaryAssetName)

- 특정 태그를 가진 모든 애셋 목록을 가져올 수 있음

 

랜덤 보상 설정

- 아이템 데이터에 ABItemData라는 태그를 지정

- 프로젝트 설정에서 해당 애셋들이 담긴 폴더를 지정

- 전체 애셋 목록 중에서 하나를 랜덤으로 선택하고 이를 로딩해 보상으로 할당

 

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


#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);
	}

	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); 
		Gate->SetupAttachment(Stage, GateSocket); 
		Gate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
		Gate->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
		Gates.Add(GateSocket, Gate); 

		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& SweepHitResult)
{
}

void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
}

계속 해왔던 컴포넌트 만들고 부착하기

 

메쉬 들어가보면 소켓 매니저 있음

 

상속해서 블루프린트 하나 만들어주기

이렇게 만들어진 걸 볼 수 있음

레벨 새로 만들고 기믹 추가


스테이트 설정

// 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& SweepHitResult);

// Gate Section
	UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
	TMap<FName, TObjectPtr<class UStaticMeshComponent>> Gates;

	UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
	TArray< TObjectPtr<class UBoxComponent> > GateTriggers;

	UFUNCTION()
	void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);

	void OpenAllGates();
	void CloseAllGates();

스테이지랑 게이트

OpenAllGates랑 CloseAllGates는 이렇게 설정됨

 

// State Section
	UPROPERTY(EditAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
	EStageState CurrentState;

	void SetState(EStageState InNewState);

	UPROPERTY()
	TMap<EStageState, FStageChangedDelegateWrapper> StateChangeActions;

	void SetReady();
	void SetFight();
	void SetChooseReward();
	void SetChooseNext();

스테이트 섹션

	// 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::SetState(EStageState InNewState)
{
	CurrentState = InNewState;

	if (StateChangeActions.Contains(CurrentState))
	{
		StateChangeActions[CurrentState].StageDelegate.ExecuteIfBound();
	}
}

SetState에서 CurrentState로 델리게이트 실행시키기

이 부분을 CurrentState에 의한 스위치문 같은 걸로 해결해도 상관없긴 함 

뭐가 되었든 CurrentState에 의해 바인딩된 네가지 함수 중 하나가 실행됨

void AABStageGimmick::SetReady()
{
	StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
	for (auto& GateTrigger : GateTriggers)
	{
		GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
	}

	OpenAllGates();
}

void AABStageGimmick::SetFight()
{
	StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
	for (auto& GateTrigger : GateTriggers)
	{
		GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
	}

	CloseAllGates();
}

void AABStageGimmick::SetChooseReward()
{
	StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
	for (auto& GateTrigger : GateTriggers)
	{
		GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
	}
}

void AABStageGimmick::SetChooseNext()
{
	StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
	for (auto& GateTrigger : GateTriggers)
	{
		GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
	}

	OpenAllGates();
}

일단 문 닫기만 구현해두기

이제 CurrentState의 변화로 문 열리고 닫히는 걸 확인하고 싶음

=> OnConstruction 함수

트랜스폼 말고도 속성의 변환까지 감지해서 에디터에서 보여줌

CurrentState 변화를 감지해서 SetState가 발동됨 

SetState가 발동되어 문이 닫히고 열리는 걸 확인 가능

 

무한 맵 설정

void AABStageGimmick::OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	SetState(EStageState::FIGHT);
}

void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	check(OverlappedComponent->ComponentTags.Num() == 1); 
	FName ComponentTag = OverlappedComponent->ComponentTags[0]; 
	FName SocketName = FName(*ComponentTag.ToString().Left(2)); 
	check(Stage->DoesSocketExist(SocketName)); 

	FVector NewLocation = Stage->GetSocketLocation(SocketName); 
	TArray<FOverlapResult> OverlapResults; 
	FCollisionQueryParams CollisionQueryParam(SCENE_QUERY_STAT(GateTrigger), false, this); 
	bool bResult = GetWorld()->OverlapMultiByObjectType( 
		OverlapResults, 
		NewLocation, 
		FQuat::Identity, 
		FCollisionObjectQueryParams::InitType::AllObjects, 
		FCollisionShape::MakeSphere(775.0f), 
		CollisionQueryParam
	);

	if (!bResult)
	{
		GetWorld()->SpawnActor<AABStageGimmick>(NewLocation, FRotator::ZeroRotator);
	}
}

트리거에 태그 달아줬으니 하나 있는지 체크해야 함

그걸로 소켓 이름 만들기

게이트 통과할 때 트리거 오버랩 발동하는데, 해당 위치에 이미 만들어진 기믹이 없으면 기믹 새로 스폰

=> 무한 맵

GetWorld()->SpawnActor<AABStageGimmick>(NewLocation, FRotator::ZeroRotator);

같은 형태로 월드의 도움을 받아 액터를 스폰

 

적 NPC 스폰

// Fight Section
protected:
	UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
	TSubclassOf<class AABCharacterNonPlayer> OpponentClass;

	UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
	float OpponentSpawnTime;

	UFUNCTION()
	void OnOpponentDestroyed(AActor* DestroyedActor);

	FTimerHandle OpponentTimerHandle;
	void OnOpponentSpawn();

TSubClassOf는 지정된 클래스 또는 서브 클래스만 참조할 수 있게 한다

확장성을 위해 저렇게 해줌 

	// Fight Section
	OpponentSpawnTime = 2.0f;
	OpponentClass = AABCharacterNonPlayer::StaticClass();

생성자에서 스폰 타임이랑, 클래스 지정

 

void AABStageGimmick::OnOpponentDestroyed(AActor* DestroyedActor)
{
	SetState(EStageState::REWARD);
}

void AABStageGimmick::OnOpponentSpawn()
{
	const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f; 
	AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator); 
	AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor); 
	if (ABOpponentCharacter) 
	{
		ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed); 
	}
}

2초 뒤 한 번 스폰하도록 설정

SetFight에서 타이머 설정해서 OnOpponentSpawn을 불러냄

 

보상 설정과 애셋 매니저

일단 ABItemData에서 PrimaryAssetId() 만들어줌

 

프로젝트 세팅의 애셋 매니저에서 애셋 클래스 지정하고,

모아두는 디렉토리 지정

 

// Reward Section
protected:
	UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
	TSubclassOf<class AABItemBox> RewardBoxClass;

	UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
	TArray<TWeakObjectPtr<class AABItemBox>> RewardBoxes;

	TMap<FName, FVector> RewardBoxLocations;

	UFUNCTION()
	void OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	void SpawnRewardBoxes();
};
	// Reward Section
	RewardBoxClass = AABItemBox::StaticClass();
	for (FName GateSocket : GateSockets)
	{
		FVector BoxLocation = Stage->GetSocketLocation(GateSocket) / 2;
		RewardBoxLocations.Add(GateSocket, BoxLocation);
	}

리워드는 일단 AABItemClass로 해두고,

스폰할 박스 위치 미리 만들어줌

TWeakObjectPtr은 약참조로, TObjectPtr은 강참조라 함

강참조는 자기가 사라지기 전까지 소유하는 오브젝트는 꼭 가지고 있는데,

약참조는 관리는 해야 하는데 독립적으로 활동해서 할 일 다하면 알아서 사라지면 좋겠을 때 사용함 

 

void AABStageGimmick::OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	for (const auto& RewardBox : RewardBoxes)
	{
		if (RewardBox.IsValid())
		{
			AABItemBox* ValidItemBox = RewardBox.Get();
			AActor* OverlappedBox = OverlappedComponent->GetOwner();
			if (OverlappedBox != ValidItemBox)
			{
				ValidItemBox->Destroy();
			}
		}
	}

	SetState(EStageState::NEXT);
}

void AABStageGimmick::SpawnRewardBoxes()
{
	for (const auto& RewardBoxLocation : RewardBoxLocations)
	{
		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);
		}
	}
}

* 리워드 박스가 약참조라 IsValid 해주고 Get으로 가져와야 함

 

그리고 박스에서 랜덤으로 아이템 로드함

 

자꾸 무기 애셋만 못찾아서 찾아보니

무기 리로드하고 무기 애셋 들어가서 다시 재저장하니까 인식된다