Skip to content

技能系统(Ability System) ​

技能系统管理单位的所有能力,包括执行技能(主动/攻击)、切换技能、变形技能和被动技能。它与指令系统、效果系统、消耗系统深度集成。

完整施法流程、目标快照、重新接近和动画表现的配合,见指令、施法与技能动画的完整生命周期。

目录 ​

类继承关系 ​

数编表(GameData) ​

GameDataAbility                    ← 技能基类(被动属性、状态、图标)
  └─ GameDataAbilityActive         ← 主动技能基类(施法时间、消耗、效果、动画)
       ├─ GameDataAbilityExecute   ← 执行技能(范围、攻击标记、目标获取)
       └─ GameDataAbilityToggle    ← 切换技能(开/关状态)
            └─ GameDataAbilityMorph ← 变形技能(变形目标单位)

运行时实例 ​

Ability                            ← 挂载在单位上的技能实例
  └─ AbilityActive                 ← 主动技能实例(冷却、充能、多阶段)
       ├─ AbilityExecute           ← 执行技能实例
       └─ AbilityToggle            ← 切换技能实例
            └─ AbilityMorph        ← 变形技能实例

数编表配置(GameData) ​

GameDataAbility — 技能基类 ​

所有技能类型的基础配置,定义被动行为、状态修改和显示信息。

csharp
public partial class GameDataAbility
{
    // 显示
    public LocalizedString? DisplayName { get; set; }
    public LocalizedString? Description { get; set; }
    public Icon? Icon { get; set; }

    // 目标
    public AbilityTargetType TargetType { get; set; }

    // 分类(可扩展枚举,默认值:CanBeDisabled, CanBeSilenced)
    public List<AbilityCategory> Categories { get; set; } = [];

    // 标志位
    public AbilityFlags Flags { get; set; } = new();

    // 等级
    public uint? LevelMax { get; set; }

    // 被动周期效果
    public FuncTime? PassivePeriod { get; set; }
    public IGameLink<GameDataEffect>? PassivePeriodicEffect { get; set; }

    // 激活/停用效果(被动技能:启用/禁用时触发;切换技能:开/关时触发)
    public IGameLink<GameDataEffect>? ActivatedEffect { get; set; }
    public IGameLink<GameDataEffect>? DeactivatedEffect { get; set; }

    // 单位属性修改(技能激活时应用到宿主单位)
    public List<UnitPropertyModification> Modifications { get; set; } = [];

    // 单位状态操作
    public List<UnitState> AddStates { get; set; } = [];
    public List<UnitState> RemoveStates { get; set; } = [];
    public List<UnitState> ImmuneStates { get; set; } = [];

    // 响应(被动触发器)
    public List<IGameLink<GameDataResponse>?>? Responses { get; set; }

    // 同步类型
    public SyncType SyncType { get; set; } = SyncType.SelfOrSight;

    // 表现
    public List<IGameLink<GameDataActor>?>? ActorArray { get; set; }
}

> 被动技能就是只使用 GameDataAbility(不使用 GameDataAbilityActive 子类)的技能。通过 Modifications、AddStates、PassivePeriodicEffect、ActivatedEffect、Responses 等字段定义被动效果。

GameDataAbilityActive — 主动技能基类 ​

所有可施放技能的公共配置,增加施法时间、消耗和效果。

csharp
public abstract partial class GameDataAbilityActive : GameDataAbility
{
    // 施法效果(施放时执行的效果链入口)
    public IGameLink<GameDataEffect>? Effect { get; set; }

    // ⚠️ 属性名是 Time,不是 CastTime
    public SpellCastTime Time { get; set; } = new();

    // 消耗(冷却 + 资源)
    public Cost? Cost { get; set; }

    // 施法中断配置
    public SpellInterruptConfig SpellInterruptConfig { get; set; } = new();

    // 标志位
    public AbilityActiveFlags AbilityActiveFlags { get; set; } = new();

    // 是否在效果执行失败时记录日志
    public bool LogExecutionFailure { get; set; }

    // 多阶段技能配置
    public List<AbilityPhaseSetting> AbilityPhaseSettings { get; set; } = [];

    // 分阶段 Actor 生成(视觉效果)
    public List<AbilityStageSpawn>? StagedSpawns { get; set; }

    // 施法动画(多个时随机选择)
    public List<IGameLink<GameDataAnimation>?>? Animation { get; set; }
}

GameDataAbilityExecute — 执行技能 ​

最常用的技能类型,用于攻击和主动释放技能。

csharp
public partial class GameDataAbilityExecute : GameDataAbilityActive
{
    // 执行标志(IsAttack 是攻击技能的关键标记)
    public AbilityExecuteFlags AbilityExecuteFlags { get; set; } = new();

    // 技能有效范围
    public FuncFloat Range { get; set; } = static (_) => 0;

    // 攻击弧度(扇形攻击角度)
    public Angle Arc { get; set; }
    public Angle ArcSlop { get; set; } = Angle.FromDegree(12);

    // ⚠️ 目标获取配置(必须配置 TargetingFilters 以避免攻击自己)
    public AcquireSettings AcquireSettings { get; set; } = new();

    // 目标指示器(客户端 UI)
    public TargetingIndicatorInfo? TargetingIndicatorInfo { get; set; }
}

完整攻击技能示例 ​

