editor unity3d
2013-11-30 21:52:30 shengfanger 阅读数 1029

splitview

using UnityEngine;
using UnityEditor;

public class SplitViewWindow : EditorWindow
{
    private Vector2 scrollPos = Vector2.zero;
    float currentScrollViewHeight;
    bool resize = false;
    Rect cursorChangeRect;

    [MenuItem("MyWindows/SplitView")]
    public static void Init()
    {
        EditorWindow t = GetWindow<SplitViewWindow>();
    }

    void OnEnable()
    {
        this.position = new Rect(200, 200, 400, 300);
        currentScrollViewHeight = this.position.height / 2;
        cursorChangeRect = new Rect(0, currentScrollViewHeight, this.position.width, 5f);
    }

    void OnGUI()
    {
        GUILayout.BeginVertical();
        scrollPos = GUILayout.BeginScrollView(scrollPos, GUILayout.Height(currentScrollViewHeight));
        for (int i = 0; i < 20; i++)
            GUILayout.Label("dfs");
        GUILayout.EndScrollView();

        ResizeScrollView();

        GUILayout.FlexibleSpace();
        GUILayout.Label("Lower part");

        GUILayout.EndVertical();
        Repaint();
    }

    private void ResizeScrollView()
    {
        GUI.color = Color.blue;
        GUI.DrawTexture(cursorChangeRect, EditorGUIUtility.whiteTexture);
        EditorGUIUtility.AddCursorRect(cursorChangeRect, MouseCursor.ResizeVertical);

        if (Event.current.type == EventType.mouseDown && cursorChangeRect.Contains(Event.current.mousePosition))
        {
            resize = true;
        }
        if (resize)
        {
            currentScrollViewHeight = Event.current.mousePosition.y;
            cursorChangeRect.Set(cursorChangeRect.x, currentScrollViewHeight, cursorChangeRect.width, cursorChangeRect.height);
        }
        if (Event.current.type == EventType.MouseUp)
            resize = false;
    }
}


2018-05-31 23:06:56 u012632851 阅读数 2052

Unity3D Editor 编辑器扩展3 Editor脚本

环境:Unity2017.2 语言:C#

 

总起:

在编辑Unity项目的时候,总不可能避免的接触到Unity自身自带的Inspector参数调整无法很好的完成某些特定功能的情况。

 

比方说想要一个按钮直接能跳到游戏的主界面进行调试,然而普通的情况下甚至连添加一个按钮都做不到。

 

这边推荐一下本人花了20几刀购买的插件——Odin Inspector & Serializer: http://sirenix.net/odininspector。该插件能够摒弃我今天要讲的Editor脚本,非常轻松的就能完成一些炫酷的EditorUI。

 

Odin,我其实研究的不是太深入,因为在实际的项目中并没有用到,所以该插件的介绍,我想过段时间深入研究后再说。

 

今天的主角是Editor脚本。

 

Editor脚本:

♦ 简单介绍

首先Editor脚本必须放在Editor文件夹或其子文件夹下,然后我们来看一个Edior脚本的例子:

//PlayerEditor.cs
[CustomEditor(typeof(Player))]
public class PlayerEditor : Editor
{
    private SerializedProperty hpProperty;

    void Awake()
    {
        Debug.Log("Awake");
    }

    void OnEnable()
    {
        Debug.Log("OnEnable");
        hpProperty = serializedObject.FindProperty("hp");
    }

    public override void OnInspectorGUI()
    {
        // 更新serializedObject,在OnInspectorGUI一开始就要调用该函数
        serializedObject.Update();

        // 创建一个Int的滑动条
        EditorGUILayout.IntSlider(hpProperty, 0, 100, new GUIContent("HP"));

        // 使用自定义的滑动条
        ProgressBar(hpProperty.intValue / 100f, "HP");

        // 保存所有对脚本修改的值
        serializedObject.ApplyModifiedProperties();
    }

    // 自定义一个滑动条
    void ProgressBar(float value, string label)
    {
        Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
        EditorGUI.ProgressBar(rect, value, label);
        EditorGUILayout.Space();
    }

    void OnDisable()
    {
        Debug.Log("OnDisable");
    }

    void OnDestroy()
    {
        Debug.Log("OnDestroy");
    }
}

// Player.cs
public class Player : MonoBehaviour
{
    public int hp;
}


效果如下:


可以注意到,PlayerEditor是继承于Editor的,除了Awake、OnEnable这类熟悉的生命周期方法外,脚本中很多操作使用到了UnityEditor.dll里的内容,而该dll是不会被打包的,所以非Editor文件夹底下的脚本千万不要引用到UnityEditor这个命名空间了。

 

Editor父类中的serializedObject代表的正是当前操作的这个脚本,使用以下操作可能更加明了:

Player player = (Player)serializedObject.targetObject;

Debug.Log(player.hp);

 

使用其成员变量targetObject可以直接获取到对应脚本进行操作。

 

具体OnInspectorGUI中的内容,我不想详细介绍了,主要是使用EditorGUILayout、GUILayout、GUILayoutUtility这三个类。

 

说到Editor,其实第一篇文章提到的MenuItem最好就是写在这样的脚本中。

 

♦ Editor脚本的结构

相比于介绍Editor的使用,我更想深入了解一下Editor这个类(使用方面,还有Odin完全可以代替Editor脚本呢)。

 

Editor的继承结构:Editor -> ScriptableObject -> UnityEngine.Object -> object。

 

Editor除了继承于ScriptableObject外,还实现了IPreviewable、IToolModeOwner接口。

 

我们刚刚使用的serializedObject是根据我们写的Component生成的SerializedObject(注意不是Editor的父类ScriptableObject)对象。

 

Editor的父类ScriptableObject可能很多人都用过,他可以很方便的保存一些配置信息,Prefab、资源引用什么的。

 

SerializedObject是UnityEditor命名空间的,用于以通用的方式操作对象的属性,可以自动处理撤销操作,和产生一些预制的样式UI。

 

看到这里,我越来越好奇Unity的对象序列化了,但是这边反编译的C#脚本引用的都是C++内容。

 

感觉有些遗憾,手头也没有C++代码,如果有大神指导如何获取该源码,希望能指条明路,谢谢!

 

个人:

看源码的时候,多数都会追到C++的代码,看得唯一感觉比较友好的就是UGUI的,因为他基本上都是基于C#来编写的,虽然内容比较繁杂,但是总也能理清思路。

 

我手头上有份Unity4 C++,但感觉不是很全,Transform几个类倒是有,像SerializedObject便找不到了,可能是我姿势不对。


2018-11-21 18:43:00 weixin_34210740 阅读数 3

需求分析

  在实际工作过程中,美术资源并不总是按照预想的方式提交的。随着项目的增大,资源管理混乱产生的影响也会成倍地增加。有些团队的做法是给相关负责人检查无误后再提交到项目中。这里我们采用另一种方法,使用代码控制资源的导入!每当资源导入项目后自动修改属性、提取数据、删除多余的资源,这种策略可以节省很多时间。

  • 案例1:初始化资源(更改属性)
      模型美术上传了一批角色的动作文件,经过检查发现很多动作做都有问题,有些导入项目时,并没有使用快捷键ctrl+D提取其中的Animction文件;有些动作应该是循环的,但是美术并未勾选循环。

  • 案例2:检查资源
      新的一批模型文件再次袭来,这一次都是角色模型,项目中所有人物模型,都需要一个挂点手部武器挂点,最后测试部门对所有人物模型都运行一次后发现,因为缺少这个挂点,部分模型出现错误。


