共计 2840 个字符,预计需要花费 8 分钟才能阅读完成。
从游戏开发的坑说起
最近在重构一个 MOBA 游戏的技能系统时,我们遇到了诡异的状态同步问题:当英雄同时触发 ” 无敌 ”(superpower)和 ” 治疗 ”(skill)时,客户端偶尔会出现治疗效果被错误延续的 bug。经过排查发现,这是典型的架构设计问题——开发初期没有清晰区分瞬时行为 (skill) 和持久状态改变(superpower)。
// 反面教材:混合管理的类型定义
interface Ability {
name: string;
cooldown: number;
isPassive?: boolean; // 到底表示 skill 还是 superpower?duration?: number; // 与 isPassive 的关系模糊
}
概念的本质差异
- 计算机科学视角的定义
- Superpower:改变实体本质属性的持久性状态(如飞行、隐身),通常具有:
- 状态持续性(直到显式移除)
- 相互排斥性(不能同时隐身和显形)
- 层级依赖(飞行需要先有翅膀)
-
Skill:短暂触发的行为单元(如发射火球、瞬间治疗),特征包括:
- 瞬时执行(可能有前摇后摇)
- 可叠加性(多个技能可以同时冷却)
- 幂等操作(重复触发效果一致)
-
类型系统的守护
用 TypeScript 类型守卫实现编译时校验:
type Timestamp = number;
// 使用 discriminated union 明确区分
type Skill = {
kind: 'skill';
lastUsed: Timestamp;
cooldown: number;
execute: () => void;};
type Superpower = {
kind: 'superpower';
isActive: boolean;
prerequisites: Bitmask;
activate: () => void;
deactivate: () => void;};
function handleAbility(ability: Skill | Superpower) {if (ability.kind === 'skill') {
// 编译器会智能推断类型
const remainingCD = ability.cooldown - (Date.now() - ability.lastUsed);
// ...
} else {
// 类型安全地处理 superpower
if (ability.isActive) ability.deactivate();}
}
ECS 架构解决方案

-
组件设计
// C++ 示例 struct SkillComponent {std::unordered_map<SkillID, CooldownTimer> cooldowns;}; struct SuperpowerComponent { std::bitset<MAX_SUPERPOWERS> activePowers; std::array<float, MAX_SUPERPOWERS> remainingDurations; }; -
系统分离
- SkillSystem:处理冷却计时、输入响应
- SuperpowerSystem:管理状态切换、依赖校验
- InteractionSystem:处理两者间的交互(如禁用技能时自动取消相关 superpower)
实战代码示例
Unity 中实现冷却与永续并存:
// Superpower 的永续性实现
public class FlightPower : MonoBehaviour {
private bool _isActive;
void Update() {if (_isActive) {
// 每帧检测依赖项(如是否被沉默)if (!CheckPrerequisites()) {Deactivate();
}
}
}
public void Activate() {
_isActive = true;
GetComponent<Rigidbody>().useGravity = false;
// 不涉及持续时间逻辑
}
}
// Skill 的冷却实现
public class FireballSkill : MonoBehaviour {
private float _cooldownRemaining;
void Update() {_cooldownRemaining = Mathf.Max(0, _cooldownRemaining - Time.deltaTime);
}
public void Cast() {if (_cooldownRemaining <= 0) {Instantiate(fireballPrefab, transform.position, transform.rotation);
_cooldownRemaining = cooldownDuration;
}
}
}
位掩码校验示例:
// 检查 superpower 组合合法性
bool validateSuperpowerCombo(uint32_t currentPowers, uint32_t newPower) {
const uint32_t INCOMPATIBLE_FLIGHT = 0b1001;
const uint32_t REQUIRES_STRENGTH = 0b0110;
// 互斥检查
if ((newPower & INCOMPATIBLE_FLIGHT) == INCOMPATIBLE_FLIGHT) {return false;}
// 依赖检查
if ((newPower & REQUIRES_STRENGTH) && !(currentPowers & STRENGTH_POWER)) {return false;}
return true;
}
性能优化关键点
- Superpowers 状态同步
- 采用增量同步:仅传输发生变化的位掩码
- 使用压缩算法:对连续的 superpower 状态进行 RLE 编码
-
客户端预测:对已知持续时间的 superpower 提前进行本地模拟
-
Skill 对象池
public class SkillPool : MonoBehaviour {private Queue<GameObject> _pool = new Queue<GameObject>(); public GameObject GetSkillInstance() {if (_pool.Count > 0) {var instance = _pool.Dequeue(); instance.SetActive(true); return instance; } return Instantiate(prefab); } public void ReturnToPool(GameObject instance) {instance.SetActive(false); _pool.Enqueue(instance); } }
开放性问题思考
当 superpowers 需要组合生效时(例如 ” 火焰附魔 ” 需要同时激活 ” 火元素掌握 ” 和 ” 武器精通 ”),可以考虑:
- 有向无环图 (DAG) 表示依赖关系
- 拓扑排序确定激活顺序
- 运行时依赖解析器维护激活状态
- 自动回滚机制:当某个依赖失效时,自动取消依赖它的所有 superpowers
这种设计在 MMO 游戏的职业天赋系统中尤其重要,你觉得还有哪些更好的实现方案?
正文完
