見出し画像

新卒研修でGameplayAbilitySystemを使って躓いたこと

初めまして。2024年にエンジニアとして新卒で入社したTKです。
新人研修としてUnreal Engine5以下の課題を行いました。

  • Unreal Quest(初級)のネイティブ化

  • Gameplay Ability Systemを用いたゲーム制作

今回は「Gameplay Ability Systemを用いたゲーム制作」の中で、Gameplay Ability System(以下GAS)を触って躓いたことと、その解決方法について話していこうと思います。
ゲーム制作の課題では、以下のような要件を実装しました。

  • 対象にホーミングしながら接近攻撃をする攻撃アビリティ実装

  • 弾がホーミングする遠距離攻撃アビリティの実装

  • ダメージを減少させるにするガードアビリティ実装

  • 一定時間無敵になる回避アビリティの実装

  • 継続攻撃ダメージを与える炎上状態のアビリティ実装

  • 敵のAIをビヘイビアツリーで実装

  • DataTableを使用したパラメーターの調整

  • その他SEのランダム再生やオーバーレイマテリアルの使用やナイアガラエフェクトの制御など

制作したゲーム

躓いたことについて

実際に課題を進めていて多くのトラブルが発生しました。
どういうトラブルが発生したのかとどうやって対応したのか記載します。

参考文献の半分以上がブループリントによる実装だった

課題を進めるにあたってチュートリアル解説をしているサイトを参考にしました。
具体的には以下の5つを参考にしました。

「仕事で使うわけだし、勉強も兼ねて基本的にすべてC++で実装しよう!」と意気込み、1つ目の参考文献から順に進めていきました。
2つ目から基本的に実装がブループリント(以下BP)になり、先輩に「全部が全部C++じゃなくて良いのですか」と聞いたところ「C++とBPの配分はプロジェクトにもよるから…」と返ってきました。
とりあえず、手を動かさないことには始まらないので、初学者の自分でも分かりそうな部分はC++、そうでなさそうな部分はBPで実装を進めていきました。
BPで書かれたものをC++に直すにあたって、「この引数の型って何?」や「引数の量、BPのと違くない?」みたいなこともありましたが、そういった時は、公式のリファレンスを見るなどして自分なりに納得しながら進めていきました。それでも理解が難しかった時や、エラーに悩まされる時は、公式のフォーラムに投稿された質問をもとに解決していきました。
研修も終盤に差し掛かると調べることにも慣れてきて、C++で実装するもの、BPで実装するものがはっきりと分かれているものも見つかりました。(5つ目の文献)(GameplayModMagnitudeCalculationクラスはC++だけ。GameplayCueNotify_ActorクラスはBPだけ。みたいに)

UE4からUIが変わって在ったものが無くなってた

研修では、UE5を使って実装していたのですがUE4のチュートリアルを参考にすると上手く進めることができないトラブルが発生しました。

上の画像がUE4、下の画像がUE5 タグのカテゴリがなくなっている

実装したのは、アビリティのクールダウンのエフェクトです。 参考にした動画(英語)ではUE4を使用していています。
最初にDuration PolicyをHas Durationにする。
次にGranted TagsのAddedにクールダウン用のタグを追加する。となっていました。
「じゃあ、やってみよう」と思い、Duration PolicyをHas Durationにし、Granted Tagsを追加しようとして、Granted Tagsがないことに気づきました。
どこを探してもそれらしきものが見つからなかったので、先輩に質問したところ「Gameplay EffectカテゴリのComponentsに色々まとめられた」とのことでした。
Componentsの配列を1つ追加し、いろいろ種類のあるComponentを上から1つずつ試していった結果、最後にあったTarget Tags Gameplay Effect Componentが動画と同じ挙動をしました。
このComponent機能は、解説しているチュートリアルが見つからなかったため、UnrealEngineの公式ドキュメントが一番参考になりました。
下の画像が最終的なエフェクト

以上が研修を進めていて発生したトラブルの紹介になります。


研修中に行った作業について

ここからは研修中に行ったことを解説していきます。
Gameplay Ability Systemがメインですがそれ以外のことも記載しているので
是非参考にしていただければと思います。

下準備編

プラグインの追加

