Download

Actor->SetReplicates(false) is not properly shutting down replication (or what I understand from it)

Context: I’m working on a multiplayer game with a Dedicated Server.

I have an Entity class that inherits from Pawn spawned by the server. It’s set to replicate on the constructor with



    bReplicates = true;
    SetReplicatingMovement(true);
    bAlwaysRelevant = true;

Replication works well. Now, when I’m about to Destroy the instantiated Entity, I want to terminate replication BEFORE destroying it. This allows clients to do their own cleanup with a valid pointer to the Entity during the entire process. Otherwise, the Entity gets destroyed midway through the client’s cleanup. This is how I tried to achieve that:


bool AEntityManager::RemoveEntity(AEntity* Entity)
{
    ...]

    // We stop replication before destroying so that Clients don't operate on a Dead Actor for cleanup. ClientEntityManager destroys it when appropiate
    Entity->SetReplicates(false);

    FTimerHandle DestroyEntityTimerHandle;
    FTimerDelegate DestroyEntityTimerDelegate = FTimerDelegate::CreateUObject(this, &AEntityManager::DestroyEntity, Entity);
    GetWorldTimerManager().SetTimer(DestroyEntityTimerHandle, DestroyEntityTimerDelegate, 1.f, false);
    ...]

}


void AEntityManager::DestroyEntity(AEntity* Entity)
{
    bool bEntityIsPendingKill = Entity->Destroy();
}

EntityManager is a Server only instance, so RemoveEntity runs only on the server. Then the Clients currently have no call to DestroyActor(Entity) in them, so the Client’s version of Entity should outlive the Destruction triggered by the server. This is NOT happening.

How can I achieve something like this? Am I misunderstanding either replication or how SetReplicates works? Or when we Destroy a replicated object there’s no way to prevent the client versions to also get destroyed?

You should call TearOff() on the Server instead, then override TornOff() to let the client cleanup and destroy the actor themselves. Tearing an actor off is a way of saying you don’t want the actor to replicate ever again. You need to keep the actor around long enough on the Server for the Torn Off state to reach all clients.

If you stop replication, you can’t garauntee the latest state of the actor will reach the client - and that includes calls to destroy the actor too. And no, you can’t block the actor being destroyed through replication. If the Server destroys a replicated actor, clients will destroy it immediatelly too (on receipt of the “destroy” packet).

This all seems superfluos though, because you could just override OnDestroyed() in the actor class and perform all cleanup there.

Interesting. I’ll look into TearOff() and update with the results. Thanks @TheJamsh . Actually I’m not talking about the Entity itself doing cleanup, but I have a ClientEntityManager that keeps reference to all entities a specific player cares about. I want this class to be able to remove it from its lists and such if necessary. I could subscribe ClientEntityManager::RemoveEntity to OnDestroyed on each entity… Right now EntityManager is broadcasting about the death of this object via


UGameManagersBPL::GetEventManager(this)->CallOnRemoveEntitySuccess(Entity);

And ClientEntityManagers are subscribed to that.

This is what the full EntityManager::RemoveEntity looks like


bool AEntityManager::RemoveEntity(AEntity* Entity)
{
    const bool bWasUnregistered = UnregisterEntity(Entity);
    if (!bWasUnregistered) return false;

    UGameManagersBPL::GetEventManager(this)->CallOnRemoveEntitySuccess(Entity);

    // TODO: This is NOT WORKING AS INTENDED. Even though we call SetReplicates(false) the entity is destroyed on clients as well when Destroyed on the server
    // We stop replication before destroying so that Clients don't operate on a Dead Actor for cleanup. ClientEntityManager destroys it when appropiate
    Entity->TearOff();

    // === This code is used to delay destruction more than a frame for Testing Destruction behavior and it's relationship with replication. Is NOT NEEDED
    // FTimerHandle DestroyEntityTimerHandle;
    // FTimerDelegate DestroyEntityTimerDelegate = FTimerDelegate::CreateUObject(this, &AEntityManager::DestroyEntity, Entity);
    // GetWorldTimerManager().SetTimer(DestroyEntityTimerHandle, DestroyEntityTimerDelegate, 1.f, false);
    // ===

    FTimerDelegate DestroyEntityTimerDelegate = FTimerDelegate::CreateUObject(this, &AEntityManager::DestroyEntity, Entity);
    GetWorldTimerManager().SetTimerForNextTick(DestroyEntityTimerDelegate);

    return true;
}

As of the comment in the middle, I’ve been playing with longer delays between SetReplicates(false) and Destroying the actor (guessing that the problem was stopping replication required some time) but destruction was always replicated no matter the delay.

*UPDATE: I tried using TearOff for my intended purposed but it didn’t work. Clients can no longer properly destroy the actor because it was created as a networked one. Tried using bForce = true but that resulted in more buggy behavior… At which point this becomes a matter of choosing a better path *

In my (hefty) experience using delays to get around networking latency or packet loss is frowned upon, as it’s a quick way to end up with a very unstable game. You can’t garauntee how long it will take for replication changes to reach a client so any delay you add is going to inevitably break at some point. I would recommend cleaning up the destroy logic a bit, making it so that destroying an actor immediatelly removes itself from this manager.

I use this pattern all the time, usually for actor components. Overriding OnRegister and OnUnregister in UActorComponent to add or remove it from the global list means the component can manage it’s own lifetime, which is far safer and means no workarounds to handle replication. Actors are no different, you can use BeginPlay/OnDestroyed to do the same thing.

TearOff stops all replication. The Server doesn’t send anymore packets once the TornOff state has been acked and the channel is closed. Once torn off a client can do whatever they want with it, including destroy it, but of course they first need to receive the “torn off” state. You will need to override OnTornOff() and use that to do your client-side cleanup.

There are several network related systems that routinely destroy and recreate actors client-side, such as network relevancy (any spawned actor which leaves the players relevancy range will be destroyed) - so realistically client objects will need to be able to manage their own lifetimes anyway. You’re better off coding the entity manager to “respond” to destroy events rather than attempt to manage them.