深度解读dragonBones使用SpriteFrame任意换肤的实现

56 阅读5分钟

需求背景

龙骨文件比较复杂,和需求关联比较紧密的结构如下:

image.png

期望的效果如下,使用spriteFrame/atlas替换Armature,达到局部/整体换肤的效果 image.png

比如上图中使用了resources/yu.plistyu的Armature进行了换肤

源码分析

切入点

// 这个是渲染龙骨纹理的核心代码
material = _getSlotMaterial(
    slot.getTexture(), // 核心思路就是在这里做文章,达到换肤的效果
    slot._blendMode
);

getTexture () {
    return this._textureData && this._textureData.spriteFrame && this._textureData.spriteFrame.getTexture();
},

slot.texture来源

image.png

 this._displayData = this._displayDatas[this._displayIndex];

image.png

来自这个rawDisplayData

set: function (value) {
    if (this._rawDisplayDatas === value) {
        return;
    }
    this._displayDirty = true;
    this._rawDisplayDatas = value;
    if (this._rawDisplayDatas !== null) {
        this._displayDatas.length = this._rawDisplayDatas.length;
        // 将rawDisplayData覆盖到_displayDatas,源头就指向了_rawDisplayDatas
        for (var i = 0, l = this._displayDatas.length; i < l; ++i) {
            var rawDisplayData = this._rawDisplayDatas[i];
            if (rawDisplayData === null) {
                rawDisplayData = this._getDefaultRawDisplayData(i);
            }
            this._displayDatas[i] = rawDisplayData;
        }
    }
}    
  BaseFactory.prototype._buildSlots = function (dataPackage, armature) {
        var currentSkin = dataPackage.skin; // 来着skin
        var defaultSkin = dataPackage.armature.defaultSkin;
        if (currentSkin === null || defaultSkin === null) {
            return;
        }
        var skinSlots = {};
        for (var k in defaultSkin.displays) {
            var displays = defaultSkin.getDisplays(k);
            skinSlots[k] = displays;// 来自displays
        }
        if (currentSkin !== defaultSkin) {
            for (var k in currentSkin.displays) {
                var displays = currentSkin.getDisplays(k);
                skinSlots[k] = displays;// 来自displays
            }
        }
        for (var _i = 0, _a = dataPackage.armature.sortedSlots; _i < _a.length; _i++) {
            var slotData = _a[_i];
            var displayDatas = slotData.name in skinSlots 
                ? skinSlots[slotData.name] // 往上找来源
                : null;
            var slot = this._buildSlot(dataPackage, slotData, armature);
            slot.rawDisplayDatas = displayDatas; // 这里,此时displayData.texture=null,哪里修改了texture呢?应该是某个地方
            if (displayDatas !== null) {
                var displayList = new Array();
                // for (const displayData of displays) 
                for (var i = 0, l = dragonBones.DragonBones.webAssembly ? displayDatas.size() : displayDatas.length; i < l; ++i) {
                    var displayData = dragonBones.DragonBones.webAssembly ? displayDatas.get(i) : displayDatas[i];
                    if (displayData !== null) {
                        // 这里会修改texture
                        displayList.push(this._getSlotDisplay(dataPackage, 
                        displayData,// 引用参数
                        null, slot));
                    }
                    else {
                        displayList.push(null);
                    }
                }
                slot._setDisplayList(displayList);
            }
            slot._setDisplayIndex(slotData.displayIndex, true);
        }
    };            
