今回のテーマは 2D ゲームのアニメーションステートマシンの実装だ。ステートとは「状態」のことで、ステートマシンというのは、キャラクターのある状態からある状態への遷移を制御する仕組みのことだ。

ある状態からは限られた状態にしか遷移できなかったり、状態によって今のアニメーションが終了してから次の状態に遷移させるのか、ただちに遷移させるのかの違いがある。例えば、「idle(待機)」と「run(移動)」はただちに双方向に遷移するが、「idle」から「attack(攻撃)」へはただちに遷移しても「attack」から「run」へは遷移せず、また「attack」から「idle」へは「attack」のアニメーションが終わってから遷移する、といった具合だ。

これらの制御を全てスクリプトでコーディングすると、コードがやや長く、複雑になりがちである。一方、Godot の「AnimationTree」ノードを利用すれば、スクリプトのコード量が減って可読性を高めることができるのだ。今回はこの「AnimationTree」ノードを使ったステートマシンの実装方法を紹介していく。

Environment
Godot のバージョン: 3.5.1
コンピュータのOS: macOS 12.6

Basic Articles
以下の記事もお役立てください。
Godot をダウンロードする
Godot のプロジェクトマネージャー
Godot の言語設定



準備

プロジェクト設定

新規プロジェクトを立ち上げたら、下準備としてプロジェクト設定をしておく。

ウインドウサイズは以下のように設定した。
project settings - window size

インプットマップに、プレイヤーキャラクターの移動と攻撃のアクションを以下のとおりに追加した。
project settings - add actions to input map


アセットをダンロードする

今回は以下のリンク先からアセットパックをダウンロードさせていただいた。

itch.io - mystic woods

ダウンロードしたフォルダの中にある「player.png」「slime.png」「objects.png」の3つのスプライトシートを利用する。この素晴らしいアセットに感謝せずにはいられない。

3つのスプライトシート(.pngファイル)は、Godot のファイルシステムへドラッグ&ドロップしてプロジェクトに追加する。

追加した画像がぼやけた感じで表示されていたら(デフォルトではそうなる)、インポートしたファイルを選択して、「インポート」タブから「プリセット」>「2D Pixel」を選択して、再インポートすること。これでドット絵特有のエッジの効いた状態で表示される。


Player シーンを作る

プレイヤーキャラクター用に「Player」シーンを作成する。シーンツリーは以下のようになる。

  • Player (KinematicBody2D)
    • Sprite
    • BodyCollisionShape (CollisionShape2D)
    • HitBox (Area2D)
      • HitBoxCollisionShape (CollisionShape2D)
    • AnimationPlayer
    • AnimationTree

Player scene tree



Player シーンのノードを編集する

Sprite ノード

  1. インスペクターにて「Texture」プロパティに先にインポートしておいたリソース「player.png」を適用する。
    Sprite node - edit properies
  2. 「Animation」>「Hframes」プロパティの値を 6 に、「Vframes」プロパティの値を 5 にする。これは適用したスプライトシートが水平方向に 6 列、垂直方向に 5 行で構成されているので、それに合わせた格好だ。無料版のスプライトシートなので 4 行目がないが気にしないでほしい。のちほど「AnimationPlayer」ノードで、このフレーム数を変化させることでアニメーションを作成していく。
    Sprite node - edit properies Sprite node - edit properies
  3. 「Offset」>「Offset」プロパティを (0, -17) に変更する。これでプレイヤーキャラクターのテクスチャの足元がちょうど座標 (0, 0) にくる。これはゲーム画面上で他のオブジェクトと重なった時にそれぞれの y 座標を比較して、手前の(y 座標が大きい)オブジェクトの方が前面に表示されるようにするための措置だ。
    Sprite node - edit properies Sprite node - edit properies

BodyCollisionShape (CollisionShape2D) ノード

  1. インスペクターにて「Shape」プロパティに「CircleShape2D」リソースを適用して、以下のようにサイズと位置を調整した。
    BodyCollisionShape node - edit properies