エディタ内のプラグインウィンドウから追加(Gameplay Abilities)します。追加するにはエディタの再起動が必要です。

モジュールの追加

Visual Studioを起動して、”[プロジェクト名]Build.cs”を開きます。 AddRangeの{}内に、”GameplayAbilities”, “GameplayTags”, “GameplayTasks”を追加します。 一度Visual Studioを閉じ、エクスプローラーでUEのプロジェクトを右クリックし、Visual Studioプロジェクトを生成しなおします。


実装編

Ability System Component

Ability Systemを使用するなら、このコンポーネントは必須です。
コンポーネントを使用するクラスは、”AbilitySystemInterface.h”をインクルードし、そのクラスをIAbilitySystemInterfaceで2重継承します。
以下にACharacterを継承したクラスで実装例を記載します。

...
#include "AbilitySystemInterface.h"
#include "GASCharacter.generated.h"

UCLASS()
class AGASCharacter : public ACharacter, public IAbilitySystemInterface
{
	GENERATED_BODY()
	
private:
	void BeginPlay() override;
	
protected:
	
	// 追加した機能:Ability Systemを使用するうえで必須のコンポーネント
	*UPROPERTTY(VisibleAnywhere)
	UAbilitySystemComponent* AbilitySystem;*
	
	// 追加した機能:このキャラクターが持つアビリティの配列
	*UPROPERTY(EditDefaultsOnly,Category = "Abilities")
	TArray<TSubclassOf<UGameplayAbility>> AbilityList;*
	
public:
	
	AGASCharacter();
	
	void Tick(float DeltaTime) override;
	
	// 追加した機能:新しいControllerが与えられたときにAbility Systemのアクタをリフレッシュする
	*void PossessdBy(AController* NewController) override;*
	
	// 追加した機能:Ability System Componentのゲッター
	*UAbilitySystemComponent* GetAbilitySystemComponent() const;*
	
}

ソースには以下のようにします。

#include "GASCharacter.h"
#include "AbilitySystemComponent.h"

AGASCharacter::AGASCharacter()
{
	
	...
	if(!IsValid(AbilitySystem))
	{
		AbilitySystem = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
	}
	
}

void AGASCharacter::BeginPlay()
{
	
	...
	if(IsValid(AbilitySystem))
	{
		int32 InputID(0);
		if(HasAuthority() && AblityList.Num() > 0)
		{
			for(auto Ability : AbilityList)
			{
				AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability.GetDefaultObject(), 1, InputID++));
			}
		}
		AbilitySystem->InitAbilityActorInfo(this, this);
	}
	
}

...

void AGASCharacter::PossessedBy(AController* NewController)
{
	
	Super::PossessedBy(NewController);
	AbilitySystem->RefreshAbilityActorInfo();
	
}

UAbilitySystemComponent* AGASCharacter::GetAbilitySystemComponent() const
{
	return AbilitySystem;
}

コンポーネントであるため、コンストラクタでCreateDefaultSubobjectします。
BeginPlayでは、AbilityListに追加されたアビリティをコンポーネントに与え、コンポーネントがアクタとして使うクラスを自身にしています。 PossessedByでは、新たにこのクラスにコントローラが与えられた場合、アクタをリフレッシュします。

Gameplay Ability

ここでいうAbilityとは、「能力」というより「できること」のイメージ(個人の感想です) です。
例えば、「攻撃する」、「ダメージを受ける」、「死亡する」などが該当します。 このAbilityの実装はBPで行います。

だいたいこんな感じで書きます。
このアビリティは攻撃アビリティになります。
Activate Abilityイベントで始まり、CommitAbilityを繋ぎ、PlayMontageを繋ぎ、EndAbilityに終わります。
CommitAbilityで、アビリティのコスト計算や、クールダウンの終了検知等が行われます。
コストを支払うことができる、クールダウンが終わっている場合は、戻り値のbool型からtrueが返ってきます。 (別にモンタージュを再生する必要もなく、死亡ならラグドールでも可)
詳細ウィンドウ(Detail)にあるTagは、このアビリティのタグや、特定条件下でアビリティを実行しないようにするために必要です。
アビリティを実行するためには、先ず、先ほど作成したキャラクターのクラスのAbillityListにこれらのアビリティを追加します。
次に、アビリティを実行したい箇所でTryActivateAbility, TryActivateAbilityByClass, TryActivateAbilitiesByTagのいずれかの関数を実行します。
これらの関数は、AbilitySystemComponentのメンバ関数であるため、そのインスタンスから呼びます。
基本的にはByTagのやつを使用すれば問題ないです。
ですが、引数がFGameplayTagContainerという型であるため、その型の宣言の仕方を覚えておく必要があります。

