本記事はQiita UnrealEngine AdventCalendar 2021の記事となります。
はじめに
本記事では、UE4でのAI機能の一つであるBehaviorTreeと、GameplayAbilitySystemの連携について調査したものとなります。最後まで読んでいただくと、AIでランダムに移動する敵キャラクターがGameplayAbilitySystemの仕組みを使って攻撃・スタミナの回復をするというサンプルゲームが完成します。若干、それぞれの分野の知識が必要な部分がありますが、基本的に操作手順は全て紹介していますので、参考にして頂ければと思います。
また、GameplayAbilitySystem(GAS)につきましては、本ブログ内で入門記事を作成しておりますので、過去記事も参考にして頂ければと思います。(今回の記事はQiita UnrealEngine AdventCalendar 2021向けの記事の為、過去記事の流れとは関連性はありませんが、同じ機能を説明している部分もありますので、記事の内容は重複している場合があります)
- 第1回「GAS入門 その1 グレイマンをジャンプさせよう」
- 第2回「GAS入門 その2 グレイマンにパンチを打たせよう」
- 第3回「GAS入門 その3 GASの構成振り返り」
- 第4回「GAS入門 その4 グレイマン疲れさせよう」
- 第5回「GAS入門 その5 グレイマンを回復させよう」
- 第6回「GAS入門 その6 パンチ後にクールタイムを導入しよう」
- ・・・以降もGAS記事は作成予定です
事の発端
事の発端は、、、今年の出張ヒストリアでの「目指せ脱UE4初心者!?知ってると開発が楽になる便利機能を紹介–DataAsset, Subsystem, GameplayAbility編 –」の講演の一コマでした。
実は、この時に初めてGASを知ったのですが、講演を聞いているうちに、今まで苦労していた部分が全て解消されそうな技術ということが分かり、興味津々に聞いていると、とても気になる一文を見つけました。
なんと!GameplayAbilitySystem(GAS)はAIのビヘイビアツリーとの連携ができるとな!
私が作っているゲームはUE4のビヘイビアツリーがキモとなっているので、そのビヘイビアツリーと、この便利そうなGASの連携というものに興味が沸いた為に、この実装方法について調べた結果を記事にすることにしました。
あ、ちなみにGASの概要については、過去記事「GAS入門 その1 グレイマンをジャンプさせよう」を参照して頂ければと思います。
GASで困ったらActionRPGサンプルプロジェクト!
GASの最適解といえば、ActionRPGサンプルの為、早速プロジェクトを開き、それっぽい所を探してみると・・・/Blueprint/AI/BossAI/BT_Bossにありました!
おおっ!ビヘイビアツリーからGameplayAbilityを呼んでいるっ!
また、UnrealFest Europe 2019内の講演「ヒーローAIゲームプレイ能力と行動ツリーの出会い」(原題「Hero AI – Gameplay Abilities Meet Behavior Trees」)の中からも、それっぽいBehaviorTreeTaskがあることを確認できました。
ということで、まずはこのあたりを解析していくことから始めます。
BTTask_UseAbility(BehaviorTreeTask)の解析
早速、ActionRPGでGameplayAbilityを呼んでいる”BTTask_UseAbility”を確認していきます。
処理を追ってみると、最初に引数で渡された対象Pawnを”RPGCharacterBase”にキャストしています。
このゲーム中のキャラクターは全て”RPGCharacterBase”を継承しているので、”RPGCharacterBase”にキャストすることが可能です。
“RPGCharacterBase”にはGASに必要なAbilitySystemComponent等が含まれている為、キャストする事で、どのキャラクターも自由にGASを使えるようになっています。また、GameplayAbilityを呼び出す為のGameplayTagはパラメータとして外部に公開しているので、ビヘイビアツリー側から指定することができます。(赤文字1番・4番の説明)
次に先ほど発動したアビリティをGetActiveAbilitiesWithTagで取得します。GameplayAbilityの発動はAbilitySystemComponentに任せている為、発動を依頼したActivateAbilitiesWithTagsノードの戻り値等からは取得できません。よって、改めてAbilitySystemComponentに問い合わせることで、発動中のGameplayAbilityを取得できるのです。これは配列で返ってくるので、その最初の要素のGameplayAbilityを取得します。(赤文字2番の説明)
そして、取得したGameplayAbilityの終了イベントを待ち、BehaviorTreeTaskの終了となります。(赤文字3番の説明)(BehaviorTreeTaskはFinishExecuteを返さないとBehaviorTreeに処理が移らない為、Abilityのイベント(例えば、アニメーション等)が終了するまで待っているという事です。)
つまり、AIで動かすキャラクターのAbilitySystemComponentにGameplayAbilityを付与しておき、BehaviorTreeTask内でGameplayAbilityを発動させるだけで、ビヘイビアツリーからGameplayAbilityをコールすることができそうです。ただし、BehaviorTreeTaskはタスクが終了した時にFinishExecuteノードを呼び出す必要がある為、GameplayAbilityの終了イベントを拾う必要がありそうです。
BTTask_UseAbility(BehaviorTreeTask)の流れを理解する
上記の説明と重複する部分もありますが、主要なノードを図で説明すると以下のようになります。
今回の調査対象はBehaviorTreeからGameplayAbilityを呼び出す方法です。つまり、下図のようにBehaviorTreeがスタートとなり、GameplayAbilityをコールするBehaviorTreeTaskのFinishExecuteがゴールとなります。それでは、処理の流れを追ってみましょう。(以下、青線が現在の処理、赤線が終了した処理となります)
BehaviorTreeから呼び出されたBehaviorTreeTask(ActionRPGサンプルではBTTask_UseAbility)のエントリーポイントであるReceiveExecuteAIノードから処理を開始します。ControlledPawn引数にはAIで動かしているキャラクターが格納されているので、キャラクター内に定義されたActivateAbilityWithTagsノード(GameplayAbilityを呼び出す処理)を実行します。
ActivateAbilityWithTagsノードに渡されたGameplayTagに紐づくGameplayAbilityを発動します。(GameplayTagの状態や、GameplayEffectによっては呼び出されない場合もありますが、今回の説明では正常な流れで説明しているので、必ず呼び出されるという事にしましょう)
同時にGetActiveAbilitiesWithTagsノードが実行されます。
GetActivateAbilitiesWithTagsは現在発動中のAbilityを取得します。つまり、ActivateAbilityWithTagsノードで発動させたGameplayAbilityが取得できるという事です。
「いやいや、ActivateAbilityWithTagsで能力を発動させた処理があるのに、何で改めて呼び出した能力を取得してんの!!面倒じゃん!」って思うかもしれませんが、能力の呼び出し処理はGameplayTagをキーにしてAbilitySystemComponentに任せているので、ActivateAbilityWithTagsノードからは発動した能力クラスを取得することができません。よって、全てがAbilitySystemComponent頼りの処理となっています。
また、同時に能力処理自体は実行中となっています。
GetActiveAbilitiesWithTagsで取得できたGameplayAbilityのOnAbilityEndedイベントのバインドノードを呼び出す事で、GameplayAbilityの能力処理が終了するタイミングを待ちます。
そして、能力処理が終わるとOnAbilityEndedからAbilityEndedイベントが呼び出され、BehaviorTreeTaskの終了となるFinishExecuteノードが呼び出されてゴールとなります。FinishExecuteが呼び出されることで、BehaviorTreeは次の処理へ移ることができるようになります。
如何でしょうか?なんとなく流れは理解できたのではないでしょうか?
次の章からは、今まで解析してきた処理をゼロから実装してみましょう。そして、GASの機能の中から、GameplayAttributeとGameplayEffectも使ってみましょう。
AIとGASを同時に実装していく為、読むだけでも大変な記事かもしれませんが、一つ一つの操作を説明することで、必ず上記のロジックが理解できるようになっていますのでお付き合いいただければと思います。
実装開始(下準備)
当ブログの過去記事「GAS入門 その3 GASの構成振り返り」でも触れていますが、キャラクタークラスに対してAbilitySystemComponentや、GameplayAbilityを付与・発動させる為の仕組みを実装した親クラスを作っておき、それをAIで動かすキャラクターに継承させたいと思います。(GASの一部機能はC++でないと実装できない為、C++でベースとなるクラスか、アクセサを用意しておく必要があります。今回はクラス設計も説明したいので、ベース(親)クラスでの実装をしていきます。)
今回作成するキャラクターの親クラスはActionRPGサンプルから引用していますが、より簡素化したクラスとなっていますので、最低限の機能を確認するにはちょうど良いサンプルになる思います。
今回は、Qiita UnrealEngine AdventCalendar 2021の記事という事もあり、この記事でも(簡単にですが)実装方法を紹介していきます。
なお、基本となっているプロジェクトは、ThirdPersonテンプレートで、検証環境はUE4.26.2です。
そして、GASを使うので、GameplayAbilitiesのプラグインの有効化は忘れないでください。(また、エディタ上でC++のエディタを適切に指定しておいてください。初期設定について詳しくは「GAS入門 その1 グレイマンをジャンプさせよう」に記載されています。)
1.GameCharacterクラスの作成
「メニュー」の「ファイル」から「新規C++クラス」を選択し、Characterを親クラスとして”GameCharacter”を作成してください。これは、そのままの意味でCharacterクラスを継承したGameCharacterクラスを作成することを意味します。
2.Build.csの設定
C++のエディタが開いたら、”プロジェクト名”.Build.csファイルのPublicDependencyModuleNamesに”GameplayAbilities”と”GameplayTags”を追加してください。これにより、GASに必要なモジュールを読み込んでくれるようになります。
// Fill out your copyright notice in the Description page of Project Settings.
using UnrealBuildTool;
public class GASSample : ModuleRules
{
public GASSample(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "GameplayAbilities", "GameplayTags" });
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
3.GameCharacter.hとGameCharacter.cppの編集
親クラスであるCharacterクラスに対して、AbilitySystemComponentを所有させ、GameplayAbilityとGameplayEffectを登録、GameplayAbilityの発動、実行中のGameplayAbilityの取得を行う機能を追加します。
まずはヘッダーファイルの定義を見ていきましょう。
GASを扱う為のメインとなるAbilitySystemComponentの定義と、上記で説明した機能の定義が主になります。
また、見落としがちですが、”IAbilitySystemInterface”を継承している点についても重要なポイントです。このインターフェイスの実装はGetAbilitySystemComponentという関数になっており、単純にこのクラスが保持しているAbilitySystemComponentを渡せば良いだけなんですが、これを忘れると後々説明するGameplayAttributeのSetterが効かなくなります。これ、マジでハマりました。
GameCharacter.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemInterface.h"
#include "GameCharacter.generated.h"
UCLASS()
class GASSAMPLE_API AGameCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AGameCharacter();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
public:
// AbilitySystemコンポーネント
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Abilities, meta = (AllowPrivateAccess = "true"))
class UAbilitySystemComponent* AbilitySystemComponent;
UAbilitySystemComponent* GetAbilitySystemComponent() const {
return AbilitySystemComponent;
};
// Abilityの登録
UFUNCTION(BlueprintCallable, meta = (DefaultToSelf = "Target"))
void AddAbility(TSubclassOf<class UGameplayAbility> Ability, int32 AbilityLevel);
// Abilityの発動
UFUNCTION(BlueprintCallable, meta = (DefaultToSelf = "Target"))
bool ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation);
// 発動中のAbilityの取得
UFUNCTION(BlueprintCallable, Category = "Abilities")
void GetActiveAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UGameplayAbility*>& ActiveAbilities);
// Effectの登録
UFUNCTION(BlueprintCallable, meta = (DefaultToSelf = "Target"))
void AddEffect(TSubclassOf<class UGameplayEffect> Effect, int32 EffectLevel);
};
GameCharacter.cppではGameCharacter.hで定義した関数(メソッド)の実装と、AbilitySystemComponentのインスタンス生成を行っています。
関数個別の説明は後ほどしますので、まずはソース全体を見渡してみましょう。
GameCharacter.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "GameCharacter.h"
// Sets default values
AGameCharacter::AGameCharacter()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
// AbilitySystemコンポーネントを作成する
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
}
// Called when the game starts or when spawned
void AGameCharacter::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AGameCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void AGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
// Abilityの登録
void AGameCharacter::AddAbility(TSubclassOf<class UGameplayAbility> Ability, int32 AbilityLevel){
if (AbilitySystemComponent && Ability){
AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability.GetDefaultObject(), AbilityLevel, -1));
}
}
// Abilityの発動
bool AGameCharacter::ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation)
{
if (AbilitySystemComponent){
return AbilitySystemComponent->TryActivateAbilitiesByTag(AbilityTags, bAllowRemoteActivation);
}
return false;
}
// 発動中のAbilityの取得
void AGameCharacter::GetActiveAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UGameplayAbility*>& ActiveAbilities)
{
TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
AbilitySystemComponent->GetActivatableGameplayAbilitySpecsByAllMatchingTags(AbilityTags, AbilitiesToActivate, false);
for (FGameplayAbilitySpec* Spec : AbilitiesToActivate)
{
TArray<UGameplayAbility*> AbilityInstances = Spec->GetAbilityInstances();
for (UGameplayAbility* ActiveAbility : AbilityInstances) {
ActiveAbilities.Add(Cast<UGameplayAbility>(ActiveAbility));
}
}
}
// Effectの登録
void AGameCharacter::AddEffect(TSubclassOf<class UGameplayEffect> Effect, int32 EffectLevel){
if (AbilitySystemComponent && Effect){
FGameplayEffectContextHandle EffectContext = AbilitySystemComponent->MakeEffectContext();
EffectContext.AddSourceObject(this);
FGameplayEffectSpecHandle NewHandle = AbilitySystemComponent->MakeOutgoingSpec(Effect, EffectLevel, EffectContext);
if (NewHandle.IsValid()) {
AbilitySystemComponent->ApplyGameplayEffectSpecToTarget(*NewHandle.Data.Get(), AbilitySystemComponent);
}
}
}
それぞれの関数(メソッド)について解説していきます。
関数単体で見ればそれほど大きな機能は無いので、C++未経験の方でもなんとなく理解はできると思います。
・コンストラクタ
コンストラクタ内ではAbilitySystemComponentのオブジェクトを生成しています。BPでいうところの「コンポーネントを追加」でAbilitySystemComponentを選択したのと同じ状態です。これで、このクラスはGASを自由に使えるようになります。
// Sets default values
AGameCharacter::AGameCharacter()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
// AbilitySystemコンポーネントを作成する
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
}
・AddAbility
引数で渡されたGameplayAbilityとAbilityLevelを、AbilitySystemComponentに登録します。日本語で説明すると「能力を付与する」処理となっています。(AbilityLevelに関しては今回のサンプルでは使用していませんが、単純にAbilityの実装側でGetLevelを叩けばこの数値を取得することがき、それで処理を切り替えたりします。)
// Abilityの登録
void AGameCharacter::AddAbility(TSubclassOf<class UGameplayAbility> Ability, int32 AbilityLevel){
if (AbilitySystemComponent && Ability){
AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability.GetDefaultObject(), AbilityLevel, -1));
}
}
・ActivateAbilitiesWithTags
引数に指定されたタグを使って、AbilitySystemComponentに付与されているGameplayAbilityを発動を試みます。
AbilitySystemComponentが管理しているGameplayTagの状態とGameplayAbilityのタグ制御の関係や、Costで指定されたGameplayEffectによって、必ずGameplayAbilityが発動するとは限りません。
// Abilityの発動
bool AGameCharacter::ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation)
{
if (AbilitySystemComponent){
return AbilitySystemComponent->TryActivateAbilitiesByTag(AbilityTags, bAllowRemoteActivation);
}
return false;
}
・GetActiveAbilitiesWithTags
AbilitySystemComponentから、引数で渡されたGameplayTagで発動する現在発動中のGameplayAbilityを取得します。
// 発動中のAbilityの取得
void AGameCharacter::GetActiveAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UGameplayAbility*>& ActiveAbilities)
{
TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
AbilitySystemComponent->GetActivatableGameplayAbilitySpecsByAllMatchingTags(AbilityTags, AbilitiesToActivate, false);
for (FGameplayAbilitySpec* Spec : AbilitiesToActivate)
{
TArray<UGameplayAbility*> AbilityInstances = Spec->GetAbilityInstances();
for (UGameplayAbility* ActiveAbility : AbilityInstances) {
ActiveAbilities.Add(Cast<UGameplayAbility>(ActiveAbility));
}
}
}
・AddEffect
引数で渡されたGameplayEffectとEffectLevelを、AbilitySystemComponentに登録します。日本語で説明すると「パラメータ(GameplayAttribute)への影響の登録」処理となっています。
// Effectの登録
void AGameCharacter::AddEffect(TSubclassOf<class UGameplayEffect> Effect, int32 EffectLevel){
if (AbilitySystemComponent && Effect){
FGameplayEffectContextHandle EffectContext = AbilitySystemComponent->MakeEffectContext();
EffectContext.AddSourceObject(this);
FGameplayEffectSpecHandle NewHandle = AbilitySystemComponent->MakeOutgoingSpec(Effect, EffectLevel, EffectContext);
if (NewHandle.IsValid()) {
AbilitySystemComponent->ApplyGameplayEffectSpecToTarget(*NewHandle.Data.Get(), AbilitySystemComponent);
}
}
}
これで下準備は完成です。コンパイルボタンを押してコンパイルエラーが出ない事を確認しておいてください。
AIを使用したキャラクターの仕様を考える
今回はビヘイビアツリーからGameplayAbilityを呼ぶ検証がメインなので、AIで複雑なロジックを組むことはしません。
よって、AIを使用したキャラクターの仕様としては・・・
1.GameplayAbilityとして「攻撃(パンチ)」の能力を付与する
2.パンチはスタミナ(GameplayAttribute&GameplayEffect)を消費する
3.スタミナが切れたらパンチを出せなくなる
4.スタミナはパンチを出していない時に少しずつ回復する(GameplayAttribute&GameplayEffect)
5.AIの動作としては単純で、「ランダムで移動」→「パンチを出す」を繰り返す。
という感じにしていこうかと思います。
AIを使用したキャラクターを作る
まずは、AIで勝手に動くキャラクターを作成します。このキャラクターはパンチで攻撃をしてくるので、敵キャラクターという事にしましょう。
1.敵キャラクター用のブループリントの作成
新規ブループリント作成でCharacterクラスをベースに作成します。名前は”BP_AIEnemy”とします。
2.AIコントローラーの作成
AIに必要なブループリントも定義していきましょう。こちらも新しいブループリントの作成から”AIController”を親クラスとして”AI_AIEnemy”を作成します。(名前がダサい・・・)
3.ビヘイビアツリーの作成
コンテンツブラウザで右クリックから「AI」→「ビヘイビアツリー」も作成しましょう。名前は”BT_AIEnemy”とします。
4.ブラックボードの作成
上記で作成した”BT_AIEnemy”をダブルクリックしてエディタを開き、「新規ブラックボード」を選択し、”BB_AIEnemy”を作成します。
5.アニメーションブループリントの作成(複製)
敵キャラクター用のアニメーションBPを用意します。今回は敵キャラクターはグレイウーマンを使う予定ですので、ボーン構造が同じグレイマンのアニメーションブループリントを流用しましょう。
“ThirdPerson_AnimBP”(/Mannequin/Animations/ThirdPerson_AnimBP)をコピーして”ANIM_AIEnemy”としました。最終的には、こんな感じで、”BP_AIEnemy”を動かす為の基本的な部品が揃いました。
6.アニメーションブループリントのAnimGraphにスロットを追加
“ANIM_AIEnemy”には、パンチを繰り出す為のアニメーションを表示する為に、アニメーションBPのAnimGraphのステートマシンと出力ポーズの間に「Defaultスロット」を入れておきます。
7.BP_AIEnemy(敵ブループリント)の実装
“BP_AIEnemy”を開き、「クラス設定」の「親クラス」には先ほど作成した”GameCharacter”を設定します。これによって、”BP_AIEnemy”はAbilitySystemComponentを持ち、GameplayAbilityの付与や発動、GameplayEffectの付与が簡単に行るようになりました。
Meshには”SK_Mannequin_Female”を選択します。Meshをカプセルコンポーネントに収まるように修正し、AnimClassには”ANIM_AIEnemy”を選択します。
最後にAIコントローラーを設定したいので、”BP_AIEnemy”の詳細パネルにある「AI Controller Class」には”AI_AIEnemy”を指定してください。
8.AIコントローラー実装
“AI_AIEnemy”を開き、イベントグラフのOnPossesからビヘイビアツリーを呼び出します。RunBehaviorTreeノードのBTAssetには、”BT_AIEnemy”を選択してください。AIコントローラーはビヘイビアツリーに全ての処理を任せるので、実装内容はこれだけです。
9.ビヘイビアツリー・ビヘイビアツリータスク・ブラックボードの実装
ビヘイビアツリーとブラックボードを編集します。
まずは、”BT_AIEnemy”を開き、新規タスクをクリックします。そして、以下のブループリントを組みます。これは、”BP_AIEnemy”は自身の周囲750の範囲で次の移動先を見つけて、その結果をブラックボードの”NextMovePoint”に書き込むという処理になります。(この時点では、ブラックボードに”NextMovePoint”という項目は無い為、下記で作成します)
保存ボタンをクリックすると、ビヘイビアツリーと同じフォルダに”BTTask_BlueprintBase_New”というビヘイビアツリータスクが生成されているので、名前を”BTT_NextMove”に変更します。
その後、先ほどのビヘイビアツリーに戻り、ブラックボードボタンをクリックし、新規キーを追加します。
型は”Vector”とし、名前は”NextMovePoint”とします。これが、上記で決められた移動先を格納しておくエリアとなります。
ビヘイビアツリーに戻り、以下のようにロジックを組みます。ロジックは単純で、”BTT_NextMove”で次の移動先を確ブラックボードの”NextMovePoint”に書き込み、標準命令のMoveToでブラックボードに書き込まれた”NextMovePoint”に移動するだけです。
10.ナビメッシュバウンズボリュームの配置
上記のビヘイビアツリーで設定したMoveToはナビメッシュバウンズボリュームが設定されていないと自動的に動いてくれない為、レベルに ナビメッシュバウンズボリューム を配置する必要があります。
レベルエディタに戻って、アクタを配置からナビメッシュバウンズボリュームをレベル上に配置してください。大きさはステージ全体を囲うように調整し、「表示」の「ナビゲーション」を使ってナビメッシュが有効になっているエリアを確認してください。
11.敵キャラクターの動作確認
レベル上に”BP_AIEnemy”を配置し、プレイボタンを押すと、以下のように”BP_AIEnemy”がレベル上を自由に動き回るようになります。
ひとまず、ここまでで敵がランダムに動くという実装が完了しました。
次は、”BP_AIEnemy”にパンチ能力を与えていきます。
AIで動くキャラクターにパンチの能力を与える
パンチのアニメーションについては、AdobeのMixamoというサービスからパンチアニメーションを拾ってきて、グレイマンのアニメーションに適用する必要があります。
手順はこのサイトを参考にしてください。
無事にパンチアニメーションをUE4にインポートできたら、そこからパンチアニメーションのアニメーションモンタージュを作成しておいてください。(アニメーションを右クリックして、「作成する」→「AnimMontageの作成」で作成可能です)
1.GameplayTagの作成
プロジェクト設定からGameplayTagを追加していきます。
このサンプルでは使わないタグも表示されていますが、とりあえずはability配下の”cooltime”と”punch”と”running”があれば大丈夫です。
2.パンチ能力の作成
新規ブループリント作成で、”GameplayAbility”を親クラスにした”GA_Punch”を作成します。
中身はこんな感じにします。
能力が発動されたら、アニメーションモンタージュでパンチアニメーションを再生しているだけです。
重要なのは、右側の詳細タブになります。
“ability.punchタグ”でパンチ発動、パンチが発動中は”ability.runningタグ”が設定される為、同時に何度も能力が発動することはありません。このように、GameplayAbilityはGameplayTagを使って能力の発動をコントロールすることができます。
GameplyaAbilityで設定できる主なGameplayTagについては、以下のようになっていますので、参考にしてください。
Ability Tags | ActivateAbilityから能力を呼び出す時に使用するタグ |
Cancel Abilities With Tag | このAbilityが発動した場合、ここに定義されたTagを持つほかのAbilityをキャンセルする |
Block Abilities With Tag | このAbilityが発動した場合、ここに定義されたTagを持つAbilityを実行させない |
Activation Owned Tags | このAbilityの発動中(ActivateAbilityからEndAbilityまで)の間、ここで定義されたTagが能力を割り当てたActorのAbilitySystemコンポーネントに付与される |
Activation Required Tags | 能力を割り当てられたActorのAbilitySystemコンポーネントがこのタグを持っている時に能力を発動することができる |
Activation Blocked Tags | 能力を割り当てられたActorのAbilitySystemコンポーネントがこのタグを持っている時に能力を発動することができない |
また、CommitAbilityノードがありますが、これは後程使いますが、あっても邪魔にはならないので、入れておいてください。そして、OnEndAbilityノードからは、イベントディスパッチャーでOnAbilityEndedというディスパッチャーを定義しておき、これを呼び出すようにしてください。
3.敵キャラクターの生成と同時にパンチ能力を付与
“BP_AIEnemy”を開き、パンチ能力を与えます。C++で下準備を行っておいたので、パンチ能力を与える処理はこれだけです。
4.AIロジックの中でパンチ能力を発動させる為のロジック作成
ビヘイビアツリー(BT_AIEnemy)からパンチ能力を発動させる為のBehaviorTreeTaskを作成していきます。
“BT_AIEnemy”を開き、新規タスクをクリックし、”BTTask_BlueprintBase”を選択してください。すると、新しいBehaviorTreeTaskが作成されますので、ActionRPGを見習って次のようなブループリントを実装します。(なお、作成したBehaviorTreeTaskには勝手に名前が付けられているので、”BTT_AbilityPunch”という名前にしました)
ここが、今回の解説の核となる部分です。
ReceiveExecuteAIノードから開始したBehaviorTreeTaskは、対象Pawn(この例ではBP_AIEnemy)をGameCharacterにキャストします。これは、BP_AIEnemyの親クラスにGameCharacterを指定しているので、基本的には失敗しないはずです。
次に、GameCharacter内で定義しておいたGameplayAbilityを発動させる命令であるActiveAbilitiesWithTagノードを呼び出します。今回はパンチ能力を呼び出すことが決まっているので、ノード上に直接”ability.punch”を設定しました。
その後、GetActiveAbilitiesWithTagノードで、BP_AIEnemyで現在発動中のGameplayAbilityを取得します。つまり、パンチ能力が取得できるということです。(ただし、スタミナ切れ等でパンチが発動できなかった場合は何も取得できません)
そして、その結果をGA_Panchにキャストし、イベントディスパッチャーで定義しておいた終了イベントを拾って、BehaviorTreeTaskの処理終了となる、FinishExecuteノードを呼び出しています。
今回は呼び出す能力もパンチ限定となっているので、GA_Panchにキャストする事は間違いではないのですが、もっと汎用的に作る場合は、GA_Panchに親クラスを作成しておき、親クラス側でイベントディスパッチャーを定義し、ここのキャスト処理も親クラスにキャストすれば、様々なGameplayAbilityの呼び出しに対応できるはずです。(最初に紹介したActionRPGサンプルのGameplayAbilityを呼び出すBehaviorTreeTaskでは、GA_AbilityBaseにキャストしているのが、その例となります)
5.ビヘイビアツリーの流れの中にパンチ能力を呼び出すビヘイビアツリータスクを追加
“BT_AIEnemy”のロジックを以下のように修正します。
MoveToで移動後にパンチ能力を発動するBehaviorTreeTaskを呼んでいるだけですね。
6.動作確認
AIキャラクターが「次の移動先を見つける」→「移動する」→「パンチ」を繰り返すようになります。
物騒な世の中になってきました。。。。今回の目的である、BehaviorTreeからGameplayAbilityシステムを呼び出すという目的は、この時点でも達成しているので、本編はここまでとなります。
引き続き、GameplayAttribute&GameplayEffectを使ったスタミナ管理を実装していきますので、興味のある方は読み進めて頂ければと思います。
AIキャラクターにスタミナを持たせる
今度は”BP_AIEnemy”にスタミナを持たせていきます。
普通に考えると、”BP_AIEnemy”に”Stamina”というintかfloatの変数を持たせるところですが、そうなると、「スタミナ切れの場合にパンチを出せない」というロジックを書く際にブランチノードが登場することになります。ブランチノードが悪というワケではありませんが、他にも値を管理しようとすると、ブループリントがブランチノードまみれになってしまう可能性があります。
GameplayAbilitySystemには、それを解決してくれるGameplayAttribute&GameplayEffectという仕組みがありますので、今回はその仕組みでスタミナ管理してみましょう。ただ、問題はGameplayAttributeはC++からしか追加できない機能の為、、、またまた、C++言語の登場となりますが・・・
1.敵キャラクター用のベースクラスの作成
まずはGameCharacterクラスをベースに”EnemyCharacter”を新規に作成します。
これは、無駄な気もしますが、設計の話しとして聞いてください。
GameCharacterクラスはこのゲーム中のキャラクターのベースとなるクラスです。ゲーム中のキャラクターとはプレイヤーかもしれないし、敵キャラクターかもしれないし、モブキャラクターかもしれません。今回、AttributeSetという機能を使ってスタミナ値を持たせるのですが、スタミナ値は全てのキャラクターが使うワケではなく、この時点では敵キャラクターだけが使うパラメータになるので、「全てのキャラクターのベース」にスタミナ値を持たせたくなかったのです。ということで、”GameCharacter”を継承した”EnemyCharacter”を作成し、そこにスタミナ値を持たせる事にしました。
新規C++クラスの作成を選択し、GameCharacterクラスを親クラスとして選択します。
で、名前を”EnemyCharacter”とします。
2.AttributeSetの作成
スタミナを持たせる為のAttributeSetというものを作成していきます。
AttributeSetとはGameplayAttriubteをまとめて格納しておくクラスで、GamplayAttributeをAttributeSetに入れて、それをActor(今回の例で言えばBP_AIEnemy)が保有する事になります。
新規C++クラスの作成を選択し、AttributeSetクラスを親クラスとして選択します。
で、名前を”EnemyAttributeSet”とします。
3.AttributeSet実装
“EnemyAttributeSet”を実装していきます。
EnemyAttributeSet.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "GameplayEffect.h"
#include "GameplayEffectExtension.h"
#include "EnemyAttributeSet.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 GASSAMPLE_API UEnemyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
// コンストラクタの定義
UEnemyAttributeSet();
// GameplayEffect動作後の処理
void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)override;
// スタミナ値の保持
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FGameplayAttributeData Stamina;
ATTRIBUTE_ACCESSORS(UEnemyAttributeSet, Stamina)
};
先ほども説明した通りですが、AttributeSetはGameplayAttributeを格納しておくクラスなので、FGameplayAttributeData型で”Stamina”が定義されています。
そしてそれ以外には、謎のマクロ(ATTRIBUTE_ACCESSORS)、コンストラクタの定義、PostGameplayEffectExecute関数のオーバーライド定義があります。
マクロについてはゲームエンジンのソース内(\Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\AttributeSet.h)に「このマクロを使えば基本的にはOKだよ」という説明がある為、そのまま流用していますが、GameplayAttributeのSetter/Getterとなる処理を定義しているものと考えてください。
コンストラクタはGameplayAttributeを初期化する場所として利用します。
オーバーライドしているPostGameplayEffectExecute関数については、GameplayAttributeをGameplayEffectが更新した後に呼び出される処理で、今回の例ではスタミナが上限・下限を超えないように制限したり、Actor(今回の例ではBP_AIEnemy)に対してGameplayAttributeが更新された事を知らせる役目を担っています。
ちょっと複雑になってきましたが、実装側のプログラムも見てみましょう。
EnemyAttributeSet.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "EnemyAttributeSet.h"
#include "EnemyCharacter.h"
UEnemyAttributeSet::UEnemyAttributeSet() :
SetStamina(100.f)
{
}
void UEnemyAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// スタミナ値の更新が走った場合はスタミナ値をMaxとMinの間に収まるようにする
if (Data.EvaluatedData.Attribute == GetStaminaAttribute()){
SetStamina(FMath::Clamp(GetStamina(), 0.f, 100.f));
}
AEnemyCharacter* EnemyCharacter = Cast<AEnemyCharacter>(GetOwningActor());
if (EnemyCharacter){
EnemyCharacter->PostGameplayEffectExecute();
}
}
実装側は拍子抜けするほど短いですね・・・
でも、ざっくりソースを眺めてみると、とてつもない違和感を感じます。。。。なんか知らない関数がいるっ!!
そう、SetStaminaとかGetStaminaAttributeって関数が登場しますが、こんなもん定義した覚えはありません。”Stamina”というGameplayAttributeだけしか定義していないのに、何なの?って感じですが、これがマクロの威力です。ヘッダーファイル内でマクロを通したことで、StaminaというGameplayAttributeに対して勝手にGetStaminaAttribute/GetStamina/SetStamina/InitStaminaという関数(正確には単なるマクロ)が生成されているのです。ここは難しく考えないで、”Stamina”のSetter/Getterが出来たんだな程度に考えておいてください。
まずは、コンストラクタですが、マクロを使ってスタミナ値に100を入れている事が分かると思います。(GameplayAttributeで保持できる値はfloat型の為、”100.f”となっている)
次にPostGameplayEffectExecuteを見ていきましょう。PostGameplayEffectExecuteはStaminaに限らず、GameplayEffectによってGameplayAttributeが変更された処理の後に呼び出させるので、この中では「Staminaが変更されたの?それなら、Stamina値は0~100に範囲に収まるようにしといてね」というロジックが書かれているのです。
そして、その後が重要です(スクリーンショットに★を付けちゃう位に)。GameplayAttributeの値に変化があった事を保有主のActorに知らせる為の処理が書かれています。このAttributeSetはEnemyCharacterに組み込まれる事を前提としているので、GetOwningActor関数で取得したActorをEnemyCharacterにキャストして、PostGameplayEffectExecute関数を呼び出しています。(EnemyCharacterにPostGameplayEffectExecuteを定義するのは後ほど・・・)
これで、EnemyCharacterはスタミナ値(に限らず、他のGameplayAbilityでも)に変化があったことを知る事ができます。
4.AttributeSetを保持するようにEnemyCharacterクラスの修正
“EnemyCharacter”は、先ほど作成したAttributeSetを保有する事と、GameplayAttributeに変化があった事を受けとる関数PostGameplayEffectExecuteを定義することが目的となります。
EnemyCharacter.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameCharacter.h"
#include "EnemyAttributeSet.h"
#include "EnemyCharacter.generated.h"
/**
*
*/
UCLASS()
class GASSAMPLE_API AEnemyCharacter : public AGameCharacter
{
GENERATED_BODY()
public:
// コンストラクタ
AEnemyCharacter();
// プレイヤー用AttributeSet
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UEnemyAttributeSet* EnemyAttributeSet;
// AttributeSetに変更があった場合の呼び出される
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, meta = (DefaultToSelf = "Target"))
void PostGameplayEffectExecute();
};
説明通りになりますが、コンストラクタ、AttributeSetの宣言、PostGameplayEffectExecute関数の宣言という最低限の構成になっています。
それでは実装を見てみましょう。
EnemyCharacter.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "EnemyCharacter.h"
// コンストラクタ
AEnemyCharacter::AEnemyCharacter(){
EnemyAttributeSet = CreateDefaultSubobject<UEnemyAttributeSet>(TEXT("AttributeSet"));
}
こちらはコンストラクタでAttributeSetのオブジェクトを宣言しているだけとなります。
あれ?PostGameplayEffectExecuteの実装は?となるかもしれませんが、ヘッダーファイル側で”BlueprintImplementableEvent”として定義している為、C++側に処理の実体を書く必要はなく、BP側で実装すれば良い事になっています。
5.BP_AIEnemyの親クラス変更
上記で作成した”EnemyCharacter”を”BP_AIEnemy”の親クラスとすることで、敵キャラクターにスタミナ値を保有させます。
ということで、”BP_AIEnemy”の親クラスを”GameCharacter”から”EnemyCharacter”に変更します。
6.スタミナゲージ用のウィジェットブループリント(UMG)の作成
“BP_AIEnemy”の頭上にスタミナゲージを表示させたいので、WidgetBlueprint(UMG)を作成します。名前は”WBP_AIEnemyStamina”としました。
適当にプログレスバーを配置してください。
そして、UMG上の値を変更する為のカスタムイベントを作成します。
これは、スタミナ値を引数で受け取ったら、その数値に合わせてプログレスバーで表示するだけの機能となります。
7.BP_AIEnemyにウィジェットブループリントを追加
“BP_AIEnemy”にWidgetコンポーネントを追加して、頭上に配置し、”Space”を”Screen”に変更し、”WidgetClass”を先ほど作成した”WBP_AIEnemyStamina”にしてください。
8.スタミナバーの表示確認(とりあえず、バーが表示されている事の確認)
プレイしてみると、敵キャラクターの頭上にバーが表示されるはずです。
9.敵キャラクターの現在のスタミナ値をウィジェットブループリントに反映する処理の追加
“BP_AIEnemy”のスタミナ値を頭上のプログレスバーに反映する為のカスタムイベントを作成します。
このイベントを呼び出せば、AbilitySystemComponentからスタミナ値を取得し、UMGのスタミナ表示を更新するという処理にしておきます。
10.敵キャラクターの初期スタミナをバーに表示
“BP_AIEnemy”のBeginPlayでスタミナ値の表示の更新を行います。これで、プレイ開始と同時に敵キャラクターのスタミナ値が頭上に表示されます。
11.スタミナの初期値がバーに反映されていることを確認
さっきまで表示されていなかったスタミナ値がMAXで表示されるようになりました。
12.パンチ能力発動によるスタミナ消費処理の作成
次に、GameplayEffectを作成し、パンチ1発につきスタミナ50を消費するようにしましょう。
新規ブループリントから”GameplayEffect”を親クラスとして、”GE_AIEnemyPunch”とします。
“GE_AIEnemyPunch”を開くと詳細パネルが充実していますので、以下のように編集します。
GameplayEffectは基本的に詳細パネルの操作でGameplayAttributeの値の制御を行います。今回は「スタミナ値」から「50」を引くという処理を定義している事になります。(まだ、パンチ能力とは紐づいていません)
13.パンチ能力にスタミナ消費処理を割り当てる
次に、”GA_Panch”を開き、「Cost Gameplay Effect Class」に上記で作成した”GE_AIEnemyPunch”を指定します。
これで、「パンチ能力を発動」したら「スタミナ値」から「50」引くという処理が完成します。でも、これだけではありません。スタミナ値から50引けない場合はパンチ能力を発動できなくなるのです!(控えめに言って凄い!)
14.スタミナ値が変更したら、ウィジェットブループリントに反映する処理の追加
“BP_AIEnemy”を開き、関数のオーバーライドからPostGameplayEffectExecuteを選択します。
すると、イベントグラフ上に”イベント Post Gameplay Effect Execute”というノードが追加されます。
このノードからUpdateStaminaノードを呼び出します。
これは、EnemyCharacterを作成した時に説明した通りですが、GameplayAttributeの値がGameplayEffectによって変化した場合に呼び出される処理の実体になります。つまり、今回のサンプルではスタミナ値が変化する度に、ここが呼び出されて、頭上のゲージには常に最新のスタミナ値が反映されることになります。(これがイベントドリブンってヤツですね)
15.動作検証
これで実行してみると、、、、パンチを一発撃つとゲージが半分になりました。
そのまま見ていると2発目でゲージが空になり、それ以降はパンチを打てなくなりました。これで、パンチによるスタミナの消費ロジックの完成です。
スタミナ自動回復
GameplayEffectを使えば、スタミナの自動回復も簡単に実装できます。
復習がてら実装してみましょう。
1.スタミナ自動回復処理の作成
GameplayEffectを親クラスとして、”GE_AIEnemyRecoveryStamina”を作成します。
“GE_AIEnemyRecoveryStamina”を実装していきます。
今回は、”GE_Punch”の時とは違い、常にActorに影響を及ぼします。(“GE_Panch”は”GA_Panch”のコストとして定義されていましたが、”GE_AIEnemyRecoveryStamina”は何らかのGameplayAbilityとは関係なく、直接Actorと関連付ける事になります)
その為、「Duration Policy」に”Infinity”を指定することで、永遠にGameplayEffectの効果を発生し続けます。そして、このGameplayEffectの機能はスタミナの回復なので、「Period」に指定した0.1秒毎に、「Modifiers」に定義した1ずつスタミナを回復させていく機能となっています。
2.スタミナ自動回復を敵キャラクターに登録
先ほども説明した通り、”GE_AIEnemyRecoveryStamina”は何らかのGameplayAbilityに紐づいているのではなく、Actorに直接紐づいている為、”BP_AIEnemy”のBeginPlayで敵キャラクターにパンチ能力を与えた処理の後に、スタミナ自動回復エフェクトも能力と同じようにActor(“BP_AIEnemy”)に与えます。
AddEffectというノードですが、”GameCharacter”で定義していましたよね。ここでやっと使われる時が来ました。
3.動作検証
パンチを放つとスタミナが減り、その後自動的にスタミナが回復していることが分かると思います。
GameplayEffect凄い!!
クールタイム導入
最後になりますが、パンチを出してから一定時間は自動回復したくないという処理を入れていきましょう。
今まで作成してきたGameplayEffectの応用編となります。
1.クールタイム処理の作成
GameplayEffectを親クラスとして、”GE_AIEnemyCooltime”を作成します。
さて、クールタイムはどのように実装したら良いのでしょうか?
クールタイム自体はスタミナを減らすワケでも、増やすワケでもありません。
あえて機能を説明するのであれば「BP_AIEnemyに紐づいている自動回復(“GE_AIEnemyRecoveryStamina”)の邪魔」をするのが、”GE_AIEnemyCooltime”の機能なのです。
この概念を頭に置いて実装していきましょう。
「DurationPolicy」は「HasDuration」を指定することで、発動してから指定された秒数だけ有効なGameplayEffectを作ることができます。ここでは2秒を指定しました。これで、なんとなく、「2秒間は自動回復の邪魔」をしたいんだなーって感じがしてきましたが、邪魔をするという処理はどのように書けば良いのでしょうか?
詳細パネルの下側を見ていくと「タグ」「GrantedTags」「Added」の中に”ability.cooltime”が設定されています。これは、このGameplayEffectが有効な時間だけ、ここで指定されたタグを保持します。つまり、”ability.cooltime”タグが保持されている間は自動回復させなければ良いのです。(ちなみに、「GameplayTagを保持」と書きましたが、保持しているのは、Actorが所有するAbilitySystemComponentです。つまり、”GameCharacter”で追加したAbilitySystemComponentです。)
参考までに、GameplayEffectを制御する為のGameplayTagについてまとめておきました。
(GameplayEffectのタグ設定は、AddedとRemovedと、RequireとIgnoreが設定できます。Addedは追加タグ、Removedは削除タグ、Requireは必須タグ、Ignoreは無視タグという意味になります)
Gameplay Effect Asset Tag | GameplayEffectが所有するラベル |
Granted Tags | このGameplayEffectを適用しているActorのAbilitySystemComponentに設定するタグ |
Ongoing Tag Requirements | GameplayEffectのスイッチ状態 GameplayEffectが正常に適用された後、有効にするには、このタグを満たす必要がある |
Application Tag Requirements | GameplayEffectを適用できるか否か アプリケーションタグが満たされない場合、GameplayEffectは失敗する |
Remove Gameplay Effects with Tags | 削除タグが満たされると、GameplayEffectはActorのAbilitySystemComponentから直接削除される このタグは、アプリケーションタグ要件の役割を果たすことも可能 |
2.パンチ能力の発動と同時にクールタイム処理を起動
“GA_Panch”でパンチの発動と同時に、”GE_AIEnemyCooltime”を有効にします。
この処理を通過してから、2秒間だけの命のGameplayEffectが実行開始される瞬間です。
3.スタミナ自動回復処理にクールタイムは動かないように設定
自動回復のGameplayEffectである”GE_AIEnemyRecoveryStamina”に、(AbilitySystemComponentが)”ability.cooltime”タグを保持している時は動かないように設定しましょう。
「タグ」の「Ongoing Tag Requirements」「Ignore Tags」に”ability.cooltime”を設定するだけです。
4.動作検証
これで、パンチ発動後の一定時間はスタミナが回復しなくなりました。素晴らしい!!!
AIの敵キャラクターに殴られないように逃げようゲームの完成!
レベル上に沢山のBP_AIEnemyを配置するだけで、逃げ回るゲームの完成となります。パンチの当たり判定は作成していないので、ゲームオーバーになったりはしませんが、、、、、
さて、これで今回の記事は終わりとなります。長々とお付き合い頂き、ありがとうございました。
終盤になるにつれて、実装したい機能に対して、実装量が減ってきてる事に気付いて頂けましたでしょうか?C++を使ったり、GASを簡単に使えるような設計・構築をしたりと、解説の最初の方はややこしい手順が多かったと思いますが、パンチ能力を発動してからは、スタミナの消費・クールタイム処理などは、ほとんどBPも触らないで実装が可能となっています。
これは、GASがGameplayTagを使って自動的に制御を行ってくれているからです。終盤になるにつれてGASが本領発揮していますので、その効果を感じ取って頂けるとありがたいです。
設計については、人それぞれの考え方や、ゲームの特性によって異なってきますので、今回の例が最適ではありません。しっかりと機能を理解して、最適な設計ができるように、GASを自分のものにして頂ければと思います。(自分への戒め)
No responses yet