[Bug] Projectiles not reliably destroyed over the network

ChristianWiele

Active member
Hi,

if you have a shootable weapon with projectiles that should be destroyed upon collision, not all projectiles are reliably destroyed on the remote client. You can reproduce the issue in the PUN demo scene:
1.) Change the ArrowPun prefab to "Destroy on collision"
2.) Build and run the demo scene.
3.) Pick up the bow and shoot all arrows on the target
4.) On the master client all arrows are destroyed upon collision, on the client several of the arrows remain visible and active.

Regards, Christian
 
PUN was recently updated and when I was testing that update I was no longer able to reproduce this issue. After you try the latest please let me know if it still doesn't work correctly.
 
I have done a fresh install and still can reproduce the problem. If I shoot on the server, everything is destroyed on both client and server. If I shoot on the client, the object is always destroyed on the server, but exactly every second projectile / arrow is not destroyed on the client.

Unity 2020.2.6
UCC 2.3.1
PUN add-on 1.1.10
Photon 2.28.1
 
If you disable Synchronize Active State on the PunTransformMonitor you should get better results. What is happening is that the sync point is being sent later than when the projectile destroys itself and this is causing the problem.
 
This seems to fix the problem only on short distances. If you shoot the arrow across the room then the issue still appears, and duplicate key errors occur:

Code:
PhotonView ID duplicate found: 128. New: View 128 on ArrowPun(Clone) (scene) old: View 128 on ArrowPun(Clone) (scene). Maybe one wasn't destroyed on scene load?! Check for 'DontDestroyOnLoad'. Destroying old entry, adding new.
UnityEngine.Debug:LogError (object)
Photon.Pun.PhotonNetwork:RegisterPhotonView (Photon.Pun.PhotonView) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonNetworkPart.cs:1003)
Photon.Pun.PhotonView:set_ViewID (int) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonView.cs:290)
Opsive.UltimateCharacterController.AddOns.Multiplayer.PhotonPun.Game.PunObjectPool:OnEvent (ExitGames.Client.Photon.EventData) (at Assets/Opsive/UltimateCharacterController/Add-Ons/Multiplayer/PhotonPUN/Scripts/Game/PunObjectPool.cs:203)
Photon.Realtime.LoadBalancingClient:OnEvent (ExitGames.Client.Photon.EventData) (at Assets/Photon/PhotonRealtime/Code/LoadBalancingClient.cs:3285)
ExitGames.Client.Photon.PeerBase:DeserializeMessageAndCallback (ExitGames.Client.Photon.StreamBuffer) (at D:/Dev/Work/photon-dotnet-sdk/PhotonDotNet/PeerBase.cs:891)
ExitGames.Client.Photon.EnetPeer:DispatchIncomingCommands () (at D:/Dev/Work/photon-dotnet-sdk/PhotonDotNet/EnetPeer.cs:559)
ExitGames.Client.Photon.PhotonPeer:DispatchIncomingCommands () (at D:/Dev/Work/photon-dotnet-sdk/PhotonDotNet/PhotonPeer.cs:1837)
Photon.Pun.PhotonHandler:Dispatch () (at Assets/Photon/PhotonUnityNetworking/Code/PhotonHandler.cs:223)
Photon.Pun.PhotonHandler:FixedUpdate () (at Assets/Photon/PhotonUnityNetworking/Code/PhotonHandler.cs:149)

