Error sending the message: NetworkId is invalid, likely caused by stale connection 0.

Hi, in my multiplayer game a player can have de character that shoot projectil. To do that I originaly Spawn the projectil when needed but it resulte a delay for the player befor the projectil is visible.

To improve that I tried to make an objectPooling système.
There is the code :

using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;

public class NetWorkObjectPooling : NetworkBehaviour
{
    public static NetWorkObjectPooling Instance;
    private Dictionary<string, Queue<NetworkObject>> poolDictionary;

    private void Awake()
    {
        if (Instance != null)
        {
            Debug.Log("Found more than one NetworkObjectPooling, new is destroyed");
            Destroy(this.gameObject);
            return;
        }
        Instance = this;
        poolDictionary = new Dictionary<string, Queue<NetworkObject>>();
        DontDestroyOnLoad(this.gameObject);
    }

    public void CreatePool(string tag, GameObject prefab, int size)
    {
        if (!IsServer) return;
        Queue<NetworkObject> objectPool = new Queue<NetworkObject>();
        List<ulong> objectIDs = new List<ulong>();

        for (int i = 0; i < size; i++)
        {
            var obj = Instantiate(prefab);
            obj.gameObject.SetActive(false);
            NetworkObject objNetwork = obj.GetComponent<NetworkObject>();
            if (objNetwork != null)
            {
                objNetwork.Spawn();
                objectPool.Enqueue(objNetwork);
                objectIDs.Add(objNetwork.NetworkObjectId);
            }
            else
            {
                Destroy(obj);
            }
        }

        poolDictionary.Add(tag, objectPool);
        AddToPoolClientRpc(tag, objectIDs.ToArray());
    }

    [ClientRpc]
    public void AddToPoolClientRpc(string tag, ulong[] objectIDs)
    {
        if (IsServer) return;

        Queue<NetworkObject> objectPool = new Queue<NetworkObject>();
        foreach (ulong id in objectIDs)
        {
            NetworkObject networkObject = NetworkManager.Singleton.SpawnManager.SpawnedObjects[id];
            if (networkObject != null)
            {
                networkObject.gameObject.SetActive(false);
                objectPool.Enqueue(networkObject);
            }
        }
        poolDictionary.Add(tag, objectPool);
    }

    public NetworkObject GetFromPool(string tag, Vector3 position, Quaternion rotation)
    {
        if (!poolDictionary.ContainsKey(tag))
        {
            Debug.LogWarning("Pool with tag " + tag + " doesn't exist.");
            return null;
        }

        NetworkObject objectToSpawn = poolDictionary[tag].Dequeue();
        DequeueServerRpc(tag, NetworkManager.Singleton.LocalClientId);
        objectToSpawn.transform.position = position;
        objectToSpawn.transform.rotation = rotation;
        objectToSpawn.gameObject.SetActive(true);
        objectToSpawn.transform.parent = null; // Unparent the object if it has a parent

        return objectToSpawn;
    }

    [ServerRpc(RequireOwnership = false)]
    private void DequeueServerRpc(string tag, ulong clientId)
    {
        DequeueClientRpc(tag, clientId);
    }

    [ClientRpc]
    private void DequeueClientRpc(string tag, ulong clientId)
    {
        if (NetworkManager.Singleton.LocalClientId == clientId) return;
        poolDictionary[tag].Dequeue();
    }

    public void ReturnToPool(string tag, GameObject objectToReturn)
    {
        if (!poolDictionary.ContainsKey(tag))
        {
            Debug.LogWarning("Pool with tag " + tag + " doesn't exist.");
            Destroy(objectToReturn.gameObject); // Si le pool n'existe pas, détruisez l'objet
            return;
        }

        objectToReturn.gameObject.SetActive(false);
        NetworkObject networkObj = objectToReturn.GetComponent<NetworkObject>();
        if (networkObj != null)
        {
            poolDictionary[tag].Enqueue(networkObj);
            EnqueueServerRpc(tag, NetworkManager.Singleton.LocalClientId, networkObj.NetworkObjectId);
        }
        else
        {
            Debug.LogWarning("Object is not on the network, we destroy it");
            Destroy(objectToReturn.gameObject); // détruisez l'objet
        }
    }

    [ServerRpc(RequireOwnership = false)]
    private void EnqueueServerRpc(string tag, ulong clientId, ulong objectId)
    {
        EnqueueClientRpc(tag, clientId, objectId);
    }

    [ClientRpc]
    private void EnqueueClientRpc(string tag, ulong clientId, ulong objectId)
    {
        if (NetworkManager.Singleton.LocalClientId == clientId) return;

        NetworkObject networkObject = NetworkManager.Singleton.SpawnManager.SpawnedObjects[objectId];
        if (networkObject != null)
        {
            poolDictionary[tag].Enqueue(networkObject);
        }
    }
}

