見出し画像

【Unreal Engine5】【Lyra機能】GamePhaseAbilityについて


今回の機能について

Unreal EngineのタグベースでEventを実行するGameplayAbilitySystem(以降GAS)を使った機能
大雑把に書くと、「Lyraでゲーム中のプレイヤーの状態を管理するために使われている機能」になります。
かなり便利なGASの拡張機能だと思っていて、これを使うと以下のような遷移などをタグベースで実行できるのではないかと思っています。

例)
タイトルからGameStartを選択した際に
データ初期化のアビリティを呼びだし
 ↓
これが終了する際にゲームシーンを呼びだし
 ↓
ゲーム開始の呼びだし
 ↓
バトル開始の呼びだし
など

ゲーム中のどのような状態かをアビリティベースで実装を切り分けて初期化部分を作ることができ、状態遷移をアビリティベースで管理できるようになります。

キーワードのクラス

Lyraより
・LyraGamePhaseAbility
 →Phase(状態)を管理するGameplayAbilityの拡張版
・LyraGamePhaseSubsystem
 →WorldSubSystemを継承して、Phase変更の実行を管理するSubSystemプログラム
・LyraGameState
 →GameStateで状態をスタートさせて動かす(LyraだとModular GameとGame Featureを使ってGameStateコンポーネントからGameStateにプラグインとして追加して機能部分をブループリントで使っている・・・はず)
・LyraAbilitySystemComponent
 →一部拡張したメソッドを使っているので、こちらも拡張が必要

今回の実装周りについて

LyraからGamePhaseAbility、GamePhaseSubsystem、GameState、AbilitySystemComponent(GameplayAbilityも何か改修が必要になった時のために継承だけしておく)を拝借して簡単な動きを作ることを目的にします。
GameplayAbilitySystemの初期設定は割愛します
やはり「おかわりはくまい」さんのサイトが昔から有名なので、こちらを参考にセットアップを進めていただければと思います。
(セットアップ記事も別途作ってもいいかもしれない)

事前準備

C++でサードパーソンプロジェクトをセットアップ

GameplayAbilitySystemのプラグインを有効化(エディタの再起動が必要)

多分ここまであれば先に進めます。

1.AbilitySystemComponentの拡張

特に大きなことをするわけではないが、AbilitySystemComponentから継承して新しくクラスを作成。

以下内容を追加する
・AbilitySystemComponentの拡張したクラスのヘッダとCPP

public:
	typedef TFunctionRef<bool(const UNGSGameplayAbility* NGSAbility, FGameplayAbilitySpecHandle Handle)> TShouldCancelAbilityFunc;
	void CancelAbilitiesByFunc(TShouldCancelAbilityFunc ShouldCancelFunc, bool bReplicateCancelAbility);