Ability Tagsは、このアビリティが持っているタグです。
Cancel Abilities with Tagは、このアビリティの実行時に、指定されたタグを持つすべてのアビリティを終了させます。
Block Abilities with Tagは、このアビリティの実行中、指定されたタグを持つアビリティの実行を禁止します。
Activation~で始まるタグは、指定されたタグがAbilitySystemComponentに与えられます。
Activation Owned Tagsは、アビリティ実行時に、AbilitySystemComponentに与えられるタグです。
このタグは、そのアビリティの終了時にコンポーネントから失われます。
Activation Required Tagsは、指定したタグがすべてコンポーネントに与えられている場合のみ、このアビリティを実行できます。
Activation Blocked Tagsは、アビリティ実行中、指定されているタグを持っている、アビリティの実行を禁止するタグです。(この場合、BlockedにAbility.Stateが指定されているため、このアビリティの実行時に自身に与えられるAbility.State.Attack01を持っている、このアビリティは実行できなくなります)

Gameplay Tag

アビリティの種類や、用途を明確化するために用いられます。

上記のようにAbility.Action.Attack01のようにドットで区切られ、最大3段階のタグを付けられます。
新しいタグを追加するときは、検索バー左の+を押し、名前を決めます。
その後、ソースにDefaultGameplayTags.iniを選ぶことでタグを追加できます。
このタグ群の型が前述したFGameplayTagContainerになります。
エディタ上で与える場合(EditAnywhereやEditDefaultsOnly等)は問題ないですが、C++で直接与えるとなると一工夫必要になります。

//タグがAbility.Action.Attack01の場合
FGameplayTagContainer Val(FGameplayTag::RequestGameplayTag(FName("Ability.Action.Attack01")));
AbilitySystem->TryActibateAbilitiesByTags(Val);

Gameplay Attribute

体力、攻撃力、防御力等のステータス関連の値を管理するものになります。
このクラスはUAttributeSetを継承して作ります。
以下は実装したコードの一部です。

#pragma once

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

#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \\
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

UCLASS()
class UGASAttributeSet : public UAttributeSet
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
	FGameplayAttributeData Health;
	ATTRIBUTE_ACCESSORS(UGASAttributeSet, Health)

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
	FGameplayAttributeData MaxHealth;
	ATTRIBUTE_ACCESSORS(UGASAttributeSet, MaxHealth)

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
	FGameplayAttributeData AttackBase;
	ATTRIBUTE_ACCESSORS(UGASAttributeSet, AttackBase)

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
	FGameplayAttributeData AttackMultiplier;
	ATTRIBUTE_ACCESSORS(UGASAttributeSet,AttackMultiplier)

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
	FGameplayAttributeData DefenceMultiplier;
	ATTRIBUTE_ACCESSORS(UGASAttributeSet,DefenceMultiplier)

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
	FGameplayAttributeData Damage;
	ATTRIBUTE_ACCESSORS(UGASAttributeSet, Damage)

	/**
	* GameplayEffect適用後に呼ばれる関数
	* @param Data - 計算に用いられたデータ
	*/
	virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;

};

#include "GASAttributeSet.h"
#include "GASCharacter.h"
#include "GameplayEffect.h"
#include "GameplayEffectExtension.h"

void UGASAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{

	Super::PostGameplayEffectExecute(Data);

	AActor* TargetActor = Data.Target.GetAvatarActor();
	AGASCharacter* TargetCharacter = Cast<AGASCharacter>(TargetActor);

	AActor* SourceActor = Data.EffectSpec.GetContext().GetOriginalInstigator();

	if (Data.EvaluatedData.Attribute != GetHealthAttribute())
	{
		return;
	}

	if (Data.EvaluatedData.ModifierOp != EGameplayModOp::Additive)
	{
		return;
	}

	if (Data.EvaluatedData.Magnitude >= 0.0f)
	{
		return;
	}

	if (!IsValid(TargetCharacter))
	{
		return;
	}

	if (GetHealth() > 0.0f)
	{
		TargetCharacter->OnDamaged(GetDamage(), GetHealth(), SourceActor);
	}
	else
	{
		TargetCharacter->OnDied(GetDamage(), SourceActor);
	}

	InitDamage(0.0f);

	InitHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));

}

PostGameplayEffectExecute関数はいずれかのGameplay Effectが適用された後に呼ばれる関数です。

Gameplay Effect

攻撃、被ダメージなどのアビリティによりAttributeの値が変更される場合に、「誰が、何の数値で、誰に、どの数値に、どのような影響を与えるか」を指定するものになります。 場合によっては、自身が自身に影響を与えることもできます。
与える影響は、加減算、乗算、除算、カスタムから選べます。
カスタムの場合は、GameplayModMignitudeCalculationクラスなどにより独自の演算を作成できます。

適用準備

「誰が」と「誰に」は事前に、それぞれのAbilitySystemComponentを指定します。
以下の場合、「thisが、Targetに、EffectClassのエフェクトを影響する」こととなります。

//.h

UPROPERTY(EditDefaultsOnly, Category = "Abilities")
TSubclassOf<UGameplayEffect> EffectClass;

/*-----------------------*/
//.cpp

FGameplayEffectSpecHandle Handle = this->AbilitySystem->MakeOutgoingSpec(EffectClass, 0.0f, FGameplayEffectContextHandle());

//第1引数は参照渡しであるが、Get()はポインタのため、二重ポインタにして渡す必要あります。
this->AbilitySystem->ApplyGameplayEffectSpecToTarget(*Handle.Data.Get(), Target->GetAbilitySystemComponent());

エフェクトのクラス本体はBPで実装します。
イベントグラフの実装は不要です。

Durationカテゴリ

Duration Policyはこのエフェクトの適用期間を決めます。

  • Instant:1回だけ適用します

  • Has Duration:指定の期間内に、指定の間隔で適用します

GameplayEffectカテゴリ

Componentsはこのエフェクトに追加効果を与えます。(基本的には追加しなくて大丈夫)

  • Target Tags Gameplay Effect Component:(このコンポーネントは正直よくわかってないです。クールダウン用エフェクトを作るならこちらを追加します)

  • Modifiersは影響を与えるAttributeを指定します。配列になっているため、一度に複数のAttributeに影響を与えることが可能です。

    • Modifier Opは計算方法を指定します。加減算、乗算、除算、上書きの中から1つ選びます。

    • Magnitude Calculation Typeはどのような値を代入するかを指定します

      • Scalable Float:値を直打ちします。 

      • Attribute Based:Attributeの値を使用します。その際、Backing Attribute内に、Attributeの種類(Attribute To Capture)と、エフェクトの適用元か適用先のAttributeを使用するか(Source)を決めます。 

      • Custom Calculation Class:後述のGameplayModMagnitudeCalculationクラスで定義された計算結果を用いります。

  • Coefficient:係数。1.0であれば等倍の値を影響します

Executionsは、このエフェクトが適用された直後に適用するエフェクトを指定できます。
Gameplay Cueカテゴリ

  • Gameplay Cue Tags:実行するキューのタグを指定します。(キューを実行しないなら何もしなくて大丈夫)

GameplayModMagnitudeCalculationクラス

このクラスを継承し、独自のC++クラスを作成します。BPでは作成不可です。

#pragma once

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

UCLASS()
class UGMMC_Attack : public UGameplayModMagnitudeCalculation
{
	GENERATED_BODY()

public:

	UGMMC_Attack();

	FGameplayEffectAttributeCaptureDefinition AttackBaseDef;
	FGameplayEffectAttributeCaptureDefinition AttackMultiplierDef;
	FGameplayEffectAttributeCaptureDefinition DefenceMultiplierDef;

	float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
	
};

#include "GMMC_Attack.h"
#include "GASAttributeSet.h"

