Problems with Destroying Items in Replication Graph

Hi everyone,

I’ve inherited some code on our project from a team member who has left, and I’m trying to resolve an issue with the replication graph that has been set up.

In our game, players can pick up items from the floor and equip them, and also drop them for others to pick up.

In the replication graph, inventory items are defined in InitGlobalActorClassSettings() as having EClassReplicationNode as Spatialize_Static, because they don’t have any physics applied to them, or the ability to move when not held by the player.

Like in the ShooterGame example, we have a broadcast which is used to notify when an item is picked up or dropped. When picked up, the weapon is removed from the static list and added to the character as a dependent actor. When dropped, it is removed. However, because we want the weapon to appear on the floor for others to see and pick up, we also add it back to the list of static items. We also set the NetDormancy to DormantAll, because there is no need for it to replicate any information once it’s lying on the floor. It’s basically a static object waiting to be picked up by someone.

I have also a requirement to be able to destroy an item in a player’s inventory. This is for thrown weapons where once the weapon “fires” and the projectile is launched, we want to destroy it so it looks like it left the player’s hand. This is done by having the player drop the weapon first (so that it runs the code to update their inventory, switch to a new item etc.), then attempting to destroy it after 0.1s

The trouble is that whenever I try to destroy the weapon, it generates an ensure failure as below:


LogOutputDevice: Error: Ensure condition failed: IsActorValidForReplication(Actor) [File:E:/Epic Games/Unreal Source/Source-UnrealEngine-4.24/Engine/Plugins/Runtime/ReplicationGraph/Source/Private/ReplicationGraph.cpp] [Line: 1681]
LogOutputDevice: Error: Actor not valid for replication (BeingDestroyed:1) (PendingKill:1) (Unreachable:0) (TearOff:0)! Actor = Weapon_Grenade_BP_C /Game/Maps/UEDPIE_1_New_Alpha_Map.New_Alpha_Map:PersistentLevel.Weapon_Grenade_BP_2, Channel = None

Here is the full list of steps used to “destroy” the item in the player’s inventory:

Item starts as being dependent on the owning character for replication. Net dormancy is Awake. All steps are done on the server unless noted

  1. Detach the weapon from the character mesh
  2. Enable collision on the weapon and make sure it’s not hidden anymore as part of the dropping code, so players can pick it up again
  3. Force Net Update on the item
  4. Set the owner to nullptr
  5. Set the net dormancy to DormantAll
  6. Flush Net Dormancy
  7. Broadcast the dropped action to the replication graph
  8. Place the item on the ground, set appropriate rotation etc
  9. Remove the item reference from the inventory, switch to a new item if there is on in the inventory etc.

Code then executes in the rep graph in response to the broadcast in step 7 above:

  1. Remove the dependency on the character
  2. Add the actor to the static list

Item is now on the static list, no dependency on character, Net dormancy is DormantAll

Finally:

  1. Set the lifespan to 0.1s so it gets destroyed on the server and clients

I have also tried calling tearoff, and setting the lifespan to 0.1 in the TornOff() event. This however leads to endless warning spam on the server about how it cannot initialise a new object because it’s no longer set to replicate.

Can anyone help me smooth out this process so it works nicely? Looking at the above steps I see some potentially questionable steps (timing of setting NetDormancy, forcing net updates etc.), but ultimately I have no idea why it gives errors and warnings when attempting to destroy the item.

Thanks!

I did respond in Discord but I think there are a few more things happening here.

The first thing is that the order of operations seems incorrect. You are calling SetDormancy() then immediatelly afterwards calling FlushNetDormancy() and changing replicated properties like rotation (assuming the item replicates movement).

FlushNetDormancy() will wake the actor. All you need to do to make the actor go Dormant is set it to DormantAll, and the server will close the actor channel down once all clients have acked the latest state of all replicated properties. Calling FlushNetDormancy() will wake the object back up, so the object won’t go dormant currently. Any calls you make to ForceNetUpdate() or FlushNetDormancy() will wake the actor and keep it awake until you set NetDormancy again.

The next issue seems confusing - you want the actor to remain on the ground for clients, but you are destroying it by setting a lifespan? Incidentally if you want to destroy an actor, you should just call Destroy() - there’s no reason to use SetLifespan(), and you should avoid it because it can mask race conditions and make them difficult to debug. Timers will never accurately work on Clients anyway.

