digitalcontent.jp マンガ×図解解説メディア

Gemini×Unityで作るカフェ経営ゲーム第2回:NavMeshとFSMで動く客AI

編集

まなびコミック編集部 監修


Unity×AI開発講座【第2回】

『Gemini & ChatGPT × Unity』で本格カフェ経営シミュレーションを作る!

自動で来店し、注文・着席・飲食・退店する自律型「客AI」をFSMとNavMeshで構築しよう!


第2回の実装を開始

「3D空間にキャラクターを読み込んだけど、指定の席まで自然に歩かせたり座らせる処理はどう書けば良い?」

カフェ経営シミュレーションの醍醐味は、お店にやってくるお客様の活気あふれる動きです。客が障害物を避けて席へ向かい、注文して飲食し、満足(または激怒)して帰っていく一連の挙動は、**「NavMesh(経路探索)」と「FSM(有限ステートマシン)」**を組み合わせることで美しく制御できます。

第2回となる本記事では、NavMeshAgentとAnimatorの連動、Enumを用いた客Stateの切り替え、頭上ワールドスペースUI(待ち時間ゲージ)、そして客の自動生成スポナーまでをAIプロンプトを活用して一気通貫で組み上げます!

1. はじめに(第2回で実装する客AIシステム)

3Dゲーム内で客キャラクターを移動させる際、壁やテーブルを無視して直線移動してしまうとゲームとして成り立ちません。Unityの「NavMesh」機能を使えば、障害物を自動で迂回するインテリジェントな動線が作成できます。

直線移動スクリプト(障害物を無視)

`transform.Translate` 等で移動させると、テーブルや壁をすり抜けたりスタックして立ち往生してしまう。

NavMesh × FSM制御(本講座の方式)

床をBakeして歩行ルートを自動計算。速度に合わせて歩行アニメーションを再生し、状態に応じて行動を自動変更する。

本章を終えると、お店の入り口に湧いたお客様が、空いているテーブルを探して歩き、座って注文待ち状態(不満ゲージ表示)になるまでの完全なAIループが完成します。

2. Step 1: NavMesh(経路探索)とAnimator(歩行・着席動作)の連動設定

まずはUnity上でキャラクターが歩くエリアを指定する「NavMeshの焼き付け(Bake)」と、キャラクターのアニメーション切り替え設定を行います。

ナビゲーションとアニメーションのセットアップ手順

設定ステップ Unity上の操作・設定内容 ポイント・注意点
1. 床と壁のBake設定 【Window】➔【AI】➔【Navigation】を開き、床を選択して【Bake】を実行 水色で表示された領域が客の歩行可能エリアになります
2. NavMeshAgentアタッチ ★最重要 客プレハブに【NavMeshAgent】コンポーネントを追加 Speed: 2.0, Stopping Distance: 0.2 に設定
3. Animator Controller作成 【Animator】で「Speed(float)」「IsSitting(bool)」のパラメータを追加 Idle / Walk / Sit のステートを作成し遷移条件(Conditions)を設定

初心者補足:NavMeshAgent(ナビメッシュエージェント)とは?

目的地(`agent.SetDestination(targetPos)`)を与えるだけで、焼き付けられた歩行エリアを判別し、衝突を避けながら最短ルートで移動してくれるUnityの自動AIコンポーネントです。

\ 客の状態遷移コードをAIに自動生成させよう! /


Step 2: CustomerAIスクリプト生成へ

3. Step 2: 有限ステートマシン(FSM)による客の行動ロジック設計

客AIの行動を「来店中」「着席済み」「注文待ち」「食事中」「退店中」という状態(State)に分解し、現在の状態に応じて処理を切り替える有限ステートマシン(FSM)をC#で構築します。

コピペで使える!客AI生成プロンプト

