ShooterGame - Server to Client Replication Issue - only works if spawns particle sys

This is a weird one, I can’t figure out why we need to do it this way, so any advice would be helpful. I’ll try to be as verbose as possible in this message.

We are basing this largely off of shooter code. We have Projectile and ImpactEffect classes. The server spawns the projectiles same as in the shooter code. When the server detects an impact (OnProjectileStop bound delegate) it calls OnImpact on the Projectile. It then calls Exploded only if it is the server. In Exploded it sets bExploded to true which is a replicated variable. The clients are supposed to receive this in the OnRep_Exploded function. This is only working for us if we spawn TracerFX on the server. The client doesn’t even need to create it unless we want to see the effect. But for some reason if the server never spawns it then the clients never receive the bExploded = true through the OnRep_Exploded. If that never gets called then the clients never see the ImpactEffect.

So does anyone know what causes the server to not send bExploded = true to the clients unless it spawns a particle system? For now the hack has been for the server to always spawn a particle system even if the clients don’t spawn one. I don’t like this solution though and would like to know what causes it and some hints that I may use to properly handle this.

Here are the things I have tried:
I have tried giving the projectile a static mesh, thinking it was a visual thing, it doesn’t change the behavior.
I have tried setting the replication on the projectile BP to be always relevant and it doesn’t change the behavior.
If I remove all particle system spawning on the server then no impact effects are ever shown.

Here is the code that we are using:

/** did it explode? */
UPROPERTY(Transient, ReplicatedUsing = OnRep_Exploded)
	bool bExploded;
UPROPERTY(EditDefaultsOnly, Transient, ReplicatedUsing = OnRep_IsTracer)
	uint8 bIsTracer : 1;

/** [client] explosion happened */
	void OnRep_Exploded();
/** [client] bIsTracer was toggled */
	void OnRep_IsTracer();

/** handle hit */
	virtual void OnImpact(const FHitResult& HitResult);
/** trigger explosion */
void Explode(const FHitResult& Impact);


//Client handler for when this projectile explodes, play FX
void AWAWProjectile::OnRep_Exploded()
	FVector ProjDirection = GetActorRotation().Vector();

	const FVector StartTrace = GetActorLocation() - ProjDirection * 200;
	const FVector EndTrace = GetActorLocation() + ProjDirection * 150;
	FHitResult Impact;

	//If we do not have a line trace to the explosion, then it must have occurred right where our projectile is
	if (!GetWorld()->LineTraceSingle(Impact, StartTrace, EndTrace, COLLISION_PROJECTILE, FCollisionQueryParams(TEXT("ProjClient"), true, Instigator)))
		// failsafe
		Impact.ImpactPoint = GetActorLocation();
		Impact.ImpactNormal = -ProjDirection;

	//Call the Explode function to display the FX

//Client handler for when this projectile's bIsTracer bool switches
void AWAWProjectile::OnRep_IsTracer()
	if (!TracerFX) { return; }
	if (bIsTracer && !TracerPSC)
		//Spawn the emitter at the location of the muzzle
		TracerPSC = UGameplayStatics::SpawnEmitterAttached(TracerFX, RootComponent);
	else if (!bIsTracer && TracerPSC)
		TracerPSC = NULL;
	//TODO: the following was added to ensure that impact FX were called on clients, the following is
	//TODO: only ever ran on the servers (this could cause problems with listen servers though always showing tracers).
	//TODO: we should eventually figure out why impactFX are not working unless the server spawns this
	else if (!bIsTracer)
		TracerPSC = UGameplayStatics::SpawnEmitterAttached(TracerFX, RootComponent);

//Handle when this projectile impacted something
void AWAWProjectile::OnImpact(const FHitResult& HitResult)
	//This projectile did not bounce and did not penetrate, so explode it
	//Only run this on the server and only if it has not exploded
	if (Role == ROLE_Authority && !bExploded)
		//Explode this projectile, play the particle FX and sound FX

		//Disable it and then destroy it

