見出し画像

Godot モデル動的構築 自分の事例5

前回の続き。

アニメーションを制御するPuppetの説明です。
下図の上側の箱~!

PuppetはMiniature系の最親クラス

この章は、モデルの動的構築というよりは、アニメーション制御のラッピングの話という感じです。

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秒かけてクロスフェードします。
でもね、これ毎フレーム呼んだりすると、こうなるんですよ。

毎フレームplayを呼び出すと

上の画像のようになってる気がします。これは横軸が時間で、"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(動作意図)引きでアニメーションを指定できます。
どんな無茶ぶりをしても、エラーを起こさず、かつ、できるだけいい感じのアニメで代替してくれる。そんな機能です。

つづく


この記事が気に入ったらサポートをしてみませんか?