UE/멀티플레이

UE C++ RPC오너십, 프로퍼티 리플리케이션

김인철_ 2026. 8. 6. 19:52

 

언리얼 RPC 설계: Ownership, Reliability, Property Replication

언리얼 멀티플레이에서 RPC(Remote Procedure Call)는 한 컴퓨터에서 호출한 함수를 네트워크를 통해 다른 컴퓨터에서 실행하는 기능이다. 클라이언트 입력을 서버에 전달하거나, 서버가 특정 클라이언트의 UI를 열거나, 폭발 효과를 여러 클라이언트에 재생할 때 사용한다.

하지만 함수에 Server, Client, NetMulticast를 붙였다고 원하는 곳에서 무조건 실행되는 것은 아니다. 실제 실행 위치는 다음 조건의 영향을 받는다.

  • RPC를 호출한 컴퓨터
  • RPC가 선언된 액터의 Owner와 Owning Connection
  • 액터와 컴포넌트의 복제 여부
  • 각 클라이언트에 대한 액터의 Relevancy
  • Reliable 또는 Unreliable 신뢰성 설정

RPC는 이미 발생한 사건을 전달하는 데 적합하다. 반면 현재 체력, 문이 열린 상태, 폭발한 지뢰처럼 나중에도 복구되어야 하는 상태는 Property Replication이 담당해야 한다. 두 기능의 차이를 이해하지 못하고 RPC만으로 상태를 맞추면 거리 기반 relevancy와 늦은 접속에서 쉽게 문제가 발생한다.

RPC는 단방향 메시지다

RPC는 호출자가 원격 실행 결과를 즉시 돌려받는 일반 함수가 아니다. 네트워크를 통과하는 단방향 호출이므로 반환값을 선언할 수 없다.

UFUNCTION(Server, Reliable)
void ServerRequestSpawnLandMine();
void ADXPlayerCharacter::ServerRequestSpawnLandMine_Implementation()
{
    // 서버에서 실행
}

헤더에는 원래 함수 이름을 선언하고, C++ 구현부에는 _Implementation 접미사가 붙은 함수를 작성한다. 실제 호출은 일반 함수처럼 원래 이름을 사용한다.

ServerRequestSpawnLandMine();

원격 실행 결과가 필요하다면 다음 중 하나로 별도 응답을 설계해야 한다.

  • 서버가 복제 프로퍼티를 변경하고 클라이언트가 OnRep로 받는다.
  • 서버가 요청한 클라이언트에 Client RPC를 보낸다.
  • 요청 ID를 함께 보내고 나중에 응답과 연결한다.

RPC가 동작하기 위한 기본 조건

RPC는 복제 가능한 네트워크 객체를 통로로 사용한다. 일반적인 Actor RPC가 원격으로 전달되려면 최소한 다음 조건을 확인해야 한다.

ADXLandMine::ADXLandMine()
{
    bReplicates = true;
}
확인 항목 의미
복제 가능한 Actor 또는 ActorComponent인가? RPC는 해당 객체의 네트워크 채널과 복제 경로를 사용한다.
액터의 bReplicates가 활성화되어 있는가? 클라이언트와 서버가 대응되는 네트워크 액터를 가져야 한다.
호출 방향에 맞는 Owning Connection이 있는가? Server RPC의 호출 권한과 Client RPC의 목적지를 결정한다.
대상 클라이언트에서 액터가 relevant한가? 특히 NetMulticast RPC의 수신 범위에 영향을 준다.
RPC 인자가 네트워크로 직렬화 가능한가? 큰 문자열이나 복잡한 데이터를 매번 보내면 대역폭과 직렬화 비용이 커진다.

같은 클래스의 액터가 서버와 클라이언트에 존재한다는 사실만으로 호출 권한이 생기지는 않는다. 어떤 연결이 그 액터를 소유하는지가 중요하다.

Owner와 Owning Connection