csharp
// 1. 定义伤害效果
var damageLink = new GameLink<GameDataEffectDamage, GameDataEffect>("hero_attack_damage");
_ = new GameDataEffectDamage(damageLink)
{
    Amount = static (ctx) => ctx.Caster.GetUnitPropertyFinal(MyData.GameDataUnitProperty.AttackDamage) ?? 0,
};

// 2. 定义攻击技能
var attackLink = new GameLink<GameDataAbilityExecute, GameDataAbility>("hero_attack");
_ = new GameDataAbilityExecute(attackLink)
{
    DisplayName = "攻击",
    TargetType = AbilityTargetType.Unit,
    Effect = damageLink,
    Range = static (e) => (float)(e.Caster.GetUnitPropertyFinal(MyData.GameDataUnitProperty.AttackRange) ?? 150),
    AbilityExecuteFlags = new() { IsAttack = true },
    Time = new()
    {
        Preswing = static (_) => TimeSpan.FromSeconds(0.4),
        Channel = static (_) => TimeSpan.FromSeconds(0.6),
        Backswing = static (_) => TimeSpan.FromSeconds(0.3),
        NormalizedDuration = static (_) => TimeSpan.FromSeconds(1.0),
    },
    AcquireSettings = new()
    {
        Recast = true,
        TargetingFilters = [new()
        {
            Required = [UnitRelationship.Enemy],
            Excluded = [UnitState.Dead, UnitState.Invulnerable],
        }],
    },
};

// 3. 将技能绑定到单位
_ = new GameDataUnit(unitLink) { Abilities = [attackLink] };

带消耗的技能示例 ​

csharp
var fireballLink = new GameLink<GameDataAbilityExecute, GameDataAbility>("fireball");
_ = new GameDataAbilityExecute(fireballLink)
{
    DisplayName = "火球术",
    TargetType = AbilityTargetType.Unit,
    Effect = fireballDamageLink,
    Range = static (_) => 600,
    Time = new()
    {
        Preswing = static (_) => TimeSpan.FromSeconds(0.3),
        Cast = static (_) => TimeSpan.FromSeconds(1.0),
        Backswing = static (_) => TimeSpan.FromSeconds(0.3),
    },
    Cost = new()
    {
        Cooldown = cooldown8sLink,  // IGameLink<GameDataCooldownActive>
        Vital = { [PropertyVital.Mana] = static (_) => 75 },
    },
    AcquireSettings = new()
    {
        TargetingFilters = [new()
        {
            Required = [UnitRelationship.Enemy],
            Excluded = [UnitState.Dead],
        }],
    },
};

GameDataAbilityToggle — 切换技能 ​

具有开/关两种状态的技能,用于光环、护盾等持续效果。

csharp
public partial class GameDataAbilityToggle : GameDataAbilityActive
{
    // 关闭状态图标
    public Icon? IconOff { get; set; }

    // 切换标志
    public AbilityToggleFlags ToggleFlags { get; set; } = new();
}

> 重要:切换技能的开/关效果使用基类 GameDataAbility 的 ActivatedEffect(开启时)和 DeactivatedEffect(关闭时),不是单独的属性。

csharp
var shieldLink = new GameLink<GameDataAbilityToggle, GameDataAbility>("magic_shield");
_ = new GameDataAbilityToggle(shieldLink)
{
    DisplayName = "魔法护盾",
    TargetType = AbilityTargetType.None,
    ActivatedEffect = shieldOnEffectLink,    // 开启时执行
    DeactivatedEffect = shieldOffEffectLink, // 关闭时执行
    Cost = new()
    {
        Cooldown = cooldown2sLink,
    },
};

GameDataAbilityMorph — 变形技能 ​

继承自 GameDataAbilityToggle,开启时将单位变形为另一个单位形态。

csharp
public partial class GameDataAbilityMorph : GameDataAbilityToggle
{
    // 变身后的目标单位数据
    public IGameLink<GameDataUnit> MorphUnit { get; set; }

    // 变形标志
    public MorphFlags MorphFlags { get; set; } = new();
}

变形技能开启时调用 unit.Morph(Cache),关闭时调用 unit.UnMorph(Cache)。开启状态的变形技能不会被其他变形操作移除。

AbilityTargetType 与效果链 ​

AbilityTargetType 决定施法目标类型,也决定传给效果链的主目标的性质。这是配置效果链时最关键的规则。

csharp
public enum AbilityTargetType
{
    None,          // 无目标,主目标 = 施法者自身
    Unit,          // 单位目标,主目标 = 选中的单位
    Ground,        // 地面点目标,主目标 = 地面坐标点
    UnitOrGround,  // 单位或地面点(优先单位)
    Vector,        // 方向目标,主目标 = 方向末端坐标点
    Interact,      // 交互技能(见下方说明)
}

Interact — 交互技能 ​

交互技能的目标关系与普通技能相反:技能挂在被交互的单位上(如 NPC、可拾取物品、开关),由其他单位来触发。

对比普通技能交互技能
技能归属施法者自己被交互的目标单位
效果执行目标选中的目标技能宿主自身(Ability.Host)
TargetingFilters 含义筛选合法的目标筛选合法的施法者(谁可以来交互)
接近行为施法者接近目标施法者接近技能宿主

运作方式:

  1. 单位 B(NPC)拥有交互技能,TargetingFilters 定义谁可以与 B 交互
  2. 单位 A 对 B 的交互技能下达指令 → A 走向 B
  3. 到达后,系统检查 A 是否通过 B 技能的 TargetingFilters(角色互换:filter 以 B 为视角检查 A)
  4. 通过后,效果以 B 为上下文执行

