<<

Today I wanted to talk about expanding Unity3D’s custom editor functionality by adding the ability to zoom in and out on the contents of the window. Additionally, we will be making the entire window itself scrollable to support content that doesn’t fit within the window.

To get started, if you have not worked with custom editor windows, I recommend reading the following: https://docs.unity3d.com/Manual/editor-EditorWindows.html ​

You can download an example Unity project with a blank zoomable and scrollable editor window script here: https://bitbucket.org/ahanold/zoomable-editor-window-example/overview ​

Let’s first start by creating a blank editor window:

using UnityEngine; ​ ​ using UnityEditor; ​ ​

public class ExampleEditor : EditorWindow ​ ​ ​ ​ ​ ​ ​ ​ { [MenuItem("Custom/Example")] ​ ​ ​ ​ ​ public static void Initialize() ​ ​ ​ ​ ​ ​ ​ ​ { ExampleEditor window = ExampleEditor.GetWindow(); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ }

public void OnGUI() ​ ​ ​ ​ ​ ​ {

} }

Next, we will need a few global properties to support the zooming function.

private float Scaling; ​ ​ ​ ​ ​ private Vector2 MousePosition; ​ ​ ​ ​ ​ private Vector2 ScrollPosition; ​ ​ ​ ​ ​ private Rect GroupRect; ​ ​ ​ ​ ​

private const float MaxGraphSize = 16000.0f; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

Scaling will represent our zoom factor. A Scaling value of 1 means we are at 100% zoom. A ​ ​ ​ Scaling value of 0.5 means that we are zoomed out to 50% zoom. ​

MousePosition will be used to track the adjusted mouse position to account for scale. If you ​ need to track your mouse position for any reason, such as detecting mouse clicks in certain locations, you will want to use the scaled MousePosition property. We will get into setting this ​ ​ later.

ScrollPosition will be used to track the current position within the scrollable area. We will get into ​ setting this later.

GroupRect is used to the recreate the scaled group for the editor window itself and for the scroll ​ view. Behind the scenes, Unity makes a default GUI.BeginGroup call that sets the default group ​ ​ size. This group also acts as a clipping area, which means that all GUI elements outside of the group will not be drawn. We will get into the importance of this later.

MaxGraphSize is a constant that will be used to make an arbitrarily large content area. ​

Now, we need to can set the value for the adjusted MousePosition property. ​ ​

public void OnGUI() ​ ​ ​ ​ ​ ​ { Event e = Event.current; ​ ​ ​ ​ ​ ​ ​ MousePosition = (e.mousePosition + ScrollPosition) / Scaling; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ }

Let’s go ahead and create our keybinding to enable zooming. To do so, we’ll create another method to the current event to detect the keybinding Ctrl+Scroll.

public void OnGUI() ​ ​ ​ ​ ​ { Event e = Event.current; ​ ​ ​ ​ ​ ​ ​ MousePosition = (e.mousePosition + ScrollPosition) / Scaling; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

ProcessScrollWheel(e); ​ ​ ​ ​ }

private void ProcessScrollWheel(Event e) ​ ​ ​ ​ ​ ​ ​ { if(e == null || e.type != EventType.ScrollWheel) ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ return; ​

if (e.control) ​ ​ ​ ​ ​ ​ { float shiftMultiplier = e.shift ? 4 : 1; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

Scaling = Mathf.Clamp(Scaling - e.delta.y * 0.01f * shiftMultiplier, 0.5f, 2f); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

e.Use(); ​ ​ ​ } }

This should ensure that our code only runs during the ScrollWheel event and while the is held. Because this will be running in a scrollable area which uses the scroll wheel as well, we don’t want to interfere.

The shiftMultiplier variable is used to increase the zooming increments for faster navigation. ​ ​ Currently, this makes it zoom in or out four times greater than normal.

Lastly, we need to call the Event.Use() method in order to consume the event so that it doesn’t propagate to the scrollable area. Otherwise, you will be zooming and scrolling around the window at the same time.

For more information on using Unity’s event system, read here: https://docs.unity3d.com/550/Documentation/ScriptReference/Event.

Now let’s draw the scrollable area. We will create several more methods for this, but the bulk of the work will be handled in a method called DrawScrollView.

private void DrawScrollView() ​ ​ ​ ​ ​ { ScaleWindowGroup(); ​ ​ }

private void ScaleWindowGroup() ​ ​ ​ ​ ​ { GUI.EndGroup(); ​ ​ ​ CalculateScaledWindowRect(); ​ ​ GUI.BeginGroup(GroupRect); ​ ​ ​ ​ ​ }

private void CalculateScaledWindowRect() ​ ​ ​ ​ ​ { GroupRect.x = 0; ​ ​ ​ ​ ​ ​ GroupRect.y = 21; ​ ​ ​ ​ ​ ​ GroupRect.width = (MaxGraphSize + ScrollPosition.x) / Scaling; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ GroupRect.height = (MaxGraphSize + ScrollPosition.y) / Scaling; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ }

