유니티 Netcode for GameObject, Synchronizing States & Events

Introduction

 

NGO는 게임 상태를 동기화해주는 옵션들이 있습니다!

  1. RPC(Remote Procedure Calls)
    • Custom Messages
  2. NetworkVariables

 

RPC

ServerRpc는 주로 플레이어가 world object를 사용하기 위해 서버에게 알릴 때 사용됩니다!

ClientRpc는 서버가 플레이어의 동기화될 때 필요하지 않은 정보나 특별한 reconnection key값을  클라이언트들에게 알릴 때 사용됩니다

 

[RPC] 속성을 사용하기 위해서는 함수 접미사에 Rpc를 붙여야합니다!

Custom Messages

Custom Messages는 특별한 시나리오를 처리하기 위해 자신만의 특별한 "netcode message type"을 만들 수 있습니다

 

 

NetworkVariable System

NetworkVarialbe은 주로 게임에 뒤늦게 참여하는 클라이언트들간의 상태를 동기화해주는데 사용되곤 합니다

 

예시

더 구체적인 내용은 아래 포스팅을 참고하시면 좋을 것 같습니다!

2023.03.17 - [Unity - GamingService/Netcode for GameObjects] - 유니티 Netcode for GameObject, NetworkVariable

 

유니티 Netcode for GameObject, NetworkVariable

# NetworkVariable 이란? NetworkVariable 는 RPC나 Custom Message 사용 없이 서버와 클라이언트간에 Variable 값(속성)을 동기화해주는 방법입니다! NetworkVariable 는 제너릭 형태로 값을 저장합니다! NetworkVariable.Va

wlsdn629.tistory.com

 

RPC과 NetworkVariable

잘못된 싱킹 매커니즘을 고르면 버그가 발생하거나, 너무 많은 대역폭을 만들거나, 코드가 복잡해질 수 있습니다

RPC와 NetworkVariable을 결정하는 빠른 방법은 스스로에게 질문하는 것입니다 :

중간에 참여하는 참여자가 동기화를 필요로 하는 정보를 받아야 하는가?

  • RPC를 사용하는 경우는 주로 즉각적인 상황에서 사용됩니다, 즉 폭발과 같이 일회성 요소에 사용되면 좋습니다(etc, door)
  • NetworkVariables를 사용하는 경우는 주로 지속적인 상황에서 사용됩니다, 주로 캐릭터의 움직임에 사용하면 좋습니다

또한, 뒤늦게 참여한 클라이언트 경우 RPC를 사용하여 바꾼 값같은 경우 실시간 공유를 받지 못합니다!

(좌) 호스트에서 문을 열어논 상태 (우) 뒤늦게 참여한 클라이언트의 문 상태
State와 isOpne 변수

그렇다면 항상 실시간 동기화 정보를 받는 NetworkVariable 변수를 사용하면 되지 왜 실시간 공유가 되지 않는 RPC를 써야만 하는 것일까요?

그 이유는 RPC는 NetworkVariable보다 단순하기 때문입니다

그리고 모든 정보를 실시간으로 받을 필요는 없습니다!

예를 들어, 폭발이 일어났다고 가정을 해보겠습니다

폭발이 발생하고 5초 뒤에 참여하게 된 클라이언트가 폭발이 일어난 정보를 필요로 할까요?

아닙니다, 뒤늦게 들어온 클라이언트는 폭발로 인해 데미지를 받아 HP가 변한 클라이언트들의 상태만 동기화 받으면 될 뿐, 폭발이 일어났다는데에 대한 정보는 필요로 하지 않습니다

즉, 모든 것을 실시간으로 동기화할 필요는 없다는 뜻입니다!

 

또한 ServerRpc의 경우 NetworkObject의 소유권을 가진 클라이언트만 호출할 수 있지만

ServerRpc(RequireOwnerShip = false)로 설정하게 되면 소유권이 없는 클라이언트도 ServerRpc 속성을 지닌 함수를 호출할 수 있습니다!

 


Execution Table

gks출처 - https://docs-multiplayer.unity3d.com/netcode/current/advanced-topics/message-system/execution-table

Network Update Loop 인프라구조는 Unity의 낮은 레벨 Player Loop Api를 사용해서 MonoBehavior로 제작된 게임 로직 실행을 하기 전 혹은 한 후 특정한 NetworkUpdateStages에 실행되도록 INetworkUpdateSystems를 NetworkUpdate()함수로 등록하도록 해준다.

 


Custom Messages

만약 NGO 메세지 시스템을 사용하고 싶지 않으면, 사용 안해도 됩니다

그대신 Custom Message를 사용하면 됩니다

 

두 가지 타입의 Custom Message가 있습니다 : 

  • Unnamed
  • Named

Unnamed Messages

unnamed messages는 한 개의 독자적인 채널을 통해 정보를 보내는 것 처럼 생각하면 됩니다