请把资源初始化属性的工作交给机器,使用命令来初始化资源!
请把检查资源的工作交给机器,使用命令来检查资源,如果资源不符合需求则弹出错误提示!

  • 图片导入回调
      注意脚本需要放到Editor文件夹下,才能正确被回调。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class AssetPostProcessor_Test1 : AssetPostprocessor {
    void OnPostprocessTexture(Texture2D texture)
    {
        TextureImporter import = assetImporter as TextureImporter;
        Debug.Log(import.assetPath);
    }
}


  • 图片导入后自动修改图片格式
      注意,MipMap是用于在远离摄像机时降低图片质量的功能,因此对于UI上的贴图都可以关闭,以减少内存占用。
public class AssetPostProcessor_Test1 : AssetPostprocessor {

    void OnPostprocessTexture(Texture2D texture)
    {
        TextureImporter import = assetImporter as TextureImporter;
        Debug.Log(import.assetPath);

        //Set mipmapEnable = false;
        import.mipmapEnabled = false;
        import.textureType = TextureImporterType.Sprite;
        //Save The Changes
        EditorUtility.SetDirty(import);
    }
}


  • 模型导入回调
public class AssetPostProcessor_Test1 : AssetPostprocessor {
    void OnPostprocessModel(GameObject obj)
    {
        ModelImporter import = assetImporter as ModelImporter;
        Debug.Log(import.assetPath);
    }
}


  • 拷贝动画文件并且删除模型文件
   void OnPostprocessModel(GameObject obj)
    {
        ModelImporter import = assetImporter as ModelImporter;
        Debug.Log(import.assetPath);
        var key = "Models".ToLower();
        var path = import.assetPath.ToLower();
        if(!path.Contains(key))
        {
            EditorApplication.delayCall += HandCopyFBXAnimation;
        }
    }

    /// <summary>
    /// 处理拷贝模型动画
    /// </summary>
    void HandCopyFBXAnimation()
    {
        EditorApplication.delayCall -= HandCopyFBXAnimation;
        ModelImporter import = assetImporter as ModelImporter;
        var asset = AssetDatabase.LoadAllAssetsAtPath(import.assetPath);
        foreach (var clipObje in asset)
        {
            if (clipObje is AnimationClip && !clipObje.name.Contains("__preview__"))
            {
                string outputPath = Path.GetDirectoryName(import.assetPath)
                                        + Path.DirectorySeparatorChar + clipObje.name + ".anim";
                var currentAsset = AssetDatabase.LoadAssetAtPath<AnimationClip>(outputPath);
                //如果存在则覆盖还是追加?
                if (currentAsset != null)
                {
                    EditorUtility.CopySerialized(clipObje, currentAsset);//不影响文件关联的情况下更新文件内容
                    EditorUtility.SetDirty(currentAsset);
                }
                //不存在则创建
                else
                {
                    var newAnim = new AnimationClip();
                    EditorUtility.CopySerialized(clipObje, newAnim);
                    AssetDatabase.CreateAsset(newAnim, outputPath);
                }
            }
        }
        //删除模型
        AssetDatabase.DeleteAsset(import.assetPath);
    }


  • 资源命名规范/工程资源路径使用命令
命令关键词(不区分大小写) 适用资源类型 作用
[UI] 图片 UI图片资源,取消 MipMap属性
[ANM] 模型 拷贝动画文件并且源模型文件
[LOOP] 模型 设置动画循环模式
[SKIP] 所有 跳过所有检查和设置



最后,我们将在下一章节讨论,导入模型自动绑定材质、使用命令检查资源是否完整、如何批量修改资源。

Be continued

转载于:https://www.jianshu.com/p/fb0a6bf29e18

2015-05-13 16:20:21 binbingg 阅读数 487
 

Unity3D Editor 编辑器简易教程

目录(?)[+]

Star

自定义编辑器简易教程 an introduction to custom editors

原文地址 http://catlikecoding.com/unity/tutorials/star/

简介 Introduction

这个教程将让你学会如何创建一个星型控件以及如何制作这个控件的自定义编辑器。你将学会:

  • 动态的建立Mesh。
  • 使用一个嵌套类。
  • 建立一个自定义编辑器。
  • 使用SerializedObject
  • 支持所见即所得。
  • 对Undo、Redo、Reset和prefab提供支持。
  • 支持多对象编辑。
  • 支持场景视图内编辑。

我们假设你已经学会了Unity C#的基础编程知识,以及Unity 编辑器的基础知识。如果你已经完成了相关的学习,Let's Go!

建立Star类 Creating the star

我们建立一个全新的Unity工程,然后建立一个新的C#脚本,将它命名为Star。我们将用这个脚本,建立一个由三角面拼接成的星,这里需要一个Mesh。

什么是Mesh?

3D模型是由多边形拼接而成,一个复杂的多边形,实际上是由多个三角面拼接而成。所以一个3D模型的表面是由多个彼此相连的三角面构成。三维空间中,构成这些三角面的点以及三角形的边的集合就是Mesh。

<span class="mark" style="background-color: rgb(255, 255, 204); ">using UnityEngine;</span>

<span class="mark" style="background-color: rgb(255, 255, 204); ">public class Star : MonoBehaviour {</span>

	<span class="mark" style="background-color: rgb(255, 255, 204); ">private Mesh mesh;</span>

<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

任何对于Mesh的使用,都必须搭配一个MeshFilter组件,而MeshFilter又被用于MeshRenderer组件。只有这样,才能被Unity绘制。所以,这些组件都必须被加载到GameObject对象上,我们的Star对象也必须这么做。

当然,我们可以手动添加这些组件,但默认的自动添加是一个更好的办法。所以我们需要添加一个RequireComponent类作为Star对象的一个特性。

什么是类的特性?

特性像对类附加一个标签,用来告诉编译器这个类需要如何处理。是除了类声明的代码之外,对类做的附加说明。另外,特性不止针对类,对方法和属性同样适用。

typeof有什么用?

typeof是一种运算符,能够获得任何类的类型描述数据,数据里最常用的就是类的名字。那为什么不直接在代码里写类的名字就好呢?因为代码中所有的赋值和运算都需要变量,直接使用类的名字会导致编译错误。

using UnityEngine;

<span class="mark" style="background-color: rgb(255, 255, 204); ">[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]</span>
public class Star : MonoBehaviour {

	private Mesh mesh;

}

现在,我们建立一个新的空GameObject,将它命名为My First Star,然后拖拽我们的脚本Star到My First Star上。你可以看到,My First Star拥有了两个组件,MeshRenderer和Star。

拖动一个,得到三个

下一个步骤是建立一个Mesh。我们需要在Unity的Start事件里来做这些事,Start事件将在程序启动的时候发生。我们还需要在MeshFilter中给这个新的Mesh起一个名字。

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	private Mesh mesh;

	<span class="mark" style="background-color: rgb(255, 255, 204); ">void Start () {</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">GetComponent<MeshFilter>().mesh = mesh = new Mesh();</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">mesh.name = "Star Mesh";</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
}
without meshwith mesh
无编辑器模式与实时预览模式

当然,现在我们在预览模式下还看不到任何东西,因为Mesh还是空的。所以让我们开始编辑顶点数组吧,我们的Star类需要一个用来设置顶点数量的属性,以及这些定点与中心的相对距离。

第一个顶点是Star的中心点,其余的顶点将顺时针排列。我们将使用四元数来计算这些点的排列。因为我们假设俯视Z轴,所以,轮转的角度是负数,否则,将使这些点做逆时针排列。我们不需要设置第一个点,因为vector默认会被设置成0, Mesh中使用本地坐标系。

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	<span class="mark" style="background-color: rgb(255, 255, 204); ">public Vector3 point = Vector3.up;</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">public int numberOfPoints = 10;</span>

	private Mesh mesh;
	<span class="mark" style="background-color: rgb(255, 255, 204); ">private Vector3[] vertices;</span>

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";

		<span class="mark" style="background-color: rgb(255, 255, 204); ">vertices = new Vector3[numberOfPoints + 1];</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">float angle = -360f / numberOfPoints;</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">for(int v = 1; v < vertices.Length; v++){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * point;</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

		<span class="mark" style="background-color: rgb(255, 255, 204); ">mesh.vertices = vertices;</span>
	}
}
新的编辑器属性

