UE4 C++ GameplayAbilitiesを勉強していくPart.3-1 GameplayEffect ModifierMagnitudeCalculationとExectuionCalculation

やあ

今回は、前回名前は出たけど触っていなかった、Modifier Magnitude CalculationGameplay Effect Execution Calculationを例を交えて勉強するよ。
まぁ、細かいところの説明は前回で大体やったから、ほぼ例の実装だけどね。

間違いなどあれば教えてくださると助かります!

追記 : GameplayEffectExecutionCalculationについて
pto8913.hatenablog.com

目次

前回

pto8913.hatenablog.com

GameplayEffect

Modifier Magnitude Calculationについて

GameplayEffectModifiersの一つ。
Modifier TypesCustom Calculation Classで指定するクラスのこと。
f:id:pto8913:20200528203421p:plain

なにをするクラスかというと、
前回まではここに-100.5といった定数を入れるだけだったのが、
コード内で指定した定数に対し、何か操作を加えその結果を返せるよというクラス。

例えば、毒で継続的に10ダメージを負うとすると、

1. この時、毒に耐性がある、ないをGameplayTagsを用いて判定
2.1 耐性がある : 毒のダメージ10 - 5の5ダメージを
2.2 耐性がない : 毒のダメージ10 + 10の20ダメージを
3. 結果の値を変えす

もちろん`Has Duration`以外にも、`Instant`、`Infinity`でも使える

みたいなことができるよ。
え? そのくらいAttributeSetPostGameplayEffectExecuteに書けばいいじゃん(笑)とか思わないでね。

あそこは変更された値の結果をセットするためのところであって、
計算をする場所ではないんだよ。

さらに言うと、ただでさえ長い関数が、そんな処理を書いてたらぐちゃぐちゃになって読みにくいよ。

Modifier Magnitude Calculationを実際に使ってみる

GameplayModMagnitudeCalculationを継承してMMC_Staminaを作成
f:id:pto8913:20200528215247p:plain

今回は凝ったことはしないで、スタミナが半分以上なら2倍スタミナをしようするようにしました。
MMC_Stamina.hに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameplayModMagnitudeCalculation.h"
#include "MMC_Stamina.generated.h"

UCLASS()
class ABILITYTEST_API UMMC_Stamina : public UGameplayModMagnitudeCalculation
{
    GENERATED_BODY()
public:
    UMMC_Stamina();
    virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;

    UPROPERTY()
        FGameplayEffectAttributeCaptureDefinition StaminaDef;

    UPROPERTY()
        FGameplayEffectAttributeCaptureDefinition MaxStaminaDef;
};

MMC_Stamina.cppに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "Abilities/Calculations/MMC_Stamina.h"
#include "GameplayEffectTypes.h"
#include "Abilities/AttributeSets/ptoAttributeSet.h"

UMMC_Stamina::UMMC_Stamina()
{
    StaminaDef.AttributeToCapture = UptoAttributeSet::GetStaminaAttribute();
    StaminaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
    StaminaDef.bSnapshot = false;

    MaxStaminaDef.AttributeToCapture = UptoAttributeSet::GetMaxStaminaAttribute();
    MaxStaminaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
    MaxStaminaDef.bSnapshot = false;

    RelevantAttributesToCapture.Add(StaminaDef);
    RelevantAttributesToCapture.Add(MaxStaminaDef);
}

float UMMC_Stamina::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
    const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
    const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

    FAggregatorEvaluateParameters EvaluationParameters;
    EvaluationParameters.SourceTags = SourceTags;
    EvaluationParameters.TargetTags = TargetTags;

    float Stamina = 0.f;
    GetCapturedAttributeMagnitude(StaminaDef, Spec, EvaluationParameters, Stamina);
    Stamina = FMath::Max<float>(Stamina, 0.f);

    float MaxStamina = 0.f;
    GetCapturedAttributeMagnitude(MaxStaminaDef, Spec, EvaluationParameters, MaxStamina);
    MaxStamina = FMath::Max<float>(MaxStamina, 1.f);

    float Res = -10.f;
    if (Stamina / MaxStamina > 0.5f)
    {
        Res *= 2.f;
    }

    return Res;
}

