[Unity] Netcode for GameObjects(NGO) (2)

조경민·2026년 4월 20일

네트워크 동기화

  • 상태(state) : 플레이어의 체력 · 점수 · 닉네임 (계속 유지됨)
    -> NetworkVariable, NetworkList

  • 이벤트(event) : 공격 · 이펙트 · 채팅 메시지 (한 번만 발생하고 끝)
    -> ServerRpc, ClientRpc

    구분NetworkVariableRPC
    성격상태 (State)이벤트 (Event)
    값 유지값이 계속 유지됨호출 시점에만 실행
    늦은 접속나중에 접속해도 최신값 수신이미 발생한 이벤트는 수신 불가
    전송 빈도값이 변경될 때만 자동 전송호출할 때마다 전송
  • 사용 예시

    데이터적합한 방식이유
    플레이어 HPNetworkVariable늦은 접속자도 현재 HP 를 알아야 함
    플레이어 닉네임NetworkVariable한 번 설정 후 유지되는 상태
    공격 요청ServerRpc서버가 판정해야 함 (치팅 방지)
    피격 이펙트ClientRpc일회성 연출. 값 유지 불필요
    채팅 메시지ServerRpcClientRpc서버 경유 후 전체 전달
    매 프레임 위치 갱신NetworkTransformNetworkVariableRPC 도 아님. NetworkTransform

NetworkVariable - 자동 동기화 변수

  • NetworkVariable 은 네트워크 전체에서 자동으로 동기화되는 변수
  • 값이 변경되면 NGO 가 모든 클라이언트에 변경 내용을 자동으로 전달하며, 게임이 시작된 후 나중에 접속한 클라이언트도 현재의 최신값을 수신할 수 있다.

생성과 권한 설정

  • NetworkVariable 은 생성 시 초기값과 함께 읽기 권한 · 쓰기 권한을 설정할 수 있다.
using Unity.Netcode;

public class PlayerStats : NetworkBehaviour
{
    // 체력 : 서버만 변경 가능 (기본값)
    private NetworkVariable<int> _health = new NetworkVariable<int>(100);

    // 닉네임 : 소유자가 직접 변경 가능
    private NetworkVariable<FixedString64Bytes> _nickname = new NetworkVariable<FixedString64Bytes>(
        default,
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Owner);
}
  • 권한 조합

    읽기쓰기사용 사례
    EveryoneServer체력 · 점수 (권위 서버 패턴)
    EveryoneOwner닉네임 (소유자가 자기 정보를 직접 설정)
    OwnerServer비공개 정보 (다른 플레이어에게 숨기는 인벤토리)

OnValueChanged 콜백

  • 값이 변경될 때 모든 피어에서 호출되는 콜백
  • UI 갱신이나 이펙트 재생에 활용
public override void OnNetworkSpawn()
{
    _health.OnValueChanged += OnHealthChanged;

    // Late Join 대응: 현재 값으로 UI 를 한 번 초기화
    OnHealthChanged(0, _health.Value);
}

public override void OnNetworkDespawn()
{
    _health.OnValueChanged -= OnHealthChanged;
}

private void OnHealthChanged(int previous, int current)
{
    _healthBar.value = current;
}

사용 가능한 타입 제약

  • NetworkVariable 의 T 에는 unmanaged 타입 (스택에 고정 크기로 할당되는 값 타입) 만 사용할 수 있음
사용 가능사용 불가
int, float, bool, ulong 등 기본 타입string (참조 타입)
Vector2, Vector3, Quaternion, ColorList, Dictionary<K,V>
enumclass 인스턴스
FixedString32Bytes ~ FixedString4096Bytes
필드가 모두 unmanaged 인 struct
  • string이 안되는 이유
    • C# 의 string 은 힙에 할당되는 참조 타입이라서 크기가 가변적이다.
    • 네트워크 직렬화는 데이터를 바이트 배열로 변환해야 하는데, 가변 길이라 매번 크기 계산과 메모리 할당이 필요해 성능 저하와 GC 부담이 발생한다.
    • 대신 크기가 컴파일 타임에 고정된 FixedString 을 사용한다.

FixedString

  • Unity.Collections 가 제공하는 고정 크기 문자열 타입

  • 이름의 숫자는 총 바이트 수, 헤더 2 바이트를 제외한 만큼을 실제로 사용 가능

  • 한글은 UTF-8 기준 한 글자당 3 바이트

    타입총 바이트사용 가능 바이트한글 최대 길이용도
    FixedString32Bytes32299자짧은 상태 태그 ("Ready", "Alive")
    FixedString64Bytes646120자플레이어 닉네임
    FixedString128Bytes12812541자짧은 채팅 메시지
    FixedString512Bytes512509169자긴 메시지 · 설명

