UE4 C++ GameplayAbilitiesを勉強していくPart.2 AttributeSetとGameplayEffect

やあ

今回は、AttributesAttributeSetGameplayEffectsについて勉強していくよ!

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

前回

pto8913.hatenablog.com

Attributesとは

Attributesは、FGameplayAttributeData構造体で定義されたfloat値で、 キャラクターの体力や、レベルなど何でも表すことができるもの!

BaseValueとCurrentValue

FGameplayAttributeDataを見ればわかるのですが。
AttributeBaseValueCurrentValueで構成されています。

GameplayEffectsは後のほうででてきますが、簡単に言うと、Attributesの書き換えができるようになるもの!と思っていてください

BaseValueは、製作者が一意に決めた値。
CurrentValueは、GameplayEffectsによる一時的な変更を加えた値。

例えばバフ等の場合、
1. キャラクターの攻撃力を10とします(BaseValue)
->この状態でのCurrentValueも10。
2. キャラクターがなにかのアイテムのバフで、攻撃力が+5され15となると、BaseValueは10のまま、CurrentValueは15になる。
3. GameplayEffectsの終了時に、CurrentValueはBaseValueの10にもどります。

例えばスタミナを消費する等の場合、
1. キャラクターのスタミナを100とします(BaseValue)
->CurrentValueも100
2. キャラクターが10のスタミナで、ジャンプをするとしたとき、BaseValueは100で、CurrentValueは90になります。
これはつまり、スタミナの消費ではBaseValueを最大値とみなすことができるということです。
そのほかにも、体力や魔力などもBaseValueを最大値とみなせる例です。
3. GameplayEffectsの終了時に、CurrentValueを徐々に戻したり、いきなり最大値に戻したりすることができます。

AttributeSetとは

AttributeSetAttributesの定義、保持、変更の管理を行うものです。

AttributeSetの作成

ここで考えることは、
A : すべてを一緒くたにしたAttributeSetを作るのか
B : 必要になりそうなものごとに分割したAttributeSetを作るのか

例えばRPGを作るとします。
戦士は魔法を使えない。としたときに、
戦士にAのAttributeSetを持たせると、
戦士から魔法に関する無駄な処理を呼び出せてしまいます。
それでも問題はないのですが、
後々、気付かないうちに戦士に魔法に関する処理を設定してしまい、
バグを発生させるかもしれません。
そのためBのようにAttributeSetを作るのもいいです。

この辺りは、自分の好みでいいと思います。

今回は、AのようにAttributeSetを作ってみます。
(が、今から作る物から継承して、魔法等の追加をすればBのようにできるようにします。)
f:id:pto8913:20200527142737p:plain

MyAttributeSet.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "ptoAttributeSet.generated.h"

// Uses macros from AttributeSet.h
/* 指定されたクラスのプロパティに対する、
セッター関数と、ゲッター関数を自動的に作ってくれるマクロ 
GetHealthやSetHealth、GetHealthAttribute, InitHealthを使えるようになります
*/
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
   GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
   GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
   GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
   GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

UCLASS()
class ABILITYTEST_API UptoAttributeSet : public UAttributeSet
{
    GENERATED_BODY()
public:
    UptoAttributeSet();
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
        FGameplayAttributeData Health;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, Health);

    UPROPERTY(BlueprintReadOnly, Category = "MaxHealth", ReplicatedUsing = OnRep_MaxHealth)
        FGameplayAttributeData MaxHealth;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, MaxHealth);

    UPROPERTY(BlueprintReadOnly, Category = "Stamina", ReplicatedUsing = OnRep_Stamina)
        FGameplayAttributeData Stamina;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, Stamina);

    UPROPERTY(BlueprintReadOnly, Category = "MaxStamina", ReplicatedUsing = OnRep_MaxStamina)
        FGameplayAttributeData MaxStamina;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, MaxStamina);

    UPROPERTY(BlueprintReadOnly, Category = "MoveSpeed", ReplicatedUsing = OnRep_MoveSpeed)
        FGameplayAttributeData MoveSpeed;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, MoveSpeed)

    UPROPERTY(BlueprintReadOnly, Category = "Damage", ReplicatedUsing = OnRep_AttackPower)
        FGameplayAttributeData AttackPower;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, AttackPower)

    UPROPERTY(BlueprintReadOnly, Category = "Damage", ReplicatedUsing = OnRep_DefensePower)
        FGameplayAttributeData DefensePower;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, DefensePower)

    UPROPERTY(BlueprintReadOnly, Category = "Damage")
        FGameplayAttributeData Damage;
    ATTRIBUTE_ACCESSORS(UptoAttributeSet, Damage);