書けたら、GE_JumpMCC_Staminaを設定
f:id:pto8913:20200528235138p:plain

テスト。

f:id:pto8913:20200528235335g:plain

できてるね。

Gameplay Effect Execution Calculationについて

GameplayEffectExecutionsCalculation Class
f:id:pto8913:20200529130047p:plain

このクラスはModifier Magnitude Calculation(MMC)クラスと同様に、Attributeを変更することができるよ。
だけど、MMCと違って複数のAttributeを変更することができるんだって。

例えば、敵からの攻撃で20ダメージを負うとする。
さらにその攻撃でスタミナを10奪われる
みたいな処理もかける。

Gameplay Effect Execution Calculationを実際に使ってみる

敵からの攻撃で20ダメージを受け、
さらにその攻撃でスタミナを10奪われる。
おまけで、その攻撃で防御力が5減るデバフをつけるよ。

これをそのままやってみるよ。

Attributeを変更できるようにする。
MyAttributeSet.cppに追加

void UptoAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);

    FGameplayEffectContextHandle Context = Data.EffectSpec.GetContext();
    UAbilitySystemComponent* Source = Context.GetOriginalInstigatorAbilitySystemComponent();
    const FGameplayTagContainer& SourceTags = *Data.EffectSpec.CapturedSourceTags.GetAggregatedTags();

    float DeltaValue = 0.f;
    if (Data.EvaluatedData.ModifierOp == EGameplayModOp::Type::Additive)
    {
        DeltaValue = Data.EvaluatedData.Magnitude;
    }

    /* これはこのAttributeSetのオーナーであるはずです */
    AActor* TargetActor = nullptr;
    AController* TargetController = nullptr;
    ACharacterBase* TargetCharacter = nullptr;
    if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
    {
        TargetActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
        TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
        TargetCharacter = Cast<ACharacterBase>(TargetActor);
    }

    if (Data.EvaluatedData.Attribute == GetDamageAttribute())
    {
        AActor* SourceActor = nullptr;
        AController* SourceController = nullptr;
        ACharacterBase* SourceCharacter = nullptr;
        if (Source && Source->AbilityActorInfo.IsValid() && Source->AbilityActorInfo->AvatarActor.IsValid())
        {
            SourceActor = Source->AbilityActorInfo->AvatarActor.Get();
            SourceController = Source->AbilityActorInfo->PlayerController.Get();
            if (SourceController == nullptr && SourceActor != nullptr)
            {
                if (APawn* Pawn = Cast<APawn>(SourceActor))
                {
                    SourceController = Pawn->GetController();
                }
            }

            if (SourceController)
            {
                SourceCharacter = Cast<ACharacterBase>(SourceController->GetPawn());
            }
            else
            {
                SourceCharacter = Cast<ACharacterBase>(SourceActor);
            }
        }

        FHitResult HitResult;
        if (Context.GetHitResult())
        {
            HitResult = *Context.GetHitResult();
        }

        const float LocalDamageDone = GetDamage();

        SetDamage(0.f);

        if (LocalDamageDone > 0.f)
        {
            const float OldHealth = GetHealth();
            SetHealth(FMath::Clamp(OldHealth - LocalDamageDone, 0.f, GetMaxHealth()));

            if (TargetCharacter)
            {
                TargetCharacter->HandleDamage(
                    LocalDamageDone, HitResult, SourceTags, SourceCharacter, SourceActor
                );

                TargetCharacter->HandleHealthChanged(
                    -LocalDamageDone, SourceTags
                );
            }
        }
    }
    else if (Data.EvaluatedData.Attribute == GetDefensePowerAttribute())
    {
        // 最大値をいじりたい場合はこう書く
        SetDefensePower(FMath::Clamp(GetDefensePower(), 0.f, GetDefensePower()));

        if (IsValid(TargetCharacter) == true)
        {
            TargetCharacter->HandleDefensePowerChanged(DeltaValue, SourceTags);
        }
    }
    else if (Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
        SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));

        if (IsValid(TargetCharacter) == true)
        {
            TargetCharacter->HandleHealthChanged(DeltaValue, SourceTags);
        }
    }
    else if (Data.EvaluatedData.Attribute == GetStaminaAttribute())
    {
        SetStamina(FMath::Clamp(GetStamina(), 0.f, GetMaxStamina()));

        if (IsValid(TargetCharacter) == true)
        {
            TargetCharacter->HandleStaminaChanged(DeltaValue, SourceTags);
        }
    }
    else if (Data.EvaluatedData.Attribute == GetMaxStaminaAttribute())
    {
        // 最大値をいじりたい場合はこう書く
        SetMaxStamina(FMath::Clamp(GetMaxStamina(), 0.f, GetMaxStamina()));

        if (IsValid(TargetCharacter) == true)
        {
            TargetCharacter->HandleStaminaChanged(DeltaValue, SourceTags);
        }
    }
}

