ハコブくん、ついに目と声を持つ。デジタルツインで実現するロボットの安全制御
LOG45HRI2026/1/25

ハコブくん、ついに目と声を持つ。デジタルツインで実現するロボットの安全制御

ハコブくんに「近接センサー」を搭載

ついに、ハコブくんに周囲の状況を察知するための「目(センサー)」を搭載する作業を行いました。 今回使用したのは、UnityのRaycast(レイキャスト)という機能です。これは現実のロボットのLiDARや超音波センサーと同じように、目に見えないレーザーを飛ばして対象物までの距離を測る技術です。

この実装により、ハコブくんは単に命令通りに動くだけのマシンから、周囲の状況を判断して自ら事故を防ぐ「知的なロボット」へと進化しました。

※薬剤搬送ロボットハコブくんは技術検証用の仮想設定です。 実用化目的ではなく、制御・UIデザイン・HRI(人間とロボットの相互作用)の本質を探求するコンセプトとして定義しています。

Raycastによる距離測定の実装

まず、Unityの Physics.Raycast を使い、ハコブくんの正面に「見えない杖」を持たせる処理を記述しました。 床から少し浮いた位置から前方1.5m先まで常にレーザーを照射し、障害物がないかを確認し続ける設定です。これがハコブくんの安全を守る守護神となります。

物理エンジンへの安全介入

次に、ROS 2からの命令とUnityの物理演算の間に、安全のための介入処理を加えました。 たとえROS 2側から「速度0.5で前進せよ」という命令が届き続けていても、Unity側のセンサーが壁を検知した瞬間に、プログラム内で速度を強制的に 0 へ書き換える仕組みです。 「脳(ROS)」が前進を指示しても、「身体(Unity)」が危険を察知して足を止める。自律型ロボットにおける安全制御の基本を形にしました。

壁の手前で止まるハコブくん

実際にターミナルから i キーを長押しし、ハコブくんを壁に向かわせるテストを行いました。 壁に激突するかと思われた瞬間、ハコブくんは壁のわずか手前でピタリと停止しました。コンソールには 「壁を検知!自動ブレーキ作動!!」 という警告が表示され、正しく安全機能が働いていることを確認できました。

ハコブくんを安全仕様にするためのコード修正

HakobuMove.cs をアップデートし、FixedUpdate 内にセンサーのロジックを組み込みました。

C#

using UnityEngine;
using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Geometry;

[RequireComponent(typeof(Rigidbody))]
public class HakobuMove : MonoBehaviour
{
    public ROSConnection ros;
    public string topicName = "/cmd_vel";

    private Rigidbody rb;
    private Vector3 lastLinearVelocity;
    private float lastAngularVelocity;

    [Header("Sensor Settings")]
    public float sensorRange = 1.5f; // 1.5メートル以内に壁があれば停止

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.useGravity = true;
        // 転倒防止の設定
        rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;

        if (ros == null) { ros = ROSConnection.GetOrCreateInstance(); }
        ros.Subscribe<TwistMsg>(topicName, MoveHakobu);
        
        Debug.Log("ハコブくん、センサーシステム起動完了。");
    }

    void MoveHakobu(TwistMsg msg)
    {
        // ROSの座標系をUnityの前進方向へ変換
        lastLinearVelocity = transform.forward * (float)msg.linear.x;
        lastAngularVelocity = -(float)msg.angular.z * Mathf.Rad2Deg;
    }

    void FixedUpdate()
    {
        float powerMultiplier = 3.0f;
        float currentSpeed = lastLinearVelocity.magnitude;

        // --- Raycastによる前方検知 ---
        RaycastHit hit;
        // 前方にセンサーを飛ばす
        bool isObstacleAhead = Physics.Raycast(transform.position, transform.forward, out hit, sensorRange);

        if (isObstacleAhead)
        {
            // 壁を検知した場合、前進速度を0にする(後退は許可)
            currentSpeed = 0;
            Debug.LogWarning("壁を検知!自動ブレーキ作動!! 対象: " + hit.collider.name);
        }

        // 物理エンジンへの速度適用
        rb.linearVelocity = transform.forward * (currentSpeed * powerMultiplier);
        rb.angularVelocity = new Vector3(0, lastAngularVelocity * Mathf.Deg2Rad, 0);

        // Scene画面での視覚化(検知時は赤、安全時は緑)
        Debug.DrawRay(transform.position, transform.forward * sensorRange, isObstacleAhead ? Color.red : Color.green);
    }
}

作業完了後の最終確認

今回の実装が正しく反映されているか、以下の手順で動作確認を行いました。

  • 壁のCollider(衝突判定)の確認 廊下の壁(Cube)を選択し、Inspectorで Box Collider が付いていることを確認しました。これがないとセンサーが壁を突き抜けてしまうためです。

  • Sceneビューでの可視化テスト 再生(Play)ボタンを押し、ハコブくんを壁に近づけました。Scene画面上で、前方のデバッグ用ラインが壁に触れた瞬間に 緑から赤へ変わることを確認しました。

