카테고리 없음

8/4 TIL

blog13869 2026. 8. 4. 20:24

[ 강의 1 - 6 NetRole ]

 

< 매크로를 통한 NetRole 출력 >

더보기
  •  새 C++ 클래스 > Pawn 부모 클래스 > “CXPawn”
  •  Path: `Player`
  • `BP_GameModeBase > Details > Default Pawn Class`에 `CXPawn` 지정

 

< CFM.h >

 

// 현재 할당돼있는 NetRole을 출력해서 확인하게 해주는 함수
static FString GetRoleString(const AActor* InActor)
{
    FString RoleString = TEXT("None");

    if (IsValid(InActor) == true)
    {
       FString LocalRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetLocalRole());
       FString RemoteRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetRemoteRole());
       
       RoleString = FString::Printf(TEXT("%s / %s"), *LocalRoleString, *RemoteRoleString);
    }

    return RoleString;
}

 

< CFMPawn.h >

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "CFMPawn.generated.h"

UCLASS()
class CFM_API ACFMPawn : public APawn
{
    GENERATED_BODY()
    
protected:
    
    // 게임이 시작됐을 때 바로 호출
    virtual void BeginPlay() override;

    //빙의됐을 때 호출
    virtual void PossessedBy(AController* NewController) override;

};

< CFMPawn.cpp >

#include "CFMPawn.h"
#include "CFM.h"


void ACFMPawn::BeginPlay()
{
    Super::BeginPlay();
    
    // 시작되자마자 현재 적용돼있는 NetRole 출력
    FString NetRoleString = CFMFunctionLibrary::GetRoleString(this);
    FString CombinedString = FString::Printf(TEXT("CXPawn::BeginPlay() %s [%s]"), *CFMFunctionLibrary::GetNetModeString(this), *NetRoleString);
    CFMFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}

void ACFMPawn::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);
    
    // 게임 시작 한 뒤 컨트롤러에 Pawn이 할당됐을 때 NetRole 출력
    FString NetRoleString = CFMFunctionLibrary::GetRoleString(this);
    FString CombinedString = FString::Printf(TEXT("CXPawn::PossessedBy() %s [%s]"), *CFMFunctionLibrary::GetNetModeString(this), *NetRoleString);
    CFMFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}

 

BP_GameModeBase > Details > Default Pawn Class에 CXPawn을 지정

 

< 실행 시 나오는 로그 확인 >

 

  • 시작 시 Simulated Proxy : 피동기화 상태 ( SetOwner 가 아직 X )
  • PlayerController할당 : Autonomous Proxy : 서버로부터 데이터를 수신 받아서 동기화도 되면서, 서버로 송신도 가능

< NetRole을 활용하여 정의된 중요 함수들 >

더보기

< AActor::HasAuthority() >

  • 입력 관련 로직이나 UI는 Autonomous Proxy에서 수행
  • AController::IsLocalController() 와 APawn::IsLocallyControlled() 함수가 제공

 

 

< 각 액터에 따른 특징 >

더보기
  • GameMode 는 HasAuthority() 함수를 호출할 필요 X
  • Pawn 은 Authority, Autonomous Proxy, Simulated Proxy가 혼재
  • UI 관련 로직은 클라이언트에서만 수행

< 로컬 롤과 리모트 롤로 분류한 이유 >

더보기
  • 만약 로컬 롤만 있었다면 분간하기가 어려웠을 케이스들이 존재
    서버에 접속한 플레이어의 캐릭터 A와 서버에서 스폰 된 캐릭터 B가 있다고 가정한다면
    A와 B의 로컬 롤은 *Authority*

 

  • 로컬 롤만으로는 서버에 접속한 플레이어의 캐릭터인지, 서버에서 스폰된건지 구분이 불가능
    여기서 만약 리모트 롤까지 알게된다면, 서버에 접속한 플레이어의 캐릭터는 **Autonomus Proxy**이고,
    서버에서 스폰된 캐릭터는 `None`이므로 구분이 가능

 

  • 구분이 가능해야 서버에서 호출되고 오너 클라이언트에서 실행될 RPC를 구현이 가능
    멀티플레이에서 각 액터들을 좀 더 정확하게 분류하게 위해 둘로 분류

 

[ 강의 2-1. Remote Procedure Call 기초 ]

 

< RPC(Remote Procedure Call) >

더보기
  • 호출하는 PC와 실행하는 PC가 달라도 되게끔 해주는 통신 기법
  • 게임에 큰 영향을 끼치지 않는 일시적인 효과들에 사용하는 편
  • 주로 코스메틱(사운드, 파티클 등)에 사용

 

< Call과 Invoke >

더보기

