【Cocos Creator】アタッチするだけ!SquashStretch (伸縮演出)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】SquashStretch の実装:アタッチするだけでジャンプ・着地時の「伸び縮み演出」を実現する汎用スクリプト

キャラクターがジャンプする瞬間に「にゅっ」と縦に伸び、着地した瞬間に「ぺたん」と横に潰れる——2Dアニメやリッチなゲームでよく見る「スquash & Stretch(伸縮)」演出を、Cocos Creator 3.8 で簡単に使い回せるコンポーネントとして実装します。

この SquashStretch コンポーネントをノードにアタッチしておけば、あとはスクリプト経由で「ジャンプ開始」「着地」などのトリガーを呼ぶだけで、柔らかい伸縮アニメーションを自動再生できます。外部の GameManager などに一切依存せず、インスペクタで数値を調整するだけでチューニング可能です。


コンポーネントの設計方針

1. 機能要件の整理

  • ノードにアタッチして使う 単体完結 のコンポーネント。
  • 「ジャンプ開始」「着地」などのタイミングで scale を一時的に変化させ、時間経過で元のスケールに戻す。
  • アニメーションは 補間(イージング)付き で、カクつかない自然な伸縮にする。
  • 演出の強さ・時間・方向などは インスペクタのプロパティ から調整できる。
  • 他のカスタムスクリプトやシングルトンに依存しない。
  • 防御的実装:必要な前提(初期スケールなど)を onLoad で安全に確保し、想定外の値にはログやクランプで対処する。

2. 伸縮の基本パターン

このコンポーネントでは、代表的な 2 つのパターンをサポートします。

  • ジャンプ開始(Jump)
    Y方向に少し伸びX方向に少し潰れる → 元のスケールへ戻る。
  • 着地(Land)
    Y方向に少し潰れX方向に少し伸びる → 元のスケールへ戻る。

これを「プリセット」として持たせ、playJump() / playLand() というメソッドで再生できるようにします。また、任意のターゲットスケールを指定して再生する playCustom() も用意し、汎用性を高めます。

3. インスペクタで設定可能なプロパティ

以下のようなプロパティ群を設計します。

  • autoPlayOnStart: boolean
    – ゲーム開始時に自動で伸縮アニメーションを再生するかどうか。
    – デフォルト: false
  • autoPlayMode: 'Jump' | 'Land'(実装上は enum)
    autoPlayOnStart が有効な場合、どのプリセットを再生するか。
    – デフォルト: Jump
  • duration: number
    – 伸縮アニメーションの合計時間(秒)。
    – デフォルト: 0.18(短めのキビキビした動き)
  • overshoot: number
    – 伸縮の「強さ」。
    – 例:0.2 の場合、1.0 → 1.2 / 0.8 程度に変化。
    – デフォルト: 0.18
  • useUniformScale: boolean
    – true: X, Y を同じ比率で変化させる(丸いキャラなど)。
    – false: X, Y を逆方向に変化させる(横に潰れて縦に伸びる等)。
    – デフォルト: false
  • jumpScaleMultiplierX: number, jumpScaleMultiplierY: number
    – ジャンプ時の X/Y 方向倍率。
    – 例:1.1 / 0.9 なら「少し横に伸びて縦に縮む」。
    – デフォルト: X=0.9, Y=1.1
  • landScaleMultiplierX: number, landScaleMultiplierY: number
    – 着地時の X/Y 方向倍率。
    – 例:1.1 / 0.8 なら「横に潰れて縦に縮む」。
    – デフォルト: X=1.1, Y=0.8
  • allowInterrupt: boolean
    – 再生中に別の伸縮リクエストが来た場合、
    – true: 現在のアニメを中断して新しいものを開始。
    – false: 無視して最後まで再生。
    – デフォルト: true
  • debugLog: boolean
    – 動作ログをコンソールに出力するか。
    – デフォルト: false

4. 外部依存をなくすためのアプローチ

  • 伸縮のトリガーは このコンポーネント自身の public メソッド として提供します。
    • playJump()
    • playLand()
    • playCustom(targetScaleX, targetScaleY)

    他スクリプトから「GetComponent してメソッドを呼ぶ」だけで利用できます。

  • 内部状態(元のスケール)は onLoad で保存し、ノードの scale を直接操作します。
  • アニメーションは update() による時間制御で行い、Action, Tween, Animation など別コンポーネントには依存しません。

TypeScriptコードの実装


import { _decorator, Component, Node, Vec3, math } from 'cc';
const { ccclass, property } = _decorator;

/**
 * SquashStretch
 * ノードの scale を時間的に変化させて「伸縮」演出を行う汎用コンポーネント。
 * - 他のスクリプトやシングルトンに依存しない。
 * - public メソッドを呼び出すだけでジャンプ/着地演出を再生できる。
 */
