Best way to replicate destructibles

Hi everyone,

I want to make use of destructibles in my game so that players can use artillery to blast holes in barricades which they can then walk through to breach a fort. The maps will be big, so it’s possible for players to be too far away for the barricades to be relevant.

For this reason I need to replicate damage so that newly connected clients joining a game in progress, or players who were out of relevancy for a while don’t see players running through solid walls. I see two possible ways to handle this:

  1. Make destructibles always relevant and replicate damage taken with a netmulticast, and also store an array of previous damage taken replicated with “InitialOnly” for new clients to “backfill” the historical damage taken. This would work, but I don’t want to make objects always relevant unless absolutely necessary, as I plan a high player count (over 200 players if possible with the 4.20 networking overhaul)

  2. Store an array of hits received and stick a repnotify on it, so as new damage is taken it’s replicated to clients who then apply the damage in the repnotify. A “dirty” client who has just connected or was out of relevancy for a period of time will simply catch up on the next repnotify.

I was wondering which would be the best approach, or indeed if there are other approaches I haven’t considered.

A couple of questions:

  1. How would I efficiently replicate an array? The size could grow quite large over the course of a game, so I don’t want an array containing dozens of hit infos being sent out to people every time some damage is taken. Does UE4 intelligently only send info for array indexes that have been modified or are new?

  2. For “historical” damage taken, is there a way to prevent chunks being produced? Essentially what I don’t want is for a new client to join and watch walls explode everywhere as their version of the game catches up. Is there a way to apply damage to a destructible that doesn’t produce chunks? Of course, I would need to track when the damage was dealt so the client knows if it is old damage or not

Any advice would be much appreciated!

Replicating all that debris is a non-starter IMO. It’s too much data to send across the wire and keep in-sync.

The way I see it, you have a couple options:

  • Create a second “destroyed” version of your barricades, and simply replicate the state (destroyed, or intact) to switch between the versions.

    • Pro: Incredibly cheap bandwidth wise.

    • Cons: All destroyed barricades will look the same (although you could build a variety of destroyed walls and randomly pick one - but people will eventually see the patterns).

  • Create some very rough voxel type system where you dynamically build the wall based on the state of the voxels and simply send a small byte stream that represents which voxels are destroyed and which aren’t (you could pack them into a single 32bit integer, etc).

    • Pro: More dynamic destruction. Bandwidth should still be pretty cheap.

    • Cons: Potentially more draw calls / processing time every time you need to rebuild the mesh from the pieces because the voxel state changed.

I would keep all non-gameplay required debris as just client side effects (so, any rubble that is spawned when you shoot the wall and bounces on the ground or what not).

Just my 2c.

Hi

I wasn’t planning to replicate the debris, more just the damage done to the destructible. As long as the server and all clients have a hole roughly in the same place then that’s all that matters, the client can happily simulate the bits flying off by itself.

Your idea is intriguing though. I could have a regular destructible mesh which, once absorbed enough damage, is replaced with a different destructible mesh which is pre-damaged. That way, it wouldn’t necessarily look the same on all clients (since it would still be receiving dynamic damage as well), but it would still have discrete stages of pre-defined destruction as well. I’ll investigate!

Hi everyone,

I managed to set up a system which I think works pretty well. I am using a class that extends ADestructibleActor.

I created a new USTRUCT called FDestructibleReplicatedHit which looks like this:



// Shortened for brevity
struct FDestructibleReplicatedHit
{
     FVector DamageLocation;
     FVector DamageDirection;
     const class UDamageType* DamageType;
     float HitDamage;
     float HitTime;
}


The HitTime acts as a timestamp and will be referenced later. I then added a TArray of FDestructibleDamageEvent objects which is replicated, and also uses a RepNotify function called OnRep_ReplicatedHits. I also have a float called latestDamageTimestamp, which will be used by the receiving client in conjunction with a replicated hit’s HitTime to determine if it needs to be processed or not:



    UPROPERTY(Replicated, ReplicatedUsing = OnRep_ReplicatedHits)
        TArray<FDestructibleReplicatedHit> replicatedHits;

    UFUNCTION()
    void OnRep_ReplicatedHits();

    float latestDamageTimestamp = 0.0f;


Next, I added some helper functions to determine if the DestructibleMesh object accumulates damage, and to find out what the damage threshold is:



bool AWaTDestructibleActor::GetAccumulatesDamage() {
    return GetDestructibleComponent()->GetDestructibleMesh()->DefaultDestructibleParameters.Flags.bAccumulateDamage;
}

float AWaTDestructibleActor::GetDamageThreshold() {
    return GetDestructibleComponent()->GetDestructibleMesh()->DefaultDestructibleParameters.DamageParameters.DamageThreshold;
}


I then added an event to listen out for OnTakePointDamage in the constructor:



AWaTDestructibleActor::AWaTDestructibleActor()
{

    bReplicates = true;
    OnTakePointDamage.AddDynamic(this, &AWaTDestructibleActor::ReceivedPointDamage);
}


And then I wrote the ReceivedPointDamage function like so:



void AWaTDestructibleActor::ReceivedPointDamage(AActor* DamagedActor, float Damage, class AController* InstigatedBy, FVector HitLocation, class UPrimitiveComponent* FHitComponent, FName BoneName, FVector ShotFromDirection, const UDamageType* DamageType, AActor* DamageCauser) {
    if (HasAuthority()) {
        if (Damage > GetDamageThreshold() || (Damage > 0.0f && GetAccumulatesDamage())) {

            float hitTimeStamp = GetWorld()->GetRealTimeSeconds();

            FDestructibleReplicatedHit newEvent(HitLocation, ShotFromDirection, DamageType, Damage, hitTimeStamp);

            replicatedHits.Add(newEvent);
        }
    }
}


You’ll see here that it only replicates a hit if the mesh accumulates damage OR if the damage is greater than the damage threshold. We ignore hits if the mesh doesn’t accumulate damage and it’s below the threshold since it doesn’t materially affect the mesh (I handle impact effects elsewhere).

Finally, this is the onrep function:



void AWaTDestructibleActor::OnRep_ReplicatedHits() {
    float newLatestTimestamp = latestDamageTimestamp;

    for (FDestructibleReplicatedHit currEvent : replicatedHits) {
        if (currEvent.HitTime > latestDamageTimestamp) {
            GetDestructibleComponent()->ApplyDamage(currEvent.HitDamage, currEvent.DamageLocation, currEvent.DamageDirection, currEvent.DamageType->DestructibleImpulse);
            if (currEvent.HitTime > newLatestTimestamp) {
                newLatestTimestamp = currEvent.HitTime;
            }
        }
    }
    latestDamageTimestamp = newLatestTimestamp;
}


You’ll see here that it checks through the array and only picks replicated hits with a timestamp later than the last time it checked. That way, there’s no need to try and sync the server and client world times, it will just base it on the server. A new joiner to the game will have latestDamageTimestamp defaulted to 0, so all hits in the array will be process. A player who was out of replication for a bit and then comes back will just pick up the new ones since the actor was last relevant.

Haven’t tried it on the live server yet, but from what I can see this should work :slight_smile:

Some additional thoughts:

  • If you’re not fussed about the impulse applied to destructible objects, you could cut the DamageType from the struct to cut down on size
  • I also plan to netquantize the FVectors to further reduce the size
  • You could also replace the timestamp with a uint16 which increments every time a hit is registered, which would give you 65535 potential values and cuts the size down by another 2 bytes (I think floats are 4 bytes in size, aren’t they?)