protected:
    /* これらのOnRep関数は、変更の結果が、複製されるときに、指定されたクラス内の指定されたメンバ変数と適切に同期されているかを確認するためのもの */
    /* 4.24の場合は引数はいりません */
    UFUNCTION()
        virtual void OnRep_Health(const FGameplayAttributeData& OldValue);

    UFUNCTION()
        virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldValue);

    UFUNCTION()
        virtual void OnRep_Stamina(const FGameplayAttributeData& OldValue);

    UFUNCTION()
        virtual void OnRep_MaxStamina(const FGameplayAttributeData& OldValue);

    UFUNCTION()
        virtual void OnRep_MoveSpeed(const FGameplayAttributeData& OldValue);

    UFUNCTION()
        virtual void OnRep_AttackPower(const FGameplayAttributeData& OldValue);

    UFUNCTION()
        virtual void OnRep_DefensePower(const FGameplayAttributeData& OldValue);
};

MyAttributeSet.cppに追加

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

#include "Abilities/AttributeSets/ptoAttributeSet.h"
#include "Net/UnrealNetWork.h"

UptoAttributeSet::UptoAttributeSet() 
    : Health(1.f)
    , MaxHealth(1.f)
    , Mana(0.f)
    , MaxMana(0.f)
    , AttackPower(1.0f)
    , DefensePower(1.0f)
    , MoveSpeed(1.0f)
    , Damage(0.0f)
{
}

void UptoAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(UptoAttributeSet, Health);
    DOREPLIFETIME(UptoAttributeSet, MaxHealth);
    DOREPLIFETIME(UptoAttributeSet, Stamina);
    DOREPLIFETIME(UptoAttributeSet, MaxStamina);
    DOREPLIFETIME(UptoAttributeSet, AttackPower);
    DOREPLIFETIME(UptoAttributeSet, DefensePower);
    DOREPLIFETIME(UptoAttributeSet, MoveSpeed);
}

void UptoAttributeSet::OnRep_Health(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UptoAttributeSet, Health, OldValue);
}

void UptoAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UptoAttributeSet, MaxHealth, OldValue);
}

void UptoAttributeSet::OnRep_Stamina(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UptoAttributeSet, Stamina, OldValue);
}

void UptoAttributeSet::OnRep_MaxStamina(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UptoAttributeSet, MaxStamina, OldValue);
}

void UptoAttributeSet::OnRep_MoveSpeed(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UptoAttributeSet, MoveSpeed, OldValue);
}

void UptoAttributeSet::OnRep_AttackPower(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UptoAttributeSet, AttackPower, OldValue);
}

void UptoAttributeSet::OnRep_DefensePower(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UptoAttributeSet, DefensePower, OldValue);
}

AttributeSetの初期化

AttributeSetはOwnerActorのコンストラクタでAbilitySystemComponentに自動で登録されます。

CharacterBase.hに追加

class UptoAttributeSet;

protected:
    UPROPERTY()
        UptoAttributeSet* AttributeSet;

CharacterBase.hに追加

ACharacterBase::ACharacterBase(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    AttributeSet = CreateDefaultSubobject<UptoAttributeSet>(TEXT("AttributeSet"));
}

これでキャラクターが体力、スタミナなどの属性Attributesを持ちました。

しかし、これだけでは変更ができません。

そこで出てくるのがGameplayEffectsです。

GameplayEffectsについて

自分ははじめ名前からして、パーティクルの発生とか音を出したりするだけなのかなーとか思っていたのですが、中身を見ると全然違いました。

実際の処理は、
アビリティが、自分や、キャラクターのAttributesGameplayTagsを変化させるためのものです。
もちろん、パーティクルや音などの発生もできます(これにはGameplayCueを使います。後々説明します)。

例えば、
・ダメージを与える、回復するなどの即時の属性変更
・移動速度上昇や、スタンなどの長期的なステータスのバフ/デバフを与える。
などの処理ができます。

AttributeSetの値の初期化

