【Cocos Creator】アタッチするだけ!WobbleEffect (ぷるぷる)の実装方法【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】WobbleEffect(ぷるぷる)の実装:アタッチするだけでスプライトをゼリーのように揺らして「ぷるぷる」させる汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで、そのノードのスプライトをノイズ関数を使って歪ませ、ゼリーのような「ぷるぷる」質感を表現する汎用コンポーネント WobbleEffect を実装します。

攻撃待機中のキャラクター、UIボタンの待機アニメーション、スライム系モンスターの質感表現など、「何もしていない時間をちょっとリッチに見せたい」ときにアタッチするだけで使えるのが特徴です。


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

機能要件の整理

  • このコンポーネントをアタッチしたノード(または指定したターゲットノード)の Sprite を「ぷるぷる」させる。
  • 揺れは ノイズ関数(疑似乱数ベース) を時間で変化させて生成し、スケール・回転・位置を微妙に歪ませてゼリー感を出す。
  • 完全に独立して動作し、他のカスタムスクリプトやシングルトンに依存しない。
  • 必要なコンポーネント(Sprite)は自動取得を試み、存在しない場合は エラーログを出して安全に無効化する。
  • インスペクタから「揺れの強さ・速さ・方向・有効/無効」などを細かく調整できる。
  • 実行中に一時停止/再開できるプロパティを用意する。

ノイズによる「ぷるぷる」のアプローチ

本格的な Perlin Noise 実装は長くなるため、ここでは以下の方針で「ノイズっぽい」動きを作ります:

  • 時間 t に対して、いくつかの 異なる周波数のサイン波 を合成し、簡易的なノイズ関数として使う。
  • 各成分に シード値 を与え、ノードごとにパターンが変わるようにする。
  • ノイズ値を 位置・回転・スケール にマッピングすることで、ゼリーっぽい「ぷるぷる」感を出す。

@property で調整可能な項目

インスペクタで設定可能なプロパティと役割は以下の通りです。

  • enabledEffect: boolean
    エフェクトの有効/無効を切り替えるフラグ。
    ・false の場合、元の Transform(位置・回転・スケール)を維持し、処理をスキップ。
  • targetNode: Node | null
    ・ぷるぷるさせたいノード。
    ・未指定(null)の場合は、このコンポーネントが付いているノード自身を対象とする。
  • positionAmplitude: Vec2
    ・位置の揺れの強さ(ピクセル単位)。
    ・x: 左右の揺れ幅、y: 上下の揺れ幅。
    ・例: (3, 5) で、最大 ±3px 横揺れ、±5px 縦揺れ。
  • rotationAmplitude: number
    ・回転の揺れの強さ(度数)。
    ・例: 5 で、最大 ±5° ほどぷるぷる回転する。
  • scaleAmplitude: Vec2
    ・スケール変化量の最大値。
    ・x: 横方向、y: 縦方向の伸縮量(1.0 を基準とした加算)。
    ・例: (0.1, 0.15) で、横は 0.9〜1.1、縦は 0.85〜1.15 程度でぷるぷる。
  • baseScale: Vec2
    ・「揺れの基準」となるスケール値。
    ・ここで指定した値を中心に揺れる。
    ・null にしておくと 開始時のスケールを自動的に基準として使用。
  • frequency: number
    ・揺れの基本速度(周波数)。
    ・値が大きいほど、ぷるぷるが速くなる。
    ・例: 1.0〜3.0 あたりが扱いやすい。
  • noiseIntensity: number
    ・ノイズの乱れ具合。
    ・0 に近いほど「規則的な揺れ」、1.0 以上で「ランダム感のある揺れ」。
  • randomSeed: number
    ・ノイズパターンを決めるシード値。
    ・同じ値なら同じ揺れ方、値を変えると揺れのパターンが変わる。
    ・0 の場合は 自動的にランダムシードを生成。
  • useUnscaledTime: boolean
    director.getDeltaTime() を使うか、game.deltaTime を使うか、などの「タイムスケール」の影響を受けるかどうかを切り替える想定ですが、
    ・Cocos Creator 3.8 では director.getDeltaTime() が標準的なため、ここでは 単純に on/off で時間進行を制御する用途に使用します。
    ・true の場合、内部的に time固定速度で進める(タイムスケールの影響を受けにくい)実装例を示します。

TypeScriptコードの実装


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

/**
 * WobbleEffect
 * 対象ノードの Transform をノイズベースで揺らし、
 * ゼリーのような「ぷるぷる」表現を行う汎用コンポーネント。
 */