void UNGSAbilitySystemComponent::CancelAbilitiesByFunc(TShouldCancelAbilityFunc ShouldCancelFunc, bool bReplicateCancelAbility)
{
	ABILITYLIST_SCOPE_LOCK();
	for (const FGameplayAbilitySpec& AbilitySpec : ActivatableAbilities.Items)
	{
		if (!AbilitySpec.IsActive())
		{
			continue;
		}

		UNGSGameplayAbility* NGSAbilityCDO = Cast<UNGSGameplayAbility>(AbilitySpec.Ability);
		if (!NGSAbilityCDO)
		{
			UE_LOG(LogTemp, Error, TEXT("CancelAbilitiesByFunc: Non-NGSGameplayAbility %s was Granted to ASC. Skipping."), *AbilitySpec.Ability.GetName());
			continue;
		}

		if (NGSAbilityCDO->GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
		{
			// Cancel all the spawned instances, not the CDO.
			TArray<UGameplayAbility*> Instances = AbilitySpec.GetAbilityInstances();
			for (UGameplayAbility* AbilityInstance : Instances)
			{
				UNGSGameplayAbility* NGSAbilityInstance = CastChecked<UNGSGameplayAbility>(AbilityInstance);

				if (ShouldCancelFunc(NGSAbilityInstance, AbilitySpec.Handle))
				{
					if (NGSAbilityInstance->CanBeCanceled())
					{
						NGSAbilityInstance->CancelAbility(AbilitySpec.Handle, AbilityActorInfo.Get(), NGSAbilityInstance->GetCurrentActivationInfo(), bReplicateCancelAbility);
					}
					else
					{
						UE_LOG(LogTemp, Error, TEXT("CancelAbilitiesByFunc: Can't cancel ability [%s] because CanBeCanceled is false."), *NGSAbilityInstance->GetName());
					}
				}
			}
		}
		else
		{
			// Cancel the non-instanced ability CDO.
			if (ShouldCancelFunc(NGSAbilityCDO, AbilitySpec.Handle))
			{
				// Non-instanced abilities can always be canceled.
				check(NGSAbilityCDO->CanBeCanceled());
				NGSAbilityCDO->CancelAbility(AbilitySpec.Handle, AbilityActorInfo.Get(), FGameplayAbilityActivationInfo(), bReplicateCancelAbility);
			}
		}
	}
}

※NGSと書かれてる部分は適当に変更してください。自身がテストで作っているプロジェクトの頭文字です。
※GameplayAbilityも継承して使っていますが、何も追加はしていません。

2.GamePhaseAbilityの作成

特に拡張はしていませんが「NGSGameplayAbility」から継承します。
処理的にはLyraから変更はしないで使えるので「LyraGamePhaseAbility」からコピペして適宜名前を変えていく形で大丈夫です。

3.GamePhaseSubsystemの作成

ベースは「WorldSubsystem」から継承します。
今回はLyraからそのまま使っていますが、多分GameInstanceSubsystemから継承して、ゲーム全体のフェーズを管理することもできるはず(そうするとなるとGameState管理ではなくGameModeになるかもしれません)
ここも処理的にはLyraから変更をしていないので「LyraGamePhaseSubsystem」からコピペして適宜名前を変えていく形で大丈夫です。

4.GameStateの拡張

ここに関しては最低限使いそうなものだけ移植して使います。
ミソはGameStateにワールドで管理する「AbilitySystemComponent」を定義して使用することです。

コード例(ヘッダ)

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemInterface.h"
#include "GameFramework/GameStateBase.h"
#include "NGSGameStateBase.generated.h"

class UAbilitySystemComponent;
class UNGSAbilitySystemComponent;
/**
 * 
 */
UCLASS(Config = Game)
class NETGAMESAMPLE_API ANGSGameStateBase : public AGameStateBase, public IAbilitySystemInterface
{
	GENERATED_BODY()

public:

	ANGSGameStateBase(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
	
	//~AActor interface
	virtual void PreInitializeComponents() override;
	virtual void PostInitializeComponents() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
	virtual void Tick(float DeltaSeconds) override;
	//~End of AActor interface

	//~IAbilitySystemInterface
	virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
	//~End of IAbilitySystemInterface

	// Gets the ability system component used for game wide things
	UFUNCTION(BlueprintCallable, Category = "NGS|GameState")
	UNGSAbilitySystemComponent* GetNGSAbilitySystemComponent() const { return AbilitySystemComponent; }	

private:
	// The ability system component subobject for game-wide things (primarily gameplay cues)
	UPROPERTY(VisibleAnywhere, Category = "NGS|GameState")
	TObjectPtr<UNGSAbilitySystemComponent> AbilitySystemComponent;
};

CPP

#include "GameModes/NGSGameStateBase.h"

#include "AbilitySystem/NGSAbilitySystemComponent.h"
#include "Async/TaskGraphInterfaces.h"
#include "Net/UnrealNetwork.h"


ANGSGameStateBase::ANGSGameStateBase(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.bStartWithTickEnabled = true;

	AbilitySystemComponent = ObjectInitializer.CreateDefaultSubobject<UNGSAbilitySystemComponent>(this, TEXT("AbilitySystemComponent"));
	AbilitySystemComponent->SetIsReplicated(true);
	AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
	
    UE_LOG(LogTemp, Display, TEXT("ANGSGameStateBase Start"));

}

void ANGSGameStateBase::PreInitializeComponents()
{
	Super::PreInitializeComponents();
}

void ANGSGameStateBase::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	check(AbilitySystemComponent);
	AbilitySystemComponent->InitAbilityActorInfo(/*Owner=*/ this, /*Avatar=*/ this);
}

UAbilitySystemComponent* ANGSGameStateBase::GetAbilitySystemComponent() const
{
	return AbilitySystemComponent;
}

void ANGSGameStateBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);
}