I supposed something is not do well because know if I want to sent RPC to an pooled object I got this message error :

Error sending the message: NetworkId is invalid, likely caused by stale connection 0.
UnityEngine.Debug:LogError (object)
Unity.Netcode.Transports.UTP.UnityTransport:SendBatchedMessages (Unity.Netcode.Transports.UTP.UnityTransport/SendTarget,Unity.Netcode.Transports.UTP.BatchedSendQueue) (at Library/PackageCache/com.unity.netcode.gameobjects@1.0.2/Runtime/Transports/UTP/UnityTransport.cs:681)
Unity.Netcode.Transports.UTP.UnityTransport:Update () (at Library/PackageCache/com.unity.netcode.gameobjects@1.0.2/Runtime/Transports/UTP/UnityTransport.cs:829)

For exemple with the missileScript :

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using static PlayerMovement;
using static Unity.VisualScripting.Member;

public class Missile :  NetworkBehaviour
{
    [SerializeField] float speed;

    public Vector3 dir;
    public int damageOnHit;
    [SerializeField] Rigidbody rb;
    private Health ownerHealth;
    [SerializeField] GameObject visualPart;
    [SerializeField] GameObject hitGameObject;

    [SerializeField] float timeBeforDespawn = 0f;
    [SerializeField] bool despawnTrigger = false;
    [SerializeField] bool hitTrigger = false;
    float currentTimer = 0f;
    private bool firstframe = true;
    private bool init = false;

    FixedString64Bytes localIdentificatorID;
    private void Start()
    {
        Debug.Log("Missile spawn");
        Debug.Log("dir :" + dir.ToString());
    }
    public void InitLocalyForTestOnly(Vector3 pos, Vector3 direction, int damageonHit, FixedString64Bytes identificatorID)
    {
        localIdentificatorID = identificatorID;
        this.transform.position = pos;
        init = true;
        dir = direction;
        this.damageOnHit = damageonHit;
        if (IdentificatorManager.instance != null)
        {
            GameObject obj = IdentificatorManager.instance.GetFromID(identificatorID.ToString());
            if (obj != null)
            {
                ownerHealth = obj.GetComponent<Health>();
                if (ownerHealth == null)
                {
                    ownerHealth = obj.GetComponentInChildren<Health>();
                }
            }
        }

    }

    public void Init(Vector3 pos, Vector3 direction, int damageonHit, FixedString64Bytes identificatorID)
    {
        if(NetworkManager.IsServer)
        {
            TestClientRpc();
            InitClientRpc(pos, direction, damageonHit, identificatorID);
        }
        else
        {
            InitServerRpc(pos, direction, damageonHit, identificatorID);
        }
    }
    [ServerRpc]
    public void InitServerRpc(Vector3 pos, Vector3 direction, int damageonHit, FixedString64Bytes identificatorID)
    {
        InitClientRpc(pos, direction, damageonHit, identificatorID);
    }

    [ClientRpc]
    public void TestClientRpc()
    {
        Debug.Log("Thats working");
    }
    [ClientRpc]
    public void InitClientRpc(Vector3 pos, Vector3 direction, int damageonHit, FixedString64Bytes identificatorID)
    {
        this.transform.position = pos;
        init = true;
        dir = direction;
        this.damageOnHit = damageonHit;
        if(IdentificatorManager.instance != null)
        {
            GameObject obj = IdentificatorManager.instance.GetFromID(identificatorID.ToString());
            if (obj != null)
            {
                ownerHealth = obj.GetComponent<Health>();
                if (ownerHealth == null)
                {
                    ownerHealth = obj.GetComponentInChildren<Health>();
                }
            }
        }
       
    }


    // Update is called once per frame
    void Update()
    {
        Debug.Log("POS" + this.transform.position.ToString());
        if (!init)
        {  
            return;
        }
        if(firstframe && init)
        {
            firstframe = false;
            visualPart.SetActive(true);
        }

        if (despawnTrigger) return;
        if(hitTrigger)
        {
            rb.velocity = new Vector2(0, 0);
            currentTimer += Time.deltaTime;
            if(currentTimer > timeBeforDespawn)
            {
                SyncActiveState syncScript = GetComponentInParent<SyncActiveState>();
                this.gameObject.SetActive(false);
                syncScript.SetActiveState(false);
                if (IsServer && gameObject.GetComponent<NetworkObject>().IsSpawned)
                {
                    Debug.Log("Despawn : " + this.gameObject.name);
                   
                    gameObject.GetComponent<NetworkObject>().Despawn();
                    despawnTrigger = true;
                }
            }
            return;
        }
        rb.velocity = dir*speed;
       
    }
    [ClientRpc]
    public void STOPClientRpc()
    {
        SyncActiveState syncScript = GetComponentInParent<SyncActiveState>();
        if (syncScript != null)
        {
            hitTrigger = true;
            currentTimer = 0f;
            visualPart.SetActive(false);

            if (hitGameObject != null) hitGameObject.SetActive(true);
            //this.gameObject.SetActive(false);
            //
            //if (IsServer && gameObject.GetComponent<NetworkObject>().IsSpawned)
            //{
            //    Debug.Log("Despawn : " + this.gameObject.name);
            //   
            //    gameObject.GetComponent<NetworkObject>().Despawn();
            //
            //}
        }
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(this.transform.position, 1.3f);
    }

    public void STOP(Collider collision)
    {
        SyncActiveState syncScript = GetComponentInParent<SyncActiveState>();
        if (syncScript != null)
        {
            hitTrigger = true;
            currentTimer = 0f;
            visualPart.SetActive(false);

            if (hitGameObject != null)
            {
                // Récupérer le point de collision et faire un raycast pour obtenir la normale
                Vector3 hitPosition = collision.ClosestPoint(transform.position);
                Vector3 hitNormal = Vector3.up; // Valeur par défaut si le raycast échoue

                RaycastHit hitInfo;
                if (Physics.Raycast(transform.position, (hitPosition - transform.position).normalized, out hitInfo))
                {
                    hitNormal = hitInfo.normal;
                }

                hitGameObject.SetActive(true);
                hitGameObject.transform.position = hitPosition;
                hitGameObject.transform.rotation = Quaternion.LookRotation(hitNormal);
            }



            if (IsServer)
            {
                STOPClientRpc();

                // Remplacer Physics2D.OverlapCircleAll par Physics.OverlapSphere pour la détection 3D
                Collider[] hitColliders = Physics.OverlapSphere(this.transform.position, 1.3f);

                foreach (Collider hitCollider in hitColliders)
                {
                    Health otherHealth = hitCollider.gameObject.GetComponent<Health>();
                    if (otherHealth != null)
                    {
                        if (ownerHealth != null && ownerHealth.gameObject.CompareTag("Player") && otherHealth.gameObject.CompareTag("Player") && MultiplayerManager.instance != null && MultiplayerManager.instance.GetDifficulty() == 0) continue;
                        else if (ownerHealth != null) otherHealth.Damage(damageOnHit, ownerHealth.transform);
                        else otherHealth.Damage(damageOnHit, this.transform);
                    }
                }

                if (IsServer && NetWorkObjectPooling.Instance != null)
                {
                    NetWorkObjectPooling.Instance.ReturnToPool("Projectile",this.gameObject);
                }
            }
            // Désactiver l'objet si nécessaire (commenté)
            // this.gameObject.SetActive(false);

            // Désancrer l'objet réseau si nécessaire (commenté)
            // if (IsServer && gameObject.GetComponent<NetworkObject>().IsSpawned)
            // {
            //     Debug.Log("Despawn : " + this.gameObject.name);
            //     gameObject.GetComponent<NetworkObject>().Despawn();
            // }
        }
    }

    void OnTriggerEnter(UnityEngine.Collider collision)
    {
        if (hitTrigger) return;

        // Récupérer le composant Health sur l'objet collisionné ou ses enfants
        Health otherHealth = collision.gameObject.GetComponent<Health>();
        if (otherHealth == null)
        {
            otherHealth = collision.gameObject.GetComponentInChildren<Health>();
        }

        // Vérifier si le composant Health trouvé est celui du propriétaire
        if (otherHealth != null && ownerHealth != null && otherHealth.gameObject == ownerHealth.gameObject)
        {
            return;
        }

        // Si un composant Health valide est trouvé, effectuer une action
        if (otherHealth != null)
        {
            // Remplacez STOP() par l'action désirée, comme infliger des dégâts
            // if (IsHost) otherHealth.Damage(damageOnHit, this.transform);
            STOP(collision);
        }

        // Si l'objet collisionné a certains tags spécifiques, effectuer une action
        if (collision.gameObject.tag == "Border" || collision.gameObject.tag == "Deco" || collision.gameObject.tag == "Wall")
        {
            STOP(collision);
        }
    }

    public override void OnNetworkDespawn()
    {
        // Logique de nettoyage ou préparation avant destruction
        base.OnNetworkDespawn(); // Appel à la base si nécessaire
        Debug.Log("Destroy" + this.gameObject.name);
        Destroy(gameObject);
    }
}

If i call the InitLocalyForTestOnly I have no problem and the missile do well localy, but if I call the Init which sendRPC, I have the error.
TestClientRpc(); produce an error as well.

