UnrealEngine4で2Dゲームを作ろう! その15 BehaviorTreeのDecorator/Task作ってみた
やっとザコのBTが出来た…… pic.twitter.com/Yuwt7GWw8x
— katze (@katze_7514) December 26, 2015
という感じで、BehaviorTree(以下BT)で敵AIを作ってみた時にわかったことを書きます。
BT全体
BT全体は上図の通りで、移動して攻撃したらちょっと待つを繰り返します。
Decorator/IsEnableAI
BTは、外からPauseしたりStopしたりする機能がないので、代わりに根元で実行を止めるDecoratorを付けました。
中身は、対応するControllerの状態を調べてBTを実行する状態かをチェックしています。
Decorator実装
Decoratorの実装は、「PerformConditionCheckAI」をオーバーライドし、boolをリターンします。
AIがついていない「PerformConditionCheck」もあり、どっちも使えるのですが、AI付きの方はBTを実行しているController/Pawnを引数として取れるのでAI付きの方が使いやすいと思います。
Task/SimpleMoveTo,Attack
SimpleMoveTo
NavMeshがリビルドエラーを吐き使えなかったので、MoveToを自前実装したのがSimpleMoveToです。中身は単純にPlayerPawnに近づいて一定距離になったら止まるというだけです。
このリビルドエラーは、AnswerHubにも投稿されていますが完全な解決には至ってないようです。
移動中は処理を継続し、PlayerPawnの一定距離内に来たらFinishExecuteを読んでTaskを終了します。
また、処理を途中で打ち切るための仕込みもいれてあります。BlackBoardにAbortフラグが立っているかをチェックして、立っていたらFinishAbortします。この辺りについては後述します。
Attack
攻撃アニメを再生して再生終わりを待つTaskです。アニメ再生終わりは、Controllerの方で検出してBlackBoardにフラグを書き込んでいます。Taskの中で直接再生終了をイベントをbindしても良いと思います。
ダメージのやりとりはコリジョンイベントの方で行っています。
Task実装
Taskは2つのイベントを実装します。
ExecuteはActorのBeginPlayに相当するのもので、Taskが実行開始時に呼ばれます。BeginPlayと違うのは、Taskが実行開始する度に呼ばれることです。
つまり、今回の例ですと、SimpleMoveTo→Attack→Wait と実行され、最初に戻り再びSimpleMoveToが実行される時にもまたExecuteが実行されます。
Tickは、Task内でFinish系ノードが呼ばれるまで毎フレーム呼ばれます。
Taskの実行中断
※ DecoratorのObserveAbortsという項目を使うことで途中中断ができたみたいです。Decoratorの値が変化した時に接続されているノードを中断するかを選択することができます。
今回、BT(というかTask)の実行を中断する必要があったのは、Playerの攻撃でノックバックしたり、HPが0になったら死亡エフェクトを表示したりする必要があるからでした。
先にも書きました通り、BT自体に動作を止める手段が用意されてないため、実行中のTask以外の動作をさせるには一端Taskを止める必要があります。
BTのデバッグ表示を見ると以下のようになっていたので、
SequenceのIsEnableAIも毎フレーム実行されるのかと勝手に思っていたのですが、そんなことはなく、上図の場合はSimpleMoveToのTickだけが呼ばれている状態になります。
Decoratorは実行"開始"条件判定を行うだけで、一度判定が通り先に進むとDecoratorは実行されなくなります。実行"継続"条件としては使用できません。
よって、Taskの実行を中断させるには、根元にIsEnableAIを仕込むだけは足りないということになり、Task内に中断する仕組みを導入する必要があります。
TaskのTick内で中断フラグをチェックする機構をいれておきます。
IsAbordはBlackBoardに用意したAbordフラグを見にいっています。BlackBoardはBT内からでも外からでも操作できますので、どこからでもTaskの実行を中断できます。
UE4の入力取得の仕組みとプラグインの作り方
「Unreal Engine 4 (UE4) 其の弐 Advent Calendar 2015」 3日目の記事になります。
UE4エディタ・BPなどが一切でません。C++のみのプログラマ向けの記事になります。
ここで示すソースコードは「4.10.0-release」タグのものになります。
UE4の入力取得
確認しておきますと、ここでいう入力とは、ゲームパッド・キーボード・マウスといった入力デバイスのことを指しています。
UE4でこの入力状態をチェックしイベントを発生させているのは2ヶ所あります。
まず、ゲームパッド及び独自の入力デバイスの処理が以下の場所。
LaunchEngineLoop.cpp 2414~2415行目
2414: FSlateApplication& SlateApp = FSlateApplication::Get(); 2415: SlateApp.PollGameDeviceState();
2415行目のPollGameDeviceStateメソッドが、ゲームパッドと入力プラグインなどで指しこんだ独自入力デバイスのチェックを行いイベントを発生します。全ActorのTick前に一括して処理しています。
FSlateApplicationクラスはプラットフォームを抽象化したクラスです。プラットフォームごとのApplicationクラスを内部でもっており、プラットフォームAPI処理を行っています。*1
2つ目は、キーボードやマウスなどウインドウメッセージによる入力処理です。メッセージ系はチェックとイベント発生の2段階にわかれて処理されています。
1段目、メッセージの確認が以下の場所。
LaunchEngineLoop.cpp 2391行目
2391: FPlatformMisc::PumpMessages(true);
メッセージキューにたまっているメッセージを確認して、一端、メッセージバッファに情報を格納しています。
その後、全ActorのTickが終わった後に、メッセージイベントを発生します。もちろん、すぐに処理すべきウインドウメッセージは、メッセージチェックの段階でも処理されていますが、入力系は遅延処理されます。
その遅延処理しているのが、以下の場所。
LaunchEngineLoop.cpp 2460行目
2460: FSlateApplication::Get().Tick();
FSlateApplicationのTickメソッドの中で、ProcessDeferredEventsメソッドが呼ばれてメッセージによるイベントを発生させます。
ざっくりですが、UE4の入力処理は上のようになっています。
ちなみにLaunchEngineLoopは、いわゆるメインループの実装です。メインスレッドが何をやってるのかを見たい時は、LaunchEngineLoopをのぞくと良いと思います。
独自の入力デバイスプラグインを作る
以上を踏まえて、独自の入力デバイスを使うためのプラグインの作り方を紹介します。独自といってもオリジナルハードという意味ではなく、UE4が公式対応していない程度の意味です。Oculusタッチなどが該当します。*2
プラグイン全般の作り方は公式ドキュメントなどを参考にしていただくとして、ここでは入力プラグインに特化した内容を紹介します。
*1:プラットフォームごとのApplictionクラスの名前は、F[プラットフォーム名]Appicationという名前になっています。Windowsだと、FWindowsApplicationクラスになります。
UnrealEngine4用プラグイン DirectInputPadPluginの使い方
UE4(Windows)でDirectInputのゲームパッドを使えるようにするプラグインを作りました。
DirectInputPadPlugin Ver.0.9のものです。
これ以降、DirectInputのゲームパッドのことをDIパッド(DIPad)、XInputのゲームパッドのことをXIパッド(XIPad)、両方合わせた場合はたんにゲームパッド(GamePad)と表記します。
インストール
- 上のGithubからソースをチェックアウト、もしくはZIPをDOWNLOADします。
- それらのソースを「DirectInputPadPlugin」という名前のフォルダを作って、そこに入れます
- 「DirectInputPadPlugin」フォルダを、[プロジェクトフォルダ]/Plugins に移動します
あとは、
を参考にソースをビルドしてください。
DirectInputのゲームパッドの接続確認
ゲームパッドをPCに挿した状態でUE4エディタを立ち上げます。動的な抜き差しには対応していませんので、エディタやゲーム起動前には挿しておいてください。
接続が成功していれば、アウトプットログに
DirectInputPadPlugin: DirectInputDriver initialized. DirectInputPadPlugin: DirectInputPad detected: 1 DirectInputPadPlugin: DirectInput Joystick Create Success. : Elecom Wired Gamepad
見つかったDIパッドの数と、その数だけ接続が確認できたDIPadの名前が表示されます。
これでDIパッドを使用する準備が完了です。
キーイベント
GamePadイベントをそのまま使うことができます。
など。
基本的には、GamePadイベントを使うことを想定しています。なぜなら、GamePadイベントはXIパッドのイベントでもありますので、ユーザーがXIパッドでもDIパッドでもどちらのゲームパッドを使っていても良くなるからです。
パッドキャリブレーション
GamePadイベントをDIパッドでも使うには、DIパッドをXIパッドの配置にする必要があります。XIパッドというのは、ようはXBOXのパッドのことです。
デフォルトでは以下のような対応になっています。
XIパッド | DIパッド |
---|---|
左スティックX | X軸 |
左スティックY | Y軸 |
右スティックX | Z軸 |
右スティックY | Z回転軸 |
方向パッド上 | POV上 |
方向パッド右 | POV右 |
方向パッド下 | POV下 |
方向パッド左 | POV左 |
A | ボタン1 |
B | ボタン2 |
X | ボタン3 |
Y | ボタン4 |
LB(L1) | ボタン5 |
RB(R1) | ボタン6 |
Lトリガー(L2) | ボタン7 |
Rトリガー(R2) | ボタン8 |
BACK | ボタン9 |
START | ボタン10 |
左スティックボタン | ボタン11 |
右スティックボタン | ボタン12 |
変更に使うノードは、SetKeyMapです。
Ver.0.9からの機能として、軸にボタン、ボタンに軸を割り当てられるなど、キャリブレーションの自由度を上げました。
図の左から
- 「XIパッドの左スティックX軸として、DIパッドのX軸を割り当てる」
- 「XIパッドの右スティックY軸(下方向)として、DIパッドのボタン1を割り当てる」
- ボタン1が押されていないなら0,ボタン1が押されていると-1 が返るようになります
- 上方向に割り当てる時は、Negativeフラグをオフにします
- 「XIパッドのボタンAとして、DIパッドのX回転(正方向)を割り当てる」
- X回転の正方向の値に応じて、ボタンAが押されているという判定がされます
- X回転の負方向の値を使うならば、Negativeをオンにします
Ver.0.8では対応できていなかったLT(L2)/RT(R2)への軸の割り当てもできるようになっています。
SetKeyMapは重複してキーを割り当てられるのでご注意ください。KeyMapを削除するには、DIKeyの項目を「DIGamePad END」にすることでできます。
GetDirectInputPadJoystickノード
PlayerIndexを使ってDIパッドを取得するノードです。DIパッドは見つかった順番にPlayerIndexが割り当てられて、対応するプレイヤーの入力として使われます。
XIパッドが同時に挿しこまれていた場合は、XIパッドを優先します。DIパッドにはXIパッドの次のPlayerIndexが割り当てられます。つまり、XIパッドが挿しこまれてないないなら0番から、XIパッドが1つ挿しこまれていたら1番から割り当てられます。
PlayerIndexに対応するDIパッドがなかった場合は、nullが返ります。
(回転)軸の反転
DIパッドによっては、XIパッドと軸の値が逆になっていることがあります。XIパッドの左スティックYは上がプラスで下がマイナスになりますが、私が普段使っているDIパッドのY軸は上がマイナスで下がプラスになっています。
このまま軸を割り当てると上下が反転した状態で処理されてしまいますので、軸の値を反転させる必要があります。その時は、SetAxisReverseノードを使います。
引数上ではボタンも選べますが、当然意味はありません。
反転フラグの値を調べるには、IsAxisReverseノードを使います。
ChangedState系ノード
パッドキャリブレーションをサポートするノード群です。
呼び出したフレームで状態に変化のあったキー情報を取得するノードです。
状態の変化とは、軸なら初期値(通常は0)以外の入力値になっている、ボタンなら押されたか離されされたか、ということになります。
IsChangedKeyStateノード
状態が変化したキーがあるかをチェックします。変化したキーがあればtrueが返ります。
Get(All)ChangedKeyStateノード
GetChangedKeyStateノードは変化のあったキー1つだけ、GetAllChangedKeyStateは変化のあったキーすべて取得します。
変化のあったDIパッドとしてのenumと変化値が取得できます。変化値は、軸の時は軸の値、ボタンの時は正だとPressed、負だとReleasedの意味になります。
Real引数をtrueにすると、反転フラグなどキー設定を無視したパッドの実入力値で変化値を取得できます。
GetChangedKeyStateは、通常は軸の変化を優先しますが、Btn引数をtrueにすることでボタンの変化を優先的に取得するようになります。
ユーザーに「Aボタンの場所を押してください」などと促し、押されたボタンを検出(GetChangeKeyState)、そのボタンをAボタンとして設定する(SetKeyMap)、という使い方を想定しています。
ユーティリティー
ゲームパッドの名前と識別子
ゲームパッドの名前はGetProductNameノード、識別子はGetGUIDノードで取得できます。
名前はまったく同じパッドの場合、同じ名前になることがあるので識別には使えませんが、ユーザーがコントロールしているパッドを示すのには使えます。
キーコンフィグ情報を保存/復元する際など、個々のパッドを識別するためにはGUIDを使用してください。ただし、このGUIDはローカルでのみ一意性が保証されてるGUIDですのでご注意ください。
入力値のクリア
現在保持している入力値をクリアします。
プラグインが初期化できたか
コンフィグ
BACKGROUND設定にするか
DefaultInput.ini に以下のセクションを追加してください
[DirectInputPadPlugin] Background=true
エディタ起動時は、常にBACKGROUND動作となります。
以上になります。
UnrealEngine4で2Dゲームを作ろう! その14 DirectInputプラグイン作った編
前回紹介したJoystickPluginは問題が発覚したために、結局自作しました。
ひとまず、
を参考にソースからビルドしてUE4に追加してください。
通常のゲームパッドイベントとして受け取れるようにしましたので、すぐに使えると思います。
とはいえ、ボタン配置がパッドごとに違うDirectInputのゲームパッドを扱うにはもう少し手を入れる必要があるので開発は続けます。
詳しい使い方は、近いうちに記事にします。
JoystickPluginの問題
あれからソースをさらに読み込んだ所、JoystickPluginは入力の取得をActorのTickタイミングで行っていることがわかりました。
ちょっとこれは問題で、ゲームジャンルによっては致命的な問題となりかねません。
ゲームパッドなどのデバイス入力は、全Actorが動く前に取得するのが望ましいです。
UE4もその辺りの事情は当然わかっているので、独自入力デバイスをさしこんで、しかるべきタイミングで入力取得メソッドを呼んでくれる仕組み*1があります。それに沿った形で実装したプラグインとなっています。
その辺りのソースを読んでいたら、キーイベント放出あたりの事情も理解できたので、通常のゲームパッドイベントと統合できたという感じです。
UnrealEngine4で2Dゲームを作ろう! その13 DirectInputを使おう編
UE4でゲームパッドを使おう!(Windows)というお話です。
※追記※ JoystickPluginを本採用するのはオススメできなくなりましたので、DirectInputのプラグインを作りました。こちらを見てみてください。
UE4のゲームパッド対応
UE4(4.10現在)が対応してるパッドは、XInputパッドのみです。
XInputというのはWindowsでXboxのパッドを使えるようにするものでAPIが整理されてて使いやすくはあるんですが、Xboxのパッドしか使えないという欠点を抱えています。もちろん、XInput対応をうたっているゲームパッドは使えますが、世の中に出回っているほとんどのPC用ゲームパッドはXInputに対応されていません。*1
多くのゲームパッドに対応するには、DirectInputへの対応が必須なのですが、UE4はサポートしていません。
フォーラムでもDirectInput対応して欲しいという要望は上がっていますが反応がよくないので、公式サポートは期待できません。
UE4でDirectInputゲームパッドを使う!
やり方は、2つあります。
Xbox 360 Controller Emulatorを使う
DirectInput入力をXInput入力に変換してアプリに渡してくれるフリーソフトです。通称360ce。使い方は、適当にググってください。
ざっとですが使ってみた所、UE4でも動きます。UE4エディタで動かすには出てきたDLLをエディタのexeファイルがあるフォルダにコピーしてください。ゲーム本体で使うなら、パッケージ化してゲームexeと同じフォルダにDLLをいれてください。
DLLを同梱して、適当にコンフィグファイルをでっち上げれば動かせるかなと思ったのですが、動作を調べた所、パッド名などを使って変換すべき入力を判定してるようです。つまり、制作側でコンフィグファイル用意しても意味がないという……。
そうなると、360ceの設定をユーザーに強いることになるので、どうなんだろうなぁ、と思ったので自分は使わないことにしました。元々使ってるユーザー向けに360ceでも大丈夫だよ!みたいなことをマニュアルに書く分にはありかなとは思います。
JoystickPluginを使う!
というわけで、UE4でDirectInputパッドを使えるようにするプラグインを使用します。ここでは、Ikarus76氏が作ったオリジナル版を対象に話を進めます。DirectInputを使いたいという要望は多いので、このプラグインをベースに派生がいくつも作られていますので気になる方はそちらも見てみると良いかと思います。
DLできるプラグインは、古いバージョンのUE4でビルドされたものですので、4.10で使うにはビルドしなおす必要があります。
ビルド方法は、前記事を参考にして下さい。
JoystickPluginを4.10でビルドを通す
そのままではコンパイルエラーでビルドが通りませんので、以下の場所を修正します。
JoystickSingleController.h 15行目/18行目
- 変更前
15: int64 ButtonsPressedLow; 18: int64 ButtonsPressedHigh;
int64をint32に変更
- 変更後
15: int32 ButtonsPressedLow; 18: int32 ButtonsPressedHigh;
この変数は、DirectInputから取得したボタンの押下情報をbitごとに保存する変数です。
int64となっていたのは、DirectInputの仕様である128個のボタン押下情報を保存するつもりだったようですが、実際には32個ずつ64個の情報しか保存してませんのでint32で問題ありません。(WinJoystick.h 878行目~898行目の処理)
WinJoystick.h 467行目
- 変更前
467: INT_PTR WINAPI WinProcCallback(
INT_PTRをLRESULTに変更
- 変更後
467: LRESULT WINAPI WinProcCallback(
これはWinAPIの仕様です。
WinJoystick.h 643行目/646行目
- 変更前
643: if (strVid && swscanf_s(strVid, L"VID_%4X", &dwVid) != 1) 646: if (strPid && swscanf_s(strPid, L"PID_%4X", &dwPid) != 1)
swscanf_sでの受け取り型の不一致ですので、適当にキャストします。
- 変更後
643: if (strVid && swscanf_s(strVid, L"VID_%4X", reinterpret_cast<uint32*>(&dwVid)) != 1) 646: if (strPid && swscanf_s(strPid, L"PID_%4X", reinterpret_cast<uint32*>(&dwPid)) != 1)
DWORDはunsined longなので、uint32と(VCでは)互換があるはずなのですが、なぜかエラーになるのでキャストします。
以上のことをしてビルドを通して、JoystickPluginを有効にすると、DirectInputのゲームパッドから入力を取る準備ができたことになります。
JoystickPluginを動作させる
JoystickPluginを有効にしてエディタを起動します。アウトプットログに「JoystickPluginLog: Direct Input initialized.」と出れば使用準備が完了です。Failになってたり、ログに何もなかったらプラグインの起動に失敗していますので、ビルドをやり直して見て下さい。
入力データを取る方法は2つあります。どちらかを選択してください。両方同時には動きません。*2
JoystickPluginActorを使う
JoystickPluginが用意している基本のActorです。これをJoystickを使いたいレベルに配置することで、Joystickの入力をBPで取得することができるようになります。
こちらの方法で大体のことはできると思います。通常の入力イベントは使えますし、アクションマッピングも動作します。
ただ、JoystickInterfaceに実装されている一括イベントが、JoystickPluginActorを使う方法では取得できませんので、それが必要な時は次の方法を使います。
自前のJoystickActorを作る
JoystickPluginActorを継承したActorか、JoystickComponentとJoystickInterfaceを両方持たせたActorを作ります。
そうすることで、何かボタンが押された、軸入力があったなどのイベントを取ることできるようになります。キーコンフィグとか作る時は便利です。
データ接続はActorのTickで行われている
入力データの接続はActorのTickで行われているので、ポーズするとJoystickの入力が取得できなくなります。JoystickActorはポーズ中もTickを呼ぶ設定にしておくと良いと思います。
JoystickPluginから取得できる情報
ボタンはゲームパッドカテゴリ、軸系はなぜかキーボードカテゴリになっています。
通常の入力イベント
キーボード入力やマウス入力と同様に、Joystick入力イベントが使えるようになります。
一括イベント
上図のように、入力の変化を一括して取得することができます。同時に複数の変化があった場合(ボタンの同時押しなど)は、変化があった回数だけイベントが呼ばれます。
この動作イベントは、JoystickInterfaceを持ったActorのグラフでのみ使用することができます。
詳細情報(JoystickSingleController)
イベントに頼らず入力情報を確認する方法もあります。
上図のGetLatestFrameノードを使うことで、最新の入力データを見ることができます。コンパイルを通す時に紹介したJoystickSingleControllerがそのデータです。詳細は、右クリしてJoystickFrameで検索すると以下のような感じです。
よく使うプロパティの使い方を軽く説明しますと、
Buttons Pressed High/Low
ボタンが押されているかをビット単位で持っています。下位ビットから始まって、Lowがボタン1から32、Highがボタン33から64まで持っています。そのため、ボタンの押下情報は「bitwise and」を使って取得します。
GetAxis/RAxis
Axisが軸入力、RAxisが回転入力です。Vectorになっていまして、VectorのXがX軸の値という風に、XYZがそのまま対応しています。
DirectInputの場合、多くのパッドは、左スティック横X軸・縦Y軸、右スティック横Z軸・縦Z回転に割り当てられてるのをちょっとだけ覚えておくと良いかもしれません。
GetPOV
POV入力です。0~2まで3つあるのはDirectInputの仕様だからですが、通常はPOV0しか使いません。いわゆる十字キーに割り当てられてることが多いです。押されてる方向のEnum値が取得できます。
XInputとの共存
JoystickPluginは、XInputパッドとして見つかったとしてもDirectInputパッドとして動作するようになっていますので、パッドはすべてJoystickとして実装する場合は特に問題はありません。*3
XInputならXInputとして扱いたい場合は、UE4のゲームパッド入力をチェックしてイベントがあったらJoystick動作を止めるとかそういう処理を書くことになるかと思います。
JoystickPlugin使用上の注意
入力を取得してるJoystickは1つだけ
最初に発見したJoystickのみ入力を取りにいっています。複数のJoystickを使う必要があるときは、派生JoystickPluginを使いましょう。
複数のJoystickActorを切り替えながら使うのも非推奨
同時に動かせなくても切り替えられるだろう? と思いきや、JoystickPluginの設計上の問題で簡単にはいきません。
JoystickActorを切り替える時はTickのON/OFFだけではダメでBeginPlayも呼ぶ必要があります。いらなくなったActorをDestoryして、新しいActorをSpawnすれば良いということではあります。
ただ、どうやっても一括イベントを取得できるActorは必ず1つだけです。
*1:XInput対応パッドはちょっとお値段が上がるので、よほどわかってる人以外買って無いと思われます
*2:両方使ってしばらく悩みましたとさ
*3:はじめはXInputパッドはDirectInputパッドから削除する実装にしようとしてたみたいですがコメントアウトされてました