三角面会被保存成顶点数组,每个面三个顶点。因为我们使用三角形来描述多边形,每个三角形都起始于相同的中点,并且与其他的三角形相连。最后一个三角形与第一个三角形相连。例如,如果有四个三角形,那么顶点数组如下{0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1}。

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	public Vector3 point = Vector3.up;
	public int numberOfPoints = 10;

	private Mesh mesh;
	private Vector3[] vertices;
	<span class="mark" style="background-color: rgb(255, 255, 204); ">private int[] triangles;</span>

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";

		vertices = new Vector3[numberOfPoints + 1];
		<span class="mark" style="background-color: rgb(255, 255, 204); ">triangles = new int[numberOfPoints * 3];</span>
		float angle = -360f / numberOfPoints;
		for(int v = 1<span class="mark" style="background-color: rgb(255, 255, 204); ">, t = 1</span>; v < vertices.Length; v++<span class="mark" style="background-color: rgb(255, 255, 204); ">, t += 3</span>){
			vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * point;
			<span class="mark" style="background-color: rgb(255, 255, 204); ">triangles[t] = v;</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">triangles[t + 1] = v + 1;</span>
		}
		<span class="mark" style="background-color: rgb(255, 255, 204); ">triangles[triangles.Length - 1] = 1;</span>

		mesh.vertices = vertices;
		<span class="mark" style="background-color: rgb(255, 255, 204); ">mesh.triangles = triangles;</span>
	}
}
game viewscene view
一个简单的星星

现在,我们的星星看起来还只是一个简单的多边形。Unity也提示说丢失材质坐标,因为默认的Shader需要这些坐标。我们不会使用一个纹理来描绘所有的星星,让我们通过建立我们自己的Shader来消除这个警告,这个Shader将只使用顶点着色。

我们建立一个新的Shader将它命名为Star,然后写入以下代码。

什么是CGPROGRAM?

Basically, data flows from the Unity engine into the graphics card, where it's processed per vertex. Then interpolated data flows from the vertices down to the individual pixels. In this case, we pass position and color data all the way down. The only additional thing we do is convert vertex positions from world space to screen space.
The statements above the CGPROGRAM switch off default lighting and depth buffer writing. Culling is switched off so we can see the triangles from both sides, not just the front. "Blend SrcAlpha OneMinusSrcAlpha" is default alpha blending, allowing for transparency.

为什么不使用fixed-function shader?

fixed-function shader已经属于过时的技术了。 CGPROGRAM 在将数据转化成屏幕像素方面拥有更强大的功能。

Shader "Star"{
	SubShader{
		Tags{ "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
		Blend SrcAlpha OneMinusSrcAlpha
		Cull Off
		Lighting Off
		ZWrite Off
		Pass{
			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag

				struct data {
					float4 vertex : POSITION;
					fixed4 color: COLOR;
				};

				data vert (data v) {
					v.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
					return v;
				}

				fixed4 frag(data f) : COLOR {
					return f.color;
				}
			ENDCG
		}
	}
}

现在我们建立一个新的材质球,命名为Star,将Shader设置为我们刚刚编写的Star,并且将这个材质球赋予My First Star。

game viewinspector
添加材质球之后

顶点着色默认是白色,所以我们的多边形现在变成了白色。我们想要一个更漂亮的星星。所以我们来为每个点定义一种颜色。

我们再添加一个frequency属性,这样我们就能让程序自动重复点的序列,而不用我们逐个定义全部的点。这个选项取代了numberOfPoints。

我们在最后需要确认frequency属性是否正确,并且星星至少拥有一个点。如果没有,我们的代码就可能出错。

Why check both for null and the length?

When freshly created, our star component won't have an array yet. It's also technically possible for scripts to explicitly set our array to null later on. We need to watch out for that, to prevent errors. Only if the array does exists do we go ahead and check its length as well.

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	<span class="mark" style="background-color: rgb(255, 255, 204); ">public Vector3[] points;</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">public int frequency = 1;</span>

	private Mesh mesh;
	private Vector3[] vertices;
	private int[] triangles;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";

		<span class="mark" style="background-color: rgb(255, 255, 204); ">if(frequency < 1){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">frequency = 1;</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">if(points == null || points.Length == 0){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">points = new Vector3[]{ Vector3.up};</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

		<span class="mark" style="background-color: rgb(255, 255, 204); ">int numberOfPoints = frequency * points.Length;</span>
		vertices = new Vector3[numberOfPoints + 1];
		triangles = new int[numberOfPoints * 3];
		float angle = -360f / numberOfPoints;
		for(int <span class="mark" style="background-color: rgb(255, 255, 204); ">iF = 0,</span> v = 1, t = 1<span class="mark" style="background-color: rgb(255, 255, 204); ">; iF < frequency; iF++){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">for(int iP = 0; iP < points.Length; iP += 1,</span> v += 1, t += 3){
				vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * <span class="mark" style="background-color: rgb(255, 255, 204); ">points[iP]</span>;
				triangles[t] = v;
				triangles[t + 1] = v + 1;
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
		}
		triangles[triangles.Length - 1] = 1;

		mesh.vertices = vertices;
		mesh.triangles = triangles;
	}
}
scene viewinspector
配置好的点

我们需要些颜色!如果把全部的顶点都指定相同的颜色就很简单,但这样太无聊了。我们来试试给每个顶点分配一个颜色。我们需要一个数组来保存这些颜色数据,而且必须保持颜色和顶点的数量一致。这有点小麻烦,我们干脆换成另外一种方式,在Star类中建立一个新的类,这个类可以保存一个顶点的颜色和位置。然后我们可以用这个类的数组来代替vector数组。

这类叫Point,如果在Star类之外使用,就是Star.Point。在Star里面Point就可以了。为了让Unity能够将Point序列化,我们为Point添加System.Serializable特性。

为什么不用结构体?

Because Star.Point is so lightweight and its data is always needed all at once, it would make sense to use a struct type and avoid the overhead that objects add. However, Unity does not support serialization of custom struct types. So you're stuck using classes to bundle data you want to store.
If you're really concerned about the object overhead and possible null errors, you can always store the offset and color data in two separate arrays. However, then you would need to make sure that these arrays always stay synchronized. While that is definitely doable, the class approach is simpler. That's why I use it in this tutorial.

<span class="mark" style="background-color: rgb(255, 255, 204); ">using System;</span>
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	<span class="mark" style="background-color: rgb(255, 255, 204); ">[Serializable]</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">public class Point {</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">public Color color;</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">public Vector3 offset;</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

	public <span class="mark" style="background-color: rgb(255, 255, 204); ">Point</span>[] points;
	public int frequency = 1;

	private Mesh mesh;
	private Vector3[] vertices;
	<span class="mark" style="background-color: rgb(255, 255, 204); ">private Color[] colors;</span>
	private int[] triangles;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";

		if(frequency < 1){
			frequency = 1;
		}
		if(points == null || points.Length == 0){
			points = new <span class="mark" style="background-color: rgb(255, 255, 204); ">Point</span>[]{ <span class="mark" style="background-color: rgb(255, 255, 204); ">new Point()</span>};
		}

		int numberOfPoints = frequency * points.Length;
		vertices = new Vector3[numberOfPoints + 1];
		<span class="mark" style="background-color: rgb(255, 255, 204); ">colors = new Color[numberOfPoints + 1];</span>
		triangles = new int[numberOfPoints * 3];
		float angle = -360f / numberOfPoints;
		for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){
			for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){
				vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP]<span class="mark" style="background-color: rgb(255, 255, 204); ">.offset</span>;
				<span class="mark" style="background-color: rgb(255, 255, 204); ">colors[v] = points[iP].color;</span>
				triangles[t] = v;
				triangles[t + 1] = v + 1;
			}
		}
		triangles[triangles.Length - 1] = 1;

		mesh.vertices = vertices;
		<span class="mark" style="background-color: rgb(255, 255, 204); ">mesh.colors = colors;</span>
		mesh.triangles = triangles;
	}
}
game viewinspector
有了颜色之后

