게임모드로는 이전에 공부했듯이, GameModeBase와 GameMode 2개가 존재
GameModeBase는 게임이 돌아가기 위한 정말 기초적인 규칙들만 존재
GameMode는 매칭 시스템, 관전자 등 추가적인 기능들을 제공
EMatchState같은 StateMachine으로 매치 상태에 따라 따로 구현할 필요 없이 편하게 사용가능하도록 기능 제공enum class EMatchState {
EnteringMap, // 맵 진입 중
WaitingToStart, // 시작 전 로비 단계
InProgress, // 실제 진행 중
WaitingPostMatch, // 종료 후 여운 (리플레이, 결과 등)
LeavingMap // 맵 전환 중
};
리스폰 기능을 통해 DefaultPawnClass를 다시 생성하고 컨트롤러에 연결해주는 기능도 제공
GameModeBase는 이런 기능들 직접 구현해야함
GameModeBase와 GameStateBase로 사용해야하고, GameMode와 GameState를 같이 사용해야함
게임을 관리하는 유일한 관리자이므로 서버에 1개만 존재하고, 클라이언트에는 존재하지 않음
결정하는 곳이므로, 데이터를 저장하지 않음. 결정로직만 구현되어 있고 결정하면서 생긴 데이터는 GameState에 저장
void AMyGameMode::StartMatch()
{
// 1. GameMode는 "결정"을 내리고
bMatchInProgress = true;
// 2. GameState에게 "정보 업데이트"를 위임
if (AMyGameState* GS = GetGameState<AMyGameState>())
{
GS->NotifyMatchStarted(MatchDuration);
}
// ... //
}
void AMyGameMode::EndMatch(bool bPlayerWon)
{
bMatchInProgress = false;
// GameState에 결과 통보
if (AMyGameState* GS = GetGameState<AMyGameState>())
{
GS->NotifyMatchEnded(bPlayerWon);
}
// 다음 단계로 진행 (예: GameInstance로 레벨 전환 요청)
if (bPlayerWon)
{
if (UGameInstance* GI = GetGameInstance())
{
UE_LOG(LogTemp, Log, TEXT("GameInstance로 다음 레벨 요청"));
}
}
}
GameMode에서의 데이터를 기록하는 전광판 역할
서버에 한 개 존재하여 게임 데이터를 관리하고, 각 클라이언트로 복제되어 읽기 접근 허용. 모든 클라이언트가 같은 정보를 볼 수 있도록 공유함
그래서 UI에서는 GameMode가 아니라 GameState에서 정보를 가져와야하고, 클라이언트에서는 GameState를 수정할 수 없고 서버에서만 수정/관리해야함
UCLASS()
class AMyGameState : public AGameStateBase
{
GENERATED_BODY()
public:
// 콜백 등록
UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_RemainingTime)
float RemainingTime = 0.0f;
// GameMode에서 동작 위임한 함수
UFUNCTION()
void NotifyMatchStarted(float Duration);
protected:
// 서버에서 RemainingTime이 갱신될 때 클라에서 자동 호출됨
UFUNCTION()
void OnRep_RemainingTime();
private:
float InitialDuration = 0.0f;
FTimerHandle CountdownHandle;
};
ReplicatedUsing=함수이름해당 변수가 네트워크에서 복제(Replication)될 때, 즉 서버에서 값이 바뀌어서 클라이언트로 값이 전달되는 순간 해당 함수를 클라이언트에서 자동으로 호출
서버에서 로직을 처리하고 변경된 값이 GameState에 반영되면, 이 값을 클라이언트에서 UI 업데이트 등 후속처리를 하게 함
서버에서는 호출 안 하고, 클라이언트에서만 호출하여 UI 업데이트 등을 함
같은 객체에 존재하는 함수에만 사용가능
// 서버가 타이머를 관리
void AMyGameState::NotifyMatchStarted(float Duration)
{
if (GetLocalRole() != ROLE_Authority) return; // 서버만 실행
InitialDuration = Duration;
RemainingTime = Duration;
// 1초마다 RemainingTime 감소시키고 자동 복제
GetWorldTimerManager().SetTimer(
CountdownHandle,
[this]()
{
// 모든 클라이언트도 동일한 data 받음
RemainingTime = FMath::Max(RemainingTime - 1.f, 0.f);
if (RemainingTime <= 0.f)
GetWorldTimerManager().ClearTimer(CountdownHandle);
},
1.0f, true
);
}
// Remaining Time 수정될 때마다 자동 콜백됨
void AMyGameState::OnRep_RemainingTime()
{
// UI 업데이트 신호를 던질 수도 있음
OnTimeUpdated.Broadcast(RemainingTime);
}
플레이어 개개인의 정보를 가지는 객체(닉네임, id, 랭킹..)
레벨 전환될 때마다 사라지고 생기는 GameMode, GameState와 달리 이 객체는 계속 유지되어 사라지지 않음
GameMode (서버만 존재)
↓
PlayerController (각 유저마다 1개, 서버+클라 공존)
↓
PlayerState (서버+모든 클라 복제)
게임모드가 컨르롤러를, 컨트롤러가 PlayerState를 소유
따라서 PlayerState를 가져오려면 지금 내 플레이어컨트롤러가 소유하고 있는 PlayerState를 가져오라고 해야함. Pawn에서 가져오는 것이 아니라.
Pawn은 컨트롤러의 입력에 따라 동작하는 단순한 마네킹일 뿐이다
// 현재 컨트롤러에서 가져오는 함수
AMyPlayerState* PS = GetPlayerState<AMyPlayerState>();
얘도 마찬가지로, 서버의 GameState에서 모든 플레이어의 원본 PlayerState 인스턴스를 가지고 관리함
따라서 서버에서 관리하고 수정해야하지, 클라이언트에서 하면 적용도 안 되고 하면 안 됨
게임 모드에서 접근해서 PlayerState를 수정해야하는 것이 올바른 방법
// ❌ 잘못된 접근 (클라에서 직접 수정)
AMyPlayerState* PS = GetWorld()->GetGameState()->PlayerArray[0];
// ✅ 올바른 방식 (클라이언트에서 서버에게 요청하기)
MyPlayerState->Server_AddScore(50);
// MyPlayerState.h
UCLASS()
class AMyPlayerState : public APlayerState
{
GENERATED_BODY()
public:
// 값 수정되어 복제되면 콜백함수 호출하여 UI 동기화
UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_Score)
int32 CurrentScore = 0;
UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_Lives)
int32 Lives = 3;
// 안전한 데이터 변경 (서버 전용 RPC)
UFUNCTION(Server, Reliable)
void Server_AddScore(int32 Points);
UFUNCTION(Server, Reliable)
void Server_TakeDamage(int32 Amount);
UFUNCTION(BlueprintPure)
bool IsGameOver() const { return Lives <= 0; }
// UI 업데이트용 Delegate
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnScoreChanged,
int32, Old,
int32, New);
UPROPERTY(BlueprintAssignable)
FOnScoreChanged OnScoreChanged;
protected:
// 클라이언트에서 자동 호출되는 콜백함수
UFUNCTION()
void OnRep_Score();
UFUNCTION()
void OnRep_Lives();
private:
int32 HighScore = 0;
};
UFUNCTION(Server, Reliable) : 서버 전용 RPC
클라이언트가 직접 실행하는게 아니라, 서버에게 요청할 때만 서버에서 실행됨
delegate를 내부에 선언하면 해당 클래스의 일부 멤버처럼 취급되어 쉽게 캡슐화되며, 외부에서는 인스턴스를 통해서만 접근할 수 있다.
또한, public, private, protected로 어떻게 선언했느냐에 따라 허용 접근 범위도 달라짐
외부 선언된 델리게이트는 여러 클래스 또는 모듈 간에 공통으로 사용되는 시그니처를 정의하는 데 주로 쓰이며, 이를 통해 코드 재사용성과 일관성을 높임
// MyPlayerState.cpp
void AMyPlayerState::Server_AddScore_Implementation(int32 Points)
{
if (Points <= 0) return;
int32 Old = CurrentScore;
CurrentScore += Points;
if (CurrentScore > HighScore)
HighScore = CurrentScore;
// GameMode에 승리 조건 검사 요청
if (AMyGameMode* GM = GetWorld()->GetAuthGameMode<AMyGameMode>())
GM->CheckVictoryCondition();
}
void AMyPlayerState::OnRep_Score()
{
// 클라에서 호출하고, delegate이용하여 broadcast
OnScoreChanged.Broadcast(CurrentScore, CurrentScore);
}
void AMyPlayerState::Server_TakeDamage_Implementation(int32 Amount)
{
Lives = FMath::Max(Lives - Amount, 0);
}
void AMyPlayerState::OnRep_Lives()
{
UE_LOG(LogTemp, Verbose, TEXT("클라: 남은 목숨 %d"), Lives);
}
PlayerState는 각 플레이어의 정보를 저장하면서, 정보바뀌면 UI에게 전달하여 클라이언트 화면에 띄어주는 역할언리얼에 존재하는 모든 시스템 위에 존재하는 중간에 사라지지 않는 관리자
GameInstance는 게임이 종료될때까지 유지되어야하는 데이터를 관리하고, GameMode는 각 레벨별로 게임 흐름을 관리
레벨 전환, 세션 관리, 총합 점수 등을 관리
UCLASS()
class UMyGameInstance : public UGameInstance
{
public:
virtual void Init() override;
// 영속적 데이터 (레벨 바뀌어도 절대 안 사라짐)
UPROPERTY(BlueprintReadWrite)
FString PlayerName = TEXT("Player");
UPROPERTY(BlueprintReadWrite)
int32 TotalScore = 0; // 전체 누적 점수
// 레벨 전환 중앙 관리
UFUNCTION(BlueprintCallable)
void LoadGameLevel(int32 LevelIndex);
};
void UMyGameInstance::LoadGameLevel(int32 LevelIndex)
{
if (bIsChangingLevel) return; // 중복 방지
bIsChangingLevel = true;
CurrentLevelIndex = LevelIndex;
// 레벨 전환 전 백업하기
if (APlayerController* PC = GetWorld()->GetFirstPlayerController()) {
if (AMyPlayerState* PS = PC->GetPlayerState<AMyPlayerState>())
TotalScore += PS->GetCurrentScore();
}
// 실제 레벨 로딩
FString LevelName = FString::Printf(TEXT("Level_%d"), LevelIndex);
UGameplayStatics::OpenLevel(GetWorld(), FName(*LevelName));
}
🎮 엔진 부팅 단계
1. UEngine 초기화
2. UGameInstance 생성 → Init() 호출 ⭐ 가장 먼저!
- 이 시점: World 없음 / GameMode 없음
- 가능: 글로벌 설정, 서브시스템 초기화, SaveGame 로드
- 불가: GetWorld(), UI 생성, GameMode 접근
🌍 첫 레벨 로딩 단계(BeginPlay 아님!)
3. World 생성 (Persistent Level 포함)
4. GameModeBase 생성 → InitGame() 호출
5. GameStateBase 생성 (GameMode 내부에서 스폰)
6. PlayerController 생성
7. PlayerState 생성 (Controller 소유)
8. Pawn 생성 → Controller 가 Possess()
9. 모든 BeginPlay() 호출 (순서 보장 ❌)
- 대상: GameMode, GameState, PlayerController,
PlayerState, Pawn, 기타 모든 Actor
- ⚠ 의존성 있는 접근은 주의!
=== 현재 레벨 정리 단계 ===
1. 모든 Actor의 EndPlay() 호출
2. GameMode / GameState 파괴
3. 일반 Actor 파괴
4. PlayerState 데이터 보존 ⭐ (실제 복사 이주)
=== 새 레벨 로딩 단계 ===
5. 새 World 생성
6. PlayerState 데이터가 새 인스턴스로 복사 ⭐ (CopyProperties)
7. 새 GameMode / GameState 생성
8. PlayerController가 새 PlayerState에 재연결
9. 새 Pawn 생성
10. 모든 BeginPlay() 호출
PlayerState는 새 인스턴스에 복사되는거라 계속 유지된다고 표현하는 것. 완전히 같은 객체는 아니었음
OpenLevel로 새 레벨을 여는 도중에, GameInstance를 제외한 나머지 것들은 새로 생기는 과정이므로, BeginPlay()전까지는 접근하지 않는 것이 좋음. GameInstance에만 접근하기
GameInstance::Init()단계
// ✅ 가능
- 글로벌 설정 로드
- 서브시스템 초기화
- SaveGame 데이터 로드
// ❌ 금지
- GetWorld() 호출 (아직 World 없음)
- GameMode / PlayerController 접근
- UI 생성
GameMode 생성자 단계
// ✅ 가능
- 기본 클래스 설정(DefaultPawn, GameState설정..)
- 게임 룰 변수 초기화
// ❌ 금지
- GameState 접근 (아직 없음)
- PlayerController 접근
GameMode::PostInitializeComponents() 단계
// ✅ 이 시점부터 GameState 접근 가능
// ⚠ PlayerController / PlayerState는 아직 안 생김
if (AMyGameState* GS = GetGameState<AMyGameState>()) {
if (GS->HasBegunPlay()) {
GS->....; // 접근 가능
}
}
BeginPlay() : 랜덤으로 실행되므로, 의존 객체에 접근 금지
PostLogin() : 모든 것이 준비된 상태로 다른 객체에 접근하는데에 문제 없음
| 위험도 | 함수 | 설명 |
|---|---|---|
| 🔴 높음 | 생성자 | 의존성 객체 아직 없음 |
| 🟠 중간 | PostInitializeComponents | GameState 정도는 존재 |
| ⚠ 주의 | BeginPlay | 호출 순서 비보장 → 의존성 접근 주의 |
| 🟢 안전 | PostLogin | PlayerController / PlayerState 모두 준비됨 |
| 🟢 안전 | StartPlay | 모든 BeginPlay 이후 완전 준비 상태 |