見出し画像

【Unreal Engine5】InputTagとGameplayAbility【前編】

※注意※
・この記事はUnreal Engine 5でC++を使った実装をやっている記事です。Blueprintも使いますが、主にC++について記述しているので、Blueprintだけで実装する類の記事ではありません。
・C++を使って実装する部分やソースコードが多々出てくるので、ある程度C++が読める方向けです(雰囲気はわかるように書くつもりですが、仕組みがある程度分かっている方が見た方がいいかもしれません)
・これが必ずしも正しい!一番効率的!というものではないので、間違っている部分や考え方がおかしい部分もあることをご承知おきください(本業が企画職なので知識が浅いです)

上記内容でも気になるという方は続きをお読みください。

前準備→https://note.com/mok0/n/nb4c9f0138193

これが終わっている状態からのスタートになります。このプロジェクトを改造していく形になります。

引き続き「Lyra」というUnreal Engine5のサンプルゲームの中から入力周りについての拡張部分をUE5のサードパーソンプロジェクト(C++版)に移植するものになります。
前回はGameplayTagを使って初期設定時にデータを読み込み、特定のTagだったらこのInputaActionで入力を受け付け、その処理をするといった内容でした。

今回はGameplayAbilitySystemと呼ばれるプラグインを使い[InputAction]→[GameplayTag]→[GameplayAbility]と紐づけて、Abilityの実行まですることを目標にしようと思います。
正直大ボリュームなので
・前編で前回作ったものをGamplayAbilitySystemに対応したものに変更
・後編で本体である「AbilitySysytemComponet」と「GamplayAbility」の2つを拡張してCharacterクラスに実装
このような流れでやらせてもらえればと思います。

GamePlayAbilitySystem周りの導入周りについては今回細かい部分は端折りますが、投稿者が勉強している際に参考にさせていただいた方々のリンクを置いておきます。

GamePlayAbilitySystemを知るきっかけとなったスライド(Youtube動画で見た時のスライド)
◆目指せ脱UE4初心者!?知ってると開発が楽になる便利機能を紹介 - DataAsset, Subsystem, GameplayAbility編 -
https://www.docswell.com/s/historia_Inc/5WVYJK-ue4-dataasset-subsystem-gameplayability
公演されていた時の動画→https://www.youtube.com/watch?v=2L1Dor22Kjs

◆個人的にC++で実装するベースの理解に非常に助かった方のサイト
https://ueblog.futuresoftware.dev/archives/category/技術メモ/gas

◆色々と基本を見た後(もしくはC++が強い方は手っ取り早いかも)に見直すとすごいと思う有志のサンプル
GASDocumentation(かなり詳しいが故に基礎的な部分の知識がないと厳しい・・・投稿者も全然読めなかった・・・)
https://github.com/sentyaanko/GASDocumentation/blob/lang-ja/README.jp.md

ここから本題
※〇〇となっているところは任意で大丈夫ですが、今回はTagInputSampleの略で「TIS」とすべて入力しています。

◆今回やったこと

前編(今回の部分)
・GameplayAbilitySystemを使えるようにする
・AbilitySystemComponent、GameplayAbilityを作成する
・AbilityとTagを紐づけるDataAsset「AbilitySet」の作成
・DataAsset「InputConfig」にAbility用の入力情報を追加
・InputComponentにAbilityのバインドをする機能を追加
・新しいTagの追加

後編
・AbilitySetとInputCofigの作成
・AbilitySysytemComponetを継承したクラスを作成し拡張する
・GamplayAbilityを継承したクラスを作成し拡張する
・CharacterにGameplayAbilitySysytemの実装をする

◆GameplayAbilitySystemを使えるようにする

プラグインを導入する
メニューにある編集→プラグインからプラグインを開き検索で「gameplayabilities」と入力すると以下の画像のプラグインが見つかります。
チェックボックスにチェックを入れることで反映します。
 ↓
チェックボックスにチェックを入れることでエディタを再起動するように促されるので、再起動をしてエディタを再起動します。
※画像ではチェックが既に入っています。