最后是关于中心点的。现在,我们还没有给它设置颜色,所以它一直保持着透明。让我们来为它添加一个颜色属性,最终,这个星星看上去变漂亮了。

using System;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point {
		public Color color;
		public Vector3 offset;
	}

	public Point[] points;
	public int frequency = 1;
	<span class="mark" style="background-color: rgb(255, 255, 204); ">public Color centerColor;</span>

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";

		if(frequency < 1){
			frequency = 1;
		}
		if(points == null || points.Length == 0){
			points = new Point[]{ new Point()};
		}

		int numberOfPoints = frequency * points.Length;
		vertices = new Vector3[numberOfPoints + 1];
		colors = new Color[numberOfPoints + 1];
		triangles = new int[numberOfPoints * 3];
		float angle = -360f / numberOfPoints;
		<span class="mark" style="background-color: rgb(255, 255, 204); ">colors[0] = centerColor;</span>
		for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){
			for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){
				vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset;
				colors[v] = points[iP].color;
				triangles[t] = v;
				triangles[t + 1] = v + 1;
			}
		}
		triangles[triangles.Length - 1] = 1;

		mesh.vertices = vertices;
		mesh.colors = colors;
		mesh.triangles = triangles;
	}
}
game viewscene viewinspector
漂亮的星星,麻烦的编辑器

建立编辑器 Creating the Inspector

现在Star看起来不错,但设计起来有些麻烦。默认的编辑器有点蛋疼,让我们自己做一个!

所有编辑器类的代码,都需要放在Editor文件夹下,只有这样Unity才能正确的识别这代码。Editor的名字对就行,放在哪倒无所谓。我们现在把Editor建立在根目录也就是Assets下。然后再建一个StarInspector类的代码文件,放在Editor里面。

编辑器的类型?

需要了解的是,编辑器面板不只有一个类型。我们这个例子里面使用的是属性面板——Inspector,其余还有 EditorWindow——编辑对话框,可以实现一个完全自定义的弹出式对话框,还有ScriptableWizard——向导对话框,以及编辑器菜单。



目录结构

因为我们的类是一个编辑器类,它需要继承Editor类而不是MonoBehaviour。我们还需要添加一个属性来告诉Unity,这个类是为Star类定义编辑器的。

<span class="mark" style="background-color: rgb(255, 255, 204); ">using UnityEditor;</span>
<span class="mark" style="background-color: rgb(255, 255, 204); ">using UnityEngine;</span>

<span class="mark" style="background-color: rgb(255, 255, 204); ">[CustomEditor(typeof(Star))]</span>
<span class="mark" style="background-color: rgb(255, 255, 204); ">public class StarInspector : Editor {}</span>

到目前为止,我们没有改变Star的编辑器。我们需要替换默认的编辑器。我们可以通过重载Editor 类的OnInspectorGUI事件来实现。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	<span class="mark" style="background-color: rgb(255, 255, 204); ">public override void OnInspectorGUI () {}</span>
}
一个空的编辑器

默认的编辑器捏?没了?

因为我们没有在OnInspectorGUI事件里写任何代码,所以一切都是空白的。DrawDefaultInspector方法可以用来绘制默认的编辑器界面,但我们本来就不想要这个,还是试试别的吧。

我们首先要确认是哪个Star对象被选中,应该在编辑器中被显示。我们可以使用target属性来表示这个对象,target属性是Editor的一个属性,我们继承了Editor,所以也继承了这个属性,可以直接使用它,非常方便。虽然这不是必须的,我们可以用 SerializedObject来包装target,这么做会很方便,因为会使对很多编辑器的操作支持变得简单,比如undo。

我们使用了SerializedObject,可以通过SerializedProperty对象来提取它的数据。我们要在OnEnable事件里初始化所有的star类的变量。这个事件会在一个添加Star组件的GameObject被选中时发生。

What's a SerializedObject?

SerializedObject is a class that acts as a wrapper or proxy for Unity objects. You can use it to extract data from the object even if you don't have a clue what's inside it. This is how the Unity inspector can show default inspectors for anything you create yourself. As a bonus, you get undo support for free.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	<span class="mark" style="background-color: rgb(255, 255, 204); ">private SerializedObject star;</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">private SerializedProperty</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">points,</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">frequency,</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">centerColor;</span>

	<span class="mark" style="background-color: rgb(255, 255, 204); ">void OnEnable () {</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">star = new SerializedObject(target);</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">points = star.FindProperty("points");</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">frequency = star.FindProperty("frequency");</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">centerColor = star.FindProperty("centerColor");</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

	public override void OnInspectorGUI () {}
}

每一次编辑器更新的时候,我们都需要确定SerializedObject被实时更新了。这就是我们要在OnInspectorGUI事件里做的第一件事。之后我们可以简单的调用EditorGUILayout.PropertyField来显示我们的属性,显示points及其内部的所有元素。之后我们结束所有属性修改并应用到选定的组件。

What's EditorGUILayout?

EditorGUILayout is a utility class for displaying stuff in the Unity editor. It contains methods for drawing all kinds of things, in this case we're simply using the default method for drawing a SerializedProperty.
There's also an EditorGUI utility class which does that same thing, but requires you to perform your own GUI layout.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		<span class="mark" style="background-color: rgb(255, 255, 204); ">star.Update();</span>

		<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.PropertyField(points, true);</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.PropertyField(frequency);</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.PropertyField(centerColor);</span>

		<span class="mark" style="background-color: rgb(255, 255, 204); ">star.ApplyModifiedProperties();</span>
	}
}
一个被重建的编辑器

现在的编辑器和默认的差不多,我们可以做的更好。我们需要重新整理一下points的显示格式,让每个点的颜色和位移信息合并为一组显示。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		<span class="mark" style="background-color: rgb(255, 255, 204); ">for(int i = 0; i < points.arraySize; i++){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.BeginHorizontal();</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">SerializedProperty point = points.GetArrayElementAtIndex(i);</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"));</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.PropertyField(point.FindPropertyRelative("color"));</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.EndHorizontal();</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		star.ApplyModifiedProperties();
	}
}
排版不良的编辑器

我们需要修正这个排版。让我们去掉颜色和位置的标签,设置颜色条的最大长度为50像素。我们通过EditorGUILayout.PropertyField方法的额外参数能够实现。因为我们对所有的对象都使用相同的配置,所以我们使用静态变量来保存这些设置。

然后再通过GUILayout.Label方法来给所有的points添加一个统一的标签。

What's a GUIContent?

GUIContent is a wrapper object for text, textures, and tooltips that you typically use as labels.

Why use GUILayout instead of EditorGUILayout?

You use the same Unity GUI system for editors that you can use for your games. GUILayout provided basic functionality like labels and buttons, while EditorGUILayout provides extra editor-specific stuff like input fields.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	<span class="mark" style="background-color: rgb(255, 255, 204); ">private static GUIContent pointContent = GUIContent.none;</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">private static GUILayoutOption colorWidth = GUILayout.MaxWidth(50f);</span>

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		<span class="mark" style="background-color: rgb(255, 255, 204); ">GUILayout.Label("Points");</span>
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset")<span class="mark" style="background-color: rgb(255, 255, 204); ">, pointContent</span>);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color")<span class="mark" style="background-color: rgb(255, 255, 204); ">, pointContent, colorWidth</span>);
			EditorGUILayout.EndHorizontal();
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		star.ApplyModifiedProperties();
	}
}
一个紧凑的编辑器

