You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/docs/性能分析-战斗单位成本与设计缺陷.md

6.7 KiB

战斗单位成本:设计缺陷与不改美术的优化

2026-09-17。只对照玩法需求与实现。同屏 100–200 怪是玩法规模,不是根因;这个规模下仍发热卡顿,说明单只单位的运行时模型过重。

需求侧:1v1 对抗、双路出怪、神将/字块部署、Spine 皮、受击死亡表现。这些都不要求「每只小怪 = 完整物理刚体 + REALTIME 骨骼 + 独立阴影节点 + 每逻辑步 setWorldPosition」。


1. 单只小怪实际挂了什么

实现里一只行军小怪不是一个 Sprite,而是一棵完整战斗对象:

部件 代码 每帧是否干活
根节点 + UITransform monsterTemplate 被 setWorldPosition 脏标记
RigidBody2D Kinematic,allowSleep=false,enabledContactListener=true Monster.enableMonsterPhysicsComponents 进战即开整个 Box2D,200 个刚体永不休眠
BoxCollider2D sensor 同上 物理岛参与;命中主路径并不走 Contact
sp.Skeleton 走路循环 presenter 绑皮后 playWalk 默认 REALTIME,从未对小怪切 cache
脚下阴影:另一棵节点,每 tick 写 worldPosition syncAllMonsterShadows 与怪同步,独立合批
血条预制 + Sorting2D + MonsterBlood.update Monster.ensureBloodBar 引擎每帧调 update;满血虽隐藏,组件仍在
Monster.update 加速特效朝向 / 攻墙空函数 200 个 Component.update
可选 7 类状态 Spine(冰/火/电/晕…) 全部强制 REALTIME 群控时再翻倍

同屏 200 只 ≈ 200 套 REALTIME Spine + 200 刚体 + 200 阴影 + 400 个 update 回调。再加双阵营字块/神将(格子 Spine 也被设成 REALTIME)。这不是「200 个单位」,是「200 个完整角色」。


2. 为什么这是设计问题(不是数量问题)

小游戏里 200 个同材质、同走路循环的单位,常规做法是:

  • 骨骼用 SHARED_CACHE / PRIVATE_CACHE(走路循环可预烘焙)
  • 位移只在渲染帧写一次节点
  • 命中走逻辑空间网格,不开 2D 物理世界
  • 排序用 Sorting2D.sortingOrder,禁止每帧 setSiblingIndex 打散合批
  • 小怪阴影/血条按需,不为每只开独立 tick

当前实现反着来:

  1. 表现绑在逻辑固定步长上
    BattleSession._tickOnce 每步都 onCombatTick → 全量 setWorldPosition + 阴影。掉帧时最多追 5 步,一帧写 5 次变换。逻辑 30Hz 够用,表现被乘上去。

  2. 物理世界是遗留物
    SkillCollisionHit 自己在 AFTER_UPDATE 里对所有怪做 AABB(4 角 × worldMatrix)。文件里写明 Contact2D 不可靠。逻辑层已有 SpatialGrid + ProjectileSystem。刚体 + contactListener 仍给每只怪打开。命中算了两遍,物理那遍几乎不产出。

  3. REALTIME 被当成默认、cache 被关掉
    状态特效、字块、陷阱、大招多处 setAnimationCacheMode(REALTIME)。MultSpine.SpineBatch.enable 默认 false。同皮小怪无法合批,DrawCall 随数量线性涨。

  4. Y 排序用 sibling
    sortMonstersByY 一旦乱序就对全部怪 setSiblingIndex。合批被打散,GPU 更差。

  5. 索敌排序在 comparator 里走路径
    _compareThreatPriority → estimatePathLength 沿剩余格 sqrt。Array.sort 会反复调 comparator,无缓存。塔多(玩家+AI)时尖峰明显,但是第二档,主墙仍是 Spine+物理+变换。

  6. 双世界
    ECS 已有 worldX/Y,Cocos 再镜像一份。每只怪每步 getComponent×N + worldPosition getter(可能重算矩阵)+ 再写回。


