tranek / GASDocumentation

My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project.
MIT License
4.26k stars 789 forks source link

Doc section 4.5.14 method 2 I believe is incorrect. #93

Open MattiaPezzanoAiv opened 1 year ago

MattiaPezzanoAiv commented 1 year ago

Section 4.5.14 in the doc page recommends creating a UGameplayEffect at runtime, this would be great because it would let me pick which attribute to change making it selectable in the details panel of my ability, or set it dynamically.

My code looks something like this:

UGameplayEffect* UGvGameplayAbility::GetCostGameplayEffect() const
{
    // This snippet is taken from https://github.com/tranek/GASDocumentation#4517-creating-dynamic-gameplay-effects-at-runtime 
    UGameplayEffect* CostGE = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Cost")));
    CostGE->DurationPolicy = EGameplayEffectDurationType::Instant;
    CostGE->Modifiers.SetNum(1);
    FGameplayModifierInfo& Info = CostGE->Modifiers[0];
    Info.ModifierMagnitude = FScalableFloat(AbilityCost);
    Info.ModifierOp = EGameplayModOp::Additive;
    Info.Attribute = CostAttribute;
    return CostGE;
}

This seems to be working during CheckCost, but it "fails" when the cost has to be actually applied because the Spec is created using GetCostGameplayEffect()->GetClass(), resulting in an empty gameplay effect being applied, therefore no cost is deducted.

To be more specific what happens in UGameplayAbility::ApplyCost is that ApplyGameplayEffectToOwner is called with the new instance of the gameplay effect as the argument, inside this function though, Effect->GetClass() is called, ending up using the default object.

Hopefully I didn't misunderstand the doc intent. I should also mention that I have read the part where this is discouraged during prediction, although I was trying to do my own tests. A solution to this would be to override UGameplayAbility::ApplyCost, and rather than invoking ApplyGameplayEffectToOwner, I could create my own FGameplayEffectSpecHandle using the dynamically created instance instead of the default object. This would look something like the following:

void UGvGameplayAbility::ApplyCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
    const FGameplayAbilityActivationInfo ActivationInfo) const
{
    UGameplayEffect* CostGE = GetCostGameplayEffect();
    if (CostGE)
    {
        // ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, CostGE, );
        if (CostGE && (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo)))
        {
            FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(CostGE, {}, GetAbilityLevel(Handle, ActorInfo));
            FGameplayEffectSpecHandle SpecHandle(GESpec);
            if (SpecHandle.IsValid())
            {
                ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
            }
        }
    }
}

Thank you in advance for your response.

tranek commented 1 year ago

Unfortunately I lost my test code for that part, so I don't have an answer for you for that specific example.

If you want to continue using a dynamically created GE, I think your approach of overriding ApplyCost() is the way to do it.