NetworkList

  • NetworkVariable 의 리스트 버전

  • 요소의 추가 · 제거 · 변경을 네트워크로 자동 동기화하며, 변경 이벤트를 수신할 수 있음

    항목NetworkVariableNetworkList
    용도단일 값 동기화가변 길이 목록 동기화
    이벤트OnValueChangedOnListChanged
    쓰기 권한Server / Owner 선택 가능Server 전용
public class ScoreBoard : NetworkBehaviour
{
    private NetworkList<int> _scores;

    private void Awake()
    {
        _scores = new NetworkList<int>();   // 필드 선언이 아닌 Awake 에서
    }

    public override void OnNetworkSpawn()
    {
        _scores.OnListChanged += OnScoresChanged;
    }

    public override void OnNetworkDespawn()
    {
        _scores.OnListChanged -= OnScoresChanged;
    }

    private void OnScoresChanged(NetworkListEvent<int> ev)
    {
        switch (ev.Type)
        {
            case NetworkListEvent<int>.EventType.Add:     /* 추가 처리 */ break;
            case NetworkListEvent<int>.EventType.Remove:  /* 제거 처리 */ break;
            case NetworkListEvent<int>.EventType.Value:   /* 값 변경 처리 */ break;
            case NetworkListEvent<int>.EventType.Clear:   /* 전체 초기화 처리 */ break;
        }
    }
}

NetworkList 의 변경 작업(Add · Remove · Clear 등) 은 서버에서만 가능. 클라이언트에서 변경이 필요하면 ServerRpc 로 서버에 요청



RPC - 원격 함수 호출

  • RPC(Remote Procedure Call, 원격 프로시저 호출)
  • 다른 네트워크 컨텍스트에 있는 메서드를 원격으로 호출하는 기법
  • NetworkVariable - "상태" 동기화 / RPC - "이벤트" 전달

작성 규칙

  • 이름이 규칙에 맞지 않으면 컴파일 에러 발생
규칙설명
ServerRpc 접미사메서드 이름이 반드시 ServerRpc 로 끝나야 함
ClientRpc 접미사메서드 이름이 반드시 ClientRpc 로 끝나야 함
[ServerRpc] / [ClientRpc] 어트리뷰트대응하는 어트리뷰트가 필수
NetworkBehaviour 상속 필수RPC 는 NetworkBehaviour 를 상속한 클래스에서만 사용 가능

ServerRpc

  • 클라이언트 → 서버
  • 클라이언트가 서버에 "이 작업을 처리해 달라" 고 요청하는 통로
  • 치팅 방지를 위해 권위 있는 연산을 서버에 위임할 때 사용

RequireOwnership 옵션

  • [ServerRpc]는 기본적으로 소유자만 호출할 수 있음.
  • 소유자가 아닌 클라이언트도 호출할 수 있게 하려면 RequireOwnership = false 를 지정
// 소유자만 호출 가능 (기본값)
[ServerRpc]
private void FireServerRpc() { }

// 누구든 호출 가능
[ServerRpc(RequireOwnership = false)]
private void SendChatServerRpc(string message) { }

발신자 ID 확인 (치팅 방지)

  • ServerRpc 메서드의 마지막 파라미터로 ServerRpcParams 를 추가하면 호출한 클라이언트의 ID 를 알 수 있음.
  • NGO 가 자동으로 값을 채워 줌
[ServerRpc(RequireOwnership = false)]
private void RequestKickServerRpc(ulong targetId, ServerRpcParams rpcParams = default)
{
    // 서버는 항상 rpcParams.Receive.SenderClientId 로 발신자를 식별해야 함 (치팅 방지)
    ulong senderId = rpcParams.Receive.SenderClientId;

    if (!IsAdmin(senderId))
    {
        Debug.LogWarning($"비관리자가 강퇴 시도: {senderId}");
        return;
    }

    NetworkManager.Singleton.DisconnectClient(targetId);
}

ClientRpc

  • 서버 → 클라이언트
  • 서버가 클라이언트(들)에게 "이 작업을 실행해 달라" 고 요청하는 통로
  • 기본적으로 모든 클라이언트에 전달되며, 호스트는 자기 자신의 클라이언트 측에서도 실행됨.
