現場レベルのゲーム制作が、すべてここで学べます。
この講座ではUnityを使って「箱庭経営シミュレーション×農業ゲーム」を作成していきます。
今回はその第7回目です。
前回はショッピングシステムを実装してバケツや種を購入できるようにしました。
前回の記事:

前回はプレイヤーが農場経営に必要なアイテムを購入するお店システムでしたが、第7回ではお客さんが買い物に来てくれるお店システムを作ります。
商品棚を追加し、そこに収穫したにんじんを陳列することでお客さんが商品を買いに来るようにしてみましょう。
3DゲームでのルールベースAIエージェントの作成も行います。
Unityでショップの商品棚を作成しよう
まずは、商品棚として使用するオブジェクトを作成していきます。
はじめに、Hierarchy上で右クリックし、「Create Empty」を選択してください。作成された空のオブジェクトの名前を「ProductShelf_Group」に変更しましょう。

作成した「ProductShelf_Group」を選択し、InspectorのTransformを次のように設定しておきましょう。

次に、Projectウィンドウを開き、
AssetHuntsl > GameDev Starter Kit – Farming > Asset > Prop > Props_Crate_01
の順にフォルダをたどっていきます。
「Props_Crate_01」を見つけたら、それを先ほど作成した「ProductShelf_Group」にドラッグ&ドロップしてください(ProductShelf_Groupの子オブジェクトになるようにします)。

配置した「Props_Crate_01」を選択し、Ctrl + D を4回押して複製しましょう。
「Props_Crate_01 (4)」が生成されるまで複製できれば、合計で5個になります。

「Props_Crate_01」から「Props_Crate_01 (4)」までを、Shiftキーを押しながらまとめて選択しましょう。複数選択した状態で、InspectorのTransformを以下のように設定します。


Pos X = #*1.2-2.3とすればOKです(画像では#*1.2+-2.3となっていますがどちらでも同じ)。

続いて、当たり判定を追加していきます。
Hierarchyから「Props_Crate_01」の右側にある「>」をクリックし、Prefab編集モードに入りましょう。

画面が以下のようにPrefab専用の編集表示に切り替わっていれば、正しくPrefab編集モードに入れています。

「Props_Crate_01」を選択した状態で、Inspectorの「Add Component」をクリックしてください。表示された一覧から「Box Collider」を選択し、コンポーネントを追加しましょう。

今回、Props_Crate_01を5つコピーして使っていますが、それらの元となるプレハブを編集したのでコピー先のProps_Crate_01(1)~Props_Crate_01(4)にもBox Colliderが設定されます。
スクリプトから商品棚を作成しよう
今回作るのは、プレイヤーが商品を棚に陳列できるシステムです。
棚には複数の置き場(スロット)が用意されていて、そこに商品をきれいに並べられます。
イメージとしてはこんな感じです。
- プレイヤーがアイテムを拾う
- 棚を見ながらキーを押すと、空いている場所に商品がスッと置かれる(陳列)
- 棚に置いた商品は「棚の子オブジェクト」になって、棚が商品を管理できる
- 棚側の処理で「棚から商品を1個取り出す(購入扱いで消す)」こともできる

↑こうした機能をプレイヤーと商品棚両方のスクリプトの組み合わせで実現させます。
プレイヤー側(ItemInteractor)の仕組み
こちらは以前作成したItemInteractorに追記していきます。
プレイヤー側のスクリプトは、ざっくり言うと「今見ている対象」と「手に持っている物」から、「やるべき行動を自動で切り替える」役目です。
- 画面中央(カメラ中心)からRayを飛ばして、目の前の対象を判定
- 手にアイテムを持っていて、見ている先が棚なら「陳列」
- さらに、状況に応じて操作案内(例:Eで陳列、Qでドロップ)を表示
つまり、プレイヤー操作の入口(E/Qの処理)を一括で管理しているスクリプトです。
商品棚側(ItemPlacementSurface)の仕組み
棚側のスクリプトは、「棚の空き場所を探して、そこに商品を置く」のがメインです。
棚には slots(置き場配列)があり、空いているスロットに順番に商品を入れていきます。
ItemPlacementSurfaceには以下のような関数を用意します。
- TryGetPlaceSlot()
空いているスロット(子がいない場所)を探して返す - PlaceItemToSlot()
商品をスロットの位置へ移動し、スロットの子にする
さらにTweenで“スッ”と置く演出も入る - onlyCrops がONなら、作物(CropItem)以外は置けない
- TryTakeOneRandom()
棚に置かれている商品をランダムで1つ取り出す(購入処理の入口)
destroyOnTake=true なら、そのまま消えて「買われた扱い」になる
つまり棚側は、陳列の受け皿+在庫管理(棚に何個あるか、取り出せるか)を担当しています。
ItemBaseに「棚に置く」処理を用意しよう
これまで、アイテムを手に持つ・落とすといった挙動は、すべて ItemBase が管理していました。
プレイヤー操作側の ItemInteractor は、状況に応じて「ItemBaseの機能を呼び出す」役割でしたよね。
今回追加する 「棚に置く(陳列する)」 動きも、同じ考え方で進めます。
棚に置いたときの 位置合わせ や 物理挙動の切り替え などを ItemBase 側にまとめて実装しておけば、ItemInteractor や棚側スクリプトからは、必要なタイミングでそれを呼び出すだけで済みます。
ItemBase に、「棚に置かれたときの処理」を追加していきましょう。
Projectウィンドウから Scripts フォルダを開き、ItemBase を見つけたらダブルクリックして開きましょう。

