ハコブくん、自律パトロールへ。自動往復走行の実装
LOG47HRI2026/1/26

ハコブくん、自律パトロールへ。自動往復走行の実装

ハコブくんが全方位の安全を手に入れた今、次の目標は「自律的な往復走行」だ。私は、ROS 2からの入力をバイパスし、ハコブくん自身が「壁を見つけたら止まり、少し待ってから反対方向へ進む」というパトロール・モードを開発することにした。

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

パトロール・モード」の設計

インスペクターからON/OFFを切り替えられる isPatrolling フラグを導入した。これがONの時、ハコブくんは外部の命令を無視し、自ら設定した速度で進み始める。

反転ロジック:衝突を「転機」に変える

前後のセンサーが壁を検知してブレーキがかかった瞬間を、方向転換の合図とした。 ただすぐに反転するのではなく、「壁の前で1秒間停止」してから反対方向へ動き出すという「間(ま)」を設けることで、ロボットらしい意思を感じさせる動きを実現した。

走り続けるワイヤーフレーム

再生ボタンを押すと、ハコブくんは力強く前進を開始した。突き当たりの壁で「⚠ BRAKE」と共に停止し、1秒後、何事もなかったかのようにバックで戻り始める。 廊下の中を規則正しく往復し続けるその姿は、もはや単なる3Dモデルではなく、完成された「動くワイヤーフレーム」そのものだった。

自動往復に対応した「HakobuMove.cs」の追加コード

既存のスクリプトに、パトロール用の変数とロジックを組み込みます。

C#

[Header("Patrol Settings")]
public bool isPatrolling = true;    // パトロールモードのON/OFF
public float patrolSpeed = 0.2f;    // パトロール時の速度
private int patrolDirection = 1;    // 1:前進, -1:後退
private float stopTimer = 0f;       // 停止時間を測るタイマー

void FixedUpdate()
{
    float powerMultiplier = 3.0f;
    float currentSpeed = rawLinearSpeed; 

    // --- パトロールモード時の速度上書き ---
    if (isPatrolling)
    {
        currentSpeed = patrolSpeed * patrolDirection;
    }

    Vector3 center = transform.position;
    RaycastHit hit;

    // 前後のセンサー判定
    Vector3 frontStart = center + transform.forward * 0.4f;
    bool frontObstacle = Physics.Raycast(frontStart, transform.forward, out hit, sensorRange);
    Vector3 backStart = center - transform.forward * 0.4f;
    bool backObstacle = Physics.Raycast(backStart, -transform.forward, out hit, sensorRange);

    bool isBraking = false;

    // 前方または後方に壁があり、その方向に進もうとしている場合
    if ((frontObstacle && currentSpeed > 0) || (backObstacle && currentSpeed < 0))
    {
        currentSpeed = 0;
        isBraking = true;
    }

    if (isBraking)
    {
        // ブレーキ演出
        if (!isWarningPlayed && warningSound != null)
        {
            audioSource.PlayOneShot(warningSound);
            isWarningPlayed = true;
            if (brakeUI != null) brakeUI.SetActive(true);
        }

        // --- パトロール中の反転ロジック ---
        if (isPatrolling)
        {
            stopTimer += Time.fixedDeltaTime;
            if (stopTimer > 1.0f) // 1秒止まったら
            {
                patrolDirection *= -1; // 方向を反転
                stopTimer = 0f;
            }
        }
    }
    else
    {
        isWarningPlayed = false;
        if (brakeUI != null) brakeUI.SetActive(false);
        stopTimer = 0f; // 動いている時はタイマーリセット
    }

    // 移動の適用
    rb.linearVelocity = transform.forward * (currentSpeed * powerMultiplier);
    rb.angularVelocity = new Vector3(0, lastAngularVelocity * Mathf.Deg2Rad, 0);

    // デバッグ表示
    Debug.DrawRay(frontStart, transform.forward * sensorRange, frontObstacle ? Color.red : Color.green);
    Debug.DrawRay(backStart, -transform.forward * sensorRange, backObstacle ? Color.red : Color.green);
}