値の初期化はエディタで値を指定できるように、GameplayEffectを使うよ。
GE_Statusを作成
f:id:pto8913:20200529200426p:plain
f:id:pto8913:20200529200436p:plain
f:id:pto8913:20200529210822p:plain

CharacterBase.hに追加

class UGameplayEffect;

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ability")
        TArray<TSubclassOf<UGameplayEffect>> PassiveGameplayEffects;

CharacterBase.cppに追加

void ACharacterBase::AddStartupGameplayAbilities()
{
    if (IsValid(AbilitySystemComp) == true)
    {
        for (TSubclassOf<UGameplayAbility>& StartupAbility : GameplayAbilities)
        {
            AbilitySystemComp->GiveAbility(FGameplayAbilitySpec(StartupAbility, ExistsLevel, INDEX_NONE, this));
        }

        for (TSubclassOf<UGameplayEffect>& Effect : PassiveGameplayEffects)
        {
            FGameplayEffectContextHandle EffectContext = AbilitySystemComp->MakeEffectContext();
            EffectContext.AddSourceObject(this);

            FGameplayEffectSpecHandle NewHandle = AbilitySystemComp->MakeOutgoingSpec(Effect, ExistsLevel, EffectContext);
            if (NewHandle.IsValid())
            {
                FActiveGameplayEffectHandle ActiveGEHandle = AbilitySystemComp->ApplyGameplayEffectSpecToTarget(*NewHandle.Data.Get(), AbilitySystemComp);
            }
        }

        bAbilitiesInitialized = true;
    }
}

f:id:pto8913:20200529210504p:plain
これでエディタで値を調整できるようになりました。

GameplayEffectsの3つの持続時間

f:id:pto8913:20200528111147p:plain

タイプ 詳細
Instant 即時
Has Duration Period秒毎にModifier MagnitudeずつDuration Magnitude秒間処理する
Infinite Has DurationDuration Magnitude秒がない版

Has DurationInfiniteの使いかたとしては、
Gameplay Effect Tagから効果の持続をする、しないを決めます。
追記
GameplayEffectを付与したり取り除いたりをする場合は、Infiniteを使いましょう。

Modifier Operation

f:id:pto8913:20200528112044p:plain

操作 説明
Add 指定されたAttributeを加算 or 減算
Multiply 指定されたAttributeを乗算
Divide 指定されたAttributeを除算
Override 指定されたAttributeを上書きする

Modifier Types

f:id:pto8913:20200528112153p:plain

タイプ 詳細
Scalable Float 一つの値を使用する。またはカーブテーブルから値を取得し係数でさらに操作することもできる
Attribute Based (すみませんよくわかってないです)
Custom calculation Class Modifier Magnitude Calculationクラスで自作の計算をし、その結果を係数でさらに操作できる
Set By Caller (すみませんよくわかってないです)

実際にAttributesを変更してみる

Attributesの変更には、PostGameplayEffectExecuteを使います。
この関数をオーバーライドし、Attributesの変更に関することをすべて記述します。

MyAttributeSet.hに追加

public:
    virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;

MyAttributeSet.cppに追加

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

試しに、GA_Jumpアビリティを使うとスタミナが10減り、アビリティの終了後最大値まで徐々に回復する。という処理を書いてみます。

書くのはBP上(C++上でもいいけど)で、
C++上で書くのは、GameplyEffectsの実行後に、値の書き換えを行い、BP上(またはC++)の関数を呼び出す処理です。

MyAttributeSet.cppに追加

#include "GameplayEffect.h"
#include "GameplayEffectExtension.h"
#include "AbilitySystemComponent.h"
#include "Abilities/Characters/Bases/CharacterBase.h"

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 == GetStaminaAttribute())
    {
        UE_LOG(LogTemp, Log, TEXT("%.f %.f"), GetStamina(), DeltaValue);
        /* 
           MaxStaminaをBaseValue
           StaminaをCurrentValue
           とみなし、Stamina - DeltaValueの値を設定している。
           実際の処理はGameplayEffectsの中で行われているため、
           ここでは結果しか見ることができない
       */
        SetStamina(FMath::Clamp(GetStamina(), 0.f, GetMaxStamina()));
        if (IsValid(TargetCharacter) == true)
        {
            TargetCharacter->HandleStaminaChanged(DeltaValue, SourceTags);
        }
    }
}