最终,看上去好多了!现在,如果我们能方便的添加和删除points就更好了。让我们试试添加这些按钮吧。

我们为每个point添加两个按钮,一个是“+”用来插入point,一个是"-"用来删除point。我们再添加一些说明使用户能够了解这些按钮的用途。我们还需要控制按钮宽度,将样式设置成mini buttons,因为这些按钮要小一些。

How does GUILayout.Button work?

The method GUILayout.Button both shows a button and returns whether it was clicked. So you typically call it inside an if statement and perform the necessary work in the corresponding code block.
What actually happens is that your own GUI method, in this case OnInspectorGUI, gets called far more often than just once. It gets called when performing layout, when repainting, and whenever a significant GUI event happens, which is quite often. Only when a mouse click event comes along that is consumed by the button, will it return true.
To get an idea, put Debug.Log(Event.current); at the start of your OnInspectorGUI method and fool around a bit.
Usually you need not worry about this, but be aware of it when performing heavy work like generating textures. You don't want to do that dozens of times per second if you don't need to.

What are the contents of a new item?

If you insert a new array element via a SerializedProperty, the new element will be a duplicate of the element just above it. If there's no other element, it gets default values.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		<span class="mark" style="background-color: rgb(255, 255, 204); ">insertContent = new GUIContent("+", "duplicate this point"),</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">deleteContent = new GUIContent("-", "delete this point"),</span>
		pointContent = GUIContent.none;

	private static GUILayoutOption
		<span class="mark" style="background-color: rgb(255, 255, 204); ">buttonWidth = GUILayout.MaxWidth(20f),</span>
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			<span class="mark" style="background-color: rgb(255, 255, 204); ">if(GUILayout.Button(insertContent, EditorStyles.miniButtonLeft, buttonWidth)){</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">points.InsertArrayElementAtIndex(i);</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">points.DeleteArrayElementAtIndex(i);</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

			EditorGUILayout.EndHorizontal();
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		star.ApplyModifiedProperties();
	}
}
添加了传送按钮和提示

看上去不错,但是怎么移动points?如果我们能够直接拖动这些点来排列它们,那就太棒了。虽然这肯定是整齐的,让我们用一个简单办法来解决它。

我们可以给每个point添加一个传送按钮。点一下,你就激活了这个point的显示。点另一个,就会跳转到另一个point,同时移动视角到当前point。

这种方式需要我们来记录哪个point是当前的焦点。我们可以使用point的索引值来记录焦点,用-1表示焦点为空。我们将改变按钮的提示信息,信息将根据按钮的状态而定,并添加一个标签来告诉用户该做什么。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none<span class="mark" style="background-color: rgb(255, 255, 204); ">,</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">teleportContent = new GUIContent("T")</span>;

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	<span class="mark" style="background-color: rgb(255, 255, 204); ">private int teleportingElement;</span>

	void OnEnable () {
		star = new SerializedObject(target);
		points = star.FindProperty("points");
		frequency = star.FindProperty("frequency");
		centerColor = star.FindProperty("centerColor");

		<span class="mark" style="background-color: rgb(255, 255, 204); ">teleportingElement = -1;</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">teleportContent.tooltip = "start teleporting this point";</span>
	}

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			<span class="mark" style="background-color: rgb(255, 255, 204); ">if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">if(teleportingElement >= 0){</span>
					<span class="mark" style="background-color: rgb(255, 255, 204); ">points.MoveArrayElement(teleportingElement, i);</span>
					<span class="mark" style="background-color: rgb(255, 255, 204); ">teleportingElement = -1;</span>
					<span class="mark" style="background-color: rgb(255, 255, 204); ">teleportContent.tooltip = "start teleporting this point";</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">else{</span>
					<span class="mark" style="background-color: rgb(255, 255, 204); ">teleportingElement = i;</span>
					<span class="mark" style="background-color: rgb(255, 255, 204); ">teleportContent.tooltip = "teleport here";</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
			if(GUILayout.Button(insertContent, EditorStyles.<span class="mark" style="background-color: rgb(255, 255, 204); ">miniButtonMid</span>, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		<span class="mark" style="background-color: rgb(255, 255, 204); ">if(teleportingElement >= 0){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">GUILayout.Label("teleporting point " + teleportingElement);</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		star.ApplyModifiedProperties();
	}
}
有传送按钮的编辑器

所见即所得 WYSIWYG

我们的编辑器对point已经很友好了,但我们还不能实时看到我们编辑过程中的结果。是时候改变这一切了!

第一件事是让Unity了解,我们的组件需要在编辑模式下被激活。我们通过ExecuteInEditMode类来声明这一属性。此后,star在编辑中的任何显示,都会调用Start方法。

因为我们建立了一个Mesh在Start方法中,它将在编辑模式下被创建。正如我们把Mesh存放在MeshFilter中,它将被保存于场景中。我们不希望这样,因为我们需要动态的创建Mesh。我们可以设置HideFlags来阻止Unity保存Mesh。于是,我们还需要确认Mesh被清理时,编辑器已经不再需要它。OnDisable事件会在每一个组件实效时被调用,它可以帮我们处理这些事情。我们需要在OnDisable中清理MeshFilter来阻止它发出缺少Mesh的警告。

using System;
using UnityEngine;

[<span class="mark" style="background-color: rgb(255, 255, 204); ">ExecuteInEditMode</span>, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point { … }

	public Point[] points;
	public int frequency = 1;
	public Color centerColor;

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";
		<span class="mark" style="background-color: rgb(255, 255, 204); ">mesh.hideFlags = HideFlags.HideAndDontSave;</span>

		if(frequency < 1){
			frequency = 1;
		}
		if(points == null || points.Length == 0){
			points = new Point[]{ new Point()};
		}

		int numberOfPoints = frequency * points.Length;
		vertices = new Vector3[numberOfPoints + 1];
		colors = new Color[numberOfPoints + 1];
		triangles = new int[numberOfPoints * 3];
		float angle = -360f / numberOfPoints;
		colors[0] = centerColor;
		for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){
			for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){
				vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset;
				colors[v] = points[iP].color;
				triangles[t] = v;
				triangles[t + 1] = v + 1;
			}
		}
		triangles[triangles.Length - 1] = 1;

		mesh.vertices = vertices;
		mesh.colors = colors;
		mesh.triangles = triangles;
	}

	<span class="mark" style="background-color: rgb(255, 255, 204); ">void OnDisable () {</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">if(Application.isEditor){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">GetComponent<MeshFilter>().mesh = null;</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">DestroyImmediate(mesh);</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
}
编辑模式下的星星

我们的星星已经显示在了编辑模式中!当我们在一个对象上关闭Star组件,星星的Mesh将被消除。当我们启用Star组件,它将不再恢复。因为Start方法仅在组件第一次激活时被调用。解决的办法是将我们的初始化代码移动到OnEnable事件中去。

做好之后,我们进一步重构代码,让我们能随时初始化Mesh。为了在不需要的时候不进行初始化,我们还需要添加少量的检查。

using System;
using UnityEngine;

