第十六部分:装备系统基础:武器、防具、属性加成、装备栏、攻击力计算

2026-05-19
339912 分钟
...

这一部分开始做 RPG 里非常重要的“角色成长系统”。

前面我们已经有了:

背包
金币
商店
任务奖励
怪物掉落
玩家攻击
玩家血量

现在装备系统要接上这些内容:

玩家获得武器

武器进入背包

玩家装备武器

攻击力增加

打怪伤害提高

玩家获得防具

装备防具

最大血量或防御增加

受到伤害减少

装备系统会让玩家从“捡东西”进一步变成“角色变强”。

1. 装备系统第一阶段要做什么

第一版先做这些:

1. 定义装备数据
2. 武器能增加攻击力
3. 防具能增加最大血量或防御
4. 玩家有装备栏
5. 背包中的装备可以被穿上
6. 穿上装备后属性刷新
7. 攻击伤害使用最终攻击力
8. 受到伤害时使用防御减伤

先不要做:

装备强化
随机词条
品质颜色
套装效果
耐久度
宝石镶嵌
双手武器
职业限制
等级限制
比较面板
拖拽换装

这些都后面再说。

第一版核心就是:

装备能穿
穿了能改变属性

2. 装备和普通物品的关系

装备本质上也是一种物品。

所以我们之前的 ItemData 里已经预留了:

enum ItemType {
    CONSUMABLE,
    MATERIAL,
    CURRENCY,
    QUEST,
    EQUIPMENT,
    CARD
}

装备就是:

item_type = EQUIPMENT

不过装备还需要额外属性:

装备部位
攻击力
防御力
最大生命加成

所以有两种做法:

做法 1
直接在 ItemData 里加装备字段

做法 2
创建 EquipmentData 继承 ItemData

第一版为了简单,可以直接在 ItemData 里加字段。

但从结构上说,EquipmentData 会更干净。

因为 Godot 支持自定义 Resource,并且 Resource 适合作为数据容器保存到磁盘,也能在 Inspector 里编辑导出的属性;之前物品、任务、商店数据都用 Resource,是同一个思路。(Godot Engine documentation)

这一部分我建议你用:

EquipmentData 继承 ItemData

这样更像正式项目。

3. EquipmentData 是什么

EquipmentData 是装备物品的数据。

比如一把木剑:

id = wooden_sword
display_name = 木剑
item_type = EQUIPMENT
slot_type = WEAPON
attack_bonus = 5
defense_bonus = 0
max_hp_bonus = 0

比如一件皮甲:

id = leather_armor
display_name = 皮甲
item_type = EQUIPMENT
slot_type = ARMOR
attack_bonus = 0
defense_bonus = 2
max_hp_bonus = 20

也就是说:

ItemData 负责所有物品的通用字段
EquipmentData 负责装备特有字段

4. EquipmentData.gd

创建:

res://scripts/items/equipment_data.gd
class_name EquipmentData
extends ItemData

enum SlotType {
    WEAPON,
    ARMOR,
    ACCESSORY
}

@export var slot_type: SlotType = SlotType.WEAPON

@export var attack_bonus: int = 0
@export var defense_bonus: int = 0
@export var max_hp_bonus: int = 0

因为它继承 ItemData,所以它也拥有:

id
display_name
description
icon
item_type
stackable
max_stack
sell_price

创建装备资源时,要记得:

item_type = EQUIPMENT
stackable = false
max_stack = 1

装备一般不堆叠。

5. 创建第一把武器

创建资源:

res://data/items/equipment/wooden_sword.tres

类型选择:

EquipmentData

设置:

id = wooden_sword
display_name = 木剑
description = 一把普通木剑攻击力 +5
item_type = EQUIPMENT
stackable = false
max_stack = 1
sell_price = 5

slot_type = WEAPON
attack_bonus = 5
defense_bonus = 0
max_hp_bonus = 0

6. 创建第一件防具

创建:

