유니티 Netcode for GameObject, NetworkVariable

NetworkVariable 이란?

NetworkVariable 는 RPC나 Custom Message 사용 없이 서버와 클라이언트간에 값을 동기화해주는 방법입니다!
NetworkVariable <T>제너릭 형태로 값을 저장합니다!
 

NetworkVariable.Value가 동기화 되는 상황

  • 뒤 늦게 참여하는 클라이언트 : 자동으로 NetworkVarialbe 값이 동기화 됩니다
  • 연결 된 클라이언트 : Network.OnvalueChanged event구독중인 클라이언트는 변화됨을 알림을 받습니다. 
    • Network.OnvalueChanged구독중인  callback 함수는 2개의 파라미터를 전달 받습니다
      1. First Parameter(Previous) : 변화되기 전의 값
      2. Second Parameter(Current) : 새롭게 변화된 NetworkVariable.Value

 

NetworkVariable General Requirements /호출 순서

  • NetworkVariable은 반드시 NetworkBehaviour 내에 정의되어야만 하며 NetworkObject컴포넌트가 붙어있어야 합니다!
  • NetworkVariable의 값은 오직 값을 초기화 할 때와 NetworkObject가 생성되어 있는 동안에만 설정할 수 있습니다!

 
클라이언트의 NetworkVariable의 값은 처음 연결할 때 자동으로 동기화 됩니다

일반적으로, 클라이언트는OnNetworkSpawn 메서드와 함께  NetworkVariable.OnValueChanged 이벤트에 등록합니다

  • NetworkObject가 씬에 놓여있을 때는 OnNetworkSpawn수 보다 Start함수가 먼저 호출됨
  • NetworkObject가 동적으로 생성될 때는 Start함수보다 OnNetworkSpawn함수가 먼저 호출됨

예를 들자면, 씬에 배치되어 있는 Ball이라는 Netcode 오브젝트에는 OnNetworkSpawn함수와  Start함수가 있는 스크립트가 부착되어 있으며

NetworkManager를 통해 Spawn되는 Player프리팹에도 마찬가지로 OnNetworkSpawn함수와  Start함수가 있는 스크립트가 부착되어 있는 상태입니다
 
에디터를 실행시키면

이렇게 Scene에 놓여 있는 Ball의 Start함수가 먼저 실행됩니다!

서버를 시작하면 Spawn된 Player 프리팹의 OnNetworkSpawn함수가 실행된 후
Ball의 OnNetworkSpawn함수가 실행되며
마지막으로  Spawn된 Player 프리팹의 Start함수 실행되는 것을 볼 수 있습니다!
 


Synchronization and Notification Example

다음 코드는 NetworkVariable이 어떻게 동기화 되는지에 대한 예시입니다!

NetworkVariable.OnValueChanged에 구독하는 방법도 알려주며,  m_SomeValue.value의 값이 바뀔 때 OnNetworkSpawn내에 있는 NetworkVariable.OnValueChanged가 변경 사항에 대한 알림을 제공합니다!

public class TestNetworkVariableSynchronization : NetworkBehaviour
{
   private NetworkVariable<int> m_SomeValue = new NetworkVariable<int>();
   private const int k_InitialValue = 1111;

   public override void OnNetworkSpawn()
   {
       if (IsServer)
       {
           m_SomeValue.Value = k_InitialValue;
           NetworkManager.OnClientConnectedCallback += NetworkManager_OnClientConnectedCallback;
       }
       else
       {
           if (m_SomeValue.Value != k_InitialValue)
           {
               Debug.LogWarning($"NetworkVariable was {m_SomeValue.Value} upon being spawned" +
                   $" when it should have been {k_InitialValue}");
           }
           else
           {
               Debug.Log($"NetworkVariable is {m_SomeValue.Value} when spawned.");
           }
           m_SomeValue.OnValueChanged += OnSomeValueChanged;
       }
   }

   private void NetworkManager_OnClientConnectedCallback(ulong obj)
   {
       StartCoroutine(StartChangingNetworkVariable());
   }

   private void OnSomeValueChanged(int previous, int current)
   {
       Debug.Log($"Detected NetworkVariable Change: Previous: {previous} | Current: {current}");
   }

   private IEnumerator StartChangingNetworkVariable()
   {
       var count = 0;
       var updateFrequency = new WaitForSeconds(0.5f);
       while (count < 4)
       {
           m_SomeValue.Value += m_SomeValue.Value;
           count++;
           yield return updateFrequency;
       }
       NetworkManager.OnClientConnectedCallback -= NetworkManager_OnClientConnectedCallback;
   }
}

위 예시는 서버클라이언트 관점에서 바라봐야 합니다
서버는 NetworkObject가 생성될 때 NetworkVariable를 초기화하며
 
클라이언트는 NetworkVariable.OnValueChanged callback함수에 등록하고 서버에 의해 최초로 값이 설정되었을 때 NetworkVariable이 동기화 됨을 확인 받습니다! 
 
