【Godot】インベントリシステム設計の基礎:Resourceとシグナルで実現するアイテム管理

作成: 2025-12-08最終更新: 2026-07-08

GodotでResourceとシグナルを使い、データ・ロジック・UIを分離した拡張性の高いインベントリシステムを設計する方法を、3層アーキテクチャと実践フローで解説します。

アイテムを拾う・使う・捨てる処理がプレイヤーのスクリプトに散らばり、装備品やスタック不可アイテムを足すたびにあちこち直す——インベントリを行き当たりばったりで作ると、あっという間にスパゲッティコードになります。

この記事では、 データ(アイテムの定義)・ロジック(管理)・UI(表示)を3つの層に分ける インベントリ設計を作ります。Resource・シングルトン・シグナルを組み合わせ、アイテムの種類が増えてもUIを変えても、核となるロジックは安定して使い回せる形を目指します。

ゲームのインベントリ画面のイメージ。格子状のスロットに、ポーション・剣・鍵・宝石などのアイテムアイコンが並んでいる

この記事でわかること

  • データ・ロジック・UIを分ける 3層アーキテクチャ
  • Resource でアイテムを .tresファイル として定義する(データ駆動)
  • シングルトンの InventoryManager で状態を 1箇所に集める
  • inventory_changed シグナルで UIを自動更新 する

Sponsored

3層に分けて設計する

堅牢なシステムの鍵は 「関心の分離」 です。インベントリを、次の3つの層に分けて作ります。

インベントリの3層アーキテクチャ図。データ層(Resource)、ロジック層(Singleton)、ビュー層(UI)がシグナルでつながる
役割Godotでの実装
データ層「アイテムとは何か」を定義するResource.tres ファイル)
ロジック層追加・削除・使用などの状態を管理するシングルトン Node(Autoload)
ビュー層(UI)状態をプレイヤーに見せるControl ノード群

こう分けると、各層は他の層の中身を知らずに、自分の仕事に集中できます。UIを作り替えてもロジックは無傷、アイテムを増やしてもUIコードは触らずに済みます。

データ層:Resourceでアイテムを定義する

アイテムの設計図を Resource で作ります。Resource はエディタで直接編集・保存できるので、剣やポーションを .tres ファイルとして どんどん量産できます(プログラマー以外でも追加できます)。データ駆動の考え方は カスタムリソースの作成と活用 でも詳しく扱っています。

ItemResourceの設計図から、剣・ポーション・鍵の具体的なアイテムが.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を別管理する必要がありません。

Sponsored

ロジック層:シングルトンで状態を管理する

インベントリの状態は、Autoload のシングルトン InventoryManager1箇所だけ 持たせます。プレイヤーも宝箱も敵も、みんなここを通してアイテムを出し入れします(Single Source of Truth)。

InventoryManagerが中央でDictionaryの状態を持ち、プレイヤー・宝箱・敵からadd/use/removeされ、inventory_changedを出す図
# 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更新の合図です。

Sponsored

ビュー層:シグナルでUIを自動更新する

UIは 状態を映すだけ の存在にします。inventory_changed を受け取ったら描き直す、それだけ。配置は GridContainer が便利です。

inventory_changedシグナルを受けてUIがグリッドを描き直す図。UIは状態を映すだけ
# 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でもローグライクでもサバイバルでも、この往復は同じ形です。

ポーションを拾って使うまでの流れ。拾う(add_item)→inventory_changed→UIが増える→使う(use_item)→item_used→回復。全部InventoryManager経由

拾うアイテム(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()                       # 拾えたら消える

これだけで、あとは自動でつながります。

  1. 拾う → add_item()_items に足して inventory_changed を出す
  2. UIがそれを受け取り、グリッドを描き直す(ポーションが増える)
  3. スロットをクリック → use_item()item_used を出して個数を減らす
  4. 回復処理は item_used を聞いた別のシステム(プレイヤーのHP)が担当する

ポイントは2つです。

  • 誰も他人の中身を触らない :拾う側は「足して」と言うだけ、UIは「使って」と言うだけ。全部 InventoryManager 経由なので、どこを直せばいいかが常に1箇所に定まります。
  • 増やすのはデータとシグナルの購読だけ :効果音や回復を足したいなら item_used を聞くノードを1つ増やすだけ。既存のコードには手を入れません。
Sponsored

よくある間違いとベストプラクティス

よくある間違いベストプラクティス
get_node("../Player").add_item() のようにロジックとUIを密結合にするシグナルとシングルトン で疎結合に。UIは通知を受け、ロジックはどこからでも呼べる
アイテムデータをスクリプトに直書きする.tresResource に分離し、データ駆動にする
毎フレームUIを再構築するinventory_changed発火時だけ 更新する(大規模ならプーリングも)
プレイヤー・UIがそれぞれ別のインベントリ情報を持つ単一の情報源InventoryManager だけが状態を持ち、他は参照するだけ

おまけ:先に知っておくと良いこと

  • セーブ&ロード_items を「アイテムID+個数」の Dictionary に落として JSONConfigFile で保存し、ロード時に再構築します。
  • UIは作り直しより使い回し :毎回スロットを全消し・全生成すると、数が多いとき重くなります。スロットを使い回す(オブジェクトプール)と軽くなります。
  • 見た目はテーマで :スロットやアイテム枠の見た目は テーマ でまとめると統一感が出ます。
  • アイテムの効果はデータに :回復量や効果を ItemResource に持たせ、item_used を聞いた側が実行すると、アイテム追加がデータ作成だけで完結します。

まとめ

  • 3層に分ける :データ(Resource)・ロジック(シングルトン)・UI(Control)を分離する
  • データ層 :アイテムを .tres として定義。増やすのはファイル作成だけ
  • ロジック層InventoryManager が唯一の状態を持ち、変更のたびに inventory_changed を出す
  • ビュー層 :シグナルを聞いて描き直すだけ。使用は use_item() とお願いするだけ

この3原則を守れば、アイテムの種類が増えても、UIのデザインが変わっても、核のロジックは安定して使い回せます。まずは ItemResource を1つ作って、add_item → UI表示 → use_item の往復を通すところから始めましょう。UIの土台は レイアウトコンテナテーマ で整えると、一気に本格的になります。