Unityを触り始めた頃は、つい「とりあえず Update() に全部書く」スタイルになりがちですよね。マウスの入力処理、Raycast、マテリアルの変更、UI更新…と、1つのスクリプトがどんどん肥大化していきます。
その結果、

  • どのゲームオブジェクトが何を担当しているのか分かりにくい
  • 一部の機能だけ別シーンや別プロジェクトで再利用しづらい
  • ちょっとした見た目変更でも巨大なスクリプトを開いて修正が必要

といった問題が出てきます。

この記事では「マウスオーバーされたオブジェクトにだけ、きれいな白い輪郭(アウトライン)を付ける」という、よくある演出をテーマに、
見た目の責務だけを担当するコンポーネントとして OutlineHighlight を実装してみます。

マウス入力やRaycastは別コンポーネントに任せて、
この OutlineHighlight は「オン・オフ命令を受けて、指定のアウトラインシェーダーを適用するだけ」に絞ることで、
プレハブにも差し込みやすく、テストしやすい構成を目指します。

【Unity】マウスオーバーで光る!「OutlineHighlight」コンポーネント

ここでは以下の2つを用意します。

  1. OutlineHighlight
    …対象オブジェクトのマテリアルにアウトライン用の値を設定する「見た目専用コンポーネント」
  2. MouseHoverHighlighter
    …カメラからRaycastして「マウスオーバー中のオブジェクト」を検出し、OutlineHighlight に命令を出すコンポーネント

この2つを組み合わせることで、Update地獄を避けつつ、どのオブジェクトにも簡単に「マウスオーバーで光る」振る舞いを足せるようにします。


フルコード:OutlineHighlight.cs & MouseHoverHighlighter.cs

Unity6(2023 以降)を想定しています。新Input Systemを使わず、Camera.main + Physics.Raycast + Input.mousePosition のシンプル構成にしてあります。


using UnityEngine;

/// <summary>対象オブジェクトのマテリアルにアウトラインを適用するコンポーネント</summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Renderer))]
public class OutlineHighlight : MonoBehaviour
{
    // --- シリアライズフィールド ------------------------------

    [Header("アウトライン設定")]

    [Tooltip("アウトライン描画に使用するマテリアル。\n" +
             "2px相当の白い縁取りになるようにシェーダー/マテリアルを設定しておきましょう。")]
    [SerializeField] private Material outlineMaterial;

    [Tooltip("アウトラインの色(シェーダー側で _OutlineColor などのプロパティに接続しておく想定)")]
    [SerializeField] private Color outlineColor = Color.white;

    [Tooltip("アウトラインの太さ(画面上で約2pxになるようにマテリアル側と合わせて調整)")]
    [SerializeField, Range(0f, 10f)] private float outlineWidth = 2f;

    [Header("挙動設定")]

    [Tooltip("初期状態でアウトラインを有効にするかどうか")]
    [SerializeField] private bool startEnabled = false;

    // --- 内部キャッシュ ------------------------------

    private Renderer _renderer;
    private Material[] _originalMaterials;   // 元のマテリアル配列
    private Material[] _outlinedMaterials;   // アウトライン付きのマテリアル配列
    private bool _isHighlighted;

    // シェーダープロパティID(文字列より高速)
    private static readonly int OutlineColorId = Shader.PropertyToID("_OutlineColor");
    private static readonly int OutlineWidthId = Shader.PropertyToID("_OutlineWidth");

    // --- Unity ライフサイクル ------------------------------

    private void Awake()
    {
        _renderer = GetComponent<Renderer>();

        // 元のマテリアルを保持(複製しておく)
        _originalMaterials = _renderer.materials;

        // アウトラインマテリアルが設定されていない場合は警告だけ出して終了
        if (outlineMaterial == null)
        {
            Debug.LogWarning(
                $"[OutlineHighlight] {name} に Outline 用マテリアルが設定されていません。",
                this
            );
            _outlinedMaterials = _originalMaterials;
            return;
        }

        // 元のマテリアル配列 + アウトラインマテリアル という構成の配列を作る
        _outlinedMaterials = new Material[_originalMaterials.Length + 1];
        for (int i = 0; i < _originalMaterials.Length; i++)
        {
            _outlinedMaterials[i] = _originalMaterials[i];
        }

        // アウトラインマテリアルをインスタンス化して最後に追加
        Material outlineInstance = Instantiate(outlineMaterial);
        _outlinedMaterials[_outlinedMaterials.Length - 1] = outlineInstance;

        // 初期状態の有効/無効を反映
        SetHighlight(startEnabled, true);
    }

