안녕하세요! 지난 포스팅에서는 다음과 같은 내용을 다뤘습니다.
AADTutorialGameMode: 튜토리얼의 흐름과 규칙을 제어하는 '두뇌'
ATutorialManager: 튜토리얼 UI를 표시하는 '얼굴'
여기서 한 가지 의문이 생깁니다. '두뇌'인 GameMode와 '얼굴'인 TutorialManager는 어떻게 서로를 직접 알지 못하는 상태에서 소통할 수 있을까요?
이번 포스팅에서는 이 둘을 포함한 여러 시스템을 느슨하게 연결해 주는 '접착제'이자, 튜토리얼의 현재 상태를 모든 플레이어에게 공유하는 '중앙 게시판' 역할을 하는 AADTutorialGameState에 대해 알아보겠습니다.
핵심 설계: 상태 저장과 전파
AADTutorialGameState의 역할은 단순하지만 매우 중요합니다.
상태 저장(State Storage): 튜토리얼의 현재 단계(CurrentPhase)를 변수로 저장합니다. GameState는 GameMode와 달리 서버에서 생성되어 모든 클라이언트에 복제(Replication)되므로, 모든 플레이어가 동일한 게임 상태를 공유할 수 있습니다.
상태 전파(State Propagation): 저장된 상태가 변경되었을 때, 이 변경 사항을 자신에게 관심 있는 다른 모든 액터에게 "상태가 바뀌었어!" 라고 알려줍니다. 이 알림 기능은 RepNotify와 Delegate를 조합하여 매우 효율적으로 구현됩니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "Tutorial/TutorialEnums.h"
#include "ADTutorialGameState.generated.h"
// ETutorialPhase를 파라미터로 받는 멀티캐스트 델리게이트 선언
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPhaseChangedDelegate, ETutorialPhase);
UCLASS()
class ABYSSDIVERUNDERWORLD_API AADTutorialGameState : public AGameState
{
GENERATED_BODY()
public:
AADTutorialGameState();
// "상태가 바뀌었음" 이벤트를 방송할 델리게이트 인스턴스
FOnPhaseChangedDelegate OnPhaseChanged;
FORCEINLINE ETutorialPhase GetCurrentPhase() const { return CurrentPhase; }
// 서버에서만 호출하여 상태를 변경하는 함수
void SetCurrentPhase(ETutorialPhase NewPhase);
// 복제될 튜토리얼 단계 변수. OnRep_PhaseChanged 함수와 연결됨
UPROPERTY(ReplicatedUsing = OnRep_PhaseChanged)
ETutorialPhase CurrentPhase;
protected:
// CurrentPhase 변수가 클라이언트에 복제 완료될 때 호출될 함수
UFUNCTION()
void OnRep_PhaseChanged();
// 어떤 변수를 복제할지 엔진에 알려주는 함수
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
헤더 파일의 핵심은 CurrentPhase 변수와 OnPhaseChanged 델리게이트입니다.
UPROPERTY(ReplicatedUsing = OnRep_PhaseChanged): CurrentPhase 변수가 네트워크를 통해 복제될 때, OnRep_PhaseChanged라는 함수를 자동으로 호출하도록 지정합니다. 이것이 RepNotify 기능입니다.
DECLARE_MULTICAST_DELEGATE_OneParam: OnPhaseChanged라는 이름의 커스텀 이벤트를 정의합니다. 여러 구독자가 이 이벤트를 함께 수신할 수 있습니다.
// 복제할 변수 등록
void AADTutorialGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// CurrentPhase 변수를 네트워크 복제 대상으로 지정
DOREPLIFETIME(AADTutorialGameState, CurrentPhase);
}
// 서버에서 상태를 변경하는 함수
void AADTutorialGameState::SetCurrentPhase(ETutorialPhase NewPhase)
{
// 이 코드는 서버(Authority)에서만 실행되어야 함
if (HasAuthority())
{
if (CurrentPhase != NewPhase)
{
CurrentPhase = NewPhase;
// 서버에서도 RepNotify 함수를 직접 호출하여 로컬 변경 사항을 전파
OnRep_PhaseChanged();
}
}
}
// RepNotify 함수: 상태 변경을 최종적으로 전파
void AADTutorialGameState::OnRep_PhaseChanged()
{
// OnPhaseChanged 델리게이트에 등록된 모든 함수들에게
// 변경된 CurrentPhase 값을 담아 방송(Broadcast)
OnPhaseChanged.Broadcast(CurrentPhase);
}
소스 코드의 동작 흐름은 다음과 같습니다.
SetCurrentPhase (서버에서만 실행): GameMode가 튜토리얼 단계를 바꾸려고 이 함수를 호출합니다. 서버는 CurrentPhase 값을 변경하고, 서버 자신을 위해 OnRep_PhaseChanged 함수를 수동으로 호출합니다.
네트워크 복제: 언리얼 엔진은 서버의 CurrentPhase 값이 변경된 것을 감지하고, 이 값을 모든 클라이언트에게 전송합니다.
OnRep_PhaseChanged (클라이언트에서 자동 실행): 변수를 수신한 클라이언트에서 ReplicatedUsing에 지정된 OnRep_PhaseChanged 함수가 자동으로 실행됩니다.
Broadcast (서버와 모든 클라이언트에서 실행): OnRep_PhaseChanged 함수는 OnPhaseChanged 델리게이트를 방송합니다. 이 델리게이트를 구독하고 있던 TutorialManager의 OnTutorialPhaseChanged 함수가 마침내 실행되어 UI를 업데이트합니다.
최종 시스템 흐름 정리 ⚙️
이제 세 클래스가 어떻게 유기적으로 협력하는지 최종적으로 정리할 수 있습니다.
[GameMode - 서버]: 플레이어의 행동 등으로 다음 단계로 넘어갈 때가 되면 GameState->SetCurrentPhase(NewPhase)를 호출합니다.
[GameState - 서버]: CurrentPhase 변수 값을 바꾸고, 로컬 OnRep_PhaseChanged()를 호출해 OnPhaseChanged 델리게이트를 방송합니다.
[네트워크 엔진]: 서버의 CurrentPhase 변수 변경을 감지하고 모든 클라이언트에 새 값을 전송합니다.
[GameState - 클라이언트]: 새 값을 수신하고, 자동으로 OnRep_PhaseChanged()를 호출해 OnPhaseChanged 델리게이트를 방송합니다.
[TutorialManager - 서버 & 클라이언트]: 구독해 둔 OnPhaseChanged 델리게이트 방송을 수신합니다. 인자로 넘어온 NewPhase 값을 이용해 DataTable을 조회하고 UI를 갱신합니다.