【Cocos Creator 3.8】HoverScale の実装:アタッチするだけで「ホバー時にふわっと拡大する」強調演出を実現する汎用スクリプト
ボタンやアイコンにマウスカーソルを乗せたとき、少しだけ大きくなってユーザーに「押せる」ことを伝える演出は、UI の基本テクニックです。この記事では、任意のノードにアタッチするだけで「ホバー時に拡大」「ホバー解除で元の大きさに戻す」アニメーションを行う汎用コンポーネント HoverScale を TypeScript で実装します。
他のスクリプトや GameManager に依存せず、このコンポーネント単体で完結する設計になっているので、どのプロジェクトでも簡単に再利用できます。
コンポーネントの設計方針
要件整理
- ノードにマウスカーソルが乗ったときに、スケールを少し大きくする。
- カーソルが離れたら、元のスケールにスムーズに戻す。
- UI ボタン(
Button)だけでなく、任意のノード(アイコン、画像、ラベルなど)に使える。 - 拡大率・アニメーション時間・補間の滑らかさなどをインスペクタから調整可能にする。
- 他のカスタムスクリプトへの依存を一切持たず、このコンポーネント単体で完結させる。
- マウスホバー検知には
UITransform+EventTargetベースの UI イベント(Node.EventType.MOUSE_ENTER/MOUSE_LEAVE)を利用する。 - 必須コンポーネント(
UITransform)が無い場合はエラーログを出して動作を止める(防御的実装)。
ホバー検知のアプローチ
Cocos Creator 3.x では、画面上の UI ノードに対してマウスイベントを受け取るには、通常以下の条件が必要です。
- ノードに
UITransformコンポーネントが付いている。 - Canvas 上に配置されており、イベントシステム(
EventSystem)が有効。
本コンポーネントでは、対象ノードに UITransform が付いていることを前提とし、Node.EventType.MOUSE_ENTER と Node.EventType.MOUSE_LEAVE を利用してホバー状態を検知します。
アニメーションのアプローチ
- 拡大・縮小は
update内で 線形補間(lerp) によってスムーズに行う。 - 開始時にノードの「元のスケール」を記録し、ホバー中は「元スケール × 拡大率」、ホバー解除時は「元スケール」に戻す。
- 複数回ホバーしても、常に「最初のスケール」を基準に動作するようにする(途中でスケールを変えても破綻しないように)。
インスペクタで設定可能なプロパティ設計
- scaleFactor: number
- ツールチップ: 「ホバー時の拡大倍率。1.0 で変化なし、1.1 で 10% 拡大。」
- 役割: ホバー時にどれだけ大きくするかを指定する倍率。
- 例: 1.05 ~ 1.2 程度が UI ではよく使われる。
- animationDuration: number
- ツールチップ: 「拡大・縮小にかける時間(秒)。0 にすると即座に切り替え。」
- 役割: 拡大・縮小が完了するまでの目安時間。
- 例: 0.08 ~ 0.2 秒程度が軽快な UI に向いている。
- smoothness: number
- ツールチップ: 「補間の滑らかさ。1 に近いほどゆっくり収束し、0 に近いほどカクッと動く。」
- 役割:
lerpに使う係数。0 < smoothness <= 1を想定。 - 例: 0.2~0.4 くらいが自然な動きになりやすい。
- enableOnTouch: boolean
- ツールチップ: 「タッチデバイスでも押下中に拡大させるかどうか。」
- 役割: スマホなどマウスホバーが無い環境で、
TOUCH_START/TOUCH_ENDを使って同様の演出をするかどうか。
- debugLog: boolean
- ツールチップ: 「true にするとホバー状態の変更をログ出力します(開発用)。」
- 役割: イベントが正しく発火しているかデバッグするためのオプション。
TypeScriptコードの実装
import { _decorator, Component, Node, UITransform, Vec3, EventMouse, EventTouch, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* HoverScale
* 任意のノードにアタッチするだけで、
* - マウスホバー時に拡大
* - ホバー解除で元のサイズに戻す
* という UI 演出を行うコンポーネント。
*/
@ccclass('HoverScale')
export class HoverScale extends Component {
@property({
tooltip: 'ホバー時の拡大倍率。\n1.0 で変化なし、1.1 で 10% 拡大します。',
min: 0.1,
step: 0.01
})
public scaleFactor: number = 1.1;
@property({
tooltip: '拡大・縮小にかける時間(秒)。\n0 にすると即座に切り替わります。',
min: 0,
step: 0.01
})
public animationDuration: number = 0.1;
@property({
tooltip: '補間の滑らかさ。\n0 に近いほどカクッと動き、1 に近いほどゆっくり収束します。\n0 < 値 <= 1 を推奨します。',
min: 0.01,
max: 1,
step: 0.01
})
public smoothness: number = 0.3;
@property({
tooltip: 'タッチデバイスでも押下中に拡大させるかどうか。\ntrue の場合、TOUCH_START/END/CANCEL でも拡大・縮小します。'
})
public enableOnTouch: boolean = true;
@property({
tooltip: 'true にすると、ホバー開始・終了などの状態変化をログ出力します(デバッグ用)。'
})
public debugLog: boolean = false;
// 内部状態
private _uiTransform: UITransform | null = null;
private _originalScale: Vec3 = new Vec3(1, 1, 1);
private _targetScale: Vec3 = new Vec3(1, 1, 1);
private _currentScale: Vec3 = new Vec3(1, 1, 1);
private _isHovering: boolean = false;
private _isTouching: boolean = false;
onLoad() {
// 必須コンポーネントの取得
this._uiTransform = this.node.getComponent(UITransform);
if (!this._uiTransform) {
warn('[HoverScale] UITransform コンポーネントが見つかりません。このノードに UITransform を追加してください。');
}
// 初期スケールを保存
this._originalScale = this.node.scale.clone();
this._currentScale = this.node.scale.clone();
this._targetScale = this.node.scale.clone();
// マウスイベント登録
this.node.on(Node.EventType.MOUSE_ENTER, this._onMouseEnter, this);
this.node.on(Node.EventType.MOUSE_LEAVE, this._onMouseLeave, this);
// タッチイベント登録(オプション)
if (this.enableOnTouch) {
this.node.on(Node.EventType.TOUCH_START, this._onTouchStart, this);
this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
this.node.on(Node.EventType.TOUCH_CANCEL, this._onTouchCancel, this);
}
}
onEnable() {
// 有効化時にスケールを元に戻す
this._originalScale = this.node.scale.clone();
this._currentScale = this.node.scale.clone();
this._targetScale = this.node.scale.clone();
this._isHovering = false;
this._isTouching = false;
}
onDisable() {
// 無効化時にスケールを元に戻す
this.node.setScale(this._originalScale);
this._currentScale = this._originalScale.clone();
this._targetScale = this._originalScale.clone();
this._isHovering = false;
this._isTouching = false;
}
onDestroy() {
// イベントの解除(メモリリーク防止)
this.node.off(Node.EventType.MOUSE_ENTER, this._onMouseEnter, this);
this.node.off(Node.EventType.MOUSE_LEAVE, this._onMouseLeave, this);
this.node.off(Node.EventType.TOUCH_START, this._onTouchStart, this);
this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
this.node.off(Node.EventType.TOUCH_CANCEL, this._onTouchCancel, this);
}
update(deltaTime: number) {
// アニメーション時間が 0 の場合は即座に切り替え
if (this.animationDuration <= 0) {
if (!this._currentScale.equals(this._targetScale)) {
this._currentScale.set(this._targetScale);
this.node.setScale(this._currentScale);
}
return;
}
// 現在スケールをターゲットスケールへ補間
// lerpFactor は deltaTime と animationDuration, smoothness から計算
const t = Math.min(1, (deltaTime / this.animationDuration) * this.smoothness);
// 線形補間: current = current + (target - current) * t
this._currentScale.x += (this._targetScale.x - this._currentScale.x) * t;
this._currentScale.y += (this._targetScale.y - this._currentScale.y) * t;
this._currentScale.z += (this._targetScale.z - this._currentScale.z) * t;
this.node.setScale(this._currentScale);
}
// ========================
// イベントハンドラ
// ========================
private _onMouseEnter(event: EventMouse) {
this._isHovering = true;
this._updateTargetScale();
if (this.debugLog) {
log('[HoverScale] MOUSE_ENTER on node:', this.node.name);
}
}
private _onMouseLeave(event: EventMouse) {
this._isHovering = false;
this._updateTargetScale();
if (this.debugLog) {
log('[HoverScale] MOUSE_LEAVE on node:', this.node.name);
}
}
private _onTouchStart(event: EventTouch) {
if (!this.enableOnTouch) {
return;
}
this._isTouching = true;
this._updateTargetScale();
if (this.debugLog) {
log('[HoverScale] TOUCH_START on node:', this.node.name);
}
}
private _onTouchEnd(event: EventTouch) {
if (!this.enableOnTouch) {
return;
}
this._isTouching = false;
this._updateTargetScale();
if (this.debugLog) {
log('[HoverScale] TOUCH_END on node:', this.node.name);
}
}
private _onTouchCancel(event: EventTouch) {
if (!this.enableOnTouch) {
return;
}
this._isTouching = false;
this._updateTargetScale();
if (this.debugLog) {
log('[HoverScale] TOUCH_CANCEL on node:', this.node.name);
}
}
/**
* 現在の状態(ホバー中 or タッチ中)に応じてターゲットスケールを更新する。
*/
private _updateTargetScale() {
const shouldScaleUp = this._isHovering || this._isTouching;
if (shouldScaleUp) {
// 元スケールに倍率をかけた値をターゲットにする
this._targetScale.set(
this._originalScale.x * this.scaleFactor,
this._originalScale.y * this.scaleFactor,
this._originalScale.z
);
} else {
// 元スケールに戻す
this._targetScale.set(this._originalScale);
}
}
}
コードのポイント解説
- onLoad
UITransformを取得し、無ければwarnログを出します(ホバーイベント自体は受け取れますが、UI ノードとしての設定漏れに気付きやすくするため)。- 初期スケールを
_originalScaleとして保存し、_currentScale/_targetScaleを初期化します。 - マウスイベント(
MOUSE_ENTER/MOUSE_LEAVE)と、必要に応じてタッチイベントを登録します。
- onEnable / onDisable
- コンポーネントが有効化されたときに状態をリセットし、無効化されたときにはスケールを元に戻しておきます。
- update
animationDurationが 0 以下なら、ターゲットスケールへ即座にジャンプします。- それ以外の場合は、
deltaTimeとanimationDuration、smoothnessを使って補間係数tを計算し、線形補間で_currentScaleを_targetScaleに近づけていきます。
- _updateTargetScale
_isHoveringまたは_isTouchingのどちらかが true の場合に拡大、両方 false の場合は元スケールに戻す、という単純な状態管理です。- 常に
_originalScaleを基準にしているため、途中でノードのスケールを変更しても、次回有効化時に改めてそのスケールを基準として動作します。
使用手順と動作確認
1. スクリプトファイルを作成する
- エディタの Assets パネルで、任意のフォルダ(例:
scripts/ui)を右クリックします。 - Create > TypeScript を選択します。
- ファイル名を
HoverScale.tsに変更します。 - 作成された
HoverScale.tsをダブルクリックして開き、内容をすべて削除してから、上記の TypeScript コードを貼り付けて保存します。
2. テスト用の UI ノードを作成する
- Hierarchy パネルで Canvas を右クリックし、Create > UI > Button を選択してテスト用のボタンを作成します。
- あるいは Create > UI > Sprite や Label など、任意の UI ノードでも構いません。
- 作成したノードを選択し、Inspector で
UITransformコンポーネントが付いていることを確認します(Button / Sprite / Label であれば自動的に付いています)。
3. HoverScale コンポーネントをアタッチする
- テスト用ノード(例:
Button)を選択した状態で、Inspector の下部にある Add Component ボタンをクリックします。 - Custom カテゴリの中から HoverScale を選択します。
- 見つからない場合は、エディタ右上の Compile アイコンでスクリプトのコンパイルが完了しているか確認してください。
4. プロパティを設定する
HoverScale をアタッチすると、Inspector に以下のプロパティが表示されます。
- Scale Factor(
scaleFactor)- 例:
1.1(10% 拡大) - 派手にしたい場合は
1.2などにしてみてください。
- 例:
- Animation Duration(
animationDuration)- 例:
0.1秒 - 瞬時に切り替えたい場合は
0に設定します。
- 例:
- Smoothness(
smoothness)- 例:
0.3 - 値を大きくするとゆっくり、値を小さくするとキビキビした動きになります。
- 例:
- Enable On Touch(
enableOnTouch)- PC ブラウザのみでテストする場合はどちらでも構いませんが、スマホでも使う場合は
trueのままにしておきます。
- PC ブラウザのみでテストする場合はどちらでも構いませんが、スマホでも使う場合は
- Debug Log(
debugLog)- イベント発火を確認したいときは
trueにして、Console にログが出るか確認します。
- イベント発火を確認したいときは
5. シーンを再生して動作を確認する
- エディタ上部の Play ボタンをクリックしてシーンを再生します。
- ゲームビュー上で、HoverScale をアタッチしたノード(ボタンやアイコン)にマウスカーソルを乗せます。
- 設定した
scaleFactorに応じて、ふわっと拡大するはずです。
- 設定した
- カーソルを外すと、元のサイズにスムーズに戻ることを確認します。
- スマホやタブレットでテストする場合は、対象ノードをタップしている間だけ拡大し、指を離すと元に戻ることを確認します(
enableOnTouchが true の場合)。 - うまく動かない場合は、以下を確認してください。
- ノードが Canvas の子孫になっているか。
- ノードに
UITransformが付いているか。 - ほかのスクリプトがノードのスケールを毎フレーム変更していないか。
Debug Logを true にして、MOUSE_ENTER/MOUSE_LEAVE/TOUCH_STARTなどのログが出ているか。
まとめ
この記事では、Cocos Creator 3.8 で使える汎用 UI コンポーネント HoverScale を実装しました。
- 任意のノードにアタッチするだけで、マウスホバーやタッチ押下時にスケールを拡大し、離すと元に戻す UI 演出を実現。
- 拡大倍率・アニメーション時間・補間の滑らかさ・タッチ対応の有無をインスペクタから簡単に調整可能。
- 他のカスタムスクリプトへの依存が一切なく、このコンポーネント単体で完結する設計。
- UI ボタン、アイコン、リストアイテム、カードなど、さまざまな UI 要素に使い回せる。
ボタンに「押せる感」を出したり、重要なアイコンだけ少し大きく反応させたりと、UI の体験を手軽に向上させられます。プロジェクトのテンプレートに組み込んでおけば、新しい画面を作るたびに同じロジックを実装する必要がなくなり、開発効率も上がります。
この HoverScale をベースに、クリック時の軽い縮小や色変更、SE 再生などを組み合わせれば、よりリッチな UI 演出コンポーネントも簡単に拡張していけるはずです。




