Unity Tutorial: Forschungsbaum-Editor Nachtrag

Dies ist ein Nachtrag zum Forschungsbaum-Editor Tutorial. Der vollständige Quellcode des Tutorials kann hier heruntergeladen werden:

Der Forschungsbaum-Editor, den wir in den letzten Tutorial-Beiträgen implementiert haben, besitzt alle notwendigen Features, um diesen (natürlich mit Anpassungen an die persönlichen Anforderungen) produktiv nutzen zu können. Allerdings kann man die Arbeit mit diesem Tool noch deutlich angenehmer gestalten, indem noch ein paar Quality of Life Features hinzugefügt werden. Diese wollen wir im Folgenden näher betrachten, wobei die Verbesserung nicht zwangsläufig auf den Forschungsbaum-Editor begrenzt sein müssen, sondern auch in beliebigen anderen Unity-Tools nach Bedarf implementiert werden können.

Snap to Grid

Bislang können die Forschungsknoten frei im Editor-Fenster verschoben werden. Angenehmer ist es jedoch oftmals, wenn die Fenster sich an einem (zunächst unsichtbares, dazu gleich mehr) Raster ausrichten. Dafür bedarf es keiner großen Änderungen am Programmcode.

Wir navigieren zu diesem Code-Abschnitt des Forschungseditors:

//Define all your Windows between BeginWindows() and EndWindows(). The { } Brackets are not needed, but make the Code easiert to read
BeginWindows();
{
    //Show a Window for all Research Nodes
    for (int i = 0; i < windowPositionAndSizes.Count; i++)
    {
        windowPositionAndSizes[i] = GUILayout.Window(researchNodes[i].researchNodeID, windowPositionAndSizes[i], DrawResearchNodeWindow, researchNodes[i].researchNodeName);
    }
}
EndWindows();

Diesen ersetzen wir durch dieses Code-Snippet. Die Änderungen sind minimal, der Effekt jedoch sofort erreicht. Die Position der Fenster wird jetzt, wenn man versucht ein Fenster zu verschieben, an ein unsichtbares Raster mit einer Feldgröße von 25 ausgerichtet. Die Feldgröße kann natürlich frei gewählt werden.

//Define all your Windows between BeginWindows() and EndWindows(). The { } Brackets are not needed, but make the Code easiert to read
BeginWindows();
{
    //Show a Window for all Research Nodes
    for (int i = 0; i < windowPositionAndSizes.Count; i++)
    {
        //Align Nodes to Grid
        Rect rect = windowPositionAndSizes[i];
        rect.x = ((int)(rect.x / 25.0f)) * 25.0f;
        rect.y = ((int)(rect.y / 25.0f)) * 25.0f;

        //Draw the Research Nodes
        windowPositionAndSizes[i] = GUILayout.Window(researchNodes[i].researchNodeID, rect, DrawResearchNodeWindow, researchNodes[i].researchNodeName);
    }
}
EndWindows();

Scrollbars

Die Fläche, die uns der Forschungsbaum-Editor bislang bereitstellt, ist auf den initial sichtbaren Bereich beschränkt. Wenn wir einen Forschungsbaum mit hunderten (oder noch mehr) Forschungsknoten designen wollen, wird dieser Platz nicht ausreichen. Um etwas mehr Raum zu schaffen, muss die Editor-Fläche vergrößert werden und hierfür benötigen wir Scrollbars. Diese lassen sich am besten mittels einiger Code-Snippets implementieren. Hierfür benötigen wir zwei neue Klassen mit folgendem Inhalt:

EditorZoomArea.cs

using UnityEngine;

/// <summary>
/// Creates a zoomable Area within an Editor Extension.
/// 
/// Bases on:
/// http://martinecker.com/martincodes/unity-editor-window-zooming/
/// </summary>
public class EditorZoomArea
{
    /// <summary>
    /// Default Tab Height.
    /// </summary>
    private const float kEditorWindowTabHeight = 21.0f;

    /// <summary>
    /// The previous Matrix needed to reset the Matrix when ending the Zoom Area.
    /// </summary>
    private static Matrix4x4 _prevGuiMatrix;

    /// <summary>
    /// Begins a zoomable Area.
    /// </summary>
    /// <param name="zoomScale">The Zoom Scale.</param>
    /// <param name="screenCoordsArea">The Anchor Screen Coordinates and Width / Height of the Zoom Area.</param>
    /// <returns>The Zoom Area.</returns>
    public static Rect Begin(float zoomScale, Rect screenCoordsArea)
    {
        // End the group Unity begins automatically for an EditorWindow to clip out the window tab. This allows us to draw outside of the size of the EditorWindow.
        GUI.EndGroup();

        //Do the Zooming
        Rect clippedArea = screenCoordsArea.ScaleSizeBy(1.0f / zoomScale, screenCoordsArea.TopLeft());
        clippedArea.y += kEditorWindowTabHeight;
        GUI.BeginGroup(clippedArea);

        _prevGuiMatrix = GUI.matrix;
        Matrix4x4 translation = Matrix4x4.TRS(clippedArea.TopLeft(), Quaternion.identity, Vector3.one);
        Matrix4x4 scale = Matrix4x4.Scale(new Vector3(zoomScale, zoomScale, 1.0f));
        GUI.matrix = translation * scale * translation.inverse * GUI.matrix;

        //Done
        return clippedArea;
    }

