アイテムを拾う・使う・捨てる処理がプレイヤーのスクリプトに散らばり、装備品やスタック不可アイテムを足すたびにあちこち直す——インベントリを行き当たりばったりで作ると、あっという間にスパゲッティコードになります。
この記事では、 データ(アイテムの定義)・ロジック(管理)・UI(表示)を3つの層に分ける インベントリ設計を作ります。Resource・シングルトン・シグナルを組み合わせ、アイテムの種類が増えてもUIを変えても、核となるロジックは安定して使い回せる形を目指します。
この記事でわかること
- データ・ロジック・UIを分ける 3層アーキテクチャ
Resourceでアイテムを .tresファイル として定義する(データ駆動)- シングルトンの
InventoryManagerで状態を 1箇所に集めるinventory_changedシグナルで UIを自動更新 する
3層に分けて設計する
堅牢なシステムの鍵は 「関心の分離」 です。インベントリを、次の3つの層に分けて作ります。

| 層 | 役割 | Godotでの実装 |
|---|---|---|
| データ層 | 「アイテムとは何か」を定義する | Resource(.tres ファイル) |
| ロジック層 | 追加・削除・使用などの状態を管理する | シングルトン Node(Autoload) |
| ビュー層(UI) | 状態をプレイヤーに見せる | Control ノード群 |
こう分けると、各層は他の層の中身を知らずに、自分の仕事に集中できます。UIを作り替えてもロジックは無傷、アイテムを増やしてもUIコードは触らずに済みます。
データ層:Resourceでアイテムを定義する
アイテムの設計図 を Resource で作ります。Resource はエディタで直接編集・保存できるので、剣やポーションを .tres ファイルとして どんどん量産できます(プログラマー以外でも追加できます)。データ駆動の考え方は カスタムリソースの作成と活用 でも詳しく扱っています。

# item_resource.gd
class_name ItemResource
extends Resource
enum ItemType { CONSUMABLE, EQUIPMENT, KEY_ITEM, MATERIAL }
@export_group("基本情報")
@export var item_name: String = "New Item"
@export_multiline var description: String = ""
@export var texture: Texture2D # アイコン
@export_group("インベントリ設定")
@export var type: ItemType = ItemType.CONSUMABLE
@export var stackable: bool = true
@export var max_stack_size: int = 99
@export_group("ゲームプレイ")
@export var can_be_used: bool = true
@export var heal_amount: int = 10
# 一意なID。保存済みならファイルパス、未保存ならインスタンスIDで代用
func get_id() -> String:
return resource_path if not resource_path.is_empty() else str(get_instance_id())
.tres のパス自体がユニークIDになるので、文字列IDを別管理する必要がありません。
ロジック層:シングルトンで状態を管理する
インベントリの状態は、Autoload のシングルトン InventoryManager に 1箇所だけ 持たせます。プレイヤーも宝箱も敵も、みんなここを通してアイテムを出し入れします(Single Source of Truth)。

# inventory_manager.gd (Autoloadに登録)
extends Node
signal inventory_changed # 中身が変わったら通知
signal item_used(item: ItemResource) # 使用時(効果音などに)
# キー: アイテムID, 値: { "resource": ItemResource, "count": int }
var _items: Dictionary = {}
const MAX_SLOTS := 30
func add_item(item: ItemResource, count := 1) -> bool:
if item == null:
return false
var id := item.get_id()
if item.stackable and _items.has(id):
_items[id].count += count
inventory_changed.emit()
return true
if _items.size() < MAX_SLOTS:
_items[id] = {"resource": item, "count": count}
inventory_changed.emit()
return true
return false # いっぱい
func use_item(id: String) -> void:
if not _items.has(id):
return
var item: ItemResource = _items[id].resource
if not item.can_be_used:
return
item_used.emit(item)
if item.type == ItemResource.ItemType.CONSUMABLE:
remove_item(id, 1) # 消費アイテムは減らす
func remove_item(id: String, count := 1) -> void:
if not _items.has(id):
return
_items[id].count -= count
if _items[id].count <= 0:
_items.erase(id)
inventory_changed.emit()
func get_inventory_data() -> Dictionary:
return _items.duplicate(true)
状態を変える処理(add / use / remove)はすべて、最後に inventory_changed.emit() を呼びます。これが次のUI更新の合図です。
ビュー層:シグナルでUIを自動更新する
UIは 状態を映すだけ の存在にします。inventory_changed を受け取ったら描き直す、それだけ。配置は GridContainer が便利です。

