네트워크 관련 궁금한 점 및 간단한 개념

개발하는 운동인·2025년 5월 22일

Q1. 메인 캐릭터 프리펩에 Network Transform vs Client Network Transform 중 어느 컴포넌트를 부착해야 하는지??

  • 서버로 패킷을 넘길 때 최소화된 정보를 넘겨줘야 하므로 불필요한 정보는 UnCheck 해줍니다.

  • Player 프리팹에 Client Network Transform컴포넌트를 붙여줍니다.

  • Network Transform을 쓰지 않고 Client Network Transform을 붙여주는 이유는 아래와 같습니다.

    • Network Transform의 경우 서버 권위적인 로직이어서 클라이언트가 서버에게 변경된 Transform을 보내고 Server가 허락해 준 다음 변경된 값들을 클라이언트들에게 뿌리는 형식입니다.

  • Client Network Transform의 경우를 사용하는 이유는 Client가 Transform 값을 직접 변경할 수 있게 하므로 서버 권한을 필요로 하지 않습니다. 이를 통해 Client는 서버에 대한 의존성 없이 자체적으로 오브젝트의 Transform을 조작할 수 있습니다.

A1

NetworkVariable 를 사용하여 클라이언트에게 권한 부여하기.

  • 정보(Data)도 동기화 잘되는지 Test 해보겠습니다. 코드를 위와 같이 작성해 주시면 됩니다.

  • 이 스크립트로 Test를 해보면, Server 쪽에서 T키를 눌렀을 경우 문제없이 잘 작동하지만 클라이언트 쪽에서 T키를 눌렀을 경우 에러(Error)가 발생합니다.

⭐ 그 이유는 NetCode는 앞서 설명드렸다시피 서버 권위적이므로 클라이언트 쪽에서 데이터를 마음대로 수정할 수 없기 때문입니다.

✅ 이럴 경우 아래와 같이 해결해 줄 수 있습니다.

  • 데이터 수정을 클라이언트에(소유자=Owner)게도 할 수 있게 권한을 부여해 주면 됩니다.

  • 위 코드와 같이 데이터 수정을 Write를 Owner에서 Everyone으로 해주면 됩니다.

  • 실행 화면, 클라이언트 쪽에서 T를 눌러도 에러가 발생하지 않고 잘 작동함.

  • NetworkVariable 는 RPC나 Custom Message 사용 없이 서버와 클라이언트간에 값을 동기화해주는 방법입니다!

  • NetworkVariable 는 제너릭 형태로 값을 저장합니다!

  • NetworkVariable은 반드시 NetworkBehaviour 내에 정의되어야만 하며 NetworkObject컴포넌트가 붙어있어야 합니다!
  • NetworkVariable의 값은 오직 값을 초기화 할 때와 NetworkObject가 생성되어 있는 동안에만 설정할 수 있습니다!
  • 클라이언트의 NetworkVariable의 값은 처음 연결할 때 자동으로 동기화 됩니다

❗ Start() vs OnNetworkSpawn() 상황에 따른 호출 순서

⭐ String 사용방법

  • string타입은 지원하지 않습니다! 왜냐하면 string은 C# immutable 타입이기 때문입니다!

  • 매 Update 마다 CG allocation을 발생하기 때문에 퍼포먼스 문제가 발생합니다!

  • 따라서, FixedString타입을 사용하여야 합니다!

📝 string 예제

public class TestFixedString : NetworkBehaviour
{
    private NetworkVariable<FixedString128Bytes> m_TextString = new NetworkVariable<FixedString128Bytes>();
 
    private string[] m_Messages ={ "This is the first message.",
    "This is the second message (not like the first)",
    "This is the third message (but not the last)",
    "This is the fourth and last message (next will roll over to the first)"
    };
 
    private int m_MessageIndex = 0;
 
    public override void OnNetworkSpawn()
    {
        if (IsServer)
        {
            m_TextString.Value = m_Messages[m_MessageIndex];
        }
        else
        {
            m_TextString.OnValueChanged += OnTextStringChanged;
  
            Debug.Log($"Client-{NetworkManager.LocalClientId}'s TextString = {m_TextString.Value}");
        }
    }
 
    public override void OnNetworkDespawn()
    {
        m_TextString.OnValueChanged -= OnTextStringChanged;        
    }
 
