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。 

enum CmpIconGroup {
  ABC = 'abc',
  DEF = 'def',
}

从初步分析结果来看,数字枚举应该也会显示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 的枚举类型了。

比如:

export const CmpIconSize = {
SCALE_0_3: 0.3,
SCALE_0_5: 0.5,
SCALE_0_8: 0.8,
SCALE_1 : 1,
SCALE_1_2: 1.2,
SCALE_1_5: 1.5,
SCALE_2 : 2,
};
 
后续属性设置可以直接 
@property({
type: cc.Enum(CmpIconType),
tooltip: '图标类型',
})
iconSize = CmpIconType.SCALE_1_2;
 
 

 

posted @ 2025-06-07 15:53  浪浪辛  阅读(113)  评论(0)    收藏  举报