BaseFactory.prototype._getSlotDisplay = function (dataPackage, displayData, rawDisplayData, slot) {
    var dataName = dataPackage !== null ? dataPackage.dataName : displayData.parent.parent.parent.name;
    var display = null;
    switch (displayData.type) {
        case 0 /* Image */: {
            var imageDisplayData = displayData;
            if (dataPackage !== null && dataPackage.textureAtlasName.length > 0) {
                imageDisplayData.texture = this._getTextureData(dataPackage.textureAtlasName, displayData.path);
                // 最终会去dragonBones.CCTextureAtlasData.textures里面取
            }
 BaseFactory.prototype.buildArmature = function (armatureName, dragonBonesName, skinName, textureAtlasName) {
        if (dragonBonesName === void 0) { dragonBonesName = ""; }
        if (skinName === void 0) { skinName = ""; }
        if (textureAtlasName === void 0) { textureAtlasName = ""; }
        var dataPackage = new BuildArmaturePackage();
        // 这里会填充dataPackage.skin,也就是上边用到的
        if (!this._fillBuildArmaturePackage(dataPackage, dragonBonesName || "", armatureName, skinName || "", textureAtlasName || "")) {
            console.warn("No armature data: " + armatureName + ", " + (dragonBonesName !== null ? dragonBonesName : ""));
            return null;
        }
        var armature = this._buildArmature(dataPackage);
        this._buildBones(dataPackage, armature);
        this._buildSlots(dataPackage, armature); // 来自这个dataPackage
        this._buildConstraints(dataPackage, armature);
        armature.invalidUpdate(null, true);
        armature.advanceTime(0.0); // Update armature pose.
        return armature;
    };
BaseFactory.prototype._fillBuildArmaturePackage = function (dataPackage, dragonBonesName, armatureName, skinName, textureAtlasName) {
        var dragonBonesData = null;
        var armatureData = null;
        if (dragonBonesName.length > 0) {
            if (dragonBonesName in this._dragonBonesDataMap) {
                // 从map取得
                dragonBonesData = this._dragonBonesDataMap[dragonBonesName];
                armatureData = dragonBonesData.getArmature(armatureName);
            }
        }
     
        if (armatureData !== null) {
          
            if (dataPackage.skin === null) {
                dataPackage.skin = armatureData.defaultSkin;// skin的真正源头
            }
            return true;
        }
        return false;
    };
    ObjectDataParser.prototype.parseTextureAtlasData = function (rawData, textureAtlasData, scale) {
        if (dragonBones.DataParser.SUB_TEXTURE in rawData) {
            var rawTextures = rawData[dragonBones.DataParser.SUB_TEXTURE];
            for (var i = 0, l = rawTextures.length; i < l; ++i) {
                var rawTexture = rawTextures[i];
                var textureData = textureAtlasData.createTexture();
                
                textureAtlasData.addTexture(textureData);
            }
        }
        return true;
    };
dragonBones.CCTextureAtlasData = cc.Class({
    extends: dragonBones.TextureAtlasData,
    name: "dragonBones.CCTextureAtlasData",

    properties: {
        _renderTexture: {
            default: null,
            serializable: false
        },

        renderTexture: {
            get () {
                return this._renderTexture;
            },
            set (value) {
                this._renderTexture = value;
                if (value) {
                    for (let k in this.textures) {
                        let textureData = this.textures[k];
                        if (!textureData.spriteFrame) {
                            let rect = null;
                            if (textureData.rotated) {
                                rect = cc.rect(textureData.region.x, textureData.region.y,
                                    textureData.region.height, textureData.region.width);
                            } else {
                                rect = cc.rect(textureData.region.x, textureData.region.y,
                                    textureData.region.width, textureData.region.height);
                            }
                            let offset = cc.v2(0, 0);
                            let size = cc.size(rect.width, rect.height);
                            textureData.spriteFrame = new cc.SpriteFrame();
                            textureData.spriteFrame.setTexture(value, rect, false, offset, size);
                        }
                    }
                } else {
                    for (let k in this.textures) {
                        let textureData = this.textures[k];
                        textureData.spriteFrame = null;
                    }
                }
                
            },
        }
    },
    statics: {
        toString: function () {
            return "[class dragonBones.CCTextureAtlasData]";
        }
    },
    createTexture : function() {
        return dragonBones.BaseObject.borrowObject(dragonBones.CCTextureData);
    }
});

整体思路图

dragonBones.png

换肤实现

换肤的基础

上图分析过程整个链路还是挺长的,不过仍旧可以在slot.getTexture上做文章,因为在提交渲染时有material的判断,应该是考虑到dragonbones会输出多个纹理

if (_mustFlush || material.getHash() !== _renderer.material.getHash()) {
    _mustFlush = false;
    _renderer._flush();
    _renderer.node = _node;
    _renderer.material = material;
}

所以我们任意切换纹理也问题不大,Engine是支持的,唯一需要我们处理好的是顶点坐标,否则会发生纹理错位

方法1:修改slot._textureData.spriteFrame实现换肤(失败)

我观察到渲染取的是_textureData.spriteFrame,我尝试给slot增加一个setTexture接口,直接修改了_textureData.spriteFrame会导致连锁反应,所有的龙骨实例都会发生同步,这不是我想要的效果。

getTexture () { 
    return this._textureData 
        && this._textureData.spriteFrame 
        && this._textureData.spriteFrame.getTexture(); 
},
setSpriteFrame(spriteFrame){
    this._textureData.spriteFrame = spriteFrame;
}

因为数据都是从_dragonBonesDataMap取的,这是一个object,因为js弱引用的原因,如果slot._textureData修改了,大家都会变。

方法2: 自定义一个SpriteFrame

既然不能对slot._textureData修改,那么我们只能自定义一个SpriteFrame

所以我这样hack了Engine:

    _customSpriteFrame: null, // 自定义SpriteFrame
    getTexture () {
        // 优先使用用户设置的纹理
        if(this._customSpriteFrame){
            return this._customSpriteFrame.getTexture();
        }else{
            return this._textureData && this._textureData.spriteFrame && this._textureData.spriteFrame.getTexture();
        }
    },

    setSpriteFrame(spriteFrame){
        this._customSpriteFrame = spriteFrame;
    },

运行效果:

image.png

这样子2个db不互相干扰了,但是发现纹理发生了错位,

如果不修正顶点坐标,就必须要求纹理布局一模一样,很明显我们换肤使用的纹理肯定和源纹理的布局是不同的。

顶点计算

 realTimeTraverse (armature, parentMat, parentOpacity) {
    let slots = armature._slots;
    let vertices, indices;

    for (let i = 0, l = slots.length; i < l; i++) {
        slot = slots[i];
        vertices = slot._localVertices;// 顶点来源
        // 顶点
        for (let vi = 0, vl = vertices.length; vi < vl;) {
            _x = vertices[vi++]; 
            _y = vertices[vi++];

            vbuf[_vfOffset++] = _x * _m00 + _y * _m04 + _m12; // x
            vbuf[_vfOffset++] = _x * _m01 + _y * _m05 + _m13; // y

            vbuf[_vfOffset++] = vertices[vi++]; // u
            vbuf[_vfOffset++] = vertices[vi++]; // v
            uintbuf[_vfOffset++] = _c; // color
        }
    }
}
  _updateFrame () {
        this._indices.length = 0;
        let indices = this._indices,
            localVertices = this._localVertices;
 

        let currentTextureData = this._textureData;
        if (!this._display || this._displayIndex < 0 || !currentTextureData || !currentTextureData.spriteFrame) return;

        let texture = currentTextureData.spriteFrame.getTexture();
        let textureAtlasWidth = texture.width;
        let textureAtlasHeight = texture.height;
        let region = currentTextureData.region;

        const currentVerticesData = (this._deformVertices !== null && this._display === this._meshDisplay) ? this._deformVertices.verticesData : null;
        
        if (currentVerticesData) {
            
        }
        else {
            // 因为我这边目前只使用了这个方式,也懒得研究了,就直接复用这部分代码即可,如果使用了其他模式可能会出现其他问题
            let l = region.x / textureAtlasWidth;
            let b = (region.y + region.height) / textureAtlasHeight;
            let r = (region.x + region.width) / textureAtlasWidth;
            let t = region.y / textureAtlasHeight;

            localVertices[vfOffset++] = 0; // 0x
            localVertices[vfOffset++] = 0; // 0y
            localVertices[vfOffset++] = l; // 0u
            localVertices[vfOffset++] = b; // 0v

            localVertices[vfOffset++] = region.width; // 1x
            localVertices[vfOffset++] = 0; // 1y
            localVertices[vfOffset++] = r; // 1u
            localVertices[vfOffset++] = b; // 1v

            localVertices[vfOffset++] = 0; // 2x
            localVertices[vfOffset++] = region.height;; // 2y
            localVertices[vfOffset++] = l; // 2u
            localVertices[vfOffset++] = t; // 2v

            localVertices[vfOffset++] = region.width; // 3x
            localVertices[vfOffset++] = region.height;; // 3y
            localVertices[vfOffset++] = r; // 3u
            localVertices[vfOffset++] = t; // 3v

            indices[0] = 0;
            indices[1] = 1;
            indices[2] = 2;
            indices[3] = 1;
            indices[4] = 3;
            indices[5] = 2;

            localVertices.length = vfOffset;
            indices.length = 6;
        }

        this._visibleDirty = true;
        this._blendModeDirty = true;
        this._colorDirty = true;
    },

currentTextureData.region的来源

var textureData = textureAtlasData.createTexture();
textureData.rotated = ObjectDataParser._getBoolean(rawTexture, dragonBones.DataParser.ROTATED, false);
textureData.name = ObjectDataParser._getString(rawTexture, dragonBones.DataParser.NAME, "");
textureData.region.x = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.X, 0.0);
textureData.region.y = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.Y, 0.0);
textureData.region.width = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.WIDTH, 0.0);
textureData.region.height = ObjectDataParser._getNumber(rawTexture, dragonBones.DataParser.HEIGHT, 0.0);