典型用途:可拾取物品、NPC 对话、机关开关、商店交互等。

效果链配方 ​

GameDataEffectDamage 等面向单位的效果只能作用于单位目标。如果技能的主目标是坐标点(Vector/Ground/None)则显然无法造成伤害,必须先用 GameDataEffectSearch 搜索范围内的单位:

AbilityTargetType主目标性质典型效果链适用场景
Unit单位实体Ability → EffectDamage单体攻击
UnitOrGround单位或地面坐标点取决于实际目标:单位 → EffectDamage;坐标 → EffectSearch → EffectDamage灵活技能(可点人也可点地)
Vector施法者前方固定距离坐标点Ability → EffectSearch(Cone) → EffectDamage扇形挥砍、发波
Ground地面坐标点Ability → EffectSearch(Circle) → EffectDamageAOE 技能
None施法者自身Ability → EffectSearch(Circle, center=Caster) → EffectDamage新星、战吼
Interact技能宿主实体Ability → EffectDamage 或自定义效果拾取、NPC 交互

SmartAcquire 传入单位时的目标转换 ​

当 SmartAcquire 自动选取到一个单位作为目标、但技能的 AbilityTargetType 无法直接接受单位目标时,系统会进行转换:

AbilityTargetTypeSmartAcquire 选到单位后的转换
Unit直接使用该单位实体作为主目标
UnitOrGround直接使用该单位实体作为主目标(与 Unit 相同)
Ground取该单位的坐标(target.Position)作为主目标
Vector仅取施法者朝向该单位的方向,主目标位置固定在施法者前方 Range + InteractRadius 处,与目标单位的实际距离无关

> Vector 的关键行为:Vector 技能的效果链主目标始终是施法者前方固定距离(Range + 施法者 InteractRadius)的坐标点。即使 SmartAcquire 选中了近处的敌人,效果链主目标也不会停在该敌人的位置——只有方向受影响。典型例子:向一个方向发波的技能会朝着自动选到的敌人方向发射,但波的终点由技能 Range 决定,不会因为敌人距离近就缩短。 > > Ground 的对比:Ground 技能会直接取目标单位的坐标作为效果链主目标,效果会落在目标单位脚下。

单体攻击(Unit 目标) ​

csharp
// 主目标是单位,直接指向伤害效果
_ = new GameDataAbilityExecute(attackLink)
{
    TargetType = AbilityTargetType.Unit,
    Effect = damageLink, // 直接 EffectDamage
};

扇形范围攻击(Vector 目标) ​

csharp
// 1. 搜索效果
_ = new GameDataEffectSearch(searchLink)
{
    Method = SearchMethod.Cone,
    // ⚠️ Vector 技能的默认主目标是方向末端坐标点,
    // 必须设为 Caster,否则搜索中心在末端而非施法者。
    TargetLocation = new() { Value = TargetLocation.Caster },

    // 扇形搜索默认取效果目标的朝向。TargetLocation 改为 Caster 后默认朝向变成施法者朝向,
    // 通常默认值就够用。但一些场合下施法者朝向不一定与技能施法方向完全一致(如转身延迟),
    // 如要精确控制方向,可显式设为从施法者到主目标(方向末端)的方向:
    Facing = new()
    {
        Location = new() { Value = TargetLocation.Caster },
        Method = EffectAngleMethod.AngleBetweenTwoPoints,
        OtherLocation = new() { Value = TargetLocation.MainTarget },
    },

    // Radius 使用技能 Range 属性,使搜索范围与技能声称的有效距离一致
    Radius = static (ctx) => ctx.Caster.GetUnitPropertyFinal(MyData.GameDataUnitProperty.AttackRange) ?? 0,
    CentralAngle = static (_) => new Angle(120),
    Effect = damageLink,
    SearchFilters = [new()
    {
        Required = [UnitRelationship.Enemy, UnitFilter.Unit],
        Excluded = [UnitState.Dead, UnitState.Invulnerable],
    }],
    // ExtendByUnitRadius = true 将搜索半径额外扩大 TargetLocation 目标(此处为施法者)
    // 的 InteractRadius,使搜索范围与指令系统的接近判定(Range + InteractRadius)一致
    SearchFlags = new SearchFlags { ExtendByUnitRadius = true },
};

// 2. 技能指向搜索效果
_ = new GameDataAbilityExecute(attackLink)
{
    TargetType = AbilityTargetType.Vector,
    Effect = searchLink, // 指向 EffectSearch,不是 EffectDamage
    AbilityExecuteFlags = new() { IsAttack = true, AlwaysAcquireTarget = true },
};

> AlwaysAcquireTarget = true 使 Vector/None 技能自动面向最近的敌人。

圆形范围攻击(Ground 目标) ​

csharp
_ = new GameDataEffectSearch(searchLink)
{
    Method = SearchMethod.Circle,
    Radius = static (_) => 300,
    Effect = damageLink,
    SearchFilters = [new()
    {
        Required = [UnitRelationship.Enemy, UnitFilter.Unit],
        Excluded = [UnitState.Dead, UnitState.Invulnerable],
    }],
};

_ = new GameDataAbilityExecute(aoeLink)
{
    TargetType = AbilityTargetType.Ground,
    Effect = searchLink,
    Range = static (_) => 800,
};

AcquireSettings — 目标获取配置 ​

AcquireSettings 配置在 GameDataAbilityExecute 上,控制技能的目标过滤和自动选择。

