【Unity】GUIで簡単なグラフを表示する


エディタ拡張で簡単なグラフを描画する方法に関するメモ．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1596227/0068f241-0c83-cb7f-6b20-5e6579d366da.png)

## 背景の描画

Handles.DrawSolidRectangleWithOutline()で背景と枠線をまとめて表示できる．(※GUI.GroupScope を使用する場合は，そちらで GUIStyle を渡して背景描画することも)

https://docs.unity3d.com/ja/2023.2/ScriptReference/Handles.DrawSolidRectangleWithOutline.html

<details> <summary>デモコード</summary>

```DemoEditorWindow.cs
// デモ表示用のエディタウインドウ
public class DemoEditorWindow : EditorWindow {

        private void OnGUI() {
            EditorGUILayout.Space();
            GUILayout.Label("-------------");
            { // グラフ描画
                var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
                Handles.DrawSolidRectangleWithOutline(rect, Colors.backdropColor, Colors.gridLineColor);
            }
            GUILayout.Label("-------------");
        }

        /// <summary>
        /// 背景と枠線を描画する
        /// </summary>
        public void DrawBackDrop(Rect rect, Color backdropColor, Color outlineColor) {
            Handles.DrawSolidRectangleWithOutline(rect, backdropColor, outlineColor);
        }

        private static class Colors {
            public static readonly Color backdropColor = new Color(0.8f, 0.8f, 0.8f, 0.1f);
            public static readonly Color gridLineColor = new Color(0.9f, 0.9f, 0.9f, 0.5f);
        }
    }
```

</details>

#### 【結果】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1596227/974894c8-ebbf-fc86-0576-6a00fe5baf66.png)

## グリッド線の描画

Handles.Line()で GUI に線を描く場合、EditorWindow 左上を原点とした座標を渡す必要がある．そのためグラフ範囲の Rect 内に描画するように変換を行っている．

https://docs.unity3d.com/ja/2023.2/ScriptReference/Handles.DrawLine.html

<details> <summary>デモコード</summary>

```DemoEditorWindow.cs
        private void OnGUI() {
			EditorGUILayout.Space();
			GUILayout.Label("-------------");
			{ // グラフ描画
				var settings = new AxisSettings(xRange: (0, 10), yRange: (0, 10), xStep: 1, yStep: 2);

				var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
				DrawBackDrop(rect, Colors.backdropColor, Colors.gridLineColor);
				DrawGrid(rect, settings, Colors.gridLineColor);
			}
			GUILayout.Label("-------------");
		}

        public void DrawGrid(Rect rect, AxisSettings settings, Color gridColor) {
			using (new Handles.DrawingScope(gridColor)) {
				var graphOrigin = rect.position;

				// X軸
				var xStart = Mathf.Ceil(settings.xRange.min / settings.xStep) * settings.xStep;
				var xTicks = EnumerableUtil.LinspaceWithStep(xStart, settings.xRange.max, settings.xStep);
				foreach (var x in xTicks) {
					var value1 = new Vector2(x, settings.yRange.min);
					var value2 = new Vector2(x, settings.yRange.max);

					// GUI座標に変換して描画
					var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
					var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
					Handles.DrawLine(p1, p2);
				}

				// Y軸
				var yStart = Mathf.Ceil(settings.yRange.min / settings.yStep) * settings.yStep;
				var yTicks = EnumerableUtil.LinspaceWithStep(yStart, settings.yRange.max, settings.yStep);
				foreach (var y in yTicks) {
					var value1 = new Vector2(settings.xRange.min, y);
					var value2 = new Vector2(settings.xRange.max, y);

					// GUI座標に変換して描画
					var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
					var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
					Handles.DrawLine(p1, p2);
				}
			}
		}

        public struct AxisSettings {
			// グラフの値域
			public (float min, float max) xRange;
			public (float min, float max) yRange;

			// グリッド幅
			public float xStep;
			public float yStep;

			public AxisSettings((float min, float max) xRange, (float min, float max) yRange, float xStep, float yStep) {
				this.xRange = xRange;
				this.yRange = yRange;
				this.xStep = xStep;
				this.yStep = yStep;
			}
		}

		private static class Convertor {

			// グラフの値域
			public static (float min, float max) xRange = (0, 10);
			public static (float min, float max) yRange = (0, 10);

			/// <summary>
			///
			/// </summary>
			public static Vector2 ValueToGraphPoint(Vector2 value, Vector2 size, AxisSettings settings) {
				var xRange = settings.xRange;
				var yRange = settings.yRange;
				float x = Mathf.Lerp(0, size.x, (value.x - xRange.min) / (xRange.max - xRange.min));
				float y = Mathf.Lerp(size.y, 0, (value.y - yRange.min) / (yRange.max - yRange.min));
				return new Vector2(x, y);
			}

		}
```