모든 AActorOwner 포인터를 가질 수 있다. Owner 체인을 바깥쪽으로 따라갔을 때 특정 PlayerController에 도달하면, 그 PlayerController의 네트워크 연결이 액터의 Owning Connection이 된다.

Client 1의 NetConnection
  ↕
Client 1의 PlayerController
  └─ Possess한 Pawn
      └─ Pawn이 소유한 무기 또는 상호작용 액터

PlayerController는 서버와 해당 소유 클라이언트에만 존재한다. 반면 Pawn은 다른 클라이언트에도 복제될 수 있지만, 그 Pawn의 Owning Connection은 Possess한 PlayerController의 연결 하나다.

개념 역할
Owner 액터 사이의 소유 관계를 나타내는 AActor*다.
Owning Connection Owner 체인 끝의 PlayerController에 연결된 네트워크 연결이다.
Authority 액터의 권위 있는 상태를 관리하는 서버 측 역할이다.
Local Control 현재 컴퓨터가 해당 Controller 또는 Pawn의 입력을 직접 제어하는지 나타낸다.

HasAuthority()IsLocallyControlled()는 서로 대체할 수 있는 조건이 아니다. 리슨 서버의 호스트 Pawn은 서버 권한을 가지면서 동시에 로컬 제어될 수 있다.

세 가지 RPC의 실행 방향

멀티플레이 게임에서 주로 사용하는 RPC는 Server, Client, NetMulticast 세 종류다.

RPC 종류 일반적인 호출 방향 실행 위치 대표 용도
Server RPC Owning Client → Server 서버 입력 요청, 상호작용 요청, 서버 권위 로직 시작
Client RPC Server → Owning Client 해당 액터를 소유한 클라이언트 개인 UI, 요청 결과, 소유자 전용 알림
NetMulticast RPC Server → Server와 관련 클라이언트 서버 및 현재 액터가 relevant한 클라이언트 폭발, 사운드, 일회성 애니메이션

UE 5.5에는 연결 반대편에서만 실행하는 Remote RPC도 있지만, 일반적인 게임플레이 코드는 방향이 분명한 Server와 Client RPC를 주로 사용한다.

Server RPC: 클라이언트의 요청을 서버 권위 로직으로 전달한다

클라이언트의 키 입력 함수는 해당 클라이언트에서 실행된다. 그 자리에서 액터를 스폰하면 로컬 월드에만 생기므로 서버와 다른 클라이언트가 알 수 없다.

복제되어야 하는 지뢰라면 클라이언트는 스폰을 직접 확정하지 않고, 자신이 소유한 Pawn이나 PlayerController를 통해 서버에 요청해야 한다.

// DXPlayerCharacter.h
UFUNCTION(Server, Reliable)
void ServerRequestSpawnLandMine();
void ADXPlayerCharacter::HandleLandMineInput(
    const FInputActionValue& InputValue
)
{
    if (IsLocallyControlled())
    {
        ServerRequestSpawnLandMine();
    }
}
void ADXPlayerCharacter::ServerRequestSpawnLandMine_Implementation()
{
    if (!IsValid(LandMineClass))
    {
        return;
    }

    const FVector SpawnLocation =
        GetActorLocation() + GetActorForwardVector() * 300.0f;

    FActorSpawnParameters SpawnParameters;
    SpawnParameters.Owner = this;

    GetWorld()->SpawnActor<ADXLandMine>(
        LandMineClass,
        SpawnLocation,
        FRotator::ZeroRotator,
        SpawnParameters
    );
}

지뢰 클래스에 bReplicates = true가 설정되어 있으면 서버에서 생성한 지뢰의 스폰 정보가 관련 클라이언트로 복제된다.

클라이언트 입력
  → 소유 Pawn의 Server RPC 호출
  → 서버에서 요청 검증
  → 서버가 지뢰 스폰
  → 복제 시스템이 관련 클라이언트에 지뢰 생성

Server RPC는 소유 클라이언트만 호출할 수 있다