CharacterBase.hに追加

UFUNCTION(BlueprintImplementableEvent)
        void OnDefensePowerChanged(float DeltaValue, const FGameplayTagContainer& EventTags);
    UFUNCTION(BlueprintImplementableEvent)
        void OnHealthChanged(float DeltaValue, const FGameplayTagContainer& EventTags);
    UFUNCTION(BlueprintImplementableEvent)
        void OnDamage(
            float DamageAmount,
            const FHitResult& HitInfo,
            const struct FGameplayTagContainer& DamageTags,
            ACharacterBase* InstigatorPawn,
            AActor* DamageCauser
        );

public:
    UFUNCTION(BlueprintCallable)
        virtual float GetDefensePower() const;
    UFUNCTION(BlueprintCallable)
        virtual float GetHealth() const;
    UFUNCTION(BlueprintCallable)
        virtual float GetMaxHealth() const;

    // ptoAttributeSetから呼ばれて、上記のOn〇〇ChangedがBPから呼ばれます
    virtual void HandleDefensePowerChanged(float DeltaValue, const FGameplayTagContainer& EventTags);
    virtual void HandleHealthChanged(float DeltaValue, const FGameplayTagContainer& EventTags);
    virtual void HandleDamage(
        float DamageAmount, 
        const FHitResult& HitInfo, 
        const struct FGameplayTagContainer& DamageTags, 
        ACharacterBase* InstigatorPawn, 
        AActor* DamageCauser
    );

CharacterBase.cppに追加

float ACharacterBase::GetHealth() const
{
    if (IsValid(AttributeSet))
    {
        return AttributeSet->GetHealth();
    }
    return 1.f;
}

float ACharacterBase::GetMaxHealth() const
{
    if (IsValid(AttributeSet))
    {
        return AttributeSet->GetMaxHealth();
    }
    return 1.f;
}

float ACharacterBase::GetDefensePower() const
{
    if (IsValid(AttributeSet))
    {
        return AttributeSet->GetDefensePower();
    }
    return 1.f;
}

void ACharacterBase::HandleStaminaChanged(float DeltaValue, const FGameplayTagContainer& EventTags)
{
    if (bAbilitiesInitialized)
    {
        OnStaminaChanged(DeltaValue, EventTags);
    }
}

void ACharacterBase::HandleDefensePowerChanged(float DeltaValue, const FGameplayTagContainer& EventTags)
{
    if (bAbilitiesInitialized)
    {
        OnDefensePowerChanged(DeltaValue, EventTags);
    }
}

void ACharacterBase::HandleHealthChanged(float DeltaValue, const FGameplayTagContainer& EventTags)
{
    if (bAbilitiesInitialized)
    {
        OnHealthChanged(DeltaValue, EventTags);
    }
}