    /// <summary>
    /// Ends the zoomable Area.
    /// </summary>
    public static void End()
    {
        //Reset Matrix, start the default GUI-Group again
        GUI.matrix = _prevGuiMatrix;
        GUI.EndGroup();
        GUI.BeginGroup(new Rect(0.0f, kEditorWindowTabHeight, Screen.width, Screen.height));
    }
}

RectExtensions.cs

using UnityEngine;

/// <summary>
/// Extends the default Rect-Implementation by adding additional Methods.
/// 
/// Bases on:
/// http://martinecker.com/martincodes/unity-editor-window-zooming/
/// </summary>
public static class RectExtensions
{
    public static Vector2 TopLeft(this Rect rect)
    {
        return new Vector2(rect.xMin, rect.yMin);
    }

    public static Vector2 Center(this Rect rect)
    {
        return new Vector2((rect.xMin + rect.xMax) / 2.0f, (rect.yMin + rect.yMax) / 2.0f);
    }

    public static Rect ScaleSizeBy(this Rect rect, float scale)
    {
        return rect.ScaleSizeBy(scale, rect.center);
    }

    public static Rect ScaleSizeBy(this Rect rect, float scale, Vector2 pivotPoint)
    {
        Rect result = rect;
        result.x -= pivotPoint.x;
        result.y -= pivotPoint.y;
        result.xMin *= scale;
        result.xMax *= scale;
        result.yMin *= scale;
        result.yMax *= scale;
        result.x += pivotPoint.x;
        result.y += pivotPoint.y;
        return result;
    }

    public static Rect ScaleSizeBy(this Rect rect, Vector2 scale)
    {
        return rect.ScaleSizeBy(scale, rect.center);
    }

    public static Rect ScaleSizeBy(this Rect rect, Vector2 scale, Vector2 pivotPoint)
    {
        Rect result = rect;
        result.x -= pivotPoint.x;
        result.y -= pivotPoint.y;
        result.xMin *= scale.x;
        result.xMax *= scale.x;
        result.yMin *= scale.y;
        result.yMax *= scale.y;
        result.x += pivotPoint.x;
        result.y += pivotPoint.y;
        return result;
    }
}

Mit Hilfe dieser Snippets ist das Hinzufügen der Scrollbars nun kein Problem mehr. Wir müssen lediglich die Größe der Editor-Fläche definieren (5000 bieter uns ausreichen Raum) und den aktuellen Scrollbar-Zustand in einer Variable speichern.

Diese Variable der EditorExample.cs als Member-Variable hinzufügen.

/// <summary>
/// The Scroll-Position for the Scrollbar withing the ResearchNode Area.
/// </summary>
private Vector2 scrollPosition = Vector2.zero;

Anschließend den Code der OnGUI-Methode um folgenden Code erweitern.

/// <summary>
/// This Method is called whenever the UI is updated.
/// </summary>
void OnGUI()
{
    EditorGUILayout.BeginHorizontal();
    {
        [...]
    }
    EditorGUILayout.EndHorizontal();

    //Scrollbars
    scrollPosition = GUI.BeginScrollView(new Rect(0, 35, position.width, (position.height - 35) / 1),
                                         scrollPosition,
                                         new Rect(0, 0, 5000, 5000));

    //Draw Lines between Nodes
    for (int i = 0; i < researchNodes.Count; i++)
    {
        [...]
    }

    //Define all your Windows between BeginWindows() and EndWindows(). The { } Brackets are not needed, but make the Code easiert to read
    BeginWindows();
    {
        [...]
    }
    EndWindows();

    GUI.EndScrollView();
}

Grid-Linien einzeichnen

Zuletzt wollen wir nun noch ein Raster visualisieren. Da sich die Forschungsknoten-Fenster bereits an einem Raster ausrichten, macht es sinn, ein gleichermaßen dimensioniertes Raster im Hintergrund anzuzeigen. Auch hierfür benötigen wir wieder einen kleinen Helfer in Form eines Snippets.

GraphBackground.cs

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

/// <summary>
/// Implementation from UnityEditor.Graphs.GraphGUI. Used to draw a Grid in the Background of EditorExtensions.
/// 
/// Bases on:
/// https://forum.unity.com/threads/how-do-i-access-the-background-image-used-for-the-animator.501876/
/// </summary>
public static class GraphBackground
{
	private static readonly Color kGridMinorColorDark = new Color(0f, 0f, 0f, 0.18f);
	private static readonly Color kGridMajorColorDark = new Color(0f, 0f, 0f, 0.28f);
	private static readonly Color kGridMinorColorLight = new Color(0f, 0f, 0f, 0.1f);
	private static readonly Color kGridMajorColorLight = new Color(0f, 0f, 0f, 0.15f);