< Call >

  • 정적인 의미
  • 컴파일 타임에 어떤 함수인지, 호출하는 곳과 실행하는 곳이 결정
  • 지금까지 정의했던 일반적인 전역 함수와 멤버 함수가 이에 해당
  • 직접적으로 함수를 호출하고 실행 (Direct)

< Invoke >

  • 동적인 의미
  • 런타임에 어떤 함수인지, 호출하는 곳과 실행하는 곳이 결정
  • 함수 포인터, 동적 바인딩, RPC 같은 개념들이 이에 해당
  • 간접적으로 함수를 호출하고 실행 (Indirect)

RPC도 마찬가지로 "RPC를 Invoke"라고 표현

< Actor Ownership >

더보기

트워크 멀티플레이가 적용되려면, 액터는 서버에서 스폰되고 `bReplicates`가 `true`여야 가능
서버에서 스폰된 후 `SetOwner(PlayerController)` 함수를 호출해야 Client-Owned Actor가 할당

  • Client-Owned Actor : PlayerController == Local PlayerController 
      ex) 내 플레이어 캐릭터
  • Owned by different client : PlayerController !== Local PlayerController
      ex) 내 화면에 보이는 친구의 플레이어 캐릭터
  • Server-Owned Actor :서버에서 스폰되었지만 SetOwner() 함수 호출 X
      ex) 보물상자 액터
  • Unowned Actor == Server-Owned Actor

< NetMulticast, Server, Client 키워드 >

더보기
  • `UFUNCTION()` 매크로와 함께 사용되는 키워드
  • 키워드 : 해당 원격 PC에서 RPC를 실행시켜 달라는 요청 
  • 실제로 실행될지는 표를 통해 판단
  • NetMulticast : 서버를 포함한 모든 클라이언트에서 해당 RPC를 실행을 요청
  • Server : 서버에서 해당 RPC를 실행을 요청
  • Client : 클라이언트에서 해당 RPC를 실행을 요청
NetMulticast RPC
Server RPC

 

Client RPC

 

 

<자주 쓰이는 케이스 >

더보기

< Server 키워드 >

  • RPC가 클라이언트에서 호출되고 서버에서 실행되어야 하는 경우
  • ClientConnection이 소유하고 있는 액터(Client-Owned)에서 RPC가 호출
  • _Validate() 함수에서 RPC를 실행할지 말지 결정

 

< NetMulticast 키워드 >

  • RPC가 서버와 모든 클라이언트에서 실행되어야 하는 경우
  • 서버에서 호출
  • 부하가 심하므로, 빈번하게 호출되게끔 코드를 작성하는 건 권장 X
      ex) Tick() 함수 내에서 NetMulticast RPC 호출

 

< Client 키워드 >

  • RPC가 서버에서 호출되고 클라이언트에서 실행되어야 하는 경우
  • 서버에서 호출

< WithValidation >

더보기
  • UFUNCTION() 매크로와 함께 사용되는 키워드
  • 서버에서 실행되는 RPC의 경우에 해당 키워드를 작성하는 것이 권장
  • 이 키워드가 붙은 RPC를 구현 할때는 _Implementation() 함수와 _Validate() 함수로 분류
  • _Validate() 함수는 해당 RPC가 서버에서 실제로 실행을 결정
  • 서버 실행 로직은 무조건적으로 신뢰되기 때문에, 위변조를 막기 위한 방어막 역할

< Unreliable VS Reliable >

더보기
  • UFUNCTION() 매크로와 함께 사용되는 키워드
  • RPC는 기본적으로 **Unreliable** 키워드를 기준으로 동작
     아무것도 안쓰면 Unreliable
  • Unreliable은 원격 PC에서 무조건 실행되리라는 보장 X
  • 반면, 원격 PC에서 무조건 실행되어야 하는 로직이라면 Reliable 키워드를 작성
  • Unreliable 의 활용 예시 : 코스메틱(이펙트, 사운드 등)처럼 게임에 큰 영향이 없는 로직
  • Reliable 의 활용 예시 : 충돌, 데미지, 스폰처럼 게임에 큰 영향을 끼치는 로직

 

 

[ 강의 2 - 2 : 멀티플레이 채팅 구현 ]

 

< 내가 작성한 채팅 메세지가 다른 PC의 화면에도 나오게 하는 방법 >

더보기

< 데디케이티드 서버 구조 >

  • 클라이언트와 클라이언트 간의 통신은 불가능
  • 서버와 클라이언트 간의 통신만 가능
  • 내가 작성한 채팅 메시지는 일단 서버로 전송
  • 서버는 전송 받은 메세지를 다시 클라이언트들에게 전송

< 멀티플레이 채팅 구현 >

더보기

< CFMPlayerController.h >

