【Cocos Creator】アタッチするだけ!ScreenShake (画面振動)の実装方法【TypeScript】

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

【Cocos Creator 3.8】ScreenShakeの実装:アタッチするだけでカメラをノイズで揺らす汎用スクリプト

ダメージ演出や爆発エフェクトのときに「画面がガタガタ揺れる」だけで、ゲームのインパクトは大きく変わります。本記事では、カメラ(Cameraコンポーネント付きノード)にアタッチするだけで、ノイズ関数による画面揺れを実現できる汎用コンポーネント「ScreenShake」を実装します。

外部のGameManagerやイベントバスなどには一切依存せず、すべてインスペクタのプロパティから制御可能な完全独立コンポーネントとして設計します。


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

機能要件の整理

  • Cameraコンポーネントを持つノードにアタッチして使用する。
  • カメラノードのローカル座標(position)を一時的にオフセットして「揺れ」を表現する。
  • 揺れはノイズ関数(ここでは高速で簡易な疑似ノイズ)を用いてランダム性を持たせる。
  • 揺れの強さ・継続時間・減衰カーブをインスペクタから調整可能にする。
  • 「今すぐ揺らしたい」ときに、他スクリプトから簡単に呼べるパブリックメソッドを用意する(ただし、このScreenShake自体は他スクリプトに依存しない)。
  • 揺れ終了後、カメラノードの位置は必ず元の位置に戻す
  • 必須コンポーネント(Camera)が無い場合は、エラーログを出して自動で機能を停止する。

ノイズベースの揺れロジック概要

  • duration: 揺れが続く合計時間
  • amplitude: 揺れの最大オフセット量(ピクセル相当)
  • frequency: ノイズの時間方向の変化速度(値が大きいほど細かくブルブル揺れる)
  • decay: 時間経過による減衰(0〜1、1に近いほど減衰が弱い)

毎フレームのupdateで経過時間に応じてノイズ値を算出し、

  • X, Y それぞれに -amplitude〜+amplitude の範囲でオフセットを加える
  • 時間経過とともにオフセットの大きさを減衰させる

という形で画面揺れを実現します。

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

ScreenShakeコンポーネントに用意するプロパティと役割は以下の通りです。

  • enabledOnStart: boolean
    • ゲーム開始時(start時)に自動的に揺れを開始するかどうか。
    • テスト用や、常時微妙に揺れているカメラを作りたいときに便利。
  • defaultDuration: number
    • shake() を引数なしで呼んだときに使われる揺れ時間(秒)。
    • 例: 0.3〜0.6 秒程度が単発のダメージに向いている。
  • defaultAmplitude: number
    • shake() を引数なしで呼んだときの揺れ強度(ピクセル相当)。
    • 例: 5〜20 程度。2Dゲームなら 10 がほどよい。
  • defaultFrequency: number
    • ノイズの周波数。値が大きいほど細かく高速に揺れる。
    • 例: 20〜60 程度。小さいと「ゆっさゆっさ」、大きいと「ブルブル」。
  • decayFactor: number
    • 時間経過でどれくらい揺れを減衰させるか(0〜1)。
    • 1.0 だと減衰なし、0.9 だと毎秒ごとに 90% に減衰するイメージ。
  • shakeOnEnable: boolean
    • コンポーネントが有効化(onEnable)されたタイミングで自動で揺れを開始するか。
    • シーン切り替え直後などに一瞬揺らしたい場合に便利。
  • useUnscaledTime: boolean
    • Time.scale に影響されない時間で揺れを進行させるかどうか。
    • ポーズ中も揺れさせたい場合などに使用。
  • debugAutoShakeInterval: number
    • 0 より大きい値を設定すると、その秒数ごとに自動で揺れを発生させるデバッグ用機能。
    • 本番では 0 にして無効化するのが基本。

これらに加え、他のスクリプトから呼び出せるパブリックメソッドとして:

  • shake(duration?: number, amplitude?: number, frequency?: number): void
    • 任意のタイミングで画面揺れを開始する。
    • 引数を省略した場合は defaultXXX プロパティを使用。
  • stopShake(): void
    • 現在の揺れを即座に停止し、カメラ位置を元に戻す。

TypeScriptコードの実装

以下が完成した ScreenShake コンポーネントの全コードです。Cocos Creator 3.8.7 でそのまま使用できます。


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

/**
 * ScreenShake
 * カメラノードにアタッチして使用する画面揺れコンポーネント。
 * ノイズ関数を用いてローカル座標をオフセットし、時間経過で減衰させます。
 */
@ccclass('ScreenShake')
export class ScreenShake extends Component {

