Custom named messages not working as expected

I’m trying to learn more about multiplayer games by developing my very own game using unity NetCode. The problem started from my need to create my own custom Rpc like communication between the players, as my game is different from the typical ones supported by NetCode.

the idea is for an online 2D mobile “fighting games” between two players. When both of them spawn the server will signal for them to start the game, by each creating a player and enemy object on their screen. When one player swipe on their screen their player object will move, message the server that it moved to the new position, so the server can approve it or correct any illegal move, and update the other layer that their enemy object should move to a new position. By this the server is supposed to ‘sync’ the two players movements, by moving the enemy object according to the other player’s player object movement.

Every thing works fine except the part of updating the enemy of the other player.
I have three custom named channels in this class:

{
    private static PlayerMovementMessages _instance;
    private static readonly object padlock = new object();

    private CustomMessagingManager.HandleNamedMessageDelegate serverApproval;
    private CustomMessagingManager.HandleNamedMessageDelegate playerRequestMove;
    private CustomMessagingManager.HandleNamedMessageDelegate serverUpdate;

    private PlayerMovementMessages() {}

    public static PlayerMovementMessages Instance()
    {
        lock(padlock)
        {
            if(_instance == null)
            {
                _instance = new PlayerMovementMessages();
            }

            return _instance;
        }
    }

    public void setServerReceiver(CustomMessagingManager.HandleNamedMessageDelegate playerRequestMove)
    {
        this.playerRequestMove = playerRequestMove;

        if(this.serverApproval != null && this.playerRequestMove != null && this.serverUpdate != null)
        {
            registerNetMessages();
        }
    }

    public void setPlayerReceiver(CustomMessagingManager.HandleNamedMessageDelegate serverApproval)
    {
        this.serverApproval = serverApproval;

        if(this.serverApproval != null && this.playerRequestMove != null && this.serverUpdate != null)
        {
            registerNetMessages();
        }
    }

    public void setEnemyReceiver(CustomMessagingManager.HandleNamedMessageDelegate serverUpdate)
    {
        this.serverUpdate = serverUpdate;

        if(this.serverApproval != null && this.playerRequestMove != null && this.serverUpdate != null)
        {
            registerNetMessages();
        }
    }

    private void registerNetMessages()
    {
        //note: custome messages are not two-way, so two channels are needed to complete the transaction

        //a channel for the player to request a move from the server
        NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("movePlayer", playerRequestMove);
        //a channel for the server to send to the player if the move was accepted
        NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("approvedMove", serverApproval);
        //a channel for the server to send target position update to the Enemy
        NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("serverUpdate", serverUpdate);

        Debug.Log("Messgaes channels ready");
    }

    public void UnrigesterNetMessages()
    {
        NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler("movePlayer");
        NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler("approvedMove");
        NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler("serverUpdate");
    }

    //syrialize the struct to byte array so it can be send in the custom message
    public static byte[] structToByte(System.Object obj)
    {
        string json = JsonUtility.ToJson(obj);
        return System.Text.Encoding.UTF8.GetBytes(json);
    }

    //desyrialize (turn the byte array) to an object to use what was sent in the custom message
    public static System.Object byteToStruct(byte[] byteArray)
    {
        string json = System.Text.Encoding.UTF8.GetString(byteArray);
        return JsonUtility.FromJson<Vector3>(json);
    }

    //player send through movePlayer channel a move for the server to approve it
    public void requestMove(Vector3 targetPosition)
    {
        
        byte[] serializedData = PlayerMovementMessages.structToByte(targetPosition);

        var manager = NetworkManager.Singleton.CustomMessagingManager;
        var writer = new FastBufferWriter(FastBufferWriter.GetWriteSize(serializedData), Allocator.Temp);

        using(writer)
        {
            writer.WriteValueSafe(serializedData);
            //send to server only once
            manager.SendNamedMessage("movePlayer", NetworkManager.ServerClientId , writer, NetworkDelivery.Reliable);
        }
    }

    //server sends through approvedMove the move ot approved to the player
    public void approveMove(Vector3 approved, ulong clientID, ulong enemyID)
    {
        byte[] serializedData = PlayerMovementMessages.structToByte(approved);

        var manager = NetworkManager.Singleton.CustomMessagingManager;
        var writer = new FastBufferWriter(FastBufferWriter.GetWriteSize(serializedData), Allocator.Temp);

        using(writer)
        {
            writer.WriteValueSafe(serializedData);
            //send to Client only once
            manager.SendNamedMessage("approvedMove", clientID , writer, NetworkDelivery.Reliable);
        }

        updateEnemyMove(approved, enemyID);
    }

    //server sends to the Enemy an update of the target position
    //after the player requested a move
    public void updateEnemyMove(Vector3 newPosition, ulong enemyID)
    {
        Debug.Log("server sent update to enemy " + enemyID);

        newPosition.y = 2.7f;

        byte[] serializedNewPosition = structToByte(newPosition);

        var manager = NetworkManager.Singleton.CustomMessagingManager;
        var writer = new FastBufferWriter(FastBufferWriter.GetWriteSize(serializedNewPosition), Allocator.Temp);

        using(writer)
        {
            writer.WriteValueSafe(serializedNewPosition);
            //send to Client only once
            manager.SendNamedMessage("serverUpdate", enemyID , writer, NetworkDelivery.Reliable);
        }
    }
}