res://data/items/equipment/leather_armor.tres

设置:

id = leather_armor
display_name = 皮甲
description = 简单的皮革护甲防御 +2最大生命 +20
item_type = EQUIPMENT
stackable = false
max_stack = 1
sell_price = 8

slot_type = ARMOR
attack_bonus = 0
defense_bonus = 2
max_hp_bonus = 20

7. 玩家基础属性和最终属性

玩家属性应该分两层:

基础属性
装备加成
最终属性

比如:

基础攻击力10
武器攻击力5
最终攻击力15

比如:

基础防御1
防具防御2
最终防御3

所以 Player 里可以写:

@export var base_attack: int = 10
@export var base_defense: int = 0
@export var base_max_hp: int = 100

var final_attack: int = 10
var final_defense: int = 0
var max_hp: int = 100

不过你前面已经有:

@export var max_hp: int = 100
var current_hp: int

现在我们要稍微调整:

base_max_hp基础最大生命
max_hp最终最大生命

8. Player 属性推荐写法

Player.gd:

signal stats_changed

@export var base_attack: int = 10
@export var base_defense: int = 0
@export var base_max_hp: int = 100

var final_attack: int = 10
var final_defense: int = 0
var max_hp: int = 100
var current_hp: int = 100

初始化时:

func _ready():
    recalculate_stats()
    current_hp = max_hp
    health_changed.emit(current_hp, max_hp)

如果你原来 _ready() 里已经有很多代码,比如信号连接、AttackArea 初始化、InteractArea 初始化,不要新建第二个 _ready()

要把这些内容合并进去。

9. EquipmentSlots 装备栏

玩家需要知道当前穿了什么。

可以先用 Dictionary:

var equipped_items: Dictionary = {
    EquipmentData.SlotType.WEAPON: null,
    EquipmentData.SlotType.ARMOR: null,
    EquipmentData.SlotType.ACCESSORY: null,
}

含义:

WEAPON 当前装备了什么
ARMOR 当前装备了什么
ACCESSORY 当前装备了什么

更清晰也可以写变量:

var equipped_weapon: EquipmentData = null
var equipped_armor: EquipmentData = null
var equipped_accessory: EquipmentData = null

第一版我更建议用 Dictionary。

因为装备栏以后会变多,Dictionary 更方便遍历。

10. Player 添加装备信号

signal equipment_changed

每次装备改变:

equipment_changed.emit()
stats_changed.emit()

UI 可以监听这些信号来刷新装备面板。

Godot 的 signal 本来就是节点或对象在某件事发生时通知其他对象的一种机制,能减少对象之间直接硬引用,这在 UI 根据玩家状态变化刷新时特别好用。(Godot Engine documentation)

11. recalculate_stats()

这个函数根据基础属性和装备加成计算最终属性。

func recalculate_stats():
    final_attack = base_attack
    final_defense = base_defense
    max_hp = base_max_hp

    for item in equipped_items.values():
        if item == null:
            continue

        final_attack += item.attack_bonus
        final_defense += item.defense_bonus
        max_hp += item.max_hp_bonus

    current_hp = min(current_hp, max_hp)

    stats_changed.emit()
    health_changed.emit(current_hp, max_hp)

重点:

先从基础属性开始
遍历所有装备
把装备加成加上去
如果最大血量变低当前血量不能超过新的 max_hp
通知 UI 刷新

比如你脱掉皮甲:

max_hp 120 变回 100
current_hp 如果原来是 115
就应该变成 100

所以要:

current_hp = min(current_hp, max_hp)

12. equip_item()

装备一个物品。

func equip_item(item: ItemData) -> bool:
    if item == null:
        return false

    if item.item_type != ItemData.ItemType.EQUIPMENT:
        return false

    if not item is EquipmentData:
        return false

    var equipment := item as EquipmentData
    equipped_items[equipment.slot_type] = equipment

    recalculate_stats()
    equipment_changed.emit()

    print("装备:", equipment.display_name)
    return true