プラグインを追加するとエディタを再起動するように言われます。

次にVSCodeを開き、前回いじった[プロジェクト名].build.csの「PublicDependencyModuleNames.AddRange」に「GameplayAbilities」「GameplayTasks」を追加する。
※厳密には今回の実装でGameplayTasks部分は使わなかった記憶ですが、エラーにならないように入れています(今後使うと思うので)

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" ,"GameplayAbilities", "GameplayTags", "GameplayTasks"});

◆AbilitySystemComponent、GameplayAbilityを作成する

エラーにならないように先にAbilitySystemComponent、GameplayAbilityのC++ファイルを作成します。

Unreal Engine5のエディタを開き、新しくC++ファイルを作成します。
ツール→新規C++クラス...を選択します。
 ↓
クラスを選択する画面が表示されるので、全てのクラス→AbilitySystemComponentを選択して次へ
※検索でAbilitySysytemComponetと入力するとすぐにわかる位置に出てきます。
 ↓
ファイル名を〇〇AbilitySysytemComponetと名付けて作成します。
 ↓
次にツール→新規C++クラス...を選択から
 ↓
クラスを選択する画面が表示されるので、全てのクラス→GameplayAbilityを選択して次へ
※検索でGameplayAbilityと入力すると色々出てきますが、「GameplayAbility」を選択します。
 ↓
ファイル名を〇〇GameplayAbilityと名付けて作成します。
 ↓
ファイルを作成すると自動でコンパイルが走り、コンパイルが通るとVSCodeでファイルが開かれます。
※コンパイルエラーになってもファイルは作成されるので、VSCodeを開いて該当のファイルを編集することは可能です。
※前編ではこの2種のファイルは何もいじりません。

◆AbilityとTagを紐づけるDataAsset「AbilitySet」の作成

Unreal Engine5のエディタを開き、新しくC++ファイルを作成します。
ツール→新規C++クラス...を選択します。
 ↓
クラスを選択する画面が表示されるので、全てのクラス→DataAssetを選択して次へ
※検索でDataAssetと入力するとすぐにわかる位置に出てきます。
 ↓
ファイル名を〇〇AbilitySetと名付けて作成します。
 ↓
ファイルを作成すると自動でコンパイルが走り、コンパイルが通るとVSCodeでファイルが開かれます。
※コンパイルエラーになってもファイルは作成されるので、VSCodeを開いて該当のファイルを編集することは可能です。

[コード]〇〇AbilitySet.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "AttributeSet.h"
#include "ActmiveGameplayEffectHandle.h"
#include "GameplayTagContainer.h"

#include "GameplayAbilitySpecHandle.h"
#include "〇〇AbilitySet.generated.h"

class UAttributeSet;
class UGameplayEffect;
class U〇〇AbilitySystemComponent;
class U〇〇GameplayAbility;
class UObject;

//Gameplay Ability
USTRUCT(BlueprintType)
struct F〇〇AbilitySet_GameplayAbility
{
	GENERATED_BODY()

public:

	// Gameplay ability to grant.
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<U〇〇GameplayAbility> Ability = nullptr;

	// Level of ability to grant.
	UPROPERTY(EditDefaultsOnly)
	int32 AbilityLevel = 1;