    @property({
        tooltip: 'ゲーム開始時(start)に自動で揺れを開始するかどうか'
    })
    public enabledOnStart: boolean = false;

    @property({
        tooltip: 'コンポーネントが有効化(onEnable)されたタイミングで自動で揺れを開始するかどうか'
    })
    public shakeOnEnable: boolean = false;

    @property({
        tooltip: 'デフォルトの揺れ時間(秒)。shake() を引数なしで呼んだときに使用されます'
    })
    public defaultDuration: number = 0.4;

    @property({
        tooltip: 'デフォルトの揺れ強度(ピクセル相当)。shake() を引数なしで呼んだときに使用されます'
    })
    public defaultAmplitude: number = 10;

    @property({
        tooltip: 'デフォルトのノイズ周波数。大きいほど細かく高速に揺れます'
    })
    public defaultFrequency: number = 30;

    @property({
        tooltip: '減衰係数(0〜1)。1に近いほど減衰が弱く、長く揺れ続けます'
    })
    public decayFactor: number = 0.9;

    @property({
        tooltip: 'Time.scale の影響を受けない時間で揺れを進行させるかどうか'
    })
    public useUnscaledTime: boolean = false;

    @property({
        tooltip: 'デバッグ用:0より大きい値を設定すると、この秒数ごとに自動で揺れを発生させます(本番では0推奨)'
    })
    public debugAutoShakeInterval: number = 0;

    // 内部状態
    private _camera: Camera | null = null;
    private _originalPosition: Vec3 = new Vec3();
    private _isShaking: boolean = false;
    private _shakeTime: number = 0;
    private _shakeDuration: number = 0;
    private _shakeAmplitude: number = 0;
    private _shakeFrequency: number = 0;
    private _seedX: number = 0;
    private _seedY: number = 0;
    private _debugTimer: number = 0;

    onLoad() {
        // 必須コンポーネントの取得
        this._camera = this.getComponent(Camera);
        if (!this._camera) {
            console.error('[ScreenShake] Camera コンポーネントが見つかりません。このコンポーネントは Camera を持つノードにアタッチしてください。');
            // Camera が無い場合は機能を停止
            this.enabled = false;
            return;
        }

        // 元のローカル位置を保存
        this.node.getPosition(this._originalPosition);

        // ランダムシードを初期化
        this._seedX = Math.random() * 1000;
        this._seedY = Math.random() * 1000;

        // プロパティの安全な範囲チェック
        if (this.defaultDuration <= 0) {
            this.defaultDuration = 0.3;
        }
        if (this.defaultFrequency <= 0) {
            this.defaultFrequency = 30;
        }
        if (this.decayFactor <= 0 || this.decayFactor > 1) {
            this.decayFactor = 0.9;
        }
    }

    onEnable() {
        // 有効化時に自動で揺らすオプション
        if (this.shakeOnEnable) {
            this.shake();
        }
        // デバッグ用タイマー初期化
        this._debugTimer = 0;
    }

    start() {
        // start 時に自動で揺らすオプション
        if (this.enabledOnStart) {
            this.shake();
        }
    }

    update(deltaTime: number) {
        // Camera が無効なら何もしない
        if (!this._camera) {
            return;
        }

        // 使用する時間スケールの決定
        const dt = this.useUnscaledTime ? game.deltaTime : deltaTime;

        // デバッグ用:一定間隔で自動揺れ
        if (this.debugAutoShakeInterval > 0) {
            this._debugTimer += dt;
            if (this._debugTimer >= this.debugAutoShakeInterval) {
                this._debugTimer = 0;
                this.shake();
            }
        }

        if (!this._isShaking) {
            return;
        }

        this._shakeTime += dt;

        // 経過割合 0〜1
        const t = math.clamp01(this._shakeTime / this._shakeDuration);

        // 時間経過による減衰(指数的に減衰)
        const decay = Math.pow(this.decayFactor, this._shakeTime);

        // ノイズベースのオフセット計算
        const timeFactor = this._shakeTime * this._shakeFrequency;

        const offsetX = this._noise(this._seedX + timeFactor) * 2 - 1; // -1〜1
        const offsetY = this._noise(this._seedY + timeFactor) * 2 - 1; // -1〜1

        const currentAmplitude = this._shakeAmplitude * (1 - t) * decay;

        const shakeOffset = new Vec3(offsetX * currentAmplitude, offsetY * currentAmplitude, 0);

        // カメラノードのローカル位置を元位置 + オフセットに設定
        const newPos = new Vec3(
            this._originalPosition.x + shakeOffset.x,
            this._originalPosition.y + shakeOffset.y,
            this._originalPosition.z
        );
        this.node.setPosition(newPos);

        // 終了判定
        if (this._shakeTime >= this._shakeDuration) {
            this.stopShake();
        }
    }

