<aside> 💡 목차
</aside>
<aside>
GameState
와 GameMode
가 있습니다.GameMode
에 직접 접근할 수 없습니다. 따라서 클라이언트도 알아야 하는 정보(예: 남은 시간, 현재 점수 등)를 GameMode
에만 두면 복잡해집니다.GameMode
에 두고, “서버-클라이언트가 공통으로 알아야 하는 상태”는 GameState
에 두는 방식을 많이 사용합니다.GameState
를 활용합니다.GameState
객체는 게임이 시작될 때 서버에서 생성되고, 클라이언트는 이를 복제 받아서 똑같은 정보를 읽을 수 있습니다. 즉, “서버와 클라이언트 모두” 동일한 정보를 가지게 됩니다.GameMode
대신 GameState
를 선택하여 전역 상태를 관리하는 방식으로 구현합니다.SpawnRandomItem()
이 스폰된 액터 포인터를 반환하도록 수정하고, 그 액터가 코인인지 확인할 수 있게 만듭니다.#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;**
}
GameStateBase
를 상속한 SpartaGameStateBase는 삭제하고 GameState
를 상속한 SpartaGameState를 생성해줍니다.SpawnedCoinCount
)하고, 플레이어가 먹은 코인 개수를 추적(CollectedCoinCount
)하여 “모두 먹었다면” 즉시 레벨 종료합니다.#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를 띄운다거나, 재시작 기능을 넣을 수도 있음
**}**
OpenLevel
) 시 주의
UGameplayStatics::OpenLevel
을 호출하면 지금 월드가 언로드 (제거) 되고, 새로운 맵이 로드되면서 BeginPlay()
가 다시 실행됩니다. 이때 GameState
도 새로 생성되기 때문에, 이전 레벨에서 유지하던 변수가 모두 초기화될 수 있습니다.BeginPlay()
: 게임 시작 시 StartLevel()
호출StartLevel()
:
SpawnedCoinCount=0
, CollectedCoinCount=0
)SpawnRandomItem()
이 ACoinItem
을 반환하면 SpawnedCoinCount
++OnLevelTimeUp
호출)OnLevelTimeUp()
: 30초가 만료되면 레벨 종료(EndLevel()
)OnCoinCollected()
: 코인 아이템을 먹을 때마다 호출.
CollectedCoinCount++
CollectedCoinCount >= SpawnedCoinCount
이면, 즉시 EndLevel()
EndLevel()
CurrentLevelIndex++
CurrentLevelIndex >= MaxLevels
이면 OnGameOver()
StartLevel()
OnGameOver()
RestartGame
등 처리(블루프린트나 GameMode에서 구현)CoinItem
은 플레이어가 닿았을 때 (ActivateItem
) 점수를 획득하고 자기 자신을 제거하는 구조입니다. 여기서 추가로 “코인을 하나 더 먹었다”고 GameState
에게 알려야 합니다#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>
GameState
, GameMode
같은 기본 클래스를 비롯해, 맵 내에서 생성된 대부분의 객체가 처음부터 다시 생성됩니다.GameState
/PlayerController
등을 파괴하지 않고 그대로 다음 맵으로 넘어가는 기능입니다.GameInstance
를 사용하기가 쉽습니다.#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);**
**}**
GameInstance
생성, GameMode
/GameState
생성, 첫 레벨 로드ASpartaGameState::BeginPlay()
→ StartLevel()
SpawnVolume
)에서 40개 아이템 스폰SpawnedCoinCount
)CoinItem::ActivateItem()
에서 GameState->AddScore()
, OnCoinCollected()
EndLevel()
EndLevel()
에서 CurrentLevelIndex++
UGameplayStatics::OpenLevel(...)
로 다음 맵 로드OnGameOver()
<aside>
</aside>
Copyright ⓒ TeamSparta All rights reserved.