デジタルツインで育つ知能 - ハコブくんの自律実装プロジェクトはじめます
LOG62HRI2026/2/10

デジタルツインで育つ知能 - ハコブくんの自律実装プロジェクトはじめます

固定されたコードから、状況を分析する自律知能へ。

搬送ロボット「ハコブくん」の自律走行を支援する「開発専用AI」としての役割を定義し、その第一歩を刻みます。 日本の医療施設規格を反映したデジタルツインの構築を皮切りに、AIが自ら制御ロジックを修正・最適化し続ける「自律実装」のサイクル。エンジニアは予測に時間を費やすのではなく、ハコブくんと共に未知の状況へ適応する知能をデザインする。そのための具体的な実装フローを公開します。

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

病院フロア自動生成スクリプト作成

まずは、日本の医療施設規格(廊下幅2.7m以上など)をパラメータとして保持し、Cubeを組み合わせてプロトタイプフロアを動的に生成する HospitalBaseBuilder.cs を作成します。

HospitalBaseBuilder.cs

このスクリプトは、Unityの空のGameObjectにアタッチして使用します。

C#

using UnityEngine;
using Unity.AI.Navigation; // NavMesh Componentsが必要
using System.Collections.Generic;

public class HospitalBaseBuilder : MonoBehaviour
{
    [Header("Hospital Scale Settings (meters)")]
    [SerializeField] private float corridorWidth = 2.7f; // 医療施設規格に準拠
    [SerializeField] private float roomSize = 6.0f;
    [SerializeField] private int roomCount = 4;
    [SerializeField] private float wallHeight = 2.5f;

    [Header("Materials (Optional)")]
    [SerializeField] private Material floorMaterial;
    [SerializeField] private Material wallMaterial;

    private NavMeshSurface _navMeshSurface;

    public void BuildLayout()
    {
        ClearExistingLayout();
        
        // 1. 床の生成
        GameObject floor = GameObject.CreatePrimitive(PrimitiveType.Cube);
        floor.name = "HospitalFloor";
        float totalLength = (roomSize + corridorWidth) * (roomCount / 2);
        float totalWidth = (roomSize * 2) + corridorWidth;
        floor.transform.localScale = new Vector3(totalLength, 0.1f, totalWidth);
        floor.transform.position = new Vector3(totalLength / 2, -0.05f, totalWidth / 2);
        ApplyMaterial(floor, floorMaterial);

        // 2. 廊下と病室のレイアウト生成
        for (int i = 0; i < roomCount; i++)
        {
            CreateRoom(i);
        }

        // 3. NavMeshの更新
        UpdateNavMesh();
        
        Debug.Log($"[ハコブくん・シミュレーター] 病院レイアウト生成完了: 廊下幅 {corridorWidth}m");
    }

    private void CreateRoom(int index)
    {
        bool isTopSide = index % 2 == 0;
        float xOffset = (index / 2) * (roomSize + 1.0f);
        float zOffset = isTopSide ? roomSize + corridorWidth : 0;

        // 病室の壁生成(簡易版:コの字型)
        GameObject roomGroup = new GameObject($"Room_{index}");
        roomGroup.transform.parent = this.transform;

        // 壁の配置(奥・左・右)
        BuildWall("BackWall", new Vector3(xOffset + roomSize / 2, wallHeight / 2, zOffset + (isTopSide ? roomSize : 0)), new Vector3(roomSize, wallHeight, 0.1f), roomGroup.transform);
        BuildWall("LeftWall", new Vector3(xOffset, wallHeight / 2, zOffset + roomSize / 2), new Vector3(0.1f, wallHeight, roomSize), roomGroup.transform);
        BuildWall("RightWall", new Vector3(xOffset + roomSize, wallHeight / 2, zOffset + roomSize / 2), new Vector3(0.1f, wallHeight, roomSize), roomGroup.transform);
    }