動作確認の成果

  • 壁に向かって i キーを押し続けても、ぶつかる直前で確実に停止しました。

  • コンソールに「自動ブレーキ作動!!」のメッセージが表示されました。

以上の手順により、ハコブくんを安全機能付きのインテリジェント・ロボットへとアップグレードすることができました。

センサー感度の微調整

自動ブレーキのロジック自体は完璧でしたが、再生した瞬間にハコブくんが「壁だ!」と判断して停止してしまいました。インスペクターを確認すると、検知対象は本来避けるべき前方ではなく、横の壁(Wall_Left)でした。 そこで、センサーの視界と壁との距離感を適正にするための対策を講じました。

自分の体への誤反応を防ぐオフセット設定

これまではハコブくんの中心(transform.position)からレーザーを飛ばしていましたが、これだとハコブくん自身の厚みによって、自分自身のコライダーを「障害物」と誤判定してしまう可能性がありました。 そこで、レーザーの開始地点を少し前方にずらし、ハコブくんの「おでこ」から光が出るようにプログラムを修正しました。

廊下の幅とセンサー射程の最適化

2.0m幅の廊下に対して、センサー射程の 1.5m は少し敏感すぎたようです。また、壁の位置が初期設定のままだと走行ルートに干渉していたため、壁の配置を見直しました。 具体的には、センサーの射程を 0.5m 程度に短縮し、適度な距離感を保てるようにしました。

調整の結果:スムーズな走行と確実な停止

これらの調整を行った結果、起動直後に止まってしまう現象が解消されました。 i キーを叩くとハコブくんは軽快に廊下を進み、本当に危険な突き当たりの壁に近づいた時だけ、静かに、そして確実に自動ブレーキがかかるようになりました。

センサーを最適化するコードの修正内容

自分自身の誤検知を防ぐために、レーザーの発射地点を少し前にずらす(オフセット)処理を追加しました。

C#

void FixedUpdate()
{
    float powerMultiplier = 3.0f;
    float currentSpeed = lastLinearVelocity.magnitude;

    // --- 修正点:発射地点を少し前にずらす ---
    // transform.forward * 0.4f を足すことで、自分自身のコライダーに当たるのを防ぎます
    Vector3 rayStart = transform.position + transform.forward * 0.4f;

    RaycastHit hit;
    bool isObstacleAhead = Physics.Raycast(rayStart, transform.forward, out hit, sensorRange);

    if (isObstacleAhead)
    {
        // 当たった対象が自分自身(Hakobu_kun)でないことを確認してブレーキをかけます
        if (hit.collider.gameObject != this.gameObject)
        {
            currentSpeed = 0;
            Debug.LogWarning("壁を検知!自動ブレーキ作動!! 対象: " + hit.collider.name);
        }
    }

    // 物理エンジンへの適用
    rb.linearVelocity = transform.forward * (currentSpeed * powerMultiplier);
    rb.angularVelocity = new Vector3(0, lastAngularVelocity * Mathf.Rad2Deg, 0);

    // デバッグ用の線も新しい開始地点に合わせます
    Debug.DrawRay(rayStart, transform.forward * sensorRange, isObstacleAhead ? Color.red : Color.green);
}

インスペクターでの設定変更

コードの修正に合わせ、Unityエディタ上で以下の微調整を行いました。

センサー射程(Sensor Range)の変更

  • Hakobu_kun を選択し、Hakobu Move スクリプト内の Sensor Range を 0.5 に設定しました。

廊下の幅(壁の位置)の調整

  • 廊下を 2.0m 幅として正しく機能させるため、Wall_Left の Position X を -1.0、Wall_Right の Position X を 1.0 に移動させ、ハコブくんの通り道を確保しました。

警告音の実装記録

自動ブレーキが正常に機能するようになりましたが、静かに止まるだけでは「なぜ止まったのか」が分かりにくい場合があります。そこで、センサーが壁を検知した瞬間に「ポーン!」という警告音を鳴らす機能を加えました。 これにより、ハコブくんは視覚的な回避だけでなく、聴覚的なコミュニケーション能力を手に入れました。

AudioSourceコンポーネントの搭載

まず、ハコブくんにスピーカーの役割を果たす AudioSource コンポーネントを追加しました。 Unityでは、このコンポーネントが音を再生する心臓部となります。今回は起動時に勝手に音が鳴らないよう Play On Awake をオフにし、プログラムから必要なタイミングでだけ鳴らす設定にしました。

鳴らしっぱなしを防ぐ制御ロジック

センサーが壁を検知している間、毎フレーム音が鳴ってしまうと「ポポポポポン!」と騒がしくなってしまいます。 そこで、「まだ音が鳴っていなければ鳴らす」というフラグ管理、あるいは PlayOneShot という機能を使うことで、壁を見つけた瞬間に一度だけ、きれいに音が響くように工夫しました。

