Advanced projectile replication

Hello everyone,

I am doing some projectile replication trials to mitigate the client lag but did not find good online ressources on this topic. I came across the UnrealTournament source code and found interesting things. Do you have other ideas or things I could change?

My current way of doing thing is like this:
I have a GameplayAbility (from GAS in Local Predicted ) so it’s first runs on the client then on the server.

  1. The client fires the ability, it spawns a dummy projectile which is not replicated. I use IsNetRelevantFor to remove the replicated projectile on the owning client, I want to only see the Dummy!
bool ARPGProjectile::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
	return !IsOwnedBy(ViewTarget);
}
  1. The server spawns the “real” projectile which is replicated and will do the damage etc… I do a “catchup” tick with delta=client ping to try to align the server projectile and the Dummy.

  2. The projectile is getting replicated on the other clients.

Code here below:

Projectile class .h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameplayEffect.h"

#include "RPGProjectile.generated.h"

/*
* Base class for projectiles with blueprint OnCollision event
* Spawns a Dummy projectile on the Owning client
*/
UCLASS()
class RPGGAME_API ARPGProjectile : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ARPGProjectile();

	UPROPERTY(BlueprintReadOnly, Replicated)
	float InitialSpeed = 0.f;

	UPROPERTY(BlueprintReadWrite, Meta = (ExposeOnSpawn = true))
	FGameplayEffectSpecHandle DamageEffectSpecHandle;

	UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
	class UProjectileMovementComponent* ProjectileMovement;

	//A sphere component to make the linetrace each frame for collision detection
	UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
	class USphereComponent* CollisionComp;

	UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
	class USceneComponent* SceneComponent;

	/** Server catchup tick to mitigate client ping */
	void CatchupTick(float DeltaTime);

	UFUNCTION(BlueprintImplementableEvent)
	void OnCollision(FHitResult OutHits);

protected:

	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;

	virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;

#if WITH_EDITORONLY_DATA
	UPROPERTY(EditAnywhere, Category = "RPG|Projectile")
	bool debug = false;
#endif

private:
	void InitializeLineTrace();
	void PerformLineTrace();

	UPROPERTY()
	FVector LastTickLocation;

	UPROPERTY()
	TArray<AActor*> IgnoredActors;
};

Projectile class .cpp
#include "AbilitySystem/GameplayAbilities/RPGProjectile.h"

#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
#include "Net/UnrealNetwork.h"


ARPGProjectile::ARPGProjectile()
{
	PrimaryActorTick.bCanEverTick = true;
	bReplicates = GetLocalRole() == ROLE_Authority;
	SceneComponent = CreateDefaultSubobject<USceneComponent>(FName("SceneComponent"));
	RootComponent = SceneComponent;
	ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(FName("ProjectileMovement"));
	CollisionComp = CreateDefaultSubobject<USphereComponent>(FName("CollisionComp"));
	CollisionComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	CollisionComp->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
}

void ARPGProjectile::CatchupTick(float DeltaTime)
{
	if (ProjectileMovement)
	{
		ProjectileMovement->TickComponent(DeltaTime, ELevelTick::LEVELTICK_All, nullptr);
	}
}

void ARPGProjectile::BeginPlay()
{
	Super::BeginPlay();
	InitializeLineTrace();
	ProjectileMovement->Velocity = ProjectileMovement->Velocity.GetSafeNormal() * InitialSpeed;
}

void ARPGProjectile::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	PerformLineTrace();
}

bool ARPGProjectile::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
	return !IsOwnedBy(ViewTarget);
}

void ARPGProjectile::InitializeLineTrace()
{
	IgnoredActors.Empty();
	IgnoredActors.Add(GetOwner());
	IgnoredActors.Add(GetInstigator());
	IgnoredActors.Add(this);
	LastTickLocation = CollisionComp->GetComponentLocation();
}

