需求背景
龙骨文件比较复杂,和需求关联比较紧密的结构如下:
期望的效果如下,使用spriteFrame/atlas
替换Armature,达到局部/整体
换肤的效果
比如上图中使用了resources/yu.plist
对yu的Armature
进行了换肤
源码分析
切入点
js 复制代码 // 这个是渲染龙骨纹理的核心代码 material = _getSlotMaterial( slot.getTexture(), // 核心思路就是在这里做文章,达到换肤的效果 slot._blendMode ); getTexture () { return this._textureData && this._textureData.spriteFrame && this._textureData.spriteFrame.getTexture(); },
slot.texture来源
kotlin
复制代码
this._displayData = this._displayDatas[this._displayIndex];
来自这个rawDisplayData
js 复制代码 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; } } }
js 复制代码 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); } };
js 复制代码 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里面取 }
js 复制代码 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; };
js 复制代码 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; };
js 复制代码 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; };
js 复制代码 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); } });
整体思路图
换肤实现
换肤的基础
上图分析过程整个链路还是挺长的,不过仍旧可以在slot.getTexture
上做文章,因为在提交渲染时有material
的判断,应该是考虑到dragonbones会输出多个纹理
ini 复制代码 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
会导致连锁反应,所有的龙骨实例都会发生同步,这不是我想要的效果。
js 复制代码 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: js 复制代码 _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; },
运行效果:
这样子2个db不互相干扰了,但是发现纹理发生了错位,
如果不修正顶点坐标,就必须要求纹理布局一模一样,很明显我们换肤使用的纹理肯定和源纹理的布局是不同的。
顶点计算
js 复制代码 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 } } } js
复制代码 _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的来源
js 复制代码 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);
整体的对应逻辑关系如下图,很直观的能看到就是龙骨配置文件里面的数据
知道了原来的计算数据来源,我们直接套用即可
js 复制代码 setSpriteFrame(spriteFrame){ this._customSpriteFrame = spriteFrame; let texture = spriteFrame.getTexture(); let textureAtlasWidth = texture.width; let textureAtlasHeight = texture.height; let region = spriteFrame.getRect(); // 这里 // ... },
至此,这种方式跑通了,也实现了最基础的换肤
后续问题
slot.display = null
龙骨在制作的时候,制作人员为了达到隐藏效果,直接把显示资源置空了,等到后续需要显示时,再切换显示,这就导致了slot.display=null的问题。
调试代码发现slot.display
来自slot.displayList
,直接修改slot.displayList
即可。
顶点被重置
龙骨在播放一遍后,第二遍播放动画纹理又错位了
我首先想到的就是顶点出问题了,排查发现,顶点的确是被重置了。
相关的堆栈逻辑:
也不想改Engine了,既然他被重置了,那我在提交纹理时,再计算一次就行了,虽然可能有那么一丢丢的性能问题,但是这样不用改Engine,小游戏是可以复用微信内置的cocos引擎代码的,加快游戏加载。
js 复制代码 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); } };
最终效果