enum SquashPresetMode {
    Jump = 0,
    Land = 1,
}

@ccclass('SquashStretch')
export class SquashStretch extends Component {

    @property({
        tooltip: 'true の場合、シーン開始時 (onStart) に自動で伸縮アニメーションを再生します。',
    })
    public autoPlayOnStart: boolean = false;

    @property({
        tooltip: 'autoPlayOnStart が true のとき、どちらのプリセットを再生するかを指定します。',
        type: Number,
    })
    public autoPlayMode: SquashPresetMode = SquashPresetMode.Jump;

    @property({
        tooltip: '伸縮アニメーション全体の再生時間(秒)。0.05〜1.0 程度を推奨します。',
        min: 0.01,
        max: 3.0,
        step: 0.01,
    })
    public duration: number = 0.18;

    @property({
        tooltip: '伸縮の強さを補正する係数。1.0 でプリセット通り、0.0 で変化なし、2.0 で約2倍の強さになります。',
        min: 0.0,
        max: 3.0,
        step: 0.05,
    })
    public overshoot: number = 1.0;

    @property({
        tooltip: 'true の場合、X/Y を同じ比率で変化させます。false の場合、プリセットの X/Y 倍率をそのまま使用します。',
    })
    public useUniformScale: boolean = false;

    @property({
        tooltip: 'ジャンプ時の X 方向スケール倍率(基準スケールに対する倍率)。',
        min: 0.1,
        max: 3.0,
        step: 0.05,
    })
    public jumpScaleMultiplierX: number = 0.9;

    @property({
        tooltip: 'ジャンプ時の Y 方向スケール倍率(基準スケールに対する倍率)。',
        min: 0.1,
        max: 3.0,
        step: 0.05,
    })
    public jumpScaleMultiplierY: number = 1.1;

    @property({
        tooltip: '着地時の X 方向スケール倍率(基準スケールに対する倍率)。',
        min: 0.1,
        max: 3.0,
        step: 0.05,
    })
    public landScaleMultiplierX: number = 1.1;

    @property({
        tooltip: '着地時の Y 方向スケール倍率(基準スケールに対する倍率)。',
        min: 0.1,
        max: 3.0,
        step: 0.05,
    })
    public landScaleMultiplierY: number = 0.8;

    @property({
        tooltip: 'true の場合、再生中でも新しい伸縮リクエストで上書きします。false の場合、再生中は新しいリクエストを無視します。',
    })
    public allowInterrupt: boolean = true;

    @property({
        tooltip: 'true にすると、伸縮開始/終了などのデバッグログをコンソールに出力します。',
    })
    public debugLog: boolean = false;

    // ---- 内部状態 ----

    /** 再生前の基準スケール(onLoad 時に取得) */
    private _baseScale: Vec3 = new Vec3(1, 1, 1);

    /** 現在再生中かどうか */
    private _isPlaying: boolean = false;

    /** 再生開始からの経過時間(秒) */
    private _elapsed: number = 0;

    /** アニメーション開始時のスケール */
    private _startScale: Vec3 = new Vec3(1, 1, 1);

    /** アニメーション中の目標スケール */
    private _targetScale: Vec3 = new Vec3(1, 1, 1);

    onLoad() {
        // 基準スケールを保存
        this._baseScale = this.node.scale.clone();

        // duration の防御的クランプ
        if (this.duration <= 0) {
            console.warn('[SquashStretch] duration が 0 以下だったため、0.1 に補正しました。');
            this.duration = 0.1;
        }

        if (this.debugLog) {
            console.log('[SquashStretch] onLoad baseScale =', this._baseScale);
        }
    }

    start() {
        // シーン開始時に自動再生する場合
        if (this.autoPlayOnStart) {
            if (this.autoPlayMode === SquashPresetMode.Jump) {
                this.playJump();
            } else {
                this.playLand();
            }
        }
    }

    update(deltaTime: number) {
        if (!this._isPlaying) {
            return;
        }

        this._elapsed += deltaTime;
        const t = math.clamp01(this._elapsed / this.duration);

        // イージング関数を適用(前半で素早く変化し、後半でゆっくり戻る)
        const easedT = this._easeOutBack(t);

        // 線形補間でスケールを更新
        const sx = math.lerp(this._startScale.x, this._targetScale.x, easedT);
        const sy = math.lerp(this._startScale.y, this._targetScale.y, easedT);
        const sz = math.lerp(this._startScale.z, this._targetScale.z, easedT);

        this.node.setScale(sx, sy, sz);

        if (t >= 1.0) {
            // アニメーション終了
            this._isPlaying = false;
            this._elapsed = 0;
            // 最終的には必ず基準スケールに戻す
            this.node.setScale(this._baseScale);

            if (this.debugLog) {
                console.log('[SquashStretch] animation finished, reset to baseScale =', this._baseScale);
            }
        }
    }

