Tech · ArtUnity

「Unity」GITreeview编辑器拓展以及位运算小补课

by Ayse, 2022-06-30


Unity

1.png 最近都在写编辑器拓展的小工具,感觉做这方面的内容写起来就像自己真的参与到了项目中x。大概完成的是一个GI检索的小工具,包含几个功能

  1. 检索在Hierarchy中选择的物体并广度优先(Queue)找到其最下层含有LODgroup或者Renderer的物体,并将其GI信息显示在面板上。同时有FrozeWindow功能,便于美术在Editor中操作。
  2. 检索后支持在页面内勾选物体通过“Apply Selection in Hierarchy”进行反选至Hierarchy,此操作支持在面板内多选并一次性勾选。
  3. 为了便于单个物体的调整,暴露了values<->Control的转换接口,可以直接在这里做简单的调整
  4. Expand和Collapse的功能

一般编写一个Editor拓展需要两样东西,首先是内容脚本,然后是继承EditorWindow 的window脚本。其他的GUI拓展也是一样的。这次用的是Unity的Treeview封装功能,所以我们就根据文档来建模板,官方文档跳转 官方其实封装得还挺完整的,记录一下自己在编写过程中遇到的小坑。


继承Treeview

public ArtTransformTreeView (TreeViewState state,MultiColumnHeader multicolumnHeader)
        : base (state,multicolumnHeader)
    {
        rowHeight = kRowHeights;
        columnIndexForTreeFoldouts = 1;
        showAlternatingRowBackgrounds = true;
        showBorder = true;
        customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f;
        Reload ();
    }

像这里就是C#和C++派生类写构造函数的写法(当然base好像是C#特有的关键字),意思是这两也是这个Treeview构造函数的参数,然后下面就把一些Treeview里Protected的参数加上一些值。


Window里面的Onenable

主要就是实现上面提到的构造函数,基本上就是构造出这个State和Header然后去初始化Treeview State的功能是标记的状态之类的Collumn 便于后面在RowGUi方法中可以调用,2.png

public MyMultiColumnHeader(MultiColumnHeaderState state)
        : base(state)
    {
        mode = Mode.DefaultHeader;
    }

    public Mode mode
    {
        get
        {
            return m_Mode;
        }
        set
        {
            m_Mode = value;
            switch (m_Mode)
            {
                case Mode.LargeHeader:
                    canSort = true;
                    height = 37f;
                    break;
                case Mode.DefaultHeader:
                    canSort = true;
                    height = DefaultGUI.defaultHeight;
                    break;
                case Mode.MinimumHeaderWithoutSorting:
                    canSort = false;
                    height = DefaultGUI.minimumHeight;
                    break;
            }
        }
    }

    protected override void ColumnHeaderGUI (MultiColumnHeaderState.Column column, Rect headerRect, int columnIndex)
    {
        // Default column header gui
        base.ColumnHeaderGUI(column, headerRect, columnIndex);

        // Add additional info for large header
        if (mode == Mode.LargeHeader)
        {
            // Show example overlay stuff on some of the columns
            if (columnIndex > 2)
            {
                headerRect.xMax -= 3f;
                var oldAlignment = EditorStyles.largeLabel.alignment;
                EditorStyles.largeLabel.alignment = TextAnchor.UpperRight;
                GUI.Label(headerRect, 36 + columnIndex + "%", EditorStyles.largeLabel);
                EditorStyles.largeLabel.alignment = oldAlignment;
            }
        }
    }

Header嘛,顾名思义就是表格的头条,这里贴代码的原因是解释一下getset方法的妙用,可以让非法输入直接过滤掉,两个字,安全。剩下window里面都是操作相关的脚本了。


修改Treeview生成中重要的脚本

在本个脚本中,Treeview是封装好的一个类,提供了基本的建表方法, 我们只需要改他们的这个重要方法就行,上面的图也可以看出,BuildRoot和BuildRows是最重要的,不过前者好像只是做了一个树根,重写BuildRows可以根据用的方法获得所有想要的物体。然后上面那张图没写的是BuildRows之后有一个RowGUI方法,这个在原来的TreeView里面就已经实现了,主要是可以生成item的名字,它会自动获取Buildrows之后的args作为要素,所以我们的root直接就会被建立。这是RowGUI方法里面的一部分,它会为每一个item也就是每一个row逐个调用 3.png 名字后面还有一个调用父类的方法,主要是父类包含了名字的部分,我们复写的其实只是toggle的部分。


其他的小提示

ExpandAll()方法

按照文档,理论上我们可以直接调用实现Expandall()方法,只需要重写GetRecursiveChild但是发现报了一个我甚至无从debug的空指针bug,说这个函数传入的参数连Getgameobject都获取不到,然后我也不知道要怎么继续去debug了,所以不如自己重写一个。发现Treeview里面的Setexpand还能用,直接建树的时候就保存一组数据就可以了。

同步Hierarchy方法

获取Scene然后用SceneManager的方法获取即可,一般方法都包括了一个Selection类封装好了,我们只需要调用即可。

脚本工具的撤回功能

在编写的时候加上这下面的代码使得系统知道你做出了改变

Undo.RecordObject(m_gameObject,"GISingleModification2");
m_gameObject.receiveGI=(ReceiveGI)EditorGUI.EnumPopup(cellRect,m_gameObject.receiveGI);
PrefabUtility.RecordPrefabInstancePropertyModifications(m_gameObject);

EnumFlag

被老板提醒enum这类属性删除的时候假如没有一个nothing或者有多个值的时候可以用下面的方式删除,但是个中原因自己并没有太清楚,于是查了一下这个叫做掩码