클라이언트가 호출한 Server RPC가 원격 서버에서 실행되려면 RPC가 선언된 액터를 그 클라이언트가 소유해야 한다.

클라이언트가 Server RPC를 호출한 액터 결과
자신의 PlayerController 서버에서 실행된다.
자신이 Possess한 Pawn 서버에서 실행된다.
자신의 Pawn이 소유하고 Owner 체인이 올바른 액터 서버에서 실행될 수 있다.
다른 플레이어가 소유한 Pawn 서버로 전달되지 않고 버려진다.
클라이언트 소유자가 없는 월드 액터 클라이언트가 직접 호출한 Server RPC는 버려진다.

월드에 배치된 문이나 상자를 클릭했을 때 그 액터에서 곧바로 Server RPC를 호출하려 하면 소유권 문제를 만나기 쉽다. 이런 경우에는 소유가 확실한 PlayerController 또는 Pawn에서 Server RPC를 호출하고, 서버가 대상 월드 액터를 검증한 뒤 상호작용시키는 구조가 안정적이다.

서버는 요청을 신뢰하지 않고 다시 판정한다

Server RPC가 실행되었다는 것은 네트워크 호출 권한을 통과했다는 뜻일 뿐, 요청 내용이 게임 규칙상 유효하다는 뜻은 아니다.

클라이언트는 다음과 같은 의도만 보내는 편이 좋다.

  • 지뢰를 설치하고 싶다.
  • 이 액터와 상호작용하고 싶다.
  • 이 방향으로 공격했다.
  • 이 숫자를 추측값으로 제출한다.

서버는 다음 정보를 직접 다시 계산하거나 검증한다.

  • 현재 게임 상태에서 행동 가능한가?
  • 요청자가 살아 있고 행동권을 가지고 있는가?
  • 대상과의 거리가 허용 범위인가?
  • 쿨다운과 자원 조건을 만족하는가?
  • 입력 크기와 형식이 정상인가?
  • 호출 빈도가 비정상적으로 높지 않은가?
void ADXPlayerCharacter::ServerRequestSpawnLandMine_Implementation()
{
    if (!CanPlaceLandMine())
    {
        return;
    }

    const FVector ServerApprovedLocation =
        CalculateLandMineLocation();

    SpawnLandMineAt(ServerApprovedLocation);
}

클라이언트가 보내 준 월드 위치와 데미지를 그대로 사용하면 치팅과 잘못된 상태를 서버가 승인하게 된다.

WithValidation의 역할

UE 5.5에서는 Server RPC에 WithValidation을 지정할 수 있다.

UFUNCTION(Server, Reliable, WithValidation)
void ServerRequestSpawnLandMine();
bool ADXPlayerCharacter::ServerRequestSpawnLandMine_Validate()
{
    return IsValid(LandMineClass);
}

_Validate()false를 반환하면 해당 호출을 보낸 클라이언트는 서버에서 연결 해제된다. 따라서 단순한 게임 규칙 실패와 악의적이거나 구조적으로 잘못된 패킷을 구분해야 한다.

예를 들어 “쿨다운이 아직 끝나지 않았다”는 일반적인 요청 거절일 수 있으므로 _Implementation()에서 무시하거나 결과를 알려 주는 편이 낫다. 반면 허용 범위를 심하게 벗어난 인자나 프로토콜 위반은 _Validate()에서 차단할 수 있다. _Validate()가 항상 true라면 보안 검증을 제공하지 않는다.

Client RPC: 특정 소유 클라이언트에만 보낸다

Client RPC는 서버가 액터의 Owning Connection을 찾아 그 클라이언트에서 함수를 실행한다.

UFUNCTION(Client, Reliable)
void ClientShowInteractionError(const FText& Message);
void ANBPlayerController::ClientShowInteractionError_Implementation(
    const FText& Message
)
{
    ShowErrorMessage(Message);
}

