【Godot】AnimationTreeとステートマシンで実現する複雑なアニメーション管理

作成: 2025-12-08最終更新: 2026-07-08

GodotのAnimationTreeとステートマシンで、待機・歩行・走行・ジャンプなど複雑なキャラクターアニメーションを効率的に管理する方法を、状態グラフの考え方と実践的なコード例で解説します。

待機、歩き、走り、ジャンプ、攻撃——キャラクターにできることが増えるほど、_physics_process の中は「速度がこれ以上なら走りアニメ、床にいなければジャンプアニメ…」という if-elif の山になっていきます。どのアニメがいつ再生されるのか、自分でも追えなくなってきた、という経験はないでしょうか。

この「アニメの切り替えロジック」を、コードから引きはがして 見えるグラフ にできるのが、Godotの AnimationTree と、その中の ステートマシンAnimationNodeStateMachine)です。コード側は「今こういう状態だよ」と伝えるだけ。どのアニメへどう繋ぐかは、グラフが面倒を見てくれます。

走るキャラクターの動きが、idle・walk・run・jumpをつないだ状態グラフによって制御されているイメージ図

この記事でわかること

  • if-elif の山を、見えるステートグラフに置き換える考え方
  • AnimationPlayer(倉庫)と AnimationTree(司令塔)の役割分担
  • travel() で「今の状態」を伝えてアニメを切り替える基本実装
  • X-Fadeによる滑らかな遷移と、コードだけで組む場合との使い分け

Sponsored

なぜAnimationTreeとステートマシンが必要なのか

キャラクターの状態が「待機・歩行」くらいなら、if 文でアニメを切り替えても困りません。しかし「走行・ジャンプ・落下・攻撃・被弾…」と増えると、_physics_process の中に if-elif-else の巨大なネスト が育ちます。条件が絡み合い、「このアニメはどの条件で再生されるのか」を追うのが一苦労になります。

if-elifの山とステートグラフの比較図。悪い例は深くネストしたコード、良い例はidle・walk・run・jumpをつないだすっきりした状態グラフ

ステートマシンを使うと、この切り替えを 状態(ノード)と遷移(矢印)のグラフ に描き替えられます。ロジックがコードから図に移るので、全体像がひと目で分かり、状態を1つ足すのもノードと矢印を追加するだけで済みます。

AnimationPlayerとAnimationTreeの役割分担

まず、登場する3つの役者を整理します。

  • AnimationPlayer(アニメの倉庫)idlewalkrunjump といった個々のアニメーションクリップを保管する場所です。作り込みは AnimationPlayerの高度な活用 を参照してください。
  • AnimationTree(司令塔) :倉庫からクリップを取り出し、どれをどの順で・どう滑らかに繋ぐかを制御するエンジンです。
  • AnimationNodeStateMachinePlayback(コードの窓口) :GDScriptから「今この状態へ」と指示するためのオブジェクトです。
AnimationPlayer(アニメの倉庫)からAnimationTree(司令塔)を通ってキャラクターのアニメーションが実行される流れの図

ポイントは、 コードから AnimationPlayer を直接叩かず、AnimationTree を通して状態を管理する ことです。こうしておくと、アニメの繋ぎ方を変えたくなってもグラフ側をいじるだけで済み、ゲームロジックのコードはそのままにできます。

Sponsored

ステートマシンの考え方:状態と遷移

ステートマシンは、 状態(State) と、状態をつなぐ 遷移(Transition) でできたグラフです。

  • 状態(State)idlewalkrun のように、その瞬間に再生されるアニメーション1つ。
  • 遷移(Transition) :状態から状態へ移る矢印。「速度が180を超えたら walk から run へ」のように、移る向きと条件を決めます。
ステートマシンのグラフ。idle・walk・runが速度条件でつながり、地上の各状態から空中のjumpへ、着地でjumpから戻る

エディタ下部の AnimationTree パネルで、Animation ノードを置いて idlewalkrunjump と名付け、矢印でつなぐだけでグラフができます。あとはコードから「今の状態」を伝えれば、AnimationTree が矢印に沿ってアニメを切り替えます。

実践:待機・歩行・走行・ジャンプを実装する

横スクロールアクションのプレイヤーを例に、実際に組んでみます。トップダウンARPGでもメトロイドヴァニアでも、「移動速度と接地状態からアニメを決める」という骨組みは同じです。

ノードとアニメーションの準備

次のノード構成にして、AnimationPlayeridlewalkrunjump の4つのアニメーションを作っておきます。

- CharacterBody2D
  - Sprite2D
  - CollisionShape2D
  - AnimationPlayer
  - AnimationTree

AnimationTreeAnim PlayerAnimationPlayer を割り当て、Tree RootNew AnimationNodeStateMachine にして、前の章のグラフを組みます。

コードから状態を伝える

コード側の仕事は「速度と接地から今の状態を決めて、AnimationTree に伝える」だけです。

実践のデータの流れ。接地と速度の入力から状態を判定し、選ばれたrunをtravelでAnimationTreeに伝える
extends CharacterBody2D

const WALK_SPEED := 100.0
const RUN_SPEED := 250.0
const JUMP_VELOCITY := -400.0

