<aside> 💡 목차

</aside>

<aside>

1. GameState를 이용한 게임 루프 구현하기


1️⃣ GameState와 GameMode

2️⃣ SpawnVolume 클래스 스폰 데이터 반환 수정

#pragma once

#include "CoreMinimal.h"
#include "ItemSpawnRow.h"
#include "GameFramework/Actor.h"
#include "SpawnVolume.generated.h"

class UBoxComponent;

UCLASS()
class SPARTAPROJECT_API ASpawnVolume : public AActor
{
	GENERATED_BODY()
	
public:	
	ASpawnVolume();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	USceneComponent* Scene;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	UBoxComponent* SpawningBox;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
	UDataTable* ItemDataTable;

	UFUNCTION(BlueprintCallable, Category = "Spawning")
	**AActor* SpawnRandomItem();** // 리턴 형식을 AActor* 로 변경

	FItemSpawnRow* GetRandomItem() const;
	**AActor* SpawnItem(TSubclassOf<AActor> ItemClass);**
	FVector GetRandomPointInVolume() const;
};
#include "SpawnVolume.h"
#include "Components/BoxComponent.h"

ASpawnVolume::ASpawnVolume()
{
	PrimaryActorTick.bCanEverTick = false;

	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);

	SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawningBox"));
	SpawningBox->SetupAttachment(Scene);

	ItemDataTable = nullptr;
}

**AActor* ASpawnVolume::SpawnRandomItem()**
{
		if (FItemSpawnRow* SelectedRow = GetRandomItem())
		{
				if (UClass* ActualClass = SelectedRow->ItemClass.Get())
				{
						// 여기서 SpawnItem()을 호출하고, 스폰된 AActor 포인터를 리턴
						**return SpawnItem(ActualClass);**
				}
		}
		
		**return nullptr;**
}

FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
	if (!ItemDataTable) return nullptr;

	TArray<FItemSpawnRow*> AllRows;
	static const FString ContextString(TEXT("ItemSpawnContext"));
	ItemDataTable->GetAllRows(ContextString, AllRows);

	if (AllRows.IsEmpty()) return nullptr;

	float TotalChance = 0.0f;
	for (const FItemSpawnRow* Row : AllRows)
	{
		if (Row)
		{
			TotalChance += Row->SpawnChance;
		}
	}

	const float RandValue = FMath::FRandRange(0.0f, TotalChance);
	
	float AccumulateChance = 0.0f;

	for (FItemSpawnRow* Row : AllRows)
	{
		AccumulateChance += Row->SpawnChance;
		if (RandValue <= AccumulateChance)
		{
			return Row;
		}
	}

	return nullptr;
}

FVector ASpawnVolume::GetRandomPointInVolume() const
{
	FVector BoxExtent = SpawningBox->GetScaledBoxExtent();
	FVector BoxOrigin = SpawningBox->GetComponentLocation();

	return BoxOrigin + FVector(
		FMath::FRandRange(-BoxExtent.X, BoxExtent.X),
		FMath::FRandRange(-BoxExtent.Y, BoxExtent.Y),
		FMath::FRandRange(-BoxExtent.Z, BoxExtent.Z)
	);
}

**AActor* ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)**
{
		**if (!ItemClass) return nullptr;**
	
		// SpawnActor가 성공하면 스폰된 액터의 포인터가 반환됨
		**AActor* SpawnedActor =** GetWorld()->SpawnActor<AActor>(
				ItemClass,
				GetRandomPointInVolume(),
				FRotator::ZeroRotator
		);
		
		**return SpawnedActor;**
}

3️⃣ GameState 기반의 게임 루프 구현

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "**SpartaGameState**.generated.h"

UCLASS()
class SPARTAPROJECT_API **ASpartaGameState** : public **AGameState**
{
		GENERATED_BODY()

public:
		ASpartaGameState();
	
		**virtual void BeginPlay() override;**
	