    private void OnTextStringChanged(FixedString128Bytes previous, FixedString128Bytes current)
    {
        Debug.Log($"Client-{NetworkManager.LocalClientId}'s TextString = {m_TextString.Value}");
    }
 
    private void LateUpdate()
    {
        if (!IsServer)
        {
            return;
        }
 
        if (Input.GetKeyDown(KeyCode.Space))
        {
            m_MessageIndex++;
            m_MessageIndex %= m_Messages.Length;
            m_TextString.Value = m_Messages[m_MessageIndex];
            Debug.Log($"Server-{NetworkManager.LocalClientId}'s TextString = {m_TextString.Value}");
        }
    }
}

  • 서버쪽에서 Space Bar를 누를 때마다 클라이언트들은 위와 같은 메세지를 순차적으로 받는다!

NetworkManager.Connection approval

  • Connection approval는 기본적인 NetworkManager에 플레이어마다 기본적으로 등록한 Player Prefab에 override를 허락해줌으로써, 특정한 Player Prefab을 만드는 것 또한 가능하게 해준다.

⭐ NetworkManager의 ConnectionApproval의 속성을 true로 세팅하지 않으면 Netcode는 기본적인 유저 인증을 사용해서, 자동적으로 기본적인 player prefab을 사용하도록 합니다

NetworkManager.ConnectionApprovalRequest

  • ClientNetworkId: 연결하는 클라이언트 identifier
  • Payload: 추가 사용자 정의 연결 데이터

NetworkManager.ConnectionApprovalResponse

  • 서버측에서, 이 함수에 연결을 시도하는 플레이어를 허용하거나 거부하는데 필요한 모든 연결 승인 응답에 관한 정보가 있습니다

📝 간단한 예제

private void Setup() 
{
    NetworkManager.Singleton.ConnectionApprovalCallback = ApprovalCheck;
    NetworkManager.Singleton.StartHost();
}
 
private void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
    // The client identifier to be authenticated
    var clientId = request.ClientNetworkId;
 
    // Additional connection data defined by user code
    var connectionData = request.Payload;
 
    // Your approval logic determines the following values
    response.Approved = true;
    response.CreatePlayerObject = true;
 
    // The Prefab hash value of the NetworkPrefab, if null the default NetworkManager player Prefab is used
    response.PlayerPrefabHash = null;
 
    // Position to spawn the player object (if null it uses default of Vector3.zero)
    response.Position = Vector3.zero;
 
    // Rotation to spawn the player object (if null it uses the default of Quaternion.identity)
    response.Rotation = Quaternion.identity;
    
    // If response.Approved is false, you can provide a message that explains the reason why via ConnectionApprovalResponse.Reason
    // On the client-side, NetworkManager.DisconnectReason will be populated with this message via DisconnectReasonMessage
    response.Reason = "Some reason for not approving the client";
 
    // If additional approval steps are needed, set this to true until the additional steps are complete
    // once it transitions from true to false the connection approval response will be processed.
    response.Pending = false;
}

Sending an approval declined reason

    1. 특별한 이유로(예를 들어, 방의 인원수가 꽉찬 경우) 클라이언트가 접속을 실패할 때, 서버 측에서 클라이언트에게 DisconnectReasonMessage를 보냅니다(클라이언트측에서의 연결요청을 거절할 수 밖에 없었던 이유로 가득 찬 NetworkManager.ConnectionApprovalResponse.Reason)
    1. 클라이언트 측에선 DisconnectReasonMessage를 받습니다(서버측에서 왜 연결요청을 거절할 수 밖에 없었던 이유에 대한 설명이 적힌 NetworkManager.ConnectionApprovalResponse. Reason)

📝 간단한 예제

using UnityEngine;
using Unity.Netcode;
 
/// <summary>
/// Connection Approval Handler Component
/// </summary>
/// <remarks>
/// This should be placed on the same GameObject as the NetworkManager.
/// It automatically declines the client connection for example purposes.
/// </remarks>
public class ConnectionApprovalHandler : MonoBehaviour
{
    private NetworkManager m_NetworkManager;
 