    // ---- パブリック API(他スクリプトから呼び出す想定) ----

    /**
     * ジャンプ時のプリセット伸縮を再生します。
     */
    public playJump(): void {
        const target = this._calculatePresetTargetScale(
            this.jumpScaleMultiplierX,
            this.jumpScaleMultiplierY
        );
        this._playInternal(target, 'Jump');
    }

    /**
     * 着地時のプリセット伸縮を再生します。
     */
    public playLand(): void {
        const target = this._calculatePresetTargetScale(
            this.landScaleMultiplierX,
            this.landScaleMultiplierY
        );
        this._playInternal(target, 'Land');
    }

    /**
     * 任意のターゲットスケールを指定して伸縮を再生します。
     * @param targetScaleX 目標 X スケール
     * @param targetScaleY 目標 Y スケール
     */
    public playCustom(targetScaleX: number, targetScaleY: number): void {
        const target = new Vec3(
            this._baseScale.x * targetScaleX,
            this._baseScale.y * targetScaleY,
            this._baseScale.z
        );
        this._playInternal(target, 'Custom');
    }

    /**
     * 再生中の伸縮アニメーションを即座に停止し、スケールを基準値に戻します。
     */
    public stopAndReset(): void {
        this._isPlaying = false;
        this._elapsed = 0;
        this.node.setScale(this._baseScale);

        if (this.debugLog) {
            console.log('[SquashStretch] stopAndReset called, reset to baseScale =', this._baseScale);
        }
    }

    // ---- 内部処理 ----

    /**
     * プリセット倍率と overshoot / useUniformScale を考慮したターゲットスケールを算出。
     */
    private _calculatePresetTargetScale(multX: number, multY: number): Vec3 {
        let x = multX;
        let y = multY;

        // overshoot を掛ける(1.0 でそのまま、2.0 で約2倍の変化)
        x = 1.0 + (x - 1.0) * this.overshoot;
        y = 1.0 + (y - 1.0) * this.overshoot;

        if (this.useUniformScale) {
            // X/Y の平均を取って同じ値にする
            const avg = (x + y) * 0.5;
            x = avg;
            y = avg;
        }

        return new Vec3(
            this._baseScale.x * x,
            this._baseScale.y * y,
            this._baseScale.z
        );
    }

    /**
     * 伸縮アニメーションの実際の再生処理。
     */
    private _playInternal(targetScale: Vec3, label: string): void {
        if (!this.allowInterrupt && this._isPlaying) {
            if (this.debugLog) {
                console.log('[SquashStretch] animation is already playing, new request ignored. label =', label);
            }
            return;
        }

        this._isPlaying = true;
        this._elapsed = 0;

        // 開始スケールは現在のスケール
        this._startScale = this.node.scale.clone();
        // ターゲットスケールは計算済みの値
        this._targetScale = targetScale.clone();

        if (this.debugLog) {
            console.log('[SquashStretch] play', label, 'from', this._startScale, 'to', this._targetScale);
        }
    }

    /**
     * 伸縮用のイージング関数(OutBack 系)。
     * t: 0〜1 → 0〜1
     */
    private _easeOutBack(t: number): number {
        // 一般的な easeOutBack 実装
        const c1 = 1.70158;
        const c3 = c1 + 1;

        return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
    }
}

コードの要点解説

  • onLoad()
    – ノードの現在の scale_baseScale として保存します。
    duration が 0 以下の場合は 0.1 に補正し、想定外の値で止まらないようにしています。
  • start()
    autoPlayOnStart が true のとき、シーン開始直後に playJump() または playLand() を呼び出し、自動で伸縮を再生します。
  • update(deltaTime)
    _isPlaying が true の間だけ動作します。
    – 経過時間 _elapsed を進め、t = elapsed / duration からイージングをかけた easedT を計算。
    _startScale_targetScale への線形補間で現在のスケールを更新します。
    t >= 1 になったら再生終了とし、スケールを _baseScale に戻します。
  • playJump() / playLand()
    – プリセット倍率(jumpScaleMultiplierX/Y, landScaleMultiplierX/Y)と overshoot, useUniformScale を考慮して目標スケールを計算し、_playInternal() を呼び出します。
  • playCustom(x, y)
    – 任意の倍率を受け取り、_baseScale に対する相対倍率として目標スケールを構築します。
  • allowInterrupt
    – 再生中に新しいリクエストが来た場合の挙動を制御。
    – false の場合、すでに _isPlaying が true なら新しいリクエストを無視します。
  • イージング関数 _easeOutBack()
    – 少し「行き過ぎて戻る」感じの動き(OutBack)を再現し、アニメーションらしい弾力を出しています。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタ左下の Assets パネルで、任意のフォルダ(例:assets/scripts)を右クリックします。
  2. Create > TypeScript を選択します。
  3. 新規スクリプトに SquashStretch.ts という名前を付けます。
  4. ダブルクリックしてエディタ(VSCode など)で開き、先ほどの TypeScript コードを 丸ごと貼り付けて保存 します。

