【Unity】HearingSensor (聴覚センサー) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unityの学習を進めていくと、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの移動、敵AI、HP管理、エフェクト制御……すべてが1つの巨大スクリプトに集まると、次のような問題が出てきます。

  • 処理の責務がごちゃ混ぜになり、バグの原因箇所が追いにくい
  • 別プロジェクトや別オブジェクトに流用しづらい
  • ちょっとした仕様変更でも広範囲を書き換える必要が出る

敵キャラクターの「聴覚処理」も、その典型です。
「プレイヤーが音を立てたら、その方向に振り向いて調査に行く」という処理を、巨大なAIスクリプトの一部として書いてしまうと、視覚処理や移動処理と絡み合ってすぐにカオスになります。

そこでこの記事では、「音を聞いて、音源の位置へ調査に向かう」だけに責務を限定したコンポーネント HearingSensor(聴覚センサー) を用意して、敵AIの聴覚をスッキリ分離してみましょう。

【Unity】音を聞いて調査に向かうシンプルAI!「HearingSensor」コンポーネント

以下のコンポーネントは、

  • プレイヤーなどが発行する「音シグナル(SoundSignal)」を受信
  • 音源位置を記憶し、そこへ向かって移動(調査)
  • 到達したら「調査完了」として状態をリセット

といった一連の「聴覚ベースの行動」だけを担当します。
移動には NavMeshAgent を使う想定ですが、NavMeshを使わない場合でも Transform ベースの追従に置き換えればOKです。

前提:音シグナルを表すクラス

まずは「どこで、どれくらいの音がしたか」を表す SoundSignal と、その通知を行うシンプルな静的クラス SoundEmitter を定義します。


using System;
using UnityEngine;

/// <summary>音の情報をまとめたデータ構造</summary>
[Serializable]
public class SoundSignal
{
    /// <summary>音が発生したワールド座標</summary>
    public Vector3 WorldPosition;

    /// <summary>音の強さ(半径などの計算に使う)</summary>
    public float Loudness;

    /// <summary>音を発生させたオブジェクト(プレイヤーなど)</summary>
    public GameObject Source;

    public SoundSignal(Vector3 worldPosition, float loudness, GameObject source)
    {
        WorldPosition = worldPosition;
        Loudness = loudness;
        Source = source;
    }
}

/// <summary>
/// グローバルに音シグナルを発行する簡易イベントハブ。
/// 敵の HearingSensor はこのイベントを購読します。
/// </summary>
public static class SoundEmitter
{
    /// <summary>音が発生したときに呼ばれるイベント</summary>
    public static event Action<SoundSignal> OnSoundEmitted;

    /// <summary>任意の場所で音を発生させるためのユーティリティ関数</summary>
    public static void EmitSound(Vector3 position, float loudness, GameObject source)
    {
        var signal = new SoundSignal(position, loudness, source);
        OnSoundEmitted?.Invoke(signal);
    }
}

SoundEmitter.EmitSound(...) を呼び出せば、シーン内の全ての HearingSensor に音が届くようなイメージです。

本体:HearingSensor コンポーネント


using UnityEngine;
using UnityEngine.AI;

/// <summary>
/// 聴覚センサーコンポーネント。
/// - SoundEmitter から音シグナルを受信
/// - 聞こえる範囲の音だけを採用
/// - 音源の位置へ NavMeshAgent で移動
/// - 到達したら「調査完了」
///
/// 「聴覚による調査」だけを担当し、攻撃や巡回パターンは別コンポーネントに分離するとスッキリします。
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
public class HearingSensor : MonoBehaviour
{
    [Header("聴覚パラメータ")]
    [SerializeField]
    [Tooltip("この距離以内の音だけを聞き取る(音の強さも考慮)")]
    private float hearingRadius = 15f;

    [SerializeField]
    [Tooltip("音がした方向を向くときの回転速度")]
    private float turnSpeed = 720f;

    [Header("調査挙動")]
    [SerializeField]
    [Tooltip("音源に十分近づいたとみなす距離")]
    private float investigateArrivalDistance = 1.5f;

    [SerializeField]
    [Tooltip("調査完了後、この秒数だけその場で様子を見る")]
    private float investigateWaitTime = 2f;

    [Header("デバッグ")]
    [SerializeField]
    [Tooltip("最後に聞いた音の位置をGizmosで表示するか")]
    private bool drawDebugGizmos = true;

    // 内部状態
    private NavMeshAgent agent;

    /// <summary>現在調査中かどうか</summary>
    public bool IsInvestigating { get; private set; }

    /// <summary>最後に聞いた音のワールド座標(調査ターゲット)</summary>
    public Vector3 LastHeardPosition { get; private set; }

    private float investigateTimer;
    private bool hasInvestigationTarget;

    private void Awake()
    {
        agent = GetComponent<NavMeshAgent>();

        // NavMeshAgent は回転を自動制御するかどうかを切り替えられます。
        // 聴覚で向きだけ変えたい場合など、必要に応じて調整してください。
        agent.updateRotation = true;
    }

