TIL_067 : 레플리케이션, 숫자 야구 게임 개발

김펭귄·2025년 11월 21일

Today What I Learned (TIL)

목록 보기
67/142

오늘 학습 키워드

  • 레플리케이션

  • 숫자 야구 게임

1. 레플리케이션

  • 서버에서 액터를 스폰하고 클라로 복제할 때, 현재 서버에서의 값들을 클라에 보내주어 클라에서 이 값들을 초기값으로 인스턴스를 다시 만드는 것이었음

  • 그러므로 이 상태까지는 서버와 클라에서의 액터의 메모리 구조랑 값은 완전히 동일

  • 하지만, 멤버변수같이 값이 이후에 바뀌게 되면 서로 달라짐

  • 이걸 동기화 시켜주는 방법이 Replication

  • 동기화는 무조건 서버에서 클라로. 클라가 값 바꾸고 서버를 동기화시키는건 절대 안 됨

1.1. 하는 방법

  1. 액터 레플리케이션 활성화
    생성자에서 bReplicatestrue로 설정하여 활성화

  2. 동기화할 멤버변수에 키워드 넣기
    UPROPERTY(Replicated) 사용하여 동기화 설정

  3. 동기화 함수 구현
    GetLifetimeReplicatedProps() 함수에 동기화할 멤버변수를 명시

// MyActor.h
UCLASS()
class MYGAME_API AMyActor : public AActor
{
    GENERATED_BODY()

public:
    AMyActor();

    // 레플리케이션 대상 변수 선언
    UPROPERTY(Replicated)
    int32 ReplicatedValue;

	// 동기화 함수
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>&
    										OutLifetimeProps) const override;
};
// MyActor.cpp
#include "MyActor.h"
#include "Net/UnrealNetwork.h"

AMyActor::AMyActor()
{
    bReplicates = true; // 액터의 동기화 설정
}

void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>&
										  OutLifetimeProps) const {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // ReplicatedValue 변수를 클라이언트로 동기화 하도록 지정
    DOREPLIFETIME(AMyActor, ReplicatedValue);
}
  1. 만약 클라이언트에서 동기화되어 값이 변경되었을 때, 특정 동작하고 싶으면 ReplicatedUsing=함수이름 키워드 사용하기
// MyActor.h
UPROPERTY(Replicated, ReplicatedUsing=OnRep_ReplicatedValue)
int32 ReplicatedValue;

UFUNCTION()
void OnRep_ReplicatedValue() { /* 추가 동작 */ }
  • 꼭 멤버변수만이 아니라, 컴포넌트, 서브 오브젝트 등 다른 것들도 레플리케이션 될 수 있다

2. 숫자 야구 게임

  • 지금까지 멀티플레이어(데디케이티드 서버)를 공부한 것을 기반으로, 여러 명이서 동시에 플레이 할 수 있는 숫자야구게임을 만들어보려고 함

  • 실시간 채팅과 동기화에 신경써보며 만들어볼 예정

2.1. 랜덤 숫자 만들기

void ACXGameModeBase::BeginPlay()
{
	Super::BeginPlay();

	FMath::RandInit(FDateTime::Now().GetTicks());
	SecretNumberString = GenerateSecreteNumber();
}

FString ACXGameModeBase::GenerateSecreteNumber()
{
	// TArray<int> Numbers 초기화 {1, 2, ... 9} //
    
	FString Result;
	for (int32 i = 0; i < 3; i++)
	{
		int32 Index = FMath::RandRange(0, Numbers.Num() - 1);
		Result.Append(FString::FromInt(Numbers[Index]));
		Numbers.RemoveAt(Index);
	}

	return FString();
}
  • FMath::RandInit(FDateTime::Now().GetTicks())로 시드값 초기화

  • 시드값을 초기화 안 해주면 매번 같은 시퀀스로 난수가 나옴

  • 한번만 초기화해주고, RandRange통해 난수 받으면 된다

2.2. 새 플레이어 접속 처리

// CXGameModeBase.h

class ACXPlayerController;

UCLASS()
class CHATX_API ACXGameModeBase : public AGameModeBase
{
	// ... //
protected:
	TArray<TObjectPtr<ACXPlayerController>> AllPlayerControllers;
};