这里的关键判断:

if not item is EquipmentData:
    return false

因为有些普通 ItemData 也可能误设置成 EQUIPMENT,这个判断能防一手。

13. unequip_item()

卸下某个部位的装备。

func unequip_item(slot_type: EquipmentData.SlotType) -> bool:
    if not equipped_items.has(slot_type):
        return false

    var old_item = equipped_items[slot_type]

    if old_item == null:
        return false

    equipped_items[slot_type] = null

    recalculate_stats()
    equipment_changed.emit()

    print("卸下:", old_item.display_name)
    return true

第一版先不考虑:

卸下后是否回到背包
背包满了怎么办

因为我们现在的背包没有容量限制。

实际上,因为装备本来还在背包里,所以卸下只是清空装备栏,不需要额外加回背包。

这是第一版最简单的做法。

14. 装备是否应该从背包里移除

有两种设计。

设计 A:

装备后物品仍然在背包里
装备栏只是引用它

优点:

简单
不用处理卸下放回背包

缺点:

背包里还能看到已经穿上的装备
可能会重复出售或重复装备

设计 B:

装备后从背包移除
卸下后放回背包

优点:

更符合很多 RPG 的逻辑

缺点:

需要处理背包满替换装备退回旧装备

第一版建议用设计 A,但 UI 上标记“已装备”。

等背包格子系统更完善后,再改设计 B。

15. get_equipped_item()

func get_equipped_item(slot_type: EquipmentData.SlotType) -> EquipmentData:
    return equipped_items.get(slot_type)

UI 显示装备栏时会用。

16. 攻击伤害使用 final_attack

前面 Player 攻击怪物时可能写了:

@export var attack_damage: int = 10

现在改成:

func get_attack_damage() -> int:
    return final_attack

命中时:

area.take_damage(get_attack_damage())

原来:

area.take_damage(attack_damage)

改成:

area.take_damage(get_attack_damage())

这样装备武器后,攻击伤害会提高。

17. 防御如何减伤

玩家受到怪物伤害时:

func take_damage(amount: int, attacker: Node = null):

可以改成:

var final_damage = calculate_received_damage(amount)

减伤函数:

func calculate_received_damage(amount: int) -> int:
    return max(amount - final_defense, 1)

意思是:

最终伤害 = 原始伤害 - 防御力
但最低至少受到 1 点伤害

比如:

怪物伤害 10
玩家防御 3
实际伤害 7

如果防御 999:

实际伤害最低 1

这样避免防御高了完全无伤。

第一版用这个公式就够。

以后可以改成百分比减伤、护甲公式、元素抗性等。

18. take_damage 使用防御

func take_damage(amount: int, attacker: Node = null):
    if current_state == PlayerState.DEAD:
        return

    if is_invincible:
        return

    var final_damage = calculate_received_damage(amount)

    current_hp -= final_damage
    current_hp = clamp(current_hp, 0, max_hp)

    health_changed.emit(current_hp, max_hp)

    print("玩家受到伤害:", final_damage)

    if current_hp <= 0:
        change_state(PlayerState.DEAD)
        died.emit()
        return

    change_state(PlayerState.HURT)
    start_invincibility()

现在穿防具后,实际受伤会降低。

19. max_hp_bonus 的问题

如果装备皮甲:

max_hp +20

那么当前血量要不要也加 20?

有两种设计。

设计 A:

只提高最大生命
当前生命不变

比如:

100/100
装备皮甲
100/120

设计 B:

提高最大生命时也补上增加的部分

比如:

100/100
装备皮甲
120/120

第一版建议设计 A。

原因:

更简单
避免通过反复穿脱装备免费回血

如果你想装备时补血,也要小心玩家反复装备刷血的问题。

20. 背包 UI 中点击装备

前面 InventoryPanel 里点击物品是使用药水:

player.use_item(item_id)

