一键导入
cocos-creator-drawcall
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
做 Cocos Creator 多机型/多分辨率适配时使用。Canvas、Widget、安全区。
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
写 Cocos Creator 动效/动画时使用。tween、Animation、Spine、性能与清理。
做 Cocos Creator 大量条目列表时使用。虚拟列表、节点复用。
写 Cocos Creator(TypeScript)时使用。组件、节点、prefab、事件、资源管理规范。
基于 SOC 职业分类
| 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 组件节点已检查,没有意外插入连续精灵序列中间打断合批。