主题
技能系统(Ability System)
技能系统管理单位的所有能力,包括执行技能(主动/攻击)、切换技能、变形技能和被动技能。它与指令系统、效果系统、消耗系统深度集成。
完整施法流程、目标快照、重新接近和动画表现的配合,见指令、施法与技能动画的完整生命周期。
目录
- 类继承关系
- 数编表配置(GameData)
- AbilityTargetType 与效果链
- AcquireSettings — 目标获取配置
- SpellCastTime — 施法时间
- Cost — 消耗配置
- SpellInterruptConfig — 中断配置
- AbilityFlags / AbilityExecuteFlags
- AbilityManager — 技能管理器
- 指令系统集成
- 运行时技能实例
- 多阶段技能
- 动画集成
- StagedSpawns — 分阶段 Actor 生成
- 被动技能
- 常见错误与排查
类继承关系
数编表(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 含义 | 筛选合法的目标 | 筛选合法的施法者(谁可以来交互) |
| 接近行为 | 施法者接近目标 | 施法者接近技能宿主 |
运作方式:
- 单位 B(NPC)拥有交互技能,
TargetingFilters定义谁可以与 B 交互 - 单位 A 对 B 的交互技能下达指令 → A 走向 B
- 到达后,系统检查 A 是否通过 B 技能的
TargetingFilters(角色互换:filter 以 B 为视角检查 A) - 通过后,效果以 B 为上下文执行
典型用途:可拾取物品、NPC 对话、机关开关、商店交互等。
效果链配方
GameDataEffectDamage 等面向单位的效果只能作用于单位目标。如果技能的主目标是坐标点(Vector/Ground/None)则显然无法造成伤害,必须先用 GameDataEffectSearch 搜索范围内的单位:
| AbilityTargetType | 主目标性质 | 典型效果链 | 适用场景 |
|---|---|---|---|
Unit | 单位实体 | Ability → EffectDamage | 单体攻击 |
UnitOrGround | 单位或地面坐标点 | 取决于实际目标:单位 → EffectDamage;坐标 → EffectSearch → EffectDamage | 灵活技能(可点人也可点地) |
Vector | 施法者前方固定距离坐标点 | Ability → EffectSearch(Cone) → EffectDamage | 扇形挥砍、发波 |
Ground | 地面坐标点 | Ability → EffectSearch(Circle) → EffectDamage | AOE 技能 |
None | 施法者自身 | Ability → EffectSearch(Circle, center=Caster) → EffectDamage | 新星、战吼 |
Interact | 技能宿主实体 | Ability → EffectDamage 或自定义效果 | 拾取、NPC 交互 |
SmartAcquire 传入单位时的目标转换
当 SmartAcquire 自动选取到一个单位作为目标、但技能的 AbilityTargetType 无法直接接受单位目标时,系统会进行转换:
| AbilityTargetType | SmartAcquire 选到单位后的转换 |
|---|---|
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 自动选取,还在特定技能类型中充当指令层的目标校验:
| AbilityTargetType | SmartAcquire 自动选取 | 手动目标校验 |
|---|---|---|
Unit | 在范围内搜索通过过滤的单位 | 手动指定的目标必须通过过滤,否则指令被拒绝(CannotTargetThat) |
Interact | 在范围内搜索通过过滤的单位 | 校验施法者(而非目标)是否通过过滤(角色反转) |
Ground / Vector / None | 在范围内搜索通过过滤的单位 | 不参与手动目标校验 |
> 注意区分指令层与执行层的过滤:TargetingFilters 控制"技能能否对某个目标下达施法指令"(指令层)。AOE 技能实际命中哪些单位,由效果树中 GameDataEffectSearch.SearchFilters 控制(执行层)。两者独立,互不影响。
SmartAcquire — 自动目标选取
当施法指令未指定目标时(如 AI 发出的攻击指令、或 AlwaysAcquireTarget 的 Vector/None 技能),系统会自动在范围内搜索合法目标:
- 以技能
Range(或AcquireRange,若配置)为搜索半径 - 用
TargetingFilters过滤候选单位 - 按
AcquireSorting排序(未配置则按距离) - 选择第一个通过的单位作为目标
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();
#endifCommandBuilder(高级用法)
csharp
#if SERVER
CommandBuilder.Create()
.WithIndex(CommandIndex.Execute)
.WithAbility(fireballLink)
.WithTarget(enemyUnit)
.AsSystem()
.Execute(unit);
#endifBoundCommandContext
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 技能指令
| 指令类型 | CommandIndex | AbilityLink | 技能查找方式 |
|---|---|---|---|
| 攻击 | Attack | 可选 | 自动查找 IsAttack=true 的技能 |
| 释放技能 | Execute | 必填 | 使用指定技能 |
| 切换 | Toggle/TurnOn/TurnOff | 必填 | 使用指定的切换技能 |
攻击指令查找逻辑:
- 如果指定了
AbilityLink,验证其IsAttack标志 - 否则遍历单位所有技能,找到第一个
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。