csharp
public class AcquireSettings
{
    // 是否支持自动施法模式
    public bool AutoCast { get; set; }

    // 技能被赋予时,自动施法是否默认开启
    public bool AutoCastOn { get; set; }

    // 自动施法遇到摇杆移动输入时的处理策略,默认 BlockWhileVectorMoving
    public AutoCastMovePolicy AutoCastMovePolicy { get; set; } = AutoCastMovePolicy.BlockWhileVectorMoving;

    // 施法完成后是否自动重新施法
    public bool Recast { get; set; }

    // Recast 时是否重新搜索最优目标
    public bool RecastUseAutoCast { get; set; }

    // 自动目标获取总开关,默认 true
    public bool Enabled { get; set; } = true;

    // 目标过滤器
    public TargetFilterComplex TargetingFilters { get; set; } = [];

    // 自动获取目标(默认 true)
    public bool SmartAcquire { get; set; } = true;

    // 自定义获取范围(null 则使用技能 Range)
    public FuncFloat? AcquireRange { get; set; }

    // 自定义目标优先级排序
    public ContextualPriorityComparer<Ability, Unit>? AcquireSorting { get; set; }
}
csharp
public enum AutoCastMovePolicy
{
    // 有摇杆移动输入时不主动发起自动施法
    BlockWhileVectorMoving,

    // 允许自动施法在摇杆移动输入期间发起
    AllowWhileVectorMoving,
}

TargetingFilters 的双重作用 ​

TargetingFilters 不仅用于 SmartAcquire 自动选取,还在特定技能类型中充当指令层的目标校验:

AbilityTargetTypeSmartAcquire 自动选取手动目标校验
Unit在范围内搜索通过过滤的单位手动指定的目标必须通过过滤,否则指令被拒绝(CannotTargetThat)
Interact在范围内搜索通过过滤的单位校验施法者(而非目标)是否通过过滤(角色反转)
Ground / Vector / None在范围内搜索通过过滤的单位不参与手动目标校验

> 注意区分指令层与执行层的过滤:TargetingFilters 控制"技能能否对某个目标下达施法指令"(指令层)。AOE 技能实际命中哪些单位,由效果树中 GameDataEffectSearch.SearchFilters 控制(执行层)。两者独立,互不影响。

SmartAcquire — 自动目标选取 ​

当施法指令未指定目标时(如 AI 发出的攻击指令、或 AlwaysAcquireTarget 的 Vector/None 技能),系统会自动在范围内搜索合法目标:

  1. 以技能 Range(或 AcquireRange,若配置)为搜索半径
  2. 用 TargetingFilters 过滤候选单位
  3. 按 AcquireSorting 排序(未配置则按距离)
  4. 选择第一个通过的单位作为目标

Enabled 和 SmartAcquire 都默认为 true。Enabled = false 会关闭 SmartAcquire、AutoCast、RecastUseAutoCast 共用的自动目标搜索;手动指定 Unit 目标时,TargetingFilters 仍会参与指令校验。TargetingFilters 为空时放行所有目标(包含施法者自身),这是单位攻击自己的根源。

AutoCast 默认不会在单位存在摇杆移动输入时主动发起施法,避免自动施法抢掉玩家当前移动意图。需要允许移动中自动施法的技能,可以配置 AutoCastMovePolicy = AutoCastMovePolicy.AllowWhileVectorMoving。

Recast 控制的是“已经发起一次施法后,是否在完成时继续尝试下发下一次同技能指令”。它常用于按住攻击、自动施法或 AI 持续普攻。Recast 不是攻击技能标记;一个技能是否会被攻击指令选中,仍由 AbilityExecuteFlags.IsAttack 决定。

详见 目标过滤系统 — 技能目标选取。

TargetingFilters 配置示例 ​

csharp
AcquireSettings = new()
{
    TargetingFilters = [new()
    {
        Required = [UnitRelationship.Enemy],                // 必须是敌人
        Excluded = [UnitState.Dead, UnitState.Invulnerable], // 排除死亡和无敌
    }],
},

> TargetingFilters 是所有攻击技能的必需配置。不配置时,SmartAcquire 会选中最近的可攻击单位(包含自身),导致单位攻击自己。这是最常见的技能配置错误。

SpellCastTime — 施法时间 ​

SpellCastTime 配置在 GameDataAbilityActive.Time 属性上(注意属性名是 Time),控制技能执行各阶段的时长。

csharp
public class SpellCastTime
{
    public FuncTime Preswing { get; set; }      // 前摇(可中断,不扣消耗)
    public FuncTime Cast { get; set; }          // 施法(效果在此阶段末尾执行)
    public FuncTime Channel { get; set; }       // 引导(持续施法)
    public FuncTime Backswing { get; set; }     // 后摇(恢复阶段)
    public FuncTime? NormalizedDuration { get; set; } // 标准化持续时间
}

FuncTime 是 Func<IExecutionContext, TimeSpan> 的别名,允许根据技能等级等因素动态计算。

执行阶段流程 ​

Approach → WaitForCooldown → Preswing → Cast → Channel → Backswing → Left
  • Preswing:前摇准备阶段,此阶段被中断不触发冷却
  • Cast:施法主体阶段,效果在此阶段执行
  • Channel:引导阶段,适合持续施法
  • Backswing:后摇恢复阶段
  • Left:技能完成

NormalizedDuration ​

当设置 NormalizedDuration 时,系统会按比例缩放 Preswing + Cast + Channel,使总时长等于 NormalizedDuration。主要用于攻击技能控制攻击间隔一致性。

