【Cocos Creator 3.8】OutlineHighlight の実装:アタッチするだけで「マウスオーバー時に白い縁取りを表示」できる汎用スクリプト
この記事では、ノードにアタッチするだけで「マウスカーソルが乗ったときに白い2pxアウトラインを表示」できる汎用コンポーネント OutlineHighlight を実装します。
マウスで選択できるオブジェクトや、ホバー時に強調したいボタン・アイテム・キャラクターなどにそのまま使えます。外部の GameManager やシングルトンには一切依存せず、このコンポーネント単体で完結するように設計します。
コンポーネントの設計方針
機能要件の整理
- このコンポーネントをアタッチしたノードに対して、マウスカーソルが乗っている間だけアウトライン(縁取り)を表示する。
- アウトラインは シェーダー(Effect / Material)ベース で描画する。
- 2D UI / Sprite を主ターゲットとし、
SpriteまたはUIRendererを持つノードに対応する。 - コンポーネントは完全に独立しており、他のカスタムスクリプトには依存しない。
- マウス座標とノードの AABB(軸平行境界ボックス)を使って簡易的な矩形ヒット判定を行う。
- アウトラインの太さ・色・フェード時間などは
@property経由でインスペクタから調整可能にする。 - 標準コンポーネント(Sprite / UIRenderer / Camera など)がなければ、コンソールに警告を出し、安全に動作を諦める。
インスペクタで設定可能なプロパティ設計
以下のようなプロパティを用意します。
- enableHighlight (boolean)
– デフォルト:true
– 全体の ON / OFF スイッチ。テスト時に一時的に無効化したいときに便利。 - outlineColor (Color)
– デフォルト:Color.WHITE
– アウトラインの色。白以外にも、黄色・赤など自由に設定可能。 - outlineWidth (number)
– デフォルト:2(ピクセル)
– アウトラインの太さ。ピクセル単位として扱い、シェーダー側では UV スケールに変換して使用。 - fadeDuration (number)
– デフォルト:0.1秒
– マウスが乗った / 離れたときに、アウトラインの強さを補間する時間。
– 0 にすると即座に ON / OFF が切り替わる。 - materialTemplate (Material | null)
– デフォルト:null
– アウトライン用のカスタムマテリアル(Effect付き) を指定するスロット。
– ここに設定されたマテリアルをclone()して、対象ノード専用のインスタンスを作成し、パラメータ(色や太さ)を制御する。 - targetRenderer (UIRenderer | null)
– デフォルト:null
– アウトラインを適用する対象のレンダラー。
– 未指定の場合はthis.node.getComponent(UIRenderer)で自動取得を試みる。 - useWorldCamera (boolean)
– デフォルト:true
–Camera.mainを使ってワールド空間でのマウス位置を判定するかどうか。
– UI カメラなど別のカメラを使いたい場合は OFF にして、customCameraを指定する。 - customCamera (Camera | null)
– デフォルト:null
– 特定のカメラでマウス座標をレイキャストしたい場合に指定。
–useWorldCamera = falseのときのみ使用される。
これらのプロパティにより、外部の管理スクリプトに頼らずに、インスペクタだけで完結して調整できるようにします。
TypeScriptコードの実装
ここでは、OutlineHighlight.ts として動作する完全なコードを掲載します。
import {
_decorator,
Component,
Node,
UIRenderer,
Material,
Color,
Camera,
input,
Input,
EventMouse,
Vec2,
Vec3,
geometry,
PhysicsSystem,
view,
UITransform,
math,
log,
warn,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* OutlineHighlight
* マウスオーバー時にアウトラインを表示する汎用コンポーネント
*
* 前提:
* - 対象ノードには UIRenderer (Sprite, Label, RichText, etc.) が付いていること
* - outline 用のカスタム Material を用意しておき、materialTemplate に設定すること
* - Effect 側で以下の uniform を用意しておくとこのスクリプトと連携しやすい:
* - uniform vec4 outlineColor;
* - uniform float outlineWidth; // ピクセルベース
* - uniform float outlineStrength; // 0.0 ~ 1.0
*/
@ccclass('OutlineHighlight')
export class OutlineHighlight extends Component {
@property({
tooltip: 'コンポーネント全体の有効/無効スイッチ。false にすると処理を行いません。',
})
public enableHighlight: boolean = true;
@property({
tooltip: 'アウトラインの色。デフォルトは白。',
})
public outlineColor: Color = Color.WHITE.clone();
@property({
tooltip: 'アウトラインの太さ(ピクセル単位のイメージ)。2 で 2px 程度の縁取りになります。',
min: 0,
step: 0.5,
})
public outlineWidth: number = 2.0;
@property({
tooltip: 'マウスオーバー/離脱時のフェード時間(秒)。0 にすると即時切り替え。',
min: 0,
step: 0.01,
})
public fadeDuration: number = 0.1;
@property({
type: Material,
tooltip: 'アウトライン用のカスタムマテリアル。必ず Effect に outlineColor / outlineWidth / outlineStrength の uniform を用意してください。',
})
public materialTemplate: Material | null = null;
@property({
type: UIRenderer,
tooltip: 'アウトラインを適用する対象 UIRenderer。未設定の場合はこのノードから自動取得を試みます。',
})
public targetRenderer: UIRenderer | null = null;
@property({
tooltip: 'true: Camera.main を使用してマウス座標をワールド座標に変換します。',
})
public useWorldCamera: boolean = true;
@property({
type: Camera,
tooltip: 'useWorldCamera が false の場合に使用するカスタムカメラ。',
})
public customCamera: Camera | null = null;
// 内部状態
private _instanceMaterial: Material | null = null;
private _hovered: boolean = false;
private _currentStrength: number = 0.0; // 0.0 ~ 1.0
private _targetStrength: number = 0.0; // 0.0 or 1.0
private _mousePos: Vec2 = new Vec2();
onLoad() {
// UIRenderer の取得
if (!this.targetRenderer) {
this.targetRenderer = this.node.getComponent(UIRenderer);
}
if (!this.targetRenderer) {
warn('[OutlineHighlight] UIRenderer が見つかりません。このコンポーネントを使用するには Sprite や Label などの UIRenderer をアタッチしてください。 Node:', this.node.name);
return;
}
// マテリアルの準備
if (!this.materialTemplate) {
warn('[OutlineHighlight] materialTemplate が設定されていません。アウトライン用の Material をインスペクタから指定してください。 Node:', this.node.name);
return;
}
// Material インスタンスを複製して、このノード専用にする
this._instanceMaterial = this.materialTemplate.clone();
this.targetRenderer.setMaterial(this._instanceMaterial, 0);
// 初期 uniform 設定
this._applyMaterialProperties(0.0);
// マウス入力リスナー登録
input.on(Input.EventType.MOUSE_MOVE, this._onMouseMove, this);
}
start() {
// ここでは特に何もしないが、将来的な拡張用に分けておく
}
onDestroy() {
// 入力リスナー解除
input.off(Input.EventType.MOUSE_MOVE, this._onMouseMove, this);
}
/**
* 毎フレーム、マウス位置とノードの AABB を使ってヒット判定し、
* アウトラインの強さを補間します。
*/
update(dt: number) {
if (!this.enableHighlight) {
// 無効化時はアウトラインを完全にオフにして終了
if (this._currentStrength !== 0.0) {
this._currentStrength = 0.0;
this._targetStrength = 0.0;
this._applyMaterialProperties(this._currentStrength);
}
return;
}
if (!this.targetRenderer || !this._instanceMaterial) {
// 必要コンポーネントがなければ何もしない
return;
}
// マウスがこのノードの矩形内に入っているかチェック
this._hovered = this._checkMouseInside();
// 目標強度を設定(ホバー中:1, 非ホバー:0)
this._targetStrength = this._hovered ? 1.0 : 0.0;
// フェード処理
if (this.fadeDuration > 0) {
const diff = this._targetStrength - this._currentStrength;
if (Math.abs(diff) > 0.0001) {
const step = dt / this.fadeDuration;
this._currentStrength += math.clamp(diff, -step, step);
this._currentStrength = math.clamp01(this._currentStrength);
this._applyMaterialProperties(this._currentStrength);
}
} else {
// 即時切り替え
if (this._currentStrength !== this._targetStrength) {
this._currentStrength = this._targetStrength;
this._applyMaterialProperties(this._currentStrength);
}
}
}
/**
* マウス移動イベント。常に最新のマウス座標を保持しておく。
*/
private _onMouseMove(event: EventMouse) {
event.getLocation(this._mousePos);
}
/**
* 現在のマウス座標が、このノードの矩形(UITransform)内にあるかどうかを判定する。
*/
private _checkMouseInside(): boolean {
const uiTrans = this.node.getComponent(UITransform);
if (!uiTrans) {
// UITransform がない場合は AABB を簡易的に使用するが、
// UI ベースを想定しているのでここでは警告して false を返す。
warn('[OutlineHighlight] UITransform が見つかりません。矩形ヒット判定が行えません。 Node:', this.node.name);
return false;
}
// 画面座標 (マウス) → ワールド座標に変換
const worldPos = this._screenToWorld(this._mousePos);
if (!worldPos) {
return false;
}
// UITransform の矩形にワールド座標点が含まれるかどうか
return uiTrans.hitTest(worldPos);
}
/**
* 画面座標 (スクリーン座標) をワールド座標に変換する。
*/
private _screenToWorld(screenPos: Vec2): Vec3 | null {
let cam: Camera | null = null;
if (this.useWorldCamera) {
cam = Camera.main;
} else {
cam = this.customCamera;
}
if (!cam) {
warn('[OutlineHighlight] 有効な Camera が見つかりません。Camera.main も customCamera も利用できません。');
return null;
}
const out = new Vec3();
// スクリーン座標からワールド座標へ変換
cam.screenToWorld(screenPos, out);
return out;
}
/**
* Material に対してアウトライン関連の uniform をまとめて反映する。
*/
private _applyMaterialProperties(strength: number) {
if (!this._instanceMaterial) {
return;
}
// 色
const c = this.outlineColor;
this._instanceMaterial.setProperty('outlineColor', new Color(c.r, c.g, c.b, c.a));
// 太さ(ピクセルベース)。Effect 側で適切にスケールして使用する想定。
this._instanceMaterial.setProperty('outlineWidth', this.outlineWidth);
// 強度(0.0 ~ 1.0)
this._instanceMaterial.setProperty('outlineStrength', strength);
}
}
主要メソッドのポイント解説
- onLoad()
–targetRendererが未設定ならthis.node.getComponent(UIRenderer)で自動取得。なければ警告を出して終了。
–materialTemplateが未設定なら警告を出して終了。
–materialTemplate.clone()でインスタンスを作成し、setMaterial()で適用。
– マウス移動イベントを購読して、常に最新のマウス座標を保持する。 - update(dt)
–enableHighlightが false のときはアウトラインを完全にオフにする。
–_checkMouseInside()でマウスがノードの矩形内にいるかをチェックし、_hoveredを更新。
–fadeDurationに応じて_currentStrengthを_targetStrengthに向かって補間し、_applyMaterialProperties()でマテリアルに反映。 - _checkMouseInside()
–UITransformを取得して、screenToWorld()で変換したマウス座標に対してhitTest()を行う。
– UI 系ノードであればこれだけで矩形ヒット判定ができる。 - _screenToWorld()
–useWorldCameraが true の場合はCamera.main、false の場合はcustomCameraを使ってスクリーン座標→ワールド座標変換。 - _applyMaterialProperties()
– マテリアルの uniformoutlineColor,outlineWidth,outlineStrengthをまとめて更新する。
– Effect 側で同名の uniform を定義しておくことで、シェーダーから利用できる。
使用手順と動作確認
ここでは、実際に Cocos Creator 3.8.7 のエディタで OutlineHighlight コンポーネントを使う手順を、マテリアル(シェーダー)の準備も含めて解説します。
1. アウトライン用 Effect / Material の準備
まずは、アウトラインを描画するためのカスタムシェーダー(Effect)と Material を用意します。
- Assets パネルで右クリック → Create → Effect を選択し、
ファイル名をoutline-sprite.effectなどにします。 - 作成した Effect をダブルクリックして開き、Sprite 用のアウトラインシェーダー を記述します。
ここでは詳細なシェーダー実装は割愛しますが、最低限以下のような uniform を定義してください。uniform vec4 outlineColor;uniform float outlineWidth;(ピクセルベースとして扱う)uniform float outlineStrength;(0.0 ~ 1.0)
そして、outlineStrength が 0 のときは通常描画、1 のときは最大アウトラインになるようにフラグメントシェーダー内で処理します。
- Effect を保存したら、Assets パネルで右クリック → Create → Material を選択し、
ファイル名をOutlineSprite.matなどにします。 OutlineSprite.matを選択し、Inspector の Effect スロットに先ほど作成したoutline-sprite.effectを設定します。- 必要に応じて、デフォルトの
outlineColorやoutlineWidthをここで設定しておいても構いません(スクリプトから上書きされます)。
これで、OutlineHighlight コンポーネントが参照するための Material が完成しました。
2. OutlineHighlight.ts を作成してコードを貼り付け
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を OutlineHighlight.ts に変更します。
- ダブルクリックで開き、先ほど掲載した
OutlineHighlightクラスのコードを全て貼り付けて保存します。
3. テスト用ノード(Sprite)を作成
- Hierarchy パネルで右クリック → Create → UI → Canvas を作成(既にある場合は不要)。
- Canvas を選択し、その子として右クリック → Create → UI → Sprite を作成します。
- 作成した Sprite ノードを選択し、Inspector で以下を確認します。
- Sprite コンポーネントが付いている(= UIRenderer を持っている)。
- 画像(SpriteFrame)が設定されている。
- UITransform のサイズが適切(例: 200×200 など)。
4. OutlineHighlight コンポーネントをアタッチ
- テスト用 Sprite ノードを選択します。
- Inspector の一番下にある Add Component ボタンをクリックします。
- Custom → OutlineHighlight を選択して追加します。
5. プロパティを設定
OutlineHighlight コンポーネントを選択した状態で、Inspector 上で以下のように設定します。
- Enable Highlight: チェック ON(デフォルトで ON)。
- Outline Color: 白(
RGBA(255, 255, 255, 255))。
※ 他の色にしたい場合はここを変更。 - Outline Width:
2(2px 相当)。 - Fade Duration:
0.1秒(滑らかにフェード)。 - Material Template: 先ほど作成した
OutlineSprite.matをドラッグ&ドロップで設定。 - Target Renderer: 空欄のままで OK(自動的に Sprite の UIRenderer が使用されます)。
- Use World Camera: チェック ON(通常の 2D ゲームならこれで問題ありません)。
- Custom Camera: 空欄のまま(Use World Camera が ON のため未使用)。
6. 再生して動作確認
- エディタ右上の Play ボタンを押してゲームを再生します。
- ゲームウィンドウ内で、先ほどの Sprite の上にマウスカーソルを移動します。
- カーソルが Sprite の矩形内に入った瞬間、白い 2px のアウトラインがふわっと表示されるのを確認します。
- カーソルを外に出すと、アウトラインがフェードアウトして消えるはずです。
もしアウトラインが表示されない場合は、以下を確認してください。
- Console に
[OutlineHighlight]の警告が出ていないか(UIRenderer / Material が不足していないか)。 OutlineSprite.matの Effect が正しく設定されているか。- Effect 内で
outlineColor,outlineWidth,outlineStrengthの uniform 名が一致しているか。 - Sprite が Canvas 内にあり、画面内に表示されているか。
まとめ
今回実装した OutlineHighlight コンポーネントは、
- ノードにアタッチして、アウトライン用 Material を 1 つ指定するだけで、マウスオーバー時の視覚的な強調を簡単に実現できる。
- 外部の GameManager やシングルトンに一切依存せず、完全に独立した再利用可能コンポーネントとして設計されている。
- 色・太さ・フェード時間・対象レンダラー・使用カメラなどをインスペクタから柔軟に調整できるため、様々なシーンで使い回しやすい。
応用例としては、
- インベントリ UI のアイテムスロットにアタッチして、選択可能なアイテムを視覚的にアピールする。
- マップ上の交互可能オブジェクト(宝箱・ドアなど)にアタッチして、プレイヤーに「ここをクリックできる」と伝える。
- ボタンやメニュー項目にアタッチして、ホバー時のフィードバックを統一した見た目で提供する。
このように、1 つの汎用コンポーネント + 1 つのカスタムマテリアルを用意しておくだけで、ゲーム全体の UI/オブジェクトのホバー表現を統一でき、開発効率と見た目の一貫性を大きく向上させることができます。
必要に応じて、OutlineHighlight をベースに「クリック時に色を変える」「キーボード選択時にも反応させる」などの拡張も簡単に行えるので、自分のプロジェクトに合わせてカスタマイズしてみてください。
