らくするです。
今回は、いまアツいGameplayAbilitiesプラグインを使ったダッシュアビリティ(ダッシュ以外にも幅広く使える!)の実装について紹介したいと思います。
本記事で紹介する方法はダッシュ以外にも色々と使い道があるため、GASの導入時についでに実装しておくといいかも…なんて思ったり。
※本記事の実装方法を用いて生じたいかなる問題に対しても責任を負えませんのでご了承ください。
今回の記事を書くに至った経緯としては、GameplayAbilitySystemによるダッシュ(スプリント)を実装しようとしたとき、GameplayAbilityクラスを1つだけ使ってShiftキーのPress/Releaseの判定をするように実装するのがブループリンターには少し厄介だったためです。
例えば、GameplayAbilityクラスを「歩行」と「走行」の2つのアビリティを用意して、インプットアクションによるイベントを使ってShiftキーを押したら「走行」アビリティを発動し、離したら「歩行」アビリティが発動するように仕組めば可能ではあるのですが、あまりスマートじゃないように思えます。
GameplayAbilityごとにキー入力を関連付けるというのはかなり多用しそうな処理であるにも関わらず、紹介や解説の記事・動画はあまりないようでしたので、自分なりにまとめてみようと思います。
誤っている部分やよろしくない記述が散見されるかもしれませんので、もしよろしければコメントやDMでご指摘いただけますと幸いです。
今回の実装にあたって、GASShooterのコードの一部の他に、Twitterでのあいす氏(@koorinonaka)からの下記リプライを参考にいたしました。ご助言ありがとうございました。
https://t.co/AgtR3eV8ST
— あいす (@koorinonaka) 2021年12月23日
4.6.2 Binding Input to the ASC にInputPressed/Releasedへの実装が書いてあります。ちょっとこの辺は仕様がややこしいですね。。GiveAbilityで渡すFGameplayAbilitySpecに指定するInputIDが一致してればAbilityTaskのInputPressedでイベントが発行されるって感じです。
GASの導入
今回使用したのはUE5EAです。
GASをプロジェクトに導入するにはC++を使った初期セットアップが必要ですが、これについては、他のブログ様などでもよくご紹介されている
おかわりはくまい氏によるセットアップ方法の紹介記事が非常に参考になります。
当該記事についてはタイトルだけ下記しておきます。
・GameplayAbilitiesの使い方(セットアップ編) - おかわりのアンリアルなメモ
ダッシュの実装
前置き
話を振り出しに戻しますが、なぜGameplayAbilityクラスを1つでダッシュを実装しようとすると面倒なのかについて少しだけ…。
今回、肝要となるのが以下の、WaitInputPress/WaitInputRelease(特に後者)というキー入力のPress/Releaseの監視をしてくれるAbilityTaskになります。
え、そいつら使えばすぐできるんじゃ…?
と思われるかもしれませんが、これらのAbilityTaskはすぐには使えません。
各GameplayAbilityをキー入力に紐づける必要があり、そのためにはプロジェクト設定のアクションマッピングだけでなく、C++を使って紐づける必要があるためです。
GameplayAbilityにキー入力を紐づける
本記事では、おかわりはくまい氏によるセットアップ記事の通りにセットアップしたものを改変して実装していきます。
EnumでインプットIDを定義する
定義する場所はどこが最適なのかわからないのでプロジェクトのヘッダーに定義しました。
DisplayNameは適当でも大丈夫ですが、各アクション名はプロジェクトのアクションマッピングと同じ名前にしておきましょう。
開発を進むにつれてインプットアクションを増やす場合はその都度、EAbilityInputIDも増やしていけば良いだけです。
プロジェクト名.h
#pragma once #include "CoreMinimal.h" UENUM(BlueprintType) enum class EAbilityInputID : uint8 { // 0 None None UMETA(DisplayName = "None"), // 1 Confirm Confirm UMETA(DisplayName = "Confirm"), // 2 Cancel Cancel UMETA(DisplayName = "Cancel"), // 3 Sprint Sprint UMETA(DisplayName = "Sprint") };
※今回は参考にしたGASShooterにConfirmやCancelといったInputIDが定義されていたので同様に追加しております。
これはおそらく、WaitForCancelInput / WaitForConfirmInputといったAbilityTaskで用いるものと思われますが、詳しく調べてないためここでは説明無しです。
GameplayAbilityクラスを拡張する
BPでアビリティ毎のバインド先の変更が簡単にできるよう、MyGameplayAbilityクラスを作成します。
MyGameplayAbility.h
#pragma once #include "CoreMinimal.h" #include "プロジェクト名/プロジェクト名.h" #include "Abilities/GameplayAbility.h" #include "MyGameplayAbility.generated.h" UCLASS() class プロジェクト名_API UMyGameplayAbility : public UGameplayAbility { GENERATED_BODY() public: UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability") EAbilityInputID AbilityInputID = EAbilityInputID::None; };
MyCharacterクラスを改変する
(変更点)
・インクルード対象にMyGameplayAbilityクラスおよびプロジェクトのヘッダーを追加する。
・AbilityListをMyGameplayAbilityクラスの配列にする。
・InputIDに対してキー入力をバインディングするためのBindASCInput()を宣言。
MyCharacter.h
#include "CoreMinimal.h" #include "GameFramework/Character.h" #include "プロジェクト名/プロジェクト名.h" #include "AbilitySystemInterface.h" #include "MyGameplayAbility.h" #include "MyCharacter.generated.h" UCLASS() class プロジェクト名_API AMyCharacter : public ACharacter { GENERATED_BODY() ・・・ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Abilities, meta = (AllowPrivateAccess = "true")) class UAbilitySystemComponent* AbilitySystem; UAbilitySystemComponent* GetAbilitySystemComponent() const { return AbilitySystem; }; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Abilities) TArray<TSubclassOf<class UMyGameplayAbility>> AbilityList; }; protected: void BindASCInput(); bool bASCInputBound;
(変更点)
・InputIDを削除。
・MyGameplayAbilityクラスを継承した各アビリティのBPクラスで設定したAbilityInputID != 0であればその値をInputIDに、それ以外はInputIDに-1が割り当てられるように。
・BindASCInput()の処理を記述。
MyCharacter.cpp
・・・ void AMyCharacterBase::BeginPlay() { Super::BeginPlay(); if (AbilitySystem) { if (HasAuthority() && AbilityList.Num() > 0) { for (auto Ability : AbilityList) { if (Ability) { if (static_cast<int32>(Ability.GetDefaultObject()->AbilityInputID) != 0) { AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability.GetDefaultObject(), 1, static_cast<int32>(Ability.GetDefaultObject()->AbilityInputID), this)); } else { AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability.GetDefaultObject(), 1, -1, this)); } } } } AbilitySystem->InitAbilityActorInfo(this, this); } } void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); ・・・ BindASCInput(); } void AMyCharacter::BindASCInput() { if (!bASCInputBound && IsValid(AbilitySystem) && IsValid(InputComponent)) { AbilitySystem->BindAbilityActivationToInputComponent(InputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"), FString("CancelTarget"), FString("EAbilityInputID"), static_cast<int32>(EAbilityInputID::Confirm), static_cast<int32>(EAbilityInputID::Cancel))); bASCInputBound = true; } }
①GiveAbilityで渡すFGameplayAbilitySpecにInputIDを指定することで、各GameplayAbilityとInputIDとを紐づけます。
②BindASCInput()内のAbilitySystem->BindAbilityActivationToInputComponentによって、先ほどEnumで定義したInputIDに対応するキーがPress/Releaseされた際にインプットアクションが発行され、さらに①で紐づけられたInputIDに対応するGameplayAbilityがActivateされるようになっています。
なお、AbilitySystem->BindAbilityActivationToInputComponentの実行タイミングはSetupPlayerInputComponentの後にしていますが、
おそらくネットワーク対応ゲームの場合はOnRep_PlayerStateのときも含めて二回とすることが望ましいです(InputComponentが存在しない可能性があるため)。
※bASCInputBoundは二重にBindASCInput()が実行されないようにするための値なので、ネットワーク対応でない場合は不要。
ダッシュのGameplayAbilityクラスを作成する
MyGameplayAbilityクラスをBP継承したGA_Sprintを作成します。
作成したら、クラスのデフォルト設定にてAbilityInputIDをSprintにしておきます。
ちなみに、上記②のようにインプットアクションをGameplayAbilityにバインディングしているため、キャラクターのBP内で「インプットアクション○○」→「TryActivateBy~~」などを記述しておく必要はありません。
つまり、ダッシュ以外のGameplayAbilityを追加した際は、
・EAbilityInputIDと、プロジェクトのアクションマッピングに同名のアクションを追加する。
・追加したアビリティBPの設定から、EAbilityInputIDを選択する。
これだけを行うだけで、キャラクターのBP内でアビリティ発動の処理を記述する必要がなくなるというわけです。
中身は必要最低限で以下のような感じにしました。
Start/Stop Sprintingの中身はキャラクターのMovement Componentを取得してSetMaxWalkSpeedで移動速度を変更しているだけです。
完了
うまくいけば、ダッシュのキーを押すとアビリティがActivateされてStart Sprintingによってスピードが上がり、キーを離すとWaitInputReleasedのOnReleaseから続くEndAbilityが実行され、Stop Sprintingによってスピードが下がって歩行に戻る といった挙動が実現しているはずです。
無駄な部分やより良い方法があればぜひ教えていただけると幸いです。