The polled objects are visible In the scene but it’s seems not initialize correctly, for exemple I added those debug :

Debug.Log("local isServer :" +IsServer.ToString());
Debug.Log("local isClient :" +IsClient.ToString());
Debug.Log("local isOwner  :" +IsOwner.ToString());
Debug.Log("local isHost   :" + IsHost.ToString());


local isServer :False
UnityEngine.Debug:Log (object)
Missile:Init (UnityEngine.Vector3,UnityEngine.Vector3,int,Unity.Collections.FixedString64Bytes) (at Assets/Missile.cs:58)
AttackScript:AttackPrefab (UnityEngine.Vector3,PlayerData/WeaponType) (at Assets/AttackScript.cs:441)
PlayerAction:Attack () (at Assets/PlayerAction.cs:84)
PlayerAction:<Start>b__14_0 () (at Assets/PlayerAction.cs:58)
UnityEngine.Events.UnityEvent:Invoke ()
RPGCharacterAnims.RPGCharacterAnimatorEvents:Hit () (at Assets/ExplosiveLLC/RPG Character Mecanim Animation Pack/Code/RPGCharacterAnimatorEvents.cs:33)

local isClient :False
UnityEngine.Debug:Log (object)
Missile:Init (UnityEngine.Vector3,UnityEngine.Vector3,int,Unity.Collections.FixedString64Bytes) (at Assets/Missile.cs:59)
AttackScript:AttackPrefab (UnityEngine.Vector3,PlayerData/WeaponType) (at Assets/AttackScript.cs:441)
PlayerAction:Attack () (at Assets/PlayerAction.cs:84)
PlayerAction:<Start>b__14_0 () (at Assets/PlayerAction.cs:58)
UnityEngine.Events.UnityEvent:Invoke ()
RPGCharacterAnims.RPGCharacterAnimatorEvents:Hit () (at Assets/ExplosiveLLC/RPG Character Mecanim Animation Pack/Code/RPGCharacterAnimatorEvents.cs:33)

local isOwner  :False
UnityEngine.Debug:Log (object)
Missile:Init (UnityEngine.Vector3,UnityEngine.Vector3,int,Unity.Collections.FixedString64Bytes) (at Assets/Missile.cs:60)
AttackScript:AttackPrefab (UnityEngine.Vector3,PlayerData/WeaponType) (at Assets/AttackScript.cs:441)
PlayerAction:Attack () (at Assets/PlayerAction.cs:84)
PlayerAction:<Start>b__14_0 () (at Assets/PlayerAction.cs:58)
UnityEngine.Events.UnityEvent:Invoke ()
RPGCharacterAnims.RPGCharacterAnimatorEvents:Hit () (at Assets/ExplosiveLLC/RPG Character Mecanim Animation Pack/Code/RPGCharacterAnimatorEvents.cs:33)

local isHost   :False
UnityEngine.Debug:Log (object)
Missile:Init (UnityEngine.Vector3,UnityEngine.Vector3,int,Unity.Collections.FixedString64Bytes) (at Assets/Missile.cs:61)
AttackScript:AttackPrefab (UnityEngine.Vector3,PlayerData/WeaponType) (at Assets/AttackScript.cs:441)
PlayerAction:Attack () (at Assets/PlayerAction.cs:84)
PlayerAction:<Start>b__14_0 () (at Assets/PlayerAction.cs:58)
UnityEngine.Events.UnityEvent:Invoke ()
RPGCharacterAnims.RPGCharacterAnimatorEvents:Hit () (at Assets/ExplosiveLLC/RPG Character Mecanim Animation Pack/Code/RPGCharacterAnimatorEvents.cs:33)

When Is call to get an object from the pool, the objets are spawned :

I don’t know what is appening…

Thanks for your help

Pooling doesn’t fix the latency issue. Also NGO has network object pooling class built in.

To fix the latency issue, when the client fires a projectile, a local non-networked version of that projectile is spawned. It does not interact with anything, it’s purely visual. If the server determines a hit, it sends an RPC and the client destroys its local projectile.

So much for the premise but the latency will merely be moved towards the impact. That however is already a worthwhile improvement for the feel of the game.

Unity’s Boss Room example game utilizes this client-side prediction of object spawning and animations.

Hi,
Thanks for your response. just for information, the problem was Settinf the GameObject.SetActive(false) when creating a pool.

Does that mean that I don’t even need to spawn projectil on network?
I can use local projectil, without interraction on clients and one with interaction on the server that can hit and damage?

Thanks for your help.

Sure, you could have the interacting projectile on the server only as long as it sends the “destroyed” RPC and clients know which of their local projectiles they should destroy.