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.
131 lines
6.7 KiB
131 lines
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 分钟。看帧时和发热,不要用编辑器。
|
|
|