動作確認のポイント

  • Patrol ModeをONにする: ハコブくんのインスペクターにある Is Patrolling にチェックを入れます。

  • 実行: 再生ボタンを押します。ROS 2(Docker)側からの命令がなくても、ハコブくんが勝手に動き出せば成功です!

  • 往復の確認: 壁に当たって、約1秒後に反対向きに走り出すか確認してください。

ハコブくんの方向転換

自動パトロールを動かしてみたところ、ハコブくんが廊下の突き当たりを目指すのではなく、左右の壁に向かって短い距離を往復し続けていることに気づきました。 ハコブくんの正面(青い軸)が廊下の進行方向ではなく、横の壁を向いていたため、センサーも移動命令も「横」に対して働いていたのが原因です。私はハコブくんの「体の向き」を調整し、正しく廊下の奥へと進むように修正を行いました。

「正面」の定義と廊下の向きの不一致の特定

Unityのインスペクターを確認したところ、壁(Wall_Left など)は廊下として機能するように配置されていましたが、ハコブくんの初期の向き(Rotation Y)が壁の正面を向いてしまっていました。 そのため、プログラムが「前へ進め」と命令を出すたびに、ハコブくんは廊下の脇にある壁へと突進し、即座に自動ブレーキがかかるという状態になっていました。

インスペクターによる物理的な方向修正

この修正にはコードの変更は必要ありませんでした。ハコブくんという「実体」そのものを回転させることで解決を図りました。 インスペクターでハコブくんの Rotation Y を 90(または -90) に設定し、ハコブくんの forward(正面)が廊下の開口部をまっすぐ向くように調整しました。

廊下を駆け抜けるパトロールの成功

向きを修正した後に再度再生したところ、ハコブくんは横の壁に惑わされることなく、遥か先にある突き当たりの壁を目指して力強く走り始めました。

これで、ようやく「廊下をパトロールする」という本来の役割を果たす「動くワイヤーフレーム」としての形が整いました。

具体的な修正手順

ハコブくんの向きを修正するために、以下の操作を行いました。

  • Hierarchy で Hakobu_kun を選択します。

  • Inspector の一番上にある Transform > Rotation の項目を確認します。

  • Rotation Y の数値が中途半端な値になっている場合、ここを 90 または -90 に書き換えます。

  • Scene画面上で、ハコブくんから伸びている 青い矢印(Forward軸) が、廊下の奥(壁のない広い方向)を向いていることを目視で確認します。

ハコブくん廊下をはみ出さないための境界構築

パトロール走行を開始したハコブくんでしたが、廊下の端に到達しても止まることを知らず、そのまま虚空へと進み続けてしまいました。私は、ハコブくんが「ここが廊下の終点だ」と認識できるように、シミュレーション空間に境界を設けることにしました。

「終点壁」の設置による物理的な解決

最も確実な方法は、廊下の両端に壁を作る方法です。 現在の Wall_Left と Wall_Right は廊下の「横」を守っていますが、これに加えて「前」と「後ろ」を塞ぐ壁を追加しました。これにより、ハコブくんのRaycastが正しく壁を捉え、1秒間の停止と反転動作が確実にトリガーされるようになります。

サイドセンサーによる「脱輪」防止

廊下を直進していても、物理演算の誤差で少しずつ左右にズレてしまうことがあります。 そこで、前後だけでなく左右(Left/Right)にも短いセンサーを伸ばしました。これにより、万が一左右の壁に擦りそうになった際も、安全装置が働いて停止できるよう、ハコブくんの「パーソナルスペース」を全方位へと拡張しました。

箱庭の中の完全自律

廊下の両端に壁を置き、全方位センサーを有効化した結果、ハコブくんは廊下から一歩もはみ出すことなく、正確な往復走行を繰り返すようになりました。 左右にズレることもなく、端に到達すれば賢く反転する。これで、展示用の「動くワイヤーフレーム」としてどこに出しても恥ずかしくない完成度に到達しました。

全方位ガード・自動往復対応の「HakobuMove.cs」

