⚡ Godot 4 道具系统终极教程
基于 Resource 的双技能独立冷却(王者荣耀风格)

本教程将带你从零开始,创建一个完全基于 Resource 的道具系统,每个道具拥有两个独立冷却时间的技能,通过两个按键(如 Q/E)触发。我们将涵盖项目设置、资源创建、技能管理器、玩家背包、冷却更新、UI显示、道具拾取等所有环节,并提供完整的代码和详细注释。

📋 目录

1. 准备工作

在开始之前,请确保你已安装 Godot 4.x 版本。我们将创建一个新项目,并建立以下文件夹结构:

res://
├── assets/          # 存放图标、图片等资源
├── items/           # 道具相关脚本和资源
├── player/          # 玩家脚本和场景
├── skills/          # 技能管理器
├── ui/              # UI 场景和脚本
└── world/           # 测试用的世界场景
💡 提示: 你可以在 Godot 文件系统面板中右键创建文件夹,保持项目整洁。

2. 创建道具数据资源 (ItemData.gd)

道具的核心是数据,我们使用 Resource 来存储每个道具的静态信息(技能ID、冷却时间)和运行时状态(剩余冷却)。

res://items/ItemData.gd

# ItemData.gd
extends Resource
class_name ItemData

# ---------- 导出变量(可在编辑器资源中设置)----------
@export var item_name: String = "未知道具"
@export var icon: Texture2D                     # 道具图标

# 技能1
@export var skill_1_id: String = ""             # 例如 "heal", "fireball"
@export var skill_1_cooldown: float = 5.0       # 冷却时间(秒)

# 技能2
@export var skill_2_id: String = ""
@export var skill_2_cooldown: float = 5.0

# ---------- 运行时变量(不导出,由代码维护)----------
var skill_1_cooldown_remaining: float = 0.0
var skill_2_cooldown_remaining: float = 0.0

# 构造函数:初始化剩余冷却为0(技能可用)
func _init() -> void:
    skill_1_cooldown_remaining = 0.0
    skill_2_cooldown_remaining = 0.0

# 可选:重置冷却(例如重新装备道具时)
func reset_cooldowns() -> void:
    skill_1_cooldown_remaining = 0.0
    skill_2_cooldown_remaining = 0.0

# 判断技能是否可用
func is_skill_ready(skill_num: int) -> bool:
    match skill_num:
        1:
            return skill_1_cooldown_remaining <= 0.0
        2:
            return skill_2_cooldown_remaining <= 0.0
        _:
            return false

# 获取技能当前冷却比例(用于UI进度条,0=冷却中,1=可用)
func get_skill_cooldown_percent(skill_num: int) -> float:
    match skill_num:
        1:
            if skill_1_cooldown <= 0:
                return 1.0
            return 1.0 - (skill_1_cooldown_remaining / skill_1_cooldown)
        2:
            if skill_2_cooldown <= 0:
                return 1.0
            return 1.0 - (skill_2_cooldown_remaining / skill_2_cooldown)
        _:
            return 1.0
📘 关于 Resource 的重要说明:

3. 技能管理器 (SkillManager 单例)

为了避免在每个玩家脚本中重复实现技能逻辑,我们创建一个全局单例来集中处理所有技能效果。这样新增技能只需在管理器里添加一个函数。

res://skills/SkillManager.gd

# SkillManager.gd
extends Node

# 技能映射字典:键是技能ID字符串,值是可调用的方法
var _skill_map: Dictionary = {}

func _ready() -> void:
    # 在这里注册所有技能
    register_skill("heal", _skill_heal)
    register_skill("fireball", _skill_fireball)
    register_skill("shield", _skill_shield)   # 示例第三个技能
    # 你可以根据需要添加更多

# 注册技能(允许外部动态添加)
func register_skill(skill_id: String, callable: Callable) -> void:
    _skill_map[skill_id] = callable

# 执行技能(由玩家调用)
# user: 技能使用者(通常是玩家节点)
# target: 可选目标(可以是敌人、位置等)
func use_skill(skill_id: String, user: Node, target = null) -> void:
    if not _skill_map.has(skill_id):
        push_error("技能管理器:未知技能ID [%s]" % skill_id)
        return
    _skill_map[skill_id].call(user, target)

# ---------- 以下是具体技能实现 ----------
# 每个技能函数接收 user 和 target 两个参数
func _skill_heal(user: Node, _target) -> void:
    # 假设玩家有 health 属性和 heal() 方法
    if user.has_method("heal"):
        user.heal(20)
    else:
        print("技能:治疗失败,玩家没有 heal 方法")

