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
当前实现反着来:
-
表现绑在逻辑固定步长上
BattleSession._tickOnce每步都onCombatTick→ 全量setWorldPosition+ 阴影。掉帧时最多追 5 步,一帧写 5 次变换。逻辑 30Hz 够用,表现被乘上去。 -
物理世界是遗留物
SkillCollisionHit自己在AFTER_UPDATE里对所有怪做 AABB(4 角 × worldMatrix)。文件里写明 Contact2D 不可靠。逻辑层已有SpatialGrid+ProjectileSystem。刚体 + contactListener 仍给每只怪打开。命中算了两遍,物理那遍几乎不产出。 -
REALTIME 被当成默认、cache 被关掉
状态特效、字块、陷阱、大招多处setAnimationCacheMode(REALTIME)。MultSpine.SpineBatch.enable默认 false。同皮小怪无法合批,DrawCall 随数量线性涨。 -
Y 排序用 sibling
sortMonstersByY一旦乱序就对全部怪setSiblingIndex。合批被打散,GPU 更差。 -
索敌排序在 comparator 里走路径
_compareThreatPriority→estimatePathLength沿剩余格sqrt。Array.sort会反复调 comparator,无缓存。塔多(玩家+AI)时尖峰明显,但是第二档,主墙仍是 Spine+物理+变换。 -
双世界
ECS 已有worldX/Y,Cocos 再镜像一份。每只怪每步getComponent×N +worldPositiongetter(可能重算矩阵)+ 再写回。
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. 建议落地顺序
- 小怪走路 SHARED/PRIVATE_CACHE(先一种皮肤试点,看闪白/换皮)
- 战斗禁用 Box2D + 去掉怪刚体
- 表现同步每渲染帧 1 次
- Y 排序改 Sorting2D,停 sibling 洗牌
- 关掉空 update、阴影改子节点、remainPath 缓存
验收:抖音 Release,同屏 ~200、2 倍速 10 分钟。看帧时和发热,不要用编辑器。