Unityを触り始めた頃、つい Update() の中に「移動」「入力」「アニメ」「当たり判定」など、全部まとめて書いてしまいがちですよね。最初は動くので満足なのですが、時間が経つとこうなります。
- どの処理がどの機能なのか分かりづらい
- ちょっとした変更で別の挙動が壊れる
- 他のオブジェクトでも使いたいのに、コピペ地獄になる
特に「スプライトアニメーション」まわりは、Update() に
// フレーム時間を足して、一定時間ごとに sprite を差し替える…
// つい他の処理と混ざってカオスになる
といったコードを書きがちです。そこでこの記事では、
- 「スプライトアニメーションだけ」を責務に持つ小さなコンポーネント
- AnimationPlayer(旧Animator)を使わず、コードだけで簡単に制御
を実現する 「SpriteAnimator」コンポーネント を作っていきます。プレイヤーにも敵にも、動くギミックにもポン付けできる、シンプルなコンポーネント指向アプローチでいきましょう。
【Unity】コードだけでサクッとスプライトアニメ!「SpriteAnimator」コンポーネント
ここでは、AnimationPlayer(Animator)を使わず、SpriteRenderer の sprite を一定間隔で切り替えるだけの簡易アニメコンポーネントを実装します。
- フレーム配列と再生速度をインスペクターから設定
- ループ再生・一度だけ再生の切り替え
- 再生 / 一時停止 / 停止(先頭フレームに戻す)をコードから制御
といった最低限だけど汎用性の高い機能に絞ることで、コンポーネントとしての責務を明確にしています。
フルコード
using UnityEngine;
/// <summary>
/// SpriteRenderer の sprite を一定間隔で切り替える簡易アニメーター。
/// AnimationPlayer(Animator)を使わず、コードだけで制御したいとき用。
/// </summary>
[RequireComponent(typeof(SpriteRenderer))]
public class SpriteAnimator : MonoBehaviour
{
// ==== アニメーション設定 ====
[Header("アニメーション設定")]
[Tooltip("アニメーションに使用するスプライトの配列(左から右に並べたフレーム順など)")]
[SerializeField] private Sprite[] frames = default;
[Tooltip("1秒間に何フレーム進めるか(FPS)。例えば 8 にすると 1秒で8枚進む。")]
[SerializeField] private float framesPerSecond = 8f;
[Tooltip("最後のフレームまで再生したら先頭に戻ってループするかどうか")]
[SerializeField] private bool loop = true;
[Tooltip("再生開始時に自動的に再生するかどうか")]
[SerializeField] private bool playOnAwake = true;
// ==== 状態管理 ====
private SpriteRenderer spriteRenderer;
// 現在のフレームインデックス(0~frames.Length-1)
private int currentFrameIndex = 0;
// 経過時間を蓄積して、一定時間ごとにフレームを進める
private float timer = 0f;
// 再生中かどうか
private bool isPlaying = false;
// 1フレームあたりの時間(秒)
private float SecondsPerFrame
{
get
{
// FPSが0以下にならないようにガード
if (framesPerSecond <= 0f)
{
return float.MaxValue; // 実質止まる
}
return 1f / framesPerSecond;
}
}
private void Awake()
{
// 必須コンポーネントをキャッシュ
spriteRenderer = GetComponent<SpriteRenderer>();
// 初期フレームを反映
ApplyFrame(currentFrameIndex);
// playOnAwake が true のときは自動再生
if (playOnAwake)
{
Play();
}
}
private void Update()
{
// 再生中でない場合は何もしない
if (!isPlaying)
{
return;
}
// フレームが設定されていない場合も何もしない
if (frames == null || frames.Length == 0)
{
return;
}
// 経過時間を足す
timer += Time.deltaTime;
// 1フレーム分の時間を超えたら次のフレームへ
while (timer >= SecondsPerFrame)
{
timer -= SecondsPerFrame;
AdvanceFrame();
}
}
/// <summary>
/// 再生を開始(または再開)する。
/// </summary>
public void Play()
{
// フレームが無い場合は再生しても意味がないので止める
if (frames == null || frames.Length == 0)
{
isPlaying = false;
return;
}
isPlaying = true;
}
/// <summary>
/// 再生を一時停止する(フレーム位置は保持)。
/// </summary>
public void Pause()
{
isPlaying = false;
}
/// <summary>
/// 再生を停止し、先頭フレームに戻す。
/// </summary>
public void Stop()
{
isPlaying = false;
timer = 0f;
currentFrameIndex = 0;
ApplyFrame(currentFrameIndex);
}
/// <summary>
/// 任意のフレームインデックスを設定して、そのフレームを表示する。
/// 範囲外のインデックスが渡された場合はクランプする。
/// </summary>
public void SetFrame(int frameIndex)
{
if (frames == null || frames.Length == 0)
{
return;
}
// 0 ~ frames.Length-1 にクランプ
currentFrameIndex = Mathf.Clamp(frameIndex, 0, frames.Length - 1);
ApplyFrame(currentFrameIndex);
}
/// <summary>
/// 現在のフレームインデックスを返す。
/// </summary>
public int GetCurrentFrameIndex()
{
return currentFrameIndex;
}
/// <summary>
/// フレーム配列を差し替える(ランタイムにアニメ切り替えしたい場合など)。
/// 差し替え後はインデックスをリセットして先頭フレームを表示する。
/// </summary>
public void SetFrames(Sprite[] newFrames, bool autoPlay = true)
{
frames = newFrames;
// 何も入っていない場合は止める
if (frames == null || frames.Length == 0)
{
Stop();
return;
}
currentFrameIndex = 0;
timer = 0f;
ApplyFrame(currentFrameIndex);
if (autoPlay)
{
Play();
}
}
/// <summary>
/// ループ再生のオン/オフを切り替える。
/// </summary>
public void SetLoop(bool shouldLoop)
{
loop = shouldLoop;
}
/// <summary>
/// 内部的に1フレーム進める処理。
/// ループ設定や最後のフレーム到達時の挙動をここで制御する。
/// </summary>
private void AdvanceFrame()
{
if (frames == null || frames.Length == 0)
{
return;
}
currentFrameIndex++;
// 最後のフレームを超えたかどうか
if (currentFrameIndex >= frames.Length)
{
if (loop)
{
// ループの場合は先頭に戻る
currentFrameIndex = 0;
}
else
{
// ループしない場合は最後のフレームで止める
currentFrameIndex = frames.Length - 1;
isPlaying = false;
}
}
ApplyFrame(currentFrameIndex);
}
/// <summary>
/// 指定したインデックスのスプライトを SpriteRenderer に適用する。
/// </summary>
private void ApplyFrame(int frameIndex)
{
if (spriteRenderer == null)
{
return;
}
if (frames == null || frames.Length == 0)
{
return;
}
// インデックスが範囲外にならないようにクランプ
frameIndex = Mathf.Clamp(frameIndex, 0, frames.Length - 1);
spriteRenderer.sprite = frames[frameIndex];
}
}
使い方の手順
ここからは、実際にこの SpriteAnimator をシーンで使う手順を見ていきましょう。例として「プレイヤーキャラ」「敵キャラ」「動く床」の3パターンを挙げます。
手順①:スプライトシートを分割してフレームを用意する
- プロジェクトに2D用のスプライトシート(歩きアニメなど)をインポートします。
- インポートしたテクスチャを選択し、Sprite Mode を Multiple に変更します。
- Sprite Editor を開き、Grid By Cell Size などでコマ割りして Apply します。
- Project ウィンドウでテクスチャを展開すると、分割された複数の Sprite アセットが見えるはずです。
これらの Sprite をそのまま SpriteAnimator の frames にドラッグ&ドロップして使います。
手順②:ゲームオブジェクトに SpriteRenderer と SpriteAnimator を付ける
- Hierarchy で右クリック → 2D Object > Sprite から新しいオブジェクトを作成(例:
Player)。 - 自動で
SpriteRendererが付くので、念のため確認します。 - Add Component ボタンから
SpriteAnimatorを追加します。 - インスペクターで
Frames配列を展開し、先ほど分割した Sprite を順番に入れていきます(歩きアニメなら、歩行1 → 2 → 3 → 4 のように順番に)。 Frames Per Secondを 8〜12 くらいに設定すると、自然な歩きアニメになります。- ループ再生したい場合は
Loopにチェックを入れます(デフォルトでオン)。 - シーン再生と同時にアニメを始めたい場合は
Play On Awakeをオンにしておきます。
手順③:プレイヤーに使う(移動スクリプトと分離)
プレイヤーキャラに「移動処理」と「アニメ処理」を分けたい場合は、
PlayerMover:入力を読んで Rigidbody2D を動かすSpriteAnimator:見た目のアニメだけを担当
という構成にするとスッキリします。例えば:
using UnityEngine;
/// <summary>
/// 簡単なプレイヤー横移動。アニメ制御は SpriteAnimator に委譲する例。
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerMover : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private SpriteAnimator walkAnimator = default;
private Rigidbody2D rb;
private float inputX;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
}
private void Update()
{
// キーボードの左右入力(-1 ~ 1)
inputX = Input.GetAxisRaw("Horizontal");
// 入力があるときだけアニメ再生、止まったら停止して先頭フレームに戻す
if (walkAnimator != null)
{
if (Mathf.Abs(inputX) > 0.01f)
{
walkAnimator.Play();
}
else
{
walkAnimator.Stop();
}
}
}
private void FixedUpdate()
{
// 物理挙動は FixedUpdate で
Vector2 velocity = rb.velocity;
velocity.x = inputX * moveSpeed;
rb.velocity = velocity;
}
}
このように、移動とアニメを別コンポーネントに分けることで、あとから
- アニメだけ差し替えたい
- 移動ロジックだけ別のキャラにも使い回したい
といったときにも柔軟に対応できます。
手順④:敵や動く床にもポン付けで使う
敵キャラの場合:
- 敵の GameObject に
SpriteAnimatorを追加 - 「待機アニメ」「攻撃アニメ」など、別の
Sprite[]を用意しておき、AIスクリプトからSetFrames()で切り替える
例えば、敵がプレイヤーを見つけたら攻撃アニメに切り替えるコードはこんな感じです。
using UnityEngine;
/// <summary>
/// 敵の状態に応じて SpriteAnimator のフレームを切り替える簡易例。
/// </summary>
public class EnemyAnimationController : MonoBehaviour
{
[SerializeField] private SpriteAnimator animator = default;
[SerializeField] private Sprite[] idleFrames = default;
[SerializeField] private Sprite[] attackFrames = default;
// ここでは単純に Inspector から切り替えるだけの例
[SerializeField] private bool isAttacking = false;
private void Start()
{
// 初期状態は待機アニメ
if (animator != null)
{
animator.SetFrames(idleFrames, autoPlay: true);
}
}
private void Update()
{
if (animator == null) return;
// isAttacking のフラグを見てアニメを切り替える
if (isAttacking)
{
animator.SetFrames(attackFrames, autoPlay: true);
}
else
{
animator.SetFrames(idleFrames, autoPlay: true);
}
}
}
動く床(ギミック)の場合:
- 「光っている」「消えている」状態をスプライトアニメで表現
- トリガーに入ったら
Play()、出たらStop()など、イベントと紐付ける
イベントとアニメを結びつける処理は、別コンポーネントにまとめておくとより見通しが良くなります。
メリットと応用
SpriteAnimator を使うことで、次のようなメリットがあります。
- プレハブがシンプルになる
アニメーション制御はSpriteAnimatorに閉じ込められているので、プレハブ側では「どのフレームを使うか」「何FPSで回すか」だけを設定すればOKです。 - レベルデザイン時に「見た目の動き」を簡単に付けられる
動く床・トラップ・背景の看板など、ちょっとした動きをつけたいオブジェクトにSpriteAnimatorを追加してスプライトを入れるだけで、すぐに「動いている感」を出せます。 - 責務が分離されるので、保守しやすい
アニメーションのロジックがゲームロジックから切り離されているため、「アニメの速さを変えたい」「ループせず一度だけ再生したい」といった変更はSpriteAnimatorだけを見れば済みます。 - アニメーションシステムに縛られない
AnimationPlayer(Animator)のステートマシンを組むほどではない、でも静止画だけだと寂しい、という2Dプロトタイピングに非常に向いています。
改造案:Ping-Pong(往復)再生を追加する
例えば、フレームを「0 → 1 → 2 → 1 → 0 → 1 → 2 → …」と往復させたいときがあります。
その場合は、SpriteAnimator に「再生方向」を持たせて、AdvanceFrame() を少し改造してみましょう。
// SpriteAnimator 内に追加するフィールド
[Tooltip("フレームを 0→1→2→1→0 のように往復させるかどうか")]
[SerializeField] private bool pingPong = false;
// 再生方向(1:正方向, -1:逆方向)
private int direction = 1;
/// <summary>
/// Ping-Pong 対応版のフレーム進行処理。
/// 既存の AdvanceFrame() と置き換えて使う。
/// </summary>
private void AdvanceFrame()
{
if (frames == null || frames.Length == 0)
{
return;
}
// Ping-Pong 再生のとき
if (pingPong && frames.Length > 1)
{
currentFrameIndex += direction;
// 端に到達したら方向を反転
if (currentFrameIndex >= frames.Length - 1)
{
currentFrameIndex = frames.Length - 1;
direction = -1;
if (!loop)
{
isPlaying = false;
}
}
else if (currentFrameIndex <= 0)
{
currentFrameIndex = 0;
direction = 1;
if (!loop)
{
isPlaying = false;
}
}
}
else
{
// 通常の一方向ループ
currentFrameIndex++;
if (currentFrameIndex >= frames.Length)
{
if (loop)
{
currentFrameIndex = 0;
}
else
{
currentFrameIndex = frames.Length - 1;
isPlaying = false;
}
}
}
ApplyFrame(currentFrameIndex);
}
このように、小さなコンポーネントとして作っておくと、「こう動かしたいな」と思ったときにピンポイントで改造しやすいのが大きな利点です。
ぜひ、自分のプロジェクトに合わせて機能を足したり、InputSystemと組み合わせたりして、コンポーネント指向のアニメ管理を育てていってみてください。