@ccclass('WobbleEffect')
export class WobbleEffect extends Component {

    @property({
        tooltip: 'エフェクトの有効/無効を切り替えます。false の場合は Transform を変更しません。'
    })
    public enabledEffect: boolean = true;

    @property({
        type: Node,
        tooltip: 'ぷるぷるさせたいターゲットノード。未指定の場合、このコンポーネントが付いているノード自身を対象とします。'
    })
    public targetNode: Node | null = null;

    @property({
        tooltip: '位置の揺れ幅(ピクセル単位)。x: 左右、y: 上下。'
    })
    public positionAmplitude: Vec2 = new Vec2(3, 5);

    @property({
        tooltip: '回転の揺れ幅(度数)。値が大きいほど大きく回転します。'
    })
    public rotationAmplitude: number = 5;

    @property({
        tooltip: 'スケール変化量の最大値。x: 横方向、y: 縦方向。例: (0.1, 0.15) で 0.9〜1.1 程度に揺れます。'
    })
    public scaleAmplitude: Vec2 = new Vec2(0.1, 0.15);

    @property({
        tooltip: '基準スケール。null の場合は開始時のスケールを基準として使用します。'
    })
    public baseScale: Vec2 | null = null;

    @property({
        tooltip: '揺れの基本速度(周波数)。値が大きいほど高速にぷるぷるします。'
    })
    public frequency: number = 1.5;

    @property({
        tooltip: 'ノイズの乱れ具合。0 に近いほど規則的、1.0 以上でランダム感が強くなります。'
    })
    public noiseIntensity: number = 1.0;

    @property({
        tooltip: 'ノイズパターンを決めるシード値。0 の場合は自動でランダムシードを生成します。'
    })
    public randomSeed: number = 0;

    @property({
        tooltip: 'true の場合、内部時間を一定速度で進め、タイムスケールの影響を受けにくくします。'
    })
    public useUnscaledTime: boolean = false;

    // 内部状態
    private _target: Node | null = null;
    private _sprite: Sprite | null = null;

    private _originalPosition: Vec3 = new Vec3();
    private _originalRotation: Vec3 = new Vec3();
    private _originalScale: Vec3 = new Vec3();

    private _baseScaleInternal: Vec2 = new Vec2(1, 1);
    private _time: number = 0;
    private _seed: number = 1;

    onLoad() {
        // ターゲットノードの決定
        this._target = this.targetNode ? this.targetNode : this.node;

        if (!this._target) {
            console.error('[WobbleEffect] ターゲットノードが取得できません。コンポーネントを正しくアタッチしてください。');
            this.enabledEffect = false;
            return;
        }

        // Sprite コンポーネントの取得(ぷるぷるさせる対象として推奨)
        this._sprite = this._target.getComponent(Sprite);
        if (!this._sprite) {
            console.warn('[WobbleEffect] ターゲットノードに Sprite コンポーネントが見つかりません。' +
                'スプライトでの視覚的なぷるぷる表現を行う場合は、Sprite を追加してください。' +
                '(Transform の揺れ自体は動作します)');
        }

        // 元の Transform を保存
        this._originalPosition = this._target.position.clone();
        this._originalRotation = this._target.eulerAngles.clone();
        this._originalScale = this._target.scale.clone();

        // 基準スケールの決定
        if (this.baseScale) {
            this._baseScaleInternal.set(this.baseScale.x, this.baseScale.y);
        } else {
            this._baseScaleInternal.set(this._originalScale.x, this._originalScale.y);
        }

        // シード値の初期化
        if (this.randomSeed !== 0) {
            this._seed = this.randomSeed;
        } else {
            // ランダムシード(簡易)
            this._seed = Math.floor(Math.random() * 100000) + 1;
        }

        this._time = 0;
    }

    start() {
        // 特に処理は不要だが、将来の拡張用に空実装を残す
    }