func _skill_fireball(user: Node, target_position = null) -> void:
    # 发射火球,target 可以是点击位置
    var fireball_scene = preload("res://scenes/fireball.tscn")  # 你需要准备这个场景
    if not fireball_scene:
        push_error("缺少火球场景")
        return
    var fireball = fireball_scene.instantiate()
    user.get_parent().add_child(fireball)
    # 设置火球位置为玩家位置 + 偏移
    fireball.global_position = user.global_position + Vector2(50, 0)
    # 如果有目标位置,可以设置方向
    if target_position is Vector2:
        fireball.direction = (target_position - user.global_position).normalized()
    print("发射火球")

func _skill_shield(user: Node, _target) -> void:
    # 生成护盾
    if user.has_method("add_shield"):
        user.add_shield(50)
    else:
        print("玩家没有 add_shield 方法")

配置 Autoload:

  1. 打开 项目 > 项目设置 > 自动加载 (Autoload)。
  2. 在“路径”处点击文件夹图标,选择 SkillManager.gd
  3. 在“节点名称”中输入 SkillManager,点击“添加”。
💡 现在你可以在任何脚本中使用 SkillManager.use_skill(...) 调用技能。

4. 玩家角色设置

创建玩家场景:

  1. 新建场景,根节点选择 CharacterBody2D,命名为 Player
  2. 添加子节点:CollisionShape2D(设置一个矩形或圆形碰撞形状)、Sprite2D(放置玩家图片)。
  3. 保存场景为 res://player/Player.tscn

5. 玩家脚本完整实现 (Player.gd)

将以下脚本附加到玩家根节点,它包含背包管理、冷却更新、技能触发、UI更新等功能。

res://player/Player.gd

extends CharacterBody2D

# ---------- 背包系统 ----------
# 存储当前拥有的道具(每个元素是 ItemData 的独立实例)
var inventory: Array[ItemData] = []
# 当前选中的道具索引(默认0)
var current_item_index: int = 0

# ---------- 玩家属性 ----------
var health: int = 100
var max_health: int = 100
var shield: int = 0

# ---------- 节点引用 ----------
@onready var skill_manager: SkillManager = $"/root/SkillManager"
@onready var sprite: Sprite2D = $Sprite2D
@onready var ui_layer: CanvasLayer  # 可以在场景中手动连接,或通过组查找

# 信号:当当前道具改变时发出(用于UI更新)
signal current_item_changed(item: ItemData)

func _ready() -> void:
    # 测试:添加两个道具(实际游戏中可能通过拾取添加)
    create_test_items()
    # 初始化UI引用(假设UI节点在场景中且名为 "UI")
    ui_layer = get_node("/root/World/UI")  # 根据你的场景结构调整
    # 发出初始信号
    if inventory.size() > 0:
        emit_signal("current_item_changed", inventory[current_item_index])

# 创建测试道具(仅用于演示)
func create_test_items() -> void:
    # 道具1:治疗药水
    var potion = ItemData.new()
    potion.item_name = "治疗药水"
    potion.icon = preload("res://assets/icons/potion.png")  # 准备一个图标
    potion.skill_1_id = "heal"
    potion.skill_1_cooldown = 3.0
    potion.skill_2_id = "shield"
    potion.skill_2_cooldown = 8.0
    add_item(potion)

    # 道具2:火焰炸弹
    var bomb = ItemData.new()
    bomb.item_name = "火焰炸弹"
    bomb.icon = preload("res://assets/icons/bomb.png")
    bomb.skill_1_id = "fireball"
    bomb.skill_1_cooldown = 5.0
    bomb.skill_2_id = "heal"
    bomb.skill_2_cooldown = 12.0
    add_item(bomb)

# ---------- 背包操作 ----------
# 添加道具(复制一份,保证每个道具独立)
func add_item(item_data: ItemData) -> void:
    var new_item = item_data.duplicate(true)  # 深度复制,包括导出的变量
    inventory.append(new_item)
    print("道具添加:", new_item.item_name)

# 移除道具(根据索引或直接传入实例)
func remove_item(index: int) -> void:
    if index >= 0 and index < inventory.size():
        inventory.remove_at(index)
        if inventory.size() == 0:
            current_item_index = 0
            emit_signal("current_item_changed", null)
        else:
            current_item_index = clamp(current_item_index, 0, inventory.size() - 1)
            emit_signal("current_item_changed", inventory[current_item_index])

# 切换道具(由数字键或UI按钮调用)
func switch_item(index: int) -> void:
    if index >= 0 and index < inventory.size():
        current_item_index = index
        emit_signal("current_item_changed", inventory[current_item_index])
        print("切换到道具:", inventory[current_item_index].item_name)

# ---------- 冷却更新(每帧)----------
func _process(delta: float) -> void:
    # 遍历所有道具,减少冷却剩余时间
    for item in inventory:
        if item.skill_1_cooldown_remaining > 0:
            item.skill_1_cooldown_remaining = max(0.0, item.skill_1_cooldown_remaining - delta)
        if item.skill_2_cooldown_remaining > 0:
            item.skill_2_cooldown_remaining = max(0.0, item.skill_2_cooldown_remaining - delta)