UI는 각 클라이언트의 로컬 화면에만 존재한다. 따라서 사망 화면, 개인 오류, 상호작용 실패처럼 한 플레이어만 알아야 하는 정보는 PlayerController의 Client RPC와 잘 맞는다.

상황 적합한 전달 방식
“내 턴이 아닙니다”라는 개인 오류 메시지 해당 PlayerController의 Client RPC
개인 인벤토리 창 열기 소유 PlayerController 또는 소유 Pawn의 Client RPC
모든 플레이어의 현재 점수 GameState 또는 PlayerState의 Property Replication
모든 플레이어가 봐야 하는 폭발 효과 서버가 호출하는 NetMulticast RPC

서버가 Client RPC를 소유 연결이 없는 액터에서 호출하면 특정 클라이언트를 찾을 수 없다. 이런 호출은 기대한 원격 클라이언트가 아니라 서버에서 로컬 실행될 수 있으므로, Client RPC는 목적지가 명확한 PlayerController나 그 연결이 소유한 액터에 두는 것이 좋다.

NetMulticast RPC: 현재 관련 있는 여러 컴퓨터에서 실행한다

NetMulticast RPC를 서버에서 호출하면 서버와 현재 해당 액터가 relevant한 클라이언트에서 실행된다.

UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayExplosionFX();
void ADXLandMine::MulticastPlayExplosionFX_Implementation()
{
    if (GetNetMode() == NM_DedicatedServer)
    {
        return;
    }

    Particle->Activate(true);
}

데디케이티드 서버는 화면과 오디오 출력이 없으므로 코스메틱 효과를 생략할 수 있다. HasAuthority() == false로 검사하면 리슨 서버 호스트의 화면에서도 효과가 빠질 수 있으므로, 데디케이티드 서버만 제외하려면 GetNetMode() == NM_DedicatedServer를 확인하는 편이 정확하다.

Multicast는 반드시 서버가 호출해야 한다

클라이언트에서 NetMulticast RPC를 호출해도 다른 컴퓨터로 전파되지 않는다. 호출한 클라이언트에서 일반 로컬 함수처럼 실행될 뿐이다.

호출 위치 NetMulticast 실행 범위
서버 서버와 현재 액터가 relevant한 모든 클라이언트
클라이언트 호출한 클라이언트에서만 로컬 실행

피격과 폭발 판정도 서버를 기준으로 시작해야 한다.

void ADXLandMine::OnLandMineBeginOverlap(
    AActor* OverlappedActor,
    AActor* OtherActor
)
{
    if (!HasAuthority())
    {
        return;
    }

    ApplyDamageOnServer(OtherActor);
    MulticastPlayExplosionFX();
}

클라이언트에서도 Overlap 이벤트가 발생할 수 있지만 데미지와 폭발 여부를 각 클라이언트가 독립적으로 확정하면 결과가 달라질 수 있다.

Multicast는 과거 사건을 저장하지 않는다

NetMulticast RPC는 호출 시점에 연결되어 있고 액터가 relevant한 클라이언트에만 전달된다.

다음 클라이언트는 과거 Multicast를 받을 수 없다.

  • RPC가 실행된 뒤 게임에 접속한 클라이언트
  • 호출 시점에 NetCullDistanceSquared 밖에 있던 클라이언트
  • 호출 시점에 다른 relevancy 조건으로 액터를 받지 못한 클라이언트
  • Unreliable RPC 패킷을 잃어버린 클라이언트

bAlwaysRelevant = true로 바꾸면 현재 연결의 거리 문제는 줄일 수 있지만, 늦은 접속자가 이미 지나간 RPC를 다시 받게 하지는 않는다. 모든 클라이언트에 항상 액터를 복제하는 비용도 추가된다.

Reliable과 Unreliable

RPC는 별도로 지정하지 않으면 Unreliable이다. 신뢰성은 “중요해 보이는가”만으로 결정하지 않고, 유실되었을 때 게임 상태가 복구 가능한지와 호출 빈도를 함께 고려해야 한다.