	// Tag used to process input for the ability.
	UPROPERTY(EditDefaultsOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

// GamaplayEffect(ここはまだ使わないですが定義だけしておきます)
USTRUCT(BlueprintType)
struct F〇〇AbilitySet_GameplayEffect
{
	GENERATED_BODY()

public:

	// Gameplay effect to grant.
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UGameplayEffect> GameplayEffect = nullptr;

	// Level of gameplay effect to grant.
	UPROPERTY(EditDefaultsOnly)
	float EffectLevel = 1.0f;
};

// Attribute(ここはまだ使わないですが定義だけしておきます)
USTRUCT(BlueprintType)
struct F〇〇AbilitySet_AttributeSet
{
	GENERATED_BODY()

public:
	// Gameplay effect to grant.
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UAttributeSet> AttributeSet;

};

//Handle
USTRUCT(BlueprintType)
struct F〇〇AbilitySet_GrantedHandles
{
	GENERATED_BODY()

public:

	void AddAbilitySpecHandle(const FGameplayAbilitySpecHandle& Handle);
	void AddGameplayEffectHandle(const FActiveGameplayEffectHandle& Handle);
	void AddAttributeSet(UAttributeSet* Set);

	void TakeFromAbilitySystem(〇〇SAbilitySystemComponent* LyraASC);

protected:

	// Handles to the granted abilities.
	UPROPERTY()
	TArray<FGameplayAbilitySpecHandle> AbilitySpecHandles;

	// Handles to the granted gameplay effects.
	UPROPERTY()
	TArray<FActiveGameplayEffectHandle> GameplayEffectHandles;

	// Pointers to the granted attribute sets
	UPROPERTY()
	TArray<TObjectPtr<UAttributeSet>> GrantedAttributeSets;
};

UCLASS(BlueprintType, Const)
class U〇〇AbilitySet : public UDataAsset
{
	GENERATED_BODY()

public:

	U〇〇AbilitySet(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

	void GiveToAbilitySystem(U〇〇AbilitySystemComponent* 〇〇ASC, F〇〇AbilitySet_GrantedHandles* OutGrantedHandles, UObject* SourceObject = nullptr) const;

protected:

	// Gameplay abilities to grant when this ability set is granted.
	UPROPERTY(EditDefaultsOnly, Category = "Gameplay Abilities", meta=(TitleProperty=Ability))
	TArray<F〇〇AbilitySet_GameplayAbility> GrantedGameplayAbilities;

	// Gameplay effects to grant when this ability set is granted.
	UPROPERTY(EditDefaultsOnly, Category = "Gameplay Effects", meta=(TitleProperty=GameplayEffect))
	TArray<F〇〇AbilitySet_GameplayEffect> GrantedGameplayEffects;

	// Attribute sets to grant when this ability set is granted.
	UPROPERTY(EditDefaultsOnly, Category = "Attribute Sets", meta=(TitleProperty=AttributeSet))
	TArray<F〇〇AbilitySet_AttributeSet> GrantedAttributes;
};

[コード]〇〇AbilitySet.cpp

#include "〇〇AbilitySet.h"

#include "〇〇GameplayAbility.h"
#include "〇〇AbilitySystemComponent.h"

void F〇〇AbilitySet_GrantedHandles::AddAbilitySpecHandle(const FGameplayAbilitySpecHandle& Handle)
{
	if (Handle.IsValid())
	{
		AbilitySpecHandles.Add(Handle);
	}
}

void F〇〇AbilitySet_GrantedHandles::AddGameplayEffectHandle(const FActiveGameplayEffectHandle& Handle)
{
	if (Handle.IsValid())
	{
		GameplayEffectHandles.Add(Handle);
	}
}

void F〇〇AbilitySet_GrantedHandles::AddAttributeSet(UAttributeSet* Set)
{
	GrantedAttributeSets.Add(Set);
}

void F〇〇AbilitySet_GrantedHandles::TakeFromAbilitySystem(U〇〇AbilitySystemComponent* 〇〇ASC)
{
	check(〇〇ASC);

	if (!〇〇ASC->IsOwnerActorAuthoritative())
	{
		// Must be authoritative to give or take ability sets.
		return;
	}

	for (const FGameplayAbilitySpecHandle& Handle : AbilitySpecHandles)
	{
		if (Handle.IsValid())
		{
			〇〇ASC->ClearAbility(Handle);
		}
	}

	for (const FActiveGameplayEffectHandle& Handle : GameplayEffectHandles)
	{
		if (Handle.IsValid())
		{
			〇〇ASC->RemoveActiveGameplayEffect(Handle);
		}
	}

	for (UAttributeSet* Set : GrantedAttributeSets)
	{
		〇〇ASC->RemoveSpawnedAttribute(Set);
	}

	AbilitySpecHandles.Reset();
	GameplayEffectHandles.Reset();
	GrantedAttributeSets.Reset();
}

U〇〇AbilitySet::U〇〇AbilitySet(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
}

void U〇〇AbilitySet::GiveToAbilitySystem(U〇〇AbilitySystemComponent* 〇〇ASC, F〇〇AbilitySet_GrantedHandles* OutGrantedHandles, UObject* SourceObject) const
{
	check(〇〇ASC);

	if (!〇〇ASC->IsOwnerActorAuthoritative())
	{
		// Must be authoritative to give or take ability sets.
		return;
	}

	// Grant the gameplay abilities.
	for (int32 AbilityIndex = 0; AbilityIndex < GrantedGameplayAbilities.Num(); ++AbilityIndex)
	{
		const F〇〇AbilitySet_GameplayAbility& AbilityToGrant = GrantedGameplayAbilities[AbilityIndex];

		if (!IsValid(AbilityToGrant.Ability))
		{
			UE_LOG(LogTemp, Error, TEXT("GrantedGameplayAbilities[%d] on ability set [%s] is not valid."), AbilityIndex, *GetNameSafe(this));
			continue;
		}

		U〇〇GameplayAbility* AbilityCDO = AbilityToGrant.Ability->GetDefaultObject<U〇〇GameplayAbility>();

		FGameplayAbilitySpec AbilitySpec(AbilityCDO, AbilityToGrant.AbilityLevel);
		AbilitySpec.SourceObject = SourceObject;
		AbilitySpec.DynamicAbilityTags.AddTag(AbilityToGrant.InputTag);

		const FGameplayAbilitySpecHandle AbilitySpecHandle = 〇〇ASC->GiveAbility(AbilitySpec);
  
		if (OutGrantedHandles)
		{
            
			OutGrantedHandles->AddAbilitySpecHandle(AbilitySpecHandle);
		}
	}

	// Grant the gameplay effects.
	for (int32 EffectIndex = 0; EffectIndex < GrantedGameplayEffects.Num(); ++EffectIndex)
	{
		const F〇〇AbilitySet_GameplayEffect& EffectToGrant = GrantedGameplayEffects[EffectIndex];

		if (!IsValid(EffectToGrant.GameplayEffect))
		{
			UE_LOG(LogTemp, Error, TEXT("GrantedGameplayEffects[%d] on ability set [%s] is not valid"), EffectIndex, *GetNameSafe(this));
			continue;
		}

		const UGameplayEffect* GameplayEffect = EffectToGrant.GameplayEffect->GetDefaultObject<UGameplayEffect>();
		const FActiveGameplayEffectHandle GameplayEffectHandle = 〇〇ASC->ApplyGameplayEffectToSelf(GameplayEffect, EffectToGrant.EffectLevel, 〇〇ASC->MakeEffectContext());

		if (OutGrantedHandles)
		{
			OutGrantedHandles->AddGameplayEffectHandle(GameplayEffectHandle);
		}
	}

	// Grant the attribute sets.
	for (int32 SetIndex = 0; SetIndex < GrantedAttributes.Num(); ++SetIndex)
	{
		const F〇〇AbilitySet_AttributeSet& SetToGrant = GrantedAttributes[SetIndex];

		if (!IsValid(SetToGrant.AttributeSet))
		{
			UE_LOG(LogTemp, Error, TEXT("GrantedAttributes[%d] on ability set [%s] is not valid"), SetIndex, *GetNameSafe(this));
			continue;
		}

		UAttributeSet* NewSet = NewObject<UAttributeSet>(〇〇ASC->GetOwner(), SetToGrant.AttributeSet);
		〇〇ASC->AddAttributeSetSubobject(NewSet);

		if (OutGrantedHandles)
		{
			OutGrantedHandles->AddAttributeSet(NewSet);
		}
	}
}

【〇〇AbilitySet.h】
・GameplayAbility、GameplayEffect、Attributeの情報をセットするための構造体を定義
※GameplayEffect、Attributeは今回では使いませんが、以下の様な切っても切れない関係性になっています。

GameplayAbility:実際のアビリティ(処理)の本体
Attribute:GameplayAbilitySystem上で扱うデータ群(ゲームなどで言うところのHP、MP、スタミナなどの実数本体)
GameplayEffect:実際の効果値を制御するデータ。この効果を与えることでAttribute値を変化させます。またAbilityを実行するためのコストとしての役割も担うことができます。
※正直こんな説明だけだと意味がつかみづらいと思うので参考リンクからイメージをつかんでいただければと思います(いつか図式化して説明できるくらい理解したい)

【〇〇AbilitySet.cpp】
・スペックハンドルといわれる、Abilityを実行させたりする本体を追加する処理を記載
・登録したスペックハンドルを削除する処理を記載
・このデータアセットに登録されているアビリティとタグの紐づけをするための処理を記載
→このデータアセットの処理で登録しているデータの追加と削除をできる

◆DataAsset「InputConfig」にAbility用の入力情報を追加

前回作った「〇〇InputConfig」にアビリティとして使う入力側のデータを切り分けて登録できるようにします。
直接的に動作するような入力はNative、スキルなどの機能的な部分をAbilityとして切り分け、バインドする機能を切り分けているようです。

[コード]〇〇InputConfig.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"

#include "〇〇InputConfig.generated.h"


class UInputAction;
class UObject;
struct FFrame;

USTRUCT(BlueprintType)
struct F〇〇InputAction
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<const UInputAction> InputAction = nullptr;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

UCLASS(BlueprintType, Const)
class U〇〇InputConfig : public UDataAsset
{
	GENERATED_BODY()
	
public:

	U〇〇InputConfig(const FObjectInitializer& ObjectInitializer);

	UFUNCTION(BlueprintCallable, Category = "〇〇|Pawn")
	const UInputAction* FindNativeInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound = true) const;

	//ここに新しくアビリティ側の検索部分を追加
	UFUNCTION(BlueprintCallable, Category = "〇〇|Pawn")
	const UInputAction* FindAbilityInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound = true) const;

public:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
	TArray<F〇〇InputAction> NativeInputActions;

	//アビリティ部分のデータを追加
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
	TArray<F〇〇InputAction> AbilityInputActions;
};

[コード]〇〇InputConfig.cpp

#include "〇〇InputConfig.h"

U〇〇InputConfig::U〇〇InputConfig(const FObjectInitializer& ObjectInitializer)
{
}

const UInputAction* U〇〇InputConfig::FindNativeInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound) const
{
	for (const F〇〇InputAction& Action : NativeInputActions)
	{
		if (Action.InputAction && (Action.InputTag == InputTag))
		{
			return Action.InputAction;
		}
	}

	if (bLogNotFound)
	{
		UE_LOG(LogTemp, Error, TEXT("Can't find NativeInputAction for InputTag [%s] on InputConfig [%s]."), *InputTag.ToString(), *GetNameSafe(this));
	}

	return nullptr;
}