void ARPGProjectile::PerformLineTrace()
{
	TArray<struct FHitResult> OutHits;

	static const FName SphereTraceMultiName(TEXT("SphereTraceMulti"));
	FCollisionQueryParams Params = FCollisionQueryParams::DefaultQueryParam;
	Params.bTraceComplex = true;
	Params.AddIgnoredActors(IgnoredActors);

	bool bHit = GetWorld()->SweepMultiByChannel(OutHits, LastTickLocation, CollisionComp->GetComponentLocation(), FQuat::Identity, ECC_WorldStatic, 
		FCollisionShape::MakeSphere(CollisionComp->GetScaledSphereRadius()), Params);


	if (bHit)
	{
		for (FHitResult& Hit : OutHits)
		{
			if (Hit.GetActor() != nullptr)
			{
				IgnoredActors.Add(Hit.GetActor());

				//@TODO: Check if the actor should stop the projectile?
				OnCollision(Hit);
			}
		}
		SetActorTickEnabled(false);
	}

#if WITH_EDITOR
	if(debug)
	{
		FVector const TraceVec = CollisionComp->GetComponentLocation() - LastTickLocation;
		float const Dist = TraceVec.Size();

		FVector const Center = LastTickLocation + TraceVec * 0.5f;
		float const HalfHeight = (Dist * 0.5f) + CollisionComp->GetScaledSphereRadius();

		FQuat const CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
		::DrawDebugCapsule(GetWorld(), Center, HalfHeight, CollisionComp->GetScaledSphereRadius(), CapsuleRot, bHit ? FColor::Red : FColor::Green, false, 5.0f);
	}
#endif

	LastTickLocation = CollisionComp->GetComponentLocation();
}

void ARPGProjectile::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME_CONDITION(ThisClass, InitialSpeed, COND_SkipOwner);
}
Gameplay Ability class .h
#pragma once

#include "CoreMinimal.h"
#include "AbilitySystem/GameplayAbilities/RPGGameplayAbility.h"
#include "RPGGameplayAbility_Projectile.generated.h"

class ARPGProjectile;

USTRUCT()
struct FDelayedProjectileInfo
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY()
	TSubclassOf<ARPGProjectile> ProjectileClass;

	UPROPERTY()
	FTransform Transform;

	UPROPERTY()
	float InitialSpeed = 0.f;

	UPROPERTY()
	FGameplayEffectSpecHandle DamageEffectSpecHandle;
};

/**
 * Base GA that spawns a replicated projectile
 */
UCLASS()
class RPGGAME_API URPGGameplayAbility_Projectile : public URPGGameplayAbility
{
	GENERATED_BODY()
	
public:
	UFUNCTION(BlueprintCallable, Category = "RPG|GameplayAbilities")
	ARPGProjectile* SpawnProjectile(TSubclassOf<ARPGProjectile> ProjectileClass, const FTransform& Transform, const FGameplayEffectSpecHandle& SpecHandle, AActor* Owner, APawn* Instigator, float MaxPredictionPing, float InitialSpeed, float Range);

private:
	void SpawnProjectileDelayed();

	//Timer handle used if the projectile is delayed due to high ping
	FTimerHandle SpawnDelayedFakeProjHandle;
	/** Delayed projectile information */
	UPROPERTY()
	FDelayedProjectileInfo DelayedProjectile;
};

Gameplay Ability class .cpp
#include "AbilitySystem/GameplayAbilities/RPGGameplayAbility_Projectile.h"

#include "AbilitySystem/GameplayAbilities/RPGProjectile.h"
#include "Player/RPGPlayerState.h"

