Multiplayer Projectile Hit Detection Problem

Hi, I’m trying to do Projectile Hit detection in multiplayer game. I’m using next approach:

  1. On the server side I’m saving all updated character’s positions with world timestamp (PositionUpdated method).
  2. When client projectile Hit some character I’m sending Hit information to the server with server world timestamp (ServerCheckHit method). (All projectile fired only on clients on server we only check Hit’s from client)
  3. On the server side I’m rewind character position to timestamp (GetRewindLocation method) and check position with Hit info (using simple sphere vs sphere intersection).

In some cases this approach working, but in some cases it has undefined behavior (please see screenshot attached “Lag Compensation Problem.png”).

I can understand why rewinded serever position is different with client check position (the timestamps is equals). Please help me find where is the problem?

Thanks in advance, Alex.

Details:

CharacterMovementCompoenet.cpp

void USCharacterMovementComponent::PerformMovement(float DeltaSeconds)
{
	Super::PerformMovement(DeltaSeconds);

	if (CharacterOwner != NULL)
	{
		ASCharacter* SCharacterOwner = Cast<ASCharacter>(CharacterOwner);
		if (SCharacterOwner != NULL)
		{
			SCharacterOwner->PositionUpdated(); // Save character position for lag compensation
		}
	}
}

Character.h

...

/** Saved character transform for hit checking (lag compensation) */
TArray<FSavedCharacterTransform> SavedTransforms;

...

Character.cpp

void ASCharacter::PositionUpdated()
{
	const float WorldTime = GetWorld()->GetTimeSeconds();
	if (SCharacterMovement != NULL)
	{
		new(SavedTransforms)FSavedCharacterTransform(GetActorLocation(), GetLookRotation(), SCharacterMovement->Velocity, WorldTime, SCharacterMovement->GetCurrentSynchTime());
	}

	if (SavedTransforms.Num() > 1 && SavedTransforms[1].Time < WorldTime - MaxLagCompensationSeconds)
	{
		SavedTransforms.RemoveAt(0);
	}
}

FVector ASCharacter::GetRewindLocation(float TargetTime)
{
	FVector TargetLocation = GetActorLocation();
	if (GetWorld()->GetTimeSeconds() - TargetTime > 0.001f)
	{
		for (int32 i = SavedTransforms.Num() - 1; i >= 0; i--)
		{
			TargetLocation = SavedTransforms[i].Location;
			if (SavedTransforms[i].Time < TargetTime)
			{
				if (i < SavedTransforms.Num() - 1)
				{
					float Percent  = (SavedTransforms[i + 1].Time == SavedTransforms[i].Time) ? 1.0f : (TargetTime - SavedTransforms[i].Time) / (SavedTransforms[i + 1].Time - SavedTransforms[i].Time);
					TargetLocation = SavedTransforms[i].Location + Percent * (SavedTransforms[i + 1].Location - SavedTransforms[i].Location);
				}
				break;
			}
		}
	}

	return TargetLocation;
}

void ASCharacter::ServerCheckHit_Implementation(float TargetTime, const FHitResult& HitInfo, const FVector_NetQuantize& ShotDirection, ASCharacter* OtherCharacter)
{
	if (OtherCharacter == NULL)
	{
		return;
	}

	if (Weapon != NULL && Weapon->CanFire())
	{
		FVector RewindLocation = OtherCharacter->GetRewindLocation(TargetTime);
		float   Distance       = (RewindLocation - HitInfo.ImpactPoint).Size2D();
		if (Distance < OtherCharacter->CollisionCheckRadius) // CollisionCheckRadius = 35; projectile sphere collision radius = 5; In some cases I'm receiving Distance > 60.0f (this is problem part) :(
		{
			// Take Damage
		}
	}
}

Projectile.cpp

void ASProjectile::ProcessHit_Implementation(AActor* OtherActor, const FHitResult& ImpactResult)
{
	bool bIsOtherActorCharacter = false;
	if (OtherActor != NULL && OtherActor != this && OtherActor != Instigator && Instigator != NULL)
	{
		ASCharacter* OwnerCharacter = Cast<ASCharacter>(Instigator);
		ASCharacter* OtherCharacter = Cast<ASCharacter>(OtherActor);
		AGameState*  GameState      = GetWorld()->GetGameState();
		if (GameState != NULL && OwnerCharacter != NULL && OwnerCharacter->IsLocallyControlled() && OtherCharacter != NULL)
		{
			OwnerCharacter->ServerCheckHit(GameState->GetServerWorldTimeSeconds(), ImpactResult, GetActorRotation().RotateVector(FVector::ForwardVector), OtherCharacter); // Check this Hit on server using Server Worl time seconds
		}

		// Client impact logic
	}

	// Shutdown logic
}

98008-lag+compensation+problem.png