当游戏策划需要精确控制普攻速度时,NormalizedDuration 是推荐的普攻周期控制字段。普攻技能通常不需要配置 Cost.Cooldown;冷却是技能消耗/可用性门槛,不是“最短攻击间隔”。冷却作为 Cost 通常在进入 Channel 并消耗资源时启动,下一次攻击要真正开始还会叠加前摇、施法/引导等阶段。若只把攻速公式写在冷却上,即使冷却缩短,施法时间仍可能卡住实际攻速。

NormalizedDuration 会保持原始 Preswing、Cast、Channel 的相对比例,只把三者压缩或拉伸到目标周期;Backswing 会按同一比例缩放,但默认可以被下一条指令打断。因此,对于固定攻速和会被属性/Buff/装备动态改变的攻速,都更适合把目标周期写在 NormalizedDuration 中。

csharp
// 固定 1 秒一次普攻;若项目有 AttackSpeed / 攻速倍率属性,把换算公式写在 NormalizedDuration。
Time = new()
{
    Preswing = static (_) => TimeSpan.FromSeconds(0.25),
    Channel = static (_) => TimeSpan.FromSeconds(0.75),
    Backswing = static (_) => TimeSpan.FromSeconds(0.25),
    NormalizedDuration = static (_) => TimeSpan.FromSeconds(1.0),
},
AcquireSettings = new()
{
    Recast = true,
    TargetingFilters = [new()
    {
        Required = [UnitRelationship.Enemy],
        Excluded = [UnitState.Dead, UnitState.Invulnerable],
    }],
},
// 普攻通常不配置 Cost.Cooldown,除非设计上需要额外限制可用性。

常见配置模式 ​

csharp
// 瞬发技能(所有时间为零,使用默认值)
Time = new(),

// 近战攻击
Time = new()
{
    Preswing = static (_) => TimeSpan.FromSeconds(0.2),
    Channel = static (_) => TimeSpan.FromSeconds(0.8),
    Backswing = static (_) => TimeSpan.FromSeconds(0.4),
    NormalizedDuration = static (_) => TimeSpan.FromSeconds(1.0),
},

// 施法技能
Time = new()
{
    Preswing = static (_) => TimeSpan.FromSeconds(0.3),
    Cast = static (_) => TimeSpan.FromSeconds(1.5),
    Backswing = static (_) => TimeSpan.FromSeconds(0.3),
},

// 引导技能
Time = new()
{
    Preswing = static (_) => TimeSpan.FromSeconds(0.5),
    Channel = static (_) => TimeSpan.FromSeconds(5.0),
    Backswing = static (_) => TimeSpan.FromSeconds(0.3),
},

// 根据等级变化的施法时间
Time = new()
{
    Cast = (ctx) => TimeSpan.FromSeconds(2.0 - ctx.Ability!.Level * 0.1),
},

Cost — 消耗配置 ​

Cost 配置在 GameDataAbilityActive.Cost 属性上,定义技能的冷却和资源消耗。

csharp
public class Cost
{
    // 冷却计时器
    public IGameLink<GameDataCooldownActive>? Cooldown { get; set; }

    // 充能系统(限次使用)
    public IGameLink<GameDataCooldownCharge>? Charge { get; set; }

    // 生命值/法力值消耗
    public QuickDictionary<PropertyVital, FuncNumber> Vital { get; set; } = [];

    // 玩家级资源消耗(金币等)
    public QuickDictionary<PropertyPlayerNumeric, FuncNumber> PlayerCost { get; set; } = [];
}

配置示例 ​

csharp
// 需要先定义冷却数编表
var cd8s = new GameLink<GameDataCooldownActive, GameDataCooldown>("cd_8s");
_ = new GameDataCooldownActive(cd8s)
{
    Duration = static (_) => TimeSpan.FromSeconds(8),
};

// 在技能上配置消耗
Cost = new()
{
    Cooldown = cd8s,
    Vital = { [PropertyVital.Mana] = static (_) => 75 },
},

运行时通过 AbilityActive.Cooldown 和 AbilityActive.Charge 属性访问冷却/充能状态。

SpellInterruptConfig — 中断配置 ​

控制技能在各阶段是否可被中断,以及中断的优先级。

csharp
public class SpellInterruptConfig
{
    // 中断优先级(被中断的阈值 / 发起中断的优先级)
    public Fixed InterruptedRequiredPriority { get; set; }
    public Fixed InterruptingPriority { get; set; }

    // 各阶段是否可中断(默认全部 true)
    public bool InterruptiblePreswing { get; set; } = true;
    public bool InterruptibleApproach { get; set; } = true;
    public bool InterruptibleWaitForCooldown { get; set; } = true;
    public bool InterruptibleCast { get; set; } = true;
    public bool InterruptibleChannel { get; set; } = true;
    public bool InterruptibleBackswing { get; set; } = true;
}

中断规则:只有当新指令的 InterruptingPriority 大于当前技能的 InterruptedRequiredPriority 时,才能中断。

AbilityFlags / AbilityExecuteFlags ​

AbilityFlags(所有技能) ​

csharp
public class AbilityFlags
{
    public bool Hidden { get; set; }                         // 隐藏技能
    public bool PersistDuringMorph { get; set; }             // 变形时保留
    public bool DisplayDescriptionOnLongClick { get; set; }  // 长按显示描述
    public bool DisableWhenDead { get; set; } = true;        // 死亡时禁用(默认 true)
}

