I have a Montage variable in my GameplayAbility class. Abiltiies are locally predicted, so when they are getting executed, PreActivate is called first on client and then on server.
I need to find a way to ensure that Montage is always the same on both client and server. I know that when you activate ability from event, you can pass in Payload, which can contain arbitrary data, but I activate the ability from TryActivateAbility():
...
if (!OwnerASC->TryActivateAbility(Spec->Handle))
{
bDone = true;
}
What is the best and most efficient way of handling this?
I faced a similar problem when attempting to activate an ability with payloads. The same applies if you want to activate an ability through input. Unfortunately, there isn’t a direct function for this—you’ll need to use ActivateAbilityFromEvent.
For your case i would considerer:
Use a random generator with the same seed on both the server and the client. This way, you will get the same random value on both ends. You could use the Ability Prediction Key as the seed to ensure consistency between the server and client.
Let the Client or Server select the montage and create an ability task to sync the montage between Server and Client.
If the ability is started by the client, it might be better to allow the client to select the montage and sync it to the server to reduce waiting time. However, this could open the door to potential cheating, as the client could always select the same montage. If it’s just a cooperative game, this approach should work fine. Otherwise, you might want to consider having the server select the montage and let the client sync with the server.
For example, check how UAbilityTask_WaitTargetData is implemented. You can try implementing your own AbilityTask or attempt to use UAbilityTask_WaitTargetData with custom target data that includes the Montage pointer.
So I did what you suggested with WaitTargetData and made my own implementation. In case someone needs it:
// Gameplay Ability
void UTBCAttackAbility::PlayMontageAndWait()
{
UAbilityTask_GetAttackMontage* GetAttackMontage{UAbilityTask_GetAttackMontage::CreateTask(this)};
GetAttackMontage->ValidData.AddWeakLambda(
this, [this](const UAnimMontage* AttackMontage)
{
Montage = AttackMontage;
Super::PlayMontageAndWait();
});
GetAttackMontage->ReadyForActivation();
}
// AbilityTask
void UAbilityTask_GetAttackMontage::Activate()
{
if (!AbilitySystemComponent.IsValid()) return;
UTBCAbilitySystemComponent* ASC{
StaticCast<UTBCAbilitySystemComponent*>(AbilitySystemComponent.Get())
};
if (Ability->GetCurrentActorInfo()->IsLocallyControlled())
{
ClientSetAttackMontage();
}
else
{
ASC->OnAbilityTaskAttackMontageSet.AddUObject(this, &ThisClass::OnTargetDataReplicatedCallback);
SetWaitingOnRemotePlayerData();
}
}
void UAbilityTask_GetAttackMontage::ClientSetAttackMontage()
{
FScopedPredictionWindow PredictionWindow{AbilitySystemComponent.Get()};
const TWeakObjectPtr AvatarActor{Ability->GetCurrentActorInfo()->AvatarActor};
if (!AvatarActor.IsValid()) return;
const UAnimMontage* AttackMontage{CastChecked<ICombatInterface>(AvatarActor.Get())->GetRandomAttackMontage()};
check(AttackMontage);
if (IsPredictingClient())
{
if (!AbilitySystemComponent.IsValid()) return;
UTBCAbilitySystemComponent* ASC{StaticCast<UTBCAbilitySystemComponent*>(AbilitySystemComponent.Get())};
ASC->CallServerSetAttackMontage(GetAbilitySpecHandle(), AttackMontage,
GetActivationPredictionKey(),
ASC->ScopedPredictionKey);
}
if (ShouldBroadcastAbilityTaskDelegates())
{
ValidData.Broadcast(AttackMontage);
}
}
void UAbilityTask_GetAttackMontage::OnTargetDataReplicatedCallback(const UAnimMontage* Data)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
if (Data)
{
ValidData.Broadcast(Data);
}
else
{
Cancelled.Broadcast(Data);
}
}
EndTask();
}
// AbilitySystemComponent
DECLARE_MULTICAST_DELEGATE_OneParam(FOnAbilityTaskAttackMontageSet, const UAnimMontage*);
void UTBCAbilitySystemComponent::CallServerSetAttackMontage(FGameplayAbilitySpecHandle AbilityHandle,
const UAnimMontage* AttackMontage,
FPredictionKey AbilityOriginalPredictionKey,
FPredictionKey CurrentPredictionKey)
{
Server_SetReplicatedAttackMontage(AbilityHandle, AbilityOriginalPredictionKey, AttackMontage, CurrentPredictionKey);
}
void UTBCAbilitySystemComponent::Server_SetReplicatedAttackMontage_Implementation(
FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey,
const UAnimMontage* ReplicatedAttackMontage, FPredictionKey CurrentPredictionKey)
{
FScopedPredictionWindow ScopedPrediction{this, CurrentPredictionKey};
// Always adds to cache to store the new data
const TSharedRef ReplicatedData{
AbilityTargetDataMap.FindOrAdd(
FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, AbilityOriginalPredictionKey))
};
if (ReplicatedData->TargetData.Num() > 0)
{
const FGameplayAbilitySpec* Spec{FindAbilitySpecFromHandle(AbilityHandle)};
if (Spec && Spec->Ability)
{
// Can happen under normal circumstances if ServerForceClientTargetData is hit
UE_LOGFMT(AbilitiesLog, Display, "Ability {Ability} is overriding pending replicated attack montage.",
*Spec->Ability->GetName());
}
}
ReplicatedData->bTargetConfirmed = true;
ReplicatedData->bTargetCancelled = false;
ReplicatedData->PredictionKey = CurrentPredictionKey;
OnAbilityTaskAttackMontageSet.Broadcast(ReplicatedAttackMontage);
}
This can be changed to any arbitrary data. I even think instead of UAnimMontage I could have just passed in UObject* and pass any object. The only thing I found strangely working is that OnAbilityTaskAttackMontageSet doesn’t get unbound, meaning, on 8th attack, 8 delegates are subscribed to it. I thought it should clear up automatically when ability task is destroyed, but maybe I should override EndTask() and clear delegate manually.