Godot 4で2Dアニメーションを作るとき、多くの人がまず AnimatedSprite2DAnimationPlayer を使いますよね。もちろんそれでOKなのですが、

  • 「とりあえず歩きモーションだけサクッと作りたい」
  • 「敵やギミック用の細かいアニメを大量に仕込むのが面倒」
  • 「AnimationPlayerのトラック管理がつらくなってきた」

みたいなタイミングで、だんだん アニメーション用ノードが増えすぎる問題 にぶつかります。
プレイヤー、敵、ギミックそれぞれに AnimationPlayer を生やして、トラックやキーをちまちま編集して…というのは、シーンが大きくなるほど管理がつらくなっていきます。

そこで今回は、AnimationPlayerを一切使わずSprite2D の frame をコードで切り替えるだけのシンプルなコンポーネント、SpriteAnimator を用意しました。

「歩き」「待機」「攻撃」みたいな簡易アニメなら、このコンポーネントをアタッチするだけでOKにしてしまいましょう。
ノード階層を増やさず、継承よりもコンポーネント追加でアニメ機能を合成するスタイルですね。


【Godot 4】コードだけでサクッとフレーム切替!「SpriteAnimator」コンポーネント

今回の SpriteAnimator は、

  • Sprite2Dhframes/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 にスプライトシートを設定します。

  1. Sprite2D にテクスチャを設定
  2. インスペクタの hframes / vframes をスプライトシートに合わせて設定
    • 例: 横4コマ・縦2コマなら hframes = 4, vframes = 2

フレーム番号は「左上から右に向かって増え、次の行に折り返す」形式で割り当てられます。

  • 0, 1, 2, 3
  • 4, 5, 6, 7

手順②:SpriteAnimatorコンポーネントをアタッチ

プレイヤー例として、シーン構成図はこんな感じです。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpriteAnimator (Node)
  1. SpriteAnimator.gdSpriteAnimator ノードにアタッチ
  2. sprite プロパティは空でもOK(親から自動取得)

敵キャラでも動く床でも、同じように「Sprite2Dの横にちょこんと置く」だけでOKです。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpriteAnimator (Node)

MovingPlatform (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpriteAnimator (Node)

手順③:アニメーション定義をインスペクタで設定

SpriteAnimator ノードを選択すると、インスペクタに animations 配列が見えます。

  1. animations に要素を追加
  2. 各要素(SpriteAnimClip)に以下を設定
    • name: "idle" / "run" など
    • frames: 使用するフレーム番号の配列
      • 例: 待機モーション: [0, 1, 2, 3]
      • 例: 走りモーション: [4, 5, 6, 7]
    • fps: 再生速度(例: 8.0)
    • loop: ループするなら true
  3. autoplay"idle" などを指定すると、シーン開始時に自動再生

例(プレイヤー用):

  • animations[0]: name = "idle", frames = [0, 1, 2, 3], fps = 4, loop = true
  • animations[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になります。
こうやって少しずつ、「自分のプロジェクト専用のアニメコンポーネント」に育てていくと、次のゲームでもそのまま再利用できて気持ちいいですね。