mcpField
A
ability-system

ability-system

github.com/jame581开发889145 次安装4 天前更新
代码仓库

关于 ability-system

使用 Godot 原生组件构建数据驱动能力系统的技能,无需第三方插件。适合希望在编辑器中配置能力,并将数据、运行行为和信号通信分离的游戏开发者。

主要功能

  1. 01以 Resource 配置能力
  2. 02AbilityComponent 管理激活与冷却
  3. 03定时增益与减益效果
  4. 04属性修饰器与游戏标签
  5. 05能力和效果事件信号

适用场景

  1. 01创建可在 Godot 编辑器中配置的能力
  2. 02管理战斗中的消耗与冷却
  3. 03应用定时增益和减益
  4. 04根据游戏标签阻止能力激活

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jame581/godotprompter --skill ability-system

安装后在 Claude Code 中输入/ability-system确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
stat-modifiers.md
tags-and-conditions.md
ui-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 绑定到组件信号,不逐帧轮询冷却状态。

评论

支持 Markdown