Unity Editor Extension via the Editor Window, Scriptable Object and Custom Editor
Hello! My name is Grisha, and I am the founder of CGDevs. Today I want to talk about the editor's extensions and tell you about one of my projects, which I decided to put in OpenSource.
Unity is a great tool, but it has a small problem. For a beginner, to make a simple room (a box with windows), you must either master 3D modeling, or try to collect something from the quad. Recently, ProBuilder has become completely free, but it is also a simplified 3D modeling package. I wanted a simple tool that allows you to quickly create environments like rooms with windows and the right UV at the same time. For a long time, I developed one plugin for Unity, which allows you to quickly prototype environments such as apartments and rooms using 2D drawing, and now I decided to put it into OpenSource. On his example, we will analyze how to expand the editor and what tools for this exist. If you're interested - welcome under cat. The link to the project at the end, as always, is attached.
Unity3d has a wide enough toolkit to expand the capabilities of the editor. Thanks to classes such as EditorWindow , as well as the functionality of Custom Inspector , Property Drawer and TreeView (+ UIElements are coming soon ), it is easy to build on your frameworks of varying degrees of complexity over your unit.
Today we will talk about one of the approaches that I used when developing my solution and about a couple of interesting tasks that I had to face.
The solution is based on the use of three classes, such as EditorWindow (all additional windows), ScriptableObject (data storage) and CustomEditor (additional inspector functionality for the Scriptable Object).
When developing editor extensions, it is important to try to adhere to the principle that the extension will be used by the Unity developers, so the interfaces must be clear, native, and written into the Unity workflow.
Let's talk about interesting problems.
In order for us to prototype something, first of all we need to learn how to draw drawings, from which we will generate our environment. To do this, we need a special EditorWindow window, in which we will display all the drawings. In principle, it would be possible to draw in SceneView, but the initial idea was that when finalizing the solution, you might want to open several drawings at the same time. In general, in the unit to create a separate window is a fairly simple task. You can read about it in Unity manuals. But the drawing grid is a more interesting task. There are several problems with this topic.
In Unity, several styles that affect the colors of windows
The fact is that most people using the Pro version of Unity use a dark theme, and in the free version only the light one is available. However, the colors that are used in the drawing editor should not blend into the background. Here you can come up with two solutions. Difficult - to make your own version of styles, check it and change the palette under the version of the unit. And simple - fill the window background with a certain color. When developing it was decided to use a simple way. An example of how this can be done is to call such code in the OnGUI method.
In essence, we simply rendered the BgColor color texture into the entire window.
Drawing and moving the grid
Here a few problems opened at once. First, it was necessary to enter your coordinate system. The fact is that for correct and convenient work we need to recalculate the GUI coordinates of the window to the coordinates of the grid. For this, two transformation methods were implemented (in essence, these are two TRS matrices)
where _ParentWindow is the window in which we are going to draw the grid, _Offset is the current position of the grid, and _Zoom is the degree of approximation.
Secondly, for drawing lines, we need the Handles.DrawLine method . The Handles class has many useful methods for drawing simple graphics in the editor, inspector or SceneView windows. At the time of development of the plug-in (Unity 5.5), Handles.DrawLine - allocated memory and as a whole worked rather slowly. For this reason, the number of possible lines for drawing was limited to the constant CELLS_IN_LINE_COUNT , and also made the “LOD level” at the zoom to achieve an acceptable fps in the editor.
For Grid almost everything is ready. His movement is described very simply. _Offset is essentially the current position of the “camera”.
In the project itself, you can familiarize yourself with the window code in general and see how you can add buttons to the window.
We go further. In addition to a separate window for drawing drawings, we need to somehow store the drawings themselves. The internal Unity serialization mechanism, the Scriptable Object, is great for this. In fact, it allows you to store the described classes in the form of assets in the project, which is very convenient and native for many developers. For example, part of the Apartment class, which is responsible for storing information about the layout as a whole.
In the editor it looks like this in the current version:
Here, of course, CustomEditor has already been applied, but nevertheless you can see that parameters such as _Dimensions, Height, IsGenerateOutside, OutsideMaterial and PlanImage are displayed in the editor.
All public fields and fields marked with [SerializeField] are serialized (that is, saved in a file in this case). This greatly helps to save drawings if necessary, but when working with ScriptableObject, and all the resources of the editor you need to remember that it is better to call AssetDatabase.SaveAssets () to save the state of the files. Otherwise, the changes will not be saved. If you just do not save the project with your hands.
Now partially analyze the class ApartmentCustomInspector, and how it works.
CustomEditor is a very powerful tool that allows you to solve elegantly many typical tasks for expanding the editor. Paired with ScriptableObject, it allows you to make simple, convenient and understandable editor extensions. This class is a bit more complicated than simply adding buttons, since in the source class you can see that the [SerializeField] private List _Rooms field is serialized. Displaying it in the inspector is, firstly, to nothing, and secondly, it can lead to unforeseen bugs and drawing conditions. The OnInspectorGUI method is responsible for drawing the inspector, and if you just need to add buttons, you can call the DrawDefaultInspector () method in it and all the fields will be drawn.
Immediately the necessary fields and buttons are drawn by hand. The EditorGUILayout class in itself has many implementations for the most different types of fields supported by the unit. But button rendering in Unity is implemented in the GUILayout class. As in this case, the processing of pressing buttons. OnInspectorGUI - handles for each user input event with the mouse (mouse movement, mouse clicks inside the editor window, etc.) If the user clicks the button in the billing box, the method returns true and work out the methods that are inside the if ' a. For example:
When you click on the Generate Mesh button, a static method is called that is responsible for generating the mesh of a particular layout.
In addition to these basic mechanisms used in the expansion of the Unity editor, I would like to separately note a very simple and very convenient tool, which for some reason many people forget about - Selection. Selection is a static class that allows you to select the necessary objects in the inspector and ProjectView.
In order to select an object, you just need to write Selection.activeObject = MyAwesomeUnityObject. And the most beautiful thing is that it works with ScriptableObject. In this project, he is responsible for selecting the drawing and the rooms in the drawing window.
Thanks for attention! I hope the article and the project will be useful to you, and you will get something new for you in one of the approaches of the Unity editor extension. And as always - a link to the GitHub project , where you can see the whole project. It is still a bit damp, but nevertheless it already allows you to make layouts in 2D simply and quickly.
Unity is a great tool, but it has a small problem. For a beginner, to make a simple room (a box with windows), you must either master 3D modeling, or try to collect something from the quad. Recently, ProBuilder has become completely free, but it is also a simplified 3D modeling package. I wanted a simple tool that allows you to quickly create environments like rooms with windows and the right UV at the same time. For a long time, I developed one plugin for Unity, which allows you to quickly prototype environments such as apartments and rooms using 2D drawing, and now I decided to put it into OpenSource. On his example, we will analyze how to expand the editor and what tools for this exist. If you're interested - welcome under cat. The link to the project at the end, as always, is attached.
Unity3d has a wide enough toolkit to expand the capabilities of the editor. Thanks to classes such as EditorWindow , as well as the functionality of Custom Inspector , Property Drawer and TreeView (+ UIElements are coming soon ), it is easy to build on your frameworks of varying degrees of complexity over your unit.
Today we will talk about one of the approaches that I used when developing my solution and about a couple of interesting tasks that I had to face.
The solution is based on the use of three classes, such as EditorWindow (all additional windows), ScriptableObject (data storage) and CustomEditor (additional inspector functionality for the Scriptable Object).
When developing editor extensions, it is important to try to adhere to the principle that the extension will be used by the Unity developers, so the interfaces must be clear, native, and written into the Unity workflow.
Let's talk about interesting problems.
In order for us to prototype something, first of all we need to learn how to draw drawings, from which we will generate our environment. To do this, we need a special EditorWindow window, in which we will display all the drawings. In principle, it would be possible to draw in SceneView, but the initial idea was that when finalizing the solution, you might want to open several drawings at the same time. In general, in the unit to create a separate window is a fairly simple task. You can read about it in Unity manuals. But the drawing grid is a more interesting task. There are several problems with this topic.
In Unity, several styles that affect the colors of windows
The fact is that most people using the Pro version of Unity use a dark theme, and in the free version only the light one is available. However, the colors that are used in the drawing editor should not blend into the background. Here you can come up with two solutions. Difficult - to make your own version of styles, check it and change the palette under the version of the unit. And simple - fill the window background with a certain color. When developing it was decided to use a simple way. An example of how this can be done is to call such code in the OnGUI method.
Color shading
GUI.color = BgColor;
GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture);
GUI.color = Color.white;
In essence, we simply rendered the BgColor color texture into the entire window.
Drawing and moving the grid
Here a few problems opened at once. First, it was necessary to enter your coordinate system. The fact is that for correct and convenient work we need to recalculate the GUI coordinates of the window to the coordinates of the grid. For this, two transformation methods were implemented (in essence, these are two TRS matrices)
Recalculation of window coordinates to screen coordinates
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);
}
where _ParentWindow is the window in which we are going to draw the grid, _Offset is the current position of the grid, and _Zoom is the degree of approximation.
Secondly, for drawing lines, we need the Handles.DrawLine method . The Handles class has many useful methods for drawing simple graphics in the editor, inspector or SceneView windows. At the time of development of the plug-in (Unity 5.5), Handles.DrawLine - allocated memory and as a whole worked rather slowly. For this reason, the number of possible lines for drawing was limited to the constant CELLS_IN_LINE_COUNT , and also made the “LOD level” at the zoom to achieve an acceptable fps in the editor.
Grid drawing
voidDrawLODLines(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))
);
}
}
For Grid almost everything is ready. His movement is described very simply. _Offset is essentially the current position of the “camera”.
Grid movement
publicvoidMove(Vector3 dv)
{
var x = _Offset.x + dv.x * _Zoom;
var y = _Offset.y + dv.y * _Zoom;
_Offset.x = x;
_Offset.y = y;
}
In the project itself, you can familiarize yourself with the window code in general and see how you can add buttons to the window.
We go further. In addition to a separate window for drawing drawings, we need to somehow store the drawings themselves. The internal Unity serialization mechanism, the Scriptable Object, is great for this. In fact, it allows you to store the described classes in the form of assets in the project, which is very convenient and native for many developers. For example, part of the Apartment class, which is responsible for storing information about the layout as a whole.
Part of the class Apartment
publicclassApartment : ScriptableObject
{
#region fieldspublicfloat Height;
publicbool IsGenerateOutside;
public Material OutsideMaterial;
public Texture PlanImage;
[SerializeField] private List<Room> _Rooms;
[SerializeField] private Rect _Dimensions;
private Vector2[] _DimensionsPoints = new Vector2[4];
#endregion
In the editor it looks like this in the current version:
Here, of course, CustomEditor has already been applied, but nevertheless you can see that parameters such as _Dimensions, Height, IsGenerateOutside, OutsideMaterial and PlanImage are displayed in the editor.
All public fields and fields marked with [SerializeField] are serialized (that is, saved in a file in this case). This greatly helps to save drawings if necessary, but when working with ScriptableObject, and all the resources of the editor you need to remember that it is better to call AssetDatabase.SaveAssets () to save the state of the files. Otherwise, the changes will not be saved. If you just do not save the project with your hands.
Now partially analyze the class ApartmentCustomInspector, and how it works.
Class ApartmentCustomInspector
[CustomEditor(typeof(Apartment))]
publicclassApartmentCustomInspector : Editor
{
private Apartment _ThisApartment;
private Rect _Dimensions;
privatevoidOnEnable()
{
_ThisApartment = (Apartment) target;
_Dimensions = _ThisApartment.Dimensions;
}
publicoverridevoidOnInspectorGUI()
{
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;
}
privatevoidTopButtons()
{
GUILayout.BeginHorizontal();
CreateNewBlueprint();
OpenBlueprint();
GUILayout.EndHorizontal();
}
privatevoidCreateNewBlueprint()
{
if (GUILayout.Button(
"Create new"
))
{
var manager = ApartmentsManager.Instance;
manager.SelectApartment(manager.CreateOrGetApartment("New Apartment" + GUID.Generate()));
}
}
privatevoidOpenBlueprint()
{
if (GUILayout.Button(
"Open in Builder"
))
{
ApartmentsManager.Instance.SelectApartment(_ThisApartment);
ApartmentBuilderWindow.Create();
}
}
privatevoidGenerateButton()
{
if (GUILayout.Button(
"Generate Mesh"
))
{
MeshBuilder.GenerateApartmentMesh(_ThisApartment);
}
}
}
CustomEditor is a very powerful tool that allows you to solve elegantly many typical tasks for expanding the editor. Paired with ScriptableObject, it allows you to make simple, convenient and understandable editor extensions. This class is a bit more complicated than simply adding buttons, since in the source class you can see that the [SerializeField] private List _Rooms field is serialized. Displaying it in the inspector is, firstly, to nothing, and secondly, it can lead to unforeseen bugs and drawing conditions. The OnInspectorGUI method is responsible for drawing the inspector, and if you just need to add buttons, you can call the DrawDefaultInspector () method in it and all the fields will be drawn.
Immediately the necessary fields and buttons are drawn by hand. The EditorGUILayout class in itself has many implementations for the most different types of fields supported by the unit. But button rendering in Unity is implemented in the GUILayout class. As in this case, the processing of pressing buttons. OnInspectorGUI - handles for each user input event with the mouse (mouse movement, mouse clicks inside the editor window, etc.) If the user clicks the button in the billing box, the method returns true and work out the methods that are inside the if ' a. For example:
Mesh generation button
privatevoidGenerateButton()
{
if (GUILayout.Button(
"Generate Mesh"
))
{
MeshBuilder.GenerateApartmentMesh(_ThisApartment);
}
}
When you click on the Generate Mesh button, a static method is called that is responsible for generating the mesh of a particular layout.
In addition to these basic mechanisms used in the expansion of the Unity editor, I would like to separately note a very simple and very convenient tool, which for some reason many people forget about - Selection. Selection is a static class that allows you to select the necessary objects in the inspector and ProjectView.
In order to select an object, you just need to write Selection.activeObject = MyAwesomeUnityObject. And the most beautiful thing is that it works with ScriptableObject. In this project, he is responsible for selecting the drawing and the rooms in the drawing window.
Thanks for attention! I hope the article and the project will be useful to you, and you will get something new for you in one of the approaches of the Unity editor extension. And as always - a link to the GitHub project , where you can see the whole project. It is still a bit damp, but nevertheless it already allows you to make layouts in 2D simply and quickly.