AbilityActiveFlags(主动技能) ​

csharp
public class AbilityActiveFlags
{
    public bool Transient { get; set; }              // 瞬态技能,立即执行且不打断当前行为
    public bool AllowEnqueueInCooldown { get; set; } // 冷却中可下达指令排队
    public bool SkipEffectPreValidation { get; set; } // 跳过入队时对子效果树的预验证
}

自动施法不在 AbilityActiveFlags 中配置;使用 GameDataAbilityExecute.AcquireSettings.AutoCast 和 AutoCastOn。

Transient = true 的技能在指令不带 CommandFlag.Queued 时,可在单位正在移动、攻击或引导另一个技能的过程中立即执行效果,无需等待或抢占当前指令;调度本身不会暂停、取消原指令。例如边移动边开启护盾,或引导技能 A 期间发动瞬态技能 B。仍需通过瞬态技能自身的执行和消耗检查,且效果主动改变队列或单位状态时仍可能影响原行为。显式带 Queued 则先排队,详见完整生命周期指南第 2 节。

AbilityExecuteFlags(执行技能) ​

csharp
public class AbilityExecuteFlags
{
    public bool IsAttack { get; set; }            // 标记为攻击技能
    public bool InfiniteCasting { get; set; }     // 无限施法
    public bool AlwaysAcquireTarget { get; set; } // None/Vector 技能总是获取目标
    public bool Reapproachable { get; set; }      // 进入 Channel 前出射程时重新接近并起手
}

> IsAttack = true 是攻击技能的关键标记。只有标记了 IsAttack 的技能才能被 Attack 指令使用。单位必须至少有一个攻击技能才能执行攻击行为。

普攻技能里常见的两个开关分工如下:

字段作用
AbilityExecuteFlags.IsAttack标记这个技能可以被 Attack 指令、AttackTarget 和默认战斗 AI 当作普攻技能查找和使用
AcquireSettings.Recast一次攻击完成后自动尝试续发下一次同技能指令,用于连续普攻;只想一条指令打一刀时保持 false

AbilityToggleFlags(切换技能) ​

csharp
public class AbilityToggleFlags
{
    public bool ToggledOn { get; set; }  // 初始为开启状态
}

AbilityCategory(可扩展枚举) ​

AbilityCategory 是可扩展枚举,框架内置值:

  • CanBeDisabled — 可被禁用
  • CanBeSilenced — 可被沉默

游戏项目可通过 [EnumExtension] 扩展自定义分类。

AbilityManager — 技能管理器 ​

AbilityManager 是 ObjectManager<Ability, GameDataAbility> 的子类,挂载在单位上管理技能。

获取管理器 ​

csharp
var manager = unit.GetComponent<AbilityManager>();

继承自 ObjectManager 的方法 ​

csharp
// 查询
Ability? Get(IGameLink<GameDataAbility> link);
T? Get<T>(IGameLink<GameDataAbility> link) where T : Ability;
HashSet<Ability> GetAll();
IEnumerable<Ability> Get(Func<Ability, bool> condition);
Ability? GetOne(Func<Ability, bool> condition);
bool IsEnabled(IGameLink<GameDataAbility> link);

// 事件
event Action<Ability>? OnObjectAttached;
event Action<Ability>? OnLinkActivated;
event Action<Ability>? OnLinkDeactivated;

AbilityManager 自有方法 ​

csharp
// 查询便利方法
bool HasAbility(IGameLink<GameDataAbility> link);
bool HasAnyAbility(params IGameLink<GameDataAbility>[] links);
bool HasAllAbilities(params IGameLink<GameDataAbility>[] links);
IEnumerable<Ability> GetActivatedAbilities();
IEnumerable<Ability> GetEnabledAbilities();
IEnumerable<Ability> GetPassiveAbilities();      // 非 AbilityActive 的技能
IEnumerable<Ability> GetActiveAbilities();       // AbilityActive 类型的技能
IEnumerable<Ability> GetAbilitiesByTargetType(AbilityTargetType type);
bool IsAbilityActivated(IGameLink<GameDataAbility> link);
bool IsAbilityEnabled(IGameLink<GameDataAbility> link);

// 攻击技能查询
IEnumerable<AbilityExecute> GetValidAttacks();
AbilityExecute? GetValidAttack();

// 批量操作
int RemoveByPredicate(Func<Ability, bool> predicate);
int CountAbilities(Func<Ability, bool> predicate);

添加技能 ​

技能通过 GameDataUnit.Abilities 列表在单位配置中指定,框架自动创建:

csharp
_ = new GameDataUnit(heroLink)
{
    Abilities = [attackLink, fireballLink, shieldLink],
};

运行时动态添加技能通过数编表的 CreateAbility 工厂方法:

csharp
#if SERVER
attackLink.Data!.CreateAbility(unit);
#endif

移除技能 ​

csharp
#if SERVER
var ability = manager.Get(attackLink);
ability?.Destroy();

// 或批量移除
manager.RemoveByPredicate(a => a is AbilityToggle);
#endif

指令系统集成 ​

扩展方法(推荐) ​

最简洁的方式是使用 CommandExtensions 提供的扩展方法:

csharp
#if SERVER
// 释放执行技能
unit.CastAbility(fireballLink, target: enemyUnit);

// 切换技能
unit.ToggleAbility(shieldLink);
unit.TurnOnAbility(shieldLink);
unit.TurnOffAbility(shieldLink);

// 攻击(自动查找 IsAttack=true 的技能)
unit.AttackTarget(enemyUnit);