한 unnamed message 당 오직 하나의 receiver 핸들러가 있어서, 메세지 헤더를 정의하는 커스텀 메세징 시스템을 구축할 때 도움이 됩니다

NGO는 커스텀 unnamed message들을 전달하고 받을 수 있으며 채널을 통해 어떤 종류의 정보를 전송하고 싶은지 정하면 됩니다

 

Unnamed Message 예시

아래는 unnamed messages를 이용한 메세징 시스템을 수행할 수 있는 기본적인 예시입니다

using UnityEngine;
using Unity.Collections;
using Unity.Netcode;

/// <summary>
/// Using an unnamed message to send a string message
/// <see cref="CustomUnnamedMessageHandler<T>"/> defined
/// further down below.
/// </summary>
public class UnnamedStringMessageHandler : CustomUnnamedMessageHandler<string>
{
    /// <summary>
    /// We override this method to define the unique message type
    /// identifier for this child derived class
    /// </summary>
    protected override byte MessageType()
    {
        // As an example, we can define message type of 1 for string messages
        return 1;
    }

    public override void OnNetworkSpawn()
    {
        // For this example, we always want to invoke the base
        base.OnNetworkSpawn();

        if (IsServer)
        {
            // Server broadcasts to all clients when a new client connects
            // (just for example purposes)
            NetworkManager.OnClientConnectedCallback += OnClientConnectedCallback;
        }
        else
        {
            // Clients send a greeting string message to the server
            SendUnnamedMessage("I am a client connecting!");
        }
    }

    public override void OnNetworkDespawn()
    {
        // For this example, we always want to invoke the base
        base.OnNetworkDespawn();

        // Whether server or not, unregister this.
        NetworkManager.OnClientDisconnectCallback -= OnClientConnectedCallback;
    }

    private void OnClientConnectedCallback(ulong clientId)
    {
        // Server broadcasts a welcome string message to all clients that
        // a new client has joined.
        SendUnnamedMessage($"Everyone welcome the newly joined client ({clientId})!");
    }

    /// <summary>
    /// For this example, we override this message to handle receiving the string
    /// message.
    /// </summary>
    protected override void OnReceivedUnnamedMessage(ulong clientId, FastBufferReader reader)
    {
        var stringMessage = string.Empty;
        reader.ReadValueSafe(out stringMessage);
        if (IsServer)
        {
            Debug.Log($"Server received unnamed message of type ({MessageType()}) from client " +
                $"({clientId}) that contained the string: \"{stringMessage}\"");

            // As an example, we can also broadcast the client message to everyone
            SendUnnamedMessage($"Newly connected client sent this greeting: \"{stringMessage}\"");
        }
        else
        {
            Debug.Log(stringMessage);
        }
    }

    /// <summary>
    /// For this example, we will send a string as the payload.
    ///
    /// IMPORTANT NOTE: You can construct your own header to be
    /// written for custom message types, this example just uses
    /// the message type value as the "header".  This provides us
    /// with the ability to have "different types" of unnamed
    /// messages.
    /// </summary>
    public override void SendUnnamedMessage(string dataToSend)
    {
        var writer = new FastBufferWriter(1100, Allocator.Temp);
        var customMessagingManager = NetworkManager.CustomMessagingManager;
        // Tip: Placing the writer within a using scope assures it will
        // be disposed upon leaving the using scope
        using (writer)
        {
            // Write our message type
            writer.WriteValueSafe(MessageType());

            // Write our string message
            writer.WriteValueSafe(dataToSend);
            if (IsServer)
            {
                // This is a server-only method that will broadcast the unnamed message.
                // Caution: Invoking this method on a client will throw an exception!
                customMessagingManager.SendUnnamedMessageToAll(writer);
            }
            else
            {
                // This method can be used by a client or server (client to server or server to client)
                customMessagingManager.SendUnnamedMessage(NetworkManager.ServerClientId, writer);
            }
        }
    }
}

/// <summary>
/// A templated class to handle sending different data types
/// per unique unnamed message type/child derived class.
/// </summary>
public class CustomUnnamedMessageHandler<T> : NetworkBehaviour
{
    /// <summary>
    /// Since there is no unique way to identify unnamed messages,
    /// adding a message type identifier to the message itself is
    /// one way to handle know:
    /// "what kind of unnamed message was received?"
    /// </summary>
    protected virtual byte MessageType()
    {
        // The default unnamed message type
        return 0;
    }

    /// <summary>
    /// For most cases, you want to register once your NetworkBehaviour's
    /// NetworkObject (typically in-scene placed) is spawned.
    /// </summary>
    public override void OnNetworkSpawn()
    {
        // Both the server-host and client(s) will always subscribe to the
        // the unnamed message received event
        NetworkManager.CustomMessagingManager.OnUnnamedMessage += ReceiveMessage;
    }