void ACharacterBase::HandleDamage(
    float DamageAmount, 
    const FHitResult& HitInfo, 
    const struct FGameplayTagContainer& DamageTags, 
    ACharacterBase* InstigatorPawn, 
    AActor* DamageCauser
)
{
    OnDamage(DamageAmount, HitInfo, DamageTags, InstigatorPawn, DamageCauser);
}

Gameplay Effect Execution Calculationの作成

敵からの攻撃で20ダメージを受け、
さらにその攻撃でスタミナを10奪われる。
おまけで、その攻撃で防御力が5減るデバフをつけるよ。

流れ
1. まずダメージを与えるアビリティを実行。Gameplay Effect Execution Calculation(EC)を持ったGameplay Effectを適用。
2. アビリティが終了したら、デバフ用のGameplay Effectを発生。

ダメージを与え、スタミナを奪うGameplay Effectの作成

f:id:pto8913:20200529154205p:plain

EC_DamageWithDFSDebuff.hに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "EC_DamageWithDFSDebuff.generated.h"

UCLASS()
class ABILITYTEST_API UEC_DamageWithDFSDebuff : public UGameplayEffectExecutionCalculation
{
    GENERATED_BODY()
public:
    UEC_DamageWithDFSDebuff();
    virtual void Execute_Implementation(
        const FGameplayEffectCustomExecutionParameters& ExecutionParams,
        OUT FGameplayEffectCustomExecutionOutput& OutExecutionParams
    ) const override;
};

EC_DamageWithDFSDebuff.cppに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "Abilities/Calculations/EC_DamageWithDFSDebuff.h"

#include "Abilities/AttributeSets/ptoAttributeSet.h"
#include "AbilitySystemComponent.h"

struct ptoDamageStatics
{
    DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower);
    DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
    DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
    DECLARE_ATTRIBUTE_CAPTUREDEF(Stamina);

    ptoDamageStatics()
    {
        DEFINE_ATTRIBUTE_CAPTUREDEF(UptoAttributeSet, DefensePower, Target, false);
        DEFINE_ATTRIBUTE_CAPTUREDEF(UptoAttributeSet, AttackPower, Source, true);
        DEFINE_ATTRIBUTE_CAPTUREDEF(UptoAttributeSet, Damage, Source, true);
        DEFINE_ATTRIBUTE_CAPTUREDEF(UptoAttributeSet, Stamina, Source, true);
    }
};

static const ptoDamageStatics& DamageStatics()
{
    static ptoDamageStatics DamageStatics;
    return DamageStatics;
}

UEC_DamageWithDFSDebuff::UEC_DamageWithDFSDebuff()
{
    RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef);
    RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef);
    RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
    RelevantAttributesToCapture.Add(DamageStatics().StaminaDef);
}

void UEC_DamageWithDFSDebuff::Execute_Implementation(
    const FGameplayEffectCustomExecutionParameters& ExecutionParams,
    OUT FGameplayEffectCustomExecutionOutput& OutExecutionParams
) const
{
    UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
    UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();

    AActor* SourceActor = SourceASC ? SourceASC->AvatarActor : nullptr;
    AActor* TargetActor = TargetASC ? TargetASC->AvatarActor : nullptr;

    const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

    const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
    const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

    FAggregatorEvaluateParameters EvaluationParameters;
    EvaluationParameters.SourceTags = SourceTags;
    EvaluationParameters.TargetTags = TargetTags;

    float DefensePower = 0.f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
        DamageStatics().DefensePowerDef,
        EvaluationParameters,
        DefensePower
    );

    float AttackPower = 0.f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
        DamageStatics().AttackPowerDef,
        EvaluationParameters,
        AttackPower
    );

    float Damage = 0.f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
        DamageStatics().DamageDef,
        EvaluationParameters,
        Damage
    );

    float Stamina = 0.f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
        DamageStatics().StaminaDef,
        EvaluationParameters,
        Stamina
    );
    float StaminaDone = -10;

    float DamageDone = Damage * (AttackPower - DefensePower);
    if (DamageDone > 0.f)
    {
        OutExecutionParams.AddOutputModifier(
            FGameplayModifierEvaluatedData(
                DamageStatics().DamageProperty,
                EGameplayModOp::Additive,
                DamageDone
            )
        );

        OutExecutionParams.AddOutputModifier(
            FGameplayModifierEvaluatedData(
                DamageStatics().StaminaProperty,
                EGameplayModOp::Additive,
                StaminaDone
            )
        );
    }
}