现在如果点击的是装备,应该装备它。

Player.use_item() 可以这样改:

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

    if item.item_type == ItemData.ItemType.EQUIPMENT:
        return equip_item(item)

    return false

这样背包里点击药水是使用,点击装备是穿上。

21. InventoryPanel 显示装备信息

原来的按钮可能是:

木剑 x1

现在可以显示:

木剑 x1 [装备]

如果已经装备了:

木剑 x1 [已装备]

InventoryPanel.gd 里可以问 Player:

func is_equipped(item: ItemData) -> bool:
    if player == null:
        return false

    if not player.has_method("is_item_equipped"):
        return false

    return player.is_item_equipped(item)

Player.gd:

func is_item_equipped(item: ItemData) -> bool:
    if item == null:
        return false

    for equipped in equipped_items.values():
        if equipped == item:
            return true

    return false

然后 InventoryPanel 按钮文本:

var text = "%s x%d" % [slot.item.display_name, slot.quantity]

if slot.item.item_type == ItemData.ItemType.EQUIPMENT:
    if is_equipped(slot.item):
        text += " [已装备]"
    else:
        text += " [装备]"

22. 装备栏 UI

除了背包里显示,还可以做一个装备面板。

结构:

EquipmentPanel Control
└── Panel
    ├── WeaponLabel Label
    ├── ArmorLabel Label
    ├── AccessoryLabel Label
    ├── AttackLabel Label
    ├── DefenseLabel Label
    └── MaxHPLabel Label

显示:

武器木剑
防具皮甲
饰品

攻击15
防御2
生命120

第一版用文字就够。

23. EquipmentPanel.gd

extends Control

@export var player: Node

@onready var weapon_label: Label = $Panel/WeaponLabel
@onready var armor_label: Label = $Panel/ArmorLabel
@onready var accessory_label: Label = $Panel/AccessoryLabel

@onready var attack_label: Label = $Panel/AttackLabel
@onready var defense_label: Label = $Panel/DefenseLabel
@onready var max_hp_label: Label = $Panel/MaxHPLabel

func _ready():
    visible = false

    if player != null:
        if player.has_signal("equipment_changed"):
            player.equipment_changed.connect(refresh)

        if player.has_signal("stats_changed"):
            player.stats_changed.connect(refresh)

        refresh()

func toggle():
    visible = not visible
    refresh()

func refresh():
    if player == null:
        return

    weapon_label.text = "武器:" + get_equipment_name(EquipmentData.SlotType.WEAPON)
    armor_label.text = "防具:" + get_equipment_name(EquipmentData.SlotType.ARMOR)
    accessory_label.text = "饰品:" + get_equipment_name(EquipmentData.SlotType.ACCESSORY)

    attack_label.text = "攻击:%d" % player.final_attack
    defense_label.text = "防御:%d" % player.final_defense
    max_hp_label.text = "生命:%d / %d" % [player.current_hp, player.max_hp]

func get_equipment_name(slot_type: EquipmentData.SlotType) -> String:
    if not player.has_method("get_equipped_item"):
        return "无"

    var item = player.get_equipped_item(slot_type)

    if item == null:
        return "无"

    return item.display_name

24. 打开装备面板

InputMap 添加:

open_equipment

绑定:

C

UI.gd:

@onready var equipment_panel: Control = $EquipmentPanel

func _unhandled_input(event):
    if event.is_action_pressed("open_equipment"):
        equipment_panel.toggle()
        get_viewport().set_input_as_handled()

也可以在 InventoryPanel 里加一个切换按钮。

第一版按 C 打开最简单。

25. 卸下装备按钮

装备面板可以先不做卸下按钮。

如果要做,可以加:

UnequipWeaponButton
UnequipArmorButton
UnequipAccessoryButton

EquipmentPanel.gd:

@onready var unequip_weapon_button: Button = $Panel/UnequipWeaponButton
@onready var unequip_armor_button: Button = $Panel/UnequipArmorButton
@onready var unequip_accessory_button: Button = $Panel/UnequipAccessoryButton

func _ready():
    unequip_weapon_button.pressed.connect(func():
        unequip(EquipmentData.SlotType.WEAPON)
    )

    unequip_armor_button.pressed.connect(func():
        unequip(EquipmentData.SlotType.ARMOR)
    )

    unequip_accessory_button.pressed.connect(func():
        unequip(EquipmentData.SlotType.ACCESSORY)
    )
func unequip(slot_type: EquipmentData.SlotType):
    if player != null and player.has_method("unequip_item"):
        player.unequip_item(slot_type)
        refresh()

第一版可以只做武器和防具。

26. 装备替换

如果你已经穿了木剑,又点击铁剑:

木剑自动被替换
铁剑成为当前武器

因为我们的 equip_item() 是:

equipped_items[equipment.slot_type] = equipment

所以同部位会自动覆盖。

旧装备仍然在背包里。

第一版没问题。

27. 商店售卖装备

装备也是 ItemData。

所以商店可以直接卖装备。

创建:

wooden_sword_shop_item.tres

设置:

item = wooden_sword.tres
price = 20
infinite_stock = false
stock = 1

加入:

weapon_shop.tres

玩家购买后:

wooden_sword 进入背包
打开背包点击木剑
装备成功
攻击力增加

这说明装备系统已经和商店打通了。

28. 怪物掉落装备

怪物掉落逻辑也可以掉装备。

Enemy 里:

drop_item = wooden_sword.tres

但装备通常掉率低。

第一版你可以测试用 100% 掉落。

以后做 DropTable 时再控制概率。

29. 任务奖励装备

QuestData 的 reward_items 也可以放装备。

比如:

完成任务奖励木剑

因为 player.pick_up_item() 对装备和普通物品都能加背包。

装备不需要特殊处理。

这就是统一 ItemData 系统的好处。

30. 攻击力显示在 HUD 还是装备面板

第一版建议只放装备面板。

HUD 里显示:

血量
金币

装备面板显示:

攻击
防御
最大生命
当前装备

不要把 HUD 塞太满。

HUD 是开车时的仪表盘,不是公司年报。

31. 防御公式以后怎么优化

当前公式:

max(amount - final_defense, 1)

简单好懂。

但缺点:

防御太高会几乎无敌
小怪可能永远只打 1
数值成长容易失控

以后可以用百分比减伤:

var damage_multiplier = 100.0 / (100.0 + final_defense)
var final_damage = max(roundi(amount * damage_multiplier), 1)

比如:

防御 0受到 100%
防御 50受到约 66%
防御 100受到 50%

但第一版不用。

先用减法公式最直观。

32. 装备属性还能有哪些

以后可以加:

critical_chance暴击率
critical_damage暴击伤害
move_speed_bonus移动速度
attack_speed_bonus攻击速度
magic_power法术强度
element_type元素类型
resistance_fire火抗
resistance_ice冰抗

但不要现在就加。

第一版只留:

attack_bonus
defense_bonus
max_hp_bonus

够用了。

33. 装备和卡牌系统的关系

你之前提过想加入卡牌元素。

装备系统以后可以和卡牌联动:

武器决定可用攻击卡
饰品增加抽牌数量
防具给防御类卡牌加成
装备提供被动卡牌

比如:

火焰法杖
攻击 +3
解锁火球卡牌

骑士盾
防御 +5
战斗开始获得格挡卡

但这属于后面“卡牌系统”。

现在装备先作为数值加成。

34. Player.gd 装备相关完整片段

signal equipment_changed
signal stats_changed

@export var base_attack: int = 10
@export var base_defense: int = 0
@export var base_max_hp: int = 100

var final_attack: int = 10
var final_defense: int = 0
var max_hp: int = 100
var current_hp: int = 100