void ANGSGameStateBase::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
}

※NGSと書かれてる部分は適当に変更してください。自身がテストで作っているプロジェクトの頭文字です。

5.ブループリントの作成

4まででC++ファイルを使って実際にブループリントを組んでゲーム内に組み込みをします。

作成する流れは以下のような形になります
・GameModeの作成(BP_GameModeと名前を付けて使っています)
 →使用するGameStateを読み込ませるために設定
※サードパーソンのプロジェクトだとワールドセッティングのGameMode設定部分の「ゲームモードオーバーライド」に初期設定でプロジェクトで初期からあるGameModeが設定されているので注意が必要

・GamePhaseAbilityを作成
 →今回はLyraに倣って「WarmUp」「Playing」「PostGame」の3種を用意

WarmUp→ゲーム開始前の状態(ゲーム画面が出て、移動などができるが、プレイヤーのアクションができなかったりちょっと制限がかかった状態などで使うフェーズ)
Playing→実際のインゲームとして動かせるフェーズ。ここでゲームを行う
PostGame→インゲームが終了して終わりに入るフェーズ。WarmUpと同様一部動作に制限などをかけたり、リザルトを出したりと切り分ける

Phase_WarmUpのBP
Phase_PlayingのBP
Phase_PostGame

・GameStateBaseを作成(BP_GameStateと名前を付けて使っています)
 →C++で作成したNGSGameStateBaseを継承します。そうすることで、C++側で実装した内容を実行をしてからBP内の処理を実行します。

BP_GameStateのBeginPlayの処理
BP_GameStateのTickの処理

・プロジェクト内に用意されているBP_ThirdPersonCharacterを編集
 →フェーズを以降させるために簡単な処理を作っています。

BP_ThirdPersonCharacterにデバッグ用のキーイベントを作る

・GameModeの設定
 →BP_GameModeを作り、BP_GameStateが動くように設定

プロジェクト設定 - マップ&モードの設定

・ワールド設定
 →ゲームモードオーバーライド設定を「なし」にする。
※親切設計でデフォルトのマップだと違うGameModeを設定してもワールド設定で個別に設定できることがわかるように上書き情報にゲームモードの設定が入っています(そういう意図だと信じたい)

ワールド設定

ここまで準備したらビルドが通れば動くはず
 →起動時にGameStateのBeginPlayが走りWarmUpが呼ばれる。その後WarmUpでカウントダウンをしていき、5までカウントしたらPlayingフェーズを呼ぶ。その後はキーボードのPを押すことでEndフラグを立てPostGameフェーズを呼ぶ。
一連の流れが見えた。
※WarmUp中にPを押してもPostGameフェーズにはいかず、PostGameフェーズでPを押してももう一度ログが呼ばれることはないことを試してみてもらえれば動作をしていることがわかると思います。

PIEのログで動作を確認

まとめ

今回はLyraのGamePhaseAbilityというGameplayAbilityの仕組みを使ったフェーズの状態遷移の拡張部分を抜粋して紹介しました。Lyra自体はものすごい多機能かつ基本機能の拡張が入っていてすごいサンプルなのですが、いかんせん複合で機能が作られていて1つ1つの機能を読み解くために別の機能の理解もしないといけないので正直、難しい(技術者のこんなこともできますよを善意で混ぜ込んだら初心者が読めなくなったパターンかもしれない)

Lyraについては個人的にUnreal Engineの基本的な動作を覚えてからでも正直難しいと思っています(ある程度分解して読めるようになるまでで集中してやってないのもありますが1,2年かかってます・・・)

今後もこういったサンプルを投稿できるようにしていこうと思います。

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