---
name: godot-inventory-system
description: Godot 4背包系统完整实现，包括数据层、逻辑层和UI层设计。支持物品管理、拖放交互、格子满了处理等常见功能。
---

# Godot 4 背包系统

完整的背包系统实现，支持物品添加、移除、拖放排序、格子容量管理等功能。

## 何时使用此技能

- 实现游戏物品管理系统
- 需要拖放交互的背包UI
- 物品类型和数量管理
- 装备栏或商店系统

## 架构设计

### 分层架构

```
背包系统
├── 数据层 (Data Layer)
│   ├── InventoryItem - 单个物品数据
│   └── InventorySlot - 格子数据（包含物品引用和数量）
├── 逻辑层 (Business Layer)
│   └── InventoryComponent - CRUD操作、堆叠逻辑
└── UI层 (Presentation Layer)
    ├── InventoryUI - 背包主面板
    └── InventorySlotUI - 单个格子UI
```

### MVC分离

- **Model**: `InventoryItem`、`InventorySlot` - 纯数据
- **View**: `InventorySlotUI` - 仅负责显示
- **Controller**: `InventoryComponent` - 处理所有业务逻辑

## 数据层设计

### InventoryItem 物品资源

```gdscript
# inventory_item.gd
class_name InventoryItem
extends Resource

## 物品唯一标识符
@export var id: StringName

## 物品显示名称
@export var item_name: String

## 物品描述
@export_multiline var description: String

## 最大堆叠数量
@export var max_stack: int = 99

## 物品图标
@export var icon: Texture2D

## 物品 Prefab（可选，用于拖放到场景）
@export var item_scene: PackedScene

## 物品稀有度
@export_enum("Common", "Uncommon", "Rare", "Epic", "Legendary") var rarity: int = 0

## 是否可以丢弃
@export var can_drop: bool = true

## 是否可以堆叠
@export var can_stack: bool = true

## 物品价值（用于商店）
@export var value: int = 1
```

### InventorySlot 格子数据

```gdscript
# inventory_slot.gd
class_name InventorySlot
extends RefCounted

## 格子索引
var index: int

## 格子中的物品
var item: InventoryItem

## 物品数量
var quantity: int = 0

## 格子是否为空
var is_empty: bool:
    get: return item == null or quantity <= 0

func can_add(new_item: InventoryItem, amount: int = 1) -> bool:
    if is_empty:
        return true
    if item.id == new_item.id and item.can_stack:
        return quantity + amount <= item.max_stack
    return false

func add(new_item: InventoryItem, amount: int = 1) -> bool:
    if not can_add(new_item, amount):
        return false
    
    if is_empty:
        item = new_item
        quantity = amount
    else:
        quantity += amount
    
    return true

func remove(amount: int = 1) -> bool:
    if is_empty or quantity < amount:
        return false
    
    quantity -= amount
    if quantity <= 0:
        clear()
    return true

func clear() -> void:
    item = null
    quantity = 0
```

## 逻辑层设计

### InventoryComponent 背包组件

