Unity×AI開発講座【第2回】
『Gemini & ChatGPT × Unity』で本格カフェ経営シミュレーションを作る!
自動で来店し、注文・着席・飲食・退店する自律型「客AI」をFSMとNavMeshで構築しよう!
「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に自動生成させよう! /
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 の作成手順
客プレハブの配下に【UI】➔【Canvas】を追加。Render Modeを【World Space】に変更し、Scaleを (0.01, 0.01, 0.01)、位置を頭上に調整します。
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;
}
}
\ 客の自動生成と座席案内を完成させよう! /
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を動的更新するレイアウト機能を制作します!
よくある質問 (FAQ)
Q. 客が障害物に引っかかって動かなくなります。解決策は?
テーブルや壁のGameObjectのStatic設定を確認し、Navigationウィンドウで再度【Bake】を行ってください。また、`NavMeshAgent` の `Radius`(カプセルの太さ)を小さく調整することも有効です。
Q. 着席アニメーション中に客の位置がずれてしまいます。
着席時に `agent.enabled = false;` を実行してAgentによる移動物理を停止し、`transform.position = targetSeat.position;` で座標を固定してください。