    /**
     * 画面揺れを開始する。
     * 引数を省略した場合は、インスペクタで設定したデフォルト値が使用されます。
     * @param duration 揺れ時間(秒)
     * @param amplitude 揺れ強度
     * @param frequency ノイズ周波数
     */
    public shake(duration?: number, amplitude?: number, frequency?: number): void {
        if (!this._camera) {
            console.warn('[ScreenShake] Camera が存在しないため、shake() は無視されました。');
            return;
        }

        // すでに揺れている場合も、パラメータを更新して再スタート
        this._shakeDuration = duration && duration > 0 ? duration : this.defaultDuration;
        this._shakeAmplitude = amplitude && amplitude > 0 ? amplitude : this.defaultAmplitude;
        this._shakeFrequency = frequency && frequency > 0 ? frequency : this.defaultFrequency;

        this._shakeTime = 0;
        this._isShaking = true;

        // 元位置を毎回更新しておくことで、他スクリプトがカメラを動かしていても破綻しにくくする
        this.node.getPosition(this._originalPosition);

        // シードを更新して、毎回少し違う揺れにする
        this._seedX = Math.random() * 1000;
        this._seedY = Math.random() * 1000;
    }

    /**
     * 現在の揺れを即座に停止し、カメラ位置を元に戻します。
     */
    public stopShake(): void {
        if (!this._camera) {
            return;
        }
        this._isShaking = false;
        this._shakeTime = 0;

        // カメラ位置を元のローカル座標に戻す
        this.node.setPosition(this._originalPosition);
    }

    /**
     * 簡易ノイズ関数
     * 0〜1 の範囲の擬似ランダム値を返します(連続的に変化)。
     * 高品質な Perlin/Simplex ノイズではありませんが、画面揺れ用途には十分です。
     */
    private _noise(x: number): number {
        // 1次元 value noise 的な簡易実装
        const i = Math.floor(x);
        const f = x - i;
        const u = f * f * (3 - 2 * f); // smoothstep

        const rand1 = this._hash(i);
        const rand2 = this._hash(i + 1);

        return this._lerp(rand1, rand2, u);
    }

    /**
     * ハッシュ関数:整数から 0〜1 の擬似乱数を生成
     */
    private _hash(n: number): number {
        const x = Math.sin(n * 127.1) * 43758.5453;
        return x - Math.floor(x);
    }

    /**
     * 線形補間
     */
    private _lerp(a: number, b: number, t: number): number {
        return a + (b - a) * t;
    }

    onDisable() {
        // 無効化時に位置を元に戻しておく
        if (this._camera) {
            this.node.setPosition(this._originalPosition);
        }
        this._isShaking = false;
    }

    onDestroy() {
        // 破棄時も安全のため元位置に戻す
        if (this._camera && this.node) {
            this.node.setPosition(this._originalPosition);
        }
    }
}

主要な処理の解説

  • onLoad
    • Camera コンポーネントを取得し、無ければエラーを出してコンポーネント自体を無効化します。
    • 元のローカル座標を保存し、ノイズ用シードやプロパティの初期値チェックを行います。
  • start / onEnable
    • enabledOnStart / shakeOnEnable が true の場合に自動で shake() を呼びます。
  • update
    • 揺れ中のみ処理を行い、経過時間から減衰係数を計算します。
    • 簡易ノイズ関数 _noise を用いて -1〜1 の値を生成し、amplitude と減衰を掛けてオフセット量を決定します。
    • 元の位置 + オフセットをカメラノードに適用し、duration を超えたら stopShake() で終了します。
    • debugAutoShakeInterval > 0 の場合、指定秒数ごとに自動で shake() を呼ぶデバッグ機能も実装しています。
  • shake()
    • 引数が指定されていなければ defaultXXX プロパティを使用します。
    • すでに揺れている場合でも、パラメータを上書きして再スタートします。
    • 毎回 _originalPosition を取得し直すことで、他スクリプトがカメラを動かしていても揺れ終了時に破綻しづらくしています。
  • stopShake()
    • 揺れフラグを下ろし、カメラ位置を _originalPosition に戻します。
    • onDisable / onDestroy でも安全のため位置を元に戻しています。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例: assets/scripts)を選択します。
  2. 右クリック → CreateTypeScript を選択します。
  3. 新規スクリプトの名前を ScreenShake.ts に変更します。
  4. ダブルクリックしてエディタ(VS Codeなど)で開き、中身をすべて削除して上記のコードをそのまま貼り付け、保存します。

2. カメラノードの準備

すでにシーンにカメラがある場合は、そのノードを使って構いません。ここでは新規作成の手順も示します。

  1. Hierarchy パネルで右クリック → Create3D ObjectCamera を選択します。
    • 2Dゲームでも Camera は 3D オブジェクトとして扱われます。
  2. 作成されたカメラノード(通常は Main Camera など)を選択し、Inspector を確認します。
  3. Camera コンポーネントが付いていることを確認してください。もし無い場合は:
    • Inspector → Add Component → 検索窓に「Camera」と入力 → Camera を追加します。

3. ScreenShake コンポーネントをアタッチ

  1. Hierarchy で カメラノード を選択します。
  2. Inspector で Add Component ボタンをクリックします。
  3. メニューの下部の Custom カテゴリを開き、ScreenShake を選択します。
    • Custom に表示されない場合は、検索窓に「ScreenShake」と入力して探してください。

4. プロパティの設定例

Inspector で ScreenShake コンポーネントの各プロパティを設定します。まずは以下の例で試してみてください。

  • enabledOnStart: チェック(ON)
  • shakeOnEnable: OFF
  • defaultDuration: 0.4
  • defaultAmplitude: 10
  • defaultFrequency: 30
  • decayFactor: 0.9
  • useUnscaledTime: OFF(通常はOFFで問題ありません)
  • debugAutoShakeInterval: 0(まずは無効)

この状態でゲームを再生すると、開始直後に0.4秒ほどブルッと揺れるのが確認できるはずです。

5. デバッグ用の自動揺れで調整する

揺れの強さや周波数を調整したいときは、毎回他スクリプトから呼ぶのではなく、debugAutoShakeInterval を活用すると効率的です。

  1. ScreenShake の debugAutoShakeInterval1.0 に設定します。
  2. ゲームを再生すると、1秒ごとに自動で画面揺れが発生します。
  3. 再生中に Inspector から
    • defaultAmplitude(例: 5 → 15 → 30)
    • defaultFrequency(例: 10 → 30 → 60)
    • decayFactor(例: 0.8 → 0.9 → 0.98)

    を変更してみて、好みの揺れ方になるまで調整します。

  4. 調整が終わったら、debugAutoShakeInterval を 0 に戻すのを忘れないようにしてください。

6. 他スクリプトから揺れを発生させる(任意)

ScreenShake は他のスクリプトに依存しませんが、他スクリプトから呼び出して使うことは可能です。例として、敵にダメージが入ったときに揺らすコードを示します。


// Enemy.ts などの任意のスクリプトから

import { _decorator, Component, Node } from 'cc';
import { ScreenShake } from './ScreenShake'; // パスはプロジェクト構成に合わせて変更

const { ccclass, property } = _decorator;

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

    @property({ tooltip: '画面揺れを担当する ScreenShake コンポーネントを持つカメラノード' })
    public screenShakeNode: Node | null = null;

    private _screenShake: ScreenShake | null = null;

    start() {
        if (this.screenShakeNode) {
            this._screenShake = this.screenShakeNode.getComponent(ScreenShake);
            if (!this._screenShake) {
                console.warn('[Enemy] 指定されたノードに ScreenShake コンポーネントが見つかりません。');
            }
        }
    }

    // 何らかのダメージ処理の中で呼ぶ
    public onDamaged() {
        if (this._screenShake) {
            // 0.3秒間、振幅15、周波数40で揺らす
            this._screenShake.shake(0.3, 15, 40);
        }
    }
}

このように、ScreenShake 側は完全に独立しており、他スクリプトからは shake() を呼ぶだけで簡単に画面揺れを追加できます。


まとめ

  • ScreenShake コンポーネントは、Camera コンポーネントを持つノードにアタッチするだけで、ノイズに基づく自然な画面揺れを実現します。
  • 揺れの時間・強度・周波数・減衰・時間スケールの扱いをすべてインスペクタから調整でき、外部の GameManager やシングルトンに一切依存しません。
  • 防御的な実装として、Camera が無い場合には自動で機能を停止し、コンソールにエラーログを出すようにしています。
  • デバッグ用の自動揺れ(debugAutoShakeInterval)を活用することで、実際に動かしながらパラメータ調整がしやすくなっています。
  • 他スクリプトからは shake() / stopShake() を呼ぶだけでよく、ダメージ・爆発・スキル発動などあらゆる演出に簡単に画面揺れを追加できます。

このような完全に独立した汎用コンポーネントを積み重ねていくことで、シーンごと・プロジェクトごとに再利用しやすくなり、ゲーム開発の効率と品質が大きく向上します。まずは本記事の ScreenShake をベースに、自分のゲームに合った揺れ方を追求してみてください。

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

URLをコピーしました!