| 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 物品资源
# 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 格子数据
# 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 背包组件
# 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
# 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 背包主面板
# 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()
使用示例
物品数据库
# 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
玩家装备背包
# 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)
最佳实践
- 数据与显示分离 - InventorySlot只存储数据,不负责显示
- 使用Resource - InventoryItem作为Resource便于序列化
- 信号解耦 - UI通过信号监听数据变化,不直接查询数据
- 堆叠逻辑 - 添加物品时优先堆叠,节省格子空间
- 保存/加载 - 使用get_save_data()和load_save_data()实现持久化
扩展功能
- 物品类型过滤 - 不同背包类型接受不同物品
- 商人系统 - 买卖物品基于value
- 装备栏 - 特殊格子只能放特定类型装备
- 合成系统 - 物品组合生成新物品