Unity3D 插件开发教程 #2:制作批处理工具

作者:Lawliet
2017-08-26
10 13 2

编者按

本文已于作者 @Lawliet 授权转载,原载于知乎,如需转载请务必联系原作者。

引言

俗语说:工欲善其事,必先利其器。

一个好的工具能让你的工作进度加快不少。

在制作关卡时,很多时候会遇到同一个物体可能需要复制多份,然后分布在不同地方。如果 一个个复制太浪费时间了,而美术和策划又不会使用代码批量复制。这时候,就需要做一个批量工具来加快制作的效率了。

正文

首先来看一下我们今天要做的批量工具面板,并且可以根据输入的变量,批量的变更每个的位移,旋转和缩放:

1
知识要点:

  • EditorWindow
  • GUI/GUILayout/EditorGUI/EditorGUILayout
  • Selection
  • Undo

使用版本:

  • Unity3D 5.3.3

目标:

  • 学习创建 EditorWindow 面板,然后使用GUI等工具绘制面板,最后批量复制对象。

整个插件的结构:
2
上一篇教程一样,在Editor目录下创建我们的批处理面板脚本————BatchingLiteWindow.cs,然后继承EditorWindow

EditorWindow是所有编辑器面板的基类,绘制面板必须要继承它。

然后我们使用MenuItem和静态函数添加启动面板的菜单。

MenuItem 的使用方法请看上一篇教程的最后部分。

public class BatchingLiteWindow : EditorWindow
{
    [MenuItem("Tools/BatchingLite")]
    public static void ShowWindow()
    {
        //GetWindow函数的意思是创建一个面板
        //类型为BatchingLiteWindow
        //第一个参数是面板的标题
        EditorWindow.GetWindow("Batcking");
    }
}

然后我们定义几个变量,用于后面使用。

/// 
/// 位移增量
/// 
private Vector3 _position;

/// 
/// 旋转增量
/// 
private Vector3 _rotation;

/// 
/// 缩放增量
/// 
private Vector3 _scale;

/// 
/// 复制的数量
/// 
private int _number;

好了,接下来,就是本文的重点之一,绘制面板了。 首先,我们定义一个函数叫OnGUI,返回值为 void。

说到 OnGUI,用过老版本 Unity 引擎的朋友应该很清楚了。这是一套 Unity 最早的 UI 引擎。这套 UI 系统有别于现在流行的UGUINGUI,是一套imGUI(Immediate Mode GUI)。如果需要深入展开 imGUI 的原理来讲,那么可能需要好几个篇章,所以在此只讲一下怎么使用。 如果有兴趣的朋友可以上网搜索资料,或者到看这个回答 如何用 C++ 从零编写 GUI? - 回答作者: 文刀秋二
首先,绘制面板,一定要使用这套GUI,并且需要在特定函数内使用,例如OnGUIOnSceneGUIOnInspectorGUIOnHeaderGUIOnPreviewGUI等。 其次,imGUI 其中一个特性是不保存状态的,例如UGUINGUI的按钮类都会保存按钮当前是按下状态还是松开状态,可imGUI的按钮是不保存这个的。
imGUI有四个绘制类。分别是GUIGUILayoutEditorGUIEditorGUILayout,他们有相同的地方和不同的地方。

  • GUI:多用与应用/游戏内绘制 UI。(编辑器绘制也可使用)
  • GUILayout:GUI的功能上增加了布局的功能。
  • EditorGUI:用于编辑器内绘制 UI。(仅限于编辑器内使用)
  • EditorGUILayout:EditorGUI 的功能上增加了布局的功能。

好了,接下就开始写绘制面板的逻辑了。

