Hey there! From our perspective, if any of the Net Execution Policies would allow both the owning client and the server to start execution of the ability, it introduces a complex case of: what if signals cross each other?
If they both start executing the ability at the same time, and both RPCs TryActivateAbility RPCs are underway to each other, then:
- If the ability is reusable (Instanced Per Actor), then the same object is now representing a different “run” on the client and server, with possibly different parameters.
- WaitNetSync ability tasks don’t have a defined behavior, since it’s no longer the case that one node is ahead and the other is catching up.
- If the ability involves movement (root motion sources), then the server will be applying movement effects that the client could not predict, which is guaranteed to result in forward corrections.
- But by letting the server RPC to the client first to activate local predicted as usual, the client can stay ahead in movement, and the server just verifies.
Arguably some of these problems can be worked around by:
- Using Instanced Per Execution abilities, so that “signals crossing” doesn’t operate on the same GA object. But generally, we don’t like Instanced Per Execution, because it’s more short-lived, possibly net addressable objects.
- We could have a handling method like “if signals cross, reject the player’s predicted activation and let them follow the server”.
- Accept movement related forward corrections. Or from a content perspective, don’t do movement tasks from locally predicted AND server-activatable abilities.
If we would allow any Local Predicted ability to also be executed by the server ahead of the client, users would run into these problems more often. Although I’m not sure whether the original design intent was to protect people from these complexities, or just because this approach was easier implemented, currently our stance is: this approach (routing through client) is easy to grasp, and developers will find their own ways around these limitations (not allowing servers to execute ahead) if they have a need for it. One such way around this is implementing your own TryActivateAbility RPCs in C++ in the ASC.
Experimental snippet
I do understand your need for sometimes letting the server immediately execute a client-predictable ability. If you don’t need the client to run the ability that time at all (i.e. movement forward corrections are fine), or if you will introduce your own client RPC to run the same ability locally without replication back to the server, then you can do this trick of overriding Net Execution Policy locally just to make the ability fire:
// Server executes 'Local Predicted' ability without waiting for client
if (UAbilitySystemComponent* ASC = GetOwner()->GetComponentByClass<UAbilitySystemComponent>())
{
FGameplayEventData EventData;
FGameplayAbilityTargetingLocationInfo TargetLoc;
TargetLoc.LiteralTransform = FTransform(LabMoveData->ChargeTargetLocation);
EventData.TargetData = UAbilitySystemBlueprintLibrary::AbilityTargetDataFromLocations(TargetLoc, TargetLoc);
FGameplayAbilitySpec* ChargeAbilSpec = ASC->FindAbilitySpecFromHandle(LabMoveData->ChargeSpecHandle);
if (ChargeAbilSpec && ChargeAbilSpec->Ability)
{
FProperty* NetExecutionPolicyProp = ChargeAbilSpec->Ability->GetClass()->FindPropertyByName("NetExecutionPolicy");
EGameplayAbilityNetExecutionPolicy::Type OverwriteExecutionPolicy = EGameplayAbilityNetExecutionPolicy::Type::ServerInitiated;
void* ValuePtr = NetExecutionPolicyProp->ContainerPtrToValuePtr<void>(ChargeAbilSpec->Ability);
int32 OldValue;
FMemory::Memcpy(&OldValue, ValuePtr, 4);
FMemory::Memcpy(ValuePtr, &OverwriteExecutionPolicy, 4);
ASC->TriggerAbilityFromGameplayEvent(LabMoveData->ChargeSpecHandle, ASC->AbilityActorInfo.Get(), FGameplayTag(), &EventData, *ASC);
FMemory::Memcpy(ValuePtr, &OldValue, 4);
}
}
I would call the above practice pretty fragile, i.e. you have to be conscious of the rammifications of the client not executing the ability, but maybe it’s helpful in case you are considering implementing your own reusable helper functions to activate abilities locally regardless of net execution policy.
I hope this post is helpful in illustrating why the engine behavior is how it is, and what to account for if you want to work around that.
[Attachment Removed]