    private void Start()
    {
        m_NetworkManager = GetComponent<NetworkManager>();
        if (m_NetworkManager != null)
        {
            m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnectCallback;
            m_NetworkManager.ConnectionApprovalCallback = ApprovalCheck;
        }
    }
 
    private void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
    {
        response.Approved = false;
        response.Reason = "Testing the declined approval message";
    }
 
    private void OnClientDisconnectCallback(ulong obj)
    {
        if (!m_NetworkManager.IsServer && m_NetworkManager.DisconnectReason != string.Empty)
        {
            Debug.Log($"Approval Declined Reason: {m_NetworkManager.DisconnectReason}");
        }
    }
}

Connection data(NetworkManager.ConnectionApprovalRequest.Payload)

  • Connection Data는 주로 Room 비밀번호, 서버가 연결을 승인해줄지 말지 결정하는데 도움을 주는거와 같은 종류로 사용되곤 합니다
  • Connection Data는 클라이언트 측에서 정의됩니다
    NetworkConfig.ConnectionData는 자동으로 서버측에게 연결 요청 메세지를 보냅니다(NetworkManager.StartClient가 호출되기 전에 클라이언트 측에 의해서)

📝 간단한 예제

using Unity.Netcode;
 
NetworkManager.Singleton.NetworkConfig.ConnectionData = System.Text.Encoding.ASCII.GetBytes("room password");
NetworkManager.Singleton.StartClient();

캐릭터 선택

  • 연결중인 플레이어가 사용하고 있는 Player Prefab을 특정한 다른것으로 사용(오버라이딩)하고 싶을 때가 있을 것입니다!
    Connection Approbal Process는 이것을 가능하게 해줍니다!

Step 1. 씬안에 놓여진 Connection approval compoent를 바꾸거나 만들면 됩니다!

public class ClientConnectionHandler : NetworkBehaviour
    {
        public List<uint> AlternatePlayerPrefabs;
 
        public void SetClientPlayerPrefab(int index)
        {
            if (index > AlternatePlayerPrefabs.Count)
            {
                Debug.LogError($"Trying to assign player Prefab index of {index} when there are onlky {AlternatePlayerPrefabs.Count} entries!");
                return;
            }
            if (NetworkManager.IsListening || IsSpawned)
            {
                Debug.LogError("This needs to be set this before connecting!");
                return;
            }
            NetworkManager.NetworkConfig.ConnectionData = System.BitConverter.GetBytes(index);
        }
 
        public override void OnNetworkSpawn()
        {
            if (IsServer)
            {
                NetworkManager.ConnectionApprovalCallback = ConnectionApprovalCallback;
            }
        }
 
        private void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
        {
            var playerPrefabIndex = System.BitConverter.ToInt32(request.Payload);
            if (AlternatePlayerPrefabs.Count < playerPrefabIndex)
            {
                response.PlayerPrefabHash = AlternatePlayerPrefabs[playerPrefabIndex];
            }
            else
            {
                Debug.LogError($"Client provided player Prefab index of {playerPrefabIndex} when there are onlky {AlternatePlayerPrefabs.Count} entries!");
                return;
            }
            // Continue filling out the response
        }
    }

Step2. Alternate Player Prefabs의 GlobalObjectIdHash 값을 복사하자!

  • AlternatePlayerPrefabs List에 채우기 위해

  • 씬 안에 놓여 있는 ConnectoinApprovalCallback이 붙어 있는 NetworkObject가 포함된 Scene 엽니다!

  • List에 추가되길 바라는 Alternate Player Prefab을 찾고, 프리팹으로 선택합니다, 그리고 GlobalObjectIdHash 값을 복사합니다!

  • AlternatePlayerPrefabs List안에 새로운 List item으로 복사된 GlobalObjectIdHash 값을 붙여줍니다!

NetworkObject

  • Netcode 게임오브젝트는 반드시 NetworkObject 컴포넌트를 가지고 있어야 하며 적어도 NetworkBehaviour 컴포넌트를 하나 가지고 있어야 합니다
  • NetworkObject가 스폰될 때, 최초로 NetworkObjct.GlobalObjectIdHash 값이 정해집니다!

NetworkBehaviours

  • NetworkObject와 관련된 그들만의 커스튬 Netcode 로직을 만들기 위한 틀이라고 생각하시면 됩니다

