LoginSignup
2
4

More than 1 year has passed since last update.

UnrealEngine GameplayAbilitySystemをLevelをまたいで使えるように改造してシーケンス制御として使う方法

Posted at

はじめに

UEにはGameplayAbilitySystem(以下GAS)という、能力やアトリビュートを実装するためのフレームワークがあります。
アクションゲームであれば主に攻撃アクションなどを実装するのに使われたりしますが、
今回はゲーム全体のシーケンス制御にGASを使ってみようというお話です

基本的な機能しか使わないのでGASの仕組みを理解するのにも良いですし、どんなゲームでも使えるテクニックだと思います。

複数人で遊ぶカードゲームを例に考えてみましょう

image.png

こういうシーケンスを作ります

  • タイトルでボタンを押すとゲームが始まり、終了したらタイトルへ戻る
  • ゲーム中は複数のシーケンスがある
    image.png

今回使うGASの機能

image.png

  • AbilitySystemComponent
     アビリティ(タスクみたいなもの)を登録するマネージャーみたいなクラス
  • GameplayAbility
     今回はこのアビリティをシーケンスのタスクとして使います
  • GameplayTag
     アビリティに紐づけるタグ
  • GameplayEvent
     アビリティにイベントを通知するクラス

GASはレベルをまたいで存在できない?

cropmoneychitose.jpg
実装している中で1つ問題にぶち当たりました。AbilitySystemComponentです。
これはActorComponentで、Actorにくっつけて使う必要があります。

なのでOpenLevelすると消えてしまうんです。
先程考えたシーケンスは「タイトル」と「インゲーム」はレベルを分けたいのでこれでは困ります。

image.png

ActorComponentを延命させる方法

簡単に言うとレベル遷移する時にOuterを無理やり切り替えてやります。

まずは、AbilitySystemComponentを継承したカスタムクラスを作ります。
デフォルトだとUnregisterComponent時にアビリティがDestroyされてしまうのでそれを防ぐためです

UCLASS()
class UContinuousAbilitySystemComponent : public UAbilitySystemComponent
{
	GENERATED_BODY()

	virtual void OnUnregister() override
	{
		// UnregisterComponentした時にAbilityがDestroyされないようにする
		UActorComponent::OnUnregister();
	}
};
  • 適当なGameInstanceSubsystemを作ってそのInitializeでUAbilitySystemComponentのオブジェクトを生成します。
  • レベル遷移前と後のDelegateに関数を登録します
void UAbilitySystemSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);

	AbilitySystem = NewObject<UContinuousAbilitySystemComponent>(this);

	// レベル遷移前にGameStateから登録解除する
	FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UAbilitySystemSubsystem::OnPreLoadMap);

	// レベル遷移後にGameStateに登録する
	FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &UAbilitySystemSubsystem::OnPostWorldInitialization);
	GetWorld()->GameStateSetEvent.AddUObject(this, &UAbilitySystemSubsystem::RegisterToGameState);
}

レベル遷移する直前に呼ばれる関数です。

  • Rename関数を使ってOuterをこのSubsystemに切り替えます。
  • AbilitySystemComponentと登録されているGameplayAbility両方にやる必要があります
void UAbilitySystemSubsystem::OnPreLoadMap(const FString& MapName)
{
	// GameStateと共に削除されてしまわないようにOuterを変更する
	AbilitySystem->UnregisterComponent();
	AbilitySystem->Rename(nullptr, this);

	for (const FGameplayAbilitySpec& AbilitySpec : AbilitySystem->GetActivatableAbilities())
	{
		const TArray<UGameplayAbility*>& AbilityInstances = AbilitySpec.GetAbilityInstances();
		for (UGameplayAbility* Ability : AbilityInstances)
		{
			Ability->Rename(nullptr, this);
		}
	}
}

レベル遷移直後に呼ばれる関数です。

  • OuterをGameStateにつけ直す関数を呼び出します
  • このタイミングでGameStateがまだ生成されていない場合があるのでGameStateSetEventにも登録しておきます
void UAbilitySystemSubsystem::OnPostWorldInitialization(UWorld* World, const UWorld::InitializationValues IVS)
{
	if (AGameStateBase* GameState = World->GetGameState())
	{
		RegisterToGameState(GameState);
	}
	else
	{
		World->GameStateSetEvent.AddUObject(this, &UAbilitySystemSubsystem::RegisterToGameState);
	}
}

OuterをGameStateにつけ直す関数です。

  • Rename関数を使ってOuterをGameStateにします。
  • RegisterComponentとInitAbilityActorInfoも呼ぶ必要があります。
