with one click
cocos-creator-drawcall
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
做 Cocos Creator 多机型/多分辨率适配时使用。Canvas、Widget、安全区。
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
写 Cocos Creator 动效/动画时使用。tween、Animation、Spine、性能与清理。
做 Cocos Creator 大量条目列表时使用。虚拟列表、节点复用。
写 Cocos Creator(TypeScript)时使用。组件、节点、prefab、事件、资源管理规范。
| name | cocos-creator-drawcall |
| description | 优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。 |
| category | gamedev |
| tags | ["cocoscreator","性能","drawcall"] |
规则: Cocos Creator 3.x 的自动合批(Auto Batching)要求相邻渲染节点使用完全相同的材质实例和贴图,且在渲染树中是连续的兄弟节点;只要中间插入一个使用不同贴图的节点,从该节点开始就会打断合批,之前和之后的节点各自成为独立批次。
为什么: AI 生成 UI 布局时完全不考虑渲染顺序,喜欢按逻辑语义组织节点层级:把背景、图标、文字、特效各自放一层,每层里再嵌套子节点。这种结构在视觉上没问题,但渲染时背景(图集A)→ 图标(图集B)→ 文字(Label 单独纹理)→ 特效(序列帧图集C)来回切换贴图,每切换一次就是一次新的 DrawCall,最终一个看起来简单的卡片 UI 产生了 8 个 DrawCall。新手以为"用了同一个图集就能合批",却不知道中间穿插了一个不同材质的节点就已经把批次切断了,从 DrawCall 面板看到的批次数远超预期。
怎么做:
Mask 组件节点——Mask 会强制开启模板测试,打断合批。规则: 同一个界面(大厅、战斗 HUD、商店)内频繁出现的精灵必须合并到同一张图集;图集尺寸控制在 1024×1024(中端机)或 2048×2048(高端机),超出则拆分多张;不同界面的独占图集在切换界面时及时释放。
为什么: 没有图集是 DrawCall 爆炸最直接的原因:100 个 UI 图片分别存为独立 PNG,渲染时每个图片一次 DrawCall,哪怕它们相邻、同材质,贴图句柄不同就无法合批。AI 生成美术资源管理代码时几乎从不提图集,直接 resources.load("sprites/icon_1") 逐个加载散图。另一个常见错误是图集做得过大:一张 4096×4096 的图集在中端安卓机上加载耗时 200~400ms,且常驻 64MB GPU 内存,换来的收益是减少了几个 DrawCall——完全不划算。图集尺寸要根据目标机型 GPU 内存上限精心规划。
怎么做:
hall_atlas(大厅界面全部图标)、battle_hud_atlas(战斗 HUD)、common_atlas(全局通用小图标),不同界面图标不混打。规则: 在 Cocos Creator 3.x 中,将完全静态的 UI 节点(不会移动、不会变色、不会显隐)与动态节点(血条、计分、移动角色)放在不同的渲染层;静态层节点上开启 Static Batching(或确保其 transform 从不改变),让引擎对静态批次做缓存而不是每帧重建。
为什么: Creator 3.x 的渲染器在每帧构建渲染批次时,只要某个节点的 transform 或颜色发生变化,该批次内的所有节点都需要重新提交顶点数据。如果把每秒更新 10 次的血量进度条和永远不动的背景装饰放在同一个合批组里,背景装饰每秒也被强制重新提交 10 次——本来可以被完全跳过的工作变成了持续的 CPU/GPU 负担。AI 不了解这一批次失效机制,倾向于按视觉层次(背景在下、前景在上)组织节点,把静态装饰和动态元素紧密交叉放置。
怎么做:
Canvas
├── StaticLayer ← 所有永不变化的背景、装饰、边框
│ └── (静态精灵群,使用同一图集,可合批)
├── DynamicLayer ← 血条、计分、角色动画
│ └── (动态节点,允许每帧更新)
└── EffectLayer ← 粒子、特效(独立材质,放最顶层集中切换)
StaticLayer 下的节点在运行时绝对不调用 setPosition、setRotation、修改 color,否则失去静态缓存价值。Is Static(Properties 面板),引擎会在构建时烘焙这些节点的合批,运行时不再每帧重建。active 切换也会使批次失效,静态层节点宁可用 opacity = 0 代替 active = false(但注意 opacity = 0 仍然参与渲染,不适用于需要彻底关闭的情况;权衡后选择)。规则: 频繁更新的动态 Label(倒计时、伤害数字、计分)使用 CHAR cache mode 或 BMFont;装饰性静态文字(标题、按钮固定文字)使用 BITMAP cache mode;绝不在高频更新的 Label 上使用 NONE cache mode 或系统字体,尤其禁止在 update 里每帧更新系统字体 Label。
为什么: Label 在 Creator 中是最容易被忽视的 DrawCall 杀手。使用系统字体(TTF)且 cache mode 为 NONE 时,每次修改 string 都会触发一次 Canvas 2D 重新排版、重新生成纹理,生成的纹理是独立的,无法与任何 Sprite 合批,每个这样的 Label 就是一个独立 DrawCall。战斗界面里若有 10 个敌人头顶的血量数字(系统字体),就是 10 个永远无法合批的 DrawCall,还带着每帧 CPU Canvas 重排的额外开销。BMFont 的文字以精灵图集形式存在,能与同图集的 UI 精灵合批,是最优解。CHAR mode 缓存已用字符的纹理,字符数量稳定后可以合批,适合动态内容。
怎么做:
BitmapFont 资源,Label 的 Font 属性指向 BMFont,cache mode 设 NONE(BMFont 本身已是精灵图集,不需要额外 cache)。label.cacheMode = Label.CacheMode.CHAR,引擎把用过的字符缓存进共享纹理,相同字体+字号的多个 Label 共享同一纹理,可合批。label.cacheMode = Label.CacheMode.BITMAP,整个 Label 渲染成一张纹理,之后不再重排,适合标题等不会改变的文本。update 里每帧给系统字体 Label 赋值;若不可避免要更新,至少做 dirty 检查:if (this._lastHp !== hp) { this.hpLabel.string = String(hp); this._lastHp = hp; }。规则: 所有 DrawCall 优化必须用工具数字验证,不凭感觉或"应该合批了"的推断交付;在编辑器 Preview 模式下打开 Render Batches 面板查看实时批次;在原生平台用 RenderDoc(Android)或 Xcode Frame Debugger(iOS)逐 DrawCall 分析实际 GPU 指令。
为什么: 这是 DrawCall 优化里被 AI 和新手最彻底忽视的一步。"我把图标都放进了同一个图集,DrawCall 应该减少了"——这句话里有太多可能出错的地方:图集打包设置里最大尺寸太小导致图标被拆到了两张图集;某个图标被意外加了 Mask;动态节点被插入了静态群中间。不用工具验证,所有这些失误都无法被发现。AI 生成优化方案时更不会主动说"请用 DrawCall 面板确认一下",导致工程师以为优化已完成,实际效果为零。
怎么做:
// 反例 — 散图精灵、动静混排、系统字体每帧更新
@ccclass
export class BattleHUD extends Component {
@property(Sprite) iconAttack: Sprite = null!; // ❌ 独立 PNG,独立 DrawCall
@property(Sprite) iconDefense: Sprite = null!; // ❌ 独立 PNG,独立 DrawCall
@property(Sprite) iconHP: Sprite = null!; // ❌ 独立 PNG,独立 DrawCall
@property(Label) hpLabel: Label = null!; // ❌ 系统字体,NONE mode
@property(Label) atkLabel: Label = null!; // ❌ 系统字体,NONE mode
private _hp = 100;
update(dt: number) {
// ❌ 每帧给系统字体 Label 赋值,每次触发 Canvas 重排 + 新纹理
this.hpLabel.string = "HP: " + this._hp;
this.atkLabel.string = "ATK: " + this._atk;
// ❌ 3 个散图图标 + 2 个系统字体 Label = 至少 5 个 DrawCall,且无法合批
}
}
// 正例 — 图集合批 + 动静分离 + CHAR mode + dirty 检查
@ccclass
export class BattleHUD extends Component {
// ✅ 图标全部来自同一图集(hud_atlas),三个精灵可合批为 1 个 DrawCall
@property(Sprite) iconAttack: Sprite = null!;
@property(Sprite) iconDefense: Sprite = null!;
@property(Sprite) iconHP: Sprite = null!;
// ✅ Label 使用 CHAR cache mode,动态数字字符缓存后可合批
@property(Label) hpLabel: Label = null!;
@property(Label) atkLabel: Label = null!;
private _hp = 100;
private _atk = 50;
private _lastHp = -1;
private _lastAtk = -1;
onLoad() {
// ✅ 确认 Label 使用 CHAR mode(也可在编辑器 Inspector 里设置)
this.hpLabel.cacheMode = Label.CacheMode.CHAR;
this.atkLabel.cacheMode = Label.CacheMode.CHAR;
}
// ✅ 事件驱动更新,不在 update 轮询
onHpChanged(newHp: number) {
if (this._lastHp === newHp) return; // ✅ dirty 检查,值未变不重排
this._lastHp = newHp;
this.hpLabel.string = String(newHp); // ✅ 只赋值数字,不拼"HP: "前缀(前缀用 BMFont 静态 Label)
}
onAtkChanged(newAtk: number) {
if (this._lastAtk === newAtk) return;
this._lastAtk = newAtk;
this.atkLabel.string = String(newAtk);
}
}
// 反例 — 节点层级结构(伪代码)
Canvas
└── GameLayer
├── Background_Sprite (图集A,静态)
├── Player_Spine (Spine 动画,每帧更新) ← ❌ 插在静态节点中间,打断合批
├── Decoration_Sprite_1 (图集A,静态) ← ❌ 被 Player 打断,无法与 Background 合批
├── Decoration_Sprite_2 (图集A,静态)
└── HP_ProgressBar (图集B,每帧更新)
// 结果:Background(1) + Player(1) + Decoration×2(2) + HP(1) = 5 DrawCall
// 正确分层后:StaticLayer合批(1) + DynamicLayer(2) = 3 DrawCall
// 正例 — 动静分离节点结构,静态节点连续合批
@ccclass
export class GameScene extends Component {
@property(Node) staticLayer: Node = null!; // ✅ 背景、装饰,永不修改 transform
@property(Node) dynamicLayer: Node = null!; // ✅ 角色、血条,允许每帧更新
@property(Node) effectLayer: Node = null!; // ✅ 粒子、Spine,独立材质集中放顶层
onLoad() {
// ✅ 运行时不对 staticLayer 的子节点做任何 transform 修改
// ✅ staticLayer 内所有节点来自同一图集 → 合批为 1 个 DrawCall
// ✅ dynamicLayer 内角色和血条 → 各自独立 DrawCall 但数量可控
// ✅ effectLayer 内特效 → 材质特殊,放顶层避免切换穿插 staticLayer
}
}
StaticLayer 下节点运行时不修改 transform/color。CHAR cache mode 或 BMFont,没有系统字体 + NONE mode 的动态 Label 在 update 里每帧赋值。Mask 组件节点已检查,没有意外插入连续精灵序列中间打断合批。