Godot 4で2Dアニメーションを作るとき、多くの人がまず AnimatedSprite2D や AnimationPlayer を使いますよね。もちろんそれでOKなのですが、
- 「とりあえず歩きモーションだけサクッと作りたい」
- 「敵やギミック用の細かいアニメを大量に仕込むのが面倒」
- 「AnimationPlayerのトラック管理がつらくなってきた」
みたいなタイミングで、だんだん アニメーション用ノードが増えすぎる問題 にぶつかります。
プレイヤー、敵、ギミックそれぞれに AnimationPlayer を生やして、トラックやキーをちまちま編集して…というのは、シーンが大きくなるほど管理がつらくなっていきます。
そこで今回は、AnimationPlayerを一切使わず、Sprite2D の frame をコードで切り替えるだけのシンプルなコンポーネント、SpriteAnimator を用意しました。
「歩き」「待機」「攻撃」みたいな簡易アニメなら、このコンポーネントをアタッチするだけでOKにしてしまいましょう。
ノード階層を増やさず、継承よりもコンポーネント追加でアニメ機能を合成するスタイルですね。
【Godot 4】コードだけでサクッとフレーム切替!「SpriteAnimator」コンポーネント
今回の SpriteAnimator は、
Sprite2Dのhframes/vframesを使ったスプライトシート前提- 複数の「アニメ名」を定義して、コードやインスペクタから再生
- AnimationPlayerを使わず、
_process内でフレーム更新 - ループ・一回再生・FPS指定などをシンプルに制御
という「簡易アニメ管理コンポーネント」です。
フルコード:SpriteAnimator.gd
extends Node
class_name SpriteAnimator
## Sprite2D のフレームをコードで切り替える簡易アニメーター。
## AnimationPlayer を使わずに、スプライトシートからアニメを再生します。
## --- データ構造定義 ---
## 1つのアニメーション定義
class_name SpriteAnimClip
class SpriteAnimClip:
## アニメ名(例: "idle", "run")
var name: StringName
## 使用するフレームインデックスの配列(左上を0として右→下に番号が振られる想定)
var frames: PackedInt32Array = PackedInt32Array()
## 1秒あたりのフレーム数(0以下ならアニメしない)
var fps: float = 8.0
## ループ再生するかどうか
var loop: bool = true
func _init(_name: StringName = "", _frames: PackedInt32Array = PackedInt32Array(), _fps: float = 8.0, _loop: bool = true) -> void:
name = _name
frames = _frames
fps = _fps
loop = _loop
## --- エクスポート変数 ---
## 対象となる Sprite2D。
## 未設定の場合は、親ノードから自動で探しにいきます。
@export var sprite: Sprite2D
## 定義済みアニメーションのリスト。
## インスペクタ上で複数のアニメを登録できます。
@export var animations: Array[SpriteAnimClip] = []
## シーン開始時に自動再生するアニメ名(空文字で自動再生なし)
@export var autoplay: StringName = ""
## 停止時にフレームをリセットするか
@export var reset_frame_on_stop: bool = true
## --- 内部状態 ---
var _current_clip: SpriteAnimClip
var _current_frame_index: int = 0
var _time_accum: float = 0.0
var _is_playing: bool = false
## アニメ再生が終了したときに発火するシグナル(ループアニメでは発火しない)
signal animation_finished(anim_name: StringName)
func _ready() -> void:
# sprite が未設定なら、親ノードから Sprite2D を自動取得
if sprite == null:
sprite = get_parent() as Sprite2D
if sprite == null:
push_warning("SpriteAnimator: 親に Sprite2D が見つかりません。sprite プロパティを設定してください。")
# autoplay が設定されていれば再生
if autoplay != "":
play(autoplay)
func _process(delta: float) -> void:
if not _is_playing:
return
if _current_clip == null:
return
if _current_clip.fps <= 0.0:
return
if _current_clip.frames.is_empty():
return
# 経過時間を加算
_time_accum += delta
var frame_time := 1.0 / _current_clip.fps
# 必要なだけフレームを進める
while _time_accum >= frame_time:
_time_accum -= frame_time
_advance_frame()
func _advance_frame() -> void:
if _current_clip == null:
return
_current_frame_index += 1
if _current_frame_index >= _current_clip.frames.size():
# 末尾に到達
if _current_clip.loop:
# ループなら先頭に戻る
_current_frame_index = 0
else:
# ループしないなら停止してシグナルを発火
_is_playing = false
_current_frame_index = _current_clip.frames.size() - 1
_apply_frame()
animation_finished.emit(_current_clip.name)
return
_apply_frame()
func _apply_frame() -> void:
if sprite == null:
return
if _current_clip == null:
return
if _current_clip.frames.is_empty():
return
var frame_index := clamp(_current_clip.frames[_current_frame_index], 0, _get_max_frame_index())
sprite.frame = frame_index
func _get_max_frame_index() -> int:
if sprite == null:
return 0
# Sprite2D のスプライトシート全体のフレーム数を計算
var total_frames := int(sprite.hframes) * int(sprite.vframes)
return max(total_frames - 1, 0)
## --- パブリックAPI ---
## 指定したアニメーション名を再生します。
## 例: play("run")
func play(anim_name: StringName, restart: bool = true) -> void:
var clip := _find_clip(anim_name)
if clip == null:
push_warning("SpriteAnimator: アニメーション '%s' が見つかりません。" % anim_name)
return
if _current_clip == clip and not restart:
# すでに再生中で、リスタート不要なら何もしない
_is_playing = true
return
_current_clip = clip
_is_playing = true
_time_accum = 0.0
_current_frame_index = 0
_apply_frame()
## 再生中のアニメーションを停止します。
func stop() -> void:
_is_playing = false
if reset_frame_on_stop and _current_clip != null and not _current_clip.frames.is_empty():
_current_frame_index = 0
_apply_frame()
## 一時停止(フレームは保持)
func pause() -> void:
_is_playing = false
## 一時停止から再開
func resume() -> void:
if _current_clip != null:
_is_playing = true
## 現在再生中のアニメーション名を返します(なければ空文字)
func get_current_animation() -> StringName:
return _current_clip.name if _current_clip != null else StringName("")
## 現在のアニメーションの再生速度(fps)を変更します。
func set_speed(fps: float) -> void:
if _current_clip == null:
return
_current_clip.fps = fps
## 内部的に保持しているアニメクリップを検索
func _find_clip(anim_name: StringName) -> SpriteAnimClip:
for clip in animations:
if clip.name == anim_name:
return clip
return null
使い方の手順
手順①:スプライトシートを設定する
まずは Sprite2D にスプライトシートを設定します。
Sprite2Dにテクスチャを設定- インスペクタの
hframes/vframesをスプライトシートに合わせて設定- 例: 横4コマ・縦2コマなら
hframes = 4,vframes = 2
- 例: 横4コマ・縦2コマなら
フレーム番号は「左上から右に向かって増え、次の行に折り返す」形式で割り当てられます。
- 0, 1, 2, 3
- 4, 5, 6, 7
手順②:SpriteAnimatorコンポーネントをアタッチ
プレイヤー例として、シーン構成図はこんな感じです。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── SpriteAnimator (Node)
SpriteAnimator.gdを SpriteAnimator ノードにアタッチspriteプロパティは空でもOK(親から自動取得)
敵キャラでも動く床でも、同じように「Sprite2Dの横にちょこんと置く」だけでOKです。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── SpriteAnimator (Node) MovingPlatform (StaticBody2D) ├── Sprite2D ├── CollisionShape2D └── SpriteAnimator (Node)
手順③:アニメーション定義をインスペクタで設定
SpriteAnimator ノードを選択すると、インスペクタに animations 配列が見えます。
animationsに要素を追加- 各要素(
SpriteAnimClip)に以下を設定- name:
"idle"/"run"など - frames: 使用するフレーム番号の配列
- 例: 待機モーション:
[0, 1, 2, 3] - 例: 走りモーション:
[4, 5, 6, 7]
- 例: 待機モーション:
- fps: 再生速度(例: 8.0)
- loop: ループするなら
true
- name:
autoplayに"idle"などを指定すると、シーン開始時に自動再生
例(プレイヤー用):
animations[0]: name ="idle", frames =[0, 1, 2, 3], fps = 4, loop = trueanimations[1]: name ="run", frames =[4, 5, 6, 7], fps = 10, loop = true
手順④:コードからアニメを切り替える
プレイヤーの移動スクリプト例です。
extends CharacterBody2D
@onready var animator: SpriteAnimator = $SpriteAnimator
const SPEED := 200.0
func _physics_process(delta: float) -> void:
var input := Vector2.ZERO
input.x = Input.get_axis("ui_left", "ui_right")
velocity.x = input.x * SPEED
move_and_slide()
# アニメ切り替え
if abs(velocity.x) > 1.0:
animator.play("run", false) # すでに run 再生中ならリスタートしない
else:
animator.play("idle", false)
# 移動方向に応じて左右反転
if input.x != 0.0:
$Sprite2D.flip_h = input.x < 0.0
敵キャラの「出現 → 一回だけ点滅 → 待機」みたいなパターンも、AnimationPlayerなしでサクッと書けます。
extends CharacterBody2D
@onready var animator: SpriteAnimator = $SpriteAnimator
func _ready() -> void:
# 出現アニメを一回だけ再生してから、待機に切り替え
animator.play("spawn")
animator.animation_finished.connect(_on_anim_finished)
func _on_anim_finished(anim_name: StringName) -> void:
if anim_name == "spawn":
animator.play("idle")
メリットと応用
SpriteAnimator コンポーネントを使うメリットをまとめると、
- ノード階層が増えない:AnimationPlayerを各キャラに生やさなくてよい
- 合成しやすい:Sprite2Dの横に「アニメ機能」を足すだけで、どんなノードにも簡易アニメを付与できる
- データ駆動:アニメ定義はインスペクタの配列にまとめておけるので、レベルデザイナもいじりやすい
- コードで完結:トラックやキーをGUIでポチポチせず、フレーム配列とfpsだけで制御できる
たとえば、
- 敵の「ダメージ時だけ赤く点滅」アニメ
- スイッチがON/OFFするときのちょっとした動き
- コインがくるくる回るアニメ
など、「細かいけど大量にあるアニメ」ほど、コンポーネント化しておくと管理が圧倒的に楽になります。
同じ SpriteAnimator をコピペでどのシーンにも載せられるので、「歩く床」「歩く敵」「歩くNPC」みたいなものも、継承せずにアニメ機能だけ合成できます。
改造案:向きによってアニメを自動切り替え
例えば「左向き」と「右向き」で別スプライトを使いたい場合、play() をラップしたヘルパーを足しても良いですね。
## 方向付きアニメ再生のヘルパー
## dir_x > 0 なら "run_right"、dir_x < 0 なら "run_left" を再生するイメージ
func play_directional(base_name: StringName, dir_x: float, restart: bool = true) -> void:
if dir_x > 0.0:
play("%s_right" % base_name, restart)
elif dir_x < 0.0:
play("%s_left" % base_name, restart)
else:
play(base_name, restart)
これを使えば、プレイヤー側では単に animator.play_directional("run", velocity.x) と呼ぶだけでOKになります。
こうやって少しずつ、「自分のプロジェクト専用のアニメコンポーネント」に育てていくと、次のゲームでもそのまま再利用できて気持ちいいですね。