ARPGProjectile* URPGGameplayAbility_Projectile::SpawnProjectile(TSubclassOf<ARPGProjectile> ProjectileClass, const FTransform& Transform, 
	const FGameplayEffectSpecHandle& DamageEffectSpecHandle, AActor* Owner, APawn* Instigator, float MaxPredictionPing, float InitialSpeed, float Range)
{
	ENetRole Role = GetOwningActorFromActorInfo()->GetLocalRole();

	float SleepTime = 0.f;
	float CatchupTime = 0.f;

	if (ARPGPlayerState* PlayerState = Cast<ARPGPlayerState>(GetOwningActorFromActorInfo()))
	{
		if ((PlayerState->ExactPing / 1000.f) > MaxPredictionPing)
		{
			SleepTime = FMath::Max(0.f, (PlayerState->ExactPing / 1000.f) - MaxPredictionPing);
			CatchupTime = MaxPredictionPing;
		}
		else
		{
			CatchupTime = PlayerState->ExactPing / 1000.f;
		}
	}

	// Ping is higher than max prediction ping, so we need to delay the projectile spawn on client
	if (SleepTime > 0.f && Role != ROLE_Authority)
	{
		if (!GetWorld()->GetTimerManager().IsTimerActive(SpawnDelayedFakeProjHandle))
		{
			DelayedProjectile.ProjectileClass = ProjectileClass;
			DelayedProjectile.Transform = Transform;
			DelayedProjectile.InitialSpeed = InitialSpeed;
			DelayedProjectile.DamageEffectSpecHandle = DamageEffectSpecHandle;
			GetWorld()->GetTimerManager().SetTimer(SpawnDelayedFakeProjHandle, this, 
				&URPGGameplayAbility_Projectile::SpawnProjectileDelayed, SleepTime, false);
		}
		return nullptr;
	}

	ARPGProjectile* Projectile = GetWorld()->SpawnActorDeferred<ARPGProjectile>(ProjectileClass, Transform, Owner,
		Instigator, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
	Projectile->DamageEffectSpecHandle = DamageEffectSpecHandle;
	Projectile->InitialSpeed = InitialSpeed;
	Projectile->FinishSpawning(Transform);
	if (Role == ROLE_Authority && CatchupTime > 0.f)
	{
		Projectile->CatchupTick(CatchupTime);
	}

	return Projectile;
}

void URPGGameplayAbility_Projectile::SpawnProjectileDelayed()
{
	ARPGProjectile* Projectile = GetWorld()->SpawnActorDeferred<ARPGProjectile>(DelayedProjectile.ProjectileClass, DelayedProjectile.Transform, 
		GetOwningActorFromActorInfo(), nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
	Projectile->InitialSpeed = DelayedProjectile.InitialSpeed;
	Projectile->DamageEffectSpecHandle = DelayedProjectile.DamageEffectSpecHandle;
	Projectile->FinishSpawning(DelayedProjectile.Transform);
}

BP Gameplay Ability

Gameplay Ability projectile posted by anonymous | blueprintUE | PasteBin For Unreal Engine

I see in the UnrealTournament they are doing further Synchronisation between the Dummy and Server projectile, I still don’t really understand how.

Do you have some improvement suggestions or comments you could share?

Thank for reading.

1 Like

I can’t edit my post so I made a reply.

An updated version of the network projectile since the first one had issues:

Projectile class.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameplayEffect.h"

#include "RPGProjectile.generated.h"

/*
* Base class for projectiles with blueprint OnCollision event
* Spawns a Dummy projectile on the Owning client
*/
UCLASS()
class RPGGAME_API ARPGProjectile : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor's properties
	ARPGProjectile();

	UPROPERTY(BlueprintReadOnly, Replicated)
	float InitialSpeed = 0.f;

	UPROPERTY(BlueprintReadWrite, Meta = (ExposeOnSpawn = true))
	FGameplayEffectSpecHandle DamageEffectSpecHandle;

	UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
	class UProjectileMovementComponent* ProjectileMovement;

	//A sphere component to make the linetrace each frame for collision detection
	UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
	class USphereComponent* CollisionComp;

	UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
	class USceneComponent* SceneComponent;

	/** Server catchup tick to mitigate client ping */
	void CatchupTick(float DeltaTime);

	UFUNCTION(BlueprintImplementableEvent)
	void OnCollision(FHitResult OutHits);

protected:

	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;

	virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;

	UPROPERTY(EditAnywhere, Category = "RPG|Projectile")
	bool EnableAdvancedReplication = true;

private:
	void InitializeLineTrace();
	void PerformLineTrace();

	// Determine the trace channel to use for the weapon trace(s)
	virtual ECollisionChannel DetermineTraceChannel(FCollisionQueryParams& TraceParams, bool bIsSimulated) const;

	UPROPERTY()
	FVector LastTickLocation;

	UPROPERTY()
	TArray<AActor*> IgnoredActors;
};

Projectile class.cpp
#include "AbilitySystem/GameplayAbilities/RPGProjectile.h"

#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
#include "Net/UnrealNetwork.h"

#include "Player/RPGPlayerController.h"
#include "Physics/RPGCollisionChannels.h"

namespace RPGConsoleVariables
{
	static float DrawProjectilePathDuration = 0.0f;
	static FAutoConsoleVariableRef CVarDrawProjectilePathDuration(
		TEXT("RPG.Debug.Projectile.Path.Duration"),
		DrawProjectilePathDuration,
		TEXT("Should we do debug drawing for projectile path (if above zero, sets how long (in seconds))"),
		ECVF_Default);

	static bool DrawSimulatedProjectileOnOwner = false;
	static FAutoConsoleVariableRef CVarDrawSimulatedProjectileOnOwner(
		TEXT("RPG.Debug.Projectile.DrawSimulatedOnOwner"),
		DrawSimulatedProjectileOnOwner,
		TEXT("Should we spawn the simulated projectile on the owner (with the proxy one)"),
		ECVF_Default);
}

ARPGProjectile::ARPGProjectile()
{
	PrimaryActorTick.bCanEverTick = true;
	bReplicates = true;
	SetReplicateMovement(false);

	SceneComponent = CreateDefaultSubobject<USceneComponent>(FName("SceneComponent"));
	RootComponent = SceneComponent;

	CollisionComp = CreateDefaultSubobject<USphereComponent>(FName("CollisionComp"));
	CollisionComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	CollisionComp->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);

	ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(FName("ProjectileMovement"));
	ProjectileMovement->UpdatedComponent = SceneComponent;
	ProjectileMovement->MaxSpeed = 6000.f;
	ProjectileMovement->bRotationFollowsVelocity = true;
	ProjectileMovement->bShouldBounce = false;

	// Die after 10 seconds by default
	InitialLifeSpan = 10.0f;

	NetPriority = 2.f;
	MinNetUpdateFrequency = 100.0f;
}

