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.