[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point { … }

	public Point[] points;
	public int frequency = 1;
	public Color centerColor;

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	<span class="mark" style="background-color: rgb(255, 255, 204); ">public</span> void <span class="mark" style="background-color: rgb(255, 255, 204); ">UpdateStar</span> () {
		<span class="mark" style="background-color: rgb(255, 255, 204); ">if(mesh == null){</span>
			GetComponent<MeshFilter>().mesh = mesh = new Mesh();
			mesh.name = "Star Mesh";
			mesh.hideFlags = HideFlags.HideAndDontSave;
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

		if(frequency < 1){
			frequency = 1;
		}
		if(points.Length == 0){
			points = new Point[]{ new Point()};
		}

		int numberOfPoints = frequency * points.Length;
		<span class="mark" style="background-color: rgb(255, 255, 204); ">if(vertices == null || vertices.Length != numberOfPoints + 1){</span>
			vertices = new Vector3[numberOfPoints + 1];
			colors = new Color[numberOfPoints + 1];
			triangles = new int[numberOfPoints * 3];
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
		float angle = -360f / numberOfPoints;
		colors[0] = centerColor;
		for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){
			for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){
				vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset;
				colors[v] = points[iP].color;
				triangles[t] = v;
				triangles[t + 1] = v + 1;
			}
		}
		triangles[triangles.Length - 1] = 1;

		mesh.vertices = vertices;
		mesh.colors = colors;
		mesh.triangles = triangles;
	}

	<span class="mark" style="background-color: rgb(255, 255, 204); ">void OnEnable () {</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">UpdateStar ();</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>

	void OnDisable () { … }
}

现在,组件再被启动时,星星不再出现。不幸的是,它不再相应修改。幸好,这很容易解决。

SerializedObject.ApplyModifiedProperties方法可以返回任何修改的实际情况。这样,我们就能很简单的调用target的UpdateStar方法。我们需要显式转换target的类型,因为编辑器需要为所有类型提供支持,所以target的类型被定义成了Object。

译者注,有一种方法可以简单的解决这个问题,写一个基类如下 

public  class InspectorBase<T> : Editor where T : UnityEngine.Object
{
		protected T Target { get { return  (T)target; } }
}                

然后全部的编辑器类都继承这个基类如下 

[CustomEditor(typeof(Star))]
public  class StarEditor : InspectorBase< Star >
{
		......
}            

这样在以后的代码里,target会自动成为你想要的类型。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
				if(teleportingElement >= 0){
					points.MoveArrayElement(teleportingElement, i);
					teleportingElement = -1;
					teleportContent.tooltip = "start teleporting this point";
				}
				else{
					teleportingElement = i;
					teleportContent.tooltip = "teleport here";
				}
			}
			if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		if(teleportingElement >= 0){
			GUILayout.Label("teleporting point " + teleportingElement);
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		<span class="mark" style="background-color: rgb(255, 255, 204); ">if(</span>star.ApplyModifiedProperties()<span class="mark" style="background-color: rgb(255, 255, 204); ">){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">((Star)target).UpdateStar();</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
	}
}
编辑中的星星

现在,Mesh没有立即更新。这让编辑轻松许多!可惜的是,它还没有支持Undo!

不幸的是,在Unity中没有一种简单的方法来支持Undo事件,但我们可以做到接近支持。在我们的案例中,我们可以检查ValidateCommand事件是否发生,来判断Undo操作。当前被选中的对象这个事件的目标,我们假设它被修改过。

What's a ValidateCommand?

ValidateCommand is a type of GUI event, which indicates that some special action happened, like undo or redo. So why isn't it called something like ExecuteCommand? Actually, that command type exists as well. While they have a slightly different meaning, in practice you use them for the exact same purpose. Unfortunately, depening on exactly where you're checking and how you're constructing your GUI, either one or the other event happens, but not both. Why this is so, I do not know.
So to be perfectly safe, you have to check for both command types. In this case, however, you can suffice with checking ValidateCommand.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
				if(teleportingElement >= 0){
					points.MoveArrayElement(teleportingElement, i);
					teleportingElement = -1;
					teleportContent.tooltip = "start teleporting this point";
				}
				else{
					teleportingElement = i;
					teleportContent.tooltip = "teleport here";
				}
			}
			if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		if(teleportingElement >= 0){
			GUILayout.Label("teleporting point " + teleportingElement);
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		if(
			star.ApplyModifiedProperties() <span class="mark" style="background-color: rgb(255, 255, 204); ">||</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">(Event.current.type == EventType.ValidateCommand &&</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">Event.current.commandName == "UndoRedoPerformed")</span>
		){
			((Star)target).UpdateStar();
		}
	}
}

最后,一个舒服的编辑过程!还有什么需要做吗?在编辑器的右上角有一个齿轮图标能够重置组件。当我们重置Star组件的时候我们的Mesh没有及时更新。

你可以定义Reset方法来监听一个组件的重置。这事Unity为Editor及其子类提供的一个方法。当这个事件发生,我们只要及时更新我们的星星就可以了。

using System;
using UnityEngine;

[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point { … }

	public Point[] points;
	public int frequency = 1;
	public Color centerColor;

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	public void UpdateStar () { … }

	void OnEnable () { … }

	void OnDisable () { … }

	<span class="mark" style="background-color: rgb(255, 255, 204); ">void Reset () {</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">UpdateStar();</span>
	<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
}

OK我们开始写Reset。我们要做什么?我们来试试prefabs?

现在使用prefabs对于我们star并没有太多意义,因为每一个star都拥有自己的独立的Mesh。如果你想使用很多个一样的star,那在建立一个3D模型并且导入Mesh是一个好主意。这样所有的star就共享了同一个Mesh。但假设我们使用prefab,就可以实例化多个同样的star然后我们还能够调整它们。

你只要简单的拖拽一个star从层级视图到项目视图,就能建立一个prefab。对prefab的更新能够影响全部的prefab实例,因为每个prefab的修改都会触发OnDisable和OnEnable。将一个实例回复成prefab同样的状态它依然能够工作。

唯一我们没有完全做好的事情是prefab的MeshFilter会显示它的Mesh类型不匹配。这事因为prefab是一个实际的资源,而动态生成的Mesh不是。这不影响功能,但还是让我们解决它吧。

一个修改后提示类型不匹配的prefab

为了停止prefab生成它的Mesh,我们不能再调用UpdateStar方法。不幸的是,这代表我们将不能再看到预览了。我们可以用PrefabUtility.GetPrefabType方法来检测编辑窗口当前的对象是不是prefab。如果是,我们简单的不更新它就行了。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
				if(teleportingElement >= 0){
					points.MoveArrayElement(teleportingElement, i);
					teleportingElement = -1;
					teleportContent.tooltip = "start teleporting this point";
				}
				else{
					teleportingElement = i;
					teleportContent.tooltip = "teleport here";
				}
			}
			if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		if(teleportingElement >= 0){
			GUILayout.Label("teleporting point " + teleportingElement);
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		if(
			star.ApplyModifiedProperties() ||
			(Event.current.type == EventType.ValidateCommand &&
			Event.current.commandName == "UndoRedoPerformed")
		){
			<span class="mark" style="background-color: rgb(255, 255, 204); ">if(PrefabUtility.GetPrefabType(target) != PrefabType.Prefab){</span>
				((Star)target).UpdateStar();
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
		}
	}
}
prefab没有了Mesh和预览

OK,我们完成了,真的?我没还没有对同时存在多个对象的情况进行支持。试试同时选择多个star。

还没有提供多对象编辑

让我们尝试多对象编辑功能吧。首先,我们需要给类添加一个属性让编辑器提供相应的支持。然后我们需要初始化所有target的SerializedObject,而不再只是一个。我们还需要把任何变化同步到全部的target上。

这样就能在编辑器中支持多个对象了,但如果一些star的point个数不一样,就会出错。因为在Unity的编辑器尝试读取全部点的资料的时候,有些点会不存在。我们可以在获得每个point的数据的时候检查一下这个point是否存在,如果不存在,就停止取值。所以我们只需要显示一个star所拥有的数量的point就可以了。

using UnityEditor;
using UnityEngine;