void ARPGProjectile::CatchupTick(float DeltaTime)
{
	if (EnableAdvancedReplication && ProjectileMovement)
	{
		ProjectileMovement->TickComponent(DeltaTime, ELevelTick::LEVELTICK_All, nullptr);
	}
}

void ARPGProjectile::BeginPlay()
{
	Super::BeginPlay();
	InitializeLineTrace();
	
	ProjectileMovement->Velocity = ProjectileMovement->Velocity.GetSafeNormal() * InitialSpeed;

	if (EnableAdvancedReplication)
	{
		if (GetLocalRole() == ROLE_Authority)
		{

		}

		if (GetLocalRole() == ROLE_SimulatedProxy)
		{
			if (ARPGPlayerController* LocalPlayerController = Cast<ARPGPlayerController>(GEngine->GetFirstLocalPlayerController(GetWorld())))
			{
				// Move projectile to match where it is on server now (to make up for replication time)
				float CatchupTickDelta = LocalPlayerController->GetPredictionTime();
				if (CatchupTickDelta > 0.f)
				{
					CatchupTick(CatchupTickDelta);
				}
			}
		}
	}
}

void ARPGProjectile::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	PerformLineTrace();
}

bool ARPGProjectile::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
	return !IsOwnedBy(ViewTarget) || RPGConsoleVariables::DrawSimulatedProjectileOnOwner;
}