//アビリティ側の検索ロジック部分の追加
const UInputAction* U〇〇InputConfig::FindAbilityInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound) const
{
	for (const F〇〇InputAction& Action : AbilityInputActions)
	{
		if (Action.InputAction && (Action.InputTag == InputTag))
		{
			return Action.InputAction;
		}
	}

	if (bLogNotFound)
	{
		UE_LOG(LogTemp, Error, TEXT("Can't find AbilityInputAction for InputTag [%s] on InputConfig [%s]."), *InputTag.ToString(), *GetNameSafe(this));
	}

	return nullptr;
}

【〇〇InputConfig.h】
・新たに「FindAbilityInputActionForTag」と「AbilityInputActions」を追加

【〇〇InputConfig.cpp】
・FindAbilityInputActionForTag
AbilityInputActionsに設定されているGameplayTagから該当するInputActionを取得する処理を追加します。
※処理的にはNativeの方のデータと同じ処理をしているが、Bind時に別に分けるために切り分けています。

◆InputComponentにAbilityのバインドをする機能を追加
前回作った「〇〇InputComponent」にアビリティをバインドする処理を追加します。

[コード]〇〇InputComponent.h

#pragma once

#include "CoreMinimal.h"
#include "EnhancedInputComponent.h"
#include "〇〇InputConfig.h"