void OnGUI()
{
    //使用Vector3Field方法绘制 Vector3的输入框,第一个为输入框的标签(显示的名字),第二个参数需要传入需要显示的Vector3值。
    //返回值为一个Vector3,当没有修改的时候,这个值为原来的值,当有修改的时候,这个返回值就是修改后的值。
    //例如,把返回值赋予给_position,这样,输入框有修改的时候,_position能够拿到最新的值。
    _position = EditorGUILayout.Vector3Field("Position", _position);

    _rotation = EditorGUILayout.Vector3Field("Rotation", _rotation);

    _scale = EditorGUILayout.Vector3Field("Scale", _scale);

    //Space的作用是空一行
    EditorGUILayout.Space();

    //然后使用IntField方法绘制一个int类型的输入框,使用与Vector3相似
    //由于复制的数量不能为负数,所以我们要限制一下修改后的数值
    _number = Mathf.Max(EditorGUILayout.IntField("Number", _number), 0);

    EditorGUILayout.Space();

    //BeginHorizontal方法和EndHorizontal是成对存在的,然后他们的作用是水平布局,在两个函数内绘制的UI会限制在一个水平位置。
    //相似的方法还有BeginVertical和EndVertical,是垂直布局。
    EditorGUILayout.BeginHorizontal();

    //绘制一个Generate Button,这里使用GUILayout而不使用EditorGUILayout是因为EditorGUILayout没有Button(不知道原因)。
    //Button方法第一个参数是button显示的label。
    //返回值为Button是否为点击,
    if (GUILayout.Button("Generate"))
    {
        Generate();
    }

    //这里缓存Cancel按钮的状态,在EndHorizontal之后再调用Cancel方法。
    bool isCancel = GUILayout.Button("Cancel");

    EditorGUILayout.EndHorizontal();

    if (isCancel)
    {
        Cancel();
    }
}

绘制完面板,然后就开始写复制部分的逻辑:

/// 
/// 生成复制对象
/// 
private void Generate()
{
    //上一篇有介绍过Selection.activeGameObject是选中的对象,然后Selection.gameObjects是多选时,所有选中的对象。
    //因为有可能是多个对象同时复制,所以使用选中对象组
    GameObject[] selectGameObjects = Selection.gameObjects;

    int len = selectGameObjects.Length;

    for (int i = 0; i < len; i++)
    {
        GameObject selectGameObject = selectGameObjects[i];

        for (int j = 0; j < _number; j++)
        {
            //根据选中对象,实例化对象,然后根据索引和增量,设置移动、旋转、缩放
            GameObject gameObject = GameObject.Instantiate(selectGameObject);

            gameObject.transform.SetParent(selectGameObject.transform.parent);

            gameObject.transform.localPosition = selectGameObject.transform.localPosition + _position * j;

            gameObject.transform.localRotation = selectGameObject.transform.localRotation * Quaternion.Euler(_rotation * j);

            gameObject.transform.localScale = selectGameObject.transform.localScale + _scale * j;

            gameObject.name = selectGameObject.name;

            //Undo是Unity3d用于设置步骤,执行/撤销等
            //RegisterCreatedObjectUndo是注册一个新创建的对象的步骤,然后名字为“Batching Create GameObject”,用于
            Undo.RegisterCreatedObjectUndo(gameObject, "Batching Create GameObject");
        }
    }
}

最后是Cancel方法

/// 
/// 取消操作
/// 同ctrl + z
/// 
private void Cancel()
{
    //PerformUndo作用跟ctrl + z一样
    Undo.PerformUndo();
}

解释一下,为啥 OnGUI 时,Cancel为什么要EditorGUILayout.EndHorizontal之后才调用,这是因为Cancel会导致之前EditorGUILayout.StartHorizontal的标记没了,然后执行EditorGUILayout.EndHorizontal会报错。

最后可以试一下面板的效果:
3
4

增加一个彩蛋,做了一个100*100*100的正方体矩阵然后直接占满 8g 内存.......

5

后记

如果大家有什么意见和建议,或者是有什么疑问,或者是有想看的知识点内容,都欢迎到评论区发上你们的评论。

最后我希望有更多人参与到插件开发的队伍里。也欢迎大家投稿。