Ownership

  • NGO는 서버권위적이므로, 서버가 Network Object의 Spawn & Despawn을 관리합니다!
    1. 기본적인 Spawn 함수입니다. 서버측에서 소유권을 얻습니다
GetComponent<NetworkObject>().Spawn();
    1. 서버가 Spawn되는 Object의 소유권을 clientId에게 줍니다
GetComponent<NetworkObject>().SpawnWithOwnership(clientId);
    1. 서버가 Netcode Object의 소유권을 clientId로 바꿔버립니다
GetComponent<NetworkObject>().ChangeOwnership(clientId);
    1. 서버가 Obejct의 소유권을 없애버립니다
      서버에게 다시 소유권이 돌아옵니다
GetComponent<NetworkObject>().RemoveOwnership();
    1. 기본적인 PlayerObject를 만들어내는데 사용됩니다
GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);

Finding PlayerObjects 특정한 clientId에 해당하는 오브젝트 찾기

NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject

NetworkObject의 상태에 따라 호출되는 함수의 순서

  • 런타임동안 생성되는 NetworkObject들은 Start함수가 호출되기 전에 OnNetworkSpawn함수가 호출됩니다!

  • 따라서!!!! 런타임에 생성되는 NetworkObject에 Start함수에서 상태를 초기화 하면 OnNetworkSpawn함수에 초기화한 값이 설정되지 않기 때문에 주의해야 합니다!

Despawn vs OnDestroy

  • Despawn은 NetworkObject가 de-spawned될 때 관련하여 NetworkBehaviour의OnNetworkDespawn에서 호출됩니다!
    Despawning은 파괴(Destroy)되기 전에 실행합니다!
  • OnDestroy는 Netcode Object를 파괴할 때 발생합니다!

Network Object가 동기화 되는 경우에는 2가지 케이스

  • 런타임에서 NetworkObejct가 생성될 때
  • 클라이언트가 Connection Approval후에 동기화가 필요할 때

NetworkObject가 런타임에서 생성될 때 직렬화되는 과정의 순서에 대한 개요

서버 관점

    1. NetworkObject 컴포넌트가 있는 게임오브젝트가 인스턴스화 됩니다
    1. NetworkObject가 생성됩니다
    • NetworkBehaviour.OnNetworkSpawn이 호출됩니다
    1. CreateObjectMessage가 생성됩니다. (내부적으로 생성)
    • NetworkObject 상태가 직렬화 됩니다
    • NetworkVariable상태가 직렬화 됩니다
    • NetworkBehaviour.OnSynchronize가 호출됩니다
    • NetworkBehaviour.OnSynchronize를 재정의하지 않으면 직렬화 Buffer에 아무것도 기록되지 않습니다!
    • CreateObjectMessage는 NetworkObject의 옵저버인 모든 클라이언트에게 보내집니다

OnSynchronize vs OnDeserialize

  • 코드 예제
using UnityEngine;
using Unity.Netcode;

public class Spawner : MonoBehaviour
{
    public GameObject healthObjectPrefab;

    void Update()
    {
        if (IsServer && Input.GetKeyDown(KeyCode.Space))
        {
            GameObject obj = Instantiate(healthObjectPrefab, Vector3.zero, Quaternion.identity);
            obj.GetComponent<NetworkObject>().Spawn();
            Debug.Log("[Server] Spawned HealthObject");
        }
    }

    private bool IsServer => NetworkManager.Singleton.IsServer;
}
using Unity.Netcode;
using UnityEngine;

public class HealthObject : NetworkBehaviour
{
    public NetworkVariable<int> Health = new NetworkVariable<int>(100);
    private string objectName = "MonsterA";

    public override void OnNetworkSpawn()
    {
        if (IsServer)
        {
            Debug.Log($"[Server] Spawned with health = {Health.Value}");
        }
        else
        {
            Debug.Log($"[Client] Spawned with health = {Health.Value}");
        }
    }

    // 서버 → 클라이언트로 objectName 전송
    public override void OnSynchronize<T>(T writer)
    {
        if (IsServer)
        {
            writer.WriteValueSafe(objectName);
        }
    }