[<span class="mark" style="background-color: rgb(255, 255, 204); ">CanEditMultipleObjects,</span> CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () {
		star = new SerializedObject(<span class="mark" style="background-color: rgb(255, 255, 204); ">targets</span>);
		points = star.FindProperty("points");
		frequency = star.FindProperty("frequency");
		centerColor = star.FindProperty("centerColor");

		teleportingElement = -1;
		teleportContent.tooltip = "start teleporting this point";
	}

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			SerializedProperty
				point = points.GetArrayElementAtIndex(i)<span class="mark" style="background-color: rgb(255, 255, 204); ">,</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">offset = point.FindPropertyRelative("offset")</span>;
			<span class="mark" style="background-color: rgb(255, 255, 204); ">if(offset == null){</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">break;</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">EditorGUILayout.BeginHorizontal();</span>
			EditorGUILayout.PropertyField(<span class="mark" style="background-color: rgb(255, 255, 204); ">offset</span>, pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
				if(teleportingElement >= 0){
					points.MoveArrayElement(teleportingElement, i);
					teleportingElement = -1;
					teleportContent.tooltip = "start teleporting this point";
				}
				else{
					teleportingElement = i;
					teleportContent.tooltip = "teleport here";
				}
			}
			if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		if(teleportingElement >= 0){
			GUILayout.Label("teleporting point " + teleportingElement);
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		if(
			star.ApplyModifiedProperties() ||
			(Event.current.type == EventType.ValidateCommand &&
			Event.current.commandName == "UndoRedoPerformed")
		){
			<span class="mark" style="background-color: rgb(255, 255, 204); ">foreach(Star s in targets){</span>
				if(PrefabUtility.GetPrefabType(<span class="mark" style="background-color: rgb(255, 255, 204); ">s</span>) != PrefabType.Prefab){
					<span class="mark" style="background-color: rgb(255, 255, 204); ">s</span>.UpdateStar();
				}
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
		}
	}
}
scene viewinspector
多对象编辑

在场景中编辑 Editing in the Scene View

现在我们拥有了一个很不错的编辑器了,但如果我们能直接在场景里编辑这些point会不会更酷一些?用OnSceneGUI事件,我们可以做到。这个方法会在一个对象被选中即将赋予target时调用。我们不能在这个事件中使用SerializedObject。事实上,你可以认为这个方法与我们编辑器类中的其它部分是完全分离的。

Why does OnSceneGUI mess with target?

Probably for backwards compatibility. Multi-object editing was introduced in Unity 3.5. Versions before that only had the target variable.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	<span class="mark" style="background-color: rgb(255, 255, 204); ">void OnSceneGUI () {}</span>
}

让我们设置一个方形的小手柄在star全部的point上面。我们只要在这些point的第一个重复周期里显示手柄就可以了,不需要把全部的重复周期都显示出来。放置这些手柄就好象生成Mesh一样,除了我们使用的是世界坐标系,不是本地坐标系,所以我们要用到star的transform。

我们可以通过Handles.FreeMoveHandle方法来绘制我们的手柄。首先,需要一个世界坐标系的位置,手柄的位置。其次,需要一个绘制手柄的角度,但我们不需要旋转。然后,还需要手柄的尺寸,我们用一个很小的尺寸就够了。我们用一个vector来保存这个尺寸,可以设置成(0.1, 0.1 0.1)。最后一个参数是定义手柄的形状。

How do we convert to world space?

You convert a point from local to world space by appling all transformation matrices of its object hierarchy to it. Unity takes care of this when rendering the scene, but sometimes you need to do it yourself. You can use the Transform.TransformPoint method for this.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	<span class="mark" style="background-color: rgb(255, 255, 204); ">private static Vector3 pointSnap = Vector3.one * 0.1f;</span>

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	void OnSceneGUI () {
		<span class="mark" style="background-color: rgb(255, 255, 204); ">Star star = (Star)target;</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">Transform starTransform = star.transform;</span>

		<span class="mark" style="background-color: rgb(255, 255, 204); ">float angle = -360f / (star.frequency * star.points.Length);</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">for(int i = 0; i < star.points.Length; i++){</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">Vector3 oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset);</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">Handles.FreeMoveHandle(oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap);</span>
		<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
	}
}
在场景中添加了控制手柄

现在还有什么可以做到更好吗?你可以点击一个手柄,让它变成黄色。我们需要比较一个手柄的初始化位置和返回位置。如果不同,说明用户拖动了手柄,我们需要将改变同步到star。star的Mesh使用本地坐标系,在把坐标改变保存之前,不要忘记转换坐标。

How do we convert to local space?

You have to perform the exact opposite steps for converting to world space, in reverse order. You can use the Transform.InverseTransformPoint method for this. Note that when going to world space we rotated in local space first, then transformed. So to convert back, we inverse transform first, then inverse rotate in local space.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static Vector3 pointSnap = Vector3.one * 0.1f;

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	void OnSceneGUI () {
		Star star = (Star)target;
		Transform starTransform = star.transform;

		float angle = -360f / (star.frequency * star.points.Length);
		for(int i = 0; i < star.points.Length; i++){
			Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);
			Vector3
				oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset)<span class="mark" style="background-color: rgb(255, 255, 204); ">,</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">newPoint =</span> Handles.FreeMoveHandle
					(oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap);
			<span class="mark" style="background-color: rgb(255, 255, 204); ">if(oldPoint != newPoint){</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">star.points[i].offset = Quaternion.Inverse(rotation) *</span>
					<span class="mark" style="background-color: rgb(255, 255, 204); ">starTransform.InverseTransformPoint(newPoint);</span>
				<span class="mark" style="background-color: rgb(255, 255, 204); ">star.UpdateStar();</span>
			<span class="mark" style="background-color: rgb(255, 255, 204); ">}</span>
		}
	}
}
我们可以在场景中编辑了

有用了!不过我们还没支持Undo!这里我们不能靠SerializedObject来解决问题,不过幸好这些手柄可以支持Undo。我们只需要告诉编辑器哪个对象被改变了,我们还应该为这次改变起一个名字。我们可以用Undo.SetSnapshotTarget来做这些事。

What's a snapshot?

If an undo step would be created for each GUI event, dragging a handle would result in an undo history filled with dozens of tiny modifications. Instead, the handles make a copy – a snapshot – of the object when movement begins and only register a single undo step with the copy when movement ends. SetSnapshotTarget tells the handles which object to use for this.
All Unity editor GUI elements essentialy do the same thing, whether it's for draggin handles, sliding numbers, typing text, or whatever.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static Vector3 pointSnap = Vector3.one * 0.1f;

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	void OnSceneGUI () {
		Star star = (Star)target;
		Transform starTransform = star.transform;
		<span class="mark" style="background-color: rgb(255, 255, 204); ">Undo.SetSnapshotTarget(star, "Move Star Point");</span>

		float angle = -360f / (star.frequency * star.points.Length);
		for(int i = 0; i < star.points.Length; i++){
			Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);
			Vector3
				oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset),
				newPoint = Handles.FreeMoveHandle
					(oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap);
			if(oldPoint != newPoint){
				star.points[i].offset = Quaternion.Inverse(rotation) *
					starTransform.InverseTransformPoint(newPoint);
				star.UpdateStar();
			}
		}
	}
}

这样,我们终于完成了!一个有趣的设计过程,不是吗?

示例代码下载

转载地址:http://blog.csdn.net/lilanfei/article/details/7680802
2017-09-27 11:56:00 weixin_34318956 阅读数 5

1.首先来个Inspector面板Editor的实现
要实现一个组件在Inspector中的Editor功能,首先需要写一个编辑器类:

6673580-4853729b401c19e8.png

要点
1.继承Editor
2.放在Editor文件夹中
3.在类的头部写[CustomEditor(typeof(你要实现编辑的组件的类名))]
4.复写OnInspectorGUI函数(此函数仅在Inspector刷新时自动调用),然后在函数里实现编辑器功能的拓展实现。例如GUILayout.Label("Hello World");
5.通过target as ‘你的类名’ 来实现引用你要扩展的组件的类的具体对象。

