本教程将带你从零开始,创建一个完全基于 Resource 的道具系统,每个道具拥有两个独立冷却时间的技能,通过两个按键(如 Q/E)触发。我们将涵盖项目设置、资源创建、技能管理器、玩家背包、冷却更新、UI显示、道具拾取等所有环节,并提供完整的代码和详细注释。
在开始之前,请确保你已安装 Godot 4.x 版本。我们将创建一个新项目,并建立以下文件夹结构:
res://
├── assets/ # 存放图标、图片等资源
├── items/ # 道具相关脚本和资源
├── player/ # 玩家脚本和场景
├── skills/ # 技能管理器
├── ui/ # UI 场景和脚本
└── world/ # 测试用的世界场景
道具的核心是数据,我们使用 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
class_name ItemData 使这个脚本成为一个新类型,可以在代码中直接使用 ItemData.new() 创建实例。@export 的变量可以在编辑器中创建 .tres 资源文件时直接填写,但我们主要用代码动态生成,所以示例中使用代码赋值。为了避免在每个玩家脚本中重复实现技能逻辑,我们创建一个全局单例来集中处理所有技能效果。这样新增技能只需在管理器里添加一个函数。
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:
SkillManager.gd。SkillManager,点击“添加”。SkillManager.use_skill(...) 调用技能。
创建玩家场景:
CharacterBody2D,命名为 Player。CollisionShape2D(设置一个矩形或圆形碰撞形状)、Sprite2D(放置玩家图片)。res://player/Player.tscn。将以下脚本附加到玩家根节点,它包含背包管理、冷却更新、技能触发、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("玩家死亡")
# 处理死亡逻辑
打开 项目 > 项目设置 > 输入映射,添加以下动作:
| 动作名称 | 按键 | 用途 |
|---|---|---|
skill_1 | Q 或 鼠标左键 | 触发当前道具的技能1 |
skill_2 | E 或 鼠标右键 | 触发当前道具的技能2 |
ui_1 | 1 | 切换到第一个道具 |
ui_2 | 2 | 切换到第二个道具 |
你可以根据需要添加更多切换按键(3,4,...)。
创建一个简单的UI来显示当前道具图标和两个技能的冷却进度。我们将使用 CanvasLayer 来确保UI显示在游戏世界之上。
CanvasLayer,命名为 UI,保存为 res://ui/UI.tscn。Panel 作为背景(可选),然后添加以下结构:
TextureRect 用于显示道具图标,命名为 ItemIcon。TextureProgressBar 分别用于技能1和技能2的冷却显示,命名为 Skill1Progress 和 Skill2Progress。Label 显示按键提示(例如 "Q" 和 "E")。你可以在 TextureProgressBar 的属性中设置 Under 和 Over 纹理,实现圆形或条形进度条。
为 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
# 可选:根据冷却状态改变颜色等
在你的主游戏场景(例如 World.tscn)中,实例化 UI.tscn 作为子节点。
为了能在游戏中获得道具,我们需要创建可拾取的道具物品(世界中的节点)。
Area2D,命名为 PickupItem。CollisionShape2D(设置形状)、Sprite2D(显示道具图标)。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" 组(在节点属性中设置)。
我们已经在上面的玩家脚本中处理了 ui_1 和 ui_2 动作,并调用了 switch_item()。你还可以在UI上添加按钮来切换道具。
Player.tscn 放入 World.tscn 中。PickupItem 实例,并设置不同的 item_data(需要在编辑器创建 .tres 资源或代码设置)。SkillManager.gd 的 _ready() 中注册新技能 ID 和对应函数。user 和 target 参数。ItemData 中为新道具设置对应的技能 ID。你可以通过代码创建 ItemData 实例并设置属性,也可以使用编辑器创建 .tres 资源文件:
ItemData 作为根类型。.tres 文件,然后在代码中通过 load("res://items/xxx.tres") 加载。答:这是因为没有使用 duplicate() 复制资源。如果你直接使用同一个 ItemData 实例添加到多个背包槽,它们会共享冷却数据。确保在 add_item() 中调用 duplicate(true)。
答:检查技能管理器中是否使用了正确的参数。确保技能函数中通过 user 调用方法,且玩家确实拥有该方法。另外,如果玩家方法需要参数,请正确传递。
答:检查UI脚本是否正确连接到玩家的 current_item_changed 信号,并确保 _process 中正确读取了道具的剩余冷却变量。
答:可以在 ItemData 中添加额外的变量,如 skill_1_power,然后在技能实现中根据该变量调整效果。技能管理器可以通过 user 获取当前道具(例如玩家当前道具),从而读取这些参数。
🎉 恭喜!你已经完成了一个完整、灵活且可扩展的 Godot 道具系统。现在你可以在此基础上添加更多创意,打造属于你自己的游戏!