第十三部分:背包与物品系统:Item Resource、拾取物、药水、金币、掉落物
这一部分开始做 RPG 里非常关键的“奖励系统”。
前面我们已经有:
玩家能移动
玩家能交互
玩家能攻击
怪物能追击
怪物能死亡
玩家有血量
现在要接上:
打死怪物掉东西
玩家捡东西
金币增加
药水进入背包
使用药水回血
UI 显示物品数量
这会让战斗和探索开始有“收益感”。
1. 物品系统的核心目标
第一阶段不要做复杂装备、词条、品质、合成。
先做最小闭环:
1. 定义物品数据
2. 玩家可以拾取物品
3. 物品进入背包
4. 背包能记录数量
5. 药水可以使用
6. 使用药水后玩家回血
7. 金币可以增加
8. 怪物死亡后可以掉落物品
先让系统能跑。
后面再扩展:
装备
任务物品
材料
卡牌
品质
堆叠上限
商店
合成
仓库
快捷栏
2. 物品应该怎么表示
游戏里的“物品”通常分两层:
ItemData:物品数据
InventorySlot:背包里的某一格
比如:
小型治疗药水
它的数据是:
名字:小型治疗药水
图标:potion_small.png
类型:消耗品
描述:恢复 30 点生命
恢复量:30
是否可堆叠:是
最大堆叠:99
而背包里的某一格是:
物品:小型治疗药水
数量:5
所以不要把“物品定义”和“背包数量”混在一起。
ItemData:这个物品是什么
InventorySlot:我拥有几个这个物品
3. 为什么建议用 Resource 做 ItemData
Godot 里很适合用 Resource 保存物品数据。
官方文档说明,Resource 是 Godot 各类资源的基类,主要用作数据容器,可以保存到磁盘,也可以通过 load() 或 preload() 从路径加载;PackedScene、Texture、TileSet 等也都是资源体系的一部分。(Godot Engine documentation)
更关键的是,Godot 支持自定义 Resource。官方文档提到,自定义 Resource 可以继承 Resource,可以暴露属性到 Inspector,保存为 .tres 文件,并且 Inspector 可以直接编辑这些自定义属性。(Godot Engine documentation)
所以物品数据非常适合这样做:
ItemData.gd
↓
PotionSmall.tres
Coin.tres
SlimeGel.tres
IronSword.tres
前端类比:
type ItemData = {
id: string
name: string
icon: string
description: string
type: string
}
Godot 里则是:
class_name ItemData
extends Resource
4. ItemData.gd
先创建脚本:
res://scripts/items/item_data.gd
内容:
class_name ItemData
extends Resource
enum ItemType {
CONSUMABLE,
MATERIAL,
CURRENCY,
QUEST,
EQUIPMENT,
CARD
}
@export var id: String = ""
@export var display_name: String = ""
@export_multiline var description: String = ""
@export var icon: Texture2D
@export var item_type: ItemType = ItemType.MATERIAL
@export var stackable: bool = true
@export var max_stack: int = 99
解释一下:
id:
程序内部用的唯一标识,比如 small_potion
display_name:
玩家看到的名字,比如 小型治疗药水
description:
描述文本
icon:
物品图标
item_type:
物品类型
stackable:
能不能堆叠
max_stack:
最大堆叠数量
5. 为什么 id 很重要
不要只用显示名字当标识。
不推荐:
小型治疗药水
作为内部识别。
因为以后你可能会:
改名字
做多语言
繁体/简体切换
英文版
更推荐:
small_potion
slime_gel
gold_coin
iron_sword
显示名字可以改,id 不轻易改。
前端开发里这就像:
const itemId = 'small_potion'
const label = t('items.small_potion.name')
别拿 label 当 key。
6. 创建第一个物品资源
有了 ItemData.gd 后,可以在 FileSystem 里创建资源:
右键
New Resource
搜索 ItemData
创建 small_potion.tres
路径建议:
res://data/items/small_potion.tres
设置:
id = small_potion
display_name = 小型治疗药水
description = 恢复 30 点生命值。
item_type = CONSUMABLE
stackable = true
max_stack = 99
icon 先可以不填。
也可以临时用一个 16x16 药水图标。
7. 治疗药水需要额外属性
普通 ItemData 只有基础数据。
药水需要:
恢复多少血
你可以直接在 ItemData 里加:
@export var heal_amount: int = 0
简单粗暴,第一版可以。
但更干净的做法是创建子类:
ConsumableItemData
第一阶段为了少绕,直接加:
@export var heal_amount: int = 0
完整 ItemData.gd 先变成:
class_name ItemData
extends Resource
enum ItemType {
CONSUMABLE,
MATERIAL,
CURRENCY,
QUEST,
EQUIPMENT,
CARD
}
@export var id: String = ""
@export var display_name: String = ""
@export_multiline var description: String = ""
@export var icon: Texture2D
@export var item_type: ItemType = ItemType.MATERIAL
@export var stackable: bool = true
@export var max_stack: int = 99
@export var heal_amount: int = 0
@export var gold_value: int = 0
heal_amount 给药水用。
gold_value 给金币类物品用。
后面系统变复杂了,再拆子类。
8. InventorySlot 是什么
背包不是简单存 ItemData 数组。
因为你还要知道数量。
所以定义一个“格子”:
InventorySlot
├── item: ItemData
└── quantity: int
可以先不做成 Resource,直接用普通脚本类。
创建:
res://scripts/items/inventory_slot.gd
class_name InventorySlot
extends RefCounted
var item: ItemData
var quantity: int = 0
func _init(p_item: ItemData = null, p_quantity: int = 0):
item = p_item
quantity = p_quantity
这里用 RefCounted 是因为它只是一个普通数据对象,不需要出现在场景树里。
9. Inventory 背包数据
背包可以先做成 Player 的子节点:
Player
└── Inventory Node
也可以直接写在 Player.gd 里。
推荐稍微拆一下,做一个 Inventory.gd。
结构:
Player CharacterBody2D
├── AnimatedSprite2D
├── CollisionShape2D
├── Inventory Node
├── InteractArea
├── AttackArea
└── Hurtbox
Inventory 负责:
添加物品
移除物品
查找物品
使用物品
发信号通知 UI
10. Inventory.gd 基础版
创建:
res://scripts/items/inventory.gd
class_name Inventory
extends Node
signal inventory_changed
var slots: Array[InventorySlot] = []
func add_item(item: ItemData, quantity: int = 1) -> bool:
if item == null:
return false
if quantity <= 0:
return false
if item.stackable:
var existing_slot = find_slot_by_item_id(item.id)
if existing_slot != null:
var new_quantity = existing_slot.quantity + quantity
existing_slot.quantity = min(new_quantity, item.max_stack)
inventory_changed.emit()
return true
var slot = InventorySlot.new(item, quantity)
slots.append(slot)
inventory_changed.emit()
return true
func remove_item(item_id: String, quantity: int = 1) -> bool:
var slot = find_slot_by_item_id(item_id)
if slot == null:
return false
if slot.quantity < quantity:
return false
slot.quantity -= quantity
if slot.quantity <= 0:
slots.erase(slot)
inventory_changed.emit()
return true
func has_item(item_id: String, quantity: int = 1) -> bool:
var slot = find_slot_by_item_id(item_id)
if slot == null:
return false
return slot.quantity >= quantity
func find_slot_by_item_id(item_id: String) -> InventorySlot:
for slot in slots:
if slot.item != null and slot.item.id == item_id:
return slot
return null
这就是最基础的背包数据结构。
11. 这个背包现在有什么限制
它目前有几个限制:
没有固定格子数量
没有处理超过 max_stack 后新开一格
没有排序
没有分类
没有装备栏
没有存档
但第一版够用。
因为我们现在最重要的是:
能捡
能加数量
能用药水
UI 能显示
不要先追求一个“最终幻想级背包系统”。
先从小木箱开始,不要一上来造自动化仓储中心。
12. 处理超过 max_stack
刚才的版本有个问题:
已有 90 个药水
又捡 20 个
max_stack 是 99
结果多出来的 11 个被吃掉了
可以先简单改:
func add_item(item: ItemData, quantity: int = 1) -> bool:
if item == null:
return false
if quantity <= 0:
return false
var remaining := quantity
if item.stackable:
for slot in slots:
if slot.item != null and slot.item.id == item.id:
var space = item.max_stack - slot.quantity
if space > 0:
var add_amount = min(space, remaining)
slot.quantity += add_amount
remaining -= add_amount
if remaining <= 0:
inventory_changed.emit()
return true
while remaining > 0:
var add_amount = remaining
if item.stackable:
add_amount = min(item.max_stack, remaining)
else:
add_amount = 1
slots.append(InventorySlot.new(item, add_amount))
remaining -= add_amount
inventory_changed.emit()
return true
这个版本支持:
99 个一组
超过后自动新增一格
不可堆叠物品每个一格
更实用。
13. 拾取物 PickUpItem
现在要让地图上有一个能捡的东西。
结构:
PickupItem Area2D
├── Sprite2D
└── CollisionShape2D
它是 Area2D,因为玩家碰到它就可以捡。
官方文档里 Area2D 的 body_entered、body_exited 等信号就是用来检测 PhysicsBody2D 进入或离开区域的,很适合拾取物、触发区这种场景。(Godot Engine documentation)
14. PickupItem.gd
extends Area2D
@export var item: ItemData
@export var quantity: int = 1
@onready var sprite: Sprite2D = $Sprite2D
func _ready():
body_entered.connect(_on_body_entered)
update_visual()
func update_visual():
if item != null and item.icon != null:
sprite.texture = item.icon
func _on_body_entered(body):
if not body.is_in_group("player"):
return
if not body.has_method("pick_up_item"):
return
var success = body.pick_up_item(item, quantity)
if success:
queue_free()
这段逻辑是:
玩家进入拾取范围
↓
调用玩家 pick_up_item()
↓
如果成功加入背包
↓
拾取物消失
15. Player.gd 添加 pick_up_item()
Player 身上要能找到 Inventory。
结构:
Player
└── Inventory
Player.gd:
@onready var inventory: Inventory = $Inventory
添加方法:
func pick_up_item(item: ItemData, quantity: int = 1) -> bool:
if item == null:
return false
if inventory == null:
return false
var success = inventory.add_item(item, quantity)
if success:
print("获得:", item.display_name, " x", quantity)
return success
现在玩家碰到掉落物,就能加入背包。
16. 金币要不要进背包
金币有两种设计:
1. 金币作为普通物品进入背包
2. 金币作为一个单独数值 gold
第一阶段建议:
金币作为单独数值
原因:
金币通常不占背包格子
商店、奖励、消费都经常用
UI 显示也更直接
Player.gd:
signal gold_changed(gold: int)
var gold: int = 0
func add_gold(amount: int):
if amount <= 0:
return
gold += amount
gold_changed.emit(gold)
如果 PickupItem 是金币,可以特殊处理。
17. ItemData 里的 gold_value 怎么用
如果 item.item_type == CURRENCY,就把它转成金币。
Player.gd:
func pick_up_item(item: ItemData, quantity: int = 1) -> bool:
if item == null:
return false
if item.item_type == ItemData.ItemType.CURRENCY:
var amount = item.gold_value * quantity
add_gold(amount)
print("获得金币:", amount)
return true
if inventory == null:
return false
var success = inventory.add_item(item, quantity)
if success:
print("获得:", item.display_name, " x", quantity)
return success
这样:
金币不进背包
药水、材料进背包
比较符合常见 RPG 习惯。
18. Gold UI
在 PlayerHUD 里可以加:
GoldLabel Label
结构:
PlayerHUD Control
├── HPBar ProgressBar
├── HPLabel Label
└── GoldLabel Label
PlayerHUD.gd:
@onready var gold_label: Label = $GoldLabel
func setup(player: Node):
if player == null:
return
if player.has_signal("health_changed"):
player.health_changed.connect(_on_player_health_changed)
if player.has_signal("gold_changed"):
player.gold_changed.connect(_on_gold_changed)
if "current_hp" in player and "max_hp" in player:
_on_player_health_changed(player.current_hp, player.max_hp)
if "gold" in player:
_on_gold_changed(player.gold)
func _on_gold_changed(gold: int):
gold_label.text = "金币:%d" % gold
现在捡金币时 UI 会更新。
19. 使用药水
Inventory 负责保存物品。
Player 负责被治疗。
物品使用可以有两种写法:
1. Player.use_item(item_id)
2. Inventory.use_item(item_id, user)
第一阶段建议写在 Player 里,简单直观:
func use_item(item_id: String) -> bool:
var slot = inventory.find_slot_by_item_id(item_id)
if slot == null:
return false
var item = slot.item
if item.item_type == ItemData.ItemType.CONSUMABLE:
if item.heal_amount > 0:
heal(item.heal_amount)
inventory.remove_item(item_id, 1)
print("使用:", item.display_name)
return true
return false
以后如果消耗品种类变多,再把使用逻辑迁移到物品系统里。
20. 临时按键使用药水
InputMap 添加:
use_potion
绑定:
Q
Player.gd 的输入里:
func handle_normal_input(event):
if event.is_action_pressed("attack"):
change_state(PlayerState.ATTACK)
return
if event.is_action_pressed("interact"):
try_interact()
return
if event.is_action_pressed("use_potion"):
use_item("small_potion")
return
这样按 Q 使用小药水。
这不是最终背包 UI,但很适合测试。
21. 药水治疗的完整闭环
现在流程是:
地图上有 PickupItem
PickupItem.item = small_potion.tres
quantity = 3
玩家碰到 PickupItem
↓
Player.pick_up_item()
↓
Inventory.add_item()
↓
背包里 small_potion 数量 +3
玩家受伤
↓
按 Q
↓
Player.use_item("small_potion")
↓
heal(30)
↓
health_changed.emit()
↓
PlayerHUD 更新血条
↓
Inventory.remove_item("small_potion", 1)
这就是 RPG 物品系统的第一个完整闭环。
22. 背包 UI 第一版
先做一个非常简单的背包列表。
结构:
UI CanvasLayer
├── PlayerHUD
├── InventoryPanel Control
│ ├── Panel
│ └── ItemList VBoxContainer
InventoryPanel 默认隐藏:
visible = false
InputMap 添加:
open_inventory
绑定:
I
B
23. InventoryPanel.gd
先用 Label 列表显示物品。
extends Control
@export var inventory: Inventory
@onready var item_list: VBoxContainer = $Panel/ItemList
func _ready():
visible = false
if inventory != null:
inventory.inventory_changed.connect(refresh)
refresh()
func toggle():
visible = not visible
func refresh():
for child in item_list.get_children():
child.queue_free()
if inventory == null:
return
for slot in inventory.slots:
var label = Label.new()
label.text = "%s x%d" % [slot.item.display_name, slot.quantity]
item_list.add_child(label)
这很朴素,但足够测试。
先不要急着做格子图标 UI。
24. UI.gd 控制背包打开关闭
UI.gd:
@onready var inventory_panel: Control = $InventoryPanel
func _unhandled_input(event):
if event.is_action_pressed("open_inventory"):
inventory_panel.toggle()
get_viewport().set_input_as_handled()
或者放 Player 里也行。
不过背包是 UI,放 UI.gd 更合适。
25. InventoryPanel 怎么拿到 Inventory
UI.gd 里 setup:
@export var player: Node
@onready var inventory_panel: Control = $InventoryPanel
func _ready():
if player != null:
inventory_panel.inventory = player.inventory
inventory_panel.refresh()
但注意:如果 player.inventory 是 @onready,UI 的 _ready() 执行顺序可能早晚有影响。
更稳一点,Player 可以提供方法:
func get_inventory() -> Inventory:
return inventory
UI:
if player != null and player.has_method("get_inventory"):
inventory_panel.inventory = player.get_inventory()
inventory_panel.refresh()
Player.gd:
func get_inventory() -> Inventory:
return inventory
26. 背包打开时要不要暂停玩家
第一版可以不暂停。
但一般 RPG 打开背包时,玩家应该不能移动和攻击。
你可以让 Player 进入一个状态:
MENU
或者设置游戏状态。
现在简单点:
if inventory_panel.visible:
player.lock_control_for_menu()
else:
player.unlock_control_from_menu()
但你目前 Player 状态机还没定义 MENU,可以先不做。
第一版允许边走边看背包也没关系。
等系统稳定,再加:
GameState.EXPLORING
GameState.INVENTORY
GameState.DIALOGUE
GameState.PAUSED
27. 怪物死亡掉落物
现在怪物死了应该掉东西。
有两种掉落:
金币
物品
先做最简单:
Slime 死亡后掉 1 个 slime_gel
可能掉一些金币
你需要一个掉落物场景:
PickupItem.tscn
前面已经做了:
PickupItem Area2D
├── Sprite2D
└── CollisionShape2D
Enemy.gd 里导出:
@export var pickup_scene: PackedScene
@export var drop_item: ItemData
@export var drop_quantity: int = 1
@export var drop_chance: float = 1.0
PackedScene 是 Godot 表示已保存场景的资源,官方文档说明它可以用于加载一个场景文件,然后通过实例化创建节点树。(Godot Engine documentation)
28. Enemy 掉落代码
func drop_loot():
if pickup_scene == null:
return
if drop_item == null:
return
if randf() > drop_chance:
return
var pickup = pickup_scene.instantiate()
get_parent().add_child(pickup)
pickup.global_position = global_position
pickup.item = drop_item
pickup.quantity = drop_quantity
死亡时调用:
func enter_state(state: EnemyState):
match state:
EnemyState.DEAD:
drop_loot()
queue_free()
如果你要播放死亡动画,就在真正 queue_free() 前掉落也可以。
比如:
func _on_animation_finished():
if current_state == EnemyState.DEAD:
drop_loot()
queue_free()
注意不要掉落两次。
29. 掉落金币
可以创建一个 gold_coin.tres:
id = gold_coin
display_name = 金币
item_type = CURRENCY
gold_value = 1
stackable = true
max_stack = 999
如果怪物掉 5 金币:
drop_item = gold_coin.tres
drop_quantity = 5
PickupItem 被玩家捡到后,Player 根据 ItemType.CURRENCY 调用 add_gold()。
30. 多种掉落物
以后怪物会有多个掉落:
金币 100%
史莱姆凝胶 60%
小药水 10%
稀有卡牌 1%
这时可以做 DropTable。
第一阶段不急。
可以先让每只怪只掉一种物品。
等跑通后,再做:
DropEntry Resource
DropTable Resource
31. DropEntry 未来设计
以后可以这样:
class_name DropEntry
extends Resource
@export var item: ItemData
@export var min_quantity: int = 1
@export var max_quantity: int = 1
@export_range(0.0, 1.0) var chance: float = 1.0
DropTable:
class_name DropTable
extends Resource
@export var entries: Array[DropEntry]
Enemy:
@export var drop_table: DropTable
但这是后面优化。
现在一只怪一个掉落就够。
32. 物品拾取方式:碰到就捡,还是按 E 捡
两种都可以。
碰到就捡:
金币
材料
药水
小掉落物
按 E 捡:
宝箱物品
重要任务物品
地上的关键道具
第一阶段建议:
普通掉落物碰到就捡
宝箱按 E 打开
这样手感比较顺。
33. 宝箱给物品
前面 Chest 只是 print。
现在可以真的给物品。
Chest.gd:
extends Area2D
@export var prompt_text: String = "按 E 打开"
@export var item: ItemData
@export var quantity: int = 1
var opened: bool = false
func interact(player: Node):
if opened:
print("宝箱已经空了")
return
opened = true
if player.has_method("pick_up_item"):
player.pick_up_item(item, quantity)
print("打开宝箱")
以后可以把宝箱动画、打开后的 Sprite、音效加进去。
34. 背包 UI 点击使用物品
第一版用 Q 测试药水就够。
如果想在 InventoryPanel 里点击使用,可以用 Button。
refresh() 里:
func refresh():
for child in item_list.get_children():
child.queue_free()
if inventory == null:
return
for slot in inventory.slots:
var button = Button.new()
button.text = "%s x%d" % [slot.item.display_name, slot.quantity]
button.pressed.connect(func():
_on_item_button_pressed(slot.item.id)
)
item_list.add_child(button)
然后:
@export var player: Node
func _on_item_button_pressed(item_id: String):
if player != null and player.has_method("use_item"):
player.use_item(item_id)
这样点击药水按钮就能使用。
但注意:如果 refresh 后 slot 被删,闭包引用 slot 可能有细节问题。
更稳一点是先保存 id:
var item_id = slot.item.id
button.pressed.connect(func():
_on_item_button_pressed(item_id)
)
35. InventoryPanel.gd 点击使用版
extends Control
@export var player: Node
@export var inventory: Inventory
@onready var item_list: VBoxContainer = $Panel/ItemList
func _ready():
visible = false
if inventory != null:
inventory.inventory_changed.connect(refresh)
refresh()
func toggle():
visible = not visible
func refresh():
for child in item_list.get_children():
child.queue_free()
if inventory == null:
return
for slot in inventory.slots:
var button = Button.new()
button.text = "%s x%d" % [slot.item.display_name, slot.quantity]
var item_id = slot.item.id
button.pressed.connect(func():
_on_item_button_pressed(item_id)
)
item_list.add_child(button)
func _on_item_button_pressed(item_id: String):
if player != null and player.has_method("use_item"):
player.use_item(item_id)
现在背包已经能:
显示物品
点击使用药水
使用后刷新数量
因为 inventory.remove_item() 会 emit inventory_changed。
36. 使用物品时要注意死亡状态
Player.use_item() 里应该判断:
if current_state == PlayerState.DEAD:
return false
否则死亡后还能喝药,可能会出现很奇怪的逻辑。
func use_item(item_id: String) -> bool:
if current_state == PlayerState.DEAD:
return false
var slot = inventory.find_slot_by_item_id(item_id)
if slot == null:
return false
var item = slot.item
if item.item_type == ItemData.ItemType.CONSUMABLE:
if item.heal_amount > 0:
heal(item.heal_amount)
inventory.remove_item(item_id, 1)
print("使用:", item.display_name)
return true
return false
37. 满血时要不要允许用药水
一般不允许。
if item.heal_amount > 0:
if current_hp >= max_hp:
print("生命值已满")
return false
heal(item.heal_amount)
inventory.remove_item(item_id, 1)
return true
这样不会浪费药水。
完整:
func use_item(item_id: String) -> bool:
if current_state == PlayerState.DEAD:
return false
var slot = inventory.find_slot_by_item_id(item_id)
if slot == null:
return false
var item = slot.item
if item.item_type == ItemData.ItemType.CONSUMABLE:
if item.heal_amount > 0:
if current_hp >= max_hp:
print("生命值已满")
return false
heal(item.heal_amount)
inventory.remove_item(item_id, 1)
print("使用:", item.display_name)
return true
return false
38. 物品图标 UI 后面怎么做
现在背包只是文字按钮。
以后可以做成格子:
InventoryGrid GridContainer
├── ItemSlotUI
├── ItemSlotUI
├── ItemSlotUI
每个 ItemSlotUI:
Panel
├── Icon TextureRect
└── QuantityLabel Label
TextureRect 是 Godot UI 里显示贴图的 Control,常用于 UI 图标、图片显示。
第一版先用文字,第二版再美化。
39. 存档问题先不要急
背包最终一定要存档。
但现在不要急着做。
以后存档时不要直接存 Resource 对象。
更常见的是存:
{
"gold": 120,
"inventory": [
{
"id": "small_potion",
"quantity": 5
},
{
"id": "slime_gel",
"quantity": 12
}
]
}
读档时根据 id 找回 ItemData。
所以 id 一开始设计好很重要。
40. ItemDatabase 以后会用到
当你读档时,只有:
small_potion
你需要找到对应的:
small_potion.tres
以后可以做:
ItemDatabase
比如:
class_name ItemDatabase
extends Node
var items: Dictionary = {}
func register_item(item: ItemData):
items[item.id] = item
func get_item(id: String) -> ItemData:
return items.get(id)
或者用一个 Resource 保存所有物品列表。
第一阶段不需要。
现在手动拖资源就够。
41. 当前推荐节点结构
现在项目可以变成:
Main Node
├── World Node2D
│ ├── Ground TileMapLayer
│ ├── Obstacles TileMapLayer
│ ├── SpawnPoints Node2D
│ │ └── PlayerSpawn Marker2D
│ ├── Drops Node2D
│ │ └── PickupItem Area2D
│ ├── Enemies Node2D
│ │ └── Slime CharacterBody2D
│ ├── Interactables Node2D
│ │ ├── Chest Area2D
│ │ ├── NPC Area2D
│ │ └── Door Area2D
│ ├── Player CharacterBody2D
│ │ ├── Inventory Node
│ │ ├── AnimatedSprite2D
│ │ ├── CollisionShape2D
│ │ ├── Camera2D
│ │ ├── InteractArea Area2D
│ │ ├── AttackArea Area2D
│ │ └── Hurtbox Area2D
│ └── Foreground TileMapLayer
└── UI CanvasLayer
├── PlayerHUD Control
│ ├── HPBar ProgressBar
│ ├── HPLabel Label
│ └── GoldLabel Label
├── InventoryPanel Control
│ └── Panel
│ └── ItemList VBoxContainer
├── InteractLabel Label
├── DialogueBox Control
└── DeathPanel Control
这已经很完整了。
42. 常见错误 1:创建不了 ItemData Resource
检查:
ItemData.gd 第一行有没有 class_name ItemData
脚本是否 extends Resource
脚本是否没有语法错误
New Resource 搜索 ItemData
官方文档也提到,要让自定义资源类出现在创建资源的界面里,GDScript 需要提供 class name。(Godot Engine documentation)
所以:
class_name ItemData
extends Resource
这两行很关键。
43. 常见错误 2:拾取物碰到了没反应
排查:
PickupItem 是不是 Area2D
有没有 CollisionShape2D
CollisionShape2D 有没有 Shape
body_entered 信号是否连接
Player 是否加入 player 组
Player 是否有 pick_up_item 方法
PickupItem.item 是否拖了 ItemData
临时打印:
func _on_body_entered(body):
print("Pickup touched by: ", body.name)
如果不打印,是碰撞检测问题。
如果打印了但没拾取,是 group 或方法问题。
44. 常见错误 3:背包 UI 不刷新
检查:
Inventory.add_item 是否 emit inventory_changed
InventoryPanel 是否连接 inventory_changed
InventoryPanel.inventory 是否设置
refresh() 路径是否正确
ItemList 是否是 VBoxContainer
临时打印:
func refresh():
print("刷新背包")
45. 常见错误 4:使用药水没回血
排查:
small_potion.tres 的 item_type 是否是 CONSUMABLE
heal_amount 是否大于 0
Player.use_item 是否拿到 slot
Player.heal 是否 emit health_changed
玩家是否已经满血
如果满血时你禁止使用,那按了没效果是正常的。
46. 常见错误 5:金币进了背包而不是 GoldLabel
检查:
gold_coin.tres 的 item_type 是否是 CURRENCY
gold_value 是否大于 0
Player.pick_up_item 是否特殊处理 CURRENCY
GoldLabel 是否连接 gold_changed
47. 常见错误 6:怪物死亡不掉落
排查:
Enemy 是否设置 pickup_scene
Enemy 是否设置 drop_item
drop_chance 是否太低
drop_loot 是否被调用
pickup_scene.instantiate() 是否成功
掉落物是否 add_child 到正确父节点
可以临时:
func drop_loot():
print("尝试掉落")
48. 常见错误 7:掉落物生成了但看不到
检查:
PickupItem 的 Sprite2D 是否有 texture
ItemData.icon 是否设置
PickupItem.global_position 是否正确
PickupItem 是否被生成到 World 下面
Z Index 是否被地图盖住
如果被地图盖住,可以调:
PickupItem 的 z_index
或者放在 Objects/Items 层上方
49. 这一部分最重要的记忆点
ItemData 负责定义物品是什么
InventorySlot 负责记录某个物品有多少
Inventory 负责添加、移除、查找物品
Resource 很适合做物品数据
金币建议第一版作为 Player 的独立数值
药水作为 CONSUMABLE 进入背包
PickupItem 用 Area2D 检测玩家拾取
怪物死亡可以实例化 PickupItem 掉落
背包 UI 先用文字列表,不急着做格子
物品 id 很重要,后面存档和多语言都靠它
50. 你现在的小练习
建议你按这个顺序做:
1. 创建 ItemData.gd
2. 创建 small_potion.tres
3. 创建 gold_coin.tres
4. 创建 slime_gel.tres
5. 创建 InventorySlot.gd
6. 创建 Inventory.gd
7. Player 下添加 Inventory 节点
8. Player 添加 pick_up_item()
9. 创建 PickupItem.tscn
10. 地图上放一个 small_potion 拾取物
11. 玩家碰到后加入背包
12. UI 添加 InventoryPanel
13. 按 I 打开背包
14. 背包显示 small_potion x数量
15. 按 Q 使用 small_potion 回血
16. GoldLabel 显示金币
17. 地图上放一个 gold_coin 拾取物
18. 玩家捡到后金币增加
19. Slime 死亡后掉落 slime_gel
20. 玩家捡起 slime_gel 后背包显示数量
完成这部分之后,你的 RPG 就有了“战斗 → 掉落 → 拾取 → 背包 → 使用”的完整奖励闭环。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录