開けたら、以下のマーカー部分を書き込んでください。
ItemBase.cs
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
using UnityEngine; using DG.Tweening; public class ItemBase : MonoBehaviour { protected Rigidbody rb; // 自分と子すべてのコライダー private Collider[] colliders; // 元レイヤー protected int originalLayer; //DOTween 用パラメータ [Header("Tween 設定")] [SerializeField] protected float pickDuration = 0.15f; [SerializeField] protected float placeDuration = 0.2f; [SerializeField] protected Ease pickEase = Ease.OutQuad; [SerializeField] protected Ease placeEase = Ease.OutQuad; // 手持ち中に使うTween(途中で拾い直しなどがあった場合にKillする用) protected Tweener moveTween; protected Tweener rotateTween; [SerializeField] private string playerRootTag = "Player"; protected virtual void Awake() { rb = GetComponent<Rigidbody>(); colliders = GetComponentsInChildren<Collider>(true); originalLayer = gameObject.layer; } // 手に持つとき public virtual void OnPickedUp(Transform handPoint) { // 既存のTweenがあれば止める KillTweens(); // 物理設定 if (rb != null) { if (!rb.isKinematic) { rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } rb.isKinematic = true; rb.useGravity = false; } // コライダーOFF(Rayを邪魔しない) SetCollidersEnabled(false); // Handレイヤーへ再帰的に変更 bool isPlayerPickup = handPoint != null && handPoint.root != null && handPoint.root.CompareTag(playerRootTag); if (isPlayerPickup) { int handLayer = LayerMask.NameToLayer("Hand"); SetLayerRecursiveInternal(transform, handLayer); } else { // Player以外が拾ったとき } // まず親を手にする(位置ズレを防ぐ) transform.SetParent(handPoint, worldPositionStays: true); // ローカル座標へ Tween で寄せる moveTween = transform.DOLocalMove(Vector3.zero, pickDuration) .SetEase(pickEase); rotateTween = transform.DOLocalRotate(Vector3.zero, pickDuration) .SetEase(pickEase); } // 落とすとき public virtual void OnDropped(Vector3 forward) { transform.SetParent(null); if (rb != null) { rb.isKinematic = false; rb.useGravity = true; float pushPower = 1.0f; rb.AddForce(forward * pushPower, ForceMode.VelocityChange); } SetCollidersEnabled(true); // レイヤー戻す SetLayerRecursiveInternal(transform, originalLayer); // 軽い演出 transform.DOPunchScale(Vector3.one * 0.05f, 0.2f, 8, 0.8f); } // 棚などに置くとき public virtual void OnPlaced(Vector3 position, Quaternion rotation, ItemPlacementSurface surface) { KillTweens(); transform.SetParent(surface.transform, worldPositionStays: true); if (rb != null) { rb.isKinematic = true; rb.useGravity = false; } // コライダーON(棚の上から再度拾えるように) SetCollidersEnabled(true); // 元レイヤーへ戻す(手カメラではなく通常カメラで表示) SetLayerRecursiveInternal(transform, originalLayer); // ふわっと指定位置/回転へ動かす moveTween = transform.DOMove(position, placeDuration) .SetEase(placeEase); rotateTween = transform.DORotateQuaternion(rotation, placeDuration) .SetEase(placeEase); } // 共通:コライダー ON/OFF protected void SetCollidersEnabled(bool enabled) { if (colliders == null) return; foreach (var c in colliders) { if (c != null) c.enabled = enabled; } } // レイヤー操作(親子含めて変更) protected void SetLayerRecursiveInternal(Transform t, int layer) { t.gameObject.layer = layer; foreach (Transform child in t) { SetLayerRecursiveInternal(child, layer); } } // Tween 停止 protected void KillTweens() { moveTween?.Kill(); rotateTween?.Kill(); moveTween = null; rotateTween = null; } protected virtual void OnDestroy() { KillTweens(); } } |
※ItemPlacementSurfaceを作成していないためエラーになりますが、先のパートで作成するので現段階では問題ありません。
スクリプトの解説
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 棚などに置くとき public virtual void OnPlaced(Vector3 position, Quaternion rotation, ItemPlacementSurface surface) { KillTweens(); transform.SetParent(surface.transform, worldPositionStays: true); if (rb != null) { rb.isKinematic = true; rb.useGravity = false; } // コライダーON(棚の上から再度拾えるように) SetCollidersEnabled(true); // 元レイヤーへ戻す(手カメラではなく通常カメラで表示) SetLayerRecursiveInternal(transform, originalLayer); // ふわっと指定位置/回転へ動かす moveTween = transform.DOMove(position, placeDuration) .SetEase(placeEase); rotateTween = transform.DORotateQuaternion(rotation, placeDuration) .SetEase(placeEase); } |
このメソッドは、アイテムを棚などの設置場所に置いたときに呼ばれる処理です。
「手に持っている状態」から「棚に安定して置かれた状態」へ切り替える役割を持っています。
① 既存のTweenを停止する
|
1 |
KillTweens(); |
拾う途中などで動いているアニメーションがあれば、ここで必ず停止します。
これをしないと、古いTweenが残って不自然な動きをしてしまう可能性があります。
② 親オブジェクトを棚に変更する
|
1 |
transform.SetParent(surface.transform, worldPositionStays: true); |
このアイテムを、棚(ItemPlacementSurface)の子オブジェクトにします。
これにより、棚の管理下に入る状態になります。
worldPositionStays: true にしているため、ワールド座標は維持したまま親だけ変更されます。
③ 物理を停止する
|
1 2 3 4 5 |
if (rb != null) { rb.isKinematic = true; rb.useGravity = false; } |
棚に置いたアイテムは、物理演算で落ちたり動いたりしてはいけません。
そのため、
- isKinematic を true にして物理の影響を止める
- 重力を無効にする
という処理を行っています。
④ コライダーをONに戻す
|
1 2 |
// コライダーON(棚の上から再度拾えるように) SetCollidersEnabled(true); |
手に持っているときはコライダーをOFFにしていました。
棚に置いたら、再び当たり判定を有効にします。
これによって、プレイヤーが再び拾える(レイキャストが当たる)ようになります。
⑤ レイヤーを元に戻す
|
1 2 |
// 元レイヤーへ戻す(手カメラではなく通常カメラで表示) SetLayerRecursiveInternal(transform, originalLayer); |
手に持っている間は「Hand」レイヤーに変更していました。
棚に置いたら、元のレイヤーに戻します。これにより、最前面に表示していた状態が戻ります。
↓もしこれをしなかったら…

↑このように以前作成したカメラ最前面に表示させる処理が動いたままになってしまいます。
⑥ 指定位置へアニメーション移動
|
1 2 3 4 5 6 |
// ふわっと指定位置/回転へ動かす moveTween = transform.DOMove(position, placeDuration) .SetEase(placeEase); rotateTween = transform.DORotateQuaternion(rotation, placeDuration) .SetEase(placeEase); |
最後に、棚の指定位置と回転へ、ふわっと移動させています。
- DOMove でワールド座標へ移動
- DORotateQuaternion で回転を合わせる
- placeEase で動きの滑らかさを調整
これによって、「スッ」と棚に並ぶ自然な演出が実現されています。
まとめ
OnPlaced は、
- 古いアニメーションを止める
- 親を棚に変更する
- 物理を止める
- コライダーを戻す
- レイヤーを戻す
- 指定位置へ滑らかに移動させる
という流れで、“持っている状態”から“棚にきれいに置かれた状態”へ切り替えるメソッドです。
操作側は「棚に置く」という命令を出すだけで、実際の細かい制御はすべて ItemBase が担当するという設計になっています。
商品棚側のスクリプトを作成しよう
次は商品棚のスクリプトを作成していきましょう。
まず、Projectウィンドウから Scripts フォルダを開きます。
フォルダ内で右クリックし、Create > MonoBehaviour Scriptを選択してください。
新しく作成されたスクリプトの名前を「ItemPlacementSurface」 に変更しましょう。

作成できたら、そのスクリプトをダブルクリックで開き、以下の内容を書き込んでください。
まとめ

今回はNavMeshエージェントなどを活用し、
- 作成した商品棚に農作物を配置し
- お客さんが来店して購入していく
という仕組みまで実装しました。
これにより「農場で作物を育てる → お店に商品を並べる → 商品が売れる」という一連の流れが完成しました。
現状は「作物を育てて売るシンプルな経営ゲーム」という形になっています。このままだとゲームとしては少し単調です。
そこで、次回の講座ではゲーム性を広げるための“妨害要素”を追加していきます。
単に売るだけではなく思い通りにいかない状況を作り上げ、プレイヤーの判断が問われるトラブル展開を取り入れます。
これによりさらにゲームらしい体験に発展させていきます。
次の記事:

現場レベルのゲーム制作が、すべてここで学べます。







コメント