#include "〇〇InputComponent.generated.h"

class UEnhancedInputLocalPlayerSubsystem;
class UInputAction;
class U〇〇InputConfig;
class UObject;

UCLASS(Config = Input)
class U〇〇InputComponent : public UEnhancedInputComponent
{
	GENERATED_BODY()

public:

	U〇〇InputComponent(const FObjectInitializer& ObjectInitializer);

	void AddInputMappings(const U〇〇InputConfig* InputConfig, UEnhancedInputLocalPlayerSubsystem* InputSubsystem) const;
	void RemoveInputMappings(const U〇〇InputConfig* InputConfig, UEnhancedInputLocalPlayerSubsystem* InputSubsystem) const;

	template<class UserClass, typename FuncType>
	void BindNativeAction(const U〇〇InputConfig* InputConfig, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent, UserClass* Object, FuncType Func, bool bLogIfNotFound);

	//アビリティのバインドをするためのtemplate定義
	template<class UserClass, typename PressedFuncType, typename ReleasedFuncType>
	void BindAbilityActions(const U〇〇InputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, TArray<uint32>& BindHandles);

	void RemoveBinds(TArray<uint32>& BindHandles);
};


template<class UserClass, typename FuncType>
void U〇〇InputComponent::BindNativeAction(const U〇〇InputConfig* InputConfig, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent, UserClass* Object, FuncType Func, bool bLogIfNotFound)
{
	check(InputConfig);
	if (const UInputAction* IA = InputConfig->FindNativeInputActionForTag(InputTag, bLogIfNotFound))
	{
		BindAction(IA, TriggerEvent, Object, Func);
	}
}

