- C++ TypedArray 的 set 方法
https://github.com/cocos/cocos-engine/blob/7cc396794959ccfbb8438b8c755090a95f30c090/native/cocos/core/TypedArray.h#L195-L197
这里的设置应该要考虑元素的大小和 array 本身的偏移, 类似这样
- TiledMap 对象层的排序
https://github.com/cocos/cocos-engine/blob/7cc396794959ccfbb8438b8c755090a95f30c090/cocos/tiledmap/tmx-xml-parser.ts#L1015
引擎直接使用了对象的坐标 y 值来排序, 但是这样的顺序和在 tiled 编辑器中是不一致的
我有在这里发过 creator如何修改tiledMap中对象层渲染顺序
还是 TiledMap 相关
fillIndicesBuffer 这个方法是在 native 上当 TiledLayer 发生变化需要重新渲染时会调用的
那么假如有两个 tiledmap 节点, map1 一直在显示中, map2 一直在隐藏中, 当把 map2 设置显示时, map2 中的 TiledLayer 就会触发这个方法调用
问题在于, 上述场景这个方法触发时, 代码中计算得到的 indexOffset 是错误的
并最终导致 native c++ 代码中
这里获取到的 map1 map2 中的 TiledLayer 的 indexOffset 错乱, 显示异常
能否发 PR 修改改呢?
上面提到的问题只有 TypedArray 的问题是比较好改我自己改了的 fix TypedArray set by sablib · Pull Request #17817 · cocos/cocos-engine · GitHub
另外两个问题都是通过其他方式绕过去了, 没有去改引擎的代码, 且我暂时也没有什么头绪要怎么改
tiled object group 排序的问题, 我只是知道在我的场景下这个排序是和 tiled 不匹配的, 对 tiled 也没那么熟悉, 不知道在 tiled 支持的所有地图中这个排序都应该要怎么实现
tiled layer 的 indexOffset 的问题我也只是调试的时候发现这里有问题, 也并不清楚要怎么改才是对的, 我自己是在上面描述的时机调用了所有相关 tiled-layer 的 markForUpdateRenderData, 都标记为 dirtyRender 然后显示就没问题了
准确来说, 不是这里代码中 indexOffset 计算逻辑有问题, 而是这个 drawInfo 和 renderData 每次都是新生成的对象, 并没有维护它在 buffer 中的 indexOffset, 像上面描述的场景中 只有 map2 的 tiled-layer 这一个 dirtyRender, 那么它在这个方法中"let indexOffset = renderData.chunk.meshBuffer.indexOffset;"所获得的 indexOffset 就是0, 因为每次 BEFORE_DRAW 的事件中, StaticVBAccessor 的 reset 都会把 meshBuffer 上的 indexOffset 重置为0. 问题在于这个每帧重置 indexOffset 为 0 的逻辑和有 dirtyRender 的标记才会执行 updateRenderData 的逻辑有些冲突, 这个冲突导致把非 dirtyRender 的数据覆盖掉了, 导致了显示异常
tiled layer 显示异常的 bug,传一个 demo
右上角的按钮,点几下就会出现这个问题了。
需要使用模拟器预览,浏览器和编辑器预览是不会出现这个问题的,这个问题在打包 iOS Android 运行也会出现。
这个 demo 是 3.8.6 的 cocos 版本,但是用哪个版本应该都会出现这个问题,我们项目是 3.8.2,也有这个问题。
TiledLayerDemo.zip (520.1 KB)
里面的资源来自这里 https://github.com/jamesbowman/tiled-maps
下面是 claude code 对这个问题的分析
TiledLayer 渲染异常问题完整分析
1. 问题描述
1.1 具体表现
当场景中存在多个 TiledMap 节点时,如果其中一个 TiledMap 从隐藏状态切换到显示状态,会导致其他正在显示的 TiledMap 出现渲染异常。
1.2 复现条件
- 场景中有两个或更多 TiledMap 节点(例如 map1 和 map2)
- map1 一直处于显示状态
- map2 一直处于隐藏状态
- 当 map2 突然设置为显示时,map1 出现渲染错乱
1.3 用户报告的临时解决方案
用户通过手动调用所有相关 TiledLayer 的 markForUpdateRenderData() 方法,强制标记所有层为 dirtyRender 来暂时绕过问题。
1.4 问题来源
- 论坛链接:[Bug] 最近发现的一些 bug
- 用户分析:每次 BEFORE_DRAW 事件中,StaticVBAccessor 会重置 meshBuffer 的 indexOffset 为 0,但只有标记为 dirtyRender 的 TiledLayer 会重新填充数据
2. 涉及组件分析
2.1 TiledLayer 组件
文件位置: cocos/tiledmap/tiled-layer.ts
主要职责:
- 渲染 TMX 地图的图层
- 管理瓦片数据和用户节点
- 实现裁剪优化
关键属性:
- _tiledDataArray: TiledDataArray // 存储渲染数据和用户节点
- _cullingDirty: boolean // 裁剪状态是否需要更新
- _verticesDirty: boolean // 顶点是否需要更新
- _userNodeDirty: boolean // 用户节点是否需要更新
- tiles: MixedGID[] // 瓦片 GID 数组
- vertices: Array // 顶点位置信息
2.2 StaticVBAccessor
文件位置: cocos/2d/renderer/static-vb-accessor.ts
主要职责:
- 管理静态顶点缓冲区的分配和回收
- 提供内存池机制优化性能
- 支持多个 MeshBuffer 的管理
关键方法:
- allocateChunk(vertexCount, indexCount): StaticVBChunk
- recycleChunk(chunk: StaticVBChunk): void
- reset(): void // 重置所有 buffer 的 indexOffset
- appendIndices(bufferId, indices): void
2.3 MeshBuffer
文件位置: cocos/2d/renderer/mesh-buffer.ts
主要职责:
- 封装实际的 GPU 缓冲区
- 管理顶点和索引数据
关键属性:
- indexOffset: number // 当前索引写入位置
- byteOffset: number // 字节偏移
- vData: Float32Array // 顶点数据
- iData: Uint16Array // 索引数据
2.4 UIRenderer 和 UIRendererManager
文件位置:
cocos/2d/framework/ui-renderer.tscocos/2d/framework/ui-renderer-manager.ts
UIRenderer 职责:
- 所有 2D 渲染组件的基类
- 管理渲染数据和材质
- 提供 dirty 标记机制
UIRendererManager 职责:
- 管理所有 UIRenderer 实例
- 批量更新 dirty 的渲染器
- 优化渲染性能
2.5 Batcher2D
文件位置: cocos/2d/renderer/batcher-2d.ts
主要职责:
- 2D 批渲染管理器
- 管理多个 StaticVBAccessor
- 上传缓冲区数据到 GPU
关键方法:
- uploadBuffers(): void {
// 上传所有缓冲区
for (const accessor of this._bufferAccessors.values()) {
accessor.uploadBuffers();
accessor.reset(); // 关键:重置所有 accessor
}
}
2.6 Director 和渲染流程
文件位置: cocos/game/director.ts
渲染流程事件:
- BEFORE_UPDATE
- AFTER_UPDATE
- BEFORE_DRAW
- AFTER_DRAW
3. 状态管理机制
3.1 Dirty 标记系统
graph TD
A[TiledLayer 状态变化] --> B{触发条件}
B -->|节点变换| C[node.hasChangedFlags]
B -->|颜色改变| D[colorChanged]
B -->|裁剪区域变化| E[_cullingDirty]
B -->|用户节点变化| F[_userNodeDirty]
B -->|动画更新| G[hasAnimation]
C --> H[_markForUpdateRenderData]
D --> H
E --> H
F --> H
G --> H
H --> I[renderData.vertDirty = true]
H --> J[uiRendererManager.markDirtyRenderer]
3.2 IndexOffset 管理
正常流程:
- 初始状态:indexOffset = 0
- 填充索引:appendIndices() → indexOffset += indices.length
- 渲染完成:保持 indexOffset 不变
- 下一帧:reset() → indexOffset = 0
异常流程:
- map1 和 map2 共享 accessor
- reset() 将两者的 indexOffset 都置为 0
- 只有 map2 重新填充数据
- map1 使用错误的 indexOffset = 0
3.3 渲染数据生命周期
stateDiagram-v2
[*] --> 创建: createData()
创建 --> 分配内存: allocateChunk()
分配内存 --> 填充数据: fillBuffers()
填充数据 --> 渲染: draw()
渲染 --> 更新检查: isDirty?
更新检查 --> 填充数据: Yes
更新检查 --> 渲染: No
渲染 --> 重置: reset()
重置 --> 更新检查
填充数据 --> 销毁: destroy()
销毁 --> [*]: recycleChunk()
4. 事件触发时序
4.1 完整的渲染循环
sequenceDiagram
participant Game
participant Director
participant Root
participant Batcher2D
participant UIRendererManager
participant StaticVBAccessor
participant TiledLayer
Game->>Director: mainLoop()
Director->>Director: tick(dt)
Note over Director: 更新阶段
Director->>Director: emit(BEFORE_UPDATE)
Director->>Director: 更新所有组件
Director->>Director: emit(AFTER_UPDATE)
Note over Director: 渲染准备阶段
Director->>Director: emit(BEFORE_DRAW)
Director->>UIRendererManager: updateAllDirtyRenderers()
UIRendererManager->>TiledLayer: updateRenderData()
alt TiledLayer is dirty
TiledLayer->>TiledLayer: destroyRenderData()
TiledLayer->>TiledLayer: traverseGrids()
TiledLayer->>StaticVBAccessor: allocateChunk()
TiledLayer->>TiledLayer: fillVertexData()
end
Note over Root: 渲染阶段
Director->>Root: frameMove(dt)
Root->>Batcher2D: update()
Root->>Batcher2D: uploadBuffers()
Note over Batcher2D: 关键点:重置所有 accessor
Batcher2D->>StaticVBAccessor: uploadBuffers()
Batcher2D->>StaticVBAccessor: reset()
StaticVBAccessor->>StaticVBAccessor: 所有 buffer.indexOffset = 0
Root->>Root: 执行渲染
Director->>Director: emit(AFTER_DRAW)
4.2 问题场景时序
sequenceDiagram
participant Frame1 as 第1帧
participant Frame2 as 第2帧
participant Accessor
participant Map1 as TiledMap1(显示)
participant Map2 as TiledMap2(隐藏)
Note over Frame1: 正常渲染
Frame1->>Accessor: reset() [indexOffset=0]
Frame1->>Map1: updateRenderData()
Map1->>Accessor: fillBuffers()
Note over Accessor: indexOffset = 1000
Frame1->>Map2: 跳过(隐藏)
Note over Frame2: Map2 变为显示
Frame2->>Accessor: reset() [indexOffset=0]
Frame2->>Map1: updateRenderData()
Note over Map1: 不是 dirty,跳过
Note over Map1: 错误:使用 indexOffset=0
Frame2->>Map2: updateRenderData()
Note over Map2: 是 dirty
Map2->>Accessor: fillBuffers()
Note over Accessor: indexOffset = 500
Note over Map1: 渲染异常!
5. 设计决策分析
5.1 StaticVBAccessor 共享设计
设计动机:
- 减少内存占用:多个组件共享同一个缓冲区
- 提高缓存效率:减少 GPU 缓冲区切换
- 简化管理:统一的内存池管理
想要达成的目的:
- 优化内存使用
- 提高批渲染效率
- 支持大量 2D 对象的高效渲染
存在的问题:
- 状态耦合:一个组件的更新影响其他组件
- 复杂性增加:需要精确管理共享状态
- 调试困难:问题不易定位
可行的其他方案:
- 每个组件独立缓冲区(内存换稳定性)
- 分组共享(按类型或更新频率分组)
- 动态切换策略(根据场景自动选择)
5.2 Reset 机制设计
设计动机:
- 简化状态管理:每帧开始时统一重置
- 避免状态累积:防止 indexOffset 溢出
- 支持动态内容:适应内容变化
想要达成的目的:
- 保证每帧渲染的正确性
- 简化缓冲区管理逻辑
- 支持动态添加/删除对象
存在的问题:
- 全局重置:影响所有共享组件
- 性能开销:每帧都要重置
- 状态不一致:部分更新导致的问题
改进方向:
- 增量重置:只重置已使用的部分
- 延迟重置:在需要时才重置
- 智能重置:根据使用情况决定重置策略
5.3 批处理优化策略
设计动机:
- 减少 Draw Call:合并相同材质的渲染
- 提高 GPU 利用率:批量提交数据
- 优化移动设备性能:减少 CPU-GPU 通信
实现方式:
- 统一的顶点格式
- 共享的缓冲区
- 延迟的数据上传
权衡考虑:
- 性能 vs 复杂性
- 内存 vs 灵活性
- 通用性 vs 特定优化
6. 问题根源深度分析
6.1 核心矛盾
共享资源 + 部分更新 + 全局重置 = 状态不一致
6.2 边界情况分析
- 单个 TiledMap:正常工作
- 多个静态 TiledMap:正常工作(都不更新)
- 多个动态 TiledMap:正常工作(都更新)
- 混合场景:出现问题(部分更新)
6.3 问题本质
这是一个典型的共享可变状态问题:
- 多个组件共享 StaticVBAccessor
- indexOffset 是可变状态
- reset 操作影响所有共享者
- 更新不同步导致状态不一致
7. 解决方案评估
7.1 方案对比表
| 方案 | 复杂度 | 内存开销 | 性能影响 | 风险 | 可维护性 |
|---|---|---|---|---|---|
| 独立 Buffer | 低 | 高 | 小 | 低 | 高 |
| 改进 Dirty 机制 | 中 | 无 | 中 | 中 | 中 |
| 优化 Reset 逻辑 | 高 | 无 | 小 | 高 | 低 |
| 混合策略 | 高 | 中 | 小 | 中 | 中 |
7.2 推荐方案:独立 Buffer 管理
实施步骤:
- 修改 Assembler:
// simple.ts
const _accessorMap = new WeakMap<TiledLayer, StaticVBAccessor>();
private getAccessor(layer: TiledLayer): StaticVBAccessor {
let accessor = _accessorMap.get(layer);
if (!accessor) {
accessor = new StaticVBAccessor(device, vfmtPosUvColor);
_accessorMap.set(layer, accessor);
// 注册到 batcher 以便统一管理
const batcher = director.root!.batcher2D;
batcher.registerAccessor(layer.uuid, accessor);
}
return accessor;
}
- 生命周期管理:
// TiledLayer.ts
onDestroy() {
super.onDestroy();
const accessor = _accessorMap.get(this);
if (accessor) {
accessor.destroy();
_accessorMap.delete(this);
const batcher = director.root!.batcher2D;
batcher.unregisterAccessor(this.uuid);
}
}
- 性能优化:
- 使用 WeakMap 避免内存泄漏
- 延迟创建 accessor
- 及时清理未使用的 accessor
7.3 ■■优化建议
-
架构改进:
- 引入 AccessorPool 管理器
- 实现智能的共享策略
- 提供配置选项
-
监控和调试:
- 添加 accessor 使用统计
- 提供可视化调试工具
- 记录状态变化日志
-
文档和测试:
- 完善相关文档
- 添加边界情况测试
- 提供最佳实践指南
8. 总结
TiledLayer 渲染异常是一个由多个设计决策相互作用产生的问题。虽然每个设计决策都有其合理性,但在特定场景下的组合导致了状态不一致。通过独立管理缓冲区或改进状态同步机制,可以有效解决这个问题。■■来看,需要在性能优化和系统稳定性之间找到更好的平衡点。
确实,我分别用 3.8.6 和 3.8.7 跑了一遍,3.8.6 是有问题的,3.8.7 看起来是好的。






两个字还被和谐了
