【Cocos Creator 3.8】CameraShaker の実装:アタッチするだけでカメラ(親ノード)をノイズでガタガタ揺らす汎用スクリプト
このガイドでは、Cocos Creator 3.8.7 + TypeScript で、「外部からメソッドを呼び出すだけで、カメラ(または任意ノード)の位置をノイズ関数で激しく揺らす」ための汎用コンポーネント CameraShaker を実装します。
このコンポーネントは、対象ノードにアタッチするだけで使え、外部の GameManager などのスクリプトには一切依存しません。
インスペクタで揺れの強さ・時間・減衰などを調整できるので、演出用のカメラ揺れや、UI のシェイク演出などにそのまま活用できます。
コンポーネントの設計方針
1. 機能要件の整理
- 外部から呼び出すトリガー
- 他のスクリプトから
shake()を呼ぶと揺れを開始する。 - 既に揺れている最中に呼んだ場合の挙動(上書き or 加算)を選べるようにする。
- 他のスクリプトから
- 揺れの原理
- 親ノード(または自身)の
positionを一時的にオフセットして揺れを表現する。 - オフセットはフレームごとに疑似乱数(ノイズ)で変化させる。
- 揺れの強さは時間経過に応じて減衰させる。
- 親ノード(または自身)の
- 原点復帰
- 揺れ開始前の「元のローカル座標」を記録し、揺れ終了時に必ずそこへ戻す。
- コンポーネントが無効化・破棄された際にも、できるだけ元の位置へ戻す。
- 外部依存をなくす
- カメラコンポーネント(
Camera)への依存は必須としない。 - 「カメラを揺らしたい場合は Camera を持つノードにアタッチする」だけでよい。
- 他のカスタムスクリプトやシングルトンには一切依存しない。
- カメラコンポーネント(
2. どのノードを揺らすか
仕様にある「親の offset を揺らす」を、Cocos Creator の構造に合わせて次のように解釈します。
- デフォルト:
useParentが true の場合、親ノードを揺らす。 - オプション:
useParentが false の場合、このコンポーネントが付いているノード自身を揺らす。
これにより、
- カメラノードの子に
CameraShakerを付けて「カメラを揺らす」 - 任意の UI ノードに直接
CameraShakerを付けて「そのノードだけ揺らす」
という 2 パターンを簡単に選べるようにします。
3. インスペクタで設定可能なプロパティ
インスペクタから調整できるプロパティと、その役割は以下の通りです。
- useParent: boolean
- 親ノードを揺らすかどうか。
- true: 親ノードを揺らす(カメラノードの子に付ける場合など)。
- false: このコンポーネントが付いているノード自身を揺らす。
- defaultDuration: number
shake()呼び出し時に時間を省略した場合のデフォルト揺れ時間(秒)。- 例: 0.4 なら 0.4 秒間揺れ続ける。
- defaultAmplitude: number
shake()呼び出し時に強さを省略した場合のデフォルト振幅(ピクセル相当)。- 値が大きいほど揺れが大きくなる。
- frequency: number
- 揺れのノイズ周波数。1 秒間にどれくらい揺れの方向が変化するかの目安。
- 大きいほど「ブルブル」と細かく震える。
- damping: number
- 時間経過に応じて振幅を減衰させる割合(0〜1)。
- 0: 減衰なし(最後まで一定の強さ)。
- 1: 完全に線形で 0 まで減衰。
- 通常は 0.5〜1.0 あたりを推奨。
- randomSeed: number
- 疑似乱数のシード値。
- 同じシード値であれば、毎回ほぼ同じ揺れパターンになる。
- 0 以下の場合は、毎回ランダムなシードで揺れる。
- overwriteMode: number (enum)
- 揺れ中に再度
shake()が呼ばれたときの挙動。 - 0: Ignore — 既に揺れている場合、新しいリクエストは無視する。
- 1: Restart — 現在の揺れを中断し、新しいパラメータで揺れをやり直す。
- 2: Additive — 現在の揺れに「残り時間と強さ」を加算して延長・増幅する。
- 揺れ中に再度
- useUnscaledTime: boolean
director.getDeltaTime()(タイムスケール影響あり)か、game.deltaTime(ほぼ同等)か、
厳密な制御が不要なためここではuseUnscaledTimeは単なる「意味上のフラグ」に留め、
実装ではdtをそのまま使用します(必要なら拡張しやすいようにプロパティだけ用意)。
また、外部スクリプトから使いやすいように、次のメソッドを公開します。
- shake(duration?: number, amplitude?: number): void
- 揺れを開始するメインメソッド。
- 引数を省略すると
defaultDuration,defaultAmplitudeを使用。
- stopShake(): void
- 現在の揺れを即座に停止し、元の位置に戻す。
TypeScriptコードの実装
以下が、完成した CameraShaker.ts の全コードです。
import { _decorator, Component, Node, Vec3, math } from 'cc';
const { ccclass, property, tooltip } = _decorator;
/**
* CameraShaker
* 任意ノード(主にカメラノード)にアタッチして、
* 外部から shake() を呼ぶだけでノイズベースのシェイクを実現するコンポーネント。
*/
enum OverwriteMode {
IGNORE = 0,
RESTART = 1,
ADDITIVE = 2,
}
@ccclass('CameraShaker')
export class CameraShaker extends Component {
@property({
tooltip: 'true: 親ノードを揺らします。\nfalse: このノード自身を揺らします。',
})
public useParent: boolean = true;
@property({
tooltip: 'shake() で時間を省略した場合に使われるデフォルトの揺れ時間(秒)。',
min: 0.01,
})
public defaultDuration: number = 0.4;
@property({
tooltip: 'shake() で強さを省略した場合に使われるデフォルトの振幅(ピクセル相当)。',
min: 0,
})
public defaultAmplitude: number = 20;
@property({
tooltip: '揺れの周波数(1秒あたりの変化回数の目安)。大きいほど細かくブルブル震えます。',
min: 0.1,
})
public frequency: number = 25;
@property({
tooltip: '振幅の減衰率(0〜1)。0: 減衰なし / 1: 線形で0まで減衰。',
min: 0,
max: 1,
})
public damping: number = 0.9;
@property({
tooltip: '疑似乱数のシード値。同じ値なら毎回ほぼ同じ揺れパターンになります。\n0 以下の場合は毎回ランダムなシードを使用します。',
})
public randomSeed: number = 0;
@property({
tooltip: '揺れ中に再度 shake() が呼ばれたときの挙動。\n0: Ignore(無視)\n1: Restart(やり直し)\n2: Additive(時間と強さを加算)',
type: OverwriteMode,
})
public overwriteMode: OverwriteMode = OverwriteMode.RESTART;
@property({
tooltip: '将来の拡張用フラグ。true の場合、ゲームのタイムスケールに影響されない揺れを意図します(本実装では dt をそのまま使用)。',
})
public useUnscaledTime: boolean = false;
// 内部状態
private _target: Node | null = null;
private _originalPosition: Vec3 = new Vec3();
private _isShaking: boolean = false;
private _elapsed: number = 0;
private _duration: number = 0;
private _amplitude: number = 0;
private _seedX: number = 0;
private _seedY: number = 0;
onLoad() {
// 揺らす対象ノードを決定
this._target = this.useParent ? this.node.parent : this.node;
if (!this._target) {
console.error('[CameraShaker] 揺らす対象ノードが見つかりません。useParent が true の場合は親ノードが必要です。', this.node);
return;
}
// 元のローカル位置を保存
this._originalPosition.set(this._target.position);
}
onEnable() {
// 有効化時に位置を再キャッシュ(エディタ上での移動にも対応)
if (this._target) {
this._originalPosition.set(this._target.position);
}
}
onDisable() {
// 無効化時にはできるだけ元の位置に戻す
this._restoreOriginalPosition();
this._isShaking = false;
}
onDestroy() {
// 破棄時も同様に元の位置へ戻す
this._restoreOriginalPosition();
}
update(dt: number) {
if (!this._isShaking || !this._target) {
return;
}
// 経過時間を更新
this._elapsed += dt;
// 正規化時間(0〜1)
const t = math.clamp01(this._elapsed / this._duration);
// 減衰係数(1 から 0 へ)
const dampingFactor = this.damping > 0 ? (1 - t * this.damping) : 1;
const currentAmplitude = this._amplitude * dampingFactor;
// ノイズ用の時間スケール
const time = this._elapsed * this.frequency;
// 疑似ノイズでオフセット生成(-1〜1 の範囲)
const nx = this._pseudoRandom(time + this._seedX) * 2 - 1;
const ny = this._pseudoRandom(time + this._seedY) * 2 - 1;
const offset = new Vec3(nx * currentAmplitude, ny * currentAmplitude, 0);
// オフセットを適用
this._target.setPosition(
this._originalPosition.x + offset.x,
this._originalPosition.y + offset.y,
this._originalPosition.z
);
// 終了判定
if (this._elapsed >= this._duration) {
this._isShaking = false;
this._restoreOriginalPosition();
}
}
/**
* 揺れを開始します。
* @param duration 揺れ時間(秒)。省略時は defaultDuration を使用。
* @param amplitude 振幅。省略時は defaultAmplitude を使用。
*/
public shake(duration?: number, amplitude?: number): void {
if (!this._target) {
this._target = this.useParent ? this.node.parent : this.node;
if (!this._target) {
console.error('[CameraShaker] 揺らす対象ノードが見つかりません。', this.node);
return;
}
}
const d = duration ?? this.defaultDuration;
const a = amplitude ?? this.defaultAmplitude;
if (d <= 0 || a <= 0) {
// 無意味なパラメータの場合は何もしない
return;
}
if (this._isShaking) {
switch (this.overwriteMode) {
case OverwriteMode.IGNORE:
// 既存の揺れを優先
return;
case OverwriteMode.RESTART:
// 下で再初期化
break;
case OverwriteMode.ADDITIVE:
// 残り時間と振幅を加算
const remaining = Math.max(0, this._duration - this._elapsed);
this._duration = remaining + d;
this._amplitude += a;
return;
}
}
// 元位置をキャッシュし直す(連続呼び出しに強くする)
this._originalPosition.set(this._target.position);
this._elapsed = 0;
this._duration = d;
this._amplitude = a;
this._isShaking = true;
// シードを決定
if (this.randomSeed > 0) {
this._seedX = this.randomSeed;
this._seedY = this.randomSeed * 1.37;
} else {
const base = Math.random() * 1000;
this._seedX = base;
this._seedY = base * 1.37;
}
}
/**
* 現在の揺れを即座に停止し、元の位置に戻します。
*/
public stopShake(): void {
this._isShaking = false;
this._elapsed = 0;
this._duration = 0;
this._amplitude = 0;
this._restoreOriginalPosition();
}
/**
* エディタ上で元位置が変わった場合などに呼び出して、
* 現在位置を「基準位置」として再キャッシュします。
*/
public recacheOriginalPosition(): void {
if (this._target) {
this._originalPosition.set(this._target.position);
}
}
// ===== 内部ヘルパー =====
private _restoreOriginalPosition(): void {
if (this._target) {
this._target.setPosition(this._originalPosition);
}
}
/**
* シンプルな疑似乱数関数(0〜1 の範囲)。
* 同じ引数に対しては常に同じ値を返します。
*/
private _pseudoRandom(n: number): number {
// 参考: https://stackoverflow.com/a/47593316
n = Math.sin(n * 12.9898) * 43758.5453;
return n - Math.floor(n);
}
}
コードのポイント解説
- onLoad / onEnable
useParentに応じて、揺らす対象ノード(_target)を決定します。_originalPositionに対象ノードのローカル座標を保存しておき、揺れ終了時にここへ戻します。- 親が存在しないのに
useParent = trueの場合は、console.errorで明示的にエラーを出します。
- update(dt)
- 揺れ中(
_isShaking === true)のみ処理を行います。 - 経過時間
_elapsedから正規化時間tを計算し、dampingに応じて振幅を減衰させます。 _pseudoRandom()を使って -1〜1 の範囲のノイズ値を生成し、currentAmplitudeを掛けてオフセットを算出します。- 元位置 + オフセットを
setPositionで適用し、_elapsed >= _durationになったら揺れを終了して元位置に戻します。
- 揺れ中(
- shake(duration?, amplitude?)
- 外部から呼び出すメイン API です。
- 引数が省略された場合は
defaultDurationとdefaultAmplitudeを使用します。 - 揺れ中の再呼び出し時は
overwriteModeに応じて- IGNORE: 何もしない
- RESTART: 新しい揺れで上書き
- ADDITIVE: 残り時間と振幅を加算(延長・増幅)
randomSeedが 0 より大きい場合は固定シード、0 以下の場合は毎回ランダムなシードで揺れます。
- stopShake()
- 揺れを即座に停止し、元位置に戻すための API です。
- _pseudoRandom(n)
- 同じ入力に対して同じ値を返す簡易的な疑似乱数関数です。
- これにより、
randomSeedを固定すると揺れパターンもほぼ固定されます。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- 作成されたファイルの名前を
CameraShaker.tsに変更します。 CameraShaker.tsをダブルクリックして開き、既存のテンプレートコードをすべて削除し、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用のシーンとノードを準備
ここでは、2 パターンの使い方を試してみます。
A. カメラを揺らす(2Dゲーム想定)
- Hierarchy パネルで右クリック → Create → UI → Canvas を作成します(既にある場合は流用して構いません)。
- Canvas の子としてカメラノードがある場合は、それを使用します。ない場合は:
- Hierarchy で右クリック → Create → 3D Object → Camera を作成します。
- この Camera ノードをシーン中央付近に配置します。
- Hierarchy で Camera ノードを選択します。
- Inspector の下部にある Add Component ボタンをクリックします。
- Custom → CameraShaker を選択してアタッチします。
- Inspector の CameraShaker セクションで、以下のように設定します(例):
- useParent:
false(カメラノード自身を揺らす) - defaultDuration:
0.4 - defaultAmplitude:
30 - frequency:
25 - damping:
0.9 - randomSeed:
0(毎回ランダム) - overwriteMode:
RESTART(プルダウンから選択)
- useParent:
この状態では、まだ shake() を呼んでいないので揺れません。
簡単なテスト用スクリプトを作って、キー入力で揺らしてみましょう。
- Assets → scripts などのフォルダで右クリック → Create → TypeScript。
- ファイル名を
TestShake.tsに変更し、以下のコードを貼り付けます。
import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc';
import { CameraShaker } from './CameraShaker';
const { ccclass } = _decorator;
@ccclass('TestShake')
export class TestShake extends Component {
private _shaker: CameraShaker | null = null;
onLoad() {
this._shaker = this.getComponent(CameraShaker);
if (!this._shaker) {
console.error('[TestShake] CameraShaker コンポーネントが同じノードに見つかりません。');
}
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._shaker) return;
if (event.keyCode === KeyCode.SPACE) {
// スペースキーでデフォルトの揺れ
this._shaker.shake();
} else if (event.keyCode === KeyCode.KEY_Q) {
// Q キーで短く弱い揺れ
this._shaker.shake(0.2, 10);
} else if (event.keyCode === KeyCode.KEY_E) {
// E キーで長く強い揺れ
this._shaker.shake(0.8, 60);
}
}
}
続いて、
- Hierarchy で先ほどの Camera ノードを選択します。
- Add Component → Custom → TestShake を追加します。
- シーンを保存し、Play ボタンで再生します。
- ゲームビューがアクティブな状態で
- Space キーを押す → カメラが 0.4 秒ほどガタガタ揺れる。
- Q キー → 短く弱い揺れ。
- E キー → 長く強い揺れ。
これで、外部スクリプトから shake() を呼び出すだけでカメラが揺れることを確認できます。
B. UI ノードを揺らす(ダメージ演出など)
今度は、UI の画像(Sprite)を揺らしてみます。
- Hierarchy で Canvas を右クリック → Create → UI → Sprite を作成します。
- 作成された Sprite ノードに、適当な画像を設定します(Inspector の Sprite コンポーネントの SpriteFrame に画像をドラッグ)。
- Sprite ノードを選択し、Add Component → Custom → CameraShaker を追加します。
- Inspector で CameraShaker の設定を次のように変更します。
- useParent:
false(自身を揺らす) - defaultDuration:
0.25 - defaultAmplitude:
15 - frequency:
30 - damping:
1.0
- useParent:
- 同じ Sprite ノードに、簡易的なテストスクリプトを付けてクリックで揺れるようにしてみます。
import { _decorator, Component, Node, EventTouch } from 'cc';
import { CameraShaker } from './CameraShaker';
const { ccclass } = _decorator;
@ccclass('ShakeOnClick')
export class ShakeOnClick extends Component {
private _shaker: CameraShaker | null = null;
onLoad() {
this._shaker = this.getComponent(CameraShaker);
if (!this._shaker) {
console.error('[ShakeOnClick] CameraShaker が同じノードに見つかりません。');
}
this.node.on(Node.EventType.TOUCH_END, this.onClicked, this);
}
onDestroy() {
this.node.off(Node.EventType.TOUCH_END, this.onClicked, this);
}
private onClicked(event: EventTouch) {
if (this._shaker) {
this._shaker.shake();
}
}
}
この ShakeOnClick.ts を作成し、Sprite ノードにアタッチすると、
ゲーム実行中にその Sprite をタップ(クリック)するたびに UI がプルプル揺れるようになります。
3. よくあるハマりポイントと確認事項
- 親を揺らすのに親がいない
useParent = trueの状態で、ルートノードにCameraShakerを付けると、親が存在せずエラーになります。- その場合は
useParent = falseにするか、1 つ上の親ノードを作成してそこにカメラをぶら下げてください。
- 揺れが弱すぎる / 強すぎる
defaultAmplitudeやshake()の第 2 引数を調整してください。- 画面解像度やカメラのズームによって「ちょうどよい値」は変わります。2D なら 10〜50 あたりから試すとよいです。
- 揺れがガクガクしすぎる / 滑らかすぎる
frequencyを下げると、ゆっくり大きく揺れる感じに。- 上げると、細かいブルブルとした揺れになります。
- 揺れが急に終わって違和感がある
dampingを 1.0 付近にすると、終盤で徐々に弱まって自然な印象になります。- 逆に 0 にすると、ずっと同じ強さで揺れて、時間切れでピタッと止まります。
まとめ
この CameraShaker コンポーネントは、
- 任意のノードにアタッチするだけで、外部から
shake()を呼ぶとノイズベースのシェイクがかかる。 - カメラでも UI でも、親ノードでも自身でも、同じコンポーネントで揺らせる。
- 揺れ時間・振幅・周波数・減衰・上書きモードなどをインスペクタから細かく調整できる。
- 他のカスタムスクリプトやシングルトンに一切依存せず、完全に独立した再利用可能コンポーネントとして使える。
実際のゲームでは、
- 敵の攻撃や爆発時にカメラを揺らしてインパクトを強調する。
- ダメージを受けた UI(HP バーやキャラクターアイコン)を軽く揺らしてフィードバックを出す。
- ガチャ演出やリザルト画面での「ドンッ」という動きをシェイクで表現する。
といった用途に、そのまま組み込むことができます。
パラメータをプリセット化しておけば、「強い揺れ」「弱い揺れ」「縦揺れ寄り」などを一括で使い回せるので、演出調整のスピードアップにもつながります。
本記事の CameraShaker.ts をプロジェクトに追加しておけば、今後のゲームでも毎回同じコンポーネントをコピーして使い回せるので、ぜひテンプレートとして活用してみてください。