		UPROPERTY(VisibleAnyWhere, BlueprintReadWrite, Category = "Score")
		int32 Score;
		// 현재 레벨에서 스폰된 코인 개수
		**UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")**
		**int32 SpawnedCoinCount;**
		// 플레이어가 수집한 코인 개수
		**UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")**
		**int32 CollectedCoinCount;**
		// 각 레벨이 유지되는 시간 (초 단위)
		**UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")**
		**float LevelDuration;**
		// 현재 진행 중인 레벨 인덱스
		**UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")**
		**int32 CurrentLevelIndex;**
		// 전체 레벨의 개수
		**UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")**
		**int32 MaxLevels;**
		// 실제 레벨 맵 이름 배열. 여기 있는 인덱스를 차례대로 연동
		**UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Level")**
		**TArray<FName> LevelMapNames;**
	
		// 매 레벨이 끝나기 전까지 시간이 흐르도록 관리하는 타이머
		**FTimerHandle LevelTimerHandle;**
	
		UFUNCTION(BlueprintPure, Category = "Score")
		int32 GetScore() const;
		UFUNCTION(BlueprintCallable, Category = "Score")
		void AddScore(int32 Amount);
		// 게임이 완전히 끝났을 때 (모든 레벨 종료) 실행되는 함수
		**UFUNCTION(BlueprintCallable, Category = "Level")**
		**void OnGameOver();**
	
		// 레벨을 시작할 때, 아이템 스폰 및 타이머 설정
		**void StartLevel();**
		// 레벨 제한 시간이 만료되었을 때 호출
		**void OnLevelTimeUp();**
		// 코인을 주웠을 때 호출
		**void OnCoinCollected();**
		// 레벨을 강제 종료하고 다음 레벨로 이동
		**void EndLevel();**
};
#include "SpartaGameState.h"
**#include "Kismet/GameplayStatics.h"
#include "SpawnVolume.h"
#include "CoinItem.h"**

ASpartaGameState::ASpartaGameState()
{
		Score = 0;
		**SpawnedCoinCount = 0;**
		**CollectedCoinCount = 0;**
		**LevelDuration = 30.0f;** // 한 레벨당 30초
		**CurrentLevelIndex = 0;**
		**MaxLevels = 3;**
}

**void ASpartaGameState::BeginPlay()
{**
		**Super::BeginPlay();**
	
		// 게임 시작 시 첫 레벨부터 진행
		**StartLevel();**
**}**

int32 ASpartaGameState::GetScore() const
{
		return Score;
}

void ASpartaGameState::AddScore(int32 Amount)
{
		Score += Amount;
		UE_LOG(LogTemp, Warning, TEXT("Score: %d"), Score);
}

**void ASpartaGameState::StartLevel()
{**
		// 레벨 시작 시, 코인 개수 초기화
		**SpawnedCoinCount = 0;**
		**CollectedCoinCount = 0;**
	
		// 현재 맵에 배치된 모든 SpawnVolume을 찾아 아이템 40개를 스폰
		**TArray<AActor*> FoundVolumes;**
		**UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);**
	
		**const int32 ItemToSpawn = 40;**
		
		**for (int32 i = 0; i < ItemToSpawn; i++)
		{
				if (FoundVolume.Num() > 0)
						{
						ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
						if (SpawnVolume)
						{
								AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();**
								// 만약 스폰된 액터가 코인 타입이라면 SpawnedCoinCount 증가
**								if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
								{
										SpawnedCoinCount++;
								}
						}				
				}
		}**
	
		// 30초 후에 OnLevelTimeUp()가 호출되도록 타이머 설정
		**GetWorldTimerManager().SetTimer(
			LevelTimerHandle,
			this,
			&ASpartaGameState::OnLevelTimeUp,
			LevelDuration,
			false
		);
	
		UE_LOG(LogTemp, Warning, TEXT("Level %d Start!, Spawned %d coin"),
			CurrentLevelIndex + 1,
			SpawnedCoinCount);**