this is the relevant code for the server

public class Server : NetworkBehaviour
{
    private List<ulong> players = new List<ulong>();
}
 public override void OnNetworkSpawn()
    {
        base.OnNetworkSpawn();

        if(!IsServer)
        {
            this.enabled = false;
            return;
        }

        //Debug.Log("server spawned");
        NetworkManager.Singleton.OnClientConnectedCallback += OnPlayerJoined;
        PlayerMovementMessages.Instance().setServerReceiver(playerRequestMove);
    }
public void playerRequestMove(ulong senderID, FastBufferReader messagePayload)
    {
        if(!IsServer)
            return;

        Vector3 requested;

        byte[] reseivedData;
        messagePayload.ReadValueSafe(out reseivedData);

        requested = (Vector3)PlayerMovementMessages.byteToStruct(reseivedData);
        ulong enemyID = getEnemyID(senderID);

        if(isIligibale(requested))
        {
            Debug.Log("server approved");
        }else
        {
            Debug.Log("server did not approved");
            requested = correctPlayerPosition(requested);
        }

        PlayerMovementMessages.Instance().approveMove(requested, senderID, enemyID);
    }

    private ulong getEnemyID(ulong senderID)
    {
        if(players.Count <2)
        {
            Debug.Log("*** server don't have two players in it's list ***");
            return 0;
        }

        if(senderID == players[0])
            return players[1];
        else
            return players[0];
    }

this is the enemy script:

public class NetEnemy : MonoBehaviour
{

    private Vector3 targetPosition;

    void Start()
    {
        PlayerMovementMessages.Instance().setEnemyReceiver(serverUpdate);

        targetPosition = transform.position;
    }

//Enemy receive target position update from the server
    private void serverUpdate(ulong senderId, FastBufferReader messagePayload)
    {
        Debug.Log("enemy received update from " + senderId);
        
        byte[] reseivedData;
        messagePayload.ReadValueSafe(out reseivedData);

        Vector3 newPosition = (Vector3)PlayerMovementMessages.byteToStruct(reseivedData);

        updateTargetPosition(newPosition);
    }

    private void updateTargetPosition(Vector3 newPosition)
    {
        targetPosition = newPosition;
        Debug.Log(NetworkManager.Singleton.LocalClientId + " enemy target position are now " + targetPosition);
    }

the weird thing is when I test the script where I pass the enemyId to the enemy update function as the same clientID ( so updating the enemy of same player 0 who made the move) it works, and the enemy moves with the player object (with all the correct Debug.Log prints), but when I try to pass the enemyID of the other player, the Debug prints stop at ‘server sent to update enemy 1’, and the enemy simply don’t receive any messages (the function doesn’t run). That’s why I believe I’m using the custom messages wrong.

Can someone please help me understand what I’m doing wrong? and how to achieve what I’m looking for?

Hi @issameral,

I think you meant to post this in the Netcode for GameObjects section?

With custom messages you indeed need to have two unique channels if you want bi-directional communication. However, I think you might find it less complicated to convert your custom messages over to be RPCs as those automatically target a specific instance and at that point you are

Here is some pseudo code to provide an example:
Approach 1

    public class NetEnemy : NetworkBehaviour
    {
        private Vector3 m_TargetPosition;
        private void Start()
        {
            m_TargetPosition = transform.position;
        }

        protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
        {
            // Optional to serialize this information with the instance in the event you
            // need late joining clients to update based on this.
            serializer.SerializeValue(ref m_TargetPosition);
            base.OnSynchronize(ref serializer);
        }

        // RPC is sent by player (owner) to the server.
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
        private void PlayerRequestMoveRpc(Vector3 position, ulong enemyId, RpcParams rpcParams = default)
        {
            var senderId = rpcParams.Receive.SenderClientId;

            // Verify sender or the like here
            var isEligible = OwnerClientId != senderId; // Generic check that the sender is indeed the owner
            if (isEligible)
            {
                var distance = m_TargetPosition - position;
                if (distance.magnitude != 0)
                {
                    m_TargetPosition = position;
                }
            }
        }
    }

The ideas with the above are:

  • The owner of the enemies invokes PlayerRequestMoveRpc that moves the AI (that follows the player).
  • The server is the only receiver of this RPC message.
  • It does not require registering all of the custom messages.
  • It sends much less data than the Json serialized approach.
  • It reduces the complexity of having to know where an issue might reside.

Approach 2
If you need the enemy to be a MonoBehaviour for architectural purposes:

    public class Player : NetworkBehaviour
    {
        // Requires populating this table
        private Dictionary<ulong, NetEnemy> m_PlayerCompanions = new Dictionary<ulong, NetEnemy>();

        // RPC is sent by player (owner) to the server.
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
        private void PlayerRequestMoveRpc(Vector3 position, ulong enemyId, RpcParams rpcParams = default)
        {
            var senderId = rpcParams.Receive.SenderClientId;

            if (m_PlayerCompanions.ContainsKey(enemyId))
            {
                m_PlayerCompanions[enemyId].UpdateTargetPosition(position);
            }
        }
    }

    public class NetEnemy : MonoBehaviour
    {
        private Vector3 m_TargetPosition;
        private void Start()
        {
            m_TargetPosition = transform.position;
        }

        internal void UpdateTargetPosition(Vector3 targetPosition)
        {
            var distance = m_TargetPosition - targetPosition;
            if (distance.magnitude != 0)
            {
                m_TargetPosition = targetPosition;
            }
        }
    }

However, the question should be raised as to how you want to have the enemies’ change in position updated for all players?

Both your custom message implementation and the proposed adjustment above (which was based on your implementation) seems to only handle moving the enemies (player companions?) locally and does not include how the enemies’ motion is synchronized.

Either way, you could add a NetworkTransform to each enemy instance… which if you do that then you should just use the 1st approach (above) as it reduces the code down to a single RPC call from the client/player that owns the enemies/companions.

Custom messages can be useful, but for this particular task it will be more complex to write, troubleshoot, and will consume more bandwidth than using something like the 1st approach where the enemies are spawned and have a NetworkBehaviour which contains an RPC.

Let me know if any of this helps you with your project’s goals?

Thanks so much for the repay.

The idea is to sync each player with the other’s enemy. So each player play with the player object from their POV.

What I’m trying to do is when player 0 move, the player object for player 0 moves, and the enemy object for player 1 moves.
And when player 1 moves, it’s player object move, and the enemy object for player 0 moves.

My problem with Rpc is they’re supposed to be “sent” and “received” (I mean executed) in the same script, from what I know. so I can’t have a defecated server, player, and enemy scripts, and have them separated and attached to different objects.

what pushed me to use custom messages, is that I want both players to have player objects that reflect their movement to them in the bottom of the screen, and an enemy object reflecting the other player movement in the top of the screen. And this being both players view.

Hi. So I solved it Guys :sob:
ok so someone mentioned that it channels may not be registered for the other player, and after some testing and debugging, it turns out this is the case.

The only thing I changed is the CustomeMessageManager class, so it handles the server and clients seperately. And it make sure that both clients register the messages channels.

public void setServerReceiver(CustomMessagingManager.HandleNamedMessageDelegate playerRequestMove)
    {
        this.playerRequestMove = playerRequestMove;
        Debug.Log(NetworkManager.Singleton.LocalClientId + " submited server reciever");
        // if(this.serverApproval != null && this.playerRequestMove != null && this.serverUpdate != null)
        // {
        //     registerNetMessages();
        // }
        if(NetworkManager.Singleton.IsServer)
        {
            registerNetMessages();
        }
    }

    public void setPlayerReceiver(CustomMessagingManager.HandleNamedMessageDelegate serverApproval)
    {
        this.serverApproval = serverApproval;
        Debug.Log(NetworkManager.Singleton.LocalClientId + " submited player reciever");
        if(this.serverApproval != null && this.serverUpdate != null)
        {
            registerNetMessages();
        }
    }

    public void setEnemyReceiver(CustomMessagingManager.HandleNamedMessageDelegate serverUpdate)
    {
        this.serverUpdate = serverUpdate;
        Debug.Log(NetworkManager.Singleton.LocalClientId + " submited enemy reciever");
        if(this.serverApproval != null && this.serverUpdate != null)
        {
            registerNetMessages();
        }
    }

    private void registerNetMessages()
    {
        //note: custome messages are not two-way, so two channels are needed to complete the transaction

        //a channel for the player to request a move from the server
        NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("movePlayer", playerRequestMove);
        //a channel for the server to send to the player if the move was accepted
        NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("approvedMove", serverApproval);
        //a channel for the server to send target position update to the Enemy
        NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("serverUpdate", serverUpdate);

        Debug.Log("Messgaes channels ready for player " + NetworkManager.Singleton.LocalClientId);
    }

and this miraclesly worked with not problems :face_holding_back_tears: