Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Bug Non-host's OwnerClientIDs are inconsistent in server and client

Discussion in 'Netcode for GameObjects' started by thelaughingzasshu, Jan 11, 2024.

  1. thelaughingzasshu

    thelaughingzasshu

    Joined:
    Jan 1, 2024
    Posts:
    12
    This code checks to make sure that if an object's OwnerClientId is not the same as the player's OwnerClientId (meaning the object does not belong to the player), something happens to the player. However, in the host side the projectile's id does match the owner player's id, but in the client side the projectile's id always has the host player's OwnerClientId, resulting in the non host player being able to do something with his own projectiles (idValue != OwnerClientId in PlayerSkills script is true when it's not). Below are the relevant portions of the codes for the scripts/classes Fire (MonoBehaviour) and PlayerSkills (NetworkBehaviour, within the Player object)

    Fire
    Code (CSharp):
    1. public ulong id;
    2. public ulong ownerID;
    3. private bool outside = false;
    4.  
    5. public void SetID(ulong i)
    6. {
    7.     id = i;
    8. }
    9.  
    PlayerSkills
    Code (CSharp):
    1.  
    2. [ServerRpc(RequireOwnership = false)]
    3. void BasicAttackServerRpc(Vector3 spawnPosition, Vector3 direction)
    4. {
    5.     int numberOfObjects = 3;
    6.  
    7.     for (int i = 0; i < numberOfObjects; i++)
    8.     {
    9.         GameObject skillInstance = Instantiate(skillObject, spawnPosition, Quaternion.identity);
    10.  
    11.         skillInstance.GetComponent<NetworkObject>().Spawn(true);
    12.         skillInstance.GetComponent<Fire>().SetID(OwnerClientId);
    13.         skillInstance.GetComponent<Fire>().SetOwnerID(OwnerClientId);
    14.         skillInstance.GetComponent<Fire>().ChangeOrientation(direction, 30 * (1 - i));
    15.         if (flash)
    16.         {
    17.             skillInstance.GetComponent<Fire>().damage *= 2;
    18.             flash = false;
    19.         }              
    20.     }
    21. }
    22.  
    23. private bool IsProjectileClose(GameObject projectile)
    24. {
    25.     float distance = Vector2.Distance(transform.position, projectile.transform.position);
    26.     float thresholdDistance = 3.0f;
    27.  
    28.     MonoBehaviour[] scriptComponents = projectile.GetComponents<MonoBehaviour>();
    29.  
    30.     foreach (MonoBehaviour scriptComponent in scriptComponents)
    31.     {
    32.         if (scriptComponent != null)
    33.         {
    34.             System.Reflection.FieldInfo idField = scriptComponent.GetType().GetField("id");        
    35.             if (idField != null)
    36.             {
    37.                 ulong idValue = (ulong)idField.GetValue(scriptComponent);
    38.                 return distance < thresholdDistance && idValue != OwnerClientId;
    39.             }
    40.         }
    41.     }
    42.  
    43.     return false;
    44. }
    Would appreciate the help. Thank you!
     
    Last edited: Jan 11, 2024
  2. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    273
    Just curious, why not use NetworkObject.SpawnWithOwnership in your BasicAttackServerRpc?
    Otherwise, everything spawned in BasicAttackServerRpc will belong to the host/server.

    Then in the IsProjectileClose method you could do something like this:
    Code (CSharp):
    1.     [ServerRpc(RequireOwnership = false)]
    2.     void BasicAttackServerRpc(Vector3 spawnPosition, Vector3 direction, ServerRpcParams serverRpcParams = default)
    3.     {
    4.         int numberOfObjects = 3;
    5.  
    6.         for (int i = 0; i < numberOfObjects; i++)
    7.         {
    8.             GameObject skillInstance = Instantiate(skillObject, spawnPosition, Quaternion.identity);
    9.  
    10.             skillInstance.GetComponent<NetworkObject>().SpawnWithOwnership(serverRpcParams.Receive.SenderClientId, true);
    11.             // Since your skillObject already is a NetworkObject, you can check against the owner and do not need the below
    12.             //skillInstance.GetComponent<Fire>().SetID(OwnerClientId);
    13.             //skillInstance.GetComponent<Fire>().SetOwnerID(OwnerClientId);
    14.             skillInstance.GetComponent<Fire>().ChangeOrientation(direction, 30 * (1 - i));
    15.             if (flash)
    16.             {
    17.                 skillInstance.GetComponent<Fire>().damage *= 2;
    18.                 flash = false;
    19.             }
    20.         }
    21.     }
    22.  
    23.     // Pass in the NetworkObject as opposed to GameObject
    24.     private bool IsProjectileClose(NetworkObject projectile)
    25.     {
    26.         // Ignore if it is owned by the local client
    27.         if (projectile.OwnerClientId == NetworkManager.LocalClientId)
    28.         {
    29.             return false;
    30.         }
    31.  
    32.         // Otherwise, if within the threshold distance then it is close
    33.         float thresholdDistance = 3.0f;
    34.         return Vector2.Distance(transform.position, projectile.transform.position) < thresholdDistance;
    35.     }
    Of course, you might also think about adding a trigger sphere to your projectiles that only trigger on specific layers that your player's colliders have which would make detecting if a projectile is in range event driven as opposed to poll driven (which would be less processor intensive).
    So then your code above could be reduced down to the BasicAttackServerRpc and something like this:
    Code (CSharp):
    1.     private void OnTriggerEnter(Collider other)
    2.     {
    3.         var colliderNetworkObject = other.gameObject.GetComponent<NetworkObject>();
    4.         if (colliderNetworkObject == null || colliderNetworkObject != null && colliderNetworkObject.OwnerClientId == NetworkManager.LocalClientId)
    5.         {
    6.             // Exit early
    7.             return;
    8.         }
    9.  
    10.         // Otherwise, perform action/event based on the projectile type
    11.     }
    You can include and exclude layers in your projectile's collider/trigger, which determine what collider types will trigger it. Then it is just a matter of invoking whatever action you want to perform upon a projectile of one's player getting within the threshold range of another player.

    So, the projectile would have a normal collider to define the boundaries of when it hits something and a collider-trigger that defines when it is "within range".
     
  3. thelaughingzasshu

    thelaughingzasshu

    Joined:
    Jan 1, 2024
    Posts:
    12
    Hello, thanks for replying! The issue with using NetworkObject.SpawnWithOwnership is that on the non-host side, the client player's projectiles freeze in place (in other words,
    Code (CSharp):
    1. skillInstance.GetComponent<Fire>().ChangeOrientation(direction, 30 * (1 - i));
    does not activate), which is why I stopped using it since I encountered the same issue last time and same this time. Could there be something related to how NetworkObject.SpawnWithOwnership works that I have no idea about?

    For reference, this is ChangeOrientation in the Fire script
    Code (CSharp):
    1. public void ChangeOrientation(Vector3 direction, float angle)
    2. {
    3.     ChangeOrientationClientRpc(direction, angle);
    4.     ChangeOrientationServerRpc(direction, angle);
    5. }
    6.  
    7. [ServerRpc(RequireOwnership = false)]
    8. private void ChangeOrientationServerRpc(Vector3 direction, float angle)
    9. {
    10.     transform.rotation = Quaternion.LookRotation(Vector3.forward, direction);
    11.     transform.Rotate(0, 0, angle);
    12.  
    13.     Rigidbody2D rb = GetComponent<Rigidbody2D>();
    14.     rb.velocity = transform.up * objectSpeed;
    15. }
    16.  
    17. [ClientRpc]
    18. private void ChangeOrientationClientRpc(Vector3 direction, float angle)
    19. {
    20.     transform.rotation = Quaternion.LookRotation(Vector3.forward, direction);
    21.     transform.Rotate(0, 0, angle);
    22.  
    23.     Rigidbody2D rb = GetComponent<Rigidbody2D>();
    24.     rb.velocity = transform.up * objectSpeed;
    25. }
     
    Last edited: Jan 14, 2024
  4. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    273
    If you are using a NetworkTransform, then you would need to make it owner authoritative (or also known as a "ClientNetworkTransform").

    Could you describe in a bit more detail what you are using the ChangeOrientation RPC methods for (other than the obvious of changing the rotation)?

    If it is just to synchronize a change in rotation, the recommended way to handle this is to use a NetworkTransform and change the orientation on the authority side (whether server or owner/client authoritative). This will then synchronize all other non-authority instances to the transform changes.
     
  5. thelaughingzasshu

    thelaughingzasshu

    Joined:
    Jan 1, 2024
    Posts:
    12
    I'm using the methods to set the rotation and also to launch the projectiles by setting their velocity. Since I am using ClientNetworkTransform and also NetworkRigidbody2D (which doesn't have the OnIsServerAuthoritative property so I can't create ClientNetworkRigidbody2D), I'm surprised that the rigidbodies' velocities can't be synced despite trying different combinations of Server and Client RPCs (though I managed to sync their rotation at one point).
     
  6. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    273
    NetworkRigidBody and NetworkRigidBody2D automatically determine whether the local instance is kinematic or not based on the associated NetworkTransform. So, if you are using an owner authoritative motion model (i.e. ClientNetworkTransform) then the owner's instance is always going to be non-kinematic.

    If you don't spawn with ownership, then the default owner will always be the server/host which if you are sending a client the update via Rpc the client's instance would be non-kinematic (i.e. any changes applied to the Rigidbody on the client side would not be applied).

    Try spawning with ownership and apply these updates to your change orientation script:
    Code (CSharp):
    1.         /// <summary>
    2.         /// This and all script that follows assumes the NetworkObject is spawned with client ownership
    3.         /// </summary>
    4.         /// <param name="direction"></param>
    5.         /// <param name="angle"></param>
    6.         public void ChangeOrientation(Vector3 direction, float angle)
    7.         {
    8.             // If we are the owner, then apply the change directly
    9.             if (OwnerClientId == NetworkManager.LocalClientId)
    10.             {
    11.                 UpdateOrientation(direction, angle);
    12.             }
    13.             else
    14.             {
    15.                 // If we are a server or host, send the change in orientation to the owner
    16.                 if (NetworkManager.IsServer)
    17.                 {
    18.                     ServerSendUpdateOrientationToOwner(direction, angle);
    19.                 }
    20.                 else
    21.                 {
    22.                     // Otherwise, send the change in orientation to the server
    23.                     // (This would only happen if the server was a host and we were targeting the host's owner objet or
    24.                     // this non-owner client was trying to apply the change...not sure if your game/logic allows that or not)
    25.                     ChangeOrientationServerRpc(direction, angle);
    26.                 }
    27.             }
    28.         }
    29.  
    30.         private void ServerSendUpdateOrientationToOwner(Vector3 direction, float angle)
    31.         {
    32.             if (!NetworkManager.IsServer)
    33.             {
    34.                 Debug.LogWarning($"Client-{NetworkManager.LocalClientId} is invoking the server only change orientation method! (ignoring)");
    35.                 return;
    36.             }
    37.             // Only send to the owner by setting the target client id list to only the owner's identifier
    38.             var clientRpcParams = new ClientRpcParams
    39.             {
    40.                 Send = new ClientRpcSendParams
    41.                 {
    42.                     TargetClientIds = new List<ulong>() { OwnerClientId }
    43.                 }
    44.             };
    45.             // Send the message to the owner
    46.             ChangeOrientationClientRpc(direction, angle, clientRpcParams);
    47.         }
    48.  
    49.  
    50.         [ServerRpc(RequireOwnership = false)]
    51.         private void ChangeOrientationServerRpc(Vector3 direction, float angle, ServerRpcParams serverRpcParams = default)
    52.         {
    53.             // Just a good idea to make sure you are not receiving messages from the owner (in case any future changes introduce a bug like this)
    54.             if (serverRpcParams.Receive.SenderClientId == OwnerClientId)
    55.             {
    56.                 Debug.LogWarning($"Client-{NetworkManager.LocalClientId} is also the owner but it also called {nameof(ChangeOrientationServerRpc)}! (ignoring)");
    57.                 return;
    58.             }
    59.  
    60.             // If the server is not the owner, then forward the message to the appropriate client.
    61.             // (This would only happen if another non-owner client was trying to apply the change...not sure if your game/logic allows that or not)
    62.             if (OwnerClientId != NetworkManager.LocalClientId)
    63.             {
    64.                 ServerSendUpdateOrientationToOwner(direction, angle);
    65.             }
    66.             else
    67.             {
    68.                 // Otherwise, we are the owner so apply the update to orientation
    69.                 UpdateOrientation(direction, angle);
    70.             }
    71.         }
    72.  
    73.         [ClientRpc]
    74.         private void ChangeOrientationClientRpc(Vector3 direction, float angle, ClientRpcParams clientRpcParams)
    75.         {
    76.             // Always have a check to assure you are sending to the right target
    77.             if (NetworkManager.LocalClientId != OwnerClientId)
    78.             {
    79.                 Debug.LogWarning($"Received a change in orientation message for ownerid-{OwnerClientId} on client-{NetworkManager.LocalClientId}! (ignoring)");
    80.                 return;
    81.             }
    82.             // Otherwise, we are the owner so apply the update to orientation
    83.             UpdateOrientation(direction, angle);
    84.         }
    85.  
    86.         /// <summary>
    87.         /// Break out the desired action/script logic to a separate method
    88.         /// This reduces the complexity and if you need to tweak how orientation is updated you only have to change one place
    89.         /// </summary>
    90.         private void UpdateOrientation(Vector3 direction, float angle)
    91.         {
    92.             transform.rotation = Quaternion.LookRotation(Vector3.forward, direction);
    93.             transform.Rotate(0, 0, angle);
    94.  
    95.             Rigidbody2D rb = GetComponent<Rigidbody2D>();
    96.             rb.velocity = transform.up * objectSpeed;
    97.         }
    Let me know if this helps you resolve your issue?
     
  7. thelaughingzasshu

    thelaughingzasshu

    Joined:
    Jan 1, 2024
    Posts:
    12
    Hello, the projectiles do fire when the non-host player fires them now, but in the host side, the client's projectiles mysteriously freeze in place when they are supposed to be destroyed (when they hit a wall or the enemy player). The host player's projectiles behave correctly as usual though and when they get displayed in the client side as well.
     

    Attached Files:

  8. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    273
    That would mean that you are possibly not destroying when despawning?
    I would add debug log information where you are colliding and also make sure that only the server is what is invoking the despawn and/or destroy on the projectile.

    Depending upon how you are detecting collisions (collider, trigger, etc.) you should be checking if you are the server and if not then exiting early (or vice versa you could also check if you are the owner and then send an RPC to despawn).
     
  9. thelaughingzasshu

    thelaughingzasshu

    Joined:
    Jan 1, 2024
    Posts:
    12
    This is my entire code for the projectile now, particularly take a look at ExplodeServerRpc

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Unity.Netcode;
    5. using System;
    6.  
    7. public class Fire : NetworkBehaviour
    8. {
    9.     [SerializeField] float objectSpeed = 30f;
    10.     [SerializeField] float explosionRadius = 3f;
    11.     public float damage = 5f;
    12.     private LayerMask layer = -1;
    13.     private Camera mainCamera;
    14.     private float cameraHalfWidth;
    15.     private float cameraHalfHeight;
    16.     //public ulong id;
    17.     //public ulong ownerID;
    18.     private bool outside = false;
    19.  
    20.     //public void SetID(ulong i)
    21.     //{
    22.     //    id = i;
    23.     //    //SetIDServerRpc(i);
    24.     //    //SetIDClientRpc(i);
    25.     //}
    26.  
    27.     public void ChangeOrientation(Vector3 direction, float angle)
    28.     {
    29.         // If we are the owner, then apply the change directly
    30.         if (OwnerClientId == NetworkManager.LocalClientId)
    31.         {
    32.             UpdateOrientation(direction, angle);
    33.         }
    34.         else
    35.         {
    36.             // If we are a server or host, send the change in orientation to the owner
    37.             if (NetworkManager.IsServer)
    38.             {
    39.                 ServerSendUpdateOrientationToOwner(direction, angle);
    40.             }
    41.             else
    42.             {
    43.                 // Otherwise, send the change in orientation to the server
    44.                 // (This would only happen if the server was a host and we were targeting the host's owner objet or
    45.                 // this non-owner client was trying to apply the change...not sure if your game/logic allows that or not)
    46.                 ChangeOrientationServerRpc(direction, angle);
    47.             }
    48.         }
    49.     }
    50.  
    51.     private void ServerSendUpdateOrientationToOwner(Vector3 direction, float angle)
    52.     {
    53.         if (!NetworkManager.IsServer)
    54.         {
    55.             Debug.LogWarning($"Client-{NetworkManager.LocalClientId} is invoking the server only change orientation method! (ignoring)");
    56.             return;
    57.         }
    58.         // Only send to the owner by setting the target client id list to only the owner's identifier
    59.         var clientRpcParams = new ClientRpcParams
    60.         {
    61.             Send = new ClientRpcSendParams
    62.             {
    63.                 TargetClientIds = new List<ulong>() { OwnerClientId }
    64.             }
    65.         };
    66.         // Send the message to the owner
    67.         ChangeOrientationClientRpc(direction, angle, clientRpcParams);
    68.     }
    69.  
    70.  
    71.     [ServerRpc(RequireOwnership = false)]
    72.     private void ChangeOrientationServerRpc(Vector3 direction, float angle, ServerRpcParams serverRpcParams = default)
    73.     {
    74.         // Just a good idea to make sure you are not receiving messages from the owner (in case any future changes introduce a bug like this)
    75.         if (serverRpcParams.Receive.SenderClientId == OwnerClientId)
    76.         {
    77.             Debug.LogWarning($"Client-{NetworkManager.LocalClientId} is also the owner but it also called {nameof(ChangeOrientationServerRpc)}! (ignoring)");
    78.             return;
    79.         }
    80.  
    81.         // If the server is not the owner, then forward the message to the appropriate client.
    82.         // (This would only happen if another non-owner client was trying to apply the change...not sure if your game/logic allows that or not)
    83.         if (OwnerClientId != NetworkManager.LocalClientId)
    84.         {
    85.             ServerSendUpdateOrientationToOwner(direction, angle);
    86.         }
    87.         else
    88.         {
    89.             // Otherwise, we are the owner so apply the update to orientation
    90.             UpdateOrientation(direction, angle);
    91.         }
    92.     }
    93.  
    94.     [ClientRpc]
    95.     private void ChangeOrientationClientRpc(Vector3 direction, float angle, ClientRpcParams clientRpcParams)
    96.     {
    97.         // Always have a check to assure you are sending to the right target
    98.         if (NetworkManager.LocalClientId != OwnerClientId)
    99.         {
    100.             Debug.LogWarning($"Received a change in orientation message for ownerid-{OwnerClientId} on client-{NetworkManager.LocalClientId}! (ignoring)");
    101.             return;
    102.         }
    103.         // Otherwise, we are the owner so apply the update to orientation
    104.         UpdateOrientation(direction, angle);
    105.     }
    106.  
    107.     /// <summary>
    108.     /// Break out the desired action/script logic to a separate method
    109.     /// This reduces the complexity and if you need to tweak how orientation is updated you only have to change one place
    110.     /// </summary>
    111.     private void UpdateOrientation(Vector3 direction, float angle)
    112.     {
    113.         transform.rotation = Quaternion.LookRotation(Vector3.forward, direction);
    114.         transform.Rotate(0, 0, angle);
    115.  
    116.         Rigidbody2D rb = GetComponent<Rigidbody2D>();
    117.         rb.velocity = transform.up * objectSpeed;
    118.     }
    119.  
    120.     private void OnTriggerEnter2D(Collider2D collision)
    121.     {
    122.         if (collision.CompareTag("Player"))
    123.         {
    124.             if (outside && collision.GetComponent<NetworkObject>().OwnerClientId != GetComponent<NetworkObject>().OwnerClientId)
    125.             {
    126.                 ExplodeServerRpc();
    127.             }
    128.         }
    129.     }
    130.  
    131.     private void OnTriggerExit2D(Collider2D collision)
    132.     {
    133.         if (collision.CompareTag("Player")) outside = true;
    134.     }
    135.  
    136.     [ServerRpc(RequireOwnership = false)]
    137.     void ExplodeServerRpc()
    138.     {
    139.         Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, explosionRadius, layer);
    140.  
    141.         foreach (Collider2D hitCollider in colliders)
    142.         {
    143.             if (hitCollider.CompareTag("Player"))
    144.             {
    145.                 hitCollider.GetComponent<PlayerMovement>().DealDamage(damage);
    146.             }
    147.         }
    148.         Destroy(gameObject);
    149.     }
    150.  
    151.     // Start is called before the first frame update
    152.     void Start()
    153.     {
    154.         mainCamera = Camera.main;
    155.         cameraHalfWidth = mainCamera.orthographicSize * mainCamera.aspect;
    156.         cameraHalfHeight = mainCamera.orthographicSize;
    157.     }
    158.  
    159.     // Update is called once per frame
    160.     void Update()
    161.     {
    162.         //delay -= Time.deltaTime;
    163.  
    164.         if (transform.position.x < -cameraHalfWidth + (transform.localScale.x / 2) || transform.position.x > cameraHalfWidth - (transform.localScale.x / 2)
    165.             || transform.position.y < -cameraHalfHeight + (transform.localScale.y) || transform.position.y > cameraHalfHeight - (transform.localScale.y))
    166.         ExplodeServerRpc();
    167.     }
    168. }
    169.  
    Now seems like it's behaving better, projectiles are no longer freezing at the last moment, but sometimes the projectiles from the non host player don't damage the host player after being destroyed, with this error appearing

    Code (CSharp):
    1.  
    2. [Netcode] Deferred messages were received for a trigger of type OnSpawn with key 52, but that trigger was not received within within 1 second(s).
    3. UnityEngine.Debug:LogWarning (object)
    4. Unity.Netcode.NetworkLog:LogWarning (string) (at ./Library/PackageCache/com.unity.netcode.gameobjects@1.7.1/Runtime/Logging/NetworkLog.cs:28)
    5. Unity.Netcode.DeferredMessageManager:PurgeTrigger (Unity.Netcode.IDeferredNetworkMessageManager/TriggerType,ulong,Unity.Netcode.DeferredMessageManager/TriggerInfo) (at ./Library/PackageCache/com.unity.netcode.gameobjects@1.7.1/Runtime/Messaging/DeferredMessageManager.cs:97)
    6. Unity.Netcode.DeferredMessageManager:CleanupStaleTriggers () (at ./Library/PackageCache/com.unity.netcode.gameobjects@1.7.1/Runtime/Messaging/DeferredMessageManager.cs:82)
    7. Unity.Netcode.NetworkManager:NetworkUpdate (Unity.Netcode.NetworkUpdateStage) (at ./Library/PackageCache/com.unity.netcode.gameobjects@1.7.1/Runtime/Core/NetworkManager.cs:69)
    8. Unity.Netcode.NetworkUpdateLoop:RunNetworkUpdateStage (Unity.Netcode.NetworkUpdateStage) (at ./Library/PackageCache/com.unity.netcode.gameobjects@1.7.1/Runtime/Core/NetworkUpdateLoop.cs:185)
    9. Unity.Netcode.NetworkUpdateLoop/NetworkPostLateUpdate/<>c:<CreateLoopSystem>b__0_0 () (at ./Library/PackageCache/com.unity.netcode.gameobjects@1.7.1/Runtime/Core/NetworkUpdateLoop.cs:268)
    10.  
     
  10. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    273
    This is most likely because the OnTriggerEnter2D is being invoked more than once on the client side. Just add a bool property that you set to true within OnTriggerEnter2D right after you invoke the ExplodeServerRpc and then exit early from OnTriggerEnter2D if it is set:

    Code (CSharp):
    1.     private bool m_HitTarget = false;
    2.     private void OnTriggerEnter2D(Collider2D collision)
    3.     {
    4.         if (!m_HitTarget  && collision.CompareTag("Player"))
    5.         {
    6.             if (outside && collision.GetComponent<NetworkObject>().OwnerClientId != GetComponent<NetworkObject>().OwnerClientId)
    7.             {
    8.                 ExplodeServerRpc();
    9.                 m_HitTarget  = true;
    10.             }
    11.         }
    12.     }
    See if that adjustment prevents the error message...it is possible that the server generated DestroyObjectMessage is "in flight" (i.e. has not reached the client yet) and the client sends a message to the server...but the NetworkObject no longer exists so it gets deferred...and then times out and that message is logged.

    Really, I would break this up into two stages:
    • The initial impact: ground zero, player impacted takes full damage.
    • The explosion radius damage: excludes player impacted (it already took damage) but checks for any other players within the explosion radius.
    With the initial impact, I would get the PlayerMovement component in OnTriggerEnter2D and then create a NetworkBehaviourReference instance that is set to the PlayerMovement component of the player hit. Then add a NetworkBehaviourReference parameter to your ExplodeServerRpc and pass in your newly created NetworkBehaviourReference to the ExplodeServerRpc... this way you don't need to find the player again (you already found it on the client side) and can apply the full damage to that player immediately within the ExplodeServerRpc method.

    Next (in your ExplodeServerRpc after applying the ground zero player's damage), instantiate and spawn a network prefab explosion FX that has a CircleCollider2D component set as a trigger and the radius of the CircleCollider2D defines the explosion radius. It would look something like this:
    Code (CSharp):
    1.  
    2.     public class Explosion2DFxBehaviour : NetworkBehaviour
    3.     {
    4.         public ParticleSystem ExplosionFx;
    5.         public List<AudioClip> ExplosionSounds;
    6.         public AudioSource AudioSource;
    7.         public float MaxDamage = 100;
    8.         private CircleCollider2D Collider;
    9.         private ulong m_GroundZeroPlayer;
    10.         private NetworkVariable<int> AudioClipToPlay = new NetworkVariable<int>();
    11.         private void Awake()
    12.         {
    13.             Collider = GetComponent<CircleCollider2D>();
    14.             // Assure it is a trigger
    15.             Collider.isTrigger = true;
    16.             // Disable the trigger (it gets enabled when spawned)
    17.             Collider.enabled = false;
    18.         }
    19.         public void Initialize(ulong groundZeroPlayer)
    20.         {
    21.             m_GroundZeroPlayer = groundZeroPlayer;
    22.         }
    23.         public override void OnNetworkSpawn()
    24.         {
    25.             if (IsServer)
    26.             {
    27.                 // Enabling this will cause OnTriggerEnter2D to be invoked
    28.                 Collider.enabled = true;
    29.                 // Optional, a way to synchronize a range of varying explosion FX to provide "unique explosion sounds"
    30.                 AudioClipToPlay.Value = Random.Range(0, ExplosionSounds.Count - 1);
    31.             }
    32.             if (ExplosionFx)
    33.             {
    34.                 ExplosionFx.Play();
    35.             }
    36.          
    37.             if (AudioSource && ExplosionSounds.Count > 0)
    38.             {
    39.                 AudioSource.clip = ExplosionSounds[AudioClipToPlay.Value];
    40.             }
    41.             base.OnNetworkSpawn();
    42.         }
    43.         public override void OnNetworkDespawn()
    44.         {
    45.             Collider.enabled = false;
    46.             base.OnNetworkDespawn();
    47.         }
    48.         private void Update()
    49.         {
    50.             if (!IsSpawned || !IsServer)
    51.             {
    52.                 return;
    53.             }
    54.             if (!IsDespawning && !ExplosionFx.IsAlive() && !AudioSource.isPlaying)
    55.             {
    56.                 IsDespawning = true;
    57.                 StartCoroutine(DelayDespawn());
    58.             }
    59.         }
    60.         private bool IsDespawning;
    61.         // You could get the average RTT for clients to determine "roughly" how
    62.         // long you need to delay the despawn to assure all instances have finished
    63.         private System.Collections.IEnumerator DelayDespawn()
    64.         {
    65.             yield return new WaitForSeconds(1.0f);
    66.             NetworkObject.Despawn();
    67.         }
    68.         private void OnTriggerEnter2D(Collider2D collision)
    69.         {
    70.             var playerMovement = collision.gameObject.GetComponent<PlayerMovement>();
    71.             if (playerMovement == null)
    72.             {
    73.                 // If no PlayerMovement component, then exit early
    74.                 return;
    75.             }
    76.             if (playerMovement.OwnerClientId == m_GroundZeroPlayer)
    77.             {
    78.                 // Ignore ground zero player since damage is already applied
    79.                 return;
    80.             }
    81.             var scaleDamage = Collider.Distance(collision).distance / Collider.radius;
    82.             // Scale damage based on other player's distance from "ground zero"
    83.             playerMovement.DealDamage(MaxDamage * scaleDamage);
    84.         }
    85.     }
    86.  
    Using an approach like this might help break things up into a more "logical order of operations" flow...
     
  11. thelaughingzasshu

    thelaughingzasshu

    Joined:
    Jan 1, 2024
    Posts:
    12
    This is my Fire/projectile script at the moment

    Code (CSharp):
    1. private bool m_HitTarget = false;
    2. private void OnTriggerEnter2D(Collider2D collision)
    3. {
    4.     if (!m_HitTarget && collision.CompareTag("Player"))
    5.     {
    6.         if (outside && collision.GetComponent<NetworkObject>().OwnerClientId != GetComponent<NetworkObject>().OwnerClientId)
    7.         {
    8.             ExplodeServerRpc();
    9.             m_HitTarget = true;
    10.         }
    11.     }
    12. }
    13.  
    14. [ServerRpc(RequireOwnership = false)]
    15. void ExplodeServerRpc(ServerRpcParams serverRpcParams = default)
    16. {
    17.     GameObject explosive = Instantiate(explosion, transform.position, Quaternion.identity);
    18.     explosive.GetComponent<NetworkObject>().SpawnWithOwnership(serverRpcParams.Receive.SenderClientId, true);
    19.     explosive.GetComponent<Explosion>().damage = damage;
    20.  
    21.     Destroy(gameObject);
    22. }
    And this is my new Explosion script

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using Unity.Netcode;
    5. using Unity.VisualScripting;
    6. using UnityEngine;
    7.  
    8. public class Explosion : NetworkBehaviour
    9. {
    10.     public ParticleSystem ExplosionFx;
    11.     public List<AudioClip> ExplosionSounds;
    12.     public AudioSource AudioSource;
    13.     public float damage = 0;
    14.     public float explosionRadius = 1f;
    15.     private LayerMask layer = -1;
    16.     private bool exploded = false;
    17.     private CircleCollider2D Collider;
    18.     private ulong m_GroundZeroPlayer;
    19.     private bool m_Exploded = false;
    20.     private NetworkVariable<int> AudioClipToPlay = new NetworkVariable<int>();
    21.  
    22.     private void Awake()
    23.     {
    24.         Collider = GetComponent<CircleCollider2D>();
    25.         // Assure it is a trigger
    26.         Collider.isTrigger = true;
    27.         // Disable the trigger (it gets enabled when spawned)
    28.         Collider.enabled = false;
    29.     }
    30.     public void Initialize(ulong groundZeroPlayer)
    31.     {
    32.         m_GroundZeroPlayer = groundZeroPlayer;
    33.     }
    34.     public override void OnNetworkSpawn()
    35.     {
    36.         if (IsServer)
    37.         {
    38.             // Enabling this will cause OnTriggerEnter2D to be invoked
    39.             Collider.enabled = true;
    40.         }
    41.         if (ExplosionFx)
    42.         {
    43.             ExplosionFx.Play();
    44.         }
    45.  
    46.         if (AudioSource && ExplosionSounds.Count > 0)
    47.         {
    48.             AudioSource.clip = ExplosionSounds[AudioClipToPlay.Value];
    49.         }
    50.         base.OnNetworkSpawn();
    51.  
    52.         if (!m_Exploded)
    53.         {
    54.             StartCoroutine(DelayDespawn());
    55.             m_Exploded = true;
    56.         }      
    57.     }
    58.     public override void OnNetworkDespawn()
    59.     {
    60.         Collider.enabled = false;
    61.         base.OnNetworkDespawn();
    62.     }
    63.     private System.Collections.IEnumerator DelayDespawn()
    64.     {
    65.         yield return new WaitForSeconds(0.2f);
    66.         NetworkObject.Despawn();
    67.         Destroy(gameObject);
    68.     }
    69.     private void Update()
    70.     {
    71.         if (!IsSpawned || !IsServer)
    72.         {
    73.             return;
    74.         }
    75.  
    76.         //if (exploded) return;
    77.  
    78.         //Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, explosionRadius, layer);
    79.  
    80.         //foreach (Collider2D hitCollider in colliders)
    81.         //{
    82.         //    if (hitCollider.CompareTag("Player"))
    83.         //    {
    84.         //        hitCollider.GetComponent<PlayerMovement>().DealDamage(damage);
    85.         //        exploded = true;
    86.         //    }
    87.         //}
    88.     }
    89.  
    90.     private void OnTriggerEnter2D(Collider2D collision)
    91.     {
    92.         var playerMovement = collision.gameObject.GetComponent<PlayerMovement>();
    93.         if (playerMovement == null || exploded)
    94.         {
    95.             // If no PlayerMovement component, then exit early
    96.             return;
    97.         }
    98.         //if (playerMovement.OwnerClientId == m_GroundZeroPlayer)
    99.         //{
    100.         //    // Ignore ground zero player since damage is already applied
    101.         //    return;
    102.         //}
    103.         // Scale damage based on other player's distance from "ground zero"
    104.         playerMovement.DealDamage(damage);
    105.         exploded = true;
    106.     }
    107. }
    108.  
    Now it works much better as the non-host player can now damage the host player ~90%+ of the time, though it is still not 100%. It might be due to this part which I'm not too sure how to implement.

     
  12. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    273
    This is a quick mock up of how you can use NetworkBehaviourReference:

    Code (CSharp):
    1.     private bool m_HitTarget = false;
    2.     private void OnTriggerEnter2D(Collider2D collision)
    3.     {
    4.         if (!m_HitTarget && outside)
    5.         {
    6.             var playerMovement = collision.gameObject.GetComponent<PlayerMovement>();
    7.             // As long as what we are hitting is a player and it is not the player who owns the projectile
    8.             if (playerMovement != null && playerMovement.OwnerClientId != OwnerClientId)
    9.             {
    10.                 // Invoke the RPC passing in a NetworkBehaviourReference of the player hit
    11.                 ExplodeServerRpc(new NetworkBehaviourReference(playerMovement));
    12.                 m_HitTarget = true;
    13.             }
    14.         }
    15.     }
    16.  
    17.     [ServerRpc(RequireOwnership = false)]
    18.     void ExplodeServerRpc(NetworkBehaviourReference networkBehaviourReference, ServerRpcParams serverRpcParams = default)
    19.     {
    20.         var targetedPlayer = (PlayerMovement)null;
    21.         if (networkBehaviourReference.TryGet(out targetedPlayer))
    22.         {
    23.             // Instantiate the explosion object
    24.             GameObject explosiveObject = Instantiate(explosion, transform.position, Quaternion.identity);
    25.             // Get the explosive component
    26.             var explosive = explosiveObject.GetComponent<Explosion>();
    27.             // Initialize the explosive and then spawn the object
    28.             if (explosive != null)
    29.             {
    30.                 explosive.Initialize(targetedPlayer, damage);
    31.                 explosive.GetComponent<NetworkObject>().SpawnWithOwnership(serverRpcParams.Receive.SenderClientId, true);
    32.                 Destroy(gameObject);
    33.             }
    34.             else
    35.             {
    36.                 Debug.LogWarning($"Could not get Explosion component!");
    37.             }
    38.         }
    39.         else
    40.         {
    41.             Debug.LogWarning($"Failed to get PlayerMovement component from NetworkBehaviourReference!");
    42.         }
    43.     }
    44.  
    Regarding not hitting all of the time...I made some tweaks to your explosion adjustments:

    Code (CSharp):
    1.  
    2. public class Explosion : NetworkBehaviour
    3. {
    4.     public ParticleSystem ExplosionFx;
    5.     public List<AudioClip> ExplosionSounds;
    6.     public AudioSource AudioSource;
    7.     public float TimeToLive = 1.5f;
    8.  
    9.     public float explosionRadius = 1f;
    10.     private float m_Damage = 0;
    11.     private LayerMask layer = -1;
    12.     private bool exploded = false;
    13.     private CircleCollider2D Collider;
    14.     private bool m_Exploded = false;
    15.     private NetworkVariable<int> AudioClipToPlay = new NetworkVariable<int>();
    16.     private float ExplosionLifeTime;
    17.     private void Awake()
    18.     {
    19.         Collider = GetComponent<CircleCollider2D>();
    20.         // Assure it is a trigger
    21.         Collider.isTrigger = true;
    22.         // Disable the trigger (it gets enabled when spawned)
    23.         Collider.enabled = false;
    24.     }
    25.     public void Initialize(PlayerMovement groundZeroPlayer, float damage)
    26.     {
    27.         // In the event you decide to pool this, you want to reset the list of damaged players
    28.         DamagedPlayers.Clear();
    29.         DamagedPlayers.Add(groundZeroPlayer);
    30.         // Go ahead and apply damage to the player hit here
    31.         m_Damage = damage;
    32.         groundZeroPlayer.DealDamage(m_Damage);
    33.     }
    34.     public override void OnNetworkSpawn()
    35.     {
    36.         if (IsServer)
    37.         {
    38.             // Enabling this will cause OnTriggerEnter2D to be invoked
    39.             Collider.enabled = true;
    40.             // Start the time to live offset by the current time
    41.             ExplosionLifeTime = Time.realtimeSinceStartup + TimeToLive;
    42.         }
    43.         if (ExplosionFx)
    44.         {
    45.             ExplosionFx.Play();
    46.         }
    47.         if (AudioSource && ExplosionSounds.Count > 0)
    48.         {
    49.             AudioSource.clip = ExplosionSounds[AudioClipToPlay.Value];
    50.         }
    51.         base.OnNetworkSpawn();
    52.     }
    53.     public override void OnNetworkDespawn()
    54.     {
    55.         Collider.enabled = false;
    56.         base.OnNetworkDespawn();
    57.     }
    58.     private System.Collections.IEnumerator DelayDespawn()
    59.     {
    60.         yield return new WaitForSeconds(0.2f);
    61.         NetworkObject.Despawn();
    62.         Destroy(gameObject);
    63.     }
    64.     private void Update()
    65.     {
    66.         if (!IsSpawned || !IsServer)
    67.         {
    68.             return;
    69.         }
    70.         // If you aren't going to key off of the FX or audio to end the explosion, then use
    71.         // something like a time to live approach (i.e. how long it hangs around before beginning the delayed despawn)
    72.         if (!m_Exploded && ExplosionLifeTime < Time.realtimeSinceStartup)
    73.         {
    74.             StartCoroutine(DelayDespawn());
    75.             m_Exploded = true;
    76.         }
    77.     }
    78.  
    79.     // Tracks the players that have already been damaged
    80.     private List<PlayerMovement> DamagedPlayers = new List<PlayerMovement>();
    81.  
    82.     /// <summary>
    83.     /// Since this can be invoked several times in the same frame (once per collider triggering it),
    84.     /// you don't want to keep it from stopping damage after it applies damage the first time.
    85.     /// </summary>
    86.     private void OnTriggerEnter2D(Collider2D collision)
    87.     {
    88.         if (m_Exploded)
    89.         {
    90.             return;
    91.         }
    92.         var playerMovement = collision.gameObject.GetComponent<PlayerMovement>();
    93.         if (playerMovement == null)
    94.         {
    95.             // If no PlayerMovement component, then exit early
    96.             return;
    97.         }
    98.         // Ignore players already damaged by the explosion
    99.         if (DamagedPlayers.Contains(playerMovement))
    100.         {
    101.             return;
    102.         }
    103.         // Apply the damage
    104.         playerMovement.DealDamage(damage);
    105.    
    106.         // Add this player to the damaged players list so the player isn't damaged more than once
    107.         // Optionally, when you are about to despawn, you could handle "players hit" statistics and use this list of players hit to add to those stats.
    108.         DamagedPlayers.Add(playerMovement);
    109.     }
    110. }
    111.  
    Give the above changes a whirl and let me know if that resolves your issue?
     
    Last edited: Jan 19, 2024
  13. thelaughingzasshu

    thelaughingzasshu

    Joined:
    Jan 1, 2024
    Posts:
    12
    Hello, the issue is finally solved! Thank you very much for your continuous help!