    private void OnDestroy()
    {
        // 生成したアウトライン用マテリアルを破棄してメモリリークを防ぐ
        if (_outlinedMaterials != null && _outlinedMaterials.Length > _originalMaterials.Length)
        {
            Material last = _outlinedMaterials[_outlinedMaterials.Length - 1];
            if (last != null && last != outlineMaterial)
            {
                Destroy(last);
            }
        }
    }

    // --- 公開API ------------------------------

    /// <summary>アウトラインを有効にする</summary>
    public void EnableHighlight()
    {
        SetHighlight(true);
    }

    /// <summary>アウトラインを無効にする</summary>
    public void DisableHighlight()
    {
        SetHighlight(false);
    }

    /// <summary>アウトラインのオン・オフを切り替える</summary>
    public void ToggleHighlight()
    {
        SetHighlight(!_isHighlighted);
    }

    /// <summary>
    /// アウトラインの色と太さを動的に変更したい場合に使うヘルパー。
    /// </summary>
    public void SetOutlineParameters(Color color, float width)
    {
        outlineColor = color;
        outlineWidth = width;

        // すでにハイライト中なら即座に反映
        if (_isHighlighted && _outlinedMaterials != null && _outlinedMaterials.Length > _originalMaterials.Length)
        {
            Material outlineMat = _outlinedMaterials[_outlinedMaterials.Length - 1];
            if (outlineMat != null)
            {
                ApplyMaterialProperties(outlineMat);
            }
        }
    }

    // --- 内部処理 ------------------------------

    /// <summary>内部用:ハイライト状態を切り替える</summary>
    private void SetHighlight(bool enabled, bool force = false)
    {
        if (!force && _isHighlighted == enabled) return;

        _isHighlighted = enabled;

        if (_renderer == null || _originalMaterials == null || _outlinedMaterials == null)
            return;

        if (enabled)
        {
            // アウトライン付きのマテリアル配列を適用
            _renderer.materials = _outlinedMaterials;

            // 最後のマテリアル(アウトライン用)にパラメータを反映
            if (_outlinedMaterials.Length > _originalMaterials.Length)
            {
                Material outlineMat = _outlinedMaterials[_outlinedMaterials.Length - 1];
                if (outlineMat != null)
                {
                    ApplyMaterialProperties(outlineMat);
                }
            }
        }
        else
        {
            // 元のマテリアル配列に戻す
            _renderer.materials = _originalMaterials;
        }
    }

    /// <summary>アウトラインマテリアルに色・太さを設定する</summary>
    private void ApplyMaterialProperties(Material mat)
    {
        if (mat.HasProperty(OutlineColorId))
        {
            mat.SetColor(OutlineColorId, outlineColor);
        }

        if (mat.HasProperty(OutlineWidthId))
        {
            mat.SetFloat(OutlineWidthId, outlineWidth);
        }
    }
}


/// <summary>
/// カメラからマウス位置に向かってRaycastし、
/// OutlineHighlight コンポーネントを持つオブジェクトをマウスオーバーでハイライトするコンポーネント。
/// </summary>
public class MouseHoverHighlighter : MonoBehaviour
{
    [Header("Raycast 設定")]

    [Tooltip("Raycast を飛ばすカメラ。未指定なら Camera.main を使用します。")]
    [SerializeField] private Camera targetCamera;

    [Tooltip("Raycast の最大距離")]
    [SerializeField] private float maxDistance = 100f;

    [Tooltip("ヒット対象とするレイヤーマスク(Everything 推奨)")]
    [SerializeField] private LayerMask layerMask = ~0;

    [Header("デバッグ")]