```gdscript
# inventory_component.gd
class_name InventoryComponent
extends Node

## 背包容量
@export var capacity: int = 20

## 信号：物品添加成功
signal item_added(slot_index: int, item: InventoryItem, quantity: int)

## 信号：物品移除成功
signal item_removed(slot_index: int, item: InventoryItem, quantity: int)

## 信号：背包满了
signal inventory_full(item: InventoryItem)

## 信号：格子数据变化
signal slot_changed(slot_index: int)

## 背包格子数组
var slots: Array[InventorySlot] = []

func _ready() -> void:
    _initialize_slots()

func _initialize_slots() -> void:
    for i in capacity:
        var slot := InventorySlot.new()
        slot.index = i
        slots.append(slot)

## 添加物品到背包
func add_item(item: InventoryItem, quantity: int = 1) -> bool:
    # 优先堆叠到已有物品
    var target_slot := _find_stackable_slot(item)
    if target_slot != null:
        var old_qty := target_slot.quantity
        target_slot.add(item, quantity)
        slot_changed.emit(target_slot.index)
        item_added.emit(target_slot.index, item, target_slot.quantity - old_qty)
        return true
    
    # 找空格子
    var empty_slot := _find_empty_slot()
    if empty_slot != null:
        empty_slot.add(item, quantity)
        slot_changed.emit(empty_slot.index)
        item_added.emit(empty_slot.index, item, quantity)
        return true
    
    # 背包满了
    inventory_full.emit(item)
    return false

## 移除物品
func remove_item(slot_index: int, quantity: int = 1) -> bool:
    if slot_index < 0 or slot_index >= slots.size():
        return false
    
    var slot: InventorySlot = slots[slot_index]
    if slot.is_empty:
        return false
    
    var item := slot.item
    if slot.remove(quantity):
        slot_changed.emit(slot_index)
        item_removed.emit(slot_index, item, quantity)
        return true
    
    return false

## 交换两个格子的物品
func swap_slots(index1: int, index2: int) -> void:
    if index1 < 0 or index1 >= slots.size() or index2 < 0 or index2 >= slots.size():
        return
    
    var temp_item := slots[index1].item
    var temp_qty := slots[index1].quantity
    
    slots[index1].item = slots[index2].item
    slots[index1].quantity = slots[index2].quantity
    
    slots[index2].item = temp_item
    slots[index2].quantity = temp_qty
    
    slot_changed.emit(index1)
    slot_changed.emit(index2)

## 移动物品到另一个背包
func move_to(target: InventoryComponent, slot_index: int, quantity: int = -1) -> bool:
    if slot_index < 0 or slot_index >= slots.size():
        return false
    
    var slot: InventorySlot = slots[slot_index]
    if slot.is_empty:
        return false
    
    var transfer_qty := quantity if quantity > 0 else slot.quantity
    
    if target.add_item(slot.item, transfer_qty):
        remove_item(slot_index, transfer_qty)
        return true
    
    return false

## 查找可堆叠的格子
func _find_stackable_slot(item: InventoryItem) -> InventorySlot:
    if not item.can_stack:
        return null
    
    for slot in slots:
        if not slot.is_empty and slot.item.id == item.id:
            if slot.quantity + 1 <= slot.item.max_stack:
                return slot
    
    return null

## 查找空格子
func _find_empty_slot() -> InventorySlot:
    for slot in slots:
        if slot.is_empty:
            return slot
    return null

## 获取指定物品的数量
func get_item_count(item_id: StringName) -> int:
    var total := 0
    for slot in slots:
        if not slot.is_empty and slot.item.id == item_id:
            total += slot.quantity
    return total

## 清空背包
func clear() -> void:
    for slot in slots:
        slot.clear()
        slot_changed.emit(slot.index)

## 保存背包数据
func get_save_data() -> Dictionary:
    var data := []
    for slot in slots:
        if not slot.is_empty:
            data.append({
                "index": slot.index,
                "item_id": slot.item.id,
                "quantity": slot.quantity
            })
    return {"slots": data, "capacity": capacity}

## 加载背包数据
func load_save_data(data: Dictionary) -> void:
    clear()
    if data.has("slots"):
        for slot_data in data.slots:
            var index: int = slot_data.index
            var item_id: StringName = slot_data.item_id
            var quantity: int = slot_data.quantity
            
            # 查找物品数据（实际项目中从物品数据库获取）
            var item := _get_item_by_id(item_id)
            if item != null and index < slots.size():
                slots[index].item = item
                slots[index].quantity = quantity
                slot_changed.emit(index)
```

## UI系统设计

### InventorySlotUI 格子UI

```gdscript
# inventory_slot_ui.gd
class_name InventorySlotUI
extends PanelContainer

## 信号：物品被拖走
signal item_drag_started(slot_index: int)

## 信号：物品放置到格子
signal item_dropped(slot_index: int, from_slot: int)

## 信号：右键点击
signal right_clicked(slot_index: int)

@export var slot_index: int = 0

@onready var icon_texture: TextureRect = %IconTexture
@onready var quantity_label: Label = %QuantityLabel
@onready var item_name_label: Label = %ItemNameLabel

var current_item: InventoryItem
var current_quantity: int = 0
var _is_dragging: bool = false

func _ready() -> void:
    mouse_filter = Control.MOUSE_FILTER_STOP
    
    # 初始化为空白
    _clear_display()

func set_slot_data(slot: InventorySlot) -> void:
    if slot.is_empty:
        _clear_display()
        return
    
    current_item = slot.item
    current_quantity = slot.quantity
    
    _update_display()

func _update_display() -> void:
    if current_item == null:
        _clear_display()
        return
    
    icon_texture.texture = current_item.icon
    icon_texture.visible = true
    
    if current_quantity > 1:
        quantity_label.text = str(current_quantity)
        quantity_label.visible = true
    else:
        quantity_label.visible = false
    
    item_name_label.text = current_item.item_name
    item_name_label.visible = true

func _clear_display() -> void:
    current_item = null
    current_quantity = 0
    icon_texture.texture = null
    icon_texture.visible = false
    quantity_label.visible = false
    item_name_label.visible = false

func _on_mouse_entered() -> void:
    if current_item != null:
        # 显示物品提示
        pass

func _on_gui_input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_LEFT:
            if event.pressed:
                _on_left_click()
            else:
                _on_left_release()
        elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
            right_clicked.emit(slot_index)

func _on_left_click() -> void:
    if current_item != null:
        item_drag_started.emit(slot_index)

func _on_left_release() -> void:
    pass
```