설정 전송 특성 적합한 사례 주의점
Reliable 수신 확인이 올 때까지 재전송한다. 드문 확정 요청, 중요한 개인 알림 확인 전까지 뒤따르는 RPC 처리가 막힐 수 있다.
Unreliable 패킷이 유실되면 다시 보내지 않는다. 빈번한 코스메틱 효과, 다음 상태로 보완 가능한 사건 실행과 순서를 보장하지 않는다.

Reliable RPC는 같은 액터에서 보낸 Reliable RPC끼리 호출 순서를 보장한다. 하지만 서로 다른 액터에서 보낸 RPC 사이에는 원래 호출 순서가 보장되지 않는다.

Actor A: ReliableRPC1()
Actor B: ReliableRPC2()
Actor A: ReliableRPC3()

위 호출이 원격 컴퓨터에서 반드시 1 → 2 → 3 순서로 실행된다고 가정할 수 없다. 여러 액터에 걸친 순서가 게임 규칙에 중요하다면 하나의 권위 있는 상태나 시퀀스 번호로 순서를 표현해야 한다.

Reliable RPC를 Tick, 빠른 발사 입력, 연속 위치 갱신처럼 높은 빈도로 호출하면 재전송 대기열과 대역폭을 쉽게 압박한다. “중요하므로 모두 Reliable”이라는 선택은 오히려 이후의 중요한 메시지까지 늦출 수 있다.

RPC와 Property Replication의 차이

RPC는 호출 사실을 전달하고, Property Replication은 서버의 현재 상태를 수렴시킨다.

구분 RPC Property Replication
표현 대상 순간적인 사건과 명령 현재 유지되어야 하는 상태
늦은 접속 과거 호출을 받지 못한다. 액터가 복제될 때 현재값을 받을 수 있다.
Relevancy 밖에 있던 클라이언트 그때 발생한 RPC를 놓칠 수 있다. 다시 relevant해지면 서버의 최신 상태로 수렴한다.
중간 변화 호출 단위로 의미를 표현한다. 모든 중간값보다 최신값이 중요하다.
대표 사례 폭발음, 피격 연출, 개인 오류 알림 폭발 여부, 점수, 남은 턴 시간, 문 상태

RPC만으로 지뢰 상태를 관리하면 생기는 문제

지뢰가 폭발했을 때 Multicast RPC 안에서 로컬 변수만 바꾼다고 가정해 보자.

void ADXLandMine::MulticastPlayExplosionFX_Implementation()
{
    if (bIsExploded)
    {
        return;
    }

    Particle->Activate(true);
    bIsExploded = true;
}

호출 당시 멀리 있던 클라이언트는 Multicast를 받지 못하므로 자신의 bIsExploded가 계속 false다. 나중에 지뢰가 relevant해져도 과거 RPC는 재생되지 않는다. 늦게 접속한 클라이언트도 같은 문제를 가진다.

bIsExploded는 사건이 아니라 현재 상태이므로 서버가 복제해야 한다.

UPROPERTY(ReplicatedUsing = OnRep_IsExploded)
bool bIsExploded = false;

UFUNCTION()
void OnRep_IsExploded();
void ADXLandMine::GetLifetimeReplicatedProps(
    TArray<FLifetimeProperty>& OutLifetimeProps
) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ThisClass, bIsExploded);
}
void ADXLandMine::OnRep_IsExploded()
{
    ApplyPersistentExplodedState();
}

클라이언트가 나중에 지뢰를 처음 보거나 다시 relevant해져도 bIsExploded의 최신값을 받아 폭발한 외형을 복원할 수 있다.

순간 효과와 영속 상태를 분리한다

폭발에는 서로 수명이 다른 두 정보가 섞여 있다.

정보 수명 전달 방식
폭발 파티클과 사운드 발생 순간에만 의미가 있다. Unreliable NetMulticast RPC
이미 폭발한 지뢰라는 사실 액터가 존재하는 동안 유지되어야 한다. ReplicatedUsing 프로퍼티
데미지 판정 권위 있는 게임 결과다. 서버 로직

