需求背景
龙骨文件比较复杂,和需求关联比较紧密的结构如下:
期望的效果如下,使用spriteFrame/atlas
替换Armature,达到局部/整体
换肤的效果
比如上图中使用了resources/yu.plist
对yu的Armature
进行了换肤
源码分析
切入点
// 这个是渲染龙骨纹理的核心代码
material = _getSlotMaterial(
slot.getTexture(), // 核心思路就是在这里做文章,达到换肤的效果
slot._blendMode
);
getTexture () {
return this._textureData && this._textureData.spriteFrame && this._textureData.spriteFrame.getTexture();
},
slot.texture来源
this._displayData = this._displayDatas[this._displayIndex];
来自这个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);
}
});
整体思路图
换肤实现
换肤的基础
上图分析过程整个链路还是挺长的,不过仍旧可以在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;
},
运行效果:
这样子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);
整体的对应逻辑关系如下图,很直观的能看到就是龙骨配置文件里面的数据
知道了原来的计算数据来源,我们直接套用即可
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引擎代码的,加快游戏加载。
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);
}
};