经验贴:tiled map踩坑与优化

项目中需要使用tiledmap,从开始使用到现在资源铺量,趟过不少坑:

1. tmx注意事项

  • tiledmap只支持1.4版本
  • 不支持等角(交错)
  • 瓦块图层只支持一种类型的图片,且不要对图片做旋转,镜像等操作
  • 图块集请嵌入到地图中,否则不识别
  • 不支持将图层组合
  • 为了方便美术制作,可以对tmx进行后处理,可让美术使用组,然后用脚本将组打散
  • 地图与资源不要放到assetbundle目录下,方便剔除不需要的资源

2. 预览等待加载时间过长之前发帖
项目中有900+小图,在预览的时候加上各种资源描述文件有3000+的文件需要下载,最长的时候我需要等待3-5分钟,简直过了忍耐极限。
究其原因是因为cocos只会在发布包中使用图集,在预览的时候直接加载原始图片,因此我们只需要在预览的时候替换成我们自己制作的图集就行了,步骤如下:

  • 通过texturepacker将地图资源打成cocos支持的图集,并放到非assetbundle目录下,这样最终才不会打进包中。
    image
  • 用脚本收集需要被替换资源文件的uuid,以及图集的uuid,将映射关系写入js对象中,并在预览模板html中载入

  • 修改引擎中deserialize.ts中获取资源依赖的代码,将依赖资源映射到我们生成的图集上:

    因为我们只在预览时才注入了convertUuid方法,因此不管你是否剥离引擎,都不会影响编译后的包。
    最终预览等待时间控制在了30秒这个可接受的时间内,又节约了不少时间去写bug。
    3. drawcall过高
    预览时因为图片没有打图集,因此合批肯定会有问题,但我们项目中图片过多,即便是发布后,因为各种交错穿插也会造成高drawcall,最高时有800+;因此通过查看源码与调试工具的帮助,最终找到解决方案如下:
  • 使用多纹理材质,控制整个地图所有图片在8张2048x2048以内,这样地图中所有元素可以共用一个材质球,shader代码如下:
// Effect Syntax Guide: https://github.com/cocos-creator/docs-3d/blob/master/zh/material-system/effect-syntax.md

CCEffect %{
  name: "tmx-sprite"
  techniques:
  - passes:
    - vert: tmx-sprite-vs:vert
      frag: tmx-sprite-fs:frag
      depthStencilState:
        depthTest: false
        depthWrite: false
      blendState:
        targets:
        - blend: true
          blendSrc: src_alpha
          blendDst: one_minus_src_alpha
          blendDstAlpha: one_minus_src_alpha
      rasterizerState:
        cullMode: none
      properties:
        texture0: { value: white }
        texture1: { value: white }
        texture2: { value: white }
        texture3: { value: white }
        texture4: { value: white }
        texture5: { value: white }
        texture6: { value: white }
        texture7: { value: white }
        alphaThreshold: { value: 0.5 }
}%

CCProgram tmx-sprite-vs %{
  precision highp float;
  #include <cc-global>
  #if USE_LOCAL
    #include <cc-local>
  #endif
  #if SAMPLE_FROM_RT
    #include <common>
  #endif
  in vec3 a_position;
  in vec2 a_texCoord;
  in vec4 a_color;

  out vec4 color;
  out vec2 uv0;

  out float texture_idx;

  vec4 vert () {
    vec4 pos = vec4(a_position, 1);

    #if USE_LOCAL
      pos = cc_matWorld * pos;
    #endif

    #if USE_PIXEL_ALIGNMENT
      pos = cc_matView * pos;
      pos.xyz = floor(pos.xyz);
      pos = cc_matProj * pos;
    #else
      pos = cc_matViewProj * pos;
    #endif

    uv0 = a_texCoord;
    #if SAMPLE_FROM_RT
      CC_HANDLE_RT_SAMPLE_FLIP(uv0);
    #endif
    color = a_color;

    texture_idx = a_color.r;

    return pos;
  }
}%


CCProgram tmx-sprite-fs %{
  precision highp float;
  #include <embedded-alpha>
  #include <alpha-test>

  in vec4 color;
  in vec2 uv0;
  in float texture_idx;

  uniform sampler2D texture0;
  uniform sampler2D texture1;
  uniform sampler2D texture2;
  uniform sampler2D texture3;
  uniform sampler2D texture4;
  uniform sampler2D texture5;
  uniform sampler2D texture6;
  uniform sampler2D texture7;

  vec4 getColor(float idx) {
    idx *= 255.0;
    if(idx < 1.0) {
      return CCSampleWithAlphaSeparated(texture0, uv0);
    } else if(idx < 2.0) {
      return CCSampleWithAlphaSeparated(texture1, uv0);
    } else if(idx < 3.0) {
      return CCSampleWithAlphaSeparated(texture2, uv0);
    } else if(idx < 4.0) {
      return CCSampleWithAlphaSeparated(texture3, uv0);
    } else if(idx < 5.0) {
      return CCSampleWithAlphaSeparated(texture4, uv0);
    } else if(idx < 6.0) {
      return CCSampleWithAlphaSeparated(texture5, uv0);
    } else if(idx < 7.0) {
      return CCSampleWithAlphaSeparated(texture6, uv0);
    } else if(idx < 8.0) {
      return CCSampleWithAlphaSeparated(texture7, uv0);
    }
    return CCSampleWithAlphaSeparated(texture0, uv0);
  }

  vec4 frag () {
    vec4 o = getColor(texture_idx);
    
    #if IS_GRAY
      float gray = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b;
      o.r = o.g = o.b = gray;
    #endif

    o.a *= color.a;
    ALPHA_TEST(o);
    return o;
  }
}%
  • 我发现即便是只使用了一个材质球,cocos也没将sprite合批,通过翻看源码发现,sprite默认按一张图片生成的hash值,因此就算是同一个材质球,使用了多个纹理不会合批,没办法只有注入一段代码,让cocos认为我们的材质球时可以合批的,关键代码如下:
