プログラミング
Bevyにおけるアニメーション:全体像
Animation in Bevy: The Big Picture (glocq.com)
要約
この記事は、ゲームエンジンBevyで3Dモデルのアニメーションを実装する際の複雑さを解消し、開発者がアニメーションの仕組みを理解するためのメンタルモデル構築を支援します。BevyのECS(Entity Component System)の基本を理解している開発者を対象に、アニメーション再生に必要な要素(AnimationPlayer、AnimationGraph、NodeIndex)とその連携方法を解説し、公式サンプルコードの具体的な流れを紐解いていきます。
全文翻訳
Grégoire Locqueville
Animation in Bevy: The Big Picture
August 27, 2026
アニメーション付きの3Dキャラクターの.glbファイルを無料アセットサイトからダウンロードしたばかりだとします。Bevyの基本的なアプリケーションは実行中です。次に、そのキャラクターをBevyアプリにスポーンさせたいと考えます。簡単だろう、おそらくそのための関数があるはずです。Bevyのウェブサイトで基本的なアニメートメッシュの例を探しますが…すぐに、事態は思ったほど単純ではないことが明らかになります。設定すべきことが多く、アニメーション関連の型は、その正確な目的が不明瞭です…。例を調整すれば、比較的早くニーズに合うようにできるかもしれませんが、例に基づいてBevyにおけるアニメーションの仕組みのメンタルモデルを形成するには、かなりの熟考が必要になるでしょう。この記事は、数週間前にBevyのアニメーションを理解しようとしたときに、私が欲しかったものです。まず、Bevyでの基本的なアニメーションを扱うのに十分なメンタルモデルを、ステップバイステップで構築します。次に、公式の例を順を追って説明し、それがメンタルモデルとどのように関連しているかを解説し、まだ遭遇していないかもしれないメソッドに出会ったときに少し手助けします。BevyのECSの基本に慣れているなら、この記事の対象読者です :)
最初の直感
BevyとECSは一旦忘れて、アニメーション付きの3Dモデルを動かすために何が必要か(非常に抽象的に)考えてみましょう。2つの要素が必要です:
スポーンされた3Dモデル(またはそれへの参照)、
再生されるアニメーション(またはそれへの参照)。
したがって、最初にアニメーションを再生する方法は、次のようなものだと期待するかもしれません:
my_model.play_animation(my_animation);
ここまでの私たちのメンタルモデルは次のようになります:
これは実際、実際のBevyの関数が動作する方法にいくらか近い、つまり、AnimationPlayer型のplayメソッド…少なくとも、そのselfが3Dモデルを参照し、そのanimation引数がアニメーションを参照している場合です。しかし、現時点では、それらがそうであるかどうかは明らかではありません。しかし、私たちの2つの要素がBevyでどのように表現されているかを詳しく見てみましょう。そうすれば、私たちの直感とplay関数の動作方法を調和させることができるかもしれません。
AnimationPlayer:3Dモデルのアニメーションを制御する方法
.glbファイルから3Dモデルをスポーンしたとします。理想的には、スポーンされたエンティティのIDを使ってそれに参照したいところですが、3Dモデルは一般的に単一のエンティティとしてスポーンされるのではなく、エンティティの階層としてスポーンされます:
モデルをアニメートするには、別の方法で参照する必要があります。Bevyには、アニメート可能な3Dモデルを参照するためのメカニズムがあります。それはAnimationPlayer型です。AnimationPlayerはコンポーネントであり、モデルがスポーンされたときに、そのモデルに対応するエンティティ階層のどこかに自動的にエンティティに挿入されます。一度AnimationPlayerを掴めば、アニメーションを再生/一時停止したり、現在再生中のものにアクセスしたりできます…しかし、それはそもそもアニメーションがあることを前提としています!それでは、それらに焦点を当てましょう。
AnimationGraph:アニメーションを保存し、組み合わせる方法
Bevyでのアニメーションの表現方法は少し複雑です。なぜなら、一般的に操作するのは単一のアニメーションではなく、複数のアニメーションを一度に格納し、それらを互いに組み合わせることができるデータ構造のインスタンスだからです。その構造がAnimationGraphです。ここではAnimationGraphの使用方法の詳細には触れません。簡単なアニメーションに慣れたら、Animation Graphの例で一般的なアニメーショングラフについてもっと学ぶことができます。今のところ、私たちのグラフが単一のアニメーションのみを含んでいるケースに焦点を当てます。その場合、アニメーションを識別するデータは次のようになります:
(参照としての)AnimationGraph、
グラフ内のアニメーションが実際にどこにあるかを示す識別子 — NodeIndex型はそのためです。
まとめ
さて、AnimationPlayer、そしてAnimationGraphとNodeIndexを持っているとします。アニメーションを再生するためにそれらを接続する方法は次のとおりです:
AnimationGraphへの参照を、AnimationPlayerと同じエンティティにコンポーネントとして挿入します。これはまさにAnimationGraphHandleが行うことです。それはAnimationGraphへのHandleを保持し、Componentトレイトを実装しています。
my_animation_player.play(my_node_index);
これで完了です!この呼び出しにより、my_animation_playerは、それ自体と同じエンティティにコンポーネントとして存在するAnimationGraphを探し、そのグラフ内の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);
これはスポーン呼び出しの結果に対して呼び出されるため、このobserve呼び出しは、与えられたシステムにスポーンされたエンティティを注入します。つまり、スポーンしたばかりのメッシュシーンのルートエンティティは、後述するように、play_animation_when_readyの本体で、scene_ready引数のエンティティフィールドとしてアクセスできます。
残りのタスクは、これまで行ったことよりも少し複雑です。私たちは次のことを行う必要があります:
AnimationPlayerコンポーネントを持つエンティティを特定する、
アニメーショングラフへのハンドルを、それにコンポーネントとして追加する、
アニメーションプレイヤーに目的のアニメーションを再生するように指示する。
しかし、ステップ1で問題があります。メッシュシーンを.glbファイルからロードしましたが、それはエンティティの階層をスポーンします。アニメーションプレイヤーは、その階層内のどこかのエンティティにアタッチされているはずですが、どのエンティティかはわかりません!もし.glbファイルの構造とBevyのECSにおけるGLTFの表現方法を非常によく知っていれば、アニメーションプレイヤーがどのエンティティにあるかを推測できるかもしれません…。いや、それは忘れて、私たちは…