이를 코드에서도 분리하면 순서 의존성이 줄어든다.

void ADXLandMine::ExplodeOnServer(AActor* DamagedActor)
{
    check(HasAuthority());

    if (bIsExploded)
    {
        return;
    }

    bIsExploded = true;
    ApplyPersistentExplodedState();

    ApplyDamageOnServer(DamagedActor);
    MulticastPlayExplosionFX();
}
void ADXLandMine::OnRep_IsExploded()
{
    ApplyPersistentExplodedState();
}
void ADXLandMine::ApplyPersistentExplodedState()
{
    SetActorEnableCollision(!bIsExploded);

    if (bIsExploded && IsValid(ExplodedMaterial))
    {
        Mesh->SetMaterial(0, ExplodedMaterial);
    }
}
void ADXLandMine::MulticastPlayExplosionFX_Implementation()
{
    if (GetNetMode() != NM_DedicatedServer)
    {
        Particle->Activate(true);
        UGameplayStatics::PlaySoundAtLocation(
            this,
            ExplosionSound,
            GetActorLocation()
        );
    }
}

이 구조에서 Multicast는 bIsExploded를 보고 조기 반환하지 않는다. 순간 효과와 상태 반영이 서로의 실행 순서에 의존하지 않기 때문이다.

  • 호출 당시 relevant한 클라이언트는 폭발 효과와 최종 외형을 본다.
  • 효과 RPC를 잃더라도 다음 프로퍼티 업데이트로 폭발한 상태는 복구된다.
  • 늦은 접속자는 오래전에 끝난 폭발 효과는 보지 않고 폭발한 외형만 본다.
  • 서버는 데미지와 중복 폭발 여부를 단독으로 판정한다.

RPC와 프로퍼티의 실행 순서를 가정하지 않는다

RPC 호출과 프로퍼티 변경을 같은 프레임에 섞으면 코드에 적힌 줄 순서가 원격 컴퓨터에서도 그대로 유지된다고 생각하기 쉽다.

bIsExploded = true;
MulticastPlayExplosionFX();

그러나 복제 시스템은 RPC와 프로퍼티 데이터를 네트워크 bunch에 직렬화하며 종류에 따라 처리 시점이 달라진다. UE 5.5에서 일반적인 비대기 RPC는 프로퍼티보다 먼저 실행되지만, Unreliable NetMulticast RPC는 큐에 들어가 프로퍼티 업데이트 뒤에 실행된다.

따라서 Multicast 안에서 다음 검사를 하면 프로퍼티가 먼저 true로 반영되어 효과가 생략될 수 있다.

void ADXLandMine::MulticastPlayExplosionFX_Implementation()
{
    if (bIsExploded)
    {
        return; // 실행 순서에 따라 효과가 사라질 수 있다.
    }

    Particle->Activate(true);
}

또한 서로 다른 복제 프로퍼티의 OnRep 호출 순서도 보장되지 않는다. 두 값이 반드시 함께 해석되어야 한다면 하나의 구조체로 묶어 복제하는 편이 좋다.

USTRUCT()
struct FExplosionState
{
    GENERATED_BODY()

    UPROPERTY()
    bool bIsExploded = false;

    UPROPERTY()
    FVector_NetQuantize ExplosionLocation;
};

핵심은 네트워크 이벤트의 우연한 도착 순서를 게임 상태 머신으로 사용하지 않는 것이다. 상태, 순간 효과, 서버 판정을 분리하면 RPC와 Property Replication의 처리 순서가 달라져도 결과를 유지할 수 있다.

NumberBaseball 프로젝트의 RPC와 상태 복제

현재 NumberBaseball 프로젝트에는 RPC와 Property Replication의 역할 차이를 보여 주는 사례가 이미 들어 있다.

채팅 입력: Owning Client에서 Server RPC로

