系列定位:Unity 老版本兼容(.NET 3.5 / C# 4)+ 项目驱动。
本章目标:把“硬编码技能逻辑”升级为“配置 + 执行引擎”的技能系统,为后续 Unity 战斗章节做核心基建。
学习目标
完成本章后,你应该能做到:
- 设计技能配置模型(伤害、冷却、施法时间、消耗、目标类型)。
- 实现技能执行管线(请求 -> 校验 -> 施法 -> 生效 -> 收尾)。
- 将技能系统与命令系统、FSM、Scheduler、EventBus 对齐。
- 对每次施法输出可追踪日志,支撑回放与线上排障。
问题背景
很多项目初期会这样写技能:
if (Input.GetKeyDown(KeyCode.Q))
{
if (mp >= 30)
{
mp -= 30;
target.Hp -= 120;
qCooldown = 8f;
}
}
短期可用,但会快速失控:
- 技能参数散落代码中,策划改数值要改程序。
- 冷却、蓝耗、距离、前摇等规则重复实现,Bug 高发。
- 日志缺失,无法回答“为什么这次没放出来”。
- 复用困难,AI 与玩家技能逻辑分叉。
核心思路:技能行为固定在引擎,技能差异写在配置。
技能系统最小架构
1. 配置定义(静态)
public sealed class SkillConfig
{
public int Id;
public string Name;
public float CooldownSeconds;
public float CastTimeSeconds;
public int ManaCost;
public float Range;
public int Damage;
public SkillTargetType TargetType;
public string EffectKey;
}
public enum SkillTargetType
{
Self = 0,
EnemySingle = 1,
EnemyArea = 2
}
2. 运行态(动态)
public sealed class SkillRuntime
{
public readonly SkillConfig Config;
public float CooldownRemain;
public SkillRuntime(SkillConfig config)
{
Config = config;
CooldownRemain = 0f;
}
}
3. 施法请求
public struct CastRequest
{
public int CasterEntityId;
public int SkillId;
public int TargetEntityId;
public float RequestTime;
}
项目驱动:做一个可上线的技能引擎
目标场景:角色有 3 个技能(火球、冲刺、护盾),同一套系统服务玩家与 AI。
第一步:技能仓库(配置加载)
public sealed class SkillRepository
{
private readonly Dictionary<int, SkillConfig> _configs = new Dictionary<int, SkillConfig>(64);
public void Add(SkillConfig config)
{
if (config == null)
{
throw new ArgumentNullException("config");
}
if (_configs.ContainsKey(config.Id))
{
throw new InvalidOperationException("Duplicate skill id=" + config.Id);
}
_configs.Add(config.Id, config);
}
public SkillConfig Get(int skillId)
{
SkillConfig cfg;
_configs.TryGetValue(skillId, out cfg);
return cfg;
}
}
第二步:施法校验器
public sealed class CastValidator
{
public bool Validate(Actor caster, Actor target, SkillRuntime runtime, out string reason)
{
if (caster == null)
{
reason = "caster_null";
return false;
}
if (runtime == null)
{
reason = "skill_not_found";
return false;
}
if (caster.Hp <= 0f)
{
reason = "caster_dead";
return false;
}
if (runtime.CooldownRemain > 0f)
{
reason = "skill_cooldown";
return false;
}
if (caster.Mana < runtime.Config.ManaCost)
{
reason = "mana_not_enough";
return false;
}
if (runtime.Config.TargetType == SkillTargetType.EnemySingle)
{
if (target == null)
{
reason = "target_null";
return false;
}
var distance = caster.DistanceTo(target);
if (distance > runtime.Config.Range)
{
reason = "target_out_of_range";
return false;
}
}
reason = "ok";
return true;
}
}
第三步:执行器(施法管线)
public sealed class SkillExecutor
{
private readonly Scheduler _scheduler;
private readonly EventBus _eventBus;
public SkillExecutor(Scheduler scheduler, EventBus eventBus)
{
_scheduler = scheduler;
_eventBus = eventBus;
}
public void Execute(Actor caster, Actor target, SkillRuntime runtime, float now)
{
var cfg = runtime.Config;
caster.Mana -= cfg.ManaCost;
runtime.CooldownRemain = cfg.CooldownSeconds;
FoundationLog.Info("Skill", "CastStart skill=" + cfg.Name + " caster=" + caster.Id + " t=" + now);
_eventBus.Publish("SkillCastStart", cfg.Id);
if (cfg.CastTimeSeconds <= 0f)
{
ApplyEffect(caster, target, runtime);
return;
}
_scheduler.Delay(now, cfg.CastTimeSeconds, delegate
{
ApplyEffect(caster, target, runtime);
});
}
private void ApplyEffect(Actor caster, Actor target, SkillRuntime runtime)
{
var cfg = runtime.Config;
if (cfg.TargetType == SkillTargetType.EnemySingle && target != null)
{
target.Hp -= cfg.Damage;
}
else if (cfg.TargetType == SkillTargetType.Self)
{
caster.Hp += cfg.Damage;
if (caster.Hp > caster.MaxHp)
{
caster.Hp = caster.MaxHp;
}
}
FoundationLog.Info("Skill", "CastApply skill=" + cfg.Name + " effect=" + cfg.EffectKey);
_eventBus.Publish("SkillCastApply", cfg.Id);
}
}
第四步:技能组件(角色侧入口)
public sealed class SkillComponent
{
private readonly Dictionary<int, SkillRuntime> _skills = new Dictionary<int, SkillRuntime>(8);
private readonly CastValidator _validator;
private readonly SkillExecutor _executor;
public SkillComponent(CastValidator validator, SkillExecutor executor)
{
_validator = validator;
_executor = executor;
}
public void AddSkill(SkillConfig config)
{
_skills[config.Id] = new SkillRuntime(config);
}
public bool TryCast(Actor caster, Actor target, int skillId, float now, out string reason)
{
SkillRuntime runtime;
_skills.TryGetValue(skillId, out runtime);
if (!_validator.Validate(caster, target, runtime, out reason))
{
FoundationLog.Warn("Skill", "CastReject skillId=" + skillId + " reason=" + reason);
return false;
}
_executor.Execute(caster, target, runtime, now);
reason = "ok";
return true;
}
public void Tick(float dt)
{
foreach (var kv in _skills)
{
var rt = kv.Value;
if (rt.CooldownRemain > 0f)
{
rt.CooldownRemain -= dt;
if (rt.CooldownRemain < 0f)
{
rt.CooldownRemain = 0f;
}
}
}
}
}
与命令系统 / FSM 串联
1. 命令触发技能
玩家按键不再直接改数值,而是投递 CastSkillCommand:
public sealed class CastSkillCommand : ICommand
{
public int Sequence { get; set; }
public float Timestamp { get; set; }
public string Name { get { return "CastSkill"; } }
public int SkillId;
public int TargetId;
public bool Execute(CommandContext context)
{
var world = (BattleWorld)context.Host;
string reason;
return world.PlayerSkills.TryCast(world.Player, world.GetActor(TargetId), SkillId, Timestamp, out reason);
}
public void Reset()
{
Sequence = 0;
Timestamp = 0f;
SkillId = 0;
TargetId = 0;
}
}
2. FSM 控制“是否可施法”
例如:DeadState、HitState 禁止主动施法;AttackState 可按战术触发指定技能。
这样能保证“行为约束”在状态层,“数值规则”在技能层,职责不冲突。
配置示例(可给策划维护)
[
{
"Id": 1001,
"Name": "FireBall",
"CooldownSeconds": 6.0,
"CastTimeSeconds": 0.4,
"ManaCost": 25,
"Range": 12.0,
"Damage": 140,
"TargetType": 1,
"EffectKey": "fx_fireball"
},
{
"Id": 1002,
"Name": "Dash",
"CooldownSeconds": 4.0,
"CastTimeSeconds": 0.0,
"ManaCost": 10,
"Range": 0.0,
"Damage": 0,
"TargetType": 0,
"EffectKey": "fx_dash"
},
{
"Id": 1003,
"Name": "Shield",
"CooldownSeconds": 12.0,
"CastTimeSeconds": 0.2,
"ManaCost": 30,
"Range": 0.0,
"Damage": 80,
"TargetType": 0,
"EffectKey": "fx_shield"
}
]
验收标准
至少达到以下结果:
- 新增一个技能只改配置,不改技能引擎代码。
- 冷却、蓝耗、距离校验全部统一走
CastValidator。 - 技能触发可由玩家命令与 AI 行为共用。
- 日志能完整追踪一次施法生命周期:
CastStart -> CastApply。
常见坑
坑 1:把“技能效果逻辑”硬塞进同一个大 switch
短期快,长期不可维护。按效果类型拆分处理器(后续可升级策略模式)。
坑 2:冷却只在客户端倒计时,不在逻辑层校验
会出现 UI 显示可用但逻辑不可用。必须以逻辑层为准。
坑 3:施法中断没有定义
如果角色死亡、眩晕、位移打断时没有策略,线上会出现“技能吞没”与“资源扣了没效果”。
本月作业
实现“技能中断系统”并接入日志:
- 新增中断原因:死亡、控制、超距。
- 施法中断时回滚策略可配置(返还蓝耗/不返还)。
- 输出
CastInterrupted事件与结构化日志。
完成后,你将拥有可持续扩展的技能主干。下一章进入“Buff/效果栈系统”,把持续伤害、加速、护盾、减益统一管理。