    private void BuildWall(string wallName, Vector3 pos, Vector3 scale, Transform parent)
    {
        GameObject wall = GameObject.CreatePrimitive(PrimitiveType.Cube);
        wall.name = wallName;
        wall.transform.position = pos;
        wall.transform.localScale = scale;
        wall.transform.parent = parent;
        ApplyMaterial(wall, wallMaterial);
    }

    private void ApplyMaterial(GameObject obj, Material mat)
    {
        if (mat != null) obj.GetComponent<Renderer>().material = mat;
    }

    private void ClearExistingLayout()
    {
        while (transform.childCount > 0)
        {
            DestroyImmediate(transform.GetChild(0).gameObject);
        }
    }

    private void UpdateNavMesh()
    {
        if (_navMeshSurface == null)
            _navMeshSurface = gameObject.AddComponent<NavMeshSurface>();
        
        _navMeshSurface.BuildNavMesh();
    }
}

実装のポイント

  • 規格のコード化: corridorWidth をデフォルトで 2.7m に設定しています。これにより、ロボットが看護師とすれ違う際の有効幅を正確にシミュレートできます。

  • NavMeshの統合: 生成と同時に NavMeshSurface.BuildNavMesh() を呼び出すことで、「ハコブくん」が即座に走行可能なエリアを認識できるようにしています。

  • 拡張性: CreateRoom メソッドを拡張することで、スタッフステーションや薬剤保管庫などの特殊な部屋タイプへの差し替えが容易な設計です。

ハコブくん・シミュレーターの開発

まずは環境構築からします。 このスクリプトは、Unity Editor上で作成し、C#の統合開発環境(Visual Studio や JetBrains Rider、あるいは VS Code)を使って記述します。

作業の詳しい手順を、シミュレーションエンジニアの視点でガイドします。

Unityプロジェクトの準備

  • Unity Hubを起動し、2022.3 LTS バージョンで新しいプロジェクトを作成します。

  • テンプレートは 「3D (Built-in Render Pipeline)」 または 「URP」 を選択してください。

  • プロジェクト名は HakobuKun_Simulator としましょう。

AI Navigationパッケージの導入

Unity-Package Manager

「ハコブくん」がフロアを認識して走るために、Unity公式のNavMesh機能が必要です。

  • Unity上部のメニューから Window > Package Manager を開きます。

  • 左上の「+」ボタン横のドロップダウンから 「Unity Registry」 を選択します。

  • 検索窓に 「AI Navigation」 と入力し、Install をクリックします。

スクリプトの作成

  • Project ウィンドウの Assets フォルダ内で右クリックし、Create > Folder で 「Scripts」 フォルダを作ります。

  • Scripts フォルダ内で右クリックし、Create > MonoBehaviour Scriptを選択。

  • 名前を正確に HospitalBaseBuilder と入力します(大文字小文字も合わせます)。

  • ファイルをダブルクリックしてエディタを開き、先ほど私が提示したコードをコピー&ペーストして保存(Ctrl+S)します。

シーンへの配置と実行

  • Unityの Hierarchy ウィンドウで右クリックし、Create Empty を選択。名前を 「HospitalManager」 にします。

  • 作成した HospitalManager に、HospitalBaseBuilder.cs をドラッグ&ドロップしてアタッチします。

  • Inspector ウィンドウに表示された Corridor Width が 2.7 になっていることを確認してください。

自動生成の実行(プロトタイプ確認)

Unity画面-フロアの自動生成

今のコードは BuildLayout() を呼び出す必要があります。手軽に試すために、一時的にコードの最後に以下を追加してみましょう。

C#

// スクリプト内の最後の方に追加
void Start() 
{
    BuildLayout();
}
  • Unity画面上部の 「Play (再生)」ボタン を押します。

  • 画面に白いCubeで構成された「病院の廊下と病室」が出現します。