    // 클라이언트에서 수신한 objectName 처리
    public override void OnDeserialize<T>(T reader)
    {
        if (!IsServer)
        {
            objectName = reader.ReadValueSafe<string>();
            Debug.Log($"[Client] Received objectName = {objectName}");
        }
    }
}
    1. 스페이스바를 통해 객체 생성 후 NGO는 서버 권위적이므로 Spawn() 호출
obj.GetComponent  <NetworkObject>().Spawn() 
  • 이 코드는 서버가 오브젝트를 네트워크에 "등록"하고, CreateObjectMessage를 생성해 클라이언트들에게 전파합니다.

  • 이 코드는 서버에서 실행되어야 하고, 서버가 클라이언트들에게 이 오브젝트의 생성 사실을 알리는 역할

    1. Spawn() 후 씬에 객체가 생성되면 -> OnNetworkSpawn() 호출 해서 아래 로그 출력한다. 서버 관점에서 3번이 실행된다. 즉, NetworkVariable 타입인 Health 변수가 직렬화 된다.

  • NGO에서 자동으로 OnSynchronize 호출하고, objectName을 직렬화 하고 CreateObjectMessage 생성한다. CreateObjectMessage 내부에 해당 직렬화 데이터가 포함되어 있다. OnDeserialize() 호출하여 CreateObjectMessage가 클라이언트들에게 전송

즉, 클라이언트 입장에서, 아래 두 로그가 출력되었다.

 Debug.Log($"[Client] Spawned with health = {Health.Value}");
 Debug.Log($"[Client] Received objectName = {objectName}");

클라이언트 관점

    1. CreateObjectMessage를 받습니다
    • NetworkObject 컴포넌트를 가진 게임오브젝트가 인스턴화됩니다
    • NetworkVariable 상태가 역직렬화되고 적용됩니다
    • NetworkBehaviour.OnSynchronize가 호출됩니다
    • NetworkBehaviour.OnSynchronize를 재정의하지 않으면 직렬화 Buffer에 아무것도 기록되지 않습니다!
    1. NetworkObject가 생성됩니다
    • NetworkBehaviour.OnNetworkSpawn이 호출됩니다

결론 : 서버 관점과 클라이언트 관점은 상반된 모습으로 동작한다.

  • 서버는 생성 → 상태 직렬화 → 메시지 생성 → 메시지 전송

  • 클라이언트는 메시지 수신 → 객체 생성 → 상태 역직렬화 → 네트워크 스폰 완료 콜백 실행

업로드중..

뒤 늦게 참가한 클라이언트 동기화 과정 순서

서버 관점

    1. SceneEventType.Synchronize 형태의 SceneEventMessage가 만들어 집니다!
    • 클라이언트에게 보여지는 모든 생성된 NetworkObject는 이미 인스턴화되었고, 직렬화된채로 생성됩니다!
    • NetworkObject 상태는 직렬화 되어 있음
    • NetworkVariable 상태는 직렬화 되어 있음
    • NetworkBehaviour.OnSynchronize가 호출 됨
    • NetworkBehaviour.OnSynchronize를 재정의하지 않으면 직렬화 Buffer에 아무것도 기록되지 않습니다!
    1. SceneEventMessage가 클라이언트에게 보내집니다

클라이언트 관점

    1. SceneEventType.Synchronize 형태의 SceneEventMessage를 받습니다!
    1. Scene의 정보는 이미 역직렬화 되어 있으며 씬도 로드되어 있습니다
    • 씬 안에 놓여 있는 NetworkObject들은 씬이 로드 될 때 인스턴화 됩니다
    1. 모든 NetworkObject는 역직렬화된 Synchroization 정보를 지향합니다
      1. 동적으로 생성되는 NetworkObject는 인스턴화되고 상태가 동기화 됩니다
      1. 각 NetworkObejct 인스턴스들은
      • NetworkVariable상태가 역직렬화되고 적용됩니다
      • NetworkBehaviour.OnSynchronize가 호출됩니다
      • NetworkBehaviour.OnSynchronize를 재정의하지 않으면 직렬화 Buffer에 아무것도 기록되지 않습니다!
      • NetworkObject가 생성됩니다
      • NetworkBehaviour.OnNetworkSpawn이 호출됩니다

출처 : https://wlsdn629.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-Netcode-for-GameObject-Connection-approval

0개의 댓글