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號