左右の壁も検知して「横への暴走」も防ぐように、ロジックを強化しました。

C#

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

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

    private Rigidbody rb;
    private AudioSource audioSource;
    public AudioClip warningSound;
    public GameObject brakeUI;
    
    private bool isWarningPlayed = false;
    private float rawLinearSpeed;
    private float lastAngularVelocity;

    [Header("Sensor Settings")]
    public float sensorRange = 0.8f;
    public float sideRange = 0.3f; // 横の壁判定は少し短めに設定

    [Header("Patrol Settings")]
    public bool isPatrolling = true;
    public float patrolSpeed = 0.2f;
    private int patrolDirection = 1;
    private float stopTimer = 0f;

    void Start()
    {
    rb = GetComponent<Rigidbody>();
    audioSource = GetComponent<AudioSource>();
    
    rb.useGravity = true;

    // Y軸の固定(FreezeRotationY)も追加します
    rb.constraints = RigidbodyConstraints.FreezeRotationX | 
                     RigidbodyConstraints.FreezeRotationY | 
                     RigidbodyConstraints.FreezeRotationZ;

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

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

    void FixedUpdate()
    {
        float powerMultiplier = 3.0f;
        float currentSpeed = rawLinearSpeed; 

        if (isPatrolling)
        {
            currentSpeed = patrolSpeed * patrolDirection;
        }

        Vector3 center = transform.position;
        RaycastHit hit;

        // --- 全方位のセンサー判定 ---
        bool frontObs = Physics.Raycast(center + transform.forward * 0.4f, transform.forward, out hit, sensorRange);
        bool backObs = Physics.Raycast(center - transform.forward * 0.4f, -transform.forward, out hit, sensorRange);
        bool leftObs = Physics.Raycast(center - transform.right * 0.4f, -transform.right, out hit, sideRange);
        bool rightObs = Physics.Raycast(center + transform.right * 0.4f, transform.right, out hit, sideRange);

        bool isBraking = false;

        // 進行方向に障害物がある、または横の壁に近すぎる場合にブレーキ
        if ((frontObs && currentSpeed > 0) || (backObs && currentSpeed < 0) || leftObs || rightObs)
        {
            currentSpeed = 0;
            isBraking = true;
        }

        if (isBraking)
        {
            if (!isWarningPlayed && warningSound != null)
            {
                audioSource.PlayOneShot(warningSound);
                isWarningPlayed = true;
                if (brakeUI != null) brakeUI.SetActive(true);
            }

            // パトロール中なら、どの理由で止まっても1秒後に反転して脱出を試みる
            if (isPatrolling)
            {
                stopTimer += Time.fixedDeltaTime;
                if (stopTimer > 1.0f) 
                {
                    patrolDirection *= -1; // 方向転換
                    stopTimer = 0f;
                }
            }
        }
        else
        {
            isWarningPlayed = false;
            if (brakeUI != null) brakeUI.SetActive(false);
            stopTimer = 0f;
        }

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

        // デバッグ表示
        Debug.DrawRay(center + transform.forward * 0.4f, transform.forward * sensorRange, frontObs ? Color.red : Color.green);
        Debug.DrawRay(center - transform.forward * 0.4f, -transform.forward * sensorRange, backObs ? Color.red : Color.green);
        Debug.DrawRay(center - transform.right * 0.4f, -transform.right * sideRange, leftObs ? Color.red : Color.green);
        Debug.DrawRay(center + transform.right * 0.4f, transform.right * sideRange, rightObs ? Color.red : Color.green);
    }
}

仕上げの作業:廊下を「箱」にする

ハコブくんを止めるための「行き止まり」を作ります。

  • Hierarchyで Wall_Left などを複製(Cmd+D / Ctrl+D)し、名前を Wall_Front と Wall_Back にします。

  • Wall_Front を廊下の突き当たりに移動させ、Rotation Y を 90度回転 させて、廊下を塞ぐように配置します。

  • 同様に Wall_Back をスタート地点の後ろに配置します。

これで、ハコブくんは4つの壁に囲まれた「安全な廊下」を、往復し続けるようになります。