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.
- 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);
}
-
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.
-
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.