로컬 플레이어가 입력한 문자열은 자신의 PlayerController에서 Server RPC로 전달된다.

UFUNCTION(Server, Reliable)
void ServerRPCPrintChatMessageString(
    const FString& InChatMessageString
);
void ANBPlayerController::SetChatMessageString(
    const FString& InChatMessageString
)
{
    if (IsLocalController())
    {
        ServerRPCPrintChatMessageString(InChatMessageString);
    }
}

PlayerController는 연결 소유가 명확하므로 클라이언트에서 서버로 요청하는 통로로 적합하다. 서버의 GameMode는 문자열이 일반 채팅인지 숫자 추측인지 판정하고, 턴과 남은 횟수 같은 서버 상태를 기준으로 처리한다.

클라이언트가 보낸 문자열에는 추가로 다음 제한을 두는 것이 좋다.

  • 최대 길이 제한
  • 허용 문자와 숫자 형식 검사
  • 호출 빈도 제한
  • 빈 문자열과 제어 문자 제거
  • 실제 턴과 제출 가능 상태 재검사

Reliable을 사용하더라도 악의적인 연속 호출까지 막아 주지는 않는다. 신뢰성은 전달 방식이고, 입력 검증과 rate limit은 서버 게임 로직의 책임이다.

개인 오류와 결과 알림: Client RPC

서버는 “내 턴이 아닙니다”, “남은 횟수를 모두 사용했습니다” 같은 결과를 요청자의 PlayerController에만 돌려준다.

UFUNCTION(Client, Reliable)
void ClientRPCPrintChatMessageString(
    const FString& InChatMessageString
);

승리와 무승부 알림도 Client RPC로 각 PlayerController에 전달한다.

UFUNCTION(Client, Reliable)
void ClientRPCSetNotificationText(
    const FText& InNotificationText
);

이 알림은 같은 문구가 연속으로 발생해도 호출마다 의미가 있고, 과거 알림을 늦게 접속한 사람에게 복구할 필요도 없다. 따라서 Property Replication보다 RPC의 사건 모델과 잘 맞는다.

접속 알림: NetMulticast RPC

GameState에는 현재 접속한 클라이언트에 로그인 메시지를 전달하는 Multicast가 있다.

UFUNCTION(NetMulticast, Reliable)
void MulticastRPCBroadcastLoginMessage(
    const FString& InNameString
);

GameState는 모든 플레이어가 공유하는 복제 액터이므로 전체 공지의 통로로 사용할 수 있다. 다만 이 호출도 과거 기록은 아니다. 나중에 접속한 플레이어가 이전 접속 메시지까지 봐야 한다면 서버가 채팅 기록을 별도 상태로 관리하고 접속 시 스냅샷을 보내야 한다.

접속 메시지는 현재 플레이어에게만 의미가 있는 일회성 알림이므로, 기록 요구가 없다면 RPC만으로 충분하다.

현재 턴: ReplicatedUsing 구조체

현재 턴의 남은 시간, 입력 가능 여부, 활성 플레이어는 GameState의 구조체로 복제된다.

UPROPERTY(ReplicatedUsing = OnRep_TurnState)
FNBTurnState TurnState;
void ANBGameStateBase::OnRep_TurnState()
{
    OnTurnStateChanged.Broadcast();
}

턴 상태는 접속 시점과 관계없이 모든 클라이언트가 현재값을 알아야 한다. RPC로 초 단위 이벤트만 보내는 것보다 서버의 최신 상태를 복제하고 UI가 OnRep에 반응하는 구조가 적합하다.

프로젝트 정보 현재 방식 선택 이유
클라이언트의 채팅·숫자 입력 Server RPC 클라이언트 의도를 서버 권위 판정으로 전달한다.
개인 오류와 승패 알림 Client RPC 특정 소유 클라이언트의 UI에 발생 시점마다 표시한다.
접속 알림 NetMulticast RPC 현재 연결된 클라이언트에 일회성 사건을 알린다.
현재 턴과 남은 시간 Property Replication 늦은 접속과 갱신 누락 이후에도 최신 상태가 필요하다.
플레이어 이름과 추측 횟수 PlayerState Property Replication 모든 플레이어가 지속적으로 참조하는 현재 상태다.