**}**

**void ASpartaGameState::OnLevelTimeUp()
{**
		// 시간이 다 되면 레벨을 종료
		**EndLevel();**
**}**

**void ASpartaGameState::OnCoinCollected()
{**
		**CollectedCoinCount++;**
	
		**UE_LOG(LogTemp, Warning, TEXT("Coin Collected: %d / %d"), 
			CollectedCoinCount,
			SpawnedCoinCount)**
	
		// 현재 레벨에서 스폰된 코인을 전부 주웠다면 즉시 레벨 종료
		**if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
		{**
				**EndLevel();**
		**}**
**}**

**void ASpartaGameState::EndLevel()
{**
		// 타이머 해제
		**GetWorldTimerManager().ClearTimer(LevelTimerHandle);**
		// 다음 레벨 인덱스로
**		CurrentLevelIndex++;**

		// 모든 레벨을 다 돌았다면 게임 오버 처리
		**if (CurrentLevelIndex >= MaxLevels)
		{**
				**OnGameOver();
				return;**
		**}**
		
		// 레벨 맵 이름이 있다면 해당 맵 불러오기
**		if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
		{
				UGamePlayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]);
		}
		else
		{**
				// 맵 이름이 없으면 게임오버
**				OnGameOver();
		}**
**}**

**void ASpartaGameState::OnGameOver()
{**
		**UE_LOG(LogTemp, Warning, TEXT("Game Over!!"));**
		// 여기서 UI를 띄운다거나, 재시작 기능을 넣을 수도 있음
**}**

4️⃣ 코인 아이템 점수 획득 로직 수정

#include "CoinItem.h"
#include "Engine/World.h"
#include "SpartaGameState.h"

ACoinItem::ACoinItem()
{
		PointValue = 0;
		ItemType = "DefaultCoin";
}

void ACoinItem::ActivateItem(AActor* Activator)
{
		if (Activator && Activator->ActorHasTag("Player"))
		{
				if (UWorld* World = GetWorld())
				{
						if (ASpartaGameState* GameState = World->GetGameState<ASpartaGameState>())
						{
								GameState->AddScore(PointValue);
								**GameState->OnCoinCollected();**
						}
				}
		
				DestroyItem();
		}
}

</aside>

<aside>

2. Game Instance를 활용한 데이터 유지하기


1️⃣ Game Instance란?

  1. Game Instance
  2. Seamless Travel

2️⃣ Game Instance 생성 및 변수 선언

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "SpartaGameInstance.generated.h"

UCLASS()
class SPARTAPROJECT_API USpartaGameInstance : public UGameInstance
{
		GENERATED_BODY()
	
public:
		**USpartaGameInstance();**
	
		// 게임 전체 누적 점수
		**UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "GameData")
		int32 TotalScore;**
		// 현재 레벨 인덱스 (GameState에서도 관리할 수 있지만, 맵 전환 후에도 살리고 싶다면 GameInstance에 복제할 수 있음)
		**UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "GameData")
		int32 CurrentLevelIndex;**
	
		**UFUNCTION(BlueprintCallable, Category = "GameData")
		void AddToScore(int32 Amount);**
};
#include "SpartaGameInstance.h"

**USpartaGameInstance::USpartaGameInstance()**
**{**
		**TotalScore = 0;**
		**CurrentLevelIndex = 0;**
**}**

**void USpartaGameInstance::AddToScore(int32 Amount)**
**{**
		**TotalScore += Amount;**
		**UE_LOG(LogTemp, Warning, TEXT("Total Score Updated: %d"), TotalScore);**
**}**

3️⃣ 전체 게임 루프 요약

  1. 게임 실행
  2. BeginPlay()
  3. 플레이어가 코인 획득
  4. 레벨 종료
  5. 다음 맵 로드
  6. Game Over </aside>

<aside>

</aside>


Copyright ⓒ TeamSparta All rights reserved.