### InventoryUI 背包主面板

```gdscript
# inventory_ui.gd
class_name InventoryUI
extends Control

@export var inventory_component: InventoryComponent
@export var slot_scene: PackedScene

@onready var grid_container: GridContainer = %GridContainer
@onready var title_label: Label = %TitleLabel

var slotuis: Array[InventorySlotUI] = []
var _dragged_slot_index: int = -1
var _drag_preview: Control

func _ready() -> void:
    assert(inventory_component != null, "InventoryComponent未设置")
    
    inventory_component.slot_changed.connect(_on_slot_changed)
    inventory_component.inventory_full.connect(_on_inventory_full)
    
    _create_slot_ui()
    _refresh_all_slots()

func _create_slot_ui() -> void:
    for i in inventory_component.capacity:
        var slot_ui: InventorySlotUI = slot_scene.instantiate()
        slot_ui.slot_index = i
        slot_ui.item_drag_started.connect(_on_item_drag_started)
        slot_ui.item_dropped.connect(_on_item_dropped)
        slot_ui.right_clicked.connect(_on_slot_right_clicked)
        grid_container.add_child(slot_ui)
        slotuis.append(slot_ui)

func _refresh_all_slots() -> void:
    for i in inventory_component.slots.size():
        slotuis[i].set_slot_data(inventory_component.slots[i])

func _on_slot_changed(slot_index: int) -> void:
    if slot_index < slotuis.size():
        slotuis[slot_index].set_slot_data(inventory_component.slots[slot_index])

func _on_inventory_full(item: InventoryItem) -> void:
    # 显示提示（例如："背包已满"）
    print("背包已满，无法添加 ", item.item_name)

func _on_item_drag_started(slot_index: int) -> void:
    _dragged_slot_index = slot_index

func _on_item_dropped(slot_index: int, from_slot: int) -> void:
    if from_slot != slot_index:
        inventory_component.swap_slots(from_slot, slot_index)

func _on_slot_right_clicked(slot_index: int) -> void:
    var slot: InventorySlot = inventory_component.slots[slot_index]
    if slot.is_empty:
        return
    
    # 丢弃物品或显示物品菜单
    if slot.item.can_drop:
        inventory_component.remove_item(slot_index, 1)

func _input(event: InputEvent) -> void:
    if event.is_action_pressed("toggle_inventory"):
        visible = not visible
        if visible:
            _refresh_all_slots()
```

## 使用示例

### 物品数据库

```gdscript
# item_database.gd (Autoload)
extends Node

@export var items: Array[InventoryItem]

func _ready() -> void:
    # 注册所有物品到全局数据库
    pass

func get_item(id: StringName) -> InventoryItem:
    for item in items:
        if item.id == id:
            return item
    return null

func has_item(id: StringName) -> bool:
    return get_item(id) != null
```

### 玩家装备背包

```gdscript
# player.gd
extends CharacterBody2D

@export var inventory: InventoryComponent

func _ready() -> void:
    # 给玩家一些初始物品
    var sword := item_database.get_item("sword")
    var potion := item_database.get_item("health_potion")
    
    if sword:
        inventory.add_item(sword, 1)
    if potion:
        inventory.add_item(potion, 5)
```

## 最佳实践

1. **数据与显示分离** - InventorySlot只存储数据，不负责显示
2. **使用Resource** - InventoryItem作为Resource便于序列化
3. **信号解耦** - UI通过信号监听数据变化，不直接查询数据
4. **堆叠逻辑** - 添加物品时优先堆叠，节省格子空间
5. **保存/加载** - 使用get_save_data()和load_save_data()实现持久化

## 扩展功能

- **物品类型过滤** - 不同背包类型接受不同物品
- **商人系统** - 买卖物品基于value
- **装备栏** - 特殊格子只能放特定类型装备
- **合成系统** - 物品组合生成新物品