    private void OnEnable()
    {
        // グローバルな音イベントを購読
        SoundEmitter.OnSoundEmitted += HandleSoundEmitted;
    }

    private void OnDisable()
    {
        // イベント購読解除(メモリリークや例外防止のため必須)
        SoundEmitter.OnSoundEmitted -= HandleSoundEmitted;
    }

    /// <summary>
    /// 音シグナルを受信したときに呼ばれるコールバック。
    /// </summary>
    private void HandleSoundEmitted(SoundSignal signal)
    {
        // 自分自身が出した音は無視する(必要なければコメントアウト)
        if (signal.Source == gameObject)
        {
            return;
        }

        // 音源との距離を計算
        float distance = Vector3.Distance(transform.position, signal.WorldPosition);

        // 音の強さに応じて実質的な可聴距離を計算(例:Loudness=2なら2倍遠くまで聞こえる)
        float effectiveHearingRadius = hearingRadius * signal.Loudness;

        // 可聴範囲外なら無視
        if (distance > effectiveHearingRadius)
        {
            return;
        }

        // 音が聞こえたので、その位置を調査ターゲットに設定
        LastHeardPosition = signal.WorldPosition;
        hasInvestigationTarget = true;
        IsInvestigating = true;
        investigateTimer = 0f;

        // NavMeshAgent に目的地をセット
        if (agent.enabled && agent.isOnNavMesh)
        {
            agent.SetDestination(LastHeardPosition);
        }

        // すぐに音の方向を向く(スナップ回転)
        Vector3 toSound = (LastHeardPosition - transform.position);
        toSound.y = 0f;
        if (toSound.sqrMagnitude > 0.001f)
        {
            Quaternion targetRot = Quaternion.LookRotation(toSound.normalized, Vector3.up);
            transform.rotation = targetRot;
        }
    }

    private void Update()
    {
        if (!IsInvestigating || !hasInvestigationTarget)
        {
            return;
        }

        // NavMeshAgent が有効なら、目的地への移動は自動で行われます。
        // ここでは「到着したかどうか」の判定と、待機ロジックだけを行います。

        // 目的地との距離を計算
        float distanceToTarget = Vector3.Distance(transform.position, LastHeardPosition);

        // 十分近づいたら「到着」とみなす
        if (distanceToTarget <= investigateArrivalDistance)
        {
            // その場で様子を見る時間をカウント
            investigateTimer += Time.deltaTime;

            if (investigateTimer >= investigateWaitTime)
            {
                // 調査完了
                IsInvestigating = false;
                hasInvestigationTarget = false;
                investigateTimer = 0f;

                // 必要ならここで「元の巡回ルートに戻る」などのイベントを発行してもOK
                // ただし、このコンポーネントは「聴覚による調査」だけに責務を絞るのがおすすめです。
            }
        }
        else
        {
            // まだ到着していない間、音の方向をゆっくり向く(自然な挙動用)
            Vector3 toTarget = LastHeardPosition - transform.position;
            toTarget.y = 0f;
            if (toTarget.sqrMagnitude > 0.001f)
            {
                Quaternion targetRot = Quaternion.LookRotation(toTarget.normalized, Vector3.up);
                transform.rotation = Quaternion.RotateTowards(
                    transform.rotation,
                    targetRot,
                    turnSpeed * Time.deltaTime
                );
            }
        }
    }

    /// <summary>
    /// デバッグ用に、最後に聞いた音の位置をSceneビューに表示
    /// </summary>
    private void OnDrawGizmosSelected()
    {
        if (!drawDebugGizmos)
        {
            return;
        }

        // 可聴範囲
        Gizmos.color = new Color(0f, 0.5f, 1f, 0.2f);
        Gizmos.DrawWireSphere(transform.position, hearingRadius);

        // 最後に聞いた音の位置
        if (Application.isPlaying && IsInvestigating)
        {
            Gizmos.color = Color.yellow;
            Gizmos.DrawSphere(LastHeardPosition, 0.3f);
            Gizmos.DrawLine(transform.position, LastHeardPosition);
        }
    }

    /// <summary>
    /// 外部から強制的に「調査中止」させたい場合に呼び出すヘルパー
    /// </summary>
    public void CancelInvestigation()
    {
        IsInvestigating = false;
        hasInvestigationTarget = false;
        investigateTimer = 0f;

        // NavMeshAgent の目的地もリセットしたい場合
        if (agent.enabled && agent.isOnNavMesh)
        {
            agent.ResetPath();
        }
    }
}

おまけ:プレイヤー側で音を出す簡単コンポーネント

「スペースキーを押したら足音を出す」ような、テスト用スクリプトも載せておきます。


using UnityEngine;

/// <summary>
/// キー入力で SoundEmitter に音を発行する簡易テスト用コンポーネント。
/// プレイヤーオブジェクトなどに付けて使います。
/// </summary>
public class PlayerNoiseTester : MonoBehaviour
{
    [SerializeField]
    [Tooltip("発生させる音の強さ(HearingSensor の hearingRadius に掛け算される)")]
    private float loudness = 1f;