整体的对应逻辑关系如下图,很直观的能看到就是龙骨配置文件里面的数据

dragonBones - 副本.png

知道了原来的计算数据来源,我们直接套用即可

setSpriteFrame(spriteFrame){
    this._customSpriteFrame = spriteFrame;
    let texture = spriteFrame.getTexture();
    let textureAtlasWidth = texture.width;
    let textureAtlasHeight = texture.height;
    let region = spriteFrame.getRect(); // 这里
    // ...
},

至此,这种方式跑通了,也实现了最基础的换肤

后续问题

slot.display = null

image.png

龙骨在制作的时候,制作人员为了达到隐藏效果,直接把显示资源置空了,等到后续需要显示时,再切换显示,这就导致了slot.display=null的问题。

调试代码发现slot.display来自slot.displayList,直接修改slot.displayList即可。

顶点被重置

龙骨在播放一遍后,第二遍播放动画纹理又错位了

image.png

我首先想到的就是顶点出问题了,排查发现,顶点的确是被重置了。

image.png

相关的堆栈逻辑:

image.png

也不想改Engine了,既然他被重置了,那我在提交纹理时,再计算一次就行了,虽然可能有那么一丢丢的性能问题,但是这样不用改Engine,小游戏是可以复用微信内置的cocos引擎代码的,加快游戏加载。

const slot_getTexture = dragonBones.CCSlot.prototype.getTexture;
dragonBones.CCSlot.prototype.getTexture = function () {
  if (this._customSpriteFrame) {
    this.setSpriteFrame(this._customSpriteFrame);
    return this._customSpriteFrame.getTexture();
  } else {
    return slot_getTexture.call(this);
  }
};

最终效果

GIF 2023-12-27 11-09-40.gif