    [Tooltip("Sceneビュー上にRayを可視化します")]
    [SerializeField] private bool drawDebugRay = false;

    private OutlineHighlight _currentHighlight; // 現在マウスオーバー中のオブジェクトの OutlineHighlight

    private void Awake()
    {
        // カメラが未設定なら main カメラを探す
        if (targetCamera == null)
        {
            targetCamera = Camera.main;
        }

        if (targetCamera == null)
        {
            Debug.LogError("[MouseHoverHighlighter] 対象カメラが見つかりません。", this);
        }
    }

    private void Update()
    {
        if (targetCamera == null) return;

        // マウス位置からRayを生成
        Ray ray = targetCamera.ScreenPointToRay(Input.mousePosition);

        if (drawDebugRay)
        {
            Debug.DrawRay(ray.origin, ray.direction * maxDistance, Color.yellow);
        }

        // Raycastでヒット判定
        if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, layerMask, QueryTriggerInteraction.Ignore))
        {
            // ヒットしたオブジェクトから OutlineHighlight を探す
            OutlineHighlight highlight = hit.collider.GetComponentInParent<OutlineHighlight>();

            if (highlight != null)
            {
                // すでに同じオブジェクトをハイライトしているなら何もしない
                if (_currentHighlight == highlight) return;

                // 以前ハイライトしていたオブジェクトがあれば解除
                if (_currentHighlight != null)
                {
                    _currentHighlight.DisableHighlight();
                }

                // 新たにヒットしたオブジェクトをハイライト
                _currentHighlight = highlight;
                _currentHighlight.EnableHighlight();
            }
            else
            {
                // OutlineHighlight を持たないオブジェクトを指している場合は、以前のハイライトを解除
                ClearCurrentHighlight();
            }
        }
        else
        {
            // 何もヒットしていない場合もハイライト解除
            ClearCurrentHighlight();
        }
    }

    /// <summary>現在のハイライトを解除して参照をクリア</summary>
    private void ClearCurrentHighlight()
    {
        if (_currentHighlight != null)
        {
            _currentHighlight.DisableHighlight();
            _currentHighlight = null;
        }
    }
}

※ このコードは「アウトライン用マテリアル」がすでに用意されている前提です。
シェーダー側では少なくとも _OutlineColor(Color)、_OutlineWidth(float)といったプロパティを持ち、
2px 程度の白い縁取りになるように設定しておきましょう。


使い方の手順

ここからは、実際に「プレイヤーがマウスで指したオブジェクトだけ白い縁取りが出る」シーンを作る手順を見ていきます。

手順①:アウトライン用マテリアルを用意する

  1. Project ウィンドウで右クリック → Create > Material を選択し、OutlineWhite2px などの名前を付ける。
  2. Inspector で、使用したいアウトラインシェーダーを選択する。
    (URP/HDRP なら Outline 系のサンプルや自作シェーダー、Built-inなら Asset Store のアウトラインシェーダーなどを利用できます。)
  3. シェーダーのパラメータで、白色かつ2px程度の太さになるように調整しておく。
    プロパティ名は _OutlineColor_OutlineWidth になるように合わせておくと、コードからの変更が効きます。

手順②:ハイライトさせたいオブジェクトに OutlineHighlight を付ける

例として、シーン内に「宝箱」の3Dモデルがあるとします。

  1. Hierarchy で TreasureChest(宝箱の GameObject)を選択。
  2. Inspector 下部の Add Component ボタンから OutlineHighlight を追加。
  3. OutlineHighlight の Outline Material に、先ほど作成した OutlineWhite2px マテリアルをドラッグ&ドロップ。
  4. 色や太さを変えたい場合は、Outline ColorOutline Width を調整。
  5. 「常に光らせたい」テストをしたいときは、Start Enabled にチェックを入れておくと、ゲーム開始直後からアウトラインが出ます。

同じ手順で、敵キャラクター、動く床、アイテムなど、マウスオーバーで強調したいオブジェクトすべてOutlineHighlight を付けておきましょう。

