使用编辑器窗口,可编写脚本的对象和自定义编辑器扩展Unity编辑器

大家好! 我叫Grisha,是CGDevs的创始人。 今天,我想谈论编辑器扩展并谈论我的一个项目,我决定将其发布在OpenSource中。

Unity是一个很棒的工具,但是有一点问题。 对于初学者来说,为了制作一个简单的房间(一个装有窗户的盒子),必须掌握3D建模或尝试从四边形中组装一些东西。 最近,它已成为完全免费的ProBuilder,但它也是一个简化的3D建模包。 我想要一个简单的工具,使我们能够快速创建带有窗户和正确UV的房间等环境。 很久以前,我为Unity开发了一个插件,该插件可让您使用2D工程图快速构建公寓和房间等环境的原型,现在我决定将其放入OpenSource。 使用他的示例,我们将分析如何扩展编辑器以及为此提供的工具。 如果您有兴趣,欢迎猫。 像往常一样,将附加到项目末尾的链接。



Unity3d具有足够广泛的工具箱来扩展编辑器的功能。 多亏了诸如EditorWindow之类的类,以及Custom InspectorProperty DrawerTreeView的功能 (+ UIElements应该很快就会出现),可以在单元顶部轻松构建复杂程度不同的框架。

今天,我们将讨论我用来开发解决方案的一种方法,以及我必须面对的一些有趣的问题。



该解决方案基于三个类的使用,例如EditorWindow (所有其他窗口), ScriptableObject (数据存储)和CustomEditor (Scriptable Object的附加检查器功能)。

在开发编辑器扩展时,重要的是尝试遵守Unity开发人员将使用扩展的原则,因此界面应清晰,原生并集成到Unity工作流中。

让我们谈谈有趣的任务。

为了使我们原型化,首先,我们需要学习如何绘制图纸,并以此为基础生成环境。 为此,我们需要一个特殊的EditorWindow窗口,在其中将显示所有图形。 原则上,可以在SceneView中进行绘制,但是最初的想法是在最终确定解决方案时,您可能希望同时打开多个图形。 通常,在一个单元中创建一个单独的窗口是一项相当简单的任务。 可以在Unity手册中找到 但是绘图网格是一个更有趣的任务。 在这个问题上有几个问题。

Unity具有几种影响窗口颜色的样式。

事实是,大多数使用Pro专业版Unity的人都使用深色主题,而在免费版中,只有浅色主题可用。 但是,在图形编辑器中使用的颜色不应与背景合并。 在这里,您可以提出两种解决方案。 困难的是制作自己的样式版本,检查并更改单元版本的调色板。 简单的事情是用某种颜色填充窗口背景。 在开发中,决定使用一种简单的方法。 如何完成此操作的一个示例是在OnGUI方法中调用此类代码。

某种颜色
GUI.color = BgColor; GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture); GUI.color = Color.white; 



本质上,我们只是将BgColor颜色纹理绘制到整个窗口。



绘制并移动网格

在这里一次发现了几个问题。 首先,您必须输入坐标系。 事实是,为了正确和方便地工作,我们需要在网格坐标中重新计算窗口的GUI坐标。 为此,实现了两种转换方法(本质上,这是两个绘制的TRS矩阵)

将窗口坐标转换为屏幕坐标
 public Vector2 GUIToGrid(Vector3 vec) { Vector2 newVec = ( new Vector2(vec.x, -vec.y) - new Vector2(_ParentWindow.position.width / 2, -_ParentWindow.position.height / 2)) * _Zoom + new Vector2(_Offset.x, -_Offset.y); return newVec.RoundCoordsToInt(); } public Vector2 GridToGUI(Vector3 vec) { return (new Vector2(vec.x - _Offset.x, -vec.y - _Offset.y) ) / _Zoom + new Vector2(_ParentWindow.position.width / 2, _ParentWindow.position.height / 2); } 



其中_ParentWindow是我们要在其中绘制网格的窗口, _Offset是网格的当前位置,而_Zoom是逼近度。

其次,要绘制线条,我们需要Handles.DrawLine方法。 Handles类具有许多在编辑器窗口,检查器或SceneView中呈现简单图形的有用方法。 在插件开发时(Unity 5.5), Handles.DrawLine-分配了内存,并且通常工作很慢。 因此,可渲染的行数受CELLS_IN_LINE_COUNT常数限制,并且在缩放时也进行了“ LOD级别”处理,以在编辑器中获得可接受的fps。

网格图
 void DrawLODLines(int level) { var gridColor = SkinManager.Instance.CurrentSkin.GridColor; var step0 = (int) Mathf.Pow(10, level); int halfCount = step0 * CELLS_IN_LINE_COUNT / 2 * 10; var length = halfCount * DEFAULT_CELL_SIZE; int offsetX = ((int) (_Offset.x / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; int offsetY = ((int) (_Offset.y / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; for (int i = -halfCount; i <= halfCount; i += step0) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 0.3f); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } offsetX = (offsetX / (10 * step0)) * 10 * step0; offsetY = (offsetY / (10 * step0)) * 10 * step0; ; for (int i = -halfCount; i <= halfCount; i += step0 * 10) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 1); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } } 



几乎所有内容都已为网格做好了准备。 他的动作非常简单。 _Offset本质上是当前的“相机”位置。

网格运动
  public void Move(Vector3 dv) { var x = _Offset.x + dv.x * _Zoom; var y = _Offset.y + dv.y * _Zoom; _Offset.x = x; _Offset.y = y; } 



