Is there anyway to force the replication order for variables?
I am getting a race condition at line 190 between AbilityStates and ActiveAbility.
If it hits line 190 before AbilityStates is replicated then the Phase will be Active.
If it hits it after then the phase will be Cooldown.
// ** FPSAbilityComponent.h **
UENUM(BlueprintType)
enum class EFPSAbilityPhase : uint8
{
Ready,
Start,
Active,
Cooldown,
Disabled
};
USTRUCT(BlueprintType)
struct FFPSAbilityState
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
EFPSAbilityPhase Phase;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
float Charges;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
float MaxCharges;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
float Cooldown;
// Duration Remaining
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
float Duration;
};
USTRUCT()
struct FFPSActiveAbility
{
GENERATED_BODY()
UPROPERTY()
uint8 ID;
UPROPERTY()
FFPSAbilityAnimation Animation;
UPROPERTY()
uint8 ForceRep : 1;
};
UCLASS( ClassGroup=(Abilities), meta=(BlueprintSpawnableComponent) )
class FPSGAME_API UFPSAbilityComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Called when the game starts
virtual void BeginPlay() override;
UFUNCTION()
void OnRep_ActiveAbility();
UFUNCTION()
void OnRep_AbilityStates();
// Set of abilities
UPROPERTY(Transient, Replicated, BlueprintReadOnly, Category = Abilities)
class UFPSAbilitySet* AbilitySet;
UPROPERTY(EditDefaultsOnly, Category = Abilities)
TArray<TSubclassOf<class UFPSAbility>> DefaultAbilityClasses;
UPROPERTY(Transient, Replicated)
TArray<class UFPSAbility*> Abilities;
// Replicated ability state
UPROPERTY(Transient, VisibleAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_AbilityStates, Category = Ability)
TArray<FFPSAbilityState> AbilityStates;
UPROPERTY(Transient, ReplicatedUsing = OnRep_ActiveAbility)
FFPSActiveAbility ActiveAbility;
}
// ** FPSAbilityComponent.cpp**
void UFPSAbilityComponent::BeginPlay()
{
Super::BeginPlay();
if (GetOwner() && GetOwner()->HasAuthority())
{
// Create the AbilitySet
AbilitySet = NewObject<UFPSAbilitySet>(this, DefaultAbilitySetClass);
for (int i = 0; i < AbilitySet->DefaultAbilityClasses.Num(); i++)
{
// Create the Ability and set it's Outer
UFPSAbility* Ability = NewObject<UFPSAbility>(this, AbilitySet->DefaultAbilityClasses[i]);
Ability->ID = i;
// Add the Ability to the AbilitySet
AbilitySet->Abilities.Add(Ability);
// Create the AbilityStates
FFPSAbilityState AbilityState;
// Set the initial property values
AbilityState.Charges = Ability->Charges;
AbilityState.MaxCharges = Ability->MaxCharges;
AbilityState.Cooldown = Ability->Cooldown;
AbilityState.Duration = Ability->Duration;
AbilityState.Phase = EFPSAbilityPhase::Ready;
// Add the AbilityState to the Array
AbilityStates.Add(AbilityState);
}
}
}
void UFPSAbilityComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UFPSAbilityComponent, AbilityStates);
// Replicate the Ability Set to everyone
// The Ability Set is responsible for finer replication granularity
DOREPLIFETIME(UFPSAbilityComponent, AbilitySet);
// Replicate the Active Ability to everyone except owner
// The owner already has the Abilities since they are replicated to him
DOREPLIFETIME_CONDITION(UFPSAbilityComponent, ActiveAbility, COND_SkipOwner);
}
void UFPSAbilityComponent::StartAbility(int32 Index)
{
// Local simulation for owning client
if (GetOwner() && Cast<AFPSCharacter>(GetOwner())->IsLocallyControlled())
{
if (AbilitySet)
{
if (AbilityStates.IsValidIndex(Index) && AbilityStates[Index].Phase == EFPSAbilityPhase::Ready
&& AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Start && AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Active)
{
LocalActiveAbilityID = Index;
SimulateAbilityLocally(Index);
}
}
}
ServerStartAbility(Index);
}
bool UFPSAbilityComponent::ServerStartAbility_Validate(int32 Index)
{
return true;
}
void UFPSAbilityComponent::ServerStartAbility_Implementation(int32 Index)
{
if (AbilitySet && AbilitySet->Abilities.IsValidIndex(Index) && AbilitySet->Abilities[Index] != nullptr)
{
if (AbilityStates[Index].Phase == EFPSAbilityPhase::Ready
&& AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Start && AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Active)
{
ConsumeCharge(Index);
AbilitySet->Abilities[Index]->Start();
}
}
}
void UFPSAbilityComponent::SimulateAbility(int32 Index)
{
// Animation
if (ActiveAbility.Animation.Pawn3P)
{
AFPSCharacter* Character = Cast<AFPSCharacter>(GetOwner());
// Play the Third Person Animation
Character->PlayAnimMontage(ActiveAbility.Animation.Pawn3P);
// Play the First Person Animation
Character->PlayAnimMontageFP(ActiveAbility.Animation.Pawn1P);
}
// Create the Ability since we aren't replicating to other clients (simulated proxies)
UFPSAbility* Ability = NewObject<UFPSAbility>(this, AbilitySet->DefaultAbilityClasses[Index]);
if (AbilityStates[Index].Phase == EFPSAbilityPhase::Start)
{
Ability->Simulate();
}
// ** RACE CONDITION, SOMETIMES PHASE IS ACTIVE AND SOMETIMES IT'S COOLDOWN
else if (AbilityStates[Index].Phase == EFPSAbilityPhase::Active || AbilityStates[Index].Phase == EFPSAbilityPhase::Cooldown)
{
Ability->SimulateExecute();
}
}
void UFPSAbilityComponent::SimulateAbilityLocally(int32 Index)
{
if (AbilitySet)
{
AFPSCharacter* Character = Cast<AFPSCharacter>(GetOwner());
// Play the Third Person Animation
Character->PlayAnimMontage(AbilitySet->Abilities[Index]->Animation.Pawn3P);
// Play the First Person Animation
Character->PlayAnimMontageFP(AbilitySet->Abilities[Index]->Animation.Pawn1P);
if (AbilitySet->Abilities[Index]->bExecuteOnStart)
{
AbilitySet->Abilities[Index]->SimulateExecute();
}
else
{
AbilitySet->Abilities[Index]->Simulate();
}
}
}
void UFPSAbilityComponent::ConsumeCharge(int32 Index)
{
AbilityStates[Index].Charges--;
}
void UFPSAbilityComponent::OnRep_AbilityStates()
{
}
void UFPSAbilityComponent::OnRep_ActiveAbility()
{
SimulateAbility(ActiveAbility.ID);
}
void UFPSAbilityComponent::SetActiveAbility(int32 AbilityID, FFPSAbilityAnimation Animation)
{
ActiveAbility.ID = AbilityID;
ActiveAbility.Animation = Animation;
}
// ** FPSAbility.h **
UCLASS(Blueprintable, BlueprintType)
class FPSGAME_API UFPSAbility : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Ability)
void Start();
UFUNCTION(BlueprintCallable, Category = Ability)
void Execute();
UFUNCTION(BlueprintCallable, Category = Ability)
void End();
UFUNCTION(BlueprintImplementableEvent, Category = Ability)
void Simulate();
UFUNCTION(BlueprintImplementableEvent, Category = Ability)
void SimulateExecute();
UFUNCTION(BlueprintImplementableEvent, Category = Ability)
void OnStart();
// Blueprint implementable version of Execute
UFUNCTION(BlueprintImplementableEvent, Category = Ability)
void OnExecute();
UFUNCTION(BlueprintImplementableEvent, Category = Ability)
void OnEnd();
// Number of charges that can be stored at a time
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
float Charges;
// Max number of charges that a player can hold at a time (0 for infinite)
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
float MaxCharges;
// Number of seconds before this ability can be recast (0 for none)
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
float Cooldown;
// How charges are restored after use
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
EFPSAbilityChargeMode ChargeMode;
// Animation to be played when this ability is executed
UPROPERTY(EditDefaultsOnly, Category = Animation)
FFPSAbilityAnimation Animation;
UPROPERTY()
int32 ID;
// Cache the casted AbilityComponent we belong to
UPROPERTY(BlueprintReadOnly, Category = Ability)
class UFPSAbilityComponent* AbilityComponent;
// Whether it should call Execute on Start
UPROPERTY(EditDefaultsOnly, Category = Ability)
bool bExecuteOnStart;
UPROPERTY(EditDefaultsOnly, Category = Ability)
float Duration;
}
// ** FPSAbility.cpp **
void UFPSAbility::Start()
{
// Update Phase
if (AbilityComponent)
{
AbilityComponent->AbilityStates[ID].Phase = EFPSAbilityPhase::Start;
}
// Instant
if (bExecuteOnStart)
{
Execute();
AbilityComponent->SetActiveAbility(ID, Animation);
}
// Animation
else if (Animation.Pawn3P)
{
AbilityComponent->SetActiveAbility(ID, Animation);
// Manually call OnRep for the Server
AbilityComponent->OnRep_ActiveAbility();
}
// Force replication, so the same ability can be called multiple times in a row
AbilityComponent->ActiveAbility.ForceRep = AbilityComponent->ActiveAbility.ForceRep >= 1 ? 0 : AbilityComponent->ActiveAbility.ForceRep + 1;
OnStart();
}
void UFPSAbility::Execute()
{
// Update Phase
if (AbilityComponent)
{
AbilityComponent->AbilityStates[ID].Phase = EFPSAbilityPhase::Active;
}
// Call the blueprint event
OnExecute();
}
void UFPSAbility::End()
{
OnEnd();
// Update phase
if (AbilityComponent)
{
AbilityComponent->AbilityStates[ID].Phase = EFPSAbilityPhase::Cooldown;
}
}
// ** FPSAbilitySet.cpp **
void UFPSAbilitySet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Replicate Abilities to owning client only
DOREPLIFETIME_CONDITION(UFPSAbilitySet, Abilities, COND_OwnerOnly);
// Replicate Ability Classes to other clients, so they can create the active one when needed
DOREPLIFETIME_CONDITION(UFPSAbilitySet, DefaultAbilityClasses, COND_SkipOwner);
}