//서버에서 출력한 채팅을 클라이언트에서 받는 함수 (Client RPC) , 채팅메세지가 날아가지 않게 끔 Reliable 키워드 추가
UFUNCTION(Client, Reliable)
void ClientRPCPrintChatMessageString(const FString& InChatMessageString);

// 채팅을 서버로 보내기 위한 함수 (Server RPC), 채팅메세지가 날아가지 않게 끔 Reliable 키워드 추가
UFUNCTION(Server, Reliable)
void ServerRPCPrintChatMessageString(const FString& InChatMessageString);

 

< CFMPlayerController.cpp >

#include "EngineUtils.h" // TActorIterator 사용 용도
void ACFMPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
    ChatMessageString = InChatMessageString;
    //PrintChatMessageString(ChatMessageString);
    // 기존 PrintChatMessageString 주석 처리 후 Controller가 할당 돼있다면 Server에 채팅 보내는 함수로 변경
    if (IsLocalController() == true)
    {
       ServerRPCPrintChatMessageString(InChatMessageString);     
    }
}

 

// RPC를 만들 때  Implementation 을 붙히는 게 국룰 문법
void ACFMPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
    // 서버에서 플레이어 컨트롤러를 확인
    for (TActorIterator<ACFMPlayerController> It(GetWorld()); It; ++It)
    { 
       // 확인한 플레이어 컨트롤러들이 유효하면 실행
       ACFMPlayerController* CXPlayerController = *It;
       if (IsValid(CXPlayerController) == true)
       {
          // 해당 플레이어컨트롤러에 ClientRPCPrintChatMessageString함수를 통해 해당 PC에서 채팅 출력
          CXPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
       }
    }
}

 

void ACFMPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
    // 출력 하는 함수만 추가 (ServerRPC 함수 쪽에서 호출용)
    PrintChatMessageString(InChatMessageString); 
}

< Client RPC 대신에 NetMulticast RPC를 사용한다면? >

더보기
  • 작동 X

  • PlayerController는 내 PC와 서버 PC에만 존재하기 때문에 NetMulticast RPC로 구현하면 내 PC와 서버 PC에만 채팅이 출력

< NetMulticast RPC를 통한 서버 접속 알리기 >

더보기
  •  새 C++ 클래스 > GameStateBase 부모 클래스 > “CXGameStateBase”

  •  Path: `Game`
     
  • Content Browser > Content > ChatX > Blueprint > Game
    새 Blueprint 클래스 > CXGameStateBase 부모 클래스 > “BP_GameStateBase”

  • BP_GameModeBase > Details > Game State Class에 BP_GameStateBase 지정

 

 

 

< CFMGameStateBase.h >

public:
    
    // MulticastRPC 를 사용해서 누군가 접속(로그인) 했을 때 알려주는(TEXT호출) 함수
    UFUNCTION(NetMulticast, Reliable)
    void MulticastRPCBroadcastLoginMessage(const FString& InNameString = FString(TEXT("XXXXXXX")));
    

 

< CFMGameStateBase.cpp >

#include "Game/CFMGameStateBase.h"
#include "Kismet/GameplayStatics.h"
#include "Player/CFMPlayerController.h"



void ACFMGameStateBase::MulticastRPCBroadcastLoginMessage_Implementation(const FString& InNameString)
{
    // HasAuthority() == false = 서버가 아니라면 = 클라이언트라면
    if (HasAuthority() == false)
    {
       //  0번 컨트롤러 (LocalPlayerController)를 가져오고
       APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
       if (IsValid(PC) == true)
       {
          // 로컬플레이어가 유요 하다면 NotificationString 으로 게임에 접속했다는 문구 출력
          ACFMPlayerController* CFMPC = Cast<ACFMPlayerController>(PC);
          if (IsValid(CFMPC) == true)
          {
             FString NotificationString = InNameString + TEXT(" has joined the game.");
             CFMPC->PrintChatMessageString(NotificationString);
          }
       }
    

 

< CFMGameStateBase.h >

public:

// 다른 플레이어 (NewPlayer)가 로그인 할 때 실행되는 함수
virtual void OnPostLogin(AController* NewPlayer) override;

 

< CFMGameStateBase.h >

#include "Game/CFMGameModeBase.h"
#include "Game/CFMGameStateBase.h"



void ACFMGameModeBase::OnPostLogin(AController* NewPlayer)
{
    // 누군가 접속 할 때 마다
    Super::OnPostLogin(NewPlayer);

    // 접속한 NewPlayer의 GameStateBase를 가져오고 
    ACFMGameStateBase* CFMGameStateBase =  GetGameState<ACFMGameStateBase>();
    // GameStateBase 가 유효하다면 MulticastRPC 로그인 메세지 출력 함수 호출
    if (IsValid(CFMGameStateBase) == true)
    {
       CFMGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
    }
}