//アビリティのバインドをするためのtemplateの本体
template<class UserClass, typename PressedFuncType, typename ReleasedFuncType>
void U〇〇InputComponent::BindAbilityActions(const U〇〇InputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, TArray<uint32>& BindHandles)
{
	check(InputConfig);

	for (const F〇〇InputAction& Action : InputConfig->AbilityInputActions)
	{
		if (Action.InputAction && Action.InputTag.IsValid())
		{
			if (PressedFunc)
			{
				BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Triggered, Object, PressedFunc, Action.InputTag).GetHandle());
			}

			if (ReleasedFunc)
			{
				BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Completed, Object, ReleasedFunc, Action.InputTag).GetHandle());
			}
		}
	}
}

[コード]〇〇InputComponent.cpp
変更なし

【〇〇InputComponent.h】
・BindAbilityActions
templateでAbilityをバインドするための処理を追加します。

【〇〇InputComponent.cpp】
こちらは追加はありません。

◆新しいTagの追加
新しいタグを追加します。

【〇〇InputComponent.h】
TAGINPUTSAMPLE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(認識させるタグ名);
→プログラム上で使うタグ名を定義します。

【〇〇InputComponent.cpp】
UE_DEFINE_GAMEPLAY_TAG_COMMENT(認識させるタグ名, "実際のタグ名", "コメント");
→実際のタグの定義をします。

ここまでで、前回作成した内容を拡張して、アビリティの登録の準備をしました。
色々追加したりしているのでコンパイルエラーが最初は多々出るかもしれませんが地道に修正していってもらえればと思います(投稿者は内容を理解してから書いてますが、コピーがベースなので移植をミスしてよくエラーを出しています・・・)
後編で、本体となる「GameplayAbilitySystem」と「GameplayAbility」を拡張します。

後編→

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