    update(deltaTime: number) {
        if (!this.enabledEffect || !this._target) {
            return;
        }

        // 時間の進め方を決定
        if (this.useUnscaledTime) {
            // 固定速度で進める(deltaTime を使わず、一定値で加算)
            this._time += 0.016; // 約 60FPS 相当
        } else {
            // 通常の deltaTime ベース
            const dt = director.getDeltaTime ? director.getDeltaTime() : deltaTime;
            this._time += dt;
        }

        const t = this._time * this.frequency;

        // ノイズ値の計算(擬似的なノイズ関数)
        const n1 = this._noise1D(t + this._seed * 0.13);
        const n2 = this._noise1D(t * 0.9 + this._seed * 0.37);
        const n3 = this._noise1D(t * 1.7 + this._seed * 0.73);

        // 位置の揺れ
        const offsetX = (n1 * 2 - 1) * this.positionAmplitude.x * this.noiseIntensity;
        const offsetY = (n2 * 2 - 1) * this.positionAmplitude.y * this.noiseIntensity;

        const newPos = new Vec3(
            this._originalPosition.x + offsetX,
            this._originalPosition.y + offsetY,
            this._originalPosition.z
        );

        // 回転の揺れ(Z軸回転のみ)
        const rotOffset = (n3 * 2 - 1) * this.rotationAmplitude * this.noiseIntensity;
        const newRot = new Vec3(
            this._originalRotation.x,
            this._originalRotation.y,
            this._originalRotation.z + rotOffset
        );

        // スケールの揺れ(x と y を別ノイズで揺らす)
        const n4 = this._noise1D(t * 1.3 + this._seed * 1.11);
        const n5 = this._noise1D(t * 1.9 + this._seed * 2.17);

        const scaleX = this._baseScaleInternal.x + (n4 * 2 - 1) * this.scaleAmplitude.x * this.noiseIntensity;
        const scaleY = this._baseScaleInternal.y + (n5 * 2 - 1) * this.scaleAmplitude.y * this.noiseIntensity;

        const newScale = new Vec3(
            scaleX,
            scaleY,
            this._originalScale.z
        );

        // Transform の適用
        this._target.setPosition(newPos);
        this._target.setRotationFromEuler(newRot);
        this._target.setScale(newScale);
    }

    onDisable() {
        // 無効化時に Transform を元に戻す
        if (this._target) {
            this._target.setPosition(this._originalPosition);
            this._target.setRotationFromEuler(this._originalRotation);
            this._target.setScale(this._originalScale);
        }
    }

    onDestroy() {
        // 破棄時にも Transform を元に戻しておく(エディタ上の意図しない変形を避ける)
        if (this._target) {
            this._target.setPosition(this._originalPosition);
            this._target.setRotationFromEuler(this._originalRotation);
            this._target.setScale(this._originalScale);
        }
    }

    /**
     * 1次元の簡易ノイズ関数
     * - いくつかの周波数のサイン波を合成して 0〜1 の値を返す。
     * - Perlin Noise ほど本格的ではないが、揺れ表現には十分。
     */
    private _noise1D(x: number): number {
        // 周波数と重みを変えたサイン波を合成
        const v1 = Math.sin(x);
        const v2 = Math.sin(x * 2.3 + 1.7);
        const v3 = Math.sin(x * 3.7 + 0.5);

        // 正規化して 0〜1 に
        const sum = v1 * 0.5 + v2 * 0.3 + v3 * 0.2;
        const normalized = (sum + 1) * 0.5; // -1〜1 を 0〜1 に変換
        return normalized;
    }
}

主要メソッドの解説

  • onLoad()
    • targetNode が指定されていればそれを、なければ this.node をターゲットとします。
    • ターゲットから Sprite を取得し、見つからなければ console.warn で警告を出します(Transform の揺れ自体は動作)。
    • ターゲットの 元の位置・回転・スケール を保存します。
    • baseScale が指定されていればそれを、なければ元のスケールを基準スケールとして保持します。
    • randomSeed が 0 の場合はランダムなシード値を生成します。
  • update(deltaTime: number)
    • enabledEffect が false またはターゲットが無効な場合は何もしません。
    • useUnscaledTime に応じて _time を進めます。
    • 内部ノイズ関数 _noise1D() を使って、時間に応じた ノイズ値 n1〜n5 を生成します。
    • それぞれのノイズ値を 位置・回転・スケール にマッピングし、新しい Transform を計算してターゲットに適用します。
  • onDisable(), onDestroy()
    • コンポーネントが無効化/破棄される際に、ターゲットの Transform を 元の状態に戻すことで、エディタ上での「変な位置・スケールで固定されてしまう」問題を防ぎます。
  • _noise1D(x: number)
    • 1次元の簡易ノイズ関数です。
    • 複数のサイン波を合成し、結果を 0〜1 の範囲に正規化した値を返します。
    • Perlin Noise などに比べて実装が軽く、ぷるぷるのような用途には十分なランダム感があります。

使用手順と動作確認

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

  1. Assets パネルで右クリックし、Create → TypeScript を選択します。
  2. ファイル名を WobbleEffect.ts として作成します。
  3. 自動生成されたテンプレートコードをすべて削除し、前述の TypeScript コードを丸ごと貼り付けて保存します。

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