# inventory_ui.gd
extends GridContainer
const SLOT := preload("res://ui/inventory_slot.tscn")
func _ready() -> void:
InventoryManager.inventory_changed.connect(_redraw) # 変更を購読
_redraw()
func _redraw() -> void:
for c in get_children():
c.queue_free() # いったん全消し
var data := InventoryManager.get_inventory_data()
for id in data:
var slot := SLOT.instantiate()
slot.update_display(data[id].resource, data[id].count)
# クリックされたら使用をロジック層に依頼するだけ
slot.gui_input.connect(func(e: InputEvent) -> void:
if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT and e.pressed:
InventoryManager.use_item(id))
add_child(slot)
UIがやるのは InventoryManager.use_item(id) と お願いするだけ 。実際に減らすか、回復するかはロジック層に完全にお任せです。だからUIのデザインを変えても、ロジックは1行も変わりません。
実践:ポーションを拾って使う
3つの層がそろったので、 「落ちているポーションを拾って、インベントリから使うと回復する」 という一連の流れを通してみます。RPGでもローグライクでもサバイバルでも、この往復は同じ形です。

拾うアイテム(Area2D)は、触れたら InventoryManager に足して自分は消えるだけです。
# item_pickup.gd
extends Area2D
@export var item: ItemResource # エディタで .tres を割り当てる
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
if InventoryManager.add_item(item): # ロジック層へ足す
queue_free() # 拾えたら消える
これだけで、あとは自動でつながります。
- 拾う →
add_item()が_itemsに足してinventory_changedを出す - UIがそれを受け取り、グリッドを描き直す(ポーションが増える)
- ス ロットをクリック →
use_item()がitem_usedを出して個数を減らす - 回復処理は
item_usedを聞いた別のシステム(プレイヤーのHP)が担当する
ポイントは2つです。
- 誰も他人の中身を触らない :拾う側は「足して」と言うだけ、UIは「使って」と言うだけ。全部
InventoryManager経由なので、どこを直せばいいかが常に1箇所に定まります。 - 増やすのはデータとシグナルの購読だけ :効果音や回復を足したいなら
item_usedを聞くノードを1つ増やすだけ。既存のコードには手を入れません。
よくある間違いとベストプラクティス
| よくある間違い | ベストプラクティス |
|---|---|
get_node("../Player").add_item() のようにロジックとUIを密結合にする | シグナルとシングルトン で疎結合に。UIは通知を受け、ロジックはどこからでも呼べる |
| アイテムデータをスクリプトに直書きする | .tres の Resource に分離し、データ駆動にする |
| 毎フレームUIを再構築する | inventory_changed の 発火時だけ 更新する(大規模な らプーリングも) |
| プレイヤー・UIがそれぞれ別のインベントリ情報を持つ | 単一の情報源 :InventoryManager だけが状態を持ち、他は参照するだけ |
おまけ:先に知っておくと良いこと
- セーブ&ロード :
_itemsを「アイテムID+個数」のDictionaryに落としてJSONやConfigFileで保存し、ロード時に再構築します。 - UIは作り直しより使い回し :毎回スロットを全消し・全生成すると、数が多いとき重くなります。スロットを使い回す(オブジェクトプール)と軽くなります。
- 見た目はテーマで :スロットやアイテム枠の見た目は テーマ でまとめると統一感が出ます。
- アイテムの効果はデータに :回復量や効果を
ItemResourceに持たせ、item_usedを聞いた側が実行すると、アイテム追加がデータ作成だけで完結します。
まとめ
- 3層に分ける :データ(
Resource)・ロジック(シングルトン)・UI(Control)を分離する - データ層 :アイテムを
.tresとして定義。増やすのはファイル作成だけ - ロジック層 :
InventoryManagerが唯一の状態を持ち、変更のたびにinventory_changedを出す - ビュー層 :シグナルを聞いて描き直すだけ。使用は
use_item()とお願いするだけ
この3原則を守れば、アイテムの種類が増えても、UIのデザインが変わっても、核のロジックは安定して使い回せます。まずは ItemResource を1つ作って、add_item → UI表示 → use_item の往復を通すところから始めましょう。UIの土台は レイアウトコンテナ と テーマ で整えると、一気に本格的になります。