【Cocos Creator 3.8】SpriteAnimatorの実装:アタッチするだけでスプライトシートのフレームアニメを再生できる汎用スクリプト
このガイドでは、Animation / AnimationClip を一切使わず、Sprite の SpriteFrame をコードで切り替えるだけの軽量なアニメーションコンポーネント「SpriteAnimator」を実装します。
任意のノードにアタッチして、インスペクタからフレーム画像と再生設定を行うだけで、キャラクターの歩き・待機・エフェクトなどの簡易アニメーションを実現できます。
コンポーネントの設計方針
目的と要件
- AnimationPlayer / AnimationClip を使わない簡易アニメーション。
- Sprite の SpriteFrame を一定間隔で切り替えてアニメーションを表現する。
- 他のノードやカスタムスクリプトに一切依存しない(このコンポーネント単体で完結)。
- インスペクタから以下を柔軟に設定できる:
- 使用するフレーム一覧
- フレームレート / フレーム間隔
- ループ再生の有無
- 再生方向(順方向・逆再生)
- 開始時に自動再生するか
- 一度だけ再生 / 指定回数で停止
- ランダム開始フレーム
- 防御的実装:
- アタッチ先ノードに
Spriteコンポーネントがない場合はエラーログを出す。 - フレーム配列が空の場合も警告を出し、
updateでは何もしない。
- アタッチ先ノードに
インスペクタで設定可能なプロパティ
SpriteAnimator コンポーネントのプロパティ設計は以下の通りです。
@property([SpriteFrame]) frames- アニメーションに使用する SpriteFrame の配列。
- インスペクタでドラッグ&ドロップして順番に並べる。
- この配列の順番がアニメーションの再生順になる。
@property({ type: CCFloat }) frameRate- 1秒あたりのフレーム数(FPS)。
- 例:12 にすると毎秒12コマで再生。
- 内部では
frameInterval = 1 / frameRate秒でフレームを切り替える。
@property loop- ループ再生するかどうか。
- オン:最後のフレームの次は最初のフレームに戻る。
- オフ:最後のフレームまで再生したら停止。
@property autoPlayOnStart- start 時に自動で再生を開始するか。
- 多くのケースではオンでよい。
@property playOnAwake- onLoad(有効化直後)で再生を開始するか。
autoPlayOnStartより早く再生したい特殊なケース用。- 通常は
falseで問題ない。
@property reverse- 逆再生するかどうか。
- オン:フレーム配列の最後から先頭に向かって再生。
- ループ時も逆方向のままループする。
@property randomStartFrame- 再生開始フレームをランダムにするかどうか。
- 同じアニメを複数体に付けて、開始タイミングをずらしたいときに便利。
@property({ type: CCInteger }) playTimes- 再生回数の上限。
0:無制限(ループ設定に従う)。1:一度だけ再生して停止。2以上:指定回数ループしたら停止。- ループ OFF で
1にすると、「1回だけ最後まで再生して停止」と同じ動き。
@property pauseOnDisable- ノードまたはコンポーネントが
disabledになったときに再生を一時停止するか。 - 有効にしておくと、UI の表示非表示に連動してアニメーションも止まる。
- ノードまたはコンポーネントが
@property resumeOnEnablepauseOnDisableが有効なとき、enabledに戻ったら自動で再開するか。
また、コード内でのみ扱う内部状態として以下を持ちます(インスペクタには表示しない):
_sprite: アタッチ先ノードのSprite参照。_isPlaying: 現在再生中かどうか。_elapsed: 前回フレーム更新からの経過時間。_currentFrameIndex: 現在のフレーム番号。_playedCount: 何回ループし終えたか。
TypeScriptコードの実装
以下が完成版の SpriteAnimator.ts です。
import { _decorator, Component, Sprite, SpriteFrame, CCFloat, CCInteger, log, warn, error } from 'cc';
const { ccclass, property } = _decorator;
/**
* SpriteAnimator
* Sprite の spriteFrame を一定間隔で切り替えて簡易アニメーションを実現するコンポーネント。
* 他のノードやスクリプトに依存せず、このコンポーネント単体で完結します。
*/
@ccclass('SpriteAnimator')
export class SpriteAnimator extends Component {
@property({
type: [SpriteFrame],
tooltip: 'アニメーションに使用する SpriteFrame の配列。\n要素の順番が再生順になります。'
})
public frames: SpriteFrame[] = [];
@property({
type: CCFloat,
tooltip: '1秒あたりのフレーム数(FPS)。\n例: 12 にすると毎秒12コマで再生します。'
})
public frameRate: number = 12;
@property({
tooltip: 'true の場合、最後のフレームの次は最初のフレームに戻ってループ再生します。'
})
public loop: boolean = true;
@property({
tooltip: 'コンポーネントが onLoad された時点で自動的に再生を開始します。'
})
public playOnAwake: boolean = false;
@property({
tooltip: 'start が呼ばれたタイミングで自動的に再生を開始します。'
})
public autoPlayOnStart: boolean = true;
@property({
tooltip: 'true の場合、フレーム配列の最後から先頭に向かって逆再生します。'
})
public reverse: boolean = false;
@property({
tooltip: 'true の場合、再生開始時のフレームインデックスをランダムにします。'
})
public randomStartFrame: boolean = false;
@property({
type: CCInteger,
tooltip: 'アニメーションの再生回数。\n0: 無制限(ループ設定に従う)\n1: 一度だけ再生して停止\n2以上: 指定回数ループしたら停止します。'
})
public playTimes: number = 0;
@property({
tooltip: 'ノードまたはコンポーネントが無効化されたときに、アニメーションを一時停止します。'
})
public pauseOnDisable: boolean = true;
@property({
tooltip: 'pauseOnDisable が true の場合に、再度有効化されたとき自動的に再開します。'
})
public resumeOnEnable: boolean = true;
// ---- 内部状態(インスペクタには表示しない) ----
private _sprite: Sprite | null = null;
private _isPlaying: boolean = false;
private _elapsed: number = 0;
private _currentFrameIndex: number = 0;
private _playedCount: number = 0; // ループ完了回数
onLoad() {
this._sprite = this.getComponent(Sprite);
if (!this._sprite) {
error('[SpriteAnimator] このコンポーネントを使用するには、同じノードに Sprite コンポーネントが必要です。');
}
// frameRate の防御的チェック
if (this.frameRate <= 0) {
warn('[SpriteAnimator] frameRate が 0 以下です。1 に補正します。');
this.frameRate = 1;
}
// 初期フレームを設定
this._resetInternalState(true);
if (this.playOnAwake) {
this.play();
}
}
start() {
// onLoad で既に playOnAwake によって再生中の場合は何もしない
if (this.autoPlayOnStart && !this._isPlaying) {
this.play();
}
}
onEnable() {
if (this.pauseOnDisable && this.resumeOnEnable && !this._isPlaying) {
// 無効化中に停止していた場合、再開する
this.play();
}
}
onDisable() {
if (this.pauseOnDisable) {
this.pause();
}
}
update(deltaTime: number) {
if (!this._isPlaying) {
return;
}
if (!this._sprite) {
// Sprite がない場合は何もできないので停止
this._isPlaying = false;
return;
}
if (!this.frames || this.frames.length === 0) {
// フレームが設定されていない場合も何もしない
return;
}
this._elapsed += deltaTime;
const interval = 1 / this.frameRate;
// 経過時間がフレーム間隔を超えたら次のフレームへ
while (this._elapsed >= interval) {
this._elapsed -= interval;
this._stepFrame();
}
}
/**
* アニメーションを再生開始(または再開)します。
*/
public play(reset: boolean = false) {
if (!this._sprite) {
error('[SpriteAnimator] Sprite コンポーネントが見つからないため、再生できません。');
return;
}
if (!this.frames || this.frames.length === 0) {
warn('[SpriteAnimator] frames が空です。アニメーションを再生できません。');
return;
}
if (reset) {
this._resetInternalState(true);
}
this._isPlaying = true;
}
/**
* アニメーションを一時停止します。
*/
public pause() {
this._isPlaying = false;
}
/**
* アニメーションを停止し、内部状態もリセットします。
*/
public stop() {
this._isPlaying = false;
this._resetInternalState(false);
}
/**
* 現在のフレームインデックスを返します。
*/
public getCurrentFrameIndex(): number {
return this._currentFrameIndex;
}
/**
* 現在のフレームを任意のインデックスに変更します。
* @param index 変更したいフレームインデックス
*/
public setCurrentFrameIndex(index: number) {
if (!this.frames || this.frames.length === 0) {
return;
}
const len = this.frames.length;
// 範囲外を防御的に補正
const clamped = Math.max(0, Math.min(len - 1, index));
this._currentFrameIndex = clamped;
this._applyFrame();
}
/**
* 内部状態のリセット処理。
*/
private _resetInternalState(randomizeStart: boolean) {
this._elapsed = 0;
this._playedCount = 0;
if (this.frames && this.frames.length > 0) {
if (randomizeStart && this.randomStartFrame) {
this._currentFrameIndex = Math.floor(Math.random() * this.frames.length);
} else {
this._currentFrameIndex = this.reverse ? this.frames.length - 1 : 0;
}
this._applyFrame();
}
}
/**
* 1フレーム進める(または戻す)処理。
*/
private _stepFrame() {
if (!this.frames || this.frames.length === 0) {
return;
}
const len = this.frames.length;
let nextIndex = this._currentFrameIndex + (this.reverse ? -1 : 1);
// 終端に到達したかどうかを判定
const reachedEnd = this.reverse ? (nextIndex < 0) : (nextIndex >= len);
if (reachedEnd) {
// 1ループ完了
this._playedCount++;
// playTimes が 0 以外で、指定回数に達していたら停止
if (this.playTimes > 0 && this._playedCount >= this.playTimes) {
// 最終フレームに固定
this._currentFrameIndex = this.reverse ? 0 : len - 1;
this._applyFrame();
this._isPlaying = false;
return;
}
if (this.loop) {
// ループする場合は先頭(または末尾)に戻す
this._currentFrameIndex = this.reverse ? (len - 1) : 0;
} else {
// ループしない場合は最終フレームで停止
this._currentFrameIndex = this.reverse ? 0 : len - 1;
this._applyFrame();
this._isPlaying = false;
return;
}
} else {
// 終端に達していないので普通にインデックスを進める
this._currentFrameIndex = nextIndex;
}
this._applyFrame();
}
/**
* 現在のフレームインデックスに対応する SpriteFrame を Sprite に適用します。
*/
private _applyFrame() {
if (!this._sprite) {
return;
}
if (!this.frames || this.frames.length === 0) {
return;
}
const frame = this.frames[this._currentFrameIndex];
if (!frame) {
warn(`[SpriteAnimator] frames[${this._currentFrameIndex}] が null です。`);
return;
}
this._sprite.spriteFrame = frame;
}
}
コードのポイント解説
- onLoad
SpriteコンポーネントをgetComponent(Sprite)で取得。- 見つからない場合は
errorを出して利用者に明示的に知らせる。 frameRateが 0 以下の場合は1に補正。_resetInternalState(true)で初期フレームを設定。playOnAwakeが true なら即座にplay()を呼ぶ。
- start
autoPlayOnStartが true かつまだ再生していない場合にplay()。playOnAwakeと両方 true の場合は onLoad の再生が優先される。
- update
_isPlayingが false のときは何もしない。framesが空なら何もしない。_elapsedにdeltaTimeを加算し、frameRateから算出したintervalを超えたら_stepFrame()を呼ぶ。whileループで、低FPSでも時間分だけフレームを進めるようにしている。
- _stepFrame
reverseに応じて次のインデックスを計算。- 終端に達したら
_playedCountを増やし、playTimesをチェック。 loopが true なら先頭(または末尾)に戻す。loopが false なら最終フレームで停止し、_isPlaying = false。
- _resetInternalState
- 経過時間やループ回数をリセット。
randomStartFrameが true なら 0〜frames.length-1からランダムに開始フレームを決定。- 逆再生時はデフォルト開始フレームを末尾にする。
- 公開メソッド
play(reset?: boolean):再生開始。引数trueで内部状態もリセット。pause():一時停止。stop():停止+内部状態リセット。getCurrentFrameIndex()/setCurrentFrameIndex():現在フレームの取得・変更。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選び、ファイル名を
SpriteAnimator.tsにします。 - 自動生成されたテンプレートコードをすべて削除し、本記事の
SpriteAnimatorのコードを丸ごと貼り付けて保存します。
2. テスト用スプライトノードの作成
- Hierarchy パネルで右クリックし、Create → 2D Object → Sprite を選択します。
- 作成されたノード名を分かりやすく
TestSpriteなどに変更しておきます。 - Inspector で、このノードに Sprite コンポーネントが付いていることを確認します。
- もし付いていなければ、Add Component → Renderer → Sprite で追加してください。
- Sprite コンポーネントの SpriteFrame に、任意の画像(スプライトシートから切り出した1コマ)を設定しておきます(後で差し替えられます)。
3. SpriteAnimator コンポーネントのアタッチ
- TestSprite ノードを選択した状態で、Inspector の一番下にある Add Component ボタンをクリックします。
- Custom Component(または Custom)の中から SpriteAnimator を選択します。
- 見つからない場合は、スクリプトの保存と TypeScript のコンパイルが終わっているか確認し、エディタを一度フォーカスし直してください。
4. アニメーション用 SpriteFrame の準備
事前に以下のような手順でフレーム画像を用意しておきます。
- Assets パネルに、アニメーション用のスプライトシート画像をインポート
- スプライトシートを選択し、Inspector → SpriteFrame の設定から Sprite Editor を開きます。
- 必要に応じて Auto Slice などでコマごとに分割して、複数の
SpriteFrameを生成します。 - Assets パネル上で、分割後の
SpriteFrame(例:walk_0,walk_1,walk_2…)が一覧できる状態にしておきます。
5. SpriteAnimator のプロパティ設定
TestSprite ノードの SpriteAnimator コンポーネントを選択し、以下のように設定します。
- Frames
- サイズ欄の「+」ボタンを押して、アニメーションに使用するフレーム数だけ要素を増やします(例:4コマなら「Size: 4」)。
- Assets パネルから、各
SpriteFrame(例:walk_0〜walk_3)を、配列の要素スロットにドラッグ&ドロップします。 - 並び順がそのまま再生順になるので、
walk_0,walk_1,walk_2,walk_3の順に並べてください。
- Frame Rate
- 例として
12を入力します。毎秒12フレームのアニメーションになります。 - より滑らかにしたい場合は 24〜30、ゆっくり見せたい場合は 6〜8 などに調整してください。
- 例として
- Loop
- 常にループさせたい場合は チェックをオンにします。
- 1回だけ再生して止めたいエフェクトなどでは オフにします。
- Play On Awake
- 通常は
falseのままで構いません。 - ノードがアクティブになった瞬間から即座に再生したい場合は オンにします。
- 通常は
- Auto Play On Start
- シーン開始時に自動再生したい場合は オン(デフォルト)。
- コードから明示的に
play()を呼び出して制御したい場合は オフにします。
- Reverse
- 逆再生したい場合のみ オンにします。
- 通常は
falseのままで問題ありません。
- Random Start Frame
- 同じアニメを複数体に付けて、開始タイミングをずらしたいときは オンにします。
- 1体だけのテストでは
falseのままで構いません。
- Play Times
- ループ回数を制限しない場合は
0のままにします。 - 1回だけ再生して停止したい場合は
1を設定し、Loop をオフにするのがおすすめです。 - 2回だけループさせたい場合は
2を設定します。
- ループ回数を制限しない場合は
- Pause On Disable / Resume On Enable
- UI の表示非表示に合わせてアニメーションも止めたい場合は両方 オン。
- ノードの有効 / 無効に関係なくアニメを進めたい場合は Pause On Disable をオフにします。
6. 再生確認
- シーンを保存し、エディタ右上の ▶(再生ボタン) でプレビューを開始します。
- シーンが再生されると、TestSprite の画像が設定したフレーム順にパラパラと切り替わり、アニメーションしていることを確認できます。
- 再生中に Inspector から
frameRateやloopを変更すると、その場で挙動が変化するので、好みの速度・ループ設定を探ってみてください。
7. 応用的な使い方の例
- ダメージエフェクト
- 爆発やヒットエフェクトなど、一度だけ再生して消えるアニメに最適。
- 設定例:
frames: 爆発の連番フレームframeRate: 20〜30loop: falseplayTimes: 1
- 待機モーション
- キャラクターの idle アニメなど、常時ループさせたいモーションに。
- 設定例:
frames: idle の連番フレームframeRate: 8〜12loop: trueplayTimes: 0
- 複数体のランダムアニメ
- 同じアニメを持つ敵キャラを大量に配置しつつ、動きのタイミングをずらしたい場合。
randomStartFrameをオンにすることで、各個体の開始フレームがランダムになり、自然な見た目になります。
まとめ
SpriteAnimator コンポーネントは、
- AnimationPlayer を使わずに軽量なフレームアニメーションを実現できる
- Sprite コンポーネントさえあれば他に何もいらない完全独立スクリプト
- インスペクタからフレーム・速度・ループ・逆再生・再生回数などを柔軟に調整可能
という特徴を持っています。
シンプルな 2D ゲームでは、「大げさなアニメーションシステムを使うほどでもないけれど、ちょっとした動きをつけたい」という場面が頻出します。
そうしたケースで、この SpriteAnimator.ts をアタッチするだけで簡易アニメが完結するため、プロトタイピングから本番開発まで、使い回しやすい汎用コンポーネントとして活躍します。
このままでも十分実用的ですが、必要に応じて「イベントコールバック(再生完了時に通知)」や「速度のランタイム変更 UI」といった機能を追加していくことで、自分のプロジェクトに最適化されたアニメーション制御コンポーネントへと発展させていけるはずです。