UGMMC_Attack::UGMMC_Attack()
{

	//攻撃側の攻撃力を取得して即時適用をする
	AttackBaseDef.AttributeToCapture = UGASAttributeSet::GetAttackBaseAttribute();
	AttackBaseDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Source;
	AttackBaseDef.bSnapshot = true;

	//攻撃側の攻撃倍率を取得して即時適用する
	AttackMultiplierDef.AttributeToCapture = UGASAttributeSet::GetAttackMultiplierAttribute();
	AttackMultiplierDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Source;
	AttackMultiplierDef.bSnapshot = true;

	//防御側の防御倍率を取得する。即時に適用はしない
	DefenceMultiplierDef.AttributeToCapture = UGASAttributeSet::GetDefenceMultiplierAttribute();
	DefenceMultiplierDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	DefenceMultiplierDef.bSnapshot = false;

	//それぞれの変数を計算の対象にする
	RelevantAttributesToCapture.Add(AttackBaseDef);
	RelevantAttributesToCapture.Add(AttackMultiplierDef);
	RelevantAttributesToCapture.Add(DefenceMultiplierDef);

}

float UGMMC_Attack::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にする
	float AttackBase = 0.0f;
	GetCapturedAttributeMagnitude(AttackBaseDef, Spec, EvaluationParameters, AttackBase);

	//キャプチャしたアトリビュートをfloatにする
	float AttackMultiplier = 0.0f;
	GetCapturedAttributeMagnitude(AttackMultiplierDef, Spec, EvaluationParameters, AttackMultiplier);

	//キャプチャしたアトリビュートをfloatにする(0の場合は除算しないように最低値を1にする)
	float DefenceMultiplier = 0.0f;
	GetCapturedAttributeMagnitude(DefenceMultiplierDef, Spec, EvaluationParameters, DefenceMultiplier);
	if (DefenceMultiplier == 0.0f)
	{
		DefenceMultiplier = 1.0f;
	}

	return AttackBase * AttackMultiplier / DefenceMultiplier;

}

コンストラクタ内の、[変数].bSnapshotは、その変数が、EffectSpecが作成された時の値を使用するか、Effectが適用された時の値をしようするかを決める。trueなら前者、falseなら後者となります。

Gameplay Cue

GameplayEffectが適用されたことによって起こる視覚効果を記述します。
例えば、炎魔法を受けたキャラクターを火傷にし、炎のパーティクルをアタッチする。などができます。
これは、GameplayCueNotify_Staticクラス、若しくはGameplayCueNotify_Actorクラスを継承したブループリントで記述します。
前者は、エフェクトが適用された瞬間の1回だけ実行し、後者はエフェクトが適用開始時、終了時等に実行されます。
以下に、GameplayCueNotify_Actorを継承したBPを例を示します。

On Active関数、On Removed関数をオーバーライドして実装します。
前者が、エフェクト実行時に呼ばれるもの。
後者がエフェクト終了時に呼ばれるものになります。

Gameplay Cue Tags

GameplayCueを識別するためのTagです。
これはGameplayTagとは異なります。
GameplayTag同様に最大3段階で書くことができますが、1段目は”GameplayCue.”で始める必要があります。

参考文献

最後に改めて参考にした文献を紹介します。
今回の研修を進めるのにとても役立ちました。ありがとうございます。


研修を終えての感想

学生の頃にもGASを触っていました。
その際に触れたときの感想は、「GASっていう便利なものあるらしいから触ってみるか。……。よくわからないから一旦あきらめるか…」となってそれ以上深くは触っていませんでした。
その後も今日まで触れていなかったのですが、先輩から紹介された資料や、自分で調べた資料を読みながら進めていたら、意外とすんなりと理解できてよかったなと思っています。 少しコツも掴めてきたような気がしていて、楽しく実装を進められてよかったです。

最後に宣伝ですが
株式会社ブラストエッジゲームズでは、一緒に開発してくれる
仲間を絶賛募集中です! 会社へのエントリーもお待ちしてます!
https://www.blastedge-games.co.jp/recruit
一緒にUnreal Engineでゲームを作りましょう!
今回の知識が少しでも皆さんのゲーム開発に役立てばと思います。
では皆さん良いゲーム開発を!

この記事が気に入ったらサポートをしてみませんか?