某竞技项目 · 技能系统占 39%+ 运行时开销的优化全纪录
一、为什么要做技能重构?
技能系统是竞技游戏的核心战斗模块,每帧都在高频运转。当它成为性能瓶颈时,影响链路非常长 — 从客户端帧率到服务器承载、从用户体验到运营成本。本次重构的驱动力来自客户端和服务器两个方向。
1.1 客户端:机型下探与卡顿分析
项目 M2 阶段的目标是将最低适配机型下探到骁龙 450 级别,长期目标是 430。然而优化初期的实机数据令人担忧:
| 时间节点 | 设备(骁龙) | 帧率 | EarlyTick 开销 | 备注 |
|---|---|---|---|---|
| 7.27 周版本 | 450 | 12.6 帧 | — | 几乎不可玩 |
| 8.16 周版本 | 450 | 20 帧 | 22.4ms | Python 相关为大头 |
| 8.16 周版本 | 660 | 30 帧 | 9.9ms | 基准参考 |
帧率只是表象。通过 Profiler 抓取,我们进一步分析了帧率波动(卡顿)情况:
结合 Profile 数据,按严重程度排列,卡顿来源如下:
性能绝对值的组成也很清晰:
- Polaris 被动技能(范围检测、图标显示判定等)— 常驻高频 tick,数量最多
- Polaris 主动技能(震慑脉冲、近战搏击等)— 单次开销大,节点图复杂
1.2 服务器:成本对比与压力
与同品类成熟产品对比(取 7 月线上数据),服务器资源利用率差距明显:
| 指标 | 本项目 | 对标产品 | 差距 |
|---|---|---|---|
| 单核 CPU 服务玩家数 | 12 人 | 50 人 | 4.2x |
| 单核 CPU 战斗局数 | 2 局 | 10 局 | 5x |
| 成本持平最低要求 | 本项目至少需要达到 8 局/核心 才能与对标产品持平 | ||
技能系统占了服务器运行开销的大头。优化技能 = 直接降低每局战斗的 CPU 消耗 = 单核承载更多局数 = 服务器成本下降。这不是"锦上添花",而是生存线。
1.3 关键结论:技能开销占比 39%+
二、技能重构之最佳实践
优化分为两个维度:优化点 1 — 去掉 Polaris 框架开销(架构层面),优化点 2 — 重构技能逻辑本身(业务层面)。两者叠加才能取得最大收益。
2.1 优化点 1:去 Polaris — 方案对比与架构演进
Polaris 是项目现有的可视化技能编辑框架,策划通过拖拽节点图(.mth 文件)来配置技能逻辑。它对策划非常友好,但运行时存在显著的框架调度开销和节点入口调用开销。
四种替代方案评估
| 方案 | 实现方式 | 优化点 | 迁移成本 | 可控性 |
|---|---|---|---|---|
| Polaris 原版 | 节点图驱动,Python 节点 | — 基准 | — | 低 |
| 手写 Python | 手写代码调用 System 接口 | 去掉框架 + 节点开销 | 中 | 高 |
| Polaris 自动展开 | 工具将节点图编译为 Python | 去掉节点调度算法 | 低 | 中 |
| Polaris C++ 版 | 框架与节点均用 C++ 实现 | 语言层面提速 | 高 | 低 |
性能对比
最终选择手写 Python 方案:性能提升最显著,代码完全可控,且能渐进式迁移 — 新旧方案可以共存,逐步替换而不影响整体进度。
2.2 两套手写体系:hybrid_skill_system & skill_system
根据技能的生命周期和运行特征,手写方案分为两套体系:
hybrid_skill_system 需要处理的 .mth 文件总计 439 个:
| 类别 | 数量 | 典型代表 |
|---|---|---|
| 主动技能 | 148 | 传送井盖、替身徽、震慑脉冲 |
| 技能行为 | 43 | change_pose 等 |
| 状态机 | 70 | — |
| 被动技能 | 178 | 高处下落踉跄等 |
2.3 核心设计原则与代码结构
原则一:一对一映射
一个 .mth 文件对应一个 .py 文件,命名规范清晰:
// Polaris 版本 change_pose.mth // 手写 Python 版本 hybrid_change_pose.py
原则二:实例化运行
技能从"跑一个节点图"变为"跑一个 Class 实例"。每个技能实例拥有自己的状态和生命周期:
原则三:System 接口组合
大部分原 Polaris 节点的逻辑已经封装在各种 System 接口中。手写版本直接调用这些接口,跳过 Polaris 的调度层,实现相同功能但开销更低。
原则四:组件式代码复用
对于跨技能的通用逻辑,使用 skill_logic_component 实现组件式复用,分为两类:
| 复用类型 | 说明 | 适用场景 |
|---|---|---|
| 持续性组件 | skill_logic_component 对象,挂载到技能实例上持续运行 | 范围检测、状态维护、周期性效果 |
| 瞬发函数 | 可复用的一次性调用函数 | 伤害计算、Buff 添加、动画播放 |
2.4 优化点 2:逻辑重构方法论
"去 Polaris"解决的是框架层面的开销,但很多技能的逻辑本身也存在不必要的浪费。重构思路分三步:
常见的重构手段包括:
- 降频 — 减少 tick 频率(如 0.4s → 2s),利用数学关系(公约数/公倍数)合并计算
- 动态降频 — 根据状态自适应调整频率(满血时 30s tick,回血中 1s tick)
- 事件驱动替代轮询 — 用事件/回调替代周期性检测(如高处下落)
- 距离分级 — 根据距离动态调整更新频率(近处高频,远处低频)
- 从玩家级提升到 Space 级 — 将"每人挂一个被动"改为"Space 挂一个 System",消除重复计算
三、案例讲解
以下 6 个案例覆盖了不同的技能类型和优化手段,每个都附有前后对比和性能数据。
案例 1:体力值回复
5 倍性能提升需求:部分技能使用需要消耗体力值,不足则不能释放。翻越消耗 25,爬管道消耗 50,体力值上限 100。
每 0.4 秒 tick 一次,每次恢复 1 点体力。
每个攻方角色挂一个 attacker_physical_strength.mth 被动。
体力值回复提升为 Space 级游戏机制。
25 和 50 的 GCD=5,每 2 秒恢复 5 点。
方法 1:GCD(25,50)=5 → 每 2 秒恢复 5 点 → 5 倍性能
方法 2:LCM(25,50)=25 → 每 10 秒恢复 25 点 → 25 倍性能
考虑到 UI 体力条的平滑插值实现成本较大,选择了方法 1 作为平衡。
案例 2:高处下落踉跄
4 倍 + 事件驱动需求反推:从高处下落后,僵直 1 秒并播放踉跄动画。
Polaris 被动,每 0.3 秒 tick 一次。
每次做射线检测 + 高度处理。
单次开销 39.89ms。
第一步:hybrid_skill_system 重写 → 9.78ms(4x)
第二步:与策划确认意图,去掉 tick,改为翻越高度检测事件驱动。
案例 3:挂机暴露(AFK 机制)
N 个被动 → 1 个 System需求:攻方 3 分钟内未执行关键行为(开点、占点、猜密码),将被持续标红,直到恢复行为。
- 每个攻方挂被动
attacker_not_afk.mth,监听行为,触发后给所有守方添加"攻方未挂机" buff - 每个守方挂被动
defender_catch_afk.mth,每 1 秒 tick,检查 buff → 给攻方添加 1 秒标红 buff - N 攻 + M 守 = N+M 个被动同时运行
- AFK 作为游戏机制挂在 Space 上,1 个 System 管理全部玩家
- 添加 3 分钟定时器,触发关键行为则重置
- 触发 AFK 后:添加 1 小时标红 buff,恢复行为后清除
案例 4:呼吸回血
动态降频 · 满血30x需求:不满血且未被眩晕时触发回血。30 秒内受击则暂停回血。
Polaris 被动,每 1 秒 tick。
无论满血与否均以固定频率检查。
skill_system Space 级。
正在回血 → 1s tick
满血 → 30s 后再 tick
案例 5:占点人描边
11 倍 · 单次 3ms→0.26ms需求:
- 守方:所有占点人始终描边(开点红色 / 未开点绿色)
- 攻方:40 米内显示描边(颜色规则同上),40 米外不显示
每 1 秒 tick 检查所有 6 个占点人状态。
单次开销 3ms。
不区分距离,统一频率。
Space System,单次开销 0.26ms。
守方:property 变更触发。
攻方:6 人独立定时器 + 距离分级降频。
距离分级策略
| 距离区间 | Tick 频率 | 设计意图 |
|---|---|---|
| < 40 米 | 按距离 [1, 7] 秒线性插值 | 可见区域,越近越需要精确 |
| 40 ~ 60 米 | 1.5 秒/次 | 边界区域,准备进入/退出可见 |
| > 60 米 | 10 秒/次 | 远处,低优先级 |
| 传送技能触发 | 立刻 tick | 位置突变,需要即时响应 |
总体期望频率从 每秒 1 次 → 平均每 6 秒 1 次,结合单次开销 11 倍降低,综合性能提升巨大。
案例 6:主动技能统一迁移
去 Polaris 框架开销所有主动技能统一采用 hybrid_skill_system 实现,从节点图驱动转为 Class 实例化运行。典型案例:
| 技能名称 | 原文件 | 迁移方式 |
|---|---|---|
| 传送井盖 | portal_.mth | hybrid_skill_system 重写 |
| 替身徽 | substitute_badge.mth | hybrid_skill_system 重写 |
| 门锁干扰器 | lock_door.mth | hybrid_skill_system 重写 |
| 震慑脉冲 | charge_hit_.mth | hybrid_skill_system 重写 |
主动技能的优化主要来自去除 Polaris 框架层的调度开销,逻辑本身变化不大,属于批量机械迁移,但收益稳定且确定性高。
案例总览
| 案例 | 优化手段 | 核心技巧 | 性能收益 |
|---|---|---|---|
| 体力值回复 | skill_system + 重构 | GCD 合并降频 | 5x |
| 高处下落踉跄 | hybrid + 重构 | Tick → 事件驱动 | 4x+ |
| AFK 机制 | skill_system + 重构 | N 被动 → 1 System | 架构级优化 |
| 呼吸回血 | skill_system + 重构 | 动态调频 | 满血 30x |
| 占点人描边 | skill_system + 重构 | 距离分级降频 | 11x |
| 主动技能 | hybrid_skill_system | 去 Polaris 框架 | ~4x |
四、总结与展望
优化方法矩阵
hybrid_skill_system
适用:主动技能、非常驻型被动/机制
思路:Class 实例化运行 + 时间线驱动 + skill_logic_component 组件复用
特点:一个 .mth 对应一个 .py,与 Polaris 共存,逐步迁移
skill_system
适用:常驻型被动 / 全局游戏机制
思路:Space 级 System + 事件驱动 / 动态降频 Tick
特点:将"每人挂被动"提升为"Space 统一管理",消除重复计算
重构三原则
- 理解策划需求与当前实现 反推真正意图,而非照搬已有实现方案。很多性能问题源于"用高成本手段实现了一个低要求的需求"。
- 最小开销实现需求 降频、事件驱动、距离分级 — 每一步都在减少无效计算。能用定时器就不 tick,能用事件就不轮询。
- 部分实现可与策划协商 允许表现层的微小调整(如体力恢复粒度),换取数量级的性能提升。这不是偷工减料,而是工程权衡。
可参考案例编号
#31778 体力值回复重构 · #31746 呼吸回血重构 · #31745 AFK 机制重构 · #32213 门锁干扰器 · #32092 鹰眼主动 · #32201 替身徽主动 · #31867 change_pose