在项目本身中,您通常可以熟悉窗口代码,并了解如何将按钮添加到窗口中。

我们走得更远。 除了用于绘制工程图的单独窗口外,我们还需要以某种方式存储工程图本身。 内部Unity序列化引擎(可脚本对象)对此非常有用。 实际上,它允许您将描述的类作为资产存储在项目中,这对于许多单元开发人员而言非常方便且本机。 例如,Apartment类的一部分,通常负责存储布局信息。

属于公寓类
  public class Apartment : ScriptableObject { #region fields public float Height; public bool IsGenerateOutside; public Material OutsideMaterial; public Texture PlanImage; [SerializeField] private List<Room> _Rooms; [SerializeField] private Rect _Dimensions; private Vector2[] _DimensionsPoints = new Vector2[4]; #endregion 



在编辑器中,当前版本如下所示:



当然,这里已经应用了CustomEditor,但是,您仍然可以注意到,编辑器中显示了诸如_Dimensions,Height,IsGenerateOutside,OutsideMaterial和PlanImage之类的参数。

所有公共字段和标有[SerializeField]的字段都被序列化(在这种情况下,保存在文件中)。 如果您需要保存工程图,那么这很有用,但是在使用ScriptableObject和所有编辑器资源时,需要记住,最好调用AssetDatabase.SaveAssets()方法来保存文件的状态。 否则,更改将不会保存。 如果只是不用手保存项目。

现在,我们将部分分析ApartmentCustomInspector类及其工作方式。

Class ApartmentCustomInspector类
  [CustomEditor(typeof(Apartment))] public class ApartmentCustomInspector : Editor { private Apartment _ThisApartment; private Rect _Dimensions; private void OnEnable() { _ThisApartment = (Apartment) target; _Dimensions = _ThisApartment.Dimensions; } public override void OnInspectorGUI() { TopButtons(); _ThisApartment.Height = EditorGUILayout.FloatField("Height (cm)", _ThisApartment.Height); var dimensions = EditorGUILayout.Vector2Field("Dimensions (cm)", _Dimensions.size).RoundCoordsToInt(); _ThisApartment.PlanImage = (Texture) EditorGUILayout.ObjectField(_ThisApartment.PlanImage, typeof(Texture), false); _ThisApartment.IsGenerateOutside = EditorGUILayout.Toggle("Generate outside (Directional Light)", _ThisApartment.IsGenerateOutside); if (_ThisApartment.IsGenerateOutside) _ThisApartment.OutsideMaterial = (Material) EditorGUILayout.ObjectField( "Outside Material", _ThisApartment.OutsideMaterial, typeof(Material), false); GenerateButton(); var dimensionsRect = new Rect(-dimensions.x / 2, -dimensions.y / 2, dimensions.x, dimensions.y); _Dimensions = dimensionsRect; _ThisApartment.Dimensions = _Dimensions; } private void TopButtons() { GUILayout.BeginHorizontal(); CreateNewBlueprint(); OpenBlueprint(); GUILayout.EndHorizontal(); } private void CreateNewBlueprint() { if (GUILayout.Button( "Create new" )) { var manager = ApartmentsManager.Instance; manager.SelectApartment(manager.CreateOrGetApartment("New Apartment" + GUID.Generate())); } } private void OpenBlueprint() { if (GUILayout.Button( "Open in Builder" )) { ApartmentsManager.Instance.SelectApartment(_ThisApartment); ApartmentBuilderWindow.Create(); } } private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } } 


CustomEditor是一个非常强大的工具,可让您优雅地解决许多扩展编辑器的典型任务。 与ScriptableObject配对,它使您可以进行简单,方便和直观的编辑器扩展。 该类比仅添加按钮要复杂一些,正如您在原始类中看到的那样,正在对[SerializeField]私有List _Rooms字段进行序列化。 首先,在检查器中将其显示为空,其次,这可能会导致无法预料的错误和绘制状态。 OnInspectorGUI方法负责呈现检查器,如果只需要添加按钮,则可以在其中调用DrawDefaultInspector()方法,然后将绘制所有字段。

然后手动绘制必要的字段和按钮。 EditorGUILayout类本身具有许多支持该单元支持的多种字段类型的实现。 但是Unity中的按钮渲染是在GUILayout类中实现的。 在这种情况下,按钮按下处理的工作方式。 OnInspectorGUI-在每个用户输入的鼠标事件(鼠标移动,鼠标在编辑器窗口内的单击等)上执行。如果用户单击框中的按钮,则该方法返回true,并处理if(由您描述)内部的方法。一个 例如:

网格生成按钮
 private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } 


单击“生成网格”按钮时,将调用静态方法,该方法负责生成特定布局的网格。

除了扩展Unity编辑器时使用的这些基本机制之外,我还要分别提及一个非常简单且非常方便的工具,出于某种原因,许多人都忘记了它-选择。 Selection是一个静态类,允许您在检查器和ProjectView中选择必要的对象。

为了选择一个对象,您只需要编写Selection.activeObject = MyAwesomeUnityObject。 最好的部分是它与ScriptableObject一起使用。 在该项目中,他负责选择图纸以及带有图纸的窗口中的房间。

感谢您的关注! 我希望本文和项目对您有所帮助,并且您将通过一种扩展Unity编辑器的方法对自己有所了解。 和往常一样- 到GitHub项目链接 ,您可以在其中看到整个项目。 它仍然有点潮湿,但是尽管如此,它已经允许您简单,快速地在2d中制定计划。

Source: https://habr.com/ru/post/zh-CN431856/


All Articles