void UAbilitySystemSubsystem::RegisterToGameState(AGameStateBase* GameState)
{
	// 変更したOuterをGameStateに戻す
	AbilitySystem->Rename(nullptr, GetWorld()->GetGameState());

	for (const FGameplayAbilitySpec& AbilitySpec : AbilitySystem->GetActivatableAbilities())
	{
		const TArray<UGameplayAbility*>& AbilityInstances = AbilitySpec.GetAbilityInstances();
		for (UGameplayAbility* Ability : AbilityInstances)
		{
			Ability->Rename(nullptr, GetWorld()->GetGameState());
		}
	}

	AbilitySystem->RegisterComponent();
	AbilitySystem->InitAbilityActorInfo(GetWorld()->GetGameState(), GetWorld()->GetGameState());
}

これで完了です!OpenLevelをしても死ななくなりました。
BPではこのように取得します。

image.png

準備ができたので各画面のシーケンスを作っていきましょう

cropSAYA0I9A8798.jpg

まずはシーケンスに必要なタグを追加する

追加方法など詳しくはこちらの記事
UnrealEngine GamePlayTagを使おう!
image.png

タイトルシーケンス用のタスク(GameplayAbility)を作る

ちなみにゲーム画面です
image.png

右クリックのGamePlayAbilityブループリントから作ります。
image.png

image.png

BPはこんな感じです。ウィジェットを作成して、ボタンが押されるまで待ちます
image.png

クラスの設定で以下の設定をします。

  • Ability Tags (このAbilityがどのタグに属しているか)
  • Activation Ownd Tag (このAbilityがアクティブになった時に有効になるタグ)

それぞれに同じタグを設定しておきます。

image.png

Abilityの登録

タイトル用のレベルを作ったらそのレベルブループリント、または登録されているGameStateのBeginPlayで以下のように登録します。

image.png

これで使えるようになりました。
プレイすると、アビリティが動き出し、Widgetが表示されれば成功です。

ボタンが押されたら次に進むようにしてみましょう

ボタンが押されたらイベントを発行

このようにイベント用のタグを指定することで、待機しているAbilityに通知することができます。

image.png

AbilityはこのWaitノードでイベントがくるのを待っているので、イベントが来ると先に進みます。
image.png

メインシーケンスを作成する

タイトル画面はできました、今度は各画面遷移を管理するメインシーケンス用のアビリティを作ります。
image.png
BPはこんな感じ。

  • タイトルが終了したらインゲーム用のレベルをオープンして
  • インゲームが終了したらタイトルをオープンします
    image.png

登録はGameInstance等で行うと良いでしょう。
image.png

インゲーム用のシーケンスも作っていこう

ちなみに、ゲーム中の画面です
image.png

タイトルと同じように必要なAbilityを追加していきます。
image.png

登録もタイトルと同じようにインゲーム用のレベルを作ってそこで登録します。
image.png

インゲーム中だけのシーケンスを管理するアビリティを作る

インゲーム中は複数のシーケンスに分かれているのでそれらを管理するアビリティを作成します
image.png

初期化して、それぞれの子シーケンスを起動、終了を待つを繰り返します。
image.png

インゲームを抜けるときはどうするの?

CancelAbilities関数を使って「InGame」タグのアビリティをキャンセルしてやるだけです。
c++からしか呼べないのでBPへ公開する関数を用意しても良さそうです。
このあたりの機能が標準で備わってるのがGASの良いところですね!

//.h
UFUNCTION(BlueprintCallable)
void CancelAbilities(FGameplayTagContainer WithTags, FGameplayTagContainer WithoutTags, UGameplayAbility* Ignore = nullptr);

//.cpp
void UAbilitySystemSubsystem::CancelAbilities(FGameplayTagContainer WithTags, FGameplayTagContainer WithoutTags, UGameplayAbility* Ignore)
{
	AbilitySystem->CancelAbilities(&WithTags, &WithoutTags, Ignore);
}

おわりに

switch caseでもできそうなことをわざわざGASを使って長々とやってきましたが、このやり方のメリットは

  • クラスの粒度が小さくなって管理しやすい
  • シーケンスの遷移を容易に変えられる(チュートリアルだったら説明のシーケンスを追加とか)
  • シーケンスのタスク自体も簡単に切り替えられる(別のゲームモードだと勝利条件が異なるとか)

です。
最初の準備が少し面倒ですが使っていくとシーケンスの作成がかなり楽になりますよ。

以上。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4