UE4 C++ GameplayAbilitiesを勉強していくPart.2 AttributeSetとGameplayEffect
やあ
今回は、Attributes
、AttributeSet
、GameplayEffects
について勉強していくよ!
間違いなどあれば教えてくださると助かります!
前回
Attributesとは
Attributes
は、FGameplayAttributeData
構造体で定義されたfloat値で、
キャラクターの体力や、レベルなど何でも表すことができるもの!
BaseValueとCurrentValue
FGameplayAttributeData
を見ればわかるのですが。
Attribute
はBaseValue
とCurrentValue
で構成されています。
※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とは
AttributeSet
はAttributes
の定義、保持、変更の管理を行うものです。
AttributeSetの作成
ここで考えることは、
A : すべてを一緒くたにしたAttributeSet
を作るのか
B : 必要になりそうなものごとに分割したAttributeSet
を作るのか
例えばRPGを作るとします。 戦士は魔法を使えない。としたときに、 戦士にAのAttributeSetを持たせると、 戦士から魔法に関する無駄な処理を呼び出せてしまいます。 それでも問題はないのですが、 後々、気付かないうちに戦士に魔法に関する処理を設定してしまい、 バグを発生させるかもしれません。 そのためBのようにAttributeSetを作るのもいいです。
この辺りは、自分の好みでいいと思います。
今回は、AのようにAttributeSet
を作ってみます。
(が、今から作る物から継承して、魔法等の追加をすればBのようにできるようにします。)
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について
自分ははじめ名前からして、パーティクルの発生とか音を出したりするだけなのかなーとか思っていたのですが、中身を見ると全然違いました。
実際の処理は、
アビリティが、自分や、キャラクターのAttributes
やGameplayTags
を変化させるためのものです。
もちろん、パーティクルや音などの発生もできます(これにはGameplayCue
を使います。後々説明します)。
例えば、
・ダメージを与える、回復するなどの即時の属性変更
・移動速度上昇や、スタンなどの長期的なステータスのバフ/デバフを与える。
などの処理ができます。
AttributeSetの値の初期化
値の初期化はエディタで値を指定できるように、GameplayEffect
を使うよ。
GE_Status
を作成
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; } }
これでエディタで値を調整できるようになりました。
GameplayEffectsの3つの持続時間
タイプ | 詳細 |
---|---|
Instant |
即時 |
Has Duration |
Period 秒毎にModifier Magnitude ずつDuration Magnitude 秒間処理する |
Infinite |
Has Duration のDuration Magnitude 秒がない版 |
Has Duration
やInfinite
の使いかたとしては、
Gameplay Effect Tag
から効果の持続をする、しないを決めます。
追記
GameplayEffect
を付与したり取り除いたりをする場合は、Infinite
を使いましょう。
Modifier Operation
操作 | 説明 |
---|---|
Add |
指定されたAttribute を加算 or 減算 |
Multiply |
指定されたAttribute を乗算 |
Divide |
指定されたAttribute を除算 |
Override |
指定されたAttribute を上書きする |
Modifier Types
タイプ | 詳細 |
---|---|
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); } } }
コード中のコメントにも書いてある通り、
MaxStamina
をBaseValue
Stamina
をCurrentValue
とみなし、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
クラスを作成
GE_Jump
が適用されるとスタミナが-10されるよ。
GE_Jump
を複製して、GE_RefreshStamina
を作成。
Period
で指定した0.1秒毎
にModifier Float Magnifiacation
の0.5
ずつスタミナが永遠に回復するようにしたよ
GameplayAbilityからGameplayEffectを呼び出す
ジャンプアニメの前でGE_Jump
を呼び、アビリティが終わったら、GE_RefreshStamina
を呼んでいます。
スタミナ用のUI作成
UIの表示
テスト結果
うんうんうまくいってるね。だけど、連打してみてほしい。
なんかだんだん回復速度上がってませんかこれ!?
なんでこうなっているかというと、
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 Tags かGranted Tags のいずれかがあるならターゲットのGameplayEffect を削除する |
GE_Jump
に追加
GE_RefreshStamina
に追加
これでGE_Jump
が適用された時に起こっているGE_RefreshStamina
を止められるよ。
GE_Jump
が適用されたときに、Effect.Movemnt.Refresh.Stamina
をGameplayEffectAssetTags
かGrantedTags
に持つのはGE_RefreshStamina
だからね。
テスト
オーケーだね。
あとは微調整で、スタミナが最大なのにGE_RefreshStamina
が呼ばれ続けるのはうれしくないので、
ThirdPersonCharacter
にActiveGameplayEffectHandle
型の変数を作成。
GA_Jump
に追加
これでおけ。
とりあえず今回はここまで。ありがとうございました。
次回
今回名前は出たけど触れなかった部分について勉強します