Bevyにおけるアニメーション:全体像
Animation in Bevy: The Big Picture (glocq.com)
要約
この記事は、ゲームエンジンBevyで3Dモデルのアニメーションを実装する際の複雑さを解消し、開発者がアニメーションの仕組みを理解するためのメンタルモデル構築を支援します。BevyのECS(Entity Component System)の基本を理解している開発者を対象に、アニメーション再生に必要な要素(AnimationPlayer、AnimationGraph、NodeIndex)とその連携方法を解説し、公式サンプルコードの具体的な流れを紐解いていきます。
全文翻訳
2026年8月27日
無料アセットサイトでアニメーション付きの3Dキャラクター.glbファイルをダウンロードしたとしましょう。Bevyアプリケーションは既に基本動作しており、このキャラクターをBevyアプリにスポーンさせたいと考えています。簡単だろう、おそらくそのための関数があるはずです。Bevyのウェブサイトで基本的なAnimated Meshの例を調べると…期待していたほど単純ではないことがすぐに明らかになります。多くの設定が必要で、アニメーション関連の型があり、その正確な目的が不明瞭です…。例を調整すれば、比較的早くニーズに合うようにできるかもしれませんが、例に基づいてBevyでのアニメーションの仕組みを理解するためのメンタルモデルを形成するには、かなりの熟考が必要になるでしょう。
この記事は、数週間前にBevyでのアニメーションを理解しようとしていたときに、私が欲しかったものです。まず、Bevyでの基本的なアニメーションを扱うのに十分なメンタルモデルを、ステップバイステップで構築します。次に、そのメンタルモデルとどのように関連しているかを説明しながら、公式の例を順を追って解説し、まだ遭遇していない可能性のあるメソッドに遭遇した際には、少し手助けをします。
BevyのECSの基本を理解しているなら、この記事の対象読者です :)
最初のアプローチ
BevyとECSは一旦忘れ、アニメーション付き3Dモデルを動かすために何が必要か(非常に抽象的に)考えてみましょう。2つの要素が必要です。
- スポーンされた3Dモデル(またはそれへの参照)、
- 再生されるアニメーション(またはそれへの参照)。
したがって、最初にアニメーションを再生する方法として期待するのは、次のようなものかもしれません。
my_model.play_animation(my_animation);
ここまでのメンタルモデルは次のようになります。
[画像: 左側に「3D Model」と題された青い箱、右側に「animation data」と題された赤い箱、両方の箱を繋ぐ「play」と題された黄色い楕円形の図]
これは実際には、Bevyの実際の関数の動作方法、つまりAnimationPlayer型のplayメソッドにかなり近いものです…少なくとも、そのselfが実際には3Dモデルを参照し、animation引数が実際にはアニメーションを参照していれば、そうなるでしょう。しかし、現時点では、それらがそうであるかどうかは明らかではありません。しかし、Bevyでの2つの要素がどのように表現されているかを詳しく見てみましょう。そうすれば、私たちの直感とplay関数の動作方法を調和させることができるかもしれません。
AnimationPlayer: 3Dモデルのアニメーションを制御する方法
.glbファイルから3Dモデルをスポーンしたとしましょう。理想的には、スポーンされたエンティティのIDを使ってそれに参照したいのですが、3Dモデルは一般的に単一のエンティティとしてスポーンされるのではなく、エンティティの階層としてスポーンされます。
[画像: エンティティのツリーを表す箱の集合を囲む「3D Model」と題された青い箱の図。各箱はコンポーネントを表す数個の箱を含んでいる]
モデルをアニメートするには、別の方法で参照する必要があります。
Bevyには、そのためのメカニズムがあります。つまり、アニメーションのための3Dモデルを参照する方法として、AnimationPlayer型があります。AnimationPlayerはComponentであり、そのモデルがスポーンされたとき、アニメート可能な3Dモデルに対応するエンティティ階層のどこかに自動的に挿入されます。
[画像: エンティティの同じ階層を表す図。ただし、コンポーネントの1つが黄色で、アニメーションプレイヤーコンポーネントを表している]
AnimationPlayerを一度取得すれば、アニメーションの再生/一時停止を指示したり、現在再生中のアニメーションにアクセスしたりできます…ただし、それはそもそもアニメーションを持っていることを前提としています!今はそれに焦点を当てましょう。
AnimationGraph: アニメーションを格納し、組み合わせる方法
Bevyでのアニメーションの表現方法は少し複雑です。なぜなら、一般的に操作するのは単一のアニメーションではなく、複数のアニメーションを一度に格納し、それらを互いに組み合わせることができるデータ構造のインスタンスだからです。その構造がAnimationGraphです。
ここではAnimationGraphがどのように使用されるかの詳細には触れません。簡単なアニメーションに慣れたら、Animation Graphの例で一般的なアニメーショングラフについてもっと学ぶことができます。今のところ、グラフに1つのアニメーションのみが含まれているケースに焦点を当てます。その場合、アニメーションを識別するデータは次のようになります。
AnimationGraph(への参照)、- グラフ内のアニメーションが実際にどこにあるかを示す識別子。これは
NodeIndex型が使用される理由です。
まとめ
さて、AnimationPlayer、そしてAnimationGraphとNodeIndexを持っているとしましょう。それらを接続してアニメーションを再生する方法は次のとおりです。
AnimationPlayerと同じエンティティに、AnimationGraphへの参照をコンポーネントとして挿入します。これはAnimationGraphHandleがまさにそのためにあります。これはAnimationGraphへのHandleを保持し、Componentトレイトを実装しています。my_animation_player.play(my_node_index);を呼び出します。
これで完了です!この呼び出しにより、my_animation_playerは、それ自体と同じエンティティにコンポーネントとして存在するAnimationGraphを探し、そのグラフ内のインデックスNodeIndexを持つアニメーションを探し、それに属するモデルをアニメートします。
この時点での私のイメージは次のとおりです。
[画像: 青い箱に囲まれたエンティティ階層を表す同じ図。コンポーネントの1つがAnimationPlayerコンポーネントを表すために黄色く色付けされているが、今やAnimationPlayerと同じエンティティに別のコンポーネントが追加されている。その新しいコンポーネントは赤く色付けされ、AnimationGraphHandleコンポーネントを表す。そのすべてを右側に、AnimationGraphとNodeIndexというサブボックスを含む「Animation Data」と題された赤い箱がある。赤い矢印がAnimationGraphHandleとAnimationGraphボックスを接続している。楕円形の「play」がAnimationPlayerコンポーネントとNodeIndexボックスを接続している]
それでは、もう一度例のコードを見てみましょう。
コード例のウォークスルー
このセクションでは、Bevyの公式例であるAnimated Meshを、途中での関連性を説明しながら、単純にウォークスルーします。主に時系列順に進みますが、一部のセクションを並べ替えたりスキップしたりするかもしれません。私の目的は、既存のインラインコメント(コメントとしてはすでに非常に徹底的です!)よりも少し詳細な説明を提供し、それをこれまで抽象的に話してきたことと結びつけることです。
最初の数行は標準的なBevyのコードです。アニメーションロジックを配置するsetup_mesh_and_animationシステムを追加することに注意してください。このシステムを今見てみましょう。最初に、.glbファイルからアニメーションデータを抽出します。
let (graph, index) = AnimationGraph::from_clip(
asset_server.load(GltfAssetLabel::Animation(2).from_asset(GLTF_PATH)),
);
アニメーションクリップをロードするためのボイラープレートがいくつかありますが、各関数呼び出しの詳細を理解する必要はありません。ただし、いくつかの点に注意できます。
GltfAssetLabel::Animationコンストラクタは、.glbファイル内のアニメーションを参照する整数を取ります。ファイル構造が正確にわからない場合は、推測する必要があります。0、1、2のような数字を入れてみて、アニメーションが表示されるか確認してください。from_clipメソッドは、AnimationGraphと、そのグラフ内のNodeIndexの両方を返します。これは、アニメーションを参照するために必要なデータです。
次に、取得したグラフをアセットストアに追加し、それへのHandleを保持します。
let graph_handle = graphs.add(graph);
メッシュもロードします。グラフと同様に、すぐに完全に理解する必要のないボイラープレートがいくつかあります。
let mesh_scene =
WorldAssetRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH)));
取得したmesh_sceneをspawnメソッドで直接スポーンできることを知っておけば十分です。ここでは、便宜上、カスタムコンポーネントanimation_to_play(例の早い段階で定義されたカスタムタイプAnimationToPlay)とバンドルします。animation_to_playには、アニメーショングラフハンドルとグラフインデックスという、必要となる2つのアニメーションデータが含まれています。
commands
.spawn((animation_to_play, mesh_scene))
そして、スポーンが適切に行われたら、play_animation_when_readyシステムに制御を渡します。
.observe(play_animation_when_ready);
spawn呼び出しの結果に対して呼び出されるため、このobserve呼び出しは、スポーンされたエンティティを引数として与えられたシステムに注入します。つまり、スポーンしたばかりのメッシュシーンのルートエンティティは、後述するように、scene_ready引数のentityフィールドからアクセスできます。
残りのタスクは、これまで行ったことよりも少し複雑です。次のことを行う必要があります。
AnimationPlayerコンポーネントを持つエンティティを特定する。- そのエンティティにアニメーショングラフへのハンドルをコンポーネントとして追加する。
- アニメーションプレイヤーに、目的のアニメーションを再生するように指示する。
しかし、ステップ1で問題が発生します。メッシュシーンを.glbファイルからロードしましたが、これはエンティティの階層をスポーンします。アニメーションプレイヤーは、その階層のどこかのエンティティにアタッチされているはずですが、どこかはわかりません!もし.glbファイルの構造と、BevyがそれらをECSにどのようにインスタンス化するかを非常に良く知っていれば、アニメーションプレイヤーを持つエンティティを推測できるかもしれません…いや、やめましょう。メッシュシーンのルートのすべての下降エンティティを手動で訪問し、AnimationPlayerコンポーネントを持っているか確認します。
メッシュシーン階層のルートエンティティを取得することから始めます。前述のように、これはscene_ready引数のentityフィールドとしてこのシステムに注入されます。そのエンティティは、必要なアニメーションデータを含むAnimationToPlay型のコンポーネントを持っているはずなので、直接getできます。
if let Ok(animation_to_play) = animations_to_play.get(scene_ready.entity) {
(animations_to_play(AnimationToPlay型のコンポーネントに対するQuery)と、そのクエリを使用してエンティティを調べることで取得する実際のAnimationToPlay(getメソッド)を混同しないでください。)
シーンのルート(およびアニメーションデータ)はありますが、シーンのルートはAnimationPlayerコンポーネントが存在する場所ではありません。私たちが知っているのは、それが私たちの.glbファイルとBevyがそれらをECSにインスタンス化する方法の詳細に依存する方法で、私たちのシーン階層のどこかのエンティティにアタッチされているだろうということです。しかし、それらすべてについて知る必要はありません。シーン内のすべてのエンティティ(つまり、ルートエンティティのすべての下降エンティティ)を単純に調べ、AnimationPlayerコンポーネントを持っているか確認できます。
for child in children.iter_descendants(scene_ready.entity) {
if let Ok(mut player) = players.get_mut(child) {
(iter_descendantsに慣れていない場合、これはQuery<&Children>(ただし、別の階層に関心がある場合はChildrenを別のRelationshipTargetに置き換えることができます)で呼び出す必要があり、引数として与えられたものから下降するすべてのエンティティを反復処理するという、期待どおりのことを行うメソッドです。)
ふう!難しい部分は終わりました。アニメーションプレイヤーを持つエンティティを見つけました。これで、そのエンティティにアニメーショングラフのハンドルを(例ではグラフが追加される前にプレイヤーにアニメーションを再生するように指示していますが、これは機能すると思いますが、直感的ではないと思います)追加できます。
まず、アニメーションプレイヤーエンティティに対応するEntityCommandsを取得します。これにより、コンポーネントを追加するなどの操作が可能になります。
commands.entity(child)
そして、animation_to_playに含まれるアニメーショングラフのハンドルをコンポーネントとして追加します。
.insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
最後に、animation_to_playで指定されたインデックスのアニメーションを再生するようにプレイヤーに指示し、ループするように指示することもできます。
player.play(animation_to_play.index).repeat();