cocos craetor2.4.5 逆向之属性面板显示原理
最近在封装一个组件,有一个字符串枚举类型, 发现不支持, 想探探原因,看看能否hook解决这个问题! 因为数字枚举类型编辑器是支持的。
cocos creator支持的所有属性类型分别是:
[ "string", "number", "boolean", "array", "object", "enum", "color", "vec2", "vec3", "String", "Float", "Boolean", "Object", "Integer", "Enum", "asset", "cc.Asset", "cc.Node", "cc.Vec2", "cc.Vec3", "cc.Size", "cc.Color", "cc.Rect", "cc.Vec4" ]
所有类型的显示模版如下:
string template(t) { let i; return (i = t.multiline ? '\n <ui-text-area class="flex-1" resize-v></ui-text-area>\n ' : '\n <ui-input class="flex-1"></ui-input>\n '); } =================== number template(t) { let i; return (i = t.slide ? '\n <ui-slider class="flex-1"></ui-slider>\n ' : '\n <ui-num-input class="flex-1"></ui-num-input>\n '); } =================== boolean <ui-checkbox class="flex-1"></ui-checkbox> =================== array <ui-num-input class="flex-1"></ui-num-input> <div slot="child"></div> =================== object <div class="child" slot="child"></div> =================== enum <ui-select class="flex-1"></ui-select> =================== color <ui-color class="flex-1"></ui-color> =================== vec2 <ui-prop name="X" id="x-comp" slidable class="fixed-label red flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="Y" id="y-comp" slidable class="fixed-label green flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> =================== vec3 <ui-prop name="X" id="x-comp" slidable class="fixed-label red flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="Y" id="y-comp" slidable class="fixed-label green flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="Z" id="z-comp" slidable class="fixed-label blue flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> =================== String template(t) { let i; return (i = t.multiline ? '\n <ui-text-area class="flex-1" resize-v></ui-text-area>\n ' : '\n <ui-input class="flex-1"></ui-input>\n '); } =================== Float template(t) { let i; return (i = t.slide ? '\n <ui-slider class="flex-1"></ui-slider>\n ' : '\n <ui-num-input class="flex-1"></ui-num-input>\n '); } =================== Boolean <ui-checkbox class="flex-1"></ui-checkbox> =================== Object <div class="child" slot="child"></div> =================== Integer template(t) { let i; return (i = t.slide ? '\n <ui-slider class="flex-1"></ui-slider>\n ' : '\n <ui-num-input class="flex-1" type="int"></ui-num-input>\n '); } =================== Enum <ui-select class="flex-1"></ui-select> =================== asset (t) => `\n <ui-asset class="flex-1" type="${t.assetType}"></ui-asset>\n ` =================== cc.Asset (t) => `\n <ui-asset class="flex-1" type="${t.assetType}"></ui-asset>\n ` =================== cc.Node (t) => `\n <ui-node class="flex-1"\n type="${t.typeid}"\n typename="${t.typename}"\n ></ui-node>\n ` =================== cc.Vec2 <ui-prop name="X" id="x-comp" subset slidable class="fixed-label flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="Y" id="y-comp" subset slidable class="fixed-label flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> =================== cc.Vec3 <ui-prop name="X" id="x-comp" subset slidable class="fixed-label flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="Y" id="y-comp" subset slidable class="fixed-label flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="Z" id="z-comp" subset slidable class="fixed-label flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> =================== cc.Size <ui-prop name="W" id="w-comp" subset slidable class="fixed-label flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="H" id="h-comp" subset slidable class="fixed-label flex-1"> <ui-num-input class="flex-1"></ui-num-input> </ui-prop> =================== cc.Color <ui-color class="flex-1"></ui-color> =================== cc.Rect <div class="vertical flex-1"> <div class="layout horizontal"> <ui-prop subset slidable name="X" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;"> <ui-num-input id="x-input" class="flex-1"></ui-num-input> </ui-prop> <ui-prop subset slidable name="Y" class="fixed-label flex-1" style="min-width: 0;"> <ui-num-input id="y-input" class="flex-1"></ui-num-input> </ui-prop> </div> <div class="layout horizontal"> <ui-prop subset slidable name="W" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;"> <ui-num-input id="w-input" class="flex-1"></ui-num-input> </ui-prop> <ui-prop subset slidable name="H" class="fixed-label flex-1" style="min-width: 0;"> <ui-num-input id="h-input" class="flex-1"></ui-num-input> </ui-prop> </div> </div> =================== cc.Vec4 <div class="vertical flex-1"> <div class="layout horizontal"> <ui-prop name="X" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;"> <ui-num-input id="x-input" class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="Y" class="fixed-label flex-1" style="min-width: 0;"> <ui-num-input id="y-input" class="flex-1"></ui-num-input> </ui-prop> </div> <div class="layout horizontal"> <ui-prop name="Z" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;"> <ui-num-input id="z-input" class="flex-1"></ui-num-input> </ui-prop> <ui-prop name="W" class="fixed-label flex-1" style="min-width: 0;"> <ui-num-input id="w-input" class="flex-1"></ui-num-input> </ui-prop> </div> </div>
有一个关键算法, 它是获取所有属性源头:
function getClassAttrs(ctor) { return ctor.hasOwnProperty('__attrs__') && ctor.__attrs__ || createAttrs(ctor); } function attr(ctor, propName, newAttrs) { var attrs = getClassAttrs(ctor); if (!CC_DEV || typeof newAttrs === 'undefined') { // get var prefix = propName + DELIMETER; var ret = {}; for (var key in attrs) { if (key.startsWith(prefix)) { ret[key.slice(prefix.length)] = attrs[key]; } } return ret; } else if (CC_DEV && typeof newAttrs === 'object') { // set cc.warn("`cc.Class.attr(obj, prop, { key: value });` is deprecated, use `cc.Class.Attr.setClassAttr(obj, prop, 'key', value);` instead please."); for (var _key in newAttrs) { attrs[propName + DELIMETER + _key] = newAttrs[_key]; } } } // returns a readonly meta object
creator编辑器自己有一个运行时, 所有的对象存放cc.engine.attachedObjsForEditor:

如果是数字枚举, 结果如下:

如果是字符串枚举, 结果如下:

这个就是问题所在了,类型是Enum,默认值string类型,enumList 为空。
enumList 元素对象类型是{name:xxx, value: v}
enumList 的数据来源算法是:
cc.Enum.getList = function (enumDef) { if (enumDef.__enums__) return enumDef.__enums__; var enums = enumDef.__enums__ = []; for (var name in enumDef) { var value = enumDef[name]; if (Number.isInteger(value)) { enums.push({ name: name, value: value }); } } enums.sort(function (a, b) { return a.value - b.value; }); return enums; };
从这个代码就可以知道enumList为啥空了, 因为只处理了interger.
写一个插件看看, 强行让它也可以处理string类型看看

现在插件运行正确,确实能赋值给enumList了, 但是还是显示类型错误

那么就需要从显示逻辑着手了,为啥显示Type Error:
这是错误组件cc-type-error-prop:

看看什么时候会用cc-type-error-prop组件:

通过断点找到了异常的地方, 大概的意思值类型String和Enum不是一个类型

这个数据本身没问题, 因为字符串枚举, 底层本身就是string。
从初步分析结果来看,数字枚举应该也会显示Type Error才对, 因为number类似不等于Enum, 再次分析正确的情况:

非常好,这就是为啥不显示的Type Error的原因了, value是2,但是o却是Enum. 并没有识别出是Number
为啥value=2能识别出是Enum, 而value='def'确是string?
继续往前跟踪, 发现之前忽略一点,只关注了prop属性值, 没有关心value值:

继续往前,看看为啥数字2为啥可以识别出Enum:

这个数字2,源头就是Enum, 再看看字符串枚举, 这个源头是啥:

数据源头也是Enum, 但是经过Editor.getNodeDump(node) 之后变成了String:

继续挖掘, 找到了根源:

如果原始数据是Enum, 值是Number类型时,强制用Enum, 而忽略了string类型。 这也是为啥Type Error的真正原因。
知道原因就好处理了, 直接hook Editor.getNodeDump方法, 解决编辑器不支持字符串枚举的方法是写一个插件,代码如下:
if(CC_EDITOR){ cc.Enum.getList = function (enumDef) { if (enumDef.__enums__) return enumDef.__enums__; var enums = enumDef.__enums__ = []; let isStr = false; for (var name in enumDef) { var value = enumDef[name]; isStr = typeof value === 'string'; enums.push({name: name,value: value}); } !isStr && enums.sort(function (a, b) { return a.value - b.value; }); return enums; }; const oldDump = Editor.getNodeDump; Editor.getNodeDump = function(node){ const ret = oldDump(node); let {types, value} = ret; if(value.__comps__){ value.__comps__.forEach((comp)=>{ const cmpType = types[comp.type]; const properties = cmpType.properties; for(let propKey in properties){ const prop = properties[propKey]; if(prop.type === 'Enum'){// 真实数据是Enum类型 comp.value[propKey].type = 'Enum'; } } }); } return ret; } Editor.UI.getProperty('Enum').inputValue = function(){ const ty = this.get_real_type(); if(ty == 'string'){ return this.$input.value; } return Number(this.$input.value); } Editor.UI.getProperty('Enum').get_real_type = function(){ if(this.__ty){ return this.__ty; } let ty = 'string'; try{ ty = typeof this._attrs.enumList[0].value; }catch(e){ // pass } this.__ty = ty; return ty; } const oldEidtorWarn = Editor.warn; Editor.warn = function(s){// hook 枚举类型保存错误的警告!因为已经支持string类型了 if(typeof s == 'string' && s.startsWith('Expecting number type of value for')){ return; } return oldEidtorWarn.apply(Editor, arguments); } }
附上最终的效果图:

碰到字符串枚举类型时编辑器再也不显示Type Error了, 哈哈,完美收工!!!
经过实际测试已经支持 string float, int 的枚举类型了。
比如:

浙公网安备 33010602011771号