2.GUILayoutOption
EditorGUILayout 与 EditorGUI功能基本差不多,它是EditorGUI的自动布局版本,用它可以快速创建默认UI,若要自定义UI样式,应该使用EditorGUI并传入GUIStyle等参数
GUILayoutOption是一种GUI布局方式的设置,常在GUILayout的具体控件中看到,
例如,

6673580-c88f1c3dcc2bb3c9.png

的最后一个参数便是一些GUILayoutOption不定参,表示可以有多个设置方案。
注意:GUILayoutOption只是表示是设置方案的意思,并不是代表有GUILayoutOption这种设置方法,GUILayoutOption其实是个空类,
具体的设置方案由具体决定。
例如:
GUILayout.Label("Hello World"), EditorStyles.helpBox);
GUILayout.Label("Hello World")表示在面板上显示一条内容为“Hello World”的Label,(下图第一条)
而EditorStyles.helpBox则表示此Label的显示方式是以帮助框的形式显示(下图第二条)
6673580-5ffd4ec3a7b417b6.png

更多的GUILayoutOption:

EditorStyles.boldLabel
6673580-0aaa09d3b878f404.png

EditorStyles.centeredGreyMiniLabel
6673580-ac22669486a9448d.png

EditorStyles.radioButton
6673580-494f91357f580fff.png

EditorStyles.whiteBoldLabel
6673580-262169d320f02b76.png

3.GUILayout 之 SelectionGrid
设置一个网格按钮,并返回按钮的序号。
string[] text = { "1", "2", "3", "4" };Debug.Log(GUILayout.SelectionGrid(-1, text, 2) + " has been choose.");
按钮可以是文字(上面的1,2,3,4四个按钮),也可以是图片等。
第一参数的-1表示按钮哪个被按下,如果为0,则表示第一按钮显示为被按下,-1表示没有。
第三个参数的2表示此网格分为2列,具体如图:

6673580-b0af2899ee8d6007.png

4.折页
首先用这句:
showState = EditorGUILayout.Foldout(showState, "State List"); 参数分别为是否打开,显示标签的名字
其实折页就相当于一个toggle按钮,并不做任何事,只是你按下它,它会返回一个是否折开或者合并的结果,
然后自己再根据这个结果显示对应内容来达到折页效果。

if (showState){  GUILayout.Label("内容", EditorStyles.inspectorDefaultMargins);}
// 为了模拟折页后的子层级的树状缩进结构,我门用EditorStyles.inspectorDefaultMargins这中设置来将内容向右缩进。
6673580-9068826f429bbafc.png

5.自定义自己的GUIStyle
在2中我们知道了如何用GUIStyle扩展GUILayout的各种控件的样式,但我们用的都是EditorStyles附带的风格,
这个EditorStyles是系统用的,我们只能用里面的风格,不能修改(其实也可以改,但改后会将系统其他所以使用该风格的地方都改)。
为了定制我们自己的风格,其实我们可以自己创建一个GUIStyle。

GUIStyle topStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);  // 这里我用EditorStyles的centeredGreyMiniLabel来初始化我的新风格
topStyle.fontStyle = FontStyle.Bold;                       // 新Style的字体为粗体
topStyle.fontSize = 20;
topStyle.richText = true;                              // 允许富文本
6673580-33759d834c2d2709.png

6.再来个Scene面板的Editor实现
基本与Inspector面板Editor的写法一样,只不过Scene面板的更新函数是OnSceneGUI,与之对应的是
Inspector面板的更新函数OnInspectorGUI。
甚至可以在同一个脚本里同时实现Inspector与Scene面板的Editor功能。
要点:
1.在OnSceneGUI()中只能通过Handles来绘制新视图,如果你想引入GUI的元素哪么就需要使用BeginGUI()和EndGUI()组合的使用。
2.同Inspector一样,仅在持有该脚本的对象被选择时才激活。

void OnSceneGUI() 
{
  //得到test脚本的对象
  Test test = (Test) target;
  //在Scene视图中,在该对象本身的位置画一个坐标轴,但画自身坐标轴的意义不大,因为自身坐标轴在被选中时本来就会画出来。但可以用来显示其他对象的坐标轴。
  Handles.PositionHandle(fsm.test.transform.position, Quaternion.identity);
  //在场景中GameObject所在的位置绘制文本框,内容为该GO的坐标
  Handles.Label(test.transform.position + Vector3.up*2, test.transform.name +" : "+ test.transform.position.ToString() );
  //开始绘制GUI
  Handles.BeginGUI();
  //规定GUI显示区域
  GUILayout.BeginArea(new Rect(100, 100, 100, 100));
  //GUI绘制一个按钮
  if(GUILayout.Button("这是一个按钮!"))
      Debug.Log("test");
  //GUI绘制文本框
  GUILayout.Label("我在编辑Scene视图");    
  GUILayout.EndArea();
  Handles.EndGUI();
 }

7.利用Gizmos在Editor里绘制图形
Gizmos是常见的DebugDraw方式之一,可方便地在Editor里绘制线段,射线,方块之类的
用法:Gizmos只能用于OnDrawGizmos函数里,该函数跟Start,Update一样会自动调用

6673580-ed2afa35bbefa6d7.png

但是一般情况下,绘制函数里的参数只有'center','size'之类的,绘制出来的图形是没有角度的,
如果有需要绘制旋转过的图形,例如一个跟随摄影机旋转的Cube
6673580-7cb993508a82f4b6.png

那么就需要利用Gizmos的重要参数——matirx,
这是Gimos的绘制矩阵,绘制的图形信息在这里,要缩放,旋转Gizmos绘制出来的图形,
只需要改变矩阵即可。

void OnSceneGUI() 
{
  //得到test脚本的对象
  Test test = (Test) target;
  //在Scene视图中,在该对象本身的位置画一个坐标轴,但画自身坐标轴的意义不大,因为自身坐标轴在被选中时本来就会画出来。但可以用来显示其他对象的坐标轴。
  Handles.PositionHandle(fsm.test.transform.position, Quaternion.identity);
  //在场景中GameObject所在的位置绘制文本框,内容为该GO的坐标
  Handles.Label(test.transform.position + Vector3.up*2, test.transform.name +" : "+ test.transform.position.ToString() );
  //开始绘制GUI
  Handles.BeginGUI();
  //规定GUI显示区域
  GUILayout.BeginArea(new Rect(100, 100, 100, 100));
  //GUI绘制一个按钮
  if(GUILayout.Button("这是一个按钮!"))
      Debug.Log("test");
  //GUI绘制文本框
  GUILayout.Label("我在编辑Scene视图");    
  GUILayout.EndArea();
  Handles.EndGUI();
 }

8.Editor菜单栏快捷键

6673580-46ea23fa9675fab4.png

6673580-3460b9b9d483dfb9.png

%代表 Ctrl

代表 Shift

9.手动将Scene标志为“修改过的”
有时候会动态在Editor脚本里创建对象在Scene里,但是动态创建之后Scene默认是认为场景没被修改过的,
这个时候是没法保存场景的,就算你强制Ctrl + S,重载场景后之前动态生成的对象也会丢失(除非你在生成对象后,又手动在场景里做一些修改)
那么我需要手动调用场景管理器里的一个方法,将当前所有打开的场景标志为“脏的”,即被改动过的,这个时候在就可以Ctrl + S保存了。
EditorSceneManager.MarkAllScenesDirty();
同理,如果你通过代码改过资源,那么也需要手动调用AssetDatabase.Refresh 刷新下编辑器。

文章转自:http://www.cnblogs.com/jeason1997/p/5109541.html

没有更多推荐了,返回首页