コード中のコメントにも書いてある通り、
MaxStaminaBaseValue
StaminaCurrentValue
とみなし、Stamina + DeltaValue の値を設定している。
実際の処理はGameplayEffectsの中で行われているのでここでは結果しか見ることができないのです。

CharacterBase.hに追加

public:
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
protected:

    UFUNCTION(BlueprintImplementableEvent)
        void OnStaminaChanged(float DeltaValue, const struct FGameplayTagContainer& EventTags);
public:
    UFUNCTION(BlueprintCallable)
        virtual float GetStamina() const;
    UFUNCTION(BlueprintCallable)
        virtual float GetMaxStamina() const;
    // ptoAttributeSetから呼ばれて、上記のOn〇〇ChangedがBPから呼ばれます
    virtual void HandleStaminaChanged(float DeltaValue, const struct FGameplayTagContainer& EventTags);

CharacterBase.cppに追加

void ACharacterBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
}

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

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

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

キャラクターのほうには特に難しいことはしていないので省略。

ここまで書けたらビルドしてエディタに移動。

GameplayEffectsの作成

GameplayEffectクラスを継承し、GE_Jumpクラスを作成
f:id:pto8913:20200527220942p:plain
f:id:pto8913:20200527221017p:plain
f:id:pto8913:20200527221138p:plain

GE_Jumpが適用されるとスタミナが-10されるよ。

GE_Jumpを複製して、GE_RefreshStaminaを作成。
f:id:pto8913:20200527222058p:plain
f:id:pto8913:20200528165639p:plain

Periodで指定した0.1秒毎Modifier Float Magnifiacation0.5ずつスタミナが永遠に回復するようにしたよ

GameplayAbilityからGameplayEffectを呼び出す

f:id:pto8913:20200527224118p:plain
ジャンプアニメの前でGE_Jumpを呼び、アビリティが終わったら、GE_RefreshStaminaを呼んでいます。

スタミナ用のUI作成

f:id:pto8913:20200527223829p:plain
f:id:pto8913:20200527223845p:plain
f:id:pto8913:20200527223859p:plain

UIの表示

f:id:pto8913:20200527223944p:plain

テスト結果

f:id:pto8913:20200527224510g:plain

うんうんうまくいってるね。だけど、連打してみてほしい。
f:id:pto8913:20200527235509g:plain
なんかだんだん回復速度上がってませんかこれ!?

なんでこうなっているかというと、
GameplayAbility終了後に実行されたGameplayEffectが終わる前に、次のGameplayAbilityが呼ばれて、その終了後のGameplayEffectが実行されて...
という風に、処理が終わる前に次の処理が呼ばれ、どんどん早くなっていってるのです。
じゃあどうすればいいのってことなんですが、GameplayAbilityを呼ぶ前に、
現在アクティブなGameplayEffectを削除してやればいいのです。

そこで便利なのがGameplayEffectTags

Gameplay Effect Tags

タグ 詳細
Gameplay Effect Asset Tags Gameplay Effectを説明するためのタグ
Granted Tags GameplayEffectが適用されたAbilitySystemComponentにも追加されるタグ。
Ongoind Tag Requirements GameplayEffectがオンかオフかを判断するタグ。
Application Tag Requirements ターゲットにGameplayEffectを適用できるかどうか判断するためのタグ。
Remove Gameplay Effects with Tags このGameplayEffectが正常に適用された時に、これらのタグがGameplay Effect Asset TagsGranted TagsのいずれかがあるならターゲットのGameplayEffectを削除する

GE_Jumpに追加
f:id:pto8913:20200528160531p:plain

GE_RefreshStaminaに追加
f:id:pto8913:20200528160637p:plain

これでGE_Jumpが適用された時に起こっているGE_RefreshStaminaを止められるよ。

GE_Jumpが適用されたときに、Effect.Movemnt.Refresh.StaminaGameplayEffectAssetTagsGrantedTagsに持つのはGE_RefreshStaminaだからね。

テスト

f:id:pto8913:20200528170458g:plain
オーケーだね。

あとは微調整で、スタミナが最大なのにGE_RefreshStaminaが呼ばれ続けるのはうれしくないので、

ThirdPersonCharacterActiveGameplayEffectHandle型の変数を作成。
f:id:pto8913:20200528170816p:plain
GA_Jumpに追加
f:id:pto8913:20200528170920p:plain

これでおけ。

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

次回

今回名前は出たけど触れなかった部分について勉強します

pto8913.hatenablog.com