StaticEditorFlags flags = GameObjectUtility.GetStaticEditorFlags(gameObject);
GameObjectUtility.SetStaticEditorFlags(gameObject, flags & (~(StaticEditorFlags.ContributeGI)));

举一个例子就是用二进制代表更多的数,看到一篇博客里面用了一个有趣的比喻,如果我要确定8个瓶子里哪个有毒,只需要三只了老鼠,因为

000=0
001=1
010=2
011=3
100=4
101=5
110=6
111=7

1此时代表的是死掉的老鼠,也就是分别将1、3、5、7号瓶子的药混起来给老鼠1吃,2、3、6、7号瓶子的药混起来给老鼠2吃,4、5、6、7号瓶子的药混起来给老鼠3吃。分药的过程可以反推来理解,假如3号瓶有毒,被毒死的就是12两只老鼠,所以12都需要吃3号瓶————类似用结果反推现实,同理只需要10个老鼠就可以确定1000个瓶子。

    // 是否允许查询,二进制第1位,0表示否,1表示是
    public static final int ALLOW_SELECT = 1 << 0; // 0001
    // 是否允许新增,二进制第2位,0表示否,1表示是
    public static final int ALLOW_INSERT = 1 << 1; // 0010
    // 是否允许修改,二进制第3位,0表示否,1表示是
    public static final int ALLOW_UPDATE = 1 << 2; // 0100
    // 是否允许删除,二进制第4位,0表示否,1表示是
    public static final int ALLOW_DELETE = 1 << 3; // 1000

那么这个位运算就来了,假如我有四个enumflag,我就相当于四只老鼠,然后我们就很方便用一个int就可以存权限的开关了,这里补充位运算的方法,|代表加上一个(有1个1就为1),~代表取反,&代表包含(有两个1才为1), flag & XXX_FLAG != 0 或者 flag & XXX_FLAG = XXX_FLAG。 去除属性则用 flag &= ~XXX_FLAG; (相当于先给flag反了之后只有去除的值为0,然后包含运算一下)上面代码的例子是先用一个flag来确定然后在直接等于去set。

位运算的其他用法

这个还有其他的用法,比如

a&1 = 0 偶数
a&1 = 1 奇数 
//求平均值防止INT溢出
public int average(int x, int y){ 
    return (x&y)+((x^y)>>1); 
}
//判断正整数是否是2的幂
public boolean power2(int x) { 
return ((x&(x-1))==0)&&(x!=0); 
}

类型

C# 里面强制转换可以用 as 如果转化失败会返回null 如果用(blabla)就可能会报错 判断数据类型可以用is

找到LOD和renderer物体的方法

感觉理论上可能有更便捷的方法,我这里用的是一个广度优先搜索,维护了一个内部不存在父子关系的Queue作为根物体列表实现的,有一点点像树的遍历的非递归方法。

protected override TreeViewItem BuildRoot()
        {
            return new TreeViewItem {id = 0, depth = -1};
        }

        //初始化找到根物体
        protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
        {
            //Scene scene = SceneManager.GetSceneAt (0);
            //var gameObjectRoots = scene.GetRootGameObjects ();
            var rows = GetRows () ?? new List<TreeViewItem> (200);
            rows.Clear ();
            List<GameObject> gameObjectRoots = new List<GameObject>();
            expandID.Clear();

            if (!Frozetoggle)
            {
                List<int> filterid = Selection.instanceIDs.ToList();

                //avoid resort gameobject or random pick or scene
                //过滤子物体和父物体多选以及scene
                for (int i = filterid.Count - 1; i >= 0; i--)
                {
                    int tempid = filterid[i];

                    if (!GetGameObject(tempid))
                    {
                        filterid.Remove(filterid[i]);
                        continue;
                    } //scene?

                    while (GetGameObject(tempid).transform.parent)
                    {
                        tempid = GetGameObject(tempid).transform.parent.gameObject.GetInstanceID();
                        if (filterid.Contains(tempid))
                        {
                            filterid.Remove(filterid[i]);
                            break;
                        }
                    }
                }

                if (filterid.Count == 0 ) { return rows; }

                //队列遍历找到所有有lod的和所有有renderer的根物体
                Queue<int> retreiveid = new Queue<int>(filterid);
                while (retreiveid.Count != 0)
                {
                    var id = retreiveid.Dequeue();
                    GameObject temp = GetGameObject(id);
                    var templod = temp.GetComponent<LODGroup>();
                    var tempmeshr = temp.GetComponent<MeshRenderer>();
                    if (templod != null || tempmeshr != null)
                    {
                        if (!gameObjectRoots.Contains(temp))
                            gameObjectRoots.Add(temp);
                    }
                    else
                    {
                        for (int i = 0; i < temp.transform.childCount; ++i)
                        {
                            var sonid = temp.transform.GetChild(i).gameObject.GetInstanceID();
                            retreiveid.Enqueue(sonid);
                        }
                    }
                }
                FrozegameObjectRoots = gameObjectRoots;
            }
            else
            {
                gameObjectRoots = FrozegameObjectRoots;
            }

            //找到根物体之后开始找儿物体
            foreach (var gameObject in gameObjectRoots)
            {
                var item = CreateTreeViewItemForGameObject (gameObject);
                expandID.Add(item.id);
                root.AddChild (item);
                rows.Add (item);
                if (gameObject.transform.childCount > 0)
                {
                    if (IsExpanded (item.id))
                    {
                        AddChildrenRecursive (gameObject, item, rows);
                    }
                    else
                    {
                        item.children = CreateChildListForCollapsedParent ();
                    }
                }
            }

            SetupDepthsFromParentsAndChildren (root);
            return rows;
        }

Enjoy

作者: Ayse

2024 © typecho & elise