HitBox (Area2D) ノード

このノードは近接攻撃のための Hit Box 用のノードだ。このノードは特に編集の必要はない。なお、近接攻撃の実装について詳しくは以下の記事で解説している。

Godot3 で作る 2D ゲームの近接攻撃の当たり判定


HitBoxCollisionShape (CollisionShape2D) ノード

「Shape」プロパティに「RectangleShape2D」リソースを適用する。このノードで Hit Box の衝突形状を設定するわけだが、そのサイズや位置は「AnimationPlayer」ノードのアニメーションの中で変化させるので、ここではいったんそのままにしておき、アニメーション作成時に設定することにする。「Disabled」プロパティもまた攻撃用のアニメーションの中で変更することになるが、攻撃時以外は Hit Box を無効にする必要があるので、オンにしておく。
HitBoxCollisionShape node - edit properies


AnimationPlayer ノード

インスペクターで「Root Node」が「Player」ノードに設定されていることを確認する。
AnimationPlayer node - edit properies


アニメーションを作る

アニメーションパネルにて、以下の 6 種類のアニメーションを作る。

  • idle
    待機中のアニメーション。「Sprite」ノードの「Frame」プロパティを変化させるだけ。ループはオン。
    Animation - idle
  • run
    移動中のアニメーション。「Sprite」ノードの「Frame」プロパティを変化させるだけ。ループはオン。
    Animation - run
  • attack1
    攻撃時のアニメーションその1。「Sprite」ノードの「Frame」プロパティを変化させるのに加えて、「BodyCollisionShape」と「HitBoxCollisionShape」の「Disabled」プロパティのオンオフを切り替える。ループはオフ。このタイミングで「HitBoxCollisionShape」の衝突形状の位置とサイズをスプライトの剣の軌跡に合せて設定しておくこと。
    Animation - attack1
  • attack2
    攻撃時のアニメーションその2。その1のアニメーション再生中に攻撃操作をするとその2が再生され、連続攻撃しているような演出にする。「attack1」を逆再生させているだけ。ループはオフ。
    Animation - attack2
  • hurt
    ダメージを受けた時のアニメーション。スプライトシートにダメージを受けた時のテクスチャはないので色を点滅させて表現する。「Sprite」ノードの「Modulate」プロパティを 白 ⇄ 赤(半透明)の2色間で複数回切り替えている。パネル左上の「+Add Track」をクリックして「Call Method Track」を2つ追加した。一つは、アニメーションの最後でset_physics_process()メソッドを呼び出すようにした。インスペクターで「Args」>「0」>「Value」をオンにする。これはこのメソッドの引数にtrue を渡しているという意味だ。ループはオフ。もう一つは、後ほどスクリプトでメソッドを定義してから追加することになる(ここでは追加後の状態を示している)。die_on_hurt_anim()メソッドを呼び出すようにした。これはライフが 0 になったら「die」アニメーションへ遷移させるメソッドになるが、スクリプトを作成する際にもう一度説明する。
    Animation - hurt
  • die
    死んだ時のアニメーション。「Sprite」ノードの「Frame」プロパティを変化させるだけ。ループはオフ。
    Animation - die

なお、「attack1」や「attack2」という命名規則は以下の動画を参考にしている。
YouTube - Name Files Logically


AnimationTree ノード

このノードを使って、「AnimationPlayer」で用意したアニメーションを制御するステートマシンを作っていく。

  1. インスペクターにて「Tree Root」プロパティで「新規 AnimationNodeStateMachine」を選択する。アニメーションの状態管理をできるようするための設定だ。
  2. 「Anim Player」プロパティで「AnimationPlayer」ノードが選択されている状態にする。
  3. 「Active」プロパティをオンにする。オンにしないとこのノードは機能しない。ただし「AnimationPlayer」ノードの方でアニメーションを追加したり編集したりする時、オフにしてアニメーションの再生を止める必要があるかもしれない。
    AnimationTree - Edit properties