警告音と共に止まるハコブくん

再びテスト走行を行いました。

壁に近づき、デバッグの線が赤に変わったその瞬間、スピーカーから「ポーン!」という軽快な警告音が響きました。停止の理由が音で伝わるようになり、シミュレーションとしての臨場感と安全性が一段と向上しました。

音声を組み込んだコードの修正内容

HakobuMove.cs に、オーディオを制御するための変数を追加しました。

C#

using UnityEngine;
using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Geometry;

[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(AudioSource))] // 自動でAudioSourceを追加
public class HakobuMove : MonoBehaviour
{
    public ROSConnection ros;
    public string topicName = "/cmd_vel";

    private Rigidbody rb;
    private AudioSource audioSource; // オーディオ用の変数
    public AudioClip warningSound;    // 再生する音のファイル
    private bool isWarningPlayed = false; // 音の重複再生防止

    private Vector3 lastLinearVelocity;
    private float lastAngularVelocity;

    [Header("Sensor Settings")]
    public float sensorRange = 0.8f;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        audioSource = GetComponent<AudioSource>();
        
        rb.useGravity = true;
        rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;

        if (ros == null) { ros = ROSConnection.GetOrCreateInstance(); }
        ros.Subscribe<TwistMsg>(topicName, MoveHakobu);
        
        Debug.Log("ハコブくん、オーディオシステム起動。");
    }

    void MoveHakobu(TwistMsg msg)
    {
        lastLinearVelocity = transform.forward * (float)msg.linear.x;
        lastAngularVelocity = -(float)msg.angular.z * Mathf.Rad2Deg;
    }

    void FixedUpdate()
    {
        float powerMultiplier = 3.0f;
        float currentSpeed = lastLinearVelocity.magnitude;

        Vector3 rayStart = transform.position + transform.forward * 0.4f;
        RaycastHit hit;
        bool isObstacleAhead = Physics.Raycast(rayStart, transform.forward, out hit, sensorRange);

        if (isObstacleAhead)
        {
            if (hit.collider.gameObject != this.gameObject)
            {
                currentSpeed = 0;
                
                // --- 修正点:音がまだ鳴っていなければ鳴らす ---
                if (!isWarningPlayed && warningSound != null)
                {
                    audioSource.PlayOneShot(warningSound);
                    isWarningPlayed = true; // 鳴らしたことを記録
                    Debug.LogWarning("壁を検知!自動ブレーキ作動!!");
                }
            }
        }
        else
        {
            // 壁がなくなれば、フラグをリセットして次回の検知に備える
            isWarningPlayed = false;
        }

        rb.linearVelocity = transform.forward * (currentSpeed * powerMultiplier);
        rb.angularVelocity = new Vector3(0, lastAngularVelocity * Mathf.Deg2Rad, 0);

        Debug.DrawRay(rayStart, transform.forward * sensorRange, isObstacleAhead ? Color.red : Color.green);
    }
}

Unityエディタでの設定手順

コードの保存が終わったら、最後にUnity上で音の設定を完了させました。

  • 音源ファイル(WAVやMP3)の準備: 「ポーン」という短い音ファイルを、Projectウィンドウにドラッグ&ドロップしてインポートします。

  • スクリプトへの紐付け:  Hakobu_kun を選択し、Inspectorに新しく現れた Warning Sound の空欄に、準備した音ファイルをセットしました。

  • AudioSourceの確認: 自動で追加された AudioSource コンポーネントの「Play On Awake」がオフになっていることを確認しました。

ハコブくんの警告音ダウンロード

ハコブくんの安全機能をより確実なものにするため、センサーが反応した際に周囲へ注意を促す「警告音」を準備することにしました。 ただ停止するだけでなく、短く印象的な音を鳴らすことで、シミュレーションとしての臨場感と実用性を高めるのが狙いです。

適切な音源の選定

ハコブくんにふさわしい「声」を探すため、無料の音素材サイトを活用しました。ロボットの電子音やシステム音に適した素材が豊富に揃っており、非常に助かりました。

  • OtoLogic: 清潔感のある電子音が多いため、病院の廊下を走るハコブくんのイメージに合う素材を選びました。

音源のダウンロード

まずはテストとして、以下のサイトなどで「決定音」や「通知音」を探してダウンロードします。

OtoLogic(ビープ音)

  • おすすめ:短く歯切れの良い音。

Unityへのインポート作業

入手したWAVファイルやMP3ファイルを、Unityの Projectウィンドウ(Assetsフォルダ)の中にドラッグ&ドロップして取り込みました。 Unityが自動的にファイルを認識してくれるため、これでいつでもハコブくんが「発声」できる準備が整いました。

スクリプトへの実装

ハコブくん(Hakobu_kun)のインスペクターにある Warning Sound の空欄に、取り込んだ音ファイルを割り当てました。 これにより、プログラム上の warningSound という変数と実際の音源が紐付き、センサーが壁を検知した瞬間に音が鳴るようになりました。