	private static Color gridMinorColor
	{
		get
		{
			if (EditorGUIUtility.isProSkin)
				return kGridMinorColorDark;
			else
				return kGridMinorColorLight;
		}
	}

	private static Color gridMajorColor
	{
		get
		{
			if (EditorGUIUtility.isProSkin)
				return kGridMajorColorDark;
			else
				return kGridMajorColorLight;
		}
	}

	public static void DrawGraphBackground(Rect position, Rect graphExtents)
	{
		if (Event.current.type == EventType.Repaint)
		{
			UnityEditor.Graphs.Styles.graphBackground.Draw(position, false, false, false, false);
		}
		DrawGrid(graphExtents);
	}

	private static void DrawGrid(Rect graphExtents)
	{
		if (Event.current.type == EventType.Repaint)
		{
			HandleUtility.ApplyWireMaterial();
			GL.PushMatrix();
			GL.Begin(1);
			DrawGridLines(graphExtents, 12.5f, gridMinorColor);
			DrawGridLines(graphExtents, 125.0f, gridMajorColor);
			GL.End();
			GL.PopMatrix();
		}
	}

	private static void DrawGridLines(Rect graphExtents, float gridSize, Color gridColor)
	{
		GL.Color(gridColor);
		for (float x = graphExtents.xMin - graphExtents.xMin % gridSize; x < graphExtents.xMax; x += gridSize)
		{
			DrawLine(new Vector2(x, graphExtents.yMin), new Vector2(x, graphExtents.yMax));
		}
		GL.Color(gridColor);
		for (float y = graphExtents.yMin - graphExtents.yMin % gridSize; y < graphExtents.yMax; y += gridSize)
		{
			DrawLine(new Vector2(graphExtents.xMin, y), new Vector2(graphExtents.xMax, y));
		}
	}

	private static void DrawLine(Vector2 p1, Vector2 p2)
	{
		GL.Vertex(p1);
		GL.Vertex(p2);
	}

	// Implementation from UnityEditor.HandleUtility
	static class HandleUtility
	{
		static Material s_HandleWireMaterial;
		static Material s_HandleWireMaterial2D;

		internal static void ApplyWireMaterial(CompareFunction zTest = CompareFunction.Always)
		{
			Material handleWireMaterial = HandleUtility.handleWireMaterial;
			handleWireMaterial.SetInt("_HandleZTest", (int)zTest);
			handleWireMaterial.SetPass(0);
		}

		private static Material handleWireMaterial
		{
			get
			{
				InitHandleMaterials();
				return (!Camera.current) ? s_HandleWireMaterial2D : s_HandleWireMaterial;
			}
		}

		private static void InitHandleMaterials()
		{
			if (!s_HandleWireMaterial)
			{
				s_HandleWireMaterial = (Material)EditorGUIUtility.LoadRequired("SceneView/HandleLines.mat");
				s_HandleWireMaterial2D = (Material)EditorGUIUtility.LoadRequired("SceneView/2DHandleLines.mat");
			}
		}
	}
}

Jetzt können wir unsere OnGUI-Methode um zwei Codezeilen erweitern und unser Grid passt sich nahtlos in das Editor-Fenster ein.

/// <summary>
/// This Method is called whenever the UI is updated.
/// </summary>
void OnGUI()
{
    EditorGUILayout.BeginHorizontal();
    {
        [...]
    }
    EditorGUILayout.EndHorizontal();

    //Scrollbars
    scrollPosition = GUI.BeginScrollView(new Rect(0, 35 / 1, position.width / 1, (position.height - 35) / 1),
                                         scrollPosition,
                                         new Rect(0, 0, 5000, 5000));

    //Background Graph
    Rect graphPosition = new Rect(0, 0, 5000, 5000);
    GraphBackground.DrawGraphBackground(graphPosition, graphPosition);

    //Draw Lines between Nodes
    for (int i = 0; i < researchNodes.Count; i++)
    {
        [...]
    }

    //Define all your Windows between BeginWindows() and EndWindows(). The { } Brackets are not needed, but make the Code easiert to read
    BeginWindows();
    {
        [...]
    }
    EndWindows();

    GUI.EndScrollView();
}

Mit der Implementierung des Rasters und der Scrollbars ist der Editor jetzt nicht nur ansehnlich, sondern auch sehr praktikabel geworden.

Dies schließt den Nachtrag zum Forschungsbaum-Editor Tutorial ab. Wenn du noch Fragen oder Anregungen hast oder ein Thema gerne weiter erläutert sehen möchtest, hinterlasse doch einfach gerne einen Kommentar!

Tagged , , .Speichere in deinen Favoriten diesen permalink.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.