void ARPGProjectile::InitializeLineTrace()
{
	IgnoredActors.Empty();
	IgnoredActors.Add(this);
	IgnoredActors.Add(GetInstigator());
	// Ignore any actors attached to the avatar doing the shooting
	TArray<AActor*> AttachedActors;
	GetInstigator()->GetAttachedActors(/*out*/ AttachedActors);
	IgnoredActors.Append(AttachedActors);

	LastTickLocation = CollisionComp->GetComponentLocation();
}

void ARPGProjectile::PerformLineTrace()
{
	TArray<struct FHitResult> OutHits;

	static const FName SphereTraceMultiName(TEXT("SphereTraceMulti"));
	FCollisionQueryParams Params = FCollisionQueryParams::DefaultQueryParam;
	Params.bReturnPhysicalMaterial = true;
	Params.AddIgnoredActors(IgnoredActors);

	const ECollisionChannel TraceChannel = DetermineTraceChannel(Params, false);

	bool bHit = GetWorld()->SweepMultiByChannel(OutHits, LastTickLocation, CollisionComp->GetComponentLocation(), FQuat::Identity, TraceChannel,
		FCollisionShape::MakeSphere(CollisionComp->GetScaledSphereRadius()), Params);

	if (bHit)
	{
		for (FHitResult& Hit : OutHits)
		{
			if (Hit.GetActor() != nullptr)
			{
				IgnoredActors.Add(Hit.GetActor());

				//@TODO: Check if the actor should stop the projectile?
				OnCollision(Hit);
			}
		}
	}

#if ENABLE_DRAW_DEBUG
	if (RPGConsoleVariables::DrawProjectilePathDuration > 0.f)
	{
		FVector const TraceVec = CollisionComp->GetComponentLocation() - LastTickLocation;
		float const Dist = TraceVec.Size();

		FVector const Center = LastTickLocation + TraceVec * 0.5f;
		float const HalfHeight = (Dist * 0.5f) + CollisionComp->GetScaledSphereRadius();

		FColor color = FColor::Black;

		switch (GetLocalRole())
		{
		case 0:
			color = FColor::Black;
			break;
		case 1:
			color = FColor::Red;
			break;
		case 2:
			color = FColor::Green;
			break;
		case 3:
			color = FColor::Blue;
			break;
		}

		FQuat const CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
		::DrawDebugCapsule(GetWorld(), Center, HalfHeight, CollisionComp->GetScaledSphereRadius(), CapsuleRot, color, false, RPGConsoleVariables::DrawProjectilePathDuration);
	}
#endif // ENABLE_DRAW_DEBUG

	LastTickLocation = CollisionComp->GetComponentLocation();
}

ECollisionChannel ARPGProjectile::DetermineTraceChannel(FCollisionQueryParams& TraceParams, bool bIsSimulated) const
{
	return RPG_TraceChannel_Weapon;
}

void ARPGProjectile::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
#if ENABLE_DRAW_DEBUG
	DOREPLIFETIME_CONDITION(ThisClass, InitialSpeed, COND_None);
#else
	DOREPLIFETIME_CONDITION(ThisClass, InitialSpeed, COND_SkipOwner);
#endif
}
Gameplay Ability class.h
#pragma once

#include "CoreMinimal.h"

#include "AbilitySystem/GameplayAbilities/RPGGameplayAbility_RangedWeapon.h"

#include "RPGGameplayAbility_Projectile.generated.h"

class ARPGProjectile;

USTRUCT()
struct FDelayedProjectileInfo
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY()
	TSubclassOf<ARPGProjectile> ProjectileClass;

	UPROPERTY()
	FTransform Transform;

	UPROPERTY()
	float InitialSpeed = 0.f;

	UPROPERTY()
	FGameplayEffectSpecHandle DamageEffectSpecHandle;
};

/**
 * Base GA that spawns a replicated projectile
 */