위 예제를 부착한 Player프리팹을 NetworkManager의 Player Prefab에 넣습니다



OnValueChanged에 구독하지 않으면 값이 변경되도 그에 대한 Message를 받지 못합니다

OnClientConnectedCallback에 구독하지 않으면 클라이언트가 서버에 접속했을 때 Message를 받지 못합니다
 


OnValueChanged Example

public class Door : NetworkBehaviour
{
    public NetworkVariable<bool> State = new NetworkVariable<bool>();

    public override void OnNetworkSpawn()
    {
        State.OnValueChanged += OnStateChanged;
    }

    public override void OnNetworkDespawn()
    {
        State.OnValueChanged -= OnStateChanged;
    }

    public void OnStateChanged(bool previous, bool current)
    {
        // note: `State.Value` will be equal to `current` here
        if (State.Value)
        {
            // door is open:
            //  - rotate door transform
            //  - play animations, sound etc.
        }
        else
        {
            // door is closed:
            //  - rotate door transform
            //  - play animations, sound etc.
        }
    }

    [ServerRpc(RequireOwnership = false)]
    public void ToggleServerRpc()
    {
        // this will cause a replication over the network
        // and ultimately invoke `OnValueChanged` on receivers
        State.Value = !State.Value;
    }
}

위 예시는 소유권이 없는(비소유권) 서버 RPC를 사용합니다

Door의 상태를 실시간으로 공유받을 수 있습니다!
 

Permissions

그렇다면 누구나 NetworkVariable에 접근해서 값을 Read하거나 Write하게 할 수 있는 것일까요?

public NetworkVariable(T value = default, 
NetworkVariableReadPermission readPerm = NetworkVariableReadPermission.Everyone, 
NetworkVariableWritePermission writePerm = NetworkVariableWritePermission.Server);

위 NetworkVariable 생성자를 보시다시피 쓰고/읽기에 대한 설정이 가능합니다

위 예시는 Read는 누구나 Write는 Server만 가능하게 하는 예시입니다!
 
NGO는 서버 권위적이므로 항상 서버는 Read & Write가 가능합니다!

    /// <summary>
    /// The permission types for reading a var
    /// </summary>
    public enum NetworkVariableReadPermission
    {
        /// <summary>
        /// Everyone can read
        /// </summary>
        Everyone,
        /// <summary>
        /// Only the owner and the server can read
        /// </summary>
        Owner,
    }

    /// <summary>
    ///  The permission types for writing a var
    /// </summary>
    public enum NetworkVariableWritePermission
    {
        /// <summary>
        /// Only the server can write
        /// </summary>
        Server,
        /// <summary>
        /// Only the owner can write
        /// </summary>
        Owner
    }

위 코드는 ReadPermission Type에는 Everyone, Owner 로 나뉘며 WritePermission Type에는 Server, Owner 로 나뉩니다!
 
Everyone은 누구든지 값을 읽을 수 있다는 점이며, Owner는 자신만이 값을 읽을 수 있다는 점입니다
Everyone같은 경우 Player Scroe, Health 등 상태 공유가 필요한 값에 쓰일 수 있으며(위 Door예시도 포함)
Owner는 총알 개수, 인벤토리 등 공개되어선 안되는 값에 쓰이면 됩니다
 
Server는 서버만이 값을 Write할 수 있다는 점입니다, 예를 들어 NPC의 체력, 생존 등 다른 클라이언트들이 건드려선 안되는 민감한 값을 처리할 때 쓰이면 됩니다
Owner는 자신이 값을 Write할 수 있다는 점이며, 플레이어의 Skin 코스튬 등의 값을 처리할 때 사용하면 됩니다!


Permissions Example

 
위 예시는 플레이어의 상태에 따라 다뤄볼만한 몇 개의 Permission 예시입니다!
그렇다면 위 Door 예시에서는 왜 ServerRPC를 이용하여 클라이언트들에게 문의 열고 닫힌 상태를 알려주었을까요?
기본적으로 Player 오브젝트가 아닌 Object들의 소유권은 주로 Server에게 있습니다
Server에서 State를 처리하는 것이 가장 적절한 경우여서  RPC를 통해 처리한 것입니다!
 

씬에 배치 되어 상호작용할 수 있는 오브젝트들은 주로 Server가 Owner가 되므로 ServerRPC를 통해 State를 관리하는 것이 좋다!

클라이언트들이 조종하는 Player Object는 Permission을 통해 State를 공유하는 것이 맞다!
 


String

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

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

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

public class TestFixedString : NetworkBehaviour
{
    /// Create your 128 byte fixed string NetworkVariable
    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)
        {
            // Assin the current value based on the current message index value
            m_TextString.Value = m_Messages[m_MessageIndex];
        }
        else
        {
            // Subscribe to the OnValueChanged event
            m_TextString.OnValueChanged += OnTextStringChanged;
            // Log the current value of the text string when the client connected
            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)
    {
        // Just log a notification when m_TextString changes
        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를 누를 때마다 클라이언트들은 위와 같은 메세지를 순차적으로 받는다!