let updateHash = RenderData.prototype.updateHash;
RenderData.prototype.updateHash = function () {
    let inst = this as RenderData;
    if(inst.material && inst.material["__multi_tex__"]) {
        const bid = this.chunk ? this.chunk.bufferId : -1;
        const hashString = `${bid}${this.layer} ${this.blendHash} ${inst.material.hash}`;
        this.dataHash = murmurhash2_32_gc(hashString, 666);
        this.hashDirty = false;
        return;
    }

    return updateHash.call(this);
}
  • 补上替换材质球的关键代码,由于color在地图中没用上,我又不想大费周折自己去修改顶点属性,就把color属性给征用了,alpha还是保持原来的作用,毕竟里面还有不少控制透明的动画,然后把红色通道作为了纹理索引:
static updateMaterial(node: Node) {
        node.layer = this._worldLayer;

        let r2d = node.getComponent(Renderable2D);
        if(!r2d) {
            node.children.forEach(n=>{this.updateMaterial(n)});
            return;
        }

        let texture: Texture2D = null;
        if(r2d instanceof Sprite) {
            texture = r2d.spriteFrame.texture as Texture2D;
        }else if(r2d instanceof TiledLayer){
            let layerInfo = r2d["_layerInfo"];
            let tid = layerInfo.tiles.find(i=>i!=0);
            let grid = r2d.texGrids.get(tid);
            if(grid) {
                texture = grid.texture;
            }
        }

        if(!texture) {
            node.children.forEach(n=>{this.updateMaterial(n)});
            return;
        }

        let key = texture["_id"];

        let mat:MatrialItem = this._materials[key];
        if(!mat) {
            console.log("new texture " + key);

            let keys = Object.keys(this._materials);
            if(keys.length > 0) {
                mat = this._materials[keys[keys.length-1]];
                if(mat.textures.length == 8) {
                    mat = null;
                }
            }
            
            if(!mat) {
                let material = new Material();
                material.initialize({effectAsset: this._commonMat.effectAsset});
                material["__multi_tex__"] = true;

                this._materialCount++;
                material.name = `${this._commonMat.effectAsset.name}Clone(${this._materialCount})`;
                
                mat = {
                    matIndex: {},
                    material: material,
                    textures: [],
                };
            }
            mat.matIndex[key] = mat.textures.length;

            mat.textures.push(texture);
            this._materials[key] = mat;

            mat.material.setProperty(`texture${mat.matIndex[key]}`, texture);                  
        }   
        
        r2d.name = mat.material.name;
        r2d.customMaterial = mat.material;
        // a不能为0
        r2d.color = new Color(mat.matIndex[key], 0, 0, 255);
        r2d.markForUpdateRenderData();

        node.children.forEach(n=>{this.updateMaterial(n)});
    }

4. 相机剔除问题

  • 瓦块地图会进行剔除,但是如果按自带剔除方法,往往会在边界留一块黑边,类似这样:

    因此只需要把可视区域扩大一些就行了:
public _updateCulling (layer: TiledLayer) {
        if (layer.enableCulling) {
            layer.node.updateWorldTransform();
            Mat4.invert(_mat4_temp, layer.node.getWorldMatrix());
            const camera = director.root!.batcher2D.getFirstRenderCamera(layer.node);
            if (camera) {
                _vec3_temp.x = -camera.width*0.5;
                _vec3_temp.y = -camera.height*0.5;
                _vec3_temp.z = 0;
                _vec3_temp2.x = camera.width * 2;
                _vec3_temp2.y = camera.height * 2;
                _vec3_temp2.z = 0;
                camera.screenToWorld(_vec3_temp, _vec3_temp);
                camera.screenToWorld(_vec3_temp2, _vec3_temp2);
                // camera.getScreenToWorldPoint(_vec2_temp, _vec2_temp);
                // camera.getScreenToWorldPoint(_vec2_temp2, _vec2_temp2);
                Vec3.transformMat4(_vec3_temp, _vec3_temp, _mat4_temp);
                Vec3.transformMat4(_vec3_temp2, _vec3_temp2, _mat4_temp);
                layer.updateViewPort(_vec3_temp.x, _vec3_temp.y, _vec3_temp2.x - _vec3_temp.x, _vec3_temp2.y - _vec3_temp.y);
            }
        }
    }
 * 优化效果:

6EfIjC7T00 oLW3OnWJ4p
最终效果还没进行相机剔除,我还发现现在即便是一个材质球,有的地方依然会中断合批,所以依然还有优化空间。

  • 对象层未进行剔除,即便不在相机范围内,也依然会进行渲染,大家可以根据项目需求选择是否需要剔除,或者选择自己的剔除方式。
15赞

感谢分享!

感谢分享!

感谢分享!

  • 不支持将图层组合 是指?

tilemap中的图层组,在cocos里面直接认不出来

感谢分享!!

有大神知道一个tiledlayer用了多个图集怎么处理dc多的问题吗?multitexture在3.7好像都无效

请问multitexture3.7有解决方案吗

mark一下

mark一下

感谢分享!

加一点 地图属性不要勾无限 不然creator里不显示

感谢分享!!

mark一下

Screenshot_2024-04-26-17-24-00-810_com.tencent.mm

Screenshot_2024-04-26-17-23-00-944_com.tencent.mm

(帖子被作者删除,如无标记将在 24 小时后自动删除)