// 서버에서 이 메서드를 호출하면 모든 클라이언트가 똑같은 로그를 출력
[ClientRpc]
public void BroadcastMessageClientRpc(string message)
{
    Debug.Log($"[전체 공지] {message}");
}
  • ClientRpc 는 서버에서만 호출 가능
    • 일반 클라이언트에서 ClientRpc 메서드를 호출하면 동작하지 않음.
    • 호스트는 자기 자신의 클라이언트 측에서도 실행되므로, 순수 클라이언트 전용 분기가 필요하면 if (IsClient && !IsServer) 로 가드

특정 클라이언트만 타깃

  • ClientRpcParams.Send.TargetClientIds를 채우면 일부 클라이언트에게만 전달할 수 있음.
  • 이 파라미터는 반드시 메서드의 마지막 파라미터여야 함.
public void SendToClient(string message, ulong targetClientId)
{
    if (!IsServer) return;   // 서버에서만 호출 가능

    ClientRpcParams rpcParams = new ClientRpcParams
    {
        Send = new ClientRpcSendParams
        {
            TargetClientIds = new ulong[] { targetClientId }
        }
    };
    SendPrivateMessageClientRpc(message, rpcParams);
}

[ClientRpc]
private void SendPrivateMessageClientRpc(string message, ClientRpcParams clientRpcParams = default)
{
    Debug.Log($"[개인 메시지] {message}");
}


하이브리드 패턴

  • NetworkVariable + RPC
public class Player : NetworkBehaviour
{
    // HP 는 NetworkVariable 로 상태 유지
    private NetworkVariable<int> _hp = new NetworkVariable<int>(100);

    // 서버 = 계산 + 검증 + 상태 결정(권한) + 동기화 책임
    [ServerRpc]
    private void AttackServerRpc()
    {
        // 서버에서 판정
        _hp.Value -= 10;                  // 상태 변경 → 자동 동기화
        PlayHitEffectClientRpc();         // 이벤트 → 일회성 연출
    }

    // 클라 = 입력(요청) + 표현
    [ClientRpc]
    private void PlayHitEffectClientRpc()
    {
        // 이펙트 재생
    }
}

예제 코드

using UnityEngine;
using Unity.Netcode;

public class RpcPractice : NetworkBehaviour
{
    [SerializeField] private Renderer _renderer;
    private int _score;

    public override void OnNetworkSpawn()
    {
        _renderer = GetComponentInChildren<Renderer>();
    }

    private void Update()
    {
        if (!IsOwner) return;

        // E 키: 서버에 점수 추가 요청 (ServerRpc)
        if (Input.GetKeyDown(KeyCode.E))
        {
            RequestAddScoreServerRpc(10);
        }

        // Q 키: 서버에 색상 변경 요청 (ServerRpc → ClientRpc 조합)
        if (Input.GetKeyDown(KeyCode.Q))
        {
            float r = Random.Range(0f, 1f);
            float g = Random.Range(0f, 1f);
            float b = Random.Range(0f, 1f);
            RequestChangeColorServerRpc(r, g, b);
        }
    }

    // --- ServerRpc ---

    // 점수 추가 요청 (클라이언트 → 서버)
    [ServerRpc]
    private void RequestAddScoreServerRpc(int amount)
    {
        // 서버에서 점수 처리
        _score += amount;
        Debug.Log($"[Server] {OwnerClientId} 번 플레이어 점수: {_score}");

        // 모든 클라이언트에게 결과 알림
        NotifyScoreUpdatedClientRpc(OwnerClientId, _score);
    }

    // 색상 변경 요청 (클라이언트 → 서버)
    [ServerRpc]
    private void RequestChangeColorServerRpc(float r, float g, float b)
    {
        // 서버가 검증 후 모든 클라이언트에 색상 변경 지시
        ApplyColorClientRpc(r, g, b);
    }

    // --- ClientRPC ---

    // 점수 갱신 알림 (서버 → 모든 클라이언트)
    [ClientRpc]
    private void NotifyScoreUpdatedClientRpc(ulong clientId, int newScore)
    {
        Debug.Log($"[Client] {clientId} 번 플레이어 점수가 {newScore} 으로 변경되었습니다.");
    }

    // 색상 적용 (서버 → 모든 클라이언트)
    [ClientRpc]
    private void ApplyColorClientRpc(float r, float g, float b)
    {
        if (_renderer == null) return;
        _renderer.material.color = new Color(r, g, b);
    }
}
profile
안녕하세요

0개의 댓글