手順③:シーンに MouseHoverHighlighter を配置する

  1. 空の GameObject を作成し、名前を MouseHoverController にする。
  2. このオブジェクトに MouseHoverHighlighter コンポーネントを追加。
  3. Target Camera を空欄のままにしておくと、自動で Camera.main が使われます。
    メインカメラが MainCamera タグを持っていることを確認してください。
  4. Layer Mask は基本的に Everything のままでOKですが、UIや不要なレイヤーを除外したい場合はここで調整。
  5. デバッグしたい場合は Draw Debug Ray にチェックを入れると、SceneビューにRayが黄色で表示されます。

これで、ゲームを再生してマウスを動かすと、Raycast が当たったオブジェクトのうち OutlineHighlight を持つものだけに白い縁取りが出るようになります。

手順④:具体的な使用例

  • プレイヤーが調べられるオブジェクト
    ドア、スイッチ、宝箱など、プレイヤーが「Eキーで調べる」対象すべてに OutlineHighlight を付けておくと、
    マウスを向けたときに「今インタラクトできるもの」が一目で分かります。
  • 敵キャラクターのロックオン対象
    敵キャラのルートオブジェクトに OutlineHighlight を付けておき、
    MouseHoverHighlighter とは別に「ロックオン中だけ EnableHighlight() する」コンポーネントを作れば、
    ロックオン中の敵を白い縁取りで強調することも簡単です。
  • 動く床や足場
    アクションゲームで、今ジャンプして乗れる足場を視覚的に分かりやすくするために、
    動く床や足場のオブジェクトに OutlineHighlight を付けておくと、レベルデザイン上の誘導にもなります。

メリットと応用

OutlineHighlight を「見た目専用コンポーネント」として分離しておくことで、いくつかのメリットがあります。

  • プレハブの再利用性が高い
    例えば「宝箱」プレハブに OutlineHighlight だけ仕込んでおけば、
    どのシーンに配置しても、シーン側では「Raycastしているコンポーネント」さえあれば同じように光らせられます。
  • 責務が分かれていてテストしやすい
    マウス入力やゲームロジックをいじらなくても、OutlineHighlight 単体で「アウトラインのオン・オフ」だけをテストできます。
    逆に、MouseHoverHighlighter 側は「Raycastで正しく対象を検出しているか」だけに集中できます。
  • レベルデザインが楽になる
    レベルデザイナーが「このオブジェクトはプレイヤーに気づいてほしい」と思ったら、
    そのオブジェクトに OutlineHighlight を足すだけで、マウスオーバー時に光るようになります。
    スクリプトを触らなくても視覚的な誘導ができるのはかなり便利です。

さらに、OutlineHighlight は「命令を受けてアウトラインのパラメータを変える」だけのシンプルなAPIなので、
MouseHoverHighlighter 以外からも簡単に再利用できます。

改造案:距離によってアウトラインの太さを変える

例えば「カメラに近いほどアウトラインを太くする」ような演出を加えたい場合、
OutlineHighlight に次のようなメソッドを追加して、他のコンポーネントから呼び出すのもアリです。


    /// <summary>
    /// カメラとの距離に応じてアウトラインの太さを調整するサンプル。
    /// minDistance で maxWidth、maxDistance で minWidth になるように線形補間します。
    /// </summary>
    public void UpdateOutlineWidthByDistance(Transform cameraTransform,
                                             float minDistance,
                                             float maxDistance,
                                             float minWidth,
                                             float maxWidth)
    {
        if (cameraTransform == null) return;

        float distance = Vector3.Distance(cameraTransform.position, transform.position);
        float t = Mathf.InverseLerp(minDistance, maxDistance, distance);
        float width = Mathf.Lerp(maxWidth, minWidth, t); // 近いほど太く

        SetOutlineParameters(outlineColor, width);
    }

このように、見た目の制御ロジックは OutlineHighlight に閉じ込めておき、
「いつ・どのくらい光らせるか」の判断は別コンポーネントに任せる
構成にしておくと、
プロジェクトが大きくなってもスクリプトの見通しが良くなります。

ぜひ、自分のプロジェクトでも「Updateに全部書く」スタイルから、こうした小さなコンポーネントに分割するスタイルにシフトしてみてください。