    public override void OnNetworkDespawn()
    {
        // Unsubscribe when the associated NetworkObject is despawned.
        NetworkManager.CustomMessagingManager.OnUnnamedMessage -= ReceiveMessage;
    }

    /// <summary>
    /// This method needs to be overridden to handle reading a unique message type
    /// (that is, derived class)
    /// </summary>
    protected virtual void OnReceivedUnnamedMessage(ulong clientId, FastBufferReader reader)
    {
    }

    /// <summary>
    /// For this unnamed message example, we always read the message type
    /// value to determine if it should be handled by this instance in the
    ///  event it's a child of the CustomUnnamedMessageHandler class.
    /// </summary>
    private void ReceiveMessage(ulong clientId, FastBufferReader reader)
    {
        var messageType = (byte)0;
        // Read the message type value that is written first when we send
        // this unnamed message.
        reader.ReadValueSafe(out messageType);
        // Example purposes only, you might handle this in a more optimal way
        if (messageType == MessageType())
        {
            OnReceivedUnnamedMessage(clientId, reader);
        }
    }

    /// <summary>
    /// For simplicity, the default does nothing
    /// </summary>
    /// <param name="dataToSend"></param>
    public virtual void SendUnnamedMessage(T dataToSend)
    {

    }
}

Named Messages

자체 메세지 시스템을 만드는 복잡함이 싫다면, NGO의 custom named message를 사용하시면 됩니다!!

custom  named message는 메세지 이름을 고유한 id로서 사용합니다(이름으로부터 해시값을 생성하고 메세지 콜백에 연결합니다)

 

커스텀 unnamed messages를 다루는 것 처럼 메세지 identification의 복잡함이 필요한지 아닌지 확실하지 않다면, custom named message를 먼저 사용해보고 나중에, 특정 named message의 하위 메세지가 필요할 때 named message 페이로드 자체에 type identifier를 통합할 수 있습니다!(unnamed message에서 하는 것 처럼)

 

Name Message 예시

using System;
using UnityEngine;
using Unity.Collections;
using Unity.Netcode;
public class CustomNamedMessageHandler : NetworkBehaviour
{
    [Tooltip("The name identifier used for this custom message handler.")]
    public string MessageName = "MyCustomNamedMessage";

    /// <summary>
    /// For most cases, you want to register once your NetworkBehaviour's
    /// NetworkObject (typically in-scene placed) is spawned.
    /// </summary>
    public override void OnNetworkSpawn()
    {
        // Both the server-host and client(s) register the custom named message.
        NetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(MessageName, ReceiveMessage);

        if (IsServer)
        {
            // Server broadcasts to all clients when a new client connects (just for example purposes)
            NetworkManager.OnClientConnectedCallback += OnClientConnectedCallback;
        }
        else
        {
            // Clients send a unique Guid to the server
            SendMessage(Guid.NewGuid());
        }
    }

    private void OnClientConnectedCallback(ulong obj)
    {
        SendMessage(Guid.NewGuid());
    }

    public override void OnNetworkDespawn()
    {
        // De-register when the associated NetworkObject is despawned.
        NetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(MessageName);
        // Whether server or not, unregister this.
        NetworkManager.OnClientDisconnectCallback -= OnClientConnectedCallback;
    }

    /// <summary>
    /// Invoked when a custom message of type <see cref="MessageName"/>
    /// </summary>
    private void ReceiveMessage(ulong senderId, FastBufferReader messagePayload)
    {
        var receivedMessageContent = new ForceNetworkSerializeByMemcpy<Guid>(new Guid());
        messagePayload.ReadValueSafe(out receivedMessageContent);
        if (IsServer)
        {
            Debug.Log($"Sever received GUID ({receivedMessageContent.Value}) from client ({senderId})");
        }
        else
        {
            Debug.Log($"Client received GUID ({receivedMessageContent.Value}) from the server.");
        }
    }

    /// <summary>
    /// Invoke this with a Guid by a client or server-host to send a
    /// custom named message.
    /// </summary>
    public void SendMessage(Guid inGameIdentifier)
    {
        var messageContent = new ForceNetworkSerializeByMemcpy<Guid>(inGameIdentifier);
        var writer = new FastBufferWriter(1100, Allocator.Temp);
        var customMessagingManager = NetworkManager.CustomMessagingManager;
        using (writer)
        {
            writer.WriteValueSafe(messageContent);
            if (IsServer)
            {
                // This is a server-only method that will broadcast the named message.
                // Caution: Invoking this method on a client will throw an exception!
                customMessagingManager.SendNamedMessageToAll(MessageName, writer);
            }
            else
            {
                // This is a client or server method that sends a named message to one target destination
                // (client to server or server to client)
                customMessagingManager.SendNamedMessage(MessageName, NetworkManager.ServerClientId, writer);
            }
        }
    }
}