Code:
InvalidOperationException: Duplicate key 128
ExitGames.Client.Photon.NonAllocDictionary`2[K,V].Add (K key, V val) (at D:/Dev/Work/photon-dotnet-sdk/PhotonDotNet/DataTypes.cs:398)
Photon.Pun.PhotonNetwork.RegisterPhotonView (Photon.Pun.PhotonView netView) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonNetworkPart.cs:1014)
Photon.Pun.PhotonView.set_ViewID (System.Int32 value) (at Assets/Photon/PhotonUnityNetworking/Code/PhotonView.cs:290)
Opsive.UltimateCharacterController.AddOns.Multiplayer.PhotonPun.Game.PunObjectPool.OnEvent (ExitGames.Client.Photon.EventData photonEvent) (at Assets/Opsive/UltimateCharacterController/Add-Ons/Multiplayer/PhotonPUN/Scripts/Game/PunObjectPool.cs:203)
Photon.Realtime.LoadBalancingClient.OnEvent (ExitGames.Client.Photon.EventData photonEvent) (at Assets/Photon/PhotonRealtime/Code/LoadBalancingClient.cs:3285)
ExitGames.Client.Photon.PeerBase.DeserializeMessageAndCallback (ExitGames.Client.Photon.StreamBuffer stream) (at D:/Dev/Work/photon-dotnet-sdk/PhotonDotNet/PeerBase.cs:891)
ExitGames.Client.Photon.EnetPeer.DispatchIncomingCommands () (at D:/Dev/Work/photon-dotnet-sdk/PhotonDotNet/EnetPeer.cs:559)
ExitGames.Client.Photon.PhotonPeer.DispatchIncomingCommands () (at D:/Dev/Work/photon-dotnet-sdk/PhotonDotNet/PhotonPeer.cs:1837)
Photon.Pun.PhotonHandler.Dispatch () (at Assets/Photon/PhotonUnityNetworking/Code/PhotonHandler.cs:223)
Photon.Pun.PhotonHandler.FixedUpdate () (at Assets/Photon/PhotonUnityNetworking/Code/PhotonHandler.cs:149)
 
I should have it fixed this time. Add the following to line 202 if PunObjectPool:

Code:
                PhotonNetwork.LocalCleanPhotonView(photonView); // NEW
                photonView.ViewID = 0;
                photonView.ViewID = (int)data[3];
 
Unfortunately, this does not fix the issue. I get the same exceptions, also in the demo scene. The only thing I've changed is to set the ArrowPun to be destroyed on collision. The problem only occurs on longer distances, like shooting the arrow across the hall. And occurs more often for the client player.
 
In my test scene I was shooting the arrows at a large distance. The error that you are getting relates to duplicate views in the dictionary but with the code above it is removing that view from the dictionary so you should no longer get the error.
 
I just reinstalled UCC / Photon / PUN add-on to be sure I am on the clean state. I made 2 changes:
1.) I set the ArrowPun to destroy on collision
2.) I added the code in PunObjectPool

I build the Pun demo scene, run the build (master) and editor (client). Walk with both characters into the hall. Take the bow with the client character. I shoot one arrow across the hall, everything seems fine. The character reloads, but on the master the second arrow appears just for a short moment and then disappears. If I now shoot the second arrow on the client, I get the exception.
 
Sorry, missed to disable the state sync on the photon view. Now it's working on the demo scene, and I have to check why I still get the error on my scene.
 
O.k., I did some more tests because the error remained on my productive installation. I found the following

1.) On my Opsive reference installation (only UCC / Photon / multiplayer add-on / built-in render pipeline) the error still occurs, but much more infrequent. Only after 20 - 50 arrows shot by the client character, the error occurs.
2.) As I am using URP in my productive installation, I created another Opsive reference installation, but with URP. On this installation the error occurs still after the second arrow.

No clue what it has to do with URP, but it is the only difference in the installations. It does not seem to be the root cause, but only fosters it for some reason.

Unity 2020.3.0 / URP 10.3.2
UCC 2.3.1
Photon 2.28.3
PUN add-on 1.1.10
UCC/URP integration package downloaded March, 12th, 2021
 
Hmm, it looks like you did everything correct. The root cause of this exception is that PhotonNetwork.photonViewList has a duplicate PhotonView. This gets added when you set the ViewID. The PhotonView should be removed when PhotonNetwork.LocalCleanPhotonView is called.

Are you able to set breakpoints/log statements to see why/when the PhotonView is being added to the photonViewList? And why it's not being removed with LocalCleanPhotonView?
 
I think the duplicate PhotonView is also just the result of another problem. The actual problem seems to be that shortly after reload the arrow on the server is disabled, but on the client it remains active. And if I then fire the arrow, the server spawns another arrow with the same PhotonView. So why is the arrow disabled on the server after reload? Do you have an idea what to debug?
 
I made some progress in the analysis. Seems to be an issue with instantiation.

1.) Player on client picks up the bow.
2.) Player is equipped with the bow. Both on server and client the visible ArrowPun is spawned with viewID 0 (under the Arrow Attachment GO).
3.) Upon shooting the arrow is spawned over the network with viewID 127.
4.) After the first arrow is shot the disconnect happens:
a) the client character on the server is re-equipped with the arrow that has viewID 127, no arrows in the object pool
b) the client character on the client is re-equipped with an arrow that has viewID 0, while the arrow with viewID 127 is inactive in the object pool

If the server character uses the bow, on both, client and server, the character is re-equipped with the viewID 127 arrow.
 
Is that within the demo scene of a fresh project? I think still placing a breakpoint within the PhotonNetwork section that modifies the list is a good starting point. This will tell you exactly when it is being added and you can look up the call stack from there.
 
Yes, this is the new install described above. I will try to further debug the problem. The error with the duplicate entry still seems not to be the root cause. It occurs only after shooting the second arrow. Any idea why the arrow with ID 127 on the client is in the object pool, and not equipped?
 
Oh man, this was a tough one. I now understand the problem, but I can't fix it on my own. Actually, I'm wondering why this issue didn't already pop up. I created the code below to explain the issue. The main problem is that the (client) object pool returns a random object, and not the one with the correct viewID instantiated by the server. Any attempt to fix it by using PhotonNetwork.LocalCleanPhotonView(photonView) sooner or later leads to inconsistencies in the list.
The best solution would be to extend the object pool to allow returning the object with the correct photon view.

Code:
                var obj = ObjectPool.Instantiate(original, (Vector3)data[1], (Quaternion)data[2]);
                var photonView = obj.GetCachedComponent<PhotonView>();
                var targetViewID = (int)data[3];
                // Check if an object with the target viewID already exists
                var existingPhotonView = PhotonNetwork.GetPhotonView(targetViewID);

                if(existingPhotonView == null)
                {
                    // Good case. The object pool instantiated a fresh object with unassigned view ID.
                    // Assign the target viewID received from the server     
                    photonView.ViewID = 0;
                    photonView.ViewID = targetViewID;   
                }
                else
                {
                    if(existingPhotonView.gameObject.activeInHierarchy)
                    {
                        if(existingPhotonView.gameObject == obj)
                        {
                            // Good case. The object pool returned the game object with the correct photon view ID.
                            // Nothing to do.
                        }
                        else
                        {
                            // Bad case. An active game object exists in the scene, but it is not the object received from object pool.
                            // Probably a race condition. The object has been destroyed on server, but not yet on client.
                            // Possible solution: destroy existing object.
                            // Need to handle exception when destroy event comes from server as object has been already destroyed.     
                        } 
                    }
                    else
                    {
                        // Bad case. There is an inactive game object in the pool with the target viewID.
                        // Possible solution: Extend object pool to return the game object with the correst viewID.
                    }
                }
 
The main problem is that the (client) object pool returns a random object
This code will destroy the object within PunObjectPool:

Code:
            } else if (photonEvent.Code == PhotonEventIDs.ObjectDestruction) {
                var photonView = PhotonNetwork.GetPhotonView((int)photonEvent.CustomData);
                // The object may have already been pooled by the time it was received over the network.
                if (ObjectPool.InstantiatedWithPool(photonView.gameObject)) {
                    DestroyInternal(photonView.gameObject);
                }
            }

And the event:

Code:
            if (PhotonNetwork.IsMasterClient) {
                var photonView = obj.GetCachedComponent<PhotonView>();
                if (photonView != null) {
                    PhotonNetwork.RaiseEvent(PhotonEventIDs.ObjectDestruction, photonView.ViewID, m_RaisedEventOptions, m_ReliableSendOptions);
                }
            }

Where do you see that it'll return a random object? The server will send the ViewID that should be removed.
 
The problem is not the destruction, but the instantiation.

Code:
var original = m_NetworkSpawnableObjects[(int)data[0]];

var obj = ObjectPool.Instantiate(original, (Vector3)data[1], (Quaternion)data[2]);

instantiates ANY instance from the pool, not the one with the specific viewID. "original" only refers to the prefab, not the instance. You don't request the view ID from the pool, but try to set the viewID on the already instantiated object. But that does not work if there is an inactive pool object with the identical viewID. The current solution assumes that instantiation on server and client follows the exact same order, but this is not ensured over the network.
 
Top