UCLASS()
class RPGGAME_API URPGGameplayAbility_Projectile : public URPGGameplayAbility_RangedWeapon
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "RPG|GameplayAbilities")

	ARPGProjectile* SpawnProjectile(TSubclassOf<ARPGProjectile> ProjectileClass, const FTransform& Transform, const FGameplayEffectSpecHandle& SpecHandle, float InitialSpeed, float Range);

private:
	void SpawnProjectileDelayed();

	//Timer handle used if the projectile is delayed due to high ping
	FTimerHandle SpawnDelayedFakeProjHandle;
	/** Delayed projectile information */
	UPROPERTY()
	FDelayedProjectileInfo DelayedProjectile;
};
Gameplay Ability class.cpp
#include "AbilitySystem/GameplayAbilities/RPGGameplayAbility_Projectile.h"

#include "AbilitySystem/GameplayAbilities/RPGProjectile.h"
#include "Player/RPGPlayerController.h"

ARPGProjectile* URPGGameplayAbility_Projectile::SpawnProjectile(TSubclassOf<ARPGProjectile> ProjectileClass, const FTransform& Transform,
	const FGameplayEffectSpecHandle& DamageEffectSpecHandle, float InitialSpeed, float Range)
{
	ARPGPlayerController* PlayerController = GetRPGPlayerControllerFromActorInfo();

	ENetRole Role = GetAvatarActorFromActorInfo()->GetLocalRole();

	float CatchupTime = PlayerController ? PlayerController->GetPredictionTime() : 0.f;

	if (CatchupTime > 0.f && Role == ROLE_AutonomousProxy)
	{
		float SleepTime = PlayerController->GetProjectileSleepTime();
		// Ping is higher than max prediction ping, so we need to delay the projectile spawn on client
		if (SleepTime > 0.f)
		{
			if (!GetWorld()->GetTimerManager().IsTimerActive(SpawnDelayedFakeProjHandle))
			{
				DelayedProjectile.ProjectileClass = ProjectileClass;
				DelayedProjectile.Transform = Transform;
				DelayedProjectile.InitialSpeed = InitialSpeed;
				DelayedProjectile.DamageEffectSpecHandle = DamageEffectSpecHandle;
				GetWorld()->GetTimerManager().SetTimer(SpawnDelayedFakeProjHandle, this,
					&URPGGameplayAbility_Projectile::SpawnProjectileDelayed, SleepTime, false);
			}
			return nullptr;
		}
	}

	ARPGProjectile* Projectile = GetWorld()->SpawnActorDeferred<ARPGProjectile>(ProjectileClass, Transform, GetAvatarActorFromActorInfo(),
		Cast<APawn>(GetAvatarActorFromActorInfo()), ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
	Projectile->DamageEffectSpecHandle = DamageEffectSpecHandle;
	Projectile->InitialSpeed = InitialSpeed;
	Projectile->FinishSpawning(Transform);
	if (Role == ROLE_Authority && CatchupTime > 0.f)
	{
		Projectile->CatchupTick(CatchupTime);
		Projectile->InitialSpeed = Projectile->GetVelocity().Size();
	}

	return Projectile;
}

void URPGGameplayAbility_Projectile::SpawnProjectileDelayed()
{
	ARPGProjectile* Projectile = GetWorld()->SpawnActorDeferred<ARPGProjectile>(DelayedProjectile.ProjectileClass, DelayedProjectile.Transform,
		GetAvatarActorFromActorInfo(), Cast<APawn>(GetAvatarActorFromActorInfo()), ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
	Projectile->InitialSpeed = DelayedProjectile.InitialSpeed;
	Projectile->DamageEffectSpecHandle = DelayedProjectile.DamageEffectSpecHandle;
	Projectile->FinishSpawning(DelayedProjectile.Transform);
}

I also use the target data from the client which is send via RPC to the server for projectile spawn transform but this is another topic.

Thanks for reading.