現在の状態

  • 病院の土台完成: 日本の医療施設規格に基づいたフロアが動的に生成されています。

  • NavMeshの統合: HospitalManager のインスペクターに NavMesh Surface が自動追加されており、「ハコブくん」がどこを走れるか計算済みの状態です。

  • 自動生成成功: Start() メソッドにより、再生ボタンを押した瞬間にレイアウトが構築されるようになっています。

部屋数を増やしてみる

Unity画面-部屋数10

パラメータ(変数)をインスペクターから変更できるように設計しました。コードを書き直す必要はなく、Unityの画面上での操作だけでフロアを拡張できます。

以下の手順で修正を行います。

部屋数とサイズの変更方法

  • Hierarchy(ヒエラルキー)ウィンドウで HospitalManager を選択します。

  • Inspector(インスペクター)ウィンドウの Hospital Base Builder (Script) の項目を確認します。

以下の数値を好きな値に書き換えます

  • Room Count: 部屋の総数です(例:4 → 10 にすると、左右5部屋ずつになります)。

  • Room Size: 1部屋の大きさです。

  • Corridor Width: 2.7mのままが推奨ですが、さらに広くすることも可能です。

変更の反映

数値を変えた後、以下のいずれかの方法でフロアが更新されます。

  • 再生中の場合: 一度再生を停止(■)し、再度再生(▶)してください。Start() メソッドが走り、新しい数値で作り直されます。

  • 再生中ではない場合: そのまま再生ボタンを押せば、新しい設定で生成されます。

最終形態へのアップデートです。

Unity画面-40部屋の病室フロアシミュレーション
  • 4大フロアセクション化: 現在の「上下2セクション」を、中央で左右に割るメイン通路を追加して「田の字型」の4セクションにします。

  • 外周壁の追加: 病院の「外壁」として、周回路の外側にぐるっと壁を立て、シミュレーション空間を完結させます。

C#

using UnityEngine;
using Unity.AI.Navigation;

public class HospitalBaseBuilder : MonoBehaviour
{
    [Header("Hospital Layout Settings")]
    [SerializeField] private float mainCorridorWidth = 4.0f;  // 中央の十字メイン通路
    [SerializeField] private float roomCorridorWidth = 2.7f;  // 各ブロック内の通路
    [SerializeField] private float outerCorridorWidth = 3.0f; // 建物の外周路
    [SerializeField] private float roomSize = 6.0f;
    [SerializeField] private int totalRooms = 40;            // 合計40部屋(1ブロック10部屋)
    [SerializeField] private float wallHeight = 2.5f;

    private NavMeshSurface _navMeshSurface;

    private void Start() => BuildLayout();

    public void BuildLayout()
    {
        ClearExistingLayout();

        int roomsPerBlock = totalRooms / 4; // 10部屋
        int roomsInRow = roomsPerBlock / 2; // 5部屋
        float blockLength = roomsInRow * (roomSize + 1.0f);
        float blockWidth = (roomSize * 2) + roomCorridorWidth;
        
        // 外周路を含めた全体の床サイズ
        float totalFloorLength = (blockLength * 2) + mainCorridorWidth + (outerCorridorWidth * 2);
        float totalFloorWidth = (blockWidth * 2) + mainCorridorWidth + (outerCorridorWidth * 2);

        // 1. 全体の床
        GameObject floor = GameObject.CreatePrimitive(PrimitiveType.Cube);
        floor.name = "HospitalFloor_Global";
        floor.transform.localScale = new Vector3(totalFloorLength, 0.1f, totalFloorWidth);
        floor.transform.position = new Vector3(0, -0.05f, 0); // 中心を0に
        floor.transform.parent = this.transform;

        // 2. 4つのブロックを生成(北西、北東、南西、南東)
        float xOffset = (blockLength / 2) + (mainCorridorWidth / 2);
        float zOffset = (blockWidth / 2) + (mainCorridorWidth / 2);

        CreateBlock("Block_NW", -xOffset,  zOffset, roomsInRow);
        CreateBlock("Block_NE",  xOffset,  zOffset, roomsInRow);
        CreateBlock("Block_SW", -xOffset, -zOffset, roomsInRow);
        CreateBlock("Block_SE",  xOffset, -zOffset, roomsInRow);

        // 3. 外壁の生成
        BuildOuterWalls(totalFloorLength, totalFloorWidth);

        UpdateNavMesh();
        Debug.Log($"[ハコブくん] 4セクション・外壁付き大規模フロア完成");
    }