@onready var animation_tree: AnimationTree = $AnimationTree
# ステートマシンをコードから操作するための窓口
@onready var state_machine: AnimationNodeStateMachinePlayback = animation_tree.get("parameters/playback")

var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")

func _ready() -> void:
    animation_tree.active = true

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y += gravity * delta
    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    var direction := Input.get_axis("ui_left", "ui_right")
    var target_speed := 0.0
    if direction:
        target_speed = RUN_SPEED if Input.is_action_pressed("ui_sprint") else WALK_SPEED
    velocity.x = move_toward(velocity.x, direction * target_speed, 20.0)

    move_and_slide()
    _update_animation()

func _update_animation() -> void:
    var target_state := "jump"
    if is_on_floor():
        var speed := absf(velocity.x)
        if speed > 180.0:
            target_state = "run"
        elif speed > 10.0:
            target_state = "walk"
        else:
            target_state = "idle"

    # 状態が変わったときだけ travel() する(毎フレーム呼ばない)
    if state_machine.get_current_node() != target_state:
        state_machine.travel(target_state)

_update_animation() は「速度と接地」から状態名を1つ決め、それが今の状態と違うときだけ travel() します。アニメの実体(何コマ目で足が地面に着くか等)は AnimationPlayer 側に任せ、コードは状態名を渡すだけ——この分担が、状態が増えても破綻しない秘訣です。

Sponsored

よくある間違いとベストプラクティス

よくある間違いベストプラクティス
毎フレーム無条件に travel() を呼ぶ今の状態と違うときだけ travel() する(上の get_current_node() チェック)
巨大な単一ステートマシン関連する状態をまとめ、ネストしたサブステートマシン(地上/空中など)に分ける
速度などをマジックナンバーで直書きconst RUN_SPEED := 250.0 のように定数や @export にして調整しやすくする
遷移の X-Fade Time が00.1〜0.3秒のクロスフェードを設定し、アニメ同士を滑らかに補間する

とくに効果が大きいのが、遷移の X-Fade Time です。0のままだとアニメがパチッと切り替わりますが、少しフェードを入れるだけで、歩き→走りの繋ぎが見違えるほど自然になります。

walkからrunへの遷移にX-Fadeを入れると、中間のポーズが重なりながらなめらかに繋がることを示す図
Sponsored

コードだけで組む場合との使い分け

AnimationTree を使わず、AnimationPlayer を直接 play() してもアニメは切り替えられます。

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        $AnimationPlayer.play("jump")
    elif absf(velocity.x) > 180.0:
        $AnimationPlayer.play("run")
    elif absf(velocity.x) > 10.0:
        $AnimationPlayer.play("walk")
    else:
        $AnimationPlayer.play("idle")
コードだけとAnimationTreeの使い分けの比較図。コードだけは状態3〜4個ならOKだが増えると複雑、AnimationTreeは状態5個以上や滑らかな遷移に向く

状態が3〜4個の単純なキャラなら、これで十分わかりやすいこともあります。ただし状態が増えると if がネストし、遷移条件やフェードの管理も手書きになってスパゲッティ化しがちです。 状態が5つ以上になる、または滑らかな遷移が欲しくなったら AnimationTree 、と覚えておくとよいでしょう。「状態そのものの管理」の考え方は ステートマシンで管理するAIとプレイヤーの状態 も参考になります。

おまけ:次に知っておくと良いこと

AnimationTree に慣れてきたら、次のノードが表現の幅を広げてくれます。今は「そういう手がある」と知っておくだけで十分です。

  • BlendSpace2D / BlendSpace1D :速度や方向のベクトルに応じて、複数アニメを滑らかにブレンドします。8方向移動の歩きアニメなどに最適です。
  • AnimationNodeOneShot :攻撃やアイテム使用のように「一度だけ再生して元の状態へ戻る」動きに便利です。移動アニメの上に攻撃を重ねられます。
  • ネストしたステートマシン :状態が増えたら「地上」「空中」で大きく分け、その中に待機・歩行や、ジャンプ・落下のサブ状態を作ると整理できます。
  • 単発の色演出は Tween で :被弾フラッシュのような単発演出は、ステートマシンに載せず Tween で足すのが手軽です。

まとめ

  • AnimationTreeAnimationPlayer(アニメの倉庫)からクリップを取り出し、遷移を制御する司令塔
  • ステートマシン :状態(ノード)と遷移(矢印)で、アニメの切り替えを見えるグラフにする
  • コードの仕事 :速度や接地から「今の状態」を決め、travel() で伝えるだけ。アニメの中身は倉庫に任せる
  • 使い分け :状態が少なければコード直書き、増えたら AnimationTree。滑らかさは X-Fade で足す

if-elif の山をグラフに置き換えると、キャラクターの動きを足したり直したりが一気に楽になります。まずは待機・歩行・走行・ジャンプの4状態から組んでみて、慣れたら BlendSpaceOneShot で方向ブレンドや攻撃を重ねていきましょう。関連して AnimatedSprite2DとAnimationPlayerの使い分け も押さえると、2Dアニメの選択肢が整理できます。