3. 不改美术、能大幅提升的手段

按预期收益排序。都不改 png/atlas/prefab 贴图。

A. 行军小怪 Spine 切缓存(最大)

  • 走路循环、同 skeletonData、无网格变形需求:SHARED_CACHE(阵营皮不同则按皮肤分两套 cache,或 PRIVATE_CACHE)
  • Boss / 需要闪白、换皮、IK 的保持 REALTIME
  • 状态特效(冰火电)同样走 SHARED_CACHE
  • 受击闪白:cache 模式下改用节点 color / 短时 overlay,不要为闪白把全场打回 REALTIME

预期:小怪骨骼 CPU 降一个数量级;这是 200 只同屏仍卡的主开关。

B. 战斗关掉 2D 物理

  • 进战不要 PhysicsSystem2D.enable = true(或只给必须做物理的机制开)
  • 怪不挂 RigidBody2D,不 enabledContactListener
  • 碰撞盒仅作尺寸数据:逻辑命中用 SpatialGrid;表现扫框用逻辑半径或缓存 AABB,禁止每飞弹全场 worldMatrix

预期:省掉 200 刚体 × 60Hz Box2D + 移节点时的物理同步。发热和卡顿都会掉。

C. 表现与逻辑解耦

  • _tickOnce 只跑 ECS;onCombatTick 每个渲染帧最多 1 次(多步追赶时只同步最后一步)
  • 节点坐标用当前逻辑位,不要 5 次 setWorldPosition
  • 2 倍速只加速逻辑时钟,Spine timeScale 可低于逻辑倍速(观感略快即可)

D. 禁止为排序打散合批

  • 用 Sorting2D.sortingOrder = f(-y) 或分桶,替代全量 setSiblingIndex
  • 降频到 10–15Hz;小怪层与血条层固定 layer,不要每只血条抢 order 打散皮肤合批

E. 拆掉每只怪的引擎 update

  • useMxMovement 后 Monster.update 可关(攻墙逻辑已空)
  • MonsterBlood 仅在滞后条运动时 enabled=true
  • 阴影挂到怪脚下作为子节点,删 syncAllMonsterShadows 的世界坐标镜像

F. 索敌路径长度缓存

  • MovementComp 在 pathIndex 变化时更新 remainPathPx,sort 只读标量
  • 不要在 comparator 里 estimatePathLength

G. 其它仍很值的

项 做法
离屏停更 LOD 用摄像机视口,不是 monsterRoot 原点;进屏再播
合批 同 atlas 小怪 enableBatch=true;评估打开 SpineBatch.enable
受击特效 保持爆发上限;AOE 合并成片,不要每只一只 REALTIME 受击 Spine
战斗帧率 逻辑 30Hz 已固定;渲染 30/45 对小游戏足够,比「200 只 REALTIME@60」更合理
iOS enableIOSHighPerformanceModePlus 关掉或仅高档
AI AiBattleBridge.tick 降到 5–10Hz,部署/刷新不需要 60

4. 不该再当成主因的

  • 再砍同屏数量:玩法就是这个密度;先把单只成本降下来。
  • 改压缩纹理 / 拆 atlas:这次范围外;A–C 不依赖美术。

5. 建议落地顺序

  1. 小怪走路 SHARED/PRIVATE_CACHE(先一种皮肤试点,看闪白/换皮)
  2. 战斗禁用 Box2D + 去掉怪刚体
  3. 表现同步每渲染帧 1 次
  4. Y 排序改 Sorting2D,停 sibling 洗牌
  5. 关掉空 update、阴影改子节点、remainPath 缓存

验收:抖音 Release,同屏 ~200、2 倍速 10 分钟。看帧时和发热,不要用编辑器。