Unity 2022.3 (C#) で動く客AIスクリプト「CustomerAI.cs」を作成してください。

【要件】
1. Enum「CustomerState」を定義してください:
   - Spawning, MovingToSeat, WaitingForOrder, Dining, Leaving
2. 必要なコンポーネント:
   - NavMeshAgent, Animator
3. 行動ロジック:
   - MovingToSeat: 指定された座席(Transform)へ移動。到着したらAnimatorの「IsSitting」をtrueにしてWaitingForOrderに遷移。
   - WaitingForOrder: タイマーをカウント(最大30秒)。注文を受けたらDiningへ遷移。タイマー超過で怒り退店(Leaving)。
   - Dining: 10秒間食事。終了したら代金を払い(GameManager.Instance.AddMoney)、座席を解放してLeavingへ遷移。
   - Leaving: 出口(Transform)へ移動。到着したらGameObjectを破棄(Destroy)。
4. Animatorのfloat変数「Speed」に `agent.velocity.magnitude` を毎フレーム同期させて歩行アニメーションを切り替えてください。

生成された C# スクリプト:CustomerAI.cs

using UnityEngine;
using UnityEngine.AI;

public enum CustomerState
{
    Spawning,
    MovingToSeat,
    WaitingForOrder,
    Dining,
    Leaving
}

public class CustomerAI : MonoBehaviour
{
    public CustomerState currentState = CustomerState.Spawning;

    private NavMeshAgent agent;
    private Animator animator;

    public Transform targetSeat;
    public Transform exitPoint;

    public float maxWaitTime = 30f;
    private float currentWaitTimer = 0f;
    private float diningTimer = 0f;

    public System.Action OnWaitTimerUpdated; // 頭上UI通知用

    private void Awake()
    {
        agent = GetComponent();
        animator = GetComponent();
    }

    private void Update()
    {
        // 歩行アニメーションの速度同期
        animator.SetFloat("Speed", agent.velocity.magnitude);

        switch (currentState)
        {
            case CustomerState.MovingToSeat:
                if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
                {
                    // 座席に到着
                    transform.rotation = targetSeat.rotation;
                    animator.SetBool("IsSitting", true);
                    agent.enabled = false; // 座席固定のためAgentオフ
                    currentState = CustomerState.WaitingForOrder;
                }
                break;

            case CustomerState.WaitingForOrder:
                currentWaitTimer += Time.deltaTime;
                OnWaitTimerUpdated?.Invoke(currentWaitTimer, maxWaitTime);

                if (currentWaitTimer >= maxWaitTime)
                {
                    // 待ち時間オーバー(怒り退店)
                    GameManager.Instance?.SetRating(GameManager.Instance.currentRating - 0.2f);
                    LeaveStore();
                }
                break;

            case CustomerState.Dining:
                diningTimer += Time.deltaTime;
                if (diningTimer >= 10f)
                {
                    // 食事完了・会計
                    GameManager.Instance?.AddMoney(800); // ドリンク+フード代
                    LeaveStore();
                }
                break;

            case CustomerState.Leaving:
                if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
                {
                    Destroy(gameObject);
                }
                break;
        }
    }

    public void AssignSeat(Transform seat)
    {
        targetSeat = seat;
        agent.SetDestination(targetSeat.position);
        currentState = CustomerState.MovingToSeat;
    }

    public void ServeOrder()
    {
        if (currentState == CustomerState.WaitingForOrder)
        {
            currentState = CustomerState.Dining;
        }
    }

    private void LeaveStore()
    {
        animator.SetBool("IsSitting", false);
        agent.enabled = true;
        if (exitPoint != null) agent.SetDestination(exitPoint.position);
        currentState = CustomerState.Leaving;
    }
}

4. Step 3: 客の不満度・待ち時間を示す「頭上UI(ワールドスペースUI)」の実装

客の頭上に「注文待ちゲージ(Slider)」をワールドスペース表示(3D空間内に配置するUI)し、待ち時間が長くなると色が変わる演出を実装します。

World Space Canvas の作成手順

1

Canvasの追加とRender Mode変更

客プレハブの配下に【UI】➔【Canvas】を追加。Render Modeを【World Space】に変更し、Scaleを (0.01, 0.01, 0.01)、位置を頭上に調整します。

2

Sliderの配置とBillboardスクリプト

Canvas内に【UI】➔【Slider】を追加。カメラが回転しても常にゲージが正面を向くよう、`transform.LookAt(Camera.main.transform);` をスクリプトに記述します。

CustomerHeadUI.cs スクリプト

using UnityEngine;
using UnityEngine.UI;

public class CustomerHeadUI : MonoBehaviour
{
    [SerializeField] private Slider waitSlider;
    [SerializeField] private Image fillImage;
    [SerializeField] private CustomerAI customerAI;

    private void OnEnable()
    {
        if (customerAI != null) customerAI.OnWaitTimerUpdated += UpdateWaitGauge;
    }

    private void OnDisable()
    {
        if (customerAI != null) customerAI.OnWaitTimerUpdated -= UpdateWaitGauge;
    }

    private void LateUpdate()
    {
        // 常にメインカメラの方向を向かせる(Billboard処理)
        if (Camera.main != null)
        {
            transform.rotation = Camera.main.transform.rotation;
        }
    }

    private void UpdateWaitGauge(float current, float max)
    {
        float ratio = current / max;
        waitSlider.value = 1.0f - ratio; // 残り時間を表示

        // 待ち時間に応じて緑 ➔ 黄 ➔ 赤へ色変化
        if (ratio > 0.7f) fillImage.color = Color.red;
        else if (ratio > 0.4f) fillImage.color = Color.yellow;
        else fillImage.color = Color.green;
    }
}

\ 客の自動生成と座席案内を完成させよう! /


Step 4: Spawner & SeatManagerへ

5. Step 4: 客スポナー(CustomerSpawner)と座席管理(SeatManager)の実装

最後に、店内の空き席を検索する `SeatManager.cs` と、一定時間ごとに客を生成して空き席へ案内する `CustomerSpawner.cs` を構築します。

SeatManager(座席の空き状況管理)

店内の全テーブル席をリスト管理。空いている席(isOccupied = false)を検索し、客へ割り当てます。

CustomerSpawner(客の自動生成)

`GameManager.Instance.isStoreOpen` がtrueの間、5〜10秒ごとに客プレハブを出入口にInstantiateします。

CustomerSpawner.cs スクリプト

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CustomerSpawner : MonoBehaviour
{
    [SerializeField] private GameObject customerPrefab;
    [SerializeField] private Transform spawnPoint;
    [SerializeField] private Transform exitPoint;
    [SerializeField] private List seatList;

    private float spawnTimer = 0f;
    public float spawnInterval = 8f;

    private void Update()
    {
        if (GameManager.Instance != null && !GameManager.Instance.isStoreOpen) return;

        spawnTimer += Time.deltaTime;
        if (spawnTimer >= spawnInterval)
        {
            spawnTimer = 0f;
            TrySpawnCustomer();
        }
    }

    private void TrySpawnCustomer()
    {
        Transform emptySeat = GetEmptySeat();
        if (emptySeat == null)
        {
            Debug.Log("満席のため客が素通りしました");
            return;
        }

        GameObject obj = Instantiate(customerPrefab, spawnPoint.position, spawnPoint.rotation);
        CustomerAI ai = obj.GetComponent();
        ai.exitPoint = exitPoint;
        ai.AssignSeat(emptySeat);
    }

    private Transform GetEmptySeat()
    {
        // 簡易的な空き席検索(拡張可能)
        if (seatList.Count > 0)
        {
            return seatList[Random.Range(0, seatList.Count)];
        }
        return null;
    }
}

6. まとめ & よくある質問 (FAQ)

お疲れ様でした!第2回では、カフェ経営ゲームに命を吹き込む「NavMesh経路探索」「Animator連動」「FSM状態遷移」「頭上UI」「スポナー処理」を完全に実装しました。これでカフェに絶え間なくお客様が訪れるようになります!

次回:第3回「3Dグリッド建築&インテリア配置システム」へ!

レイキャストを用いて1m単位のグリッドに家具やマシンを配置し、NavMeshを動的更新するレイアウト機能を制作します!


第3回記事を読む(準備中)

よくある質問 (FAQ)

Q. 客が障害物に引っかかって動かなくなります。解決策は?

テーブルや壁のGameObjectのStatic設定を確認し、Navigationウィンドウで再度【Bake】を行ってください。また、`NavMeshAgent` の `Radius`(カプセルの太さ)を小さく調整することも有効です。

Q. 着席アニメーション中に客の位置がずれてしまいます。

着席時に `agent.enabled = false;` を実行してAgentによる移動物理を停止し、`transform.position = targetSeat.position;` で座標を固定してください。

サクッと読めて超わかりやすい!

【無料】マンガで全部わかる!

図解&ストーリー形式だから活字が苦手でもスルスル理解!全話を一覧からチェックしよう

マンガ全話一覧を見る(無料)
マンガ全話一覧へ 次話を読む