</details>

<details> <summary>util.cs</summary>

```Util.cs
 public static class EnumerableUtil {

        /// <summary>
        /// ステップサイズで線形に配置されたデータを作成する
        /// </summary>
        public static IEnumerable<float> LinspaceWithStep(float start, float end, float step) {
            if (step <= 0) throw new InvalidOperationException("Step must be positive and non-zero.");

            int count = (int)((end - start) / step) + 1;
            return Enumerable
                .Range(0, count)
                .Select(i => start + i * step)
                .TakeWhile(value => value <= end);
        }
}
```

</details>

#### 【結果】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1596227/8c12acf0-7e4d-7746-351e-75366d63b84d.png)

## データの描画

GUI.ClipScope、GUI.GroupScope のスコープ内では指定した Rect の position を基準とした相対座標で GUI メソッドを実行できる．また描画される図形も指定 Rect 内に制限される（Rect からはみ出た部分は描画されない）．

https://docs.unity3d.com/ja/2023.2/ScriptReference/GUI.GroupScope.html

<details> <summary>デモコード</summary>

```DemoEditorWindow.cs
        private void OnGUI() {
			EditorGUILayout.Space();
			GUILayout.Label("-------------");
			{ // グラフ描画
				var settings = new AxisSettings(xRange: (0, 10), yRange: (0, 10), xStep: 1, yStep: 2);
				var dummyData = EnumerableUtil.LinspaceWithStep(-2, 12, 0.5f)
					.Select(x => new Vector2(x, Mathf.Sin(x) + x))
					.ToArray();

				var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
				DrawBackDrop(rect, Colors.backdropColor, Colors.gridLineColor);
				DrawGrid(rect, settings, Colors.gridLineColor);
				DrawData(rect, settings, dummyData, Colors.dataColor);
			}
			GUILayout.Label("-------------");
		}

		/// <summary>
		/// 点列を描画する
		/// </summary>
		public void DrawData(Rect rect, AxisSettings settings, Vector2[] values, Color color) {
			using (new GUI.ClipScope(rect)) // ※スコープ内のGUIメソッドは指定Rectを基準として処理される
			using (new Handles.DrawingScope(color)) {
				for (int i = 0; i < values.Length - 1; i++) {
					Vector2 p1 = Convertor.ValueToGraphPoint(values[i], rect.size, settings);
					Vector2 p2 = Convertor.ValueToGraphPoint(values[i + 1], rect.size, settings);
					Handles.DrawLine(p1, p2);
				}

				foreach (var value in values) {
					Vector2 p = Convertor.ValueToGraphPoint(value, rect.size, settings);
					Handles.DrawSolidDisc(p, Vector3.forward, 2f);
				}
			}
		}
```

</details>

#### 【結果】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1596227/e3a2d764-52d4-d01a-40bc-7e4c47e9ddd8.png)

#### 参考

https://tips.hecomi.com/entry/2014/06/23/222805
https://qiita.com/hinagawa/items/4388f2f61cb33b9975e8

<details> <summary>デモコード全文</summary>