// CXGameModeBase.cpp
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
    
	ACXPlayerController* NewCXPlayerController =Cast<ACXPlayerController>(NewPlayer);
        
	if (IsValid(NewCXPlayerController))
		AllPlayerControllers.Add(NewCXPlayerController);
}
  • OnPostLogin은 새 클라이언트가 접속하여 로그인 된 후에 호출되는 함수
    그래서 이 함수에서 새 플레이어 컨트롤러를 컨트롤러 관리 배열에 추가

2.3. 플레이어 입력 처리

// CXPlayerController.h
UFUNCTION(Server, Reliable)
void ServerRPCPrintChatMessageString(const FString& InChatMessageString);

// CXPlayerController.cpp
void ACXPlayerController::ServerRPCPrintChatMessageString_Implementation(
							const FString& InChatMessageString)
{
	ACXGameModeBase* CXGameModeBase = GetWorld()->GetAuthGameMode<ACXGameModeBase>();
	CXGameModeBase->PrintChatMessageString(this, InChatMessageString);
}
  • ServerRPCPrintChatMessageString() 이 함수는 Server RPC함수로, 클라에서 채팅쳐서 이 함수 호출하면, 서버에 있는 해당 컨트롤러의 이 함수가 실행됨

  • 서버에는 게임모드 있으니 GetWorld()->GetAuthGameMode<ACXGameModeBase>(); 이 함수로 게임모드 가져와 각 클라이언트 화면에 채팅의 정답을 보여주는 함수 호출

  • 헷갈렸는데, 서버에서 실행하는 프로그램과 클라에서 실행하는 프로그램은 동일함. 그래서 클라에도 게임모드 파일은 있는거지 World상에서 생성만 안 되는 것

2.4. 플레이어 입력 정답 처리

  • 정답 판정 로직은 서버에서 동작해야하므로, 게임모드에 구현

  • 숫자입력하면 정답을, 그냥 채팅이면 채팅을 화면에 띄움

// CXGameModeBase.cpp

void ACXGameModeBase::PrintChatMessageString(
	ACXPlayerController* InChattingPlayerController,
    const FString& InChatMessageString)
{
	FString Message = InChatMessageString;
	if (IsGuessNumberString(InChatMessageString))	// 숫자입력한거면
	{
		FString Result = JudgeResult(SecretNumberString, InChatMessageString);
		Message.Append(" -> ").Append(Result);
	}

	// 모든 클라이언트에게 Clinet RPC 보냄
	for (const TObjectPtr<ACXPlayerController> CXPlayerController :
    	 AllPlayerControllers)
	{
		CXPlayerController->ClientRPCPrintChatMessageString(Message);
	}
}
  • Client RPC인 ClientRPCPrintChatMessageString를 통해 각 클라에게 전달

2.5. 결과 영상

  • 정답이 984일때

3. 숫자야구게임(2)

3.1. 클라별 이름 설정

  • 플레이어별 이름은 플레이어 스테이트에 저장

  • 값 설정은 서버에서 해줘야함. 그리고 클라로 동기화 작업 필요(레플리케이션)

// CXPlayerState.cpp

#include "Player/CXPlayerState.h"
#include "Net/UnrealNetwork.h"

void ACXPlayerState::BeginPlay()
{
	Super::BeginPlay();

	bReplicates = true;		// 레플리케이션 활성화
}

void ACXPlayerState::GetLifetimeReplicatedProps(
					TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	// PlayerNameString 변수를 등록
	DOREPLIFETIME(ACXPlayerState, PlayerNameString);
}

3.2. 서버에서 클라 이름 설정해주기

  • 새 클라가 서버에 접속한 이후에 해주면 되므로 GameModeBase::PostLogin()에서 해줌
// CXGameModeBase.cpp

void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);

	ACXPlayerController* NewCXPlayerController =Cast<ACXPlayerController>(NewPlayer);
	if (IsValid(NewCXPlayerController))
	{
    	// 모든 클라의 플레이어컨트롤러 배열
		AllPlayerControllers.Add(NewCXPlayerController);

		ACXPlayerState* CXPlayerState = NewCXPlayerController->
        									GetPlayerState<ACXPlayerState>();
                                            
		if (IsValid(CXPlayerState))
		{
			CXPlayerState->PlayerNameString = TEXT("Player") +
            			FString::FromInt(AllPlayerControllers.Num());
		}
}
  • 이렇게 하면, 서버에 접속한 클라이언트에게 "Player숫자"형식으로 서버와 클라이언트의 PlayerState에 이름이 설정되게 된다

  • 지금 만든 PlayerState는 꼭 GameMode에 등록해주기

profile
반갑습니다

0개의 댓글