共计 3229 个字符,预计需要花费 9 分钟才能阅读完成。
为什么需要重构技能系统?
在开发游戏或 SaaS 系统时,技能系统往往是业务逻辑最复杂的部分之一。我见过太多项目因为技能系统设计不当而陷入维护噩梦:

- 硬编码灾难 :每个新技能都要写一堆 if-else,代码迅速膨胀到数万行
- 效果耦合 :火球术和治疗术的代码相互引用,改一处崩十处
- 叠加冲突 :两个减速效果叠加后速度变成负数,角色开始倒着走
- 调试困难 :技能触发像侦探破案,要追踪十几层调用栈
架构选型:三大流派对比
1. 基于继承的方案(传统 OOP)
class Skill {
cooldown: number;
abstract cast(): void;}
class Fireball extends Skill {cast() {/* 火球逻辑 */}
}
优点 :符合直觉,适合简单系统
缺点 :多重继承导致钻石问题,新增效果要修改基类
2. 基于组件的方案(现代游戏常用)
interface EffectComponent {apply(target: Entity): void;
}
class Skill {components: EffectComponent[];
}
优点 :灵活组合,适合效果多变的卡牌游戏
缺点 :组件间通信复杂,性能开销较大
3. ECS 架构(极致性能向)
// 定义组件
class CooldownComponent {/*...*/}
// 定义系统
class SkillSystem {update(entities: Entity[]) {/*...*/}
}
优点 :缓存友好,适合 MMO 等大规模场景
缺点 :学习曲线陡峭,过度设计风险
核心实现:模块化技能框架
1. 效果抽象:乐高积木式设计
/**
* 技能效果基类
* @abstract
*/
abstract class SkillEffect {abstract apply(caster: Entity, target: Entity): void;
priority = 0; // 效果优先级
}
// 具体效果实现
class DamageEffect extends SkillEffect {constructor(public amount: number) {super(); }
apply(caster: Entity, target: Entity) {target.health -= this.amount;}
}
2. 条件判定:策略模式解耦
interface Condition {check(caster: Entity, target: Entity): boolean;
}
class RangeCondition implements Condition {constructor(private maxRange: number) {}
check(caster: Entity, target: Entity) {return distance(caster, target) <= this.maxRange;
}
}
3. 事件驱动:用消息总线处理连锁反应
class SkillEventBus {private listeners = new Map<string, Function[]>();
on(event: string, callback: Function) {if (!this.listeners.has(event)) {this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
emit(event: string, ...args: any[]) {this.listeners.get(event)?.forEach(fn => fn(...args));
}
}
// 使用示例
bus.on('SKILL_CAST', (skill) => {console.log(`${skill.name} 被释放了!`);
});
性能优化实战技巧
1. 对象池:应对高频技能
class EffectPool {private pool = new Map<Function, SkillEffect[]>();
get<T extends SkillEffect>(type: new () => T): T {if (!this.pool.has(type)) {return new type();
}
return this.pool.get(type)!.pop() || new type();
}
release(effect: SkillEffect) {
const type = effect.constructor;
if (!this.pool.has(type)) {this.pool.set(type, []);
}
this.pool.get(type)!.push(effect);
}
}
2. 条件短路:提前终止无效计算
class CompositeCondition implements Condition {
constructor(private conditions: Condition[],
private mode: 'AND' | 'OR'
) {}
check(caster: Entity, target: Entity) {for (const cond of this.conditions) {const result = cond.check(caster, target);
// OR 模式下遇到 true 立即返回
if (this.mode === 'OR' && result) return true;
// AND 模式下遇到 false 立即返回
if (this.mode === 'AND' && !result) return false;
}
return this.mode === 'AND';
}
}
3. 事件清理:防止内存泄漏
class Skill {private cleanup: Function[] = [];
on(event: string, callback: Function) {bus.on(event, callback);
this.cleanup.push(() => bus.off(event, callback));
}
dispose() {this.cleanup.forEach(fn => fn());
}
}
避坑指南:血泪教训总结
- ID 冲突问题 :
- 采用三级命名规范:
系统_类型_名称(如combat_skill_fireball) -
使用 UUID 生成器替代自增 ID
-
效果幂等性 :
- 为每个效果实例添加唯一标识
-
在效果管理器里记录已应用的效果 ID
-
网络同步方案 :
- 客户端预测 + 服务器校验
- 使用确定性锁步(Lockstep)算法
- 关键状态采用 CRC 校验
思考题:技能回滚系统设计
当我们需要支持战斗回放 / 撤销功能时,可以考虑:
- 命令模式 :将每个技能操作封装为可逆命令对象
- 状态快照 :使用备忘录模式保存关键帧状态
- 事件溯源 :通过重放事件日志重建任意时间点状态
interface ReversibleCommand {execute(): void;
undo(): void;}
class CastSkillCommand implements ReversibleCommand {
private targetHealthSnapshot?: number;
constructor(
private skill: Skill,
private caster: Entity,
private target: Entity
) {}
execute() {
this.targetHealthSnapshot = this.target.health;
this.skill.cast(this.caster, this.target);
}
undo() {if (this.targetHealthSnapshot !== undefined) {this.target.health = this.targetHealthSnapshot;}
}
}
写在最后
构建一个好的技能系统就像设计语言语法——需要平衡表达力与约束性。本文方案在《山海幻想》项目中验证,支持了 200+ 技能的无冲突组合。记住:没有银弹架构,选择最适合你团队认知水平和项目规模的方案才是王道。
正文完
发表至: 技术开发
近两天内