    private void CreateBlock(string blockName, float xCenter, float zCenter, int roomsInRow)
    {
        GameObject blockGroup = new GameObject(blockName);
        blockGroup.transform.parent = this.transform;

        for (int i = 0; i < roomsInRow; i++)
        {
            float xPos = xCenter - (roomsInRow * (roomSize + 1.0f) / 2) + (i * (roomSize + 1.0f)) + (roomSize / 2);
            
            // 各ブロック内の「北列」と「南列」
            float northRowZ = zCenter + (roomCorridorWidth / 2 + roomSize / 2);
            float southRowZ = zCenter - (roomCorridorWidth / 2 + roomSize / 2);

            CreateUPathRoom(xPos, northRowZ, true, blockGroup.transform); 
            CreateUPathRoom(xPos, southRowZ, false, blockGroup.transform);
        }
    }

    private void CreateUPathRoom(float xPos, float zPos, bool openToSouth, Transform parent)
    {
        GameObject roomGroup = new GameObject($"Room_X{xPos}_Z{zPos}");
        roomGroup.transform.parent = parent;

        float backWallZ = openToSouth ? zPos + roomSize / 2 : zPos - roomSize / 2;
        BuildWall("Back", new Vector3(xPos, wallHeight / 2, backWallZ), new Vector3(roomSize, wallHeight, 0.1f), roomGroup.transform);
        BuildWall("Left", new Vector3(xPos - roomSize / 2, wallHeight / 2, zPos), new Vector3(0.1f, wallHeight, roomSize), roomGroup.transform);
        BuildWall("Right", new Vector3(xPos + roomSize / 2, wallHeight / 2, zPos), new Vector3(0.1f, wallHeight, roomSize), roomGroup.transform);
    }

    private void BuildOuterWalls(float length, float width)
    {
        GameObject wallGroup = new GameObject("OuterWalls");
        wallGroup.transform.parent = this.transform;

        // 四方の外壁
        BuildWall("Outer_N", new Vector3(0, wallHeight / 2,  width / 2), new Vector3(length, wallHeight, 0.1f), wallGroup.transform);
        BuildWall("Outer_S", new Vector3(0, wallHeight / 2, -width / 2), new Vector3(length, wallHeight, 0.1f), wallGroup.transform);
        BuildWall("Outer_E", new Vector3( length / 2, wallHeight / 2, 0), new Vector3(0.1f, wallHeight, width), wallGroup.transform);
        BuildWall("Outer_W", new Vector3(-length / 2, wallHeight / 2, 0), new Vector3(0.1f, wallHeight, width), wallGroup.transform);
    }

    private void BuildWall(string wallName, Vector3 pos, Vector3 scale, Transform parent)
    {
        GameObject wall = GameObject.CreatePrimitive(PrimitiveType.Cube);
        wall.name = wallName;
        wall.transform.position = pos;
        wall.transform.localScale = scale;
        wall.transform.parent = parent;
    }

    private void ClearExistingLayout()
    {
        while (transform.childCount > 0)
            DestroyImmediate(transform.GetChild(0).gameObject);
    }

    private void UpdateNavMesh()
    {
        if (_navMeshSurface == null) _navMeshSurface = gameObject.AddComponent<NavMeshSurface>();
        _navMeshSurface.BuildNavMesh();
    }
}