var equipped_items: Dictionary = {
    EquipmentData.SlotType.WEAPON: null,
    EquipmentData.SlotType.ARMOR: null,
    EquipmentData.SlotType.ACCESSORY: null,
}

func recalculate_stats():
    final_attack = base_attack
    final_defense = base_defense
    max_hp = base_max_hp

    for item in equipped_items.values():
        if item == null:
            continue

        final_attack += item.attack_bonus
        final_defense += item.defense_bonus
        max_hp += item.max_hp_bonus

    current_hp = min(current_hp, max_hp)

    stats_changed.emit()
    health_changed.emit(current_hp, max_hp)

func equip_item(item: ItemData) -> bool:
    if item == null:
        return false

    if item.item_type != ItemData.ItemType.EQUIPMENT:
        return false

    if not item is EquipmentData:
        return false

    var equipment := item as EquipmentData
    equipped_items[equipment.slot_type] = equipment

    recalculate_stats()
    equipment_changed.emit()

    print("装备:", equipment.display_name)
    return true

func unequip_item(slot_type: EquipmentData.SlotType) -> bool:
    if not equipped_items.has(slot_type):
        return false

    var old_item = equipped_items[slot_type]

    if old_item == null:
        return false

    equipped_items[slot_type] = null

    recalculate_stats()
    equipment_changed.emit()

    print("卸下:", old_item.display_name)
    return true

func get_equipped_item(slot_type: EquipmentData.SlotType) -> EquipmentData:
    return equipped_items.get(slot_type)

func is_item_equipped(item: ItemData) -> bool:
    if item == null:
        return false

    for equipped in equipped_items.values():
        if equipped == item:
            return true

    return false

func get_attack_damage() -> int:
    return final_attack

func calculate_received_damage(amount: int) -> int:
    return max(amount - final_defense, 1)

35. use_item() 装备版

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

    if item.item_type == ItemData.ItemType.EQUIPMENT:
        return equip_item(item)

    return false

36. 攻击命中改成 final_attack

func _on_attack_area_entered(area):
    if current_state != PlayerState.ATTACK:
        return

    if not area.is_in_group("hurtbox"):
        return

    if hit_targets.has(area):
        return

    hit_targets.append(area)

    if area.has_method("take_damage"):
        area.take_damage(get_attack_damage())

37. 受伤改成防御减伤

func take_damage(amount: int, attacker: Node = null):
    if current_state == PlayerState.DEAD:
        return

    if is_invincible:
        return

    var final_damage = calculate_received_damage(amount)

    current_hp -= final_damage
    current_hp = clamp(current_hp, 0, max_hp)

    health_changed.emit(current_hp, max_hp)

    print("玩家受到伤害:", final_damage)

    if current_hp <= 0:
        change_state(PlayerState.DEAD)
        died.emit()
        return

    change_state(PlayerState.HURT)
    start_invincibility()

38. 常见错误 1:创建不了 EquipmentData 资源

检查:

EquipmentData.gd 是否有 class_name EquipmentData
是否 extends ItemData
ItemData.gd 是否有 class_name ItemData
脚本是否有语法错误

如果 ItemData.gd 有报错,EquipmentData 也会创建不了。

39. 常见错误 2:装备点击后没变化

排查:

装备资源 item_type 是否设置为 EQUIPMENT
装备资源是否是 EquipmentData不是普通 ItemData
InventoryPanel 点击是否调用 player.use_item()
Player.use_item 是否处理 EQUIPMENT
Player.equip_item 是否成功

临时打印:

print("点击使用:", item.display_name, " 类型:", item.item_type)

40. 常见错误 3:攻击力没增加

排查:

EquipmentData.attack_bonus 是否大于 0
equip_item 后是否调用 recalculate_stats
攻击命中时是否调用 get_attack_damage()
是否还在用旧的 attack_damage 变量

如果你还写着:

area.take_damage(attack_damage)

那装备攻击力当然不会生效。

要改成:

area.take_damage(get_attack_damage())

41. 常见错误 4:防御没生效

排查:

take_damage 是否调用 calculate_received_damage
final_defense 是否正确计算
防具 defense_bonus 是否设置
装备是否真的穿上

临时打印:

print("原始伤害:", amount, " 防御:", final_defense, " 最终伤害:", final_damage)

42. 常见错误 5:装备最大血量后血条不变

排查:

max_hp_bonus 是否设置
recalculate_stats 是否更新 max_hp
recalculate_stats 是否 emit health_changed
PlayerHUD 是否监听 health_changed

血条 UI 不会自己知道 max_hp 变了。

必须 emit:

health_changed.emit(current_hp, max_hp)

43. 常见错误 6:装备后背包 UI 不显示已装备

检查:

Player 是否有 is_item_equipped 方法
InventoryPanel 是否能拿到 player
刷新时是否判断 item_type == EQUIPMENT
装备后是否 emit equipment_changed
InventoryPanel 是否监听 equipment_changed 或手动 refresh

可以在 UI 里连接:

player.equipment_changed.connect(refresh)

44. 常见错误 7:同一把装备卖掉后还穿着

这是设计 A 的缺点。

因为装备后仍然保留在背包里,如果商店出售该物品,装备栏可能还引用着这个 Resource。

解决方案:

第一版暂时不允许出售已装备物品

出售时判断:

if current_player.has_method("is_item_equipped"):
    if current_player.is_item_equipped(slot.item):
        print("已装备物品不能出售")
        return

ShopPanel 的出售按钮也可以禁用。

这是一个很重要的小补丁。

45. 禁止出售已装备物品

ShopPanel.gd 的 add_sell_item_button 里:

var is_equipped = false

if current_player != null and current_player.has_method("is_item_equipped"):
    is_equipped = current_player.is_item_equipped(slot.item)

button.disabled = sell_price <= 0 or is_equipped

if is_equipped:
    button.text += " [已装备]"

sell_item 里也防一次:

if current_player.has_method("is_item_equipped"):
    if current_player.is_item_equipped(slot.item):
        print("已装备物品不能出售")
        return

UI 禁用是用户体验。

函数里判断是安全保护。

两层都要有,才稳。

46. 这一部分最重要的记忆点

装备本质上也是一种物品
EquipmentData 可以继承 ItemData
装备栏记录当前穿了什么
基础属性和最终属性要分开
recalculate_stats 根据装备重新计算属性
武器影响 final_attack
防具影响 final_defense max_hp
攻击时用 get_attack_damage()
受伤时用 calculate_received_damage()
装备变化要发 equipment_changed / stats_changed
第一版装备后可以仍留在背包里但要禁止出售已装备物品

47. 你现在的小练习

建议你按这个顺序做:

1. 创建 EquipmentData.gd
2. 创建 wooden_sword.tres
3. 创建 leather_armor.tres
4. Player 添加 base_attack / base_defense / base_max_hp
5. Player 添加 final_attack / final_defense / max_hp
6. Player 添加 equipped_items
7. Player recalculate_stats()
8. Player equip_item()
9. Player unequip_item()
10. Player.use_item 支持 EQUIPMENT
11. 攻击命中改用 get_attack_damage()
12. 受伤改用 calculate_received_damage()
13. 背包里放一个 wooden_sword
14. 点击 wooden_sword 后攻击力增加
15. 打怪时伤害提高
16. 背包里放一个 leather_armor
17. 点击 leather_armor 后防御和最大生命增加
18. 怪物攻击玩家时伤害降低
19. 添加 EquipmentPanel
20. C 打开装备面板
21. 显示武器防具攻击防御生命
22. 商店出售 wooden_sword
23. 玩家购买后装备
24. 禁止出售已装备物品

完成这部分之后,你的 RPG 就有了“获得装备 → 装备变强 → 战斗数值改变 → 商店买卖装备”的成长闭环。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...