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