Unity×AI開発講座【第3回】
『Gemini & ChatGPT × Unity』で本格カフェ経営シミュレーションを作る!
3D空間に家具・機器を1m単位でグリッド配置し、NavMeshをリアルタイム自動再計算しよう!
「マウス操作でカフェの床にテーブルやエスプレッソマシンを綺麗に並べたい!でも配置した時に客が壁や家具にめり込まないか心配…」
カフェ経営ゲームの満足度を決定づけるのが、自由度の高い店舗ビルド(内装カスタマイズ)機能です。マウスの指す位置に1m単位でスナップ配置し、設置と同時に客の歩行経路(NavMesh)を動的更新する仕組みがあれば、本格経営シミュレーションのクオリティへと一気に進化します。
第3回となる本記事では、3Dレイキャストによるグリッドスナップ計算、家具データのScriptableObject管理、配置・回転・削除機能、そしてNavMeshSurfaceによるリアルタイムNavMesh再焼き付け(Rebake)を、AIプロンプトを活用して完全実装していきます!
目次
1. はじめに(第3回で実装する建築&レイアウトシステム)
3D空間にオブジェクトを整然と配置するには、マウスのワールド座標を取得し、指定した格子(グリッド)の目の位置に数値を固定する「グリッドスナップ」処理が必要です。
フリー配置(整列しない危険な構造)
マウス位置にそのまま配置すると、テーブル同士のスキマが半端になり、斜めに傾いたり見た目が雑になってしまう。
3Dグリッド配置(本講座の方式)
1m(または0.5m)刻みでピタッと位置合わせ(スナップ)。回転やプレビュー表示で美しいカフェ内装をストレスなくビルドできる。
本章では、プレビュー表示を見ながら配置・回転・削除を行うレイアウト機能と、家具を置くたびに客のAI動線を再構築するリアルタイムNavMesh機能を構築します。
2. Step 1: 3Dグリッド配置アルゴリズムとマウスレイキャストの設計
グリッド配置の核心は「カメラから出た光線(Raycast)が床と交差したポイントを取得し、Mathf.FloorやMathf.Round関数で小数点を丸める数式」にあります。
レイキャストとグリッドスナップの基本構成
| 要素・ステップ | 処理内容・数式 | 解説とポイント |
|---|---|---|
| 1. Mouse Raycast | `Camera.main.ScreenPointToRay(Input.mousePosition)` | 画面上のマウスカーソルから3D空間に向けて透明な光線を照射 |
| 2. スナップ計算 ★最重要 | `new Vector3(Mathf.Floor(x) + 0.5f, y, Mathf.Floor(z) + 0.5f)` | 1m単位のマス目の「中央」にピッタリ配置させるためのオフセット数式 |
| 3. プレビュー表示 | 半透明マテリアル(半透明の緑や青)を適用 | 設置予定の場所を視覚的に提示し、回転状態や重なりを直感確認できるようにする |
初心者補足:Raycast(レイキャスト)とは?
画面上の特定の場所(マウスクリック等)から直線状の「見えないレーザー」を飛ばし、ヒットした3Dオブジェクト(今回は床面)の正確なxyz座標を取得するUnityの必須機能です。
\ グリッド配置スクリプトをAIに自動生成させよう! /
3. Step 2: AIプロンプトによる「GridBuildingSystem.cs」スクリプトの自動生成
配置・90度回転(Rキー)・右クリック削除・プレビュー移動を行う `GridBuildingSystem.cs` を生成させます。
コピペで使える!グリッド配置生成プロンプト
Unity 2022.3 (C#) で動作する「GridBuildingSystem.cs」を作成してください。
【要件】
1. マウス位置からのRaycastで「Floor」レイヤーの座標を取得してください。
2. 取得した座標を1m単位にスナップする関数 `GetGridPosition(Vector3 rawPos)` を作成してください。
3. 選択中の家具のプレビュー用GameObjectをマウスの動きに追従させて表示してください。
4. キー操作:
- Rキー:プレビューオブジェクトを90度回転(Rotate(0, 90, 0))
- 左クリック:プレビュー位置に本番の家具Prefabを生成(Instantiate)し、GameManagerから資金を引く。
- 右クリック:Raycastで「Furniture」レイヤーに当たったオブジェクトを破棄(Destroy)し、資金を返金する。
5. 配置完了時、NavMeshSurfaceのBuildNavMesh()を呼び出して動的に経路を更新するイベントを発火させてください。
生成された C# スクリプト:GridBuildingSystem.cs
using UnityEngine;
using Unity.AI.Navigation;
public class GridBuildingSystem : MonoBehaviour
{
[SerializeField] private LayerMask floorLayer;
[SerializeField] private LayerMask furnitureLayer;
[SerializeField] private PlaceableItemSO selectedItem;
[SerializeField] private NavMeshSurface navMeshSurface;
private GameObject previewObject;
private float currentYRotation = 0f;
private void Update()
{
HandlePreviewAndPlacement();
HandleRotation();
HandleDeletion();
}
public void SetSelectedItem(PlaceableItemSO newItem)
{
selectedItem = newItem;
if (previewObject != null) Destroy(previewObject);
if (selectedItem != null && selectedItem.previewPrefab != null)
{
previewObject = Instantiate(selectedItem.previewPrefab);
}
}
private void HandlePreviewAndPlacement()
{
if (selectedItem == null) return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f, floorLayer))
{
Vector3 gridPos = GetGridPosition(hit.point);
if (previewObject != null)
{
previewObject.transform.position = gridPos;
previewObject.transform.rotation = Quaternion.Euler(0, currentYRotation, 0);
}
// 左クリックで配置
if (Input.GetMouseButtonDown(0))
{
if (GameManager.Instance != null && GameManager.Instance.currentMoney >= selectedItem.cost)
{
GameManager.Instance.AddMoney(-selectedItem.cost);
Instantiate(selectedItem.placedPrefab, gridPos, Quaternion.Euler(0, currentYRotation, 0));
// NavMeshの動的再構築
if (navMeshSurface != null) navMeshSurface.BuildNavMesh();
}
else
{
Debug.LogWarning("資金が不足しています!");
}
}
}
}
private void HandleRotation()
{
if (Input.GetKeyDown(KeyCode.R))
{
currentYRotation = (currentYRotation + 90f) % 360f;
}
}
private void HandleDeletion()
{
if (Input.GetMouseButtonDown(1)) // 右クリックで削除
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f, furnitureLayer))
{
Destroy(hit.collider.gameObject);
if (navMeshSurface != null) navMeshSurface.BuildNavMesh();
}
}
}
private Vector3 GetGridPosition(Vector3 rawPos)
{
float x = Mathf.Floor(rawPos.x) + 0.5f;
float z = Mathf.Floor(rawPos.z) + 0.5f;
return new Vector3(x, rawPos.y, z);
}
}
4. Step 3: 配置オブジェクトのデータ管理(ScriptableObject)
家具やエスプレッソマシンのデータをC#に直接書き込まず(ハードコーディング回避)、アセット化して直感的に管理できる `ScriptableObject` を導入します。
ScriptableObject データ構造
名前・価格(Cost)・インテリア魅力度(Appeal)・プレハブ(Preview/Placed)を定義し、プロジェクトウィンドウ内で量産可能です。
カテゴリ(Category)分類
「Furniture(家具)」「Equipment(機器)」「Decor(装飾)」「Lighting(照明)」のEnumでUI分類を行えます。
PlaceableItemSO.cs スクリプト
using UnityEngine;
public enum ItemCategory
{
Furniture,
Equipment,
Decor,
Lighting
}
[CreateAssetMenu(fileName = "NewPlaceableItem", menuName = "Cafe/Placeable Item")]
public class PlaceableItemSO : ScriptableObject
{
public string itemName;
public ItemCategory category;
public int cost;
public float appealScore; // 店舗評価(映え度)加算用
public GameObject previewPrefab; // 半透明プレビュー用
public GameObject placedPrefab; // コライダー・NavMeshObstacle付き本番用
}
\ 家具を置いた瞬間にNavMeshを自動再計算! /
5. Step 4: 配置時のリアルタイムNavMesh動的更新(NavMeshSurface)
家具を新しく設置したり撤去した際、客AIがその場所を正しく迂回できるよう、Unityの **NavMeshComponents(NavMeshSurface)** を使用して実行時に歩行経路をリベイク(再構築)します。
NavMeshSurfaceの導入とリアルタイムリベイク設定手順
【Package Manager】を開き、【Com.unity.ai.navigation】(AI Navigationパッケージ)をプロジェクトにインストールします。
床(CafeFloor)または親オブジェクトに【NavMeshSurface】を追加。【Collect Objects】を「All」または「Volume」に設定します。
`GridBuildingSystem.cs` 内の家具配置・削除完了タイミングで `navMeshSurface.BuildNavMesh();` を呼ぶことで、1フレームでAIの歩行可能領域が最新化されます。
6. まとめ & よくある質問 (FAQ)
第3回では、カフェシミュレーションの醍醐味である「3Dグリッド配置」「回転・削除」「ScriptableObject管理」「NavMeshのリアルタイム再構築」を完成させました。プレイヤーが自由に内装を作り込める土台の完成です!
次回:第4回「パラメータ連動メニュー開発&クラフトUI」へ!
コーヒー豆・抽出量・ミルク泡立て率等のパラメータをUI調整し、販売価格・原価率・評価を動的算出する開発システムを作ります!
よくある質問 (FAQ)
Q. 配置した家具の上に別の家具が重なって置けてしまいます。どう防げば良いですか?
`Physics.OverlapBox` または `Physics.CheckSphere` 関数を用いて、スナップ予定位置に既存の「Furniture」レイヤーコライダーが存在するかを配置前に判定し、重なりがある場合は `Instantiate` をブロックする処理を追加してください。
Q. `navMeshSurface.BuildNavMesh()` を実行すると一瞬処理が重くなります(スパイク発生)。
家具全体を壊して毎回全マップを再リベイクすると重くなります。置かれた家具のPrefabに `NavMeshObstacle` コンポーネントをアタッチし、【Carve】にチェックを入れることで、BuildNavMeshを呼ばずにリアルタイムで穴あけ(くり抜き)処理が実行され高速化できます。