Godot モデル動的構築 自分の事例5
前回の続き。
アニメーションを制御するPuppetの説明です。
下図の上側の箱~!
この章は、モデルの動的構築というよりは、アニメーション制御のラッピングの話という感じです。
AnimationTree
Q. AnimationTree使わんの?
A. 使わなくてもGodot好きだよ。
高レベルな機能を使わなくてもストレスが無いのがGodotエンジンのいいところだと思ってます。
パート1で書いた通り、GUIで作成するGodot専用のデータは作りたくないんですよ。フットワーク的に。GUIじゃなくスクリプトで作ることができるかもしれない(前作はUnityのAnimatorControllerをエディタスクリプトで生成してました)ですけど、まあできればそれもしたくない。
AnimationPlayerの制御
前述の通り、親クラスであるDollがAnimationPlayerノードの制御を他者にゆだねているので、子クラスであるPuppetがAnimationPlayerを制御します。
AnimationPlayerの難しさ
AnimationPlayerには、再生スピードを設定したり、pause関数でポーズをかけたりする機能があります。でもねえ。ここら辺の機能が由来でねえ。AnimationPlayer自体の制御がねえ。ちょっとねえ。筆者には難しいんですよね・・・。
たとえばAnimationPlayerのis_playing関数は、アニメーションが最後まで到達したときにfalseを返します。そして、custom_speedが0のときもfalseを返すんですね。また、pause関数でポーズをかけた時もfalseを返します。
なので、is_playingがfalseのときに、アニメが終了した結果なのか、これ単体では判断できなくって、「文脈」を把握しておく必要があるんですね。これまでに自分のコードが何したか記憶しておかないといけない。
is_playing == false のとき
・アニメが最後まで到達した
・custom_speedがゼロ
・pause中
のどれか
あとね、AnimationPlayer .play関数は、1フレームに何回も呼んだり、毎フレーム無駄に呼んだりしないほうがいいんですよ。
同じアニメーション指定する限りは「見た目上は」無害ですが、引数のcustom_blendが絡んでくるとそうも言ってられません。custom_blendはドキュメントに詳細が無い引数ですが、ようするにクロスフェードするための機能です。1.0と指定すると、新しくplayしたアニメに1.0秒かけてクロスフェードします。
でもね、これ毎フレーム呼んだりすると、こうなるんですよ。
上の画像のようになってる気がします。これは横軸が時間で、"AnimA"再生中に毎フレーム、「"AnimB"へ4フレームかけてフェードせよ」とplayを呼んだ場合の概念です。
なんかね、こんなふうに、音楽や動画でいうとこの、「トラック」が増えるような感じになるんですよ。たぶんね。テストプログラム組んだら、そういう動作してる。
となると、先ほど言った、同じアニメなら毎フレーム呼んでも無害って話もちょっと怪しい感じがします。120フレーム尺のアニメ、もしかして120トラックで再生してない?って。
なので、playは必要な時だけ呼ぶようにしないといけないし、そのためには今何を再生していて、どんな状態なのか知らないといけない。再生が終了したかどうかはis_playingで知れるけど、それも扱いには注意が必要・・・。
ああ!
こうなるともはや、AnimationPlayerを普通に使うのをあきらめた方がシンプルになる気がします。使用する機能に制限をかけて、代替機能を自分(Puppet)側で実現することにしました。
高度な機能要らないし。
再生進行はこっちに任せて
PuppetではAnimationPlayerの_processでアニメを進行させません。
AnimationPlayer. playback_process_modeを、常にMANUALに設定し、進行をAnimationPlayer .advance関数の呼び出しに集約してます。これは進行管理を徹底するためです。
なので、Puppetの_process内でAnimationPlayerをクロックさせます。毎フレーム、モードや設定値(スピードなど)に応じて、適切にadvance関数を呼び出します。
また、AnimationPlayer .pause関数を禁止にします。使わない代わりに、同等の機能をPuppet側に持たせます。ポーズ中はadvance関数を呼ばない感じです。
同様に、Puppet側に再生スピード設定を持たせます。advance関数で進める時間に掛け算します。
# 概念はこんな感じ
if _is_pausing == false:
_anim_player.advance(delta * _speed_scale)
ところで、AnimationPlayerの本来のマニュアルモードのような使い方をしたいときもあると思います。キャラクターの使用者側や、キャラクターの親クラスが、アニメを進行させる機能です。
なので、Puppet自体にもマニュアルモードを用意しています。Puppetの_processで駆動しない代わりに、外部からadvance_anim関数を呼び出すと、内部でAnimationPlayerのadvanceが呼ばれます。(下図)
これは、いろんなクラスが互いに協調せずに勝手にPuppetのadvance_animを呼んでも、特に副作用が無い作りにしています。文脈に依存しない。
ループもこっちに任せて
また、AnimationPlayer. loop_modeは常にNONEに設定し、Godotによる自動のループも許容してません。(Animationリソース自体もインポート設定のデフォルト値=ループ無しです。)代わりにPuppet側にループ機能を持たせます。これは、ループしたかどうかを確実に検知するためです。
ループするアニメの再生時は、再生中のアニメーションが終了したと判断したら、再度0秒からplayしなおします。
たしかに、これだとループタイミングがフレームに依存してしまい、ぎこちないループになってしまいます。
でも、このぎこちなさが目立つのは低フレームレートの時で、それってゲーム自体がぎこちなくなってるんだから別にいいんじゃないの。という気もします。
あと、これのメリットとして、秒単位でめっちゃラグったときでも、フレーム間で2ループ以上しないことが保証される点です。これに由来するバグとかグリッチって、普段プレイするゲームで心当たりありますよね。
2つのレイヤー
Puppetは、ノーマルアニメーションとワンショットアニメーションの、2つのレイヤーで動作します。それぞれにアニメを指定して再生させる仕組みです。
ノーマルアニメは、ワンショットアニメ非再生時に再生されるループアニメです。いわゆる待機アニメに限らず、歩行、弓でねらいをつける、力をためる、といったループ系のアニメは全部これを使います。
同フレームや毎フレーム、同じループアニメを何回呼んでも副作用ありません。文脈に依存しない。
この機能は「今このループしといてね」という意図なので、呼ぶたびに再生位置がリセットされたりもしません。ランダムな位置から再生するオプションもあります。(これマジ要るよね)
ワンショットアニメは文字通り、1回再生して終わりです。終了後はノーマルアニメに移行します。ワンショットアニメの終了判定や、現在の再生時間は確実に取れるようになっています。
同フレームや毎フレーム、同じループアニメを何回呼んでも副作用ありません。文脈に依存しない。
この機能は「今からこのアニメを始めてね」という意図なので、同じアニメを再生中であっても、0.0秒からリスタートされます。もちろん、秒数を指定して途中から再生することもできます。(これも要るよね)
ループするアニメにイントロ・アウトロ付けるアイデアもあったんですけど、今のところ不要と判断しました。前作ではイントロ付けれたんですけど、このモジュールレベルでは導入しない方がいいかな、と。やるならもっと上位のクラス。
合図
キュー機能。コンテナのQueueではなく合図のCueです。
指定アニメーションが指定時間を経過したときに、事前に設定したフラグ(int値)を立てて、これを外部から取得できます。複数登録ができます。
用途としては、
・攻撃ヒットタイミング
・VFX発生
・足音、など
これは個体(インスタンス)ごとに必要になるというよりは、同じクリーチャーなら同一の条件だと思うので、定義リソースCueTableをセットして使います。
AnimMap
Puppetはオプション機能として、AnimMapを提供します。これは、ファンタジーRPGとかポケモンのような、たくさんの種類のクリーチャーが同じシステムで動作するゲームで使うことを想定してます。
これだけやたら思想が強い。
こんな感じでAnimMapのテンプレートをゲームごとに作成します。このテンプレート自体は1ゲームタイトル中に1つ、せいぜい3つ(戦闘キャラ、非戦闘キャラ、モブなど)しかない想定です。
これは見ての通り、ツリー状のグラフ定義Resourceで、各節がIntent(動作意図)を表してます。子節は親節の動作意図を、より具体化したものです。逆を言うと、親は子で代替可能な動作意図です。
そして、このテンプレートの各ノードに対して、AnimationLibrary上のアニメーション名を割り当てて、クリーチャー専用のAnimMap Resourceにします。これをクリーチャータイプごとに作成します。
このとき、無いアニメは空欄で構いません。ドラゴンは剣攻撃できないし、犬はホバリングできません。人間はブレス攻撃持ってないこともあるでしょう。
定義が無い場合は、親ノードがフォールバック先になるので。
さらに、同じアニメを重複して登録しても特に問題ないです。上記テンプレの例でいうと、"攻撃"と"スキル"の指示ではパンチしたいけど、抽象的に"アクト"と指示されたときはあえてガッツポーズしたい、とかそういうことあるでしょう。
こうやって作成した、クリーチャーごとのAnimMapをPuppetにセットしておけば、Intent(動作意図)引きでアニメーションを指定できます。
どんな無茶ぶりをしても、エラーを起こさず、かつ、できるだけいい感じのアニメで代替してくれる。そんな機能です。
つづく