```.cs
#if UNITY_EDITOR
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

namespace nitou.EditorScripts {

	// デモ表示用のエディタウインドウ
	public class DemoEditorWindow : EditorWindow {

		[MenuItem(MenuItemName.Prefix.Develop + "Test/Demo Graph")]

		public static void Open() => GetWindow<DemoEditorWindow>("Demo Graph");

		private void OnGUI() {
			EditorGUILayout.Space();
			GUILayout.Label("-------------");
			{ // グラフ描画
				var settings = new AxisSettings(xRange: (0, 10), yRange: (0, 10), xStep: 1, yStep: 2);
				var dummyData = EnumerableUtil.LinspaceWithStep(-2, 12, 0.5f)
					.Select(x => new Vector2(x, Mathf.Sin(x) + x))
					.ToArray();

				var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
				DrawBackDrop(rect, Colors.backdropColor, Colors.gridLineColor);
				DrawGrid(rect, settings, Colors.gridLineColor);
				DrawData(rect, settings, dummyData, Colors.dataColor);
			}
			GUILayout.Label("-------------");
		}

		/// <summary>
		/// 点列を描画する
		/// </summary>
		public void DrawData(Rect rect, AxisSettings settings, Vector2[] values, Color color) {
			using (new GUI.ClipScope(rect)) // ※スコープ内のGUIメソッドは指定Rectを基準として処理される
			using (new Handles.DrawingScope(color)) {
				for (int i = 0; i < values.Length - 1; i++) {
					Vector2 p1 = Convertor.ValueToGraphPoint(values[i], rect.size, settings);
					Vector2 p2 = Convertor.ValueToGraphPoint(values[i + 1], rect.size, settings);
					Handles.DrawLine(p1, p2);
				}

				foreach (var value in values) {
					Vector2 p = Convertor.ValueToGraphPoint(value, rect.size, settings);
					Handles.DrawSolidDisc(p, Vector3.forward, 2f);
				}
			}
		}


		public void DrawGrid(Rect rect, AxisSettings settings, Color gridColor) {
			using (new Handles.DrawingScope(gridColor)) {
				var graphOrigin = rect.position;

				// X軸
				var xStart = Mathf.Ceil(settings.xRange.min / settings.xStep) * settings.xStep;
				var xTicks = EnumerableUtil.LinspaceWithStep(xStart, settings.xRange.max, settings.xStep);
				foreach (var x in xTicks) {
					var value1 = new Vector2(x, settings.yRange.min);
					var value2 = new Vector2(x, settings.yRange.max);

					// GUI座標に変換して描画
					var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
					var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
					Handles.DrawLine(p1, p2);
				}

				// Y軸
				var yStart = Mathf.Ceil(settings.yRange.min / settings.yStep) * settings.yStep;
				var yTicks = EnumerableUtil.LinspaceWithStep(yStart, settings.yRange.max, settings.yStep);
				foreach (var y in yTicks) {
					var value1 = new Vector2(settings.xRange.min, y);
					var value2 = new Vector2(settings.xRange.max, y);

					// GUI座標に変換して描画
					var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
					var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
					Handles.DrawLine(p1, p2);
				}
			}
		}


		/// <summary>
		/// 背景と枠線を描画する
		/// </summary>
		public void DrawBackDrop(Rect rect, Color backdropColor, Color outlineColor) {
			Handles.DrawSolidRectangleWithOutline(rect, backdropColor, outlineColor);
		}

		private static class Colors {
			public static readonly Color backdropColor = new Color(0.8f, 0.8f, 0.8f, 0.1f);
			public static readonly Color gridLineColor = new Color(0.9f, 0.9f, 0.9f, 0.5f);
			public static readonly Color dataColor = Color.green;
		}


		public struct AxisSettings {
			// グラフの値域
			public (float min, float max) xRange;
			public (float min, float max) yRange;

			// グリッド幅
			public float xStep;
			public float yStep;

			public AxisSettings((float min, float max) xRange, (float min, float max) yRange, float xStep, float yStep) {
				this.xRange = xRange;
				this.yRange = yRange;
				this.xStep = xStep;
				this.yStep = yStep;
			}
		}

		private static class Convertor {
			/// <summary>
			/// グラフ範囲の座標に変換する
			/// </summary>
			public static Vector2 ValueToGraphPoint(Vector2 value, Vector2 size, AxisSettings settings) {
				var xRange = settings.xRange;
				var yRange = settings.yRange;
				float x = Mathf.Lerp(0, size.x, (value.x - xRange.min) / (xRange.max - xRange.min));
				float y = Mathf.Lerp(size.y, 0, (value.y - yRange.min) / (yRange.max - yRange.min));
				return new Vector2(x, y);
			}

		}
	}
}
#endif
```

</details>