The first thing we want to do is recreate the default group using the properly scaled rect. We can accomplish this by simply calling GUI.EndGroup() outside of any other layouts (i.e. GUILayout.BeginVertical). Next we need to recalculate the scaled rect. We want the y value to ​ ​ start at the bottom of the window’s in the actual content area, which is 21 pixels from the top. Once GroupRect is set, we can simply call GUI.BeginGroup(GroupRect) to reinitialize the ​ ​ group with the right scale.

Here you can see the clipping issues with the default group when it isn’t re-created with scaling.

Next, we’ll want to create the scroll view. Simply make a call to EditorGUILayout.BeginScrollView using the global ScrollPosition variable. We’ll also want to ​ ​ ensure the both the horizontal and vertical scroll bars show at all times (it makes it much easier for calculating window sizes if it’s not changing), so we pass in true for the ​ ​ alwaysShowHorizontal and alwaysShowVertical parameters. ​ ​ ​

private void DrawScrollView() ​ ​ ​ ​ ​ { ScaleWindowGroup(); ​

ScrollPosition = EditorGUILayout.BeginScrollView(ScrollPosition, true, true); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ }

The key to getting scroll views to work without causing clipping issues when scaling is to recreate the group that the scroll view uses behind the scenes. We’ll want to run through a similar process that we did for the window group before.

private void DrawScrollView() ​ ​ ​ ​ ​ { ScaleWindowGroup(); ​

ScrollPosition = EditorGUILayout.BeginScrollView(ScrollPosition, true, true); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

ScaleScrollGroup(); ​ }

//... private void ScaleScrollGroup() ​ ​ ​ ​ ​ { GUI.EndGroup(); ​ ​ ​ CalculateScaledScrollRect(); ​ GUI.BeginGroup(GroupRect); ​ ​ ​ ​ ​ } private void CalculateScaledScrollRect() ​ ​ ​ ​ ​ { GroupRect.x = -ScrollPosition.x / Scaling; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ GroupRect.y = -ScrollPosition.y / Scaling; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ GroupRect.width = (position.width + ScrollPosition.x - GUI.skin.verticalScrollbar.fixedWidth) / Scaling; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ GroupRect.height = (position.height + ScrollPosition.y - 21 - GUI.skin.horizontalScrollbar.fixedHeight) / ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ Scaling; }

Because we’re calling this within the scroll view layout, it’s only going to end the group for the scroll view, and not the total window. There are a few key differences when calculating the rect for the scroll view. First of all, in order to simulate scrolling, we need to offset the x and y position by the scroll position. Secondly, to calculate the width and height, we want it to fill the visible screen, which is the width and height of the content area minus the scroll bars. Additionally, we need to add the scroll position offsets to the width and height otherwise the rect will come up short if we’ve scrolled at all.

To actually adjust the scale of the editor window contents, we have to adjust the matrix of the GUI.

private void DrawScrollView() ​ ​ ​ ​ ​ { //...

Matrix4x4 old = GUI.matrix; ​ ​ ​ ​ ​ ​

Matrix4x4 translation = Matrix4x4.TRS(new Vector3(0, 21, 1), Quaternion.identity, Vector3.one); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

Matrix4x4 scale = Matrix4x4.Scale(new Vector3(Scaling, Scaling, Scaling)); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

GUI.matrix = translation * scale * translation.inverse; ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

GUILayout.BeginArea(new Rect(0, 0, MaxGraphSize * Scaling, MaxGraphSize * Scaling)); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

/* ​ * ​ * ADD YOUR SCALABLE CONTENT HERE! ​ ​ * ​ */ ​

GUILayout.EndArea(); ​ ​ ​ ​ }

Before making any modifications, we need to cache the current GUI matrix in order to reset it later. Next, we need to create a translation and scale matrix. For the translation matrix, we want the Vector3 set to the beginning of the content area. Remember that the 21 for the y-offset is ​ ​ accounting for the height of the window tab. For the scale matrix, simply apply the Scaling value ​ ​ to all coordinates. Now, we set the matrix equal the product of the translation, scale and ​ ​ ​ ​ transform.inverse matrices. Finally, we begin a new area sized to the desired scale. The width ​ and height can scaled to any size, or even disregarded if you want the scrollview content size to size dynamically based on the GUILayout or EditorGUILayout contents.

Congratulations, your scroll view is now scaled! There are still some closing operations we need to perform, but you can now add whatever content you’d like and it should be appropriately scaled.

After you have added your scalable content, you will want to close out the scaled area (if you are using a scaled area). Next we’ll need to close out the and reset the custom groups that we created.

private void DrawScrollView() ​ ​ ​ ​ ​ { // ... ​

// Restore the matrix. ​ GUI.matrix = old; ​ ​ ​ ​

// Stop the scrollable view. ​ EditorGUILayout.EndScrollView(); ​ ​ ​ ​

// Reset the windows group for any additional content ​ GUI.EndGroup(); ​ ​ ​ GUI.BeginGroup(new Rect(0, GroupHeaderSize, position.width, position.height)); ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ }

To begin the closing process, we first need to reset the GUI.matrix back to it’s original size and scale using our cached matrix. Next, we want to end the scroll view, which will in turn end the custom group for the scroll view. Then we want to end the group that we created for the window and recreate it to it’s original scale.