# ---------- 输入处理 ----------
func _unhandled_input(event: InputEvent) -> void:
    # 技能按键
    if event.is_action_pressed("skill_1"):
        use_current_item_skill(1)
    elif event.is_action_pressed("skill_2"):
        use_current_item_skill(2)

    # 道具切换按键(数字键1,2)
    if event.is_action_pressed("ui_1"):
        switch_item(0)
    elif event.is_action_pressed("ui_2"):
        switch_item(1)
    # 你可以继续添加 3,4...

# 使用当前道具的指定技能
func use_current_item_skill(skill_num: int) -> void:
    if inventory.is_empty():
        print("背包为空")
        return

    var item = inventory[current_item_index]

    # 获取技能ID和当前冷却
    var skill_id: String
    var cooldown_remaining: float
    var cooldown_total: float

    match skill_num:
        1:
            skill_id = item.skill_1_id
            cooldown_remaining = item.skill_1_cooldown_remaining
            cooldown_total = item.skill_1_cooldown
        2:
            skill_id = item.skill_2_id
            cooldown_remaining = item.skill_2_cooldown_remaining
            cooldown_total = item.skill_2_cooldown
        _:
            return

    # 检查冷却
    if cooldown_remaining > 0:
        print("技能冷却中,剩余 %.1f 秒" % cooldown_remaining)
        return

    # 检查技能ID是否有效
    if skill_id.is_empty():
        print("当前技能未设置ID")
        return

    # 执行技能效果(通过技能管理器)
    # 这里我们可以传递鼠标位置作为目标(如果需要)
    var mouse_pos = get_global_mouse_position()
    skill_manager.use_skill(skill_id, self, mouse_pos)

    # 设置冷却
    if skill_num == 1:
        item.skill_1_cooldown_remaining = cooldown_total
    else:
        item.skill_2_cooldown_remaining = cooldown_total

    print("使用技能 ", skill_num, ",冷却开始")

    # 更新UI(通过信号,UI监听即可)
    emit_signal("current_item_changed", item)

# ---------- 玩家自身的方法(供技能回调)----------
func heal(amount: int) -> void:
    health = min(health + amount, max_health)
    print("治疗 +%d,当前生命 %d/%d" % [amount, health, max_health])

func add_shield(amount: int) -> void:
    shield += amount
    print("护盾 +%d,当前护盾 %d" % [amount, shield])

# 可选:受伤方法
func take_damage(damage: int) -> void:
    if shield > 0:
        var absorbed = min(shield, damage)
        shield -= absorbed
        damage -= absorbed
    health -= damage
    health = max(health, 0)
    print("受到伤害,生命 %d/%d,护盾 %d" % [health, max_health, shield])
    if health <= 0:
        die()

func die() -> void:
    print("玩家死亡")
    # 处理死亡逻辑

6. 输入映射配置

打开 项目 > 项目设置 > 输入映射,添加以下动作:

动作名称按键用途
skill_1Q 或 鼠标左键触发当前道具的技能1
skill_2E 或 鼠标右键触发当前道具的技能2
ui_11切换到第一个道具
ui_22切换到第二个道具

你可以根据需要添加更多切换按键(3,4,...)。

7. 冷却UI界面制作

创建一个简单的UI来显示当前道具图标和两个技能的冷却进度。我们将使用 CanvasLayer 来确保UI显示在游戏世界之上。

7.1 创建UI场景

  1. 新建场景,根节点选择 CanvasLayer,命名为 UI,保存为 res://ui/UI.tscn
  2. 添加子节点:一个 Panel 作为背景(可选),然后添加以下结构:

你可以在 TextureProgressBar 的属性中设置 UnderOver 纹理,实现圆形或条形进度条。

7.2 UI脚本

UI 根节点添加脚本 UI.gd

extends CanvasLayer

@onready var item_icon: TextureRect = $ItemIcon
@onready var skill1_progress: TextureProgressBar = $Skill1Progress
@onready var skill2_progress: TextureProgressBar = $Skill2Progress
@onready var skill1_label: Label = $Skill1Label
@onready var skill2_label: Label = $Skill2Label

# 引用玩家(需要根据场景结构设置)
var player: Node

func _ready() -> void:
    # 查找玩家节点(假设玩家在根节点下名为 "Player")
    player = get_node("/root/World/Player")
    if player:
        # 连接玩家信号
        player.connect("current_item_changed", Callable(self, "_on_current_item_changed"))
    else:
        push_error("UI: 找不到玩家节点")