The order of operations should look something like the following (assuming that you don’t want to destroy the actor)

  1. Remove the item from inventory. This should notify replication graph to remove the dependency. You need to do this before the parent actor is destroyed.
  2. Set any item properties. Detach it, place on ground, set the owner to null, modify the transform etc.
  3. Call ForceNetUpdate() if you must, but you probably don’t need to.
  4. Set NetDormancy to DORM_DormantAll.

I’m still not sure why you’re destroying the actor though, if you want it to remain on the ground you shouldn’t be destroying it.

Thank you for the response TheJamsh. The problem with Discord sadly is that if I have to go out for a bit and then come back, your response will often be lost in amongst the dozens of messages that have come in since then!

In this particular case, I want to destroy the item because this is a function to permanently remove an item from the player’s inventory. This is used primarily for two reasons:

  1. Ammo pouches count as inventory items, which need to be permanently destroyed once empty, since we don’t want useless empty pouches lying around or clogging up inventory space

  2. Thrown weapons (e.g. a grenade or throwing axe) need to be destroyed once “fired”, so it looks like the weapon has left the player’s hand and gone flying towards their target. In reality, the grenade equipped in the player’s hand is a regular weapon which fires a separate grenade projectile, and the original weapon is destroyed once fired

I thought I would use the drop code to handle all the clean up for removing the item from the player and their dependency, and then destroy it as part of the destruction process, hence why I’m going to the trouble of setting its location, rotation etc even though it’s unnecessary.

The lifespan was an attempt to see if I could fix the issue by having it call Destroy on a separate network tick after it has set the dormancy, detached the actor etc.

Maybe the better option is to write a completely separate function for destroying an item rather than doing all the dropping process and then destroying at the end.

I will try your suggestion and see if it works :slight_smile: Thank you again!

Ok so I gave it a go. This is how I now destroy the item in the player’s inventory:

  1. Remove the item from the player’s array
  2. If the item is currently held in the character’s hand, then automatically switch to a new item if possible and set the held item pointer to null
  3. Destroy the object

This seems to be a much cleaner approach since none of the dropping code needs to be run for this, and it now stops errors appearing.

I do still however have an issue with conditional spatialization. When the weapon is on the floor it should be considered static and when picked up it should be considered dependent on the base character. In the unequip broadcast, the rep graph removes the dependency on the owning character and adds the item to the static list. In the equip broadcast it does the opposite: removes it from the static list and adds the new owner as a dependency. I have a couple of issues however:

  1. Items can be placed in the level by a designer to be picked up
  2. Items are also dynamically spawned for the player at runtime and equipped to them when they spawn

I have tried several approaches:

  1. In InitGlobalActorClassSettings() set the item class to be EClassReplicationNode::Spatialize_Static

    This generates warnings whenever I destroy the item in the player’s inventory because it tries to remove it from the static list even though it has already been removed when the actor picked it up. It also causes the same warning for some reason if the item is placed in the level by the designer and the player picks it up, but NOT when the item is spawned at runtime and then equipped to the player

  2. In InitGlobalActorClassSettings() set the item class to be EClassReplicationNode::NotRouted

    This stops the warnings when the item is destroyed in the player’s inventory, but causes warnings when the item is first picked up because it’s trying to remove it from the static list when it hasn’t been added at first

How am I supposed to handle this situation?

It’s just a special case that you’ll need to handle case-by-case I guess, just by checking if the object is in the list before removing it.

I would probably go with the NotRouted approach. I know ShooterGame tries to avoid dependencies between the replication graph and the game code, but I personally think it’s fine to tailor rep graph to your specific game (just not the other way around).

I managed to get it working I think. I in fact used Spatialize_Dormancy, which apparently adds the object to the static list when dormant, and dynamic list when awake. Given that the item is set to DormantAll when dropped and awake when picked up, this seems ideal. I removed the lines of code to explicitly add or remove it to/from the static list and the warnings have all gone. Seems to be working fine.

The only other change I had to make was to handle when a player crashes or disconnects suddenly. For some reason destroy() doesn’t immediately work in that situation, so I do set a 0.1s lifespan for that. No warnings, or errors :slight_smile: