今回のテーマは 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 の言語設定
準備
プロジェクト設定
新規プロジェクトを立ち上げたら、下準備としてプロジェクト設定をしておく。
ウインドウサイズは以下のように設定した。
インプットマップに、プレイヤーキャラクターの移動と攻撃のアクションを以下のとおりに追加した。
アセットをダンロードする
今回は以下のリンク先からアセットパックをダウンロードさせていただいた。
ダウンロードしたフォルダの中にある「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 シーンのノードを編集する
Sprite ノード
- インスペクターにて「Texture」プロパティに先にインポートしておいたリソース「player.png」を適用する。
- 「Animation」>「Hframes」プロパティの値を 6 に、「Vframes」プロパティの値を 5 にする。これは適用したスプライトシートが水平方向に 6 列、垂直方向に 5 行で構成されているので、それに合わせた格好だ。無料版のスプライトシートなので 4 行目がないが気にしないでほしい。のちほど「AnimationPlayer」ノードで、このフレーム数を変化させることでアニメーションを作成していく。
- 「Offset」>「Offset」プロパティを (0, -17) に変更する。これでプレイヤーキャラクターのテクスチャの足元がちょうど座標 (0, 0) にくる。これはゲーム画面上で他のオブジェクトと重なった時にそれぞれの y 座標を比較して、手前の(y 座標が大きい)オブジェクトの方が前面に表示されるようにするための措置だ。
BodyCollisionShape (CollisionShape2D) ノード
- インスペクターにて「Shape」プロパティに「CircleShape2D」リソースを適用して、以下のようにサイズと位置を調整した。
HitBox (Area2D) ノード
このノードは近接攻撃のための Hit Box 用のノードだ。このノードは特に編集の必要はない。なお、近接攻撃の実装について詳しくは以下の記事で解説している。
HitBoxCollisionShape (CollisionShape2D) ノード
「Shape」プロパティに「RectangleShape2D」リソースを適用する。このノードで Hit Box の衝突形状を設定するわけだが、そのサイズや位置は「AnimationPlayer」ノードのアニメーションの中で変化させるので、ここではいったんそのままにしておき、アニメーション作成時に設定することにする。「Disabled」プロパティもまた攻撃用のアニメーションの中で変更することになるが、攻撃時以外は Hit Box を無効にする必要があるので、オンにしておく。
AnimationPlayer ノード
インスペクターで「Root Node」が「Player」ノードに設定されていることを確認する。
アニメーションを作る
アニメーションパネルにて、以下の 6 種類のアニメーションを作る。
- idle
待機中のアニメーション。「Sprite」ノードの「Frame」プロパティを変化させるだけ。ループはオン。 - run
移動中のアニメーション。「Sprite」ノードの「Frame」プロパティを変化させるだけ。ループはオン。 - attack1
攻撃時のアニメーションその1。「Sprite」ノードの「Frame」プロパティを変化させるのに加えて、「BodyCollisionShape」と「HitBoxCollisionShape」の「Disabled」プロパティのオンオフを切り替える。ループはオフ。このタイミングで「HitBoxCollisionShape」の衝突形状の位置とサイズをスプライトの剣の軌跡に合せて設定しておくこと。 - attack2
攻撃時のアニメーションその2。その1のアニメーション再生中に攻撃操作をするとその2が再生され、連続攻撃しているような演出にする。「attack1」を逆再生させているだけ。ループはオフ。 - hurt
ダメージを受けた時のアニメーション。スプライトシートにダメージを受けた時のテクスチャはないので色を点滅させて表現する。「Sprite」ノードの「Modulate」プロパティを 白 ⇄ 赤(半透明)の2色間で複数回切り替えている。パネル左上の「+Add Track」をクリックして「Call Method Track」を2つ追加した。一つは、アニメーションの最後でset_physics_process()
メソッドを呼び出すようにした。インスペクターで「Args」>「0」>「Value」をオンにする。これはこのメソッドの引数にtrue
を渡しているという意味だ。ループはオフ。もう一つは、後ほどスクリプトでメソッドを定義してから追加することになる(ここでは追加後の状態を示している)。die_on_hurt_anim()
メソッドを呼び出すようにした。これはライフが 0 になったら「die」アニメーションへ遷移させるメソッドになるが、スクリプトを作成する際にもう一度説明する。 - die
死んだ時のアニメーション。「Sprite」ノードの「Frame」プロパティを変化させるだけ。ループはオフ。
なお、「attack1」や「attack2」という命名規則は以下の動画を参考にしている。
YouTube - Name Files Logically
AnimationTree ノード
このノードを使って、「AnimationPlayer」で用意したアニメーションを制御するステートマシンを作っていく。
- インスペクターにて「Tree Root」プロパティで「新規 AnimationNodeStateMachine」を選択する。アニメーションの状態管理をできるようするための設定だ。
- 「Anim Player」プロパティで「AnimationPlayer」ノードが選択されている状態にする。
- 「Active」プロパティをオンにする。オンにしないとこのノードは機能しない。ただし「AnimationPlayer」ノードの方でアニメーションを追加したり編集したりする時、オフにしてアニメーションの再生を止める必要があるかもしれない。
ステートマシンのアニメーションツリーを作る
ここからはアニメーションパネル上での作業になる。ステートマシンを担うアニメーションツリーを構成していく。
- アニメーションツリーパネルを開く。
- 選択・移動ツールでパネル内を右クリックするか、
ノード作成ツールでパネル内をクリックすると、
「AnimationPlayer」で作成したアニメーションの中からノードとして追加したいアニメーションを選択できる。 - ひとまず「AnimationPlayer」で作成したアニメーションを全て、アニメーションツリーのノードとして追加する。以下のスクリーンショットではノードが整頓されているが、実際のアニメーションツリー作成では、ノード同士を接続していきながら、都度、見やすい配置に調整することになるだろう。
- 続いて、選択・移動ツールで Shift キーを押しながら、
またはノード接続ツールで
ノードからノードへドラッグ操作をすると、ノード同士を接続することができる。 - 接続した矢印を選択した状態でインスペクターを見ると「AnimationNodeStateMachineTransition」というリソースであることがわかる。このリソースの「Switch Mode」プロパティをデフォルトの「Immediate」から「AtEnd」に変更すると、そのノードのアニメーションが終了してから次のノードのアニメーションへ遷移させることができる。この時やパネル内の矢印は線が入った表示になる。さらに「Auto Advance」プロパティをオンにしておけば、スクリプトで制御せずとも自動的に遷移させることができる。この場合、矢印は緑色になる。
例えば、プレイヤーキャラクターが敵からダメージを受けた時は、ただちに「idle」から「hurt」へアニメーションを切り替えるが、「hurt」のアニメーションが終了したら自動的に「idle」へ遷移させたい。この場合は以下のようになる。 - ゲーム開始時の最初のアニメーションは自動再生させたいので「idle」アニメーションのノードを選択して自動再生を有効にしておく。
同様に、プレイヤーキャラクターが死んだ時の「die」アニメーションのノードを選択して最後のノードとして設定しておく。 - 最終的に以下のような構成とした。今回は移動中は攻撃できないようにしている。
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」シーンをメインシーンに設定してプロジェクトを実行する。
ステートマシンによって、状況に合わせてアニメーションが円滑に遷移しているのがおわかりいただけるだろうか。
おわりに
今回はステートマシンを利用して、プレイヤーキャラクターのアニメーション(状態)を遷移させる方法について説明した。作業のポイントを振り返っておこう。
- ゲームで使用する状態を先に洗い出しておく
- 「AnimationPlayer」で状態ごとにアニメーションを作る
- 「AnimationTree」でステートマシンを作る
- スクリプトでアニメーションの遷移を制御する
実際には、アニメーションの数が多いとアニメーションツリーパネル内でノードとそれらを繋ぐ線がごちゃごちゃしてきて、見た目上わかりにくくなるかもしれない。そのような場合は、頭の中を整理する意味で、先に紙にペンで書き出してみると良い。
もちろん「AnimationTree」ノードに頼らず、スクリプトでステートマシンを作ることもできるので、もしアニメーションツリーパネルの編集が苦手な場合は、そちらを試してみるのも良いだろう。
参考
- Godot Docs - Introduction to the animation features
- Godot Docs - Using AnimationTree
- Godot Docs - AnimationNodeStateMachinePlayback
- KidsCanCode - CONTROLLING ANIMATION STATES
- YouTube - Godot Recipes: Animation States
- YouTube - Make an Action RPG in Godot 3.2 (P9 | Attacking Animation + State Machines)
- YouTube - Name Files Logically
- itch.io - mystic woods