【Cocos Creator 3.8】ScalePulse の実装:アタッチするだけでノードを周期的に拡大縮小させて「注目」を集める汎用スクリプト
UIボタンや取得可能なアイテムなど、「ここを見て!」とユーザーの視線を集めたい場面は多くあります。この記事では、任意のノードにアタッチするだけで、ノードの scale を一定のリズムで拡大縮小させる汎用コンポーネント ScalePulse を Cocos Creator 3.8(TypeScript)で実装します。
外部の GameManager などには一切依存せず、すべてインスペクタから設定可能なプロパティだけで動作するように設計します。
コンポーネントの設計方針
1. 機能要件の整理
- このコンポーネントをアタッチしたノードの
scaleを、時間経過に応じて周期的に拡大・縮小させる。 - 拡大・縮小の最小スケール・最大スケール・周期(スピード)をインスペクタから調整可能にする。
- 元のスケール(初期スケール)を基準に、相対的に拡大縮小できるようにする。
- ゲームの一時停止や演出の都合で、一時停止・再開を簡単に制御できるようにする。
- エディタ上でのプレビュー(エディタ実行時・ゲーム実行時の両方)で同じように動作する。
2. 外部依存をなくす設計アプローチ
- 外部のシングルトンや GameManager には一切依存しない。
- インスペクタの
@propertyだけで挙動をカスタマイズできるようにする。 update()内で時間経過(dt)を使ってスケールを補間し、Tween や他ノードに依存しない実装にする。- ノードが無効化されたとき・破棄されたときに、内部状態をリセットし、予期しないスケールが残らないようにする。
3. インスペクタで設定可能なプロパティ設計
以下のプロパティを用意します。
- enabledPulse: boolean
デフォルト:true
パルス(拡大縮小アニメーション)を有効にするかどうかのフラグ。実行中にオン/オフを切り替えることも可能。 - useInitialScaleAsBase: boolean
デフォルト:truetrue: ノードの初期スケールを基準に拡大縮小する。false:baseScaleプロパティで指定したスケールを基準にする。
- baseScale: Vec3
デフォルト:(1, 1, 1)
useInitialScaleAsBaseがfalseのときに使用する基準スケール。UI なら通常(1,1,1)を想定。 - minScaleMultiplier: number
デフォルト:0.9
基準スケールに対する最小倍率。例: 基準が(1,1,1)で0.9の場合、最小スケールは(0.9, 0.9, 0.9)。 - maxScaleMultiplier: number
デフォルト:1.1
基準スケールに対する最大倍率。例:1.1の場合、最大スケールは(1.1, 1.1, 1.1)。 - pulseDuration: number
デフォルト:0.8(秒)
1 回の「拡大 → 縮小」サイクルにかかる時間。値が小さいほど速く脈打つように拡大縮小する。 - playOnStart: boolean
デフォルト:true
start()時に自動でパルスを開始するかどうか。 - resetScaleOnDisable: boolean
デフォルト:true
コンポーネントやノードが無効化されたときに、スケールを基準スケールに戻すかどうか。 - axisX / axisY / axisZ: boolean
デフォルト:true, true, true
どの軸方向のスケールを変化させるか。2D UI では通常X, Yをtrue、Zは無視してもよい。
挙動のイメージ:
- 時間
tに応じて、sin波を使い、minScaleMultiplierとmaxScaleMultiplierの間を滑らかに往復する。 - サイクル時間は
pulseDurationで調整する。
TypeScriptコードの実装
以下が完成した ScalePulse.ts の全コードです。
import { _decorator, Component, Node, Vec3, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* ScalePulse
* 任意のノードにアタッチするだけで、ノードの scale を
* 一定のリズムで拡大縮小させる汎用コンポーネント。
*/
@ccclass('ScalePulse')
export class ScalePulse extends Component {
@property({
tooltip: 'パルス(拡大縮小アニメーション)を有効にするかどうか'
})
public enabledPulse: boolean = true;
@property({
tooltip: 'ノードの初期スケールを基準にするかどうか。\ntrue: 初期スケールを基準に拡大縮小\nfalse: 下の baseScale を基準に拡大縮小'
})
public useInitialScaleAsBase: boolean = true;
@property({
tooltip: '基準スケール。\nuseInitialScaleAsBase = false のときのみ使用されます。',
})
public baseScale: Vec3 = new Vec3(1, 1, 1);
@property({
tooltip: '最小スケール倍率(基準スケールに対する割合)。\n例: 0.9 の場合、基準スケールの 90% まで縮小します。'
})
public minScaleMultiplier: number = 0.9;
@property({
tooltip: '最大スケール倍率(基準スケールに対する割合)。\n例: 1.1 の場合、基準スケールの 110% まで拡大します。'
})
public maxScaleMultiplier: number = 1.1;
@property({
tooltip: '1 回の「拡大 → 縮小」サイクルにかかる時間(秒)。\n値が小さいほど速く脈打つように拡大縮小します。'
})
public pulseDuration: number = 0.8;
@property({
tooltip: 'シーン開始時(start)に自動でパルスを開始するかどうか。'
})
public playOnStart: boolean = true;
@property({
tooltip: 'コンポーネントまたはノードが無効化されたときに、スケールを基準スケールにリセットするかどうか。'
})
public resetScaleOnDisable: boolean = true;
@property({
tooltip: 'X 軸方向のスケールを変化させるかどうか。'
})
public axisX: boolean = true;
@property({
tooltip: 'Y 軸方向のスケールを変化させるかどうか。'
})
public axisY: boolean = true;
@property({
tooltip: 'Z 軸方向のスケールを変化させるかどうか。'
})
public axisZ: boolean = true;
// 内部状態
private _baseScaleInternal: Vec3 = new Vec3(1, 1, 1);
private _time: number = 0;
private _isPlaying: boolean = false;
onLoad() {
// ノードが存在することを前提にする(Component は必ず Node にアタッチされる)
if (!this.node) {
console.error('[ScalePulse] node が存在しません。コンポーネントは必ずノードにアタッチしてください。');
return;
}
// 初期スケールを記録
const currentScale = this.node.scale.clone();
if (this.useInitialScaleAsBase) {
this._baseScaleInternal.set(currentScale);
} else {
// baseScale を使用するが、0 や負の値は防ぐ
this._baseScaleInternal.set(
this._safeScaleValue(this.baseScale.x, 'baseScale.x'),
this._safeScaleValue(this.baseScale.y, 'baseScale.y'),
this._safeScaleValue(this.baseScale.z, 'baseScale.z'),
);
}
// min/max の関係を防御的にチェック
if (this.minScaleMultiplier > this.maxScaleMultiplier) {
console.warn('[ScalePulse] minScaleMultiplier が maxScaleMultiplier より大きいため、値を入れ替えます。');
const tmp = this.minScaleMultiplier;
this.minScaleMultiplier = this.maxScaleMultiplier;
this.maxScaleMultiplier = tmp;
}
if (this.pulseDuration <= 0) {
console.warn('[ScalePulse] pulseDuration が 0 以下のため、0.1 に補正します。');
this.pulseDuration = 0.1;
}
// 時間を初期化
this._time = 0;
}
start() {
// 開始時に自動再生するかどうか
if (this.playOnStart && this.enabledPulse) {
this.play();
} else {
this.stop();
}
}
update(dt: number) {
if (!this.enabledPulse || !this._isPlaying) {
return;
}
// 経過時間を更新
this._time += dt;
// pulseDuration を 1 サイクル(拡大 + 縮小)とみなす
const cycleTime = this.pulseDuration;
const normalizedTime = (this._time % cycleTime) / cycleTime; // 0.0 - 1.0
// 0 - 1 - 0 の三角波にしたいので、sin を使って滑らかに補間する
// sin(0) = 0, sin(π) = 0, sin(π/2) = 1
const angle = normalizedTime * Math.PI; // 0 - π
const sinValue = Math.sin(angle); // 0 - 1 - 0
// sinValue (0-1-0) を min-max の範囲にマッピング
const min = this.minScaleMultiplier;
const max = this.maxScaleMultiplier;
const scaleMultiplier = min + (max - min) * sinValue;
// 各軸ごとにスケールを適用
const base = this._baseScaleInternal;
const newScale = new Vec3(
this.axisX ? base.x * scaleMultiplier : this.node.scale.x,
this.axisY ? base.y * scaleMultiplier : this.node.scale.y,
this.axisZ ? base.z * scaleMultiplier : this.node.scale.z,
);
this.node.setScale(newScale);
}
onEnable() {
// 有効化時に playOnStart が true かつ enabledPulse が true なら再生
if (this.playOnStart && this.enabledPulse) {
this.play();
}
}
onDisable() {
// 無効化時にスケールをリセットするオプション
if (this.resetScaleOnDisable) {
this.resetScale();
}
this._isPlaying = false;
}
onDestroy() {
// 破棄時も念のためスケールをリセット(任意)
if (this.resetScaleOnDisable && this.node) {
this.resetScale();
}
}
/**
* パルスアニメーションを開始します。
*/
public play(): void {
this._isPlaying = true;
}
/**
* パルスアニメーションを停止します(現在のスケールはそのまま)。
*/
public stop(): void {
this._isPlaying = false;
}
/**
* スケールを基準スケールにリセットします。
*/
public resetScale(): void {
if (!this.node) {
return;
}
this.node.setScale(this._baseScaleInternal);
}
/**
* スケール値に対する防御的チェック。
* 0 以下の値が指定された場合は 0.0001 に補正し、警告を出します。
*/
private _safeScaleValue(value: number, name: string): number {
if (value <= 0) {
console.warn(`[ScalePulse] ${name} が 0 以下です。見えなくなる可能性があるため、0.0001 に補正します。`);
return 0.0001;
}
return value;
}
}
コードの要点解説
- onLoad()
- ノードの現在のスケールを取得し、
useInitialScaleAsBaseの設定に応じて内部用の基準スケール_baseScaleInternalを決定します。 minScaleMultiplierとmaxScaleMultiplierの大小関係をチェックし、逆なら入れ替えて防御的に補正します。pulseDurationが 0 以下の場合は 0.1 秒に補正します。
- ノードの現在のスケールを取得し、
- start()
playOnStartとenabledPulseの両方がtrueのとき、自動的にパルスを開始します。- そうでない場合は
stop()で再生フラグをオフにします。
- update(dt)
enabledPulseと内部フラグ_isPlayingの両方がtrueのときのみ処理します。_timeにdtを加算し、pulseDurationを 1 サイクルとして 0〜1 の正規化時間normalizedTimeを計算します。sin関数を使って 0→1→0 の滑らかな値sinValueを作り、minScaleMultiplier〜maxScaleMultiplierの間を補間します。- 各軸の有効フラグ(
axisX/Y/Z)に応じて、新しいスケールを計算しnode.setScale()で適用します。
- onEnable() / onDisable() / onDestroy()
- 有効化時には
playOnStartとenabledPulseを見て自動再生します。 - 無効化時には、
resetScaleOnDisableがtrueの場合にスケールを基準スケールに戻します。 - 破棄時にも念のためスケールをリセットしておくことで、エディタ上でのプレビューなどでも予期しないスケールが残りにくくなります。
- 有効化時には
- play() / stop() / resetScale()
- 外部(他スクリプト)からも呼び出せる制御メソッドですが、このコンポーネント単体でもインスペクタの設定だけで十分に運用できます。
resetScale()は、エディタの「Call Function」などから呼び出して、スケールを元に戻す用途にも使えます。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - 右クリックして Create → TypeScript を選択します。
- 新しく作成されたファイルの名前を
ScalePulse.tsに変更します。 ScalePulse.tsをダブルクリックして開き、既存のテンプレートコードをすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成(UIボタンの例)
UI ボタンに適用して、クリックを誘うようなパルスアニメーションにしてみます。
- Hierarchy パネルで、すでに Canvas がある場合はそれを使用し、なければメニューから Create → UI → Canvas で作成します。
- Canvas を選択した状態で、メニューから Create → UI → Button を選択し、テスト用のボタンを作成します。
- 作成された Button ノード(例:
Button)を選択します。
3. ScalePulse コンポーネントのアタッチ
- Button ノードを選択した状態で、Inspector パネルの下部にある Add Component ボタンをクリックします。
- Custom Component(または Custom)の中から ScalePulse を選択して追加します。
※表示されない場合は、スクリプトのコンパイルが終わるまで数秒待ってから再度確認してください。
4. インスペクタでプロパティを設定
Button ノードの ScalePulse コンポーネントのプロパティを、まずは以下のように設定してみてください。
- Enabled Pulse:
true - Use Initial Scale As Base:
true
(ボタンの初期スケールを基準にします) - Base Scale:
(1, 1, 1)
(上記設定では使用されませんが、念のため 1,1,1 にしておきます) - Min Scale Multiplier:
0.9 - Max Scale Multiplier:
1.1 - Pulse Duration:
0.8
(ゆっくりとした脈動) - Play On Start:
true - Reset Scale On Disable:
true - Axis X:
true - Axis Y:
true - Axis Z:
false(2D UI なら Z は無視で OK)
5. 再生して動作確認
- エディタ右上の Play(▶) ボタンを押してゲームを実行します。
- シーンが再生されると、対象の Button ノードが基準スケールの 90%〜110% の間で、約 0.8 秒周期でゆっくりと拡大縮小するはずです。
- Pulse Duration を
0.3くらいに変更してみると、より速い点滅のような動きになります。 - Min Scale Multiplier を
0.7、Max Scale Multiplier を1.3に変更すると、より大きく鼓動するような動きになります。
6. 3D オブジェクトでの使用例
3D アイテム(例: 宝箱やコイン)に対しても同じコンポーネントをそのまま使えます。
- Hierarchy で 3D オブジェクト(例: Create → 3D Object → Cube)を作成します。
- Cube ノードを選択し、Add Component → Custom → ScalePulse を追加します。
- 3D の場合は Axis X/Y/Z をすべて
trueにしておくと、全方向に拡大縮小します。 - ゲームを再生して、オブジェクトが脈動するように拡大縮小することを確認します。
7. よくある調整パターン
- ほんの少しだけ目立たせたい UI ボタン
- Min:
0.95 - Max:
1.05 - Pulse Duration:
1.0
- Min:
- 強く主張したい「ガチャ」ボタンなど
- Min:
0.8 - Max:
1.2 - Pulse Duration:
0.5
- Min:
- 取得可能なアイテム(コインなど)
- Min:
0.9 - Max:
1.2 - Pulse Duration:
0.6
- Min:
まとめ
この記事では、任意のノードにアタッチするだけでスケールを周期的に拡大縮小させる汎用コンポーネント ScalePulse を Cocos Creator 3.8(TypeScript)で実装しました。
- 外部の GameManager や Tween、他ノードに依存せず、このスクリプト単体で完結する構成。
- インスペクタから 最小/最大倍率・周期・軸・自動再生・リセット挙動 を柔軟に設定可能。
- UI ボタン、3D アイテム、アイコン、ポップアップなど、さまざまな場面で「注目させたい要素」にそのまま再利用できる。
このような「アタッチするだけで完結する汎用コンポーネント」を積み重ねておくと、演出の追加・調整がエディタ上のプロパティ操作だけで済むようになり、プロジェクト全体の開発効率が大きく向上します。
本コンポーネントをベースに、色の点滅や位置の揺れなど、他の表現にも発展させてみてください。