# 当玩家切换道具时调用
func _on_current_item_changed(item: ItemData) -> void:
    if item == null:
        # 无道具,清空显示
        item_icon.texture = null
        skill1_progress.value = 0
        skill2_progress.value = 0
        skill1_label.text = ""
        skill2_label.text = ""
        return

    # 更新图标
    item_icon.texture = item.icon

    # 初始化进度条(后续由 _process 持续更新)
    # 但我们可以先设置最大值
    skill1_progress.max_value = 1.0
    skill2_progress.max_value = 1.0

    # 可选:显示技能按键提示
    skill1_label.text = "Q"
    skill2_label.text = "E"

func _process(delta: float) -> void:
    if not player or player.inventory.is_empty():
        return

    var item = player.inventory[player.current_item_index]

    # 更新技能1冷却进度
    if item.skill_1_cooldown > 0:
        var percent = 1.0 - (item.skill_1_cooldown_remaining / item.skill_1_cooldown)
        skill1_progress.value = percent
    else:
        skill1_progress.value = 1.0

    # 更新技能2冷却进度
    if item.skill_2_cooldown > 0:
        var percent = 1.0 - (item.skill_2_cooldown_remaining / item.skill_2_cooldown)
        skill2_progress.value = percent
    else:
        skill2_progress.value = 1.0

    # 可选:根据冷却状态改变颜色等

7.3 将UI加入主场景

在你的主游戏场景(例如 World.tscn)中,实例化 UI.tscn 作为子节点。

8. 道具拾取系统

为了能在游戏中获得道具,我们需要创建可拾取的道具物品(世界中的节点)。

8.1 创建拾取物品场景

  1. 新建场景,根节点选择 Area2D,命名为 PickupItem
  2. 添加子节点:CollisionShape2D(设置形状)、Sprite2D(显示道具图标)。
  3. 添加脚本 PickupItem.gd
extends Area2D

# 这个拾取物所包含的道具数据(在编辑器中赋值或代码设置)
@export var item_data: ItemData

func _ready() -> void:
    # 如果设置了图标,则更新Sprite
    if item_data and item_data.icon:
        $Sprite2D.texture = item_data.icon
    # 连接信号
    body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node) -> void:
    if body.is_in_group("player"):  # 将玩家节点加入"player"组
        if body.has_method("add_item"):
            body.add_item(item_data)
            queue_free()  # 拾取后消失

记得将玩家节点加入 "player" 组(在节点属性中设置)。

9. 道具切换(数字键)

我们已经在上面的玩家脚本中处理了 ui_1ui_2 动作,并调用了 switch_item()。你还可以在UI上添加按钮来切换道具。

10. 测试与调试

  1. Player.tscn 放入 World.tscn 中。
  2. 添加一些 PickupItem 实例,并设置不同的 item_data(需要在编辑器创建 .tres 资源或代码设置)。
  3. 运行场景,按 Q/E 使用技能,观察控制台输出和UI变化。
  4. 按 1/2 切换道具,检查图标和冷却是否正确变化。
  5. 拾取新道具,背包应增加物品,并能切换到新道具。

11. 扩展新技能与道具

添加新技能

  1. SkillManager.gd_ready() 中注册新技能 ID 和对应函数。
  2. 实现技能函数,可以利用 usertarget 参数。
  3. ItemData 中为新道具设置对应的技能 ID。

添加新道具

你可以通过代码创建 ItemData 实例并设置属性,也可以使用编辑器创建 .tres 资源文件:

  1. 在文件系统右键,选择 新建 > 资源
  2. 选择 ItemData 作为根类型。
  3. 在资源属性面板中填写名称、图标、技能ID和冷却时间。
  4. 保存为 .tres 文件,然后在代码中通过 load("res://items/xxx.tres") 加载。

12. 常见问题解答

❓ 为什么道具的冷却会互相影响?

答:这是因为没有使用 duplicate() 复制资源。如果你直接使用同一个 ItemData 实例添加到多个背包槽,它们会共享冷却数据。确保在 add_item() 中调用 duplicate(true)

❓ 技能效果无法调用玩家方法(如 heal)?

答:检查技能管理器中是否使用了正确的参数。确保技能函数中通过 user 调用方法,且玩家确实拥有该方法。另外,如果玩家方法需要参数,请正确传递。

❓ 冷却进度条不更新?

答:检查UI脚本是否正确连接到玩家的 current_item_changed 信号,并确保 _process 中正确读取了道具的剩余冷却变量。

❓ 如何让技能有多个等级或效果?

答:可以在 ItemData 中添加额外的变量,如 skill_1_power,然后在技能实现中根据该变量调整效果。技能管理器可以通过 user 获取当前道具(例如玩家当前道具),从而读取这些参数。


🎉 恭喜!你已经完成了一个完整、灵活且可扩展的 Godot 道具系统。现在你可以在此基础上添加更多创意,打造属于你自己的游戏!