//Explodes this projectile when it impacts something
void AWAWProjectile::Explode(const FHitResult& Impact)
	//Figure out a location to explode this projectile, do not explode it inside
	//of a mesh, otherwise no damage will result
	// effects and damage origin shouldn't be placed inside mesh at impact point
	const FVector NudgedImpactLocation = Impact.ImpactPoint + Impact.ImpactNormal * 10.0f;

	//If this weapon has explosion damage, radius, and a valid damage type then apply the damage radially (in a sphere)
	if (WeaponConfig.ExplosionDamage > 0 && WeaponConfig.ExplosionRadius > 0 && WeaponConfig.DamageType)
		UGameplayStatics::ApplyRadialDamage(this, WeaponConfig.ExplosionDamage, NudgedImpactLocation, WeaponConfig.ExplosionRadius, WeaponConfig.DamageType, TArray<AActor*>(), this, MyController.Get());

	////Spawn any explosion effects if there is one
	//if (ExplosionTemplate)
	//	const FRotator SpawnRotation = Impact.ImpactNormal.Rotation();

	//	AWAWExplosionEffect* EffectActor = GetWorld()->SpawnActorDeferred<AWAWExplosionEffect>(ExplosionTemplate, NudgedImpactLocation, SpawnRotation);
	//	//Tell the explosion effect which material we impacted so that it can spawn the appropriate FX
	//	if (EffectActor)
	//	{
	//		EffectActor->SurfaceHit = Impact;
	//		UGameplayStatics::FinishSpawningActor(EffectActor, FTransform(SpawnRotation, NudgedImpactLocation));
	//	}

	//If we hit something then show the impact point
	if (ImpactTemplate && Impact.bBlockingHit)
		FHitResult UseImpact = Impact;

		// trace again to find component lost during replication
		if (!Impact.Component.IsValid())
			const FVector StartTrace = Impact.ImpactPoint + Impact.ImpactNormal * 10.0f;
			const FVector EndTrace = Impact.ImpactPoint - Impact.ImpactNormal * 10.0f;
			FHitResult Hit = AWAWWeapon::WeaponTrace(GetWorld(), MyController.Get(), StartTrace, EndTrace);
			UseImpact = Hit;
//			UE_LOG(LogProjectile, Warning, TEXT("Impact point was not valid, recomputed impact point to spawn FX"));

		//Spawn the bullet holes / FX at the impact point and set type of surface it hit
		AWAWImpactEffect* EffectActor = GetWorld()->SpawnActorDeferred<AWAWImpactEffect>(ImpactTemplate, Impact.ImpactPoint, Impact.ImpactNormal.Rotation());
		if (EffectActor)
			EffectActor->SurfaceHit = UseImpact;
			UGameplayStatics::FinishSpawningActor(EffectActor, FTransform(Impact.ImpactNormal.Rotation(), Impact.ImpactPoint));

	//We will not explode more than once, so set this to true
	bExploded = true;

Here is the projectile replication setup:



Anyone have any ideas why the impact FX will not show up on clients unless the server spawns something that is attached to the projectile? Could it be getting destroyed too soon to replicate to clients?

Hey there. This may be a shot in the dark. I’ve been snooping around the ShooterGame project as well and noticed that oddly enough, they never directly call their RepNotify functions. I’ve asked a question about this as the way it’s supposed to work in C++ is:

  1. Set variable
  2. Manually call RepNotify function

I notice that above in your code, you don’t call OnRep_Exploded() after setting bExploded = true; Maybe try this and see how it works. I don’t understand why the sample never calls it and yet the clients still get the notifies, I’m wondering if there’s a mystery config variable somewhere.

After some more thought, I’m wondering if the RepNotifies might piggy-back on other network traffic without explicitly being called. This might explain why spawning an actor causes the notify to get called since spawning on the server will cause the actor to get replicated and as a result the RepNotify might get thrown into the bundle of packets.

I think hyperdr1ve is on to something there.

Try setting “always relevant” to true and upping the network priority? I would expect that would cause the network traffic for that object to be sent when it changes. At the least it will narrow it down to a network packet issue. Also, did you set the DO_LIFETIME stuff on the variable?

It’d really help if we had a thorough breakdown of all the networking stuff in shootergame. But not going to happen so I guess we muddle along.

OK, I finally figured this out. I used the work-around until just now. I was working on grenades and I was having issues until I figured out that they were getting auto destroyed. I looked in the engine source to see their logic and basically auto-destroy will destroy the actor if there is no sound, no particle system, and no timeline components. So I needed the particle system component because it prevented the actor from getting auto destroyed. Here is the culprit:
bAutoDestroyWhenFinished = true;

Just remove that line and it will work properly. We may have added that to shooter code, I’m not sure right now, but here is the logic in the engine source:

void AActor::Tick( float DeltaSeconds )

	if (bAutoDestroyWhenFinished)
		bool bOKToDestroy = true;

		// @todo: naive implementation, needs improved
		TInlineComponentArray<UActorComponent*> Components;

		for (int32 CompIdx=0; CompIdx<Components.Num(); ++CompIdx)
			UActorComponent* const Comp = Components[CompIdx];

			UParticleSystemComponent* const PSC = Cast<UParticleSystemComponent>(Comp);
			if ( PSC && (PSC->bIsActive || !PSC->bWasCompleted) )
				bOKToDestroy = false;

			UAudioComponent* const AC = Cast<UAudioComponent>(Comp);
			if (AC && AC->IsPlaying())
				bOKToDestroy = false;

			UTimelineComponent* const TL = Cast<UTimelineComponent>(Comp);
			if (TL && TL->IsPlaying())
				bOKToDestroy = false;

		// die!
		if (bOKToDestroy)