// 停止
unit.Stop();
#endif

CommandBuilder(高级用法) ​

csharp
#if SERVER
CommandBuilder.Create()
    .WithIndex(CommandIndex.Execute)
    .WithAbility(fireballLink)
    .WithTarget(enemyUnit)
    .AsSystem()
    .Execute(unit);
#endif

BoundCommandContext ​

csharp
#if SERVER
var ctx = unit.AsSystem(); // 或 unit.AsAI(), unit.AsUser()
ctx.CastAbility(fireballLink, target: enemyUnit);
ctx.AttackTarget(enemyUnit);
ctx.MoveTo(position);
ctx.Stop();
#endif

直接构建 Command ​

csharp
#if SERVER
var command = new Command
{
    AbilityLink = fireballLink,
    Index = CommandIndex.Execute,
    Type = ComponentTagEx.AbilityManager,
    Target = enemyUnit,
    Flag = CommandFlag.IsSystem,  // 或 IsUser, IsAI
};
var result = command.IssueOrder(unit);
#endif

攻击指令 vs 技能指令 ​

指令类型CommandIndexAbilityLink技能查找方式
攻击Attack可选自动查找 IsAttack=true 的技能
释放技能Execute必填使用指定技能
切换Toggle/TurnOn/TurnOff必填使用指定的切换技能

攻击指令查找逻辑:

  1. 如果指定了 AbilityLink,验证其 IsAttack 标志
  2. 否则遍历单位所有技能,找到第一个 IsAttack && IsValid && IsEnabled 的技能

运行时技能实例 ​

Ability 基类 ​

csharp
public partial class Ability
{
    public AbilityManager Manager { get; }        // 所属管理器
    public IGameLink<GameDataAbility> Link { get; } // 数编表链接
    public GameDataAbility Cache { get; }           // 数编表数据(通过此访问配置)
    public uint Level { get; set; }                 // 技能等级(服务端可写)
    public uint? LevelMax { get; }                  // 最大等级
    public bool IsGrantedByItem { get; }            // 是否由物品授予
    public Item? Item { get; }                      // 关联物品
    public bool Paused { get; }                     // 是否暂停
    public bool ShouldPersistDuringMorph { get; set; } // 变形时保留

    // 数据替换
    public virtual bool Replace(IGameLink<GameDataAbility> link);
}

技能的 IsActivated、IsEnabled、HasState() 等方法继承自 AttachedObject 基类。

AbilityActive(主动技能实例) ​

csharp
public abstract partial class AbilityActive : Ability
{
    public CooldownActive? Cooldown { get; }    // 冷却状态
    public CooldownCharge? Charge { get; }      // 充能状态
    public PhaseContext? PhaseInfo { get; }  // 多阶段信息
    public bool IsMultiPhase { get; }           // 是否多阶段

    public Cooldown? GetCooldown(CooldownType type);

    // 服务端专有
    #if SERVER
    public virtual void SetPhase(int phase);
    public virtual void SpendPhase();
    public virtual void AddPhase(int delta);
    public event Action? OnCooldownAffordable;  // 冷却完成事件
    public event Action? OnChargeAffordable;    // 充能完成事件
    #endif
}

AbilityExecute(执行技能实例) ​

csharp
public partial class AbilityExecute : AbilityActive
{
    public bool IsAttack { get; }            // 是否攻击技能
    public float Range { get; }              // 有效范围
    public bool AlwaysAcquireTarget { get; } // 是否总是获取目标
}

AbilityToggle(切换技能实例) ​

csharp
public partial class AbilityToggle : AbilityActive
{
    public bool ToggledOn { get; }  // 当前是否开启

    #if SERVER
    public bool Toggle();            // 智能切换(返回是否成功)
    public bool Toggle(bool on);     // 指定开关状态
    #endif
}

AbilityMorph(变形技能实例) ​

csharp
public partial class AbilityMorph : AbilityToggle
{
    // 开启状态时 IsPersistDuringMorph 始终为 true
    public override bool IsPersistDuringMorph { get; }
}

多阶段技能 ​

多阶段技能通过 AbilityPhaseSettings 配置,每个阶段可以替换技能的数据配置,实现蓄力等效果。

csharp
public class AbilityPhaseSetting
{
    public required IGameLink<GameDataAbility> Link { get; set; }  // 该阶段的数据
    public FuncTime? ResetDuration { get; set; }                   // 自动重置时间
    public bool AutoIncreasePhase { get; set; } = true;            // 施法后自动递进
}
csharp
// 三段斩配置
var slash1Link = new GameLink<GameDataAbilityExecute, GameDataAbility>("slash_1");
var slash2Link = new GameLink<GameDataAbilityExecute, GameDataAbility>("slash_2");
var slash3Link = new GameLink<GameDataAbilityExecute, GameDataAbility>("slash_3");

_ = new GameDataAbilityExecute(slash1Link)
{
    AbilityPhaseSettings = [
        new() { Link = slash1Link, ResetDuration = static (_) => TimeSpan.FromSeconds(2) },
        new() { Link = slash2Link, ResetDuration = static (_) => TimeSpan.FromSeconds(2) },
        new() { Link = slash3Link }, // 最后一段不需要 ResetDuration
    ],
    // ... 其他配置
};

运行时通过 PhaseInfo 访问当前阶段:

csharp
if (ability is AbilityActive active && active.IsMultiPhase)
{
    int phase = active.PhaseInfo.CurrentPhase;
}