RPC 선택 순서

RPC 종류부터 고르기보다 전달하려는 정보의 성격부터 판단하면 설계가 단순해진다.

1. 나중에도 복구되어야 하는 현재 상태인가?
   → Property Replication 또는 ReplicatedUsing

2. 클라이언트가 서버에 게임 행동을 요청하는가?
   → 소유 Pawn/PlayerController의 Server RPC

3. 서버가 한 플레이어에게만 결과를 알려야 하는가?
   → Owning Connection이 분명한 액터의 Client RPC

4. 현재 관련된 여러 클라이언트에 순간 효과를 재생하는가?
   → 서버가 호출하는 NetMulticast RPC

5. 한 번 유실되어도 다음 상태나 다음 효과로 복구 가능한가?
   → Unreliable 검토

6. 반드시 한 번 전달되어야 하고 호출 빈도가 낮은가?
   → Reliable 검토
잘못된 접근 발생 가능한 문제 개선 방향
클라이언트가 복제 액터를 직접 스폰한다. 로컬에만 생성되어 서버와 다른 클라이언트가 모른다. Server RPC로 요청하고 서버가 스폰한다.
소유권 없는 월드 액터에서 Server RPC를 호출한다. 호출이 서버로 전달되지 않는다. 소유 PlayerController 또는 Pawn을 요청 통로로 사용한다.
클라이언트가 NetMulticast를 호출한다. 다른 컴퓨터로 전파되지 않는다. 서버 판정 뒤 서버가 Multicast를 호출한다.
영속 상태를 Multicast의 로컬 변수로만 관리한다. 늦은 접속과 relevancy 재진입에서 상태가 사라진다. 상태는 복제 프로퍼티로 관리한다.
모든 호출을 Reliable로 만든다. 재전송 대기와 대역폭 포화로 후속 RPC가 지연된다. 유실 허용 여부와 호출 빈도로 신뢰성을 결정한다.
RPC와 OnRep의 실행 순서에 게임 규칙을 의존한다. 패킷 상태와 RPC 종류에 따라 결과가 달라진다. 권위 상태, 순간 효과, 표시 로직을 분리한다.

RPC 문제를 추적하는 체크리스트

증상 확인할 항목
Server RPC가 서버에서 실행되지 않는다. 호출 클라이언트가 액터의 Owning Connection인지, 액터가 복제되는지 확인한다.
Client RPC가 원하는 클라이언트에 도착하지 않는다. Owner 체인이 해당 PlayerController로 이어지는지 확인한다.
Multicast가 호출한 클라이언트에서만 실행된다. 서버가 호출했는지 확인한다.
멀리 있던 클라이언트가 과거 효과를 모른다. Relevancy 범위와 영속 상태의 Property Replication 여부를 확인한다.
늦게 접속하면 액터 외형이 초기 상태다. 외형을 Multicast만으로 바꾸지 않았는지, RepNotify로 복원 가능한지 확인한다.
같은 프레임의 RPC와 프로퍼티 결과가 예상과 다르다. 신뢰성, Unreliable Multicast 큐 처리, OnRep 순서 의존성을 확인한다.
Reliable RPC 이후 다른 메시지가 밀린다. 호출 빈도, 인자 크기, 패킷 손실, Reliable 남용 여부를 확인한다.

RPC 디버깅에서는 함수에 로그만 넣기보다 NetMode, LocalRole, RemoteRole, Owner, IsLocalController() 또는 IsLocallyControlled()를 함께 출력해야 한다. 같은 함수가 서버와 여러 클라이언트에 존재하는 각 액터 인스턴스에서 실행될 수 있기 때문이다.