ステートマシンのアニメーションツリーを作る

ここからはアニメーションパネル上での作業になる。ステートマシンを担うアニメーションツリーを構成していく。

  1. アニメーションツリーパネルを開く。
    AnimationTree pannel
  2. 選択・移動ツールでパネル内を右クリックするか、
    AnimationTree pannel ノード作成ツールでパネル内をクリックすると、
    AnimationTree pannel 「AnimationPlayer」で作成したアニメーションの中からノードとして追加したいアニメーションを選択できる。
    AnimationTree pannel
  3. ひとまず「AnimationPlayer」で作成したアニメーションを全て、アニメーションツリーのノードとして追加する。以下のスクリーンショットではノードが整頓されているが、実際のアニメーションツリー作成では、ノード同士を接続していきながら、都度、見やすい配置に調整することになるだろう。
    AnimationTree pannel
  4. 続いて、選択・移動ツールで Shift キーを押しながら、
    AnimationTree pannel またはノード接続ツールで
    AnimationTree pannel ノードからノードへドラッグ操作をすると、ノード同士を接続することができる。
    AnimationTree pannel
  5. 接続した矢印を選択した状態でインスペクターを見ると「AnimationNodeStateMachineTransition」というリソースであることがわかる。このリソースの「Switch Mode」プロパティをデフォルトの「Immediate」から「AtEnd」に変更すると、そのノードのアニメーションが終了してから次のノードのアニメーションへ遷移させることができる。この時やパネル内の矢印は線が入った表示になる。AnimationTree pannel AnimationTree pannel さらに「Auto Advance」プロパティをオンにしておけば、スクリプトで制御せずとも自動的に遷移させることができる。この場合、矢印は緑色になる。
    AnimationTree pannel AnimationTree pannel 例えば、プレイヤーキャラクターが敵からダメージを受けた時は、ただちに「idle」から「hurt」へアニメーションを切り替えるが、「hurt」のアニメーションが終了したら自動的に「idle」へ遷移させたい。この場合は以下のようになる。
    AnimationTree pannel
  6. ゲーム開始時の最初のアニメーションは自動再生させたいので「idle」アニメーションのノードを選択して自動再生を有効にしておく。
    AnimationTree pannel AnimationTree pannel 同様に、プレイヤーキャラクターが死んだ時の「die」アニメーションのノードを選択して最後のノードとして設定しておく。
    AnimationTree pannel AnimationTree pannel
  7. 最終的に以下のような構成とした。今回は移動中は攻撃できないようにしている。
    AnimationTree pannel


Player ノードにスクリプトをアタッチする

「Player」ノードにスクリプトをアタッチして、以下のように編集する。

###Player.gd###
extends KinematicBody2D

# ライフ
var life: int = 3
# 速さ
var speed := 80.0
# 速度(方向を含む)
var velocity: Vector2

# Spriteノードの参照
onready var sprite = $Sprite
# AnimationTree ノードの Parameters > Playback プロパティ
# つまり AnimationNodeStateMachinePlayback リソース
onready var state_machine = $AnimationTree.get("parameters/playback")


func _physics_process(_delta):
	get_input()
	velocity = move_and_slide(velocity)

# プレイヤーの入力に関するメソッド
func get_input():
    # 現在のステートを取得
	var current_state = state_machine.get_current_node()

    # 攻撃の入力
	if Input.is_action_just_pressed("attack"):
        # 今のステートが attack1 でなければ attack1 へ遷移
		if current_state != "attack1":
			state_machine.travel("attack1")
        # 今のステートが attack1 の場合は attack2 へ遷移
		else:
			state_machine.travel("attack2")
        # 攻撃の入力した場合、移動の入力は受け付けずに終了
		return

    # 移動の入力
	velocity = Vector2()
	if Input.is_action_pressed("right"):
		velocity.x += 1
		sprite.flip_h = false
	if Input.is_action_pressed("left"):
		velocity.x -= 1
		sprite.flip_h = true
	if Input.is_action_pressed("down"):
		velocity.y += 1
	if Input.is_action_pressed("up"):
		velocity.y -= 1
	velocity = velocity.normalized() * speed
    # 移動速度の長さが 0 の場合はステートを idle へ遷移
	if velocity.length() == 0:
		state_machine.travel("idle")
    # 移動速度の長さが 0 より大きい場合はステートを run へ遷移
	if velocity.length() > 0:
		state_machine.travel("run")