动画集成 ​

技能动画通过 GameDataAbilityActive.Animation 配置。支持多个动画随机选择。

csharp
_ = new GameDataAbilityExecute(attackLink)
{
    Animation = [attackAnimation1Link, attackAnimation2Link],
    // ...
};

动画播放与施法阶段自动同步:

  • Preswing → 创建控制器并调用 Preswing()
  • Cast → 调用 Cast()
  • Channel → 调用 Channel()
  • Backswing → 动画 Backswing
  • Approach 回跳 → 停止本轮动画并清理阶段表现
  • Left → 动画停止

普通动画、动画元组和 Sequence 对阶段通知的响应不同,不能把动画时间轴直接等同于服务端施法阶段。三种控制器的完整映射见技能动画生命周期指南第 6 节。

StagedSpawns — 分阶段 Actor 生成 ​

StagedSpawns 控制技能执行过程中视觉效果的生命周期。

csharp
public class AbilityStageSpawn
{
    public required List<IGameLink<GameDataActor>> Spawns { get; set; }
    public AbilitySpawnStage SpawnStage { get; set; } = AbilitySpawnStage.Cast;
    public AbilitySpawnStage ClearStage { get; set; } = AbilitySpawnStage.Left;
}

阶段顺序:Preswing → Cast → Channel → Backswing → Left

csharp
StagedSpawns = [
    // 蓄力特效:前摇时出现,施法时消失
    new()
    {
        SpawnStage = AbilitySpawnStage.Preswing,
        ClearStage = AbilitySpawnStage.Cast,
        Spawns = [chargeEffectLink],
    },
    // 施法特效:施法时出现,技能结束时消失
    new()
    {
        SpawnStage = AbilitySpawnStage.Cast,
        ClearStage = AbilitySpawnStage.Left,
        Spawns = [castEffectLink, projectileLink],
    },
],

普通标记为 IsTransient 的 Actor 按自身生命周期结束,不受 ClearStage 控制。受阶段管理的非瞬态 Actor 在 Left 或重新接近的 Approach 阶段清理,再次 Preswing 时也会清理旧记录并重新创建。

Left 用于清理,不创建新的阶段 Actor。同一阶段先创建再清理,SpawnStage 与 ClearStage 相同时,非瞬态 Actor 会立刻被清除。手工创建或序列内部生成的对象应按各自生命周期管理。扇形预警与主效果的配合示例见阶段表现指南第 7 节。

被动技能 ​

不继承 GameDataAbilityActive 的技能即为被动技能,添加到单位后自动生效。

csharp
var auraLink = new GameLink<GameDataAbility, GameDataAbility>("mana_regen_aura");
_ = new GameDataAbility(auraLink)
{
    DisplayName = "法力恢复光环",
    TargetType = AbilityTargetType.None,
    PassivePeriod = static (_) => TimeSpan.FromSeconds(1),
    PassivePeriodicEffect = regenEffectLink,
    Modifications = [
        new UnitPropertyModification
        {
            Property = MyData.GameDataUnitProperty.ManaRegen,
            Value = static (_) => 2,
        },
    ],
};

被动技能的 ActivatedEffect 在技能附加且激活时执行,DeactivatedEffect 在技能禁用或移除时执行。

运行时操作:

csharp
#if SERVER
// 临时禁用被动技能
var ability = manager.Get(auraLink);
if (ability is not null)
{
    ability.AddState(AttachedObjectState.Disabled);

    await Game.Delay(TimeSpan.FromSeconds(5));

    if (ability.IsValid)
    {
        ability.RemoveState(AttachedObjectState.Disabled);
    }
}
#endif

常见错误与排查 ​

单位攻击自己 ​

原因:AcquireSettings.TargetingFilters 未配置。SmartAcquire 默认开启,选中最近目标(含自身)。

修复:

csharp
AcquireSettings = new()
{
    TargetingFilters = [new()
    {
        Required = [UnitRelationship.Enemy],
        Excluded = [UnitState.Dead, UnitState.Invulnerable],
    }],
},

效果不执行 / 对坐标造伤害 ​

原因:AbilityTargetType 为 Vector/Ground/None 时直接指向 EffectDamage。EffectDamage 需要单位目标。

修复:插入 EffectSearch 搜索范围内的单位,然后对搜索到的单位执行伤害。参见 效果链配方。

攻击指令无效 ​

原因:技能未设置 AbilityExecuteFlags.IsAttack = true,或技能被禁用/不可用。

修复:确保攻击技能配置了 AbilityExecuteFlags = new() { IsAttack = true }。

施法时间不生效 ​

原因:属性名写成了 CastTime,正确属性名是 Time。

修复:Time = new() { Preswing = ..., Cast = ..., Backswing = ... }

冷却不生效 ​

原因:直接在技能上设置了不存在的 Cooldown 属性。冷却通过 Cost 配置。

修复:先定义 GameDataCooldownActive 数编表,然后通过 Cost = new() { Cooldown = cdLink } 配置。

普攻间隔或攻速不符合预期 ​

原因:把普攻间隔配置到 Cost.Cooldown,或只在冷却上读取攻速属性。冷却不是最短攻击间隔,它不会压缩 Preswing、Cast、Channel 等施法阶段。

修复:需要精确控制普攻速度时,在 Time.NormalizedDuration 中配置目标攻击周期;普攻通常不配置冷却。只有设计上还需要额外的可用性限制时,才给普攻补 Cost.Cooldown。

相关文档 ​