とりあえずテストする

GamapleyEffectの作成

f:id:pto8913:20200529230601p:plain

GamapleyAbilityの作成

f:id:pto8913:20200529230732p:plain

アビリティを設定

f:id:pto8913:20200529220802p:plain

UIの作成

f:id:pto8913:20200529220903p:plain
テキストブロックにはCharacterRefからそれぞれに対応したものをバインドしてね。

テスト結果

f:id:pto8913:20200529230921g:plain

デバフ用のGameplayEffectの作成

f:id:pto8913:20200530000648p:plain
Has DurationPeriod0にすることで、経過時間後にAttributeの値が元に戻るよ。
これでデバフができたね。

このGameplayEffectをさっき作ったアビリティの最後に追加
f:id:pto8913:20200530000917p:plain

これでテスト
f:id:pto8913:20200530001454g:plain
ちょっとわかりにくいけど、アビリティが終わった時に、防御が-5されて、その5秒後に防御が50に戻ってるね。

オイオイオイ、全然活用できてないんじゃないの!?(大事なこと)

f:id:pto8913:20200530004127p:plain
ここの値、全然活用してないじゃないの!?
何のための値なんだい!?って思ったよね。

例えば、
Attribute.Damage100とする。
Attribute.Damageをキャプチャしたときに、

/* Execute_Implementationの中だと思ってください */

float Damage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
    DamageStatics().DamageDef,
    EvaluationParameters,
    Damage
);
/* 
ここでキャプチャされた値に、Scalable Float Magnitudeで指定した
値がDamageに追加(Modifier Opで指定したオペレーション)される

Scalable Float Magnitude を 2としたとき

ModOp.Add : Damage = 100 + 2 の 102になる
ModOp.Mul : Damage = 100 x 2 の 200になる
ModOp.Div : Damage = 100 / 2 の 50になる
ModOp.Override : Damage = 2 になる
ModOp.InValid : よくわかってない

この値をいじってAddOutputModifierしてやる。
AddOutputModifierするときに注意が必要で、

OutExecutionParams.AddOutputModifier(
   FGameplayModifierEvaluatedData(
       DamageStatics().DamageProperty,
       EGameplayModOp::Additive,
       Damage
   )
);

EGameplayModOp に指定したオペレーションによって、
AttributeSetのPostGameplayEffectExecuteでセットされる。
 */

float DamageDone = Damage * 2;

OutExecutionParams.AddOutputModifier(
    FGameplayModifierEvaluatedData(
        DamageStatics().DamageProperty,
        EGameplayModOp::Additive,
        DamageDone
    )
);

ということを理解すると、
UEC_DamageWithDFSDebuff::Execute_Implementationの中身を書き換えられて、

float Stamina = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
    DamageStatics().StaminaDef,
    EvaluationParameters,
    Stamina
);

OutExecutionParams.AddOutputModifier(
    FGameplayModifierEvaluatedData(
        DamageStatics().StaminaProperty,
        EGameplayModOp::Additive,
        Stamina
    )
);

となり、
GE_DamageWithDFSDebuff
f:id:pto8913:20200530010300p:plain
とできる。

f:id:pto8913:20200530010646g:plain
うまくいってるね。

だいぶわかってきたかも
f:id:pto8913:20200526223340p:plain

今回はここまで、ありがとうございました。

次回

まだ触れてないGameplay Effectの機能のコストとクールダウンを勉強していくよ。

pto8913.hatenablog.com