# ダメージを受けた時に呼び出されるメソッド
func hurt():
    # ライフが 1 減る
	life -= 1
    # 物理プロセスを停止して入力を受け付けなくする
	set_physics_process(false)
    # ステートを hurt へ遷移
	state_machine.travel("hurt")


# hurt アニメーションの最後で呼び出される
func die_on_hurt_anim():
    # ライフが 0 より大きければ何もせず終了
	if life > 0: return
    # ステートを die へ遷移
	state_machine.travel("die")
    # 物理プロセスを停止して入力を受け付けなくする
	set_physics_process(false)

先にも述べたが、die_on_hurt_animのメソッドは「hurt」アニメーション内で呼び出す必要がある。このタイミングで一度アニメーションパネルに戻って「AnimationPlayer」ノードの「hurt」アニメーションを編集しておくこと。

「AnimationTree」ノードの「Parameters」>「Playback」プロパティの値になっている「AnimationNodeStateMachinePlayback」リソースのクラスにはget_current_nodeというメソッドが用意されており、これを呼び出すと現在のアニメーションノードを取得することができる。if文で現在のノードによって条件分岐させたい場合に便利だ。また、travelメソッドは引数に遷移させたいアニメーションノードの名前を文字列で渡すことで、アニメーションの遷移をスクリプトで制御することができる。もちろん、アニメーションツリー上で遷移する設定(矢印でつながっている状態)にしておく必要がある。

スクリプトの最後に「HitBox (Area2D)」ノードの「body_entered」シグナルを接続して、生成されたメソッドを以下のように編集する。

###Player.gd###
func _on_HitBox_body_entered(body):
    # 剣に当たったボディオブジェクトの hurt メソッドを呼び出す
	body.hurt()


動作確認

今回は動作確認用に別途「Tree」シーンと「Slime」シーンを作成した。どちらも「Player」シーンの「HitBox」との衝突でダメージを与えることができる。また「Slime」シーンの方にも「HitBox」ノードを用意し、「Player」が「Slime」に当たるとプレイヤー側がダメージを受けてライフが1つ減る仕様だ。「Player」のlifeプロパティは 3 に設定しているので、「Slime」に3回当たると死ぬようになっている。

「World」シーンを用意し、「Player」、「Tree」、「Slime」それぞれのシーンのインスタンスを追加した。動作確認のため、「World」シーンをメインシーンに設定してプロジェクトを実行する。
World scene tree

ステートマシンによって、状況に合わせてアニメーションが円滑に遷移しているのがおわかりいただけるだろうか。
World scene - 2D Workspace



おわりに

今回はステートマシンを利用して、プレイヤーキャラクターのアニメーション(状態)を遷移させる方法について説明した。作業のポイントを振り返っておこう。

  • ゲームで使用する状態を先に洗い出しておく
  • 「AnimationPlayer」で状態ごとにアニメーションを作る
  • 「AnimationTree」でステートマシンを作る
  • スクリプトでアニメーションの遷移を制御する

実際には、アニメーションの数が多いとアニメーションツリーパネル内でノードとそれらを繋ぐ線がごちゃごちゃしてきて、見た目上わかりにくくなるかもしれない。そのような場合は、頭の中を整理する意味で、先に紙にペンで書き出してみると良い。

もちろん「AnimationTree」ノードに頼らず、スクリプトでステートマシンを作ることもできるので、もしアニメーションツリーパネルの編集が苦手な場合は、そちらを試してみるのも良いだろう。



参考