关于 ability-system
使用 Godot 原生组件构建数据驱动能力系统的技能,无需第三方插件。适合希望在编辑器中配置能力,并将数据、运行行为和信号通信分离的游戏开发者。
主要功能
- 01以 Resource 配置能力
- 02AbilityComponent 管理激活与冷却
- 03定时增益与减益效果
- 04属性修饰器与游戏标签
- 05能力和效果事件信号
适用场景
- 01创建可在 Godot 编辑器中配置的能力
- 02管理战斗中的消耗与冷却
- 03应用定时增益和减益
- 04根据游戏标签阻止能力激活
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill ability-system安装后在 Claude Code 中输入/ability-system确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/stat-modifiers.mdtags-and-conditions.mdui-binding.md详细介绍
能力系统
使用 Godot 原生组件构建数据驱动的能力系统:能力是 Resource,由 AbilityComponent 节点持有并运行,效果、属性和标签在此基础上组合。无需第三方插件。
相关技能:
resource-pattern用于Resource数据容器,component-system用于组件节点模式,event-bus用于跨系统能力事件,state-machine用于施法者状态(例如施法/眩晕),hud-system用于冷却时间 UI。
1. 架构概览
Godot 4.x 中的能力系统由三个协作层组成:
-
数据层 —
Ability(Resource): 每项能力都是Resource子类,带有导出字段(ability_name、cost、cooldown、cast_time)以及两个方法:can_activate(caster) -> bool用于验证前置条件,activate(caster) -> void用于执行效果。将能力存储为 Resource 后,设计师就可以在 Godot 编辑器中创建和调整能力,而无需修改代码。 -
行为层 —
AbilityComponent(Node): 添加到任何需要使用能力的实体上的单个节点。它持有已授予的能力集合,负责执行费用和冷却时间规则,并调用Ability.activate()。四个信号可在不产生耦合的情况下通知游戏其余部分:ability_activated(ability)、ability_failed(ability, reason)、cooldown_started(ability, duration)和cooldown_finished(ability)。运行时可通过grant(ability)授予新能力,并通过try_activate(ability_name)触发能力。 -
效果层 — 属性修饰器、增益/减益和游戏标签: 能力可以读写施法者的
StatSet(一种拥有命名StatModifier条目的字典的 Resource),以应用临时或永久的属性变更。施法者的子节点GameplayTagContainer用于限制能力激活——例如,“眩晕”标签可以阻止任何能力触发。以下参考文档深入介绍这三部分:
核心规则: 数据放在 Resources 中,行为由组件处理,通过信号通信。
这种分离方式意味着 Ability 资源不包含任何 Node 引用,可以像其他 Godot Resource 一样安全地复制、保存和加载。AbilityComponent 负责持有运行时状态(冷却计时器、激活的能力集合),使 Ability 资源保持无状态,并可由多个施法者同时复用。
2. 能力(费用/冷却时间/施法)
GDScript
# ability.gd
class_name Ability
extends Resource
@export var ability_name: String
@export var cost: float = 0.0
@export var cooldown: float = 1.0
@export var cast_time: float = 0.0
# Override in subclasses or compose via exported effect resources.
func can_activate(caster: Node) -> bool:
return true
func activate(caster: Node) -> void:
pass
# ability_component.gd
class_name AbilityComponent
extends Node
signal ability_activated(ability: Ability)
signal ability_failed(ability: Ability, reason: String)
signal cooldown_started(ability: Ability, duration: float)
signal cooldown_finished(ability: Ability)
@export var resource_pool: float = 100.0
var _granted: Dictionary = {} # ability_name -> Ability
var _cooldowns: Dictionary = {} # ability_name -> seconds remaining
func grant(ability: Ability) -> void:
_granted[ability.ability_name] = ability
func _process(delta: float) -> void:
for name in _cooldowns.keys():
_cooldowns[name] -= delta
if _cooldowns[name] <= 0.0:
_cooldowns.erase(name)
if _granted.has(name):
cooldown_finished.emit(_granted[name])
func try_activate(ability_name: String) -> bool:
var ability: Ability = _granted.get(ability_name)
if ability == null:
return false
if _cooldowns.has(ability_name):
ability_failed.emit(ability, "on_cooldown")
return false
if resource_pool < ability.cost:
ability_failed.emit(ability, "insufficient_resource")
return false
if not ability.can_activate(get_parent()):
ability_failed.emit(ability, "conditions_unmet")
return false
resource_pool -= ability.cost
ability.activate(get_parent())
_cooldowns[ability_name] = ability.cooldown
cooldown_started.emit(ability, ability.cooldown)
ability_activated.emit(ability)
return true
C#
// Ability.cs
using Godot;
[GlobalClass]
public partial class Ability : Resource
{
[Export] public string AbilityName { get; set; } = "";
[Export] public float Cost { get; set; } = 0.0f;
[Export] public float Cooldown { get; set; } = 1.0f;
[Export] public float CastTime { get; set; } = 0.0f;
public virtual bool CanActivate(Node caster) => true;
public virtual void Activate(Node caster) { }
}
// AbilityComponent.cs
using Godot;
using System.Collections.Generic;
public partial class AbilityComponent : Node
{
[Signal] public delegate void AbilityActivatedEventHandler(Ability ability);
[Signal] public delegate void AbilityFailedEventHandler(Ability ability, string reason);
[Signal] public delegate void CooldownStartedEventHandler(Ability ability, float duration);
[Signal] public delegate void CooldownFinishedEventHandler(Ability ability);
[Export] public float ResourcePool { get; set; } = 100.0f;
private readonly Dictionary<string, Ability> _granted = new();
private readonly Dictionary<string, float> _cooldowns = new();
public void Grant(Ability ability) => _granted[ability.AbilityName] = ability;
public override void _Process(double delta)
{
foreach (var name in new List<string>(_cooldowns.Keys))
{
_cooldowns[name] -= (float)delta;
if (_cooldowns[name] <= 0.0f)
{
_cooldowns.Remove(name);
if (_granted.TryGetValue(name, out var finished))
EmitSignal(SignalName.CooldownFinished, finished);
}
}
}
public bool TryActivate(string abilityName)
{
if (!_granted.TryGetValue(abilityName, out var ability)) return false;
if (_cooldowns.ContainsKey(abilityName))
{
EmitSignal(SignalName.AbilityFailed, ability, "on_cooldown");
return false;
}
if (ResourcePool < ability.Cost)
{
EmitSignal(SignalName.AbilityFailed, ability, "insufficient_resource");
return false;
}
if (!ability.CanActivate(GetParent()))
{
EmitSignal(SignalName.AbilityFailed, ability, "conditions_unmet");
return false;
}
ResourcePool -= ability.Cost;
ability.Activate(GetParent());
_cooldowns[abilityName] = ability.Cooldown;
EmitSignal(SignalName.CooldownStarted, ability, ability.Cooldown);
EmitSignal(SignalName.AbilityActivated, ability);
return true;
}
}
3. 增益和减益(限时效果)
效果是 Resource 子类——设计师在编辑器中创建效果,再由能力应用。EffectHolder 节点负责运行时管理:跟踪经过的时间、调用周期性 tick,并在效果到期时将其移除。
GDScript
# effect.gd
class_name Effect
extends Resource
@export var effect_name: String
@export var duration: float = 5.0 # seconds; <= 0 means instant
@export var tick_interval: float = 0.0 # 0 = no periodic tick
func on_apply(target: Node) -> void: pass
func on_tick(target: Node) -> void: pass
func on_expire(target: Node) -> void: pass
# effect_holder.gd
class_name EffectHolder
extends Node
signal effect_applied(effect: Effect)
signal effect_expired(effect: Effect)
# effect -> [elapsed, tick_accum]
var _active: Dictionary = {}
func apply_effect(effect: Effect) -> void:
effect.on_apply(get_parent())
effect_applied.emit(effect)
if effect.duration <= 0.0:
effect.on_expire(get_parent())
effect_expired.emit(effect)
return
_active[effect] = [0.0, 0.0]
func _process(delta: float) -> void:
var to_remove: Array = []
for effect in _active:
_active[effect][0] += delta
if effect.tick_interval > 0.0:
_active[effect][1] += delta
if _active[effect][1] >= effect.tick_interval:
_active[effect][1] -= effect.tick_interval
effect.on_tick(get_parent())
if effect.duration > 0.0 and _active[effect][0] >= effect.duration:
to_remove.append(effect)
for effect in to_remove:
_active.erase(effect)
effect.on_expire(get_parent())
effect_expired.emit(effect)
C#
// Effect.cs
using Godot;
[GlobalClass]
public partial class Effect : Resource
{
[Export] public string EffectName { get; set; } = "";
[Export] public float Duration { get; set; } = 5.0f; // <= 0 = instant
[Export] public float TickInterval { get; set; } = 0.0f; // 0 = no tick
public virtual void OnApply(Node target) { }
public virtual void OnTick(Node target) { }
public virtual void OnExpire(Node target) { }
}
// EffectHolder.cs
using Godot;
using System.Collections.Generic;
public partial class EffectHolder : Node
{
[Signal] public delegate void EffectAppliedEventHandler(Effect effect);
[Signal] public delegate void EffectExpiredEventHandler(Effect effect);
private readonly Dictionary<Effect, (float Elapsed, float TickAccum)> _active = new();
public void ApplyEffect(Effect effect)
{
effect.OnApply(GetParent());
EmitSignal(SignalName.EffectApplied, effect);
if (effect.Duration <= 0f)
{
effect.OnExpire(GetParent());
EmitSignal(SignalName.EffectExpired, effect);
return;
}
_active[effect] = (0f, 0f);
}
public override void _Process(double delta)
{
var toRemove = new List<Effect>();
foreach (var effect in new List<Effect>(_active.Keys))
{
var (elapsed, tickAccum) = _active[effect];
elapsed += (float)delta;
if (effect.TickInterval > 0f)
{
tickAccum += (float)delta;
if (tickAccum >= effect.TickInterval)
{
tickAccum -= effect.TickInterval;
effect.OnTick(GetParent());
}
}
if (effect.Duration > 0f && elapsed >= effect.Duration)
toRemove.Add(effect);
else
_active[effect] = (elapsed, tickAccum); // still active — write back updated elapsed and tick accumulator
}
foreach (var effect in toRemove)
{
_active.Remove(effect);
effect.OnExpire(GetParent());
EmitSignal(SignalName.EffectExpired, effect);
}
}
}
易错点: 移除条目时,切勿直接遍历
_active。GDScript 会构建to_remove列表,并在循环结束后再擦除条目;C# 则出于相同原因遍历new List<Effect>(_active.Keys)。
实现检查清单
- 能力是
Resource;行为由AbilityComponent负责,而非写入数据中。 - 激活前先验证费用、冷却时间和
can_activate()。 - 冷却时间在
_process/_Process中递减,并发出开始/结束信号。 - 增益/减益会应用 → tick → 到期,并发出信号;持续时间会按来源刷新或叠加。
- 属性修饰器按确定顺序重新计算(ADD → MULTIPLY → OVERRIDE)并限制在范围内。
- 能力限制使用游戏标签容器;免疫效果会阻挡效果。
- HUD 绑定到组件信号,不逐帧轮询冷却状态。