2. テスト用ノードの作成

ここでは簡単な 2D シーンでテストする例を示します。

  1. Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択し、テスト用のスプライトを作成します。
  2. 作成した Sprite ノードを選択し、Inspector で SizeSpriteFrame を適当に設定してキャラクターっぽく見えるようにします。

3. SquashStretch コンポーネントのアタッチ

  1. テスト用の Sprite ノードを選択した状態で、右側の Inspector を確認します。
  2. Add Component ボタンをクリックします。
  3. Custom カテゴリの中から SquashStretch を選択して追加します。
    (Custom に見当たらない場合は、プロジェクトを一度保存し、Cocos Creator を再起動してみてください。)

4. プロパティの設定例

Inspector 上で、SquashStretch の各プロパティを次のように設定してみます。

  • Auto Play On Start: ON
  • Auto Play Mode: Jump
  • Duration: 0.18
  • Overshoot: 1.2(少し強め)
  • Use Uniform Scale: OFF
  • Jump Scale Multiplier X: 0.9
  • Jump Scale Multiplier Y: 1.1
  • Land Scale Multiplier X: 1.1
  • Land Scale Multiplier Y: 0.8
  • Allow Interrupt: ON
  • Debug Log: 必要に応じて ON(動作確認時は ON が便利)

この状態で Play ボタンを押すと、シーン開始と同時にスプライトが「ピョン」と伸びるような動きが 1 回再生されるはずです。

5. 他スクリプトからの呼び出し例

実際のゲームでは、「ジャンプボタンを押した瞬間」「地面に着地した瞬間」などで伸縮を再生したいことが多いです。その場合は、同じノードか子ノードにアタッチした別スクリプトから SquashStretch を取得してメソッドを呼び出します。


import { _decorator, Component, Node, input, Input, EventKeyboard, KeyCode } from 'cc';
import { SquashStretch } from './SquashStretch';
const { ccclass, property } = _decorator;

@ccclass('JumpTestController')
export class JumpTestController extends Component {

    @property({
        tooltip: '伸縮演出を適用したいノード(通常は自分自身)',
        type: Node,
    })
    public targetNode: Node | null = null;

    private _squash: SquashStretch | null = null;

    onLoad() {
        if (!this.targetNode) {
            this.targetNode = this.node;
        }

        if (this.targetNode) {
            this._squash = this.targetNode.getComponent(SquashStretch);
            if (!this._squash) {
                console.error('[JumpTestController] targetNode に SquashStretch がアタッチされていません。');
            }
        }

        input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
    }

    onDestroy() {
        input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
    }

    private _onKeyDown(event: EventKeyboard) {
        if (!this._squash) {
            return;
        }

        if (event.keyCode === KeyCode.SPACE) {
            // スペースキーでジャンプ演出
            this._squash.playJump();
        } else if (event.keyCode === KeyCode.ENTER) {
            // Enter キーで着地演出
            this._squash.playLand();
        }
    }
}

上記のように、SquashStretch 自体は外部依存がなく、他スクリプト側で自由にトリガーできます。

6. 動作確認のポイント

  • 再生中のスケールが Inspector でリアルタイムに変化するか確認する。
  • アニメーション終了後、スケールが 必ず元の値 に戻っているか確認する。
  • Allow Interrupt を OFF にした状態で、連打しても 1 回分しか再生されないことを確認する。
  • Use Uniform Scale を ON にすると、縦横同じ比率で「ふにゃっ」とする動きになることを確認する。

まとめ

  • SquashStretch は、ノードにアタッチして public メソッドを呼ぶだけで、ジャンプ・着地などの瞬間的な伸縮演出 を実現できる汎用コンポーネントです。
  • 外部の GameManager や Tween、Animation コンポーネントに依存せず、単体スクリプトとして完結 しているため、どのプロジェクトにも簡単にコピペ導入できます。
  • インスペクタから Duration / Overshoot / プリセット倍率 / Uniform 化 などを調整できるため、キャラクターごとに「柔らかさ」を微調整できます。
  • 応用例としては:
    • ダメージを受けた瞬間の「ビクッ」としたリアクション。
    • UI ボタンのクリック時に「ポヨン」と膨らむ演出。
    • 敵撃破時に一瞬潰してから消えるエフェクト。

    など、あらゆる「動きにメリハリをつけたい場面」で活用できます。

このコンポーネントをプロジェクトの「標準パーツ」として用意しておくと、演出のクオリティを手軽に底上げでき、シーン内のあらゆるノードに「アタッチして呼ぶだけ」で統一感のあるアニメーション表現を追加できます。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!