ここでは、単純なスプライトをぷるぷるさせる例を説明します。

  1. Hierarchy パネルで右クリックし、Create → 2D Object → Sprite を選択します。
  2. 作成された Sprite ノードを選択し、Inspector の Sprite コンポーネントで任意の画像(例: スライムの画像やボタン画像)を設定します。
  3. シーン上で見やすい位置に移動・スケールを調整しておきます。

3. WobbleEffect のアタッチ

  1. ぷるぷるさせたいノード(先ほど作成した Sprite ノード)を選択します。
  2. Inspector 下部の Add Component ボタンをクリックします。
  3. メニューから Custom → WobbleEffect を選択して追加します。
  4. 追加された WobbleEffect コンポーネントの各プロパティを設定します。

4. プロパティ設定の例

まずは分かりやすい「ぷるぷる」設定例を紹介します。

  • enabledEffect: チェックを入れる(true)。
  • targetNode: 空欄のまま(自分自身をターゲットにする)。
  • positionAmplitude: (3, 5)
  • rotationAmplitude: 4
  • scaleAmplitude: (0.08, 0.12)
  • baseScale: 空欄(null のまま。現在のスケールを基準にする)。
  • frequency: 1.8
  • noiseIntensity: 1.0
  • randomSeed: 0(ランダムで OK。特定パターンを再現したい場合のみ数値を入れる)。
  • useUnscaledTime: チェックなし(false)。

5. 再生して動作確認

  1. エディタ上部の ▶(Play)ボタン をクリックしてゲームを再生します。
  2. シーン上の Sprite が、その場で ぷるぷると揺れているのを確認します。
  3. もし揺れが弱い/強すぎる場合は、ゲームを一旦停止してから以下を微調整します:
    • もっと大きく揺らしたいpositionAmplitudescaleAmplituderotationAmplitude を少しずつ増やす。
    • 動きが速すぎる/遅すぎるfrequency を調整(1.0〜3.0 の範囲で試す)。
    • もっとランダム感がほしいnoiseIntensity を 1.2〜1.5 くらいまで上げる。
    • 規則正しく揺れてほしいnoiseIntensity を 0.5〜0.8 くらいまで下げる。

6. 複数ノードへの適用とパターンの変化

  • 同じ WobbleEffect複数のノードにアタッチすると、それぞれが独立したシード値で揺れるため、同じタイミングで同じ動きになりにくいようになっています。
  • もし「全員まったく同じ揺れ方をしてほしい」場合は、randomSeed に同じ値(例: 12345)を設定してください。
  • 逆に「特定のキャラだけ揺れ方を変えたい」場合は、そのキャラの randomSeed に別の数値を入れると、揺れパターンが変わります。

7. 別ノードをターゲットにする例

UI ボタンの「親ノード」に WobbleEffect を付けて、中のアイコンだけをぷるぷるさせたい場合などは、以下のように設定します。

  1. 親ノードに WobbleEffect をアタッチします。
  2. targetNode プロパティに、ぷるぷるさせたい 子ノード(アイコンの Sprite ノード) をドラッグ&ドロップします。
  3. これにより、親ノードは固定されたまま、子ノードだけがぷるぷる揺れるようになります。

まとめ

この WobbleEffect コンポーネントは、

  • アタッチするだけで ゼリーのような「ぷるぷる」表現を付与できる。
  • 位置・回転・スケールの揺れを ノイズベースで生成するため、単純な往復アニメーションよりも 自然でランダム感のある動きを実現できる。
  • 外部スクリプトに一切依存せず、必要な設定はすべてインスペクタから行える。
  • Sprite が無い場合も Transform の揺れだけは動作し、エラーログ/警告で状況が分かるようになっている。

キャラクターの待機モーション、スライム系オブジェクト、UI ボタンのホバー演出など、「とりあえず動かしておきたい」ノードにポン付けできるのが最大の利点です。
このコンポーネント単体で完結しているため、プロジェクト内のどのシーン・どのノードにも コピペ&アタッチで簡単に再利用できます。

必要に応じて、

  • 特定の入力(クリック/タップ)時だけ揺らす
  • ダメージを受けた瞬間だけ揺れを強くする
  • 時間経過で揺れが弱まるフェードアウト機能を追加する

といった拡張も、同じ設計方針(外部依存なし・@property ベース)で実装しやすいはずです。
まずは今回の WobbleEffect をプロジェクトに組み込んで、「待機中のぷるぷる」を量産してみてください。

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をコピーしました!