    [SerializeField]
    [Tooltip("何秒ごとに音を出せるか(連打防止)")]
    private float noiseCooldown = 0.5f;

    private float cooldownTimer;

    private void Update()
    {
        cooldownTimer -= Time.deltaTime;

        // スペースキーで音を出す(Input System を使う場合はここを書き換える)
        if (Input.GetKeyDown(KeyCode.Space) && cooldownTimer <= 0f)
        {
            EmitFootstep();
            cooldownTimer = noiseCooldown;
        }
    }

    /// <summary>現在位置で音を発生させる</summary>
    private void EmitFootstep()
    {
        Vector3 pos = transform.position;
        SoundEmitter.EmitSound(pos, loudness, gameObject);

        // デバッグログ
        Debug.Log($"[PlayerNoiseTester] 音を発生: pos={pos}, loudness={loudness}");
    }
}

使い方の手順

  1. 敵キャラクターに HearingSensor を追加する
    • 敵用のプレハブ(Enemy など)を開く
    • NavMeshAgent コンポーネントを追加し、移動速度や半径を設定
    • 上記の HearingSensor スクリプトをアタッチ
    • HearingRadius(聴覚範囲)InvestigateArrivalDistance を好みに調整
  2. プレイヤーに PlayerNoiseTester を追加する
    • プレイヤーの GameObject に PlayerNoiseTester をアタッチ
    • Loudness を 1〜2 くらいに設定(値が大きいほど遠くまで聞こえる)
    • ゲーム再生中にスペースキーを押すと、その位置で音シグナルが発生
  3. NavMesh をベイクしておく
    • 地面(Floor)などに Navigation Static を設定
    • Window > AI > Navigation から NavMesh をベイク
    • 敵キャラが NavMesh 上に乗っていることを確認

    これで、敵の HearingSensorSoundEmitter から届いた音の位置へ NavMesh 経路で移動してくれます。

  4. 具体的な使用例
    • プレイヤーの足音
      CharacterController ベースのプレイヤーなら、「一定距離歩くごと」「着地したとき」などで SoundEmitter.EmitSound() を呼び出すことで、足音に反応する敵を簡単に実現できます。
    • 投げた石(デコイ)
      石のプレハブに「着地時に一度だけ音を出すコンポーネント」を付け、OnCollisionEnterEmitSound() を呼べば、「石を投げた方向に敵が釣られていく」ステルス要素が作れます。
    • アラーム付きの動く床
      プレイヤーが乗ると大きな音を立てる床に SoundEmitter.EmitSound() を仕込めば、「誤って乗ると周囲の敵が一斉に集まってくる」トラップギミックも簡単です。

メリットと応用

HearingSensor を使う最大のメリットは、「敵AIの聴覚だけをコンポーネントとして独立させられる」 ことです。

  • 巡回AI、追跡AI、攻撃AIなどと分離でき、1つ1つのスクリプトが小さく保てる
  • 「聴覚を持つ敵」「聴覚を持たない敵」を、コンポーネントの有無で切り替えられる
  • プレハブ化しておけば、新しい敵を作るときにドラッグ&ドロップで聴覚を付与できる
  • レベルデザイン側は「どこでどれくらいの音を出すか」だけを考えればよく、AIロジックを触らずにギミックを追加できる

たとえば、ボス敵には HearingSensor を付けず、雑魚敵にだけ付けることで、「音に釣られるのは雑魚だけ」というゲーム性を簡単に作れます。
また、「特定の音種だけに反応する聴覚センサー」や「聞こえた音の履歴を保持して行動パターンを変える」などにも発展させやすいです。

改造案:音の「種類」によるフィルタリング

例えば「足音には反応するけど、味方の銃声には反応しない」といった区別をしたい場合、SoundSignal に種類を追加し、HearingSensor 側でフィルタリングするのが簡単です。

以下は、HearingSensor に「特定の音種だけを受け取る」フィルタ処理を追加する一例です(イメージ用の1メソッド)。


/// <summary>
/// 受信した音シグナルが、このセンサーにとって有効かどうかを判定する例。
/// SoundSignal に enum SoundType などを追加して、ここで条件分岐するとよいです。
/// </summary>
private bool IsAcceptableSound(SoundSignal signal)
{
    // 例: 味方が出した音は無視したい場合
    // (味方のタグを "Ally" にしておく想定)
    if (signal.Source != null && signal.Source.CompareTag("Ally"))
    {
        return false;
    }

    // ここに「音の種類」や「時間帯」などの条件を増やしていけます。
    return true;
}

実際には HandleSoundEmitted の冒頭で if (!IsAcceptableSound(signal)) return; のように呼び出してあげればOKです。
このように、小さな関数単位で責務を切り出していくと、聴覚ロジックもどんどん拡張しやすくなりますね。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!