【Cocos Creator 3.8】ShakeUIの実装:アタッチするだけでHPバーやアイコンを「ガタガタ揺らす」汎用スクリプト
UIのHPバーやアイコンを「ダメージを受けたときだけガタッと揺らしたい」「ボタンを押したときに少しだけシェイクさせたい」といった演出は、多くのゲームで使われる定番エフェクトです。
この記事では、任意のUIノードにアタッチするだけで揺らし演出を実現できる汎用コンポーネント ShakeUI を、Cocos Creator 3.8 + TypeScript で実装します。
外部のGameManagerなどには一切依存せず、このスクリプト単体で完結するように設計します。
ボタンやHPバーなどにアタッチして、スクリプト内の public triggerShake() を呼ぶだけでシェイクが始まるようにします(インスペクタから「自動再生」させることも可能にします)。
コンポーネントの設計方針
1. 機能要件の整理
- 任意のUIノード(HPバー、アイコン、ボタンなど)にアタッチして使える。
- 位置のシェイク(上下左右に小刻みに動く)を実装する。
- シェイクの強さ(振れ幅)、時間、減衰の有無などをインスペクタから調整できる。
- 他のスクリプトに依存せず、このコンポーネント単体で完結する。
- シェイク中は、元のローカル位置からの相対オフセットだけを加える(終了後は必ず元の位置に戻す)。
- シェイクは
- スクリプト読み込み時に自動再生(オプション)
- 他スクリプトから
triggerShake()を呼び出し - インスペクタの「テスト用ボタン」からエディタ上で試す
の3パターンで開始できるようにする。
UI用なので、基本的には 2D画面空間(Canvas配下のノード)での使用を想定していますが、通常のノードでも同様に動作します。
2. 外部依存をなくす設計アプローチ
- シェイクの状態管理(残り時間、現在のオフセットなど)はすべてこのコンポーネント内で完結。
- 外部の「ダメージ管理」や「アニメーション管理」などには一切依存しない。
- 他のスクリプトからシェイクさせたい場合は、
getComponent(ShakeUI)で取得してtriggerShake()を呼ぶだけでよいようにする。 - 標準コンポーネント(SpriteやUITransformなど)への依存は必須ではないため、特に取得を強制しない(どのノードでも動くようにする)。
3. インスペクタで設定可能なプロパティ設計
以下のようなプロパティを用意します。
- shakeDuration: number
- シェイク全体の時間(秒)。
- 例: 0.3〜0.6 秒程度が使いやすい。
- shakeStrength: number
- シェイクの最大振れ幅(ピクセル)。
- 例: 5〜30。値が大きいほど大きく揺れる。
- shakeFrequency: number
- 1秒あたりの揺れの周期数(ランダム方向の更新頻度)。
- 例: 20〜60。値が大きいほど細かくブルブルする。
- attenuate: boolean
- 時間経過とともに揺れの強さを減衰させるかどうか。
- ON の場合、最初が最大で徐々に弱くなって止まる。
- autoPlayOnStart: boolean
- シーン開始時(
start())に自動で一度シェイクを再生するかどうか。 - 演出確認用や「登場時に揺らす」などに使える。
- シーン開始時(
- timeScale: number
- シェイクの時間スケール。1.0 が通常、0.5 で半分の速度、2.0 で2倍速。
- 「ゆっくり大きく揺らす」「素早く小刻みに揺らす」などの調整に使える。
- useLocalSpace: boolean
- true: ローカル座標(
setPosition)で揺らす。 - false: ワールド座標(
setWorldPosition)で揺らす。 - 通常は UI ノードなので true 推奨。
- true: ローカル座標(
- editorTestButton: boolean
- インスペクタから ON にしたときに一度だけシェイクを実行し、すぐ false に戻す「テスト用スイッチ」。
- ゲームを再生せずに、エディタ上でシェイク挙動を確認する用途を想定。
これらをすべて @property で公開し、インスペクタから数値を触るだけで好みのシェイクに調整できるようにします。
TypeScriptコードの実装
以下が完成した ShakeUI.ts の全コードです。
import { _decorator, Component, Node, Vec3, randomRange, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* ShakeUI
* 任意のノードを「位置シェイク」させる汎用コンポーネント。
* - 他スクリプトから triggerShake() を呼ぶだけでシェイク開始
* - インスペクタから強さ・時間・減衰などを調整可能
*/
@ccclass('ShakeUI')
export class ShakeUI extends Component {
@property({
tooltip: 'シェイク全体の時間(秒)。0.3〜0.6 くらいが扱いやすい値です。',
min: 0.01,
})
public shakeDuration: number = 0.4;
@property({
tooltip: 'シェイクの最大振れ幅(ピクセル)。値が大きいほど大きく揺れます。',
min: 0,
})
public shakeStrength: number = 15;
@property({
tooltip: '1秒あたりの揺れの更新回数(周波数)。値が大きいほど細かくブルブルします。',
min: 1,
max: 120,
})
public shakeFrequency: number = 40;
@property({
tooltip: '時間経過とともに揺れの強さを減衰させるかどうか。',
})
public attenuate: boolean = true;
@property({
tooltip: 'シーン開始時(start)に自動で一度シェイクを再生します。',
})
public autoPlayOnStart: boolean = false;
@property({
tooltip: 'シェイクの時間スケール。1.0 が通常、2.0 で2倍速、0.5で半分の速度。',
min: 0.01,
})
public timeScale: number = 1.0;
@property({
tooltip: 'true: ローカル座標で揺らす(通常のUIはこちら)。false: ワールド座標で揺らす。',
})
public useLocalSpace: boolean = true;
@property({
tooltip: 'エディタ上でのテスト用。ON にすると一度だけシェイクして自動で OFF に戻ります。',
})
public editorTestButton: boolean = false;
// 内部状態管理用
private _originalPosition: Vec3 = new Vec3();
private _shaking: boolean = false;
private _elapsed: number = 0;
private _shakeInterval: number = 0;
private _timeSinceLastShake: number = 0;
private _currentOffset: Vec3 = new Vec3();
onLoad() {
// 初期位置を保存しておく
if (this.useLocalSpace) {
this._originalPosition.set(this.node.position);
} else {
this._originalPosition.set(this.node.worldPosition);
}
// 振動間隔(何秒ごとに方向を変えるか)を計算
this._recalculateInterval();
}
start() {
// シーン開始時に自動再生したい場合
if (this.autoPlayOnStart) {
this.triggerShake();
}
}
update(deltaTime: number) {
// エディタ上のテストボタン処理
this._handleEditorTestButton();
if (!this._shaking) {
return;
}
// timeScale を考慮
const dt = deltaTime * this.timeScale;
this._elapsed += dt;
this._timeSinceLastShake += dt;
// シェイク終了判定
if (this._elapsed >= this.shakeDuration) {
this._shaking = false;
this._elapsed = 0;
this._timeSinceLastShake = 0;
this._currentOffset.set(0, 0, 0);
this._applyPosition();
return;
}
// 一定時間ごとにランダムな方向にオフセットを更新
if (this._timeSinceLastShake >= this._shakeInterval) {
this._timeSinceLastShake = 0;
this._updateRandomOffset();
}
// 現在のオフセットを適用
this._applyPosition();
}
/**
* 外部から呼び出してシェイクを開始する公開メソッド。
* 例: this.node.getComponent(ShakeUI)?.triggerShake();
*/
public triggerShake(): void {
if (this.shakeDuration <= 0 || this.shakeStrength <= 0) {
// 無効な設定の場合は何もしない
console.warn('[ShakeUI] shakeDuration または shakeStrength が 0 以下のため、シェイクは開始されません。', this.node.name);
return;
}
// 現在位置を基準として記録し直す(連続で呼ばれても破綻しないように)
if (this.useLocalSpace) {
this._originalPosition.set(this.node.position);
} else {
this._originalPosition.set(this.node.worldPosition);
}
this._shaking = true;
this._elapsed = 0;
this._timeSinceLastShake = this._shakeInterval; // すぐに最初のオフセットを生成させる
this._currentOffset.set(0, 0, 0);
}
/**
* エディタ上でのテスト用ボタン処理。
* editorTestButton を true にすると一度だけシェイクし、自動で false に戻す。
*/
private _handleEditorTestButton(): void {
if (this.editorTestButton) {
this.editorTestButton = false;
this.triggerShake();
}
}
/**
* 振動の更新間隔を再計算。
* shakeFrequency(回/秒) から 1 / frequency 秒ごとにオフセットを更新する。
*/
private _recalculateInterval(): void {
const freq = math.clamp(this.shakeFrequency, 1, 120);
this._shakeInterval = 1 / freq;
}
/**
* ランダムな方向・強さのオフセットを計算して _currentOffset に格納する。
*/
private _updateRandomOffset(): void {
// 残り時間に応じて強さを減衰させる
let strength = this.shakeStrength;
if (this.attenuate) {
const t = this._elapsed / this.shakeDuration; // 0.0〜1.0
const factor = 1.0 - t; // 線形減衰
strength *= factor;
}
if (strength <= 0) {
this._currentOffset.set(0, 0, 0);
return;
}
// -1〜1 のランダム方向ベクトルを2Dで生成
const offsetX = randomRange(-1, 1);
const offsetY = randomRange(-1, 1);
const len = Math.sqrt(offsetX * offsetX + offsetY * offsetY) || 1;
const nx = (offsetX / len) * strength;
const ny = (offsetY / len) * strength;
this._currentOffset.set(nx, ny, 0);
}
/**
* 現在の _originalPosition + _currentOffset をノードに適用する。
*/
private _applyPosition(): void {
const targetPos = new Vec3(
this._originalPosition.x + this._currentOffset.x,
this._originalPosition.y + this._currentOffset.y,
this._originalPosition.z + this._currentOffset.z,
);
if (this.useLocalSpace) {
this.node.setPosition(targetPos);
} else {
this.node.setWorldPosition(targetPos);
}
}
/**
* プロパティ変更時に呼ばれる(エディタ上のみ)。
* shakeFrequency 変更時に内部の更新間隔を再計算する。
*/
protected onValidate() {
this._recalculateInterval();
}
onDisable() {
// コンポーネントが無効化されたときは位置を元に戻しておく
if (this.useLocalSpace) {
this.node.setPosition(this._originalPosition);
} else {
this.node.setWorldPosition(this._originalPosition);
}
this._shaking = false;
}
onDestroy() {
// 破棄時も位置をリセットしておく(念のため)
if (this.node && this.node.isValid) {
if (this.useLocalSpace) {
this.node.setPosition(this._originalPosition);
} else {
this.node.setWorldPosition(this._originalPosition);
}
}
}
}
コードのポイント解説
onLoad()- アタッチされたノードの現在位置を
_originalPositionとして保存します。 - シェイク中はこの位置を基準にオフセットを加え、終了時には必ずこの位置に戻します。
shakeFrequencyから内部の_shakeInterval(オフセット更新間隔)を計算します。
- アタッチされたノードの現在位置を
start()autoPlayOnStartが true の場合、シーン開始時に自動でtriggerShake()を呼んで一度だけシェイクさせます。
update(deltaTime)- 毎フレーム、シェイク中かどうかを判定し、シェイク中であれば
- 経過時間の更新(
_elapsed) - オフセット更新タイミングの管理(
_timeSinceLastShake) - シェイク終了判定(
_elapsed >= shakeDuration) - 必要に応じてランダムオフセットの再計算
- 現在のオフセットをノードに適用
を行います。
- 経過時間の更新(
timeScaleを掛けているので、シェイク速度を簡単に調整できます。- また、毎フレーム
editorTestButtonを監視し、ON にされたら一度だけシェイクを実行して自動で OFF に戻します。
- 毎フレーム、シェイク中かどうかを判定し、シェイク中であれば
triggerShake()- 外部から呼び出すための公開メソッドです。
- 現在のノード位置を基準位置として再記録し、
_shakingを true にしてシェイクを開始します。 shakeDurationやshakeStrengthが 0 以下のときは、警告ログを出してシェイクを開始しないようにしています(防御的実装)。
_updateRandomOffset()- ランダムな2D方向ベクトル(-1〜1)を生成し、正規化してから
shakeStrengthを掛けてオフセットとしています。 attenuateが true の場合は、経過時間に応じて線形に強さを減衰させます。
- ランダムな2D方向ベクトル(-1〜1)を生成し、正規化してから
_applyPosition()- 内部で保持している
_originalPositionと_currentOffsetから最終的な目標位置を計算し、ノードに適用します。 useLocalSpaceに応じてsetPositionかsetWorldPositionを使い分けています。
- 内部で保持している
onDisable()/onDestroy()- コンポーネントが無効化されたり破棄されたときに、念のためノード位置を元の位置に戻すようにしています。
- これにより、シェイク途中でシーン遷移・非アクティブ化された場合でも、位置のずれが残ることを防ぎます。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで、シェイク用スクリプトを置きたいフォルダ(例:
assets/scripts/ui)を選択します。 - そのフォルダ内で右クリック → Create → TypeScript を選択します。
- 新しく作成されたファイルに
ShakeUI.tsという名前を付けます。 ShakeUI.tsをダブルクリックしてエディタ(VSCodeなど)で開き、先ほどのコードを丸ごと貼り付けて保存します。
2. テスト用のUIノードを作成
ここでは簡単な例として、HPバーやアイコンの代わりに Sprite を揺らしてみます。
- Hierarchy パネルで右クリック → Create → Canvas を選択して、2D UI 用の Canvas を作成します(既にある場合はこの手順は不要)。
- Canvas を選択した状態で、右クリック → Create → UI → Sprite を選択し、テスト用のスプライトノード(例:
TestIcon)を作成します。 - Inspector の Sprite コンポーネントで、任意の画像(アイコンなど)を SpriteFrame に設定しておきます。
この TestIcon ノードに ShakeUI をアタッチしていきます。
3. ShakeUI コンポーネントをアタッチ
- Hierarchy で
TestIconノードを選択します。 - Inspector の一番下にある Add Component ボタンをクリックします。
- 表示されたメニューから Custom → ShakeUI を選択します。
- もし Custom カテゴリに
ShakeUIが表示されない場合は、スクリプトの保存漏れや TypeScript ビルドエラーがないか確認してください。
- もし Custom カテゴリに
4. プロパティの設定例
ShakeUI をアタッチすると、Inspector に以下のようなプロパティが表示されます。
- Shake Duration(shakeDuration)
- 例:
0.45
- 例:
- Shake Strength(shakeStrength)
- 例:
20
- 例:
- Shake Frequency(shakeFrequency)
- 例:
50
- 例:
- Attenuate(attenuate)
- ON(チェックを入れる)
- Auto Play On Start(autoPlayOnStart)
- まずは ON にして、シーン開始時に自動再生されるか確認してみましょう。
- Time Scale(timeScale)
- 例:
1.0(標準速度)
- 例:
- Use Local Space(useLocalSpace)
- Canvas 配下の UI であれば ON のままで問題ありません。
- Editor Test Button(editorTestButton)
- 通常は OFF。テストしたいときにだけ ON にします。
5. シーン再生での動作確認
- エディタ上部の Play ボタンを押して、シーンを再生します。
autoPlayOnStartを ON にしている場合、シーン開始時にTestIconがガタガタと揺れるはずです。- 揺れが弱い/強すぎる場合は
- Shake Strength(例: 10〜30)
- Shake Duration(例: 0.2〜0.6)
- Shake Frequency(例: 20〜70)
を調整しながら、自分のゲームに合ったパラメータを探してみてください。
6. エディタ上での「Editor Test Button」による確認
再生せずに、エディタ上でざっくりと挙動を確認したい場合:
- シーンは再生してもしなくてもOKです(再生中の方がフレーム更新が安定します)。
TestIconノードを選択し、Inspector で Editor Test Button を ON にします。- 次のフレームで自動的に OFF に戻りつつ、一度だけシェイクが実行されます。
- パラメータを少し変えて、再度 Editor Test Button を ON → OFF させることで、素早く調整できます。
7. 他のスクリプトからダメージ時にシェイクする例
他のコンポーネントから ShakeUI を呼び出して使うときも、ShakeUI 側に外部依存はなく、単に triggerShake() を呼ぶだけです。
例: HPバーのノードに ShakeUI をアタッチしておき、「ダメージを受けたとき」にシェイクさせるスクリプト:
import { _decorator, Component, Node } from 'cc';
import { ShakeUI } from './ShakeUI';
const { ccclass, property } = _decorator;
@ccclass('DummyDamageExample')
export class DummyDamageExample extends Component {
@property({ tooltip: 'ダメージ時に揺らしたいノード(ShakeUI がアタッチされていること)' })
public targetNode: Node | null = null;
// ダメージを受けたときに呼ばれる想定のメソッド
public onDamaged(): void {
if (!this.targetNode) {
console.warn('[DummyDamageExample] targetNode が設定されていません。');
return;
}
const shaker = this.targetNode.getComponent(ShakeUI);
if (!shaker) {
console.warn('[DummyDamageExample] targetNode に ShakeUI がアタッチされていません。');
return;
}
shaker.triggerShake();
}
}
このように、ShakeUI 自体は完全に独立しており、どんなスクリプトからでも簡単に呼び出せます。
まとめ
- ShakeUI は、任意のノードにアタッチするだけで「位置シェイク演出」を実現できる汎用コンポーネントです。
- シェイクの時間・強さ・周波数・減衰・座標系をすべてインスペクタから調整できるため、ゲームごとに最適な揺れ方を簡単に作り込めます。
- 外部の GameManager やシングルトンに一切依存しないため、どのプロジェクトにもそのままコピペして再利用可能です。
- HPバー、アイコン、ボタン、警告ウィンドウなど、UI全般の「注目させたいタイミング」でさっと使い回せるのが利点です。
このように、1つのノードに完結した汎用コンポーネントを積み重ねていくことで、ゲーム全体の演出や挙動を「アタッチしてパラメータをいじるだけ」で組み立てられるようになり、開発効率が大きく向上します。
本記事の ShakeUI をベースに、スケールシェイク(拡大縮小)や回転シェイクなどのバリエーションも簡単に派生させられるので、ぜひプロジェクトに組み込んでみてください。




