Search Unity

Showcase Customizable Scene View Panel for the editor

Discussion in 'Scripting' started by orionsyndrome, May 16, 2023.

  1. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,106
    This is for when you're quickly trying out something live in the editor, whether it's some geometric math, or whatever, and all you wish for are maybe some buttons and readouts without having to write a fully fledged custom editor.

    tl;dr full code + demo usage at the end of the post

    So let's build a simple customizable readout panel that runs live in the scene view. Now you can see your mouse coordinates constantly updated, for example, instead of having to spam the console.



    This is going to be a pure C# class (SimpleSceneViewPanel) that you instantiate inside your MonoBehaviour script (UserScript) with an intent to run it live in the editor.

    Both of these should be in the Editor folder. To make this tutorial easier to read, I've deliberately left out all
    #if UNITY_EDITOR
    safeguards, however they are pretty much mandatory if you want to use this for MonoBehaviours that are supposed to be included in the build. I've included the safeguards in the final SimpleSceneViewPanel code at the end of the post, so it doesn't have to be in the Editor folder. But depending on what you do with the MB script, you should pay attention to this.

    I'm using such tools exclusively for the in-editor prototypes, that's the whole point of having a MonoBehaviour interact directly with the SceneView.

    Inside UserScript, we can prepare the basics and throw in some mouse tracking to have something to test later.
    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using EditorToolkit;
    4.  
    5. [ExecuteInEditMode]
    6. public class UserClass : MonoBehaviour {
    7.  
    8.   SimpleSceneViewPanel _panel;
    9.  
    10.   // when we enable the script, we want the panel to appear
    11.   void OnEnable() {
    12.     _panel = new SimpleSceneViewPanel("Window Test");
    13.     SceneView.duringSceneGui += onSceneGui;
    14.   }
    15.  
    16.   // when we disable the script, the panel should disappear
    17.   void OnDisable() {
    18.     if(_panel is not null) {
    19.       _panel.Enabled = false;
    20.       _panel = null;
    21.     }
    22.     SceneView.duringSceneGui -= onSceneGui;
    23.   }
    24.  
    25.   Vector2 _mp; // mouse position in scene view
    26.   Vector3 _mpw; // mouse position as a world coordinate on the XY plane
    27.  
    28.   void onSceneGui(SceneView view) {
    29.     var e = Event.current;
    30.     if(e.isMouse) {
    31.       _mp = e.mousePosition;
    32.       _mpw = toWorldPoint(_mp);
    33.     }
    34.   }
    35.  
    36.   Vector3 toWorldPoint(Vector2 mp) {
    37.     var ray = HandleUtility.GUIPointToWorldRay(pos);
    38.     var plane = new Plane(Vector3.back, Vector3.zero);
    39.     plane.Raycast(ray, out var result);
    40.     return ray.origin + result * ray.direction;
    41.   }
    42.  
    43. }
    So that's some simple boilerplate, let's now see what we need for the panel class. It's supposed to be very simple, but we still want the ability to show or hide this panel, and also to prevent it from running altogether. We also want some window caption just to get started.
    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. namespace EditorToolkit {
    5.  
    6.   public class SimpleSceneViewPanel {
    7.  
    8.     public string Caption { get; private set; }
    9.     public bool Visible { get; set; }
    10.     public bool Enabled { get; set; }
    11.  
    12.     public SimpleSceneViewPanel(string caption) ...
    13.     public void Refresh() ...
    14.  
    15.   }
    16.  
    17. }
    Let's fill in the details. For example Enabled property should hook this up to duringSceneGui so we can draw the window. So instead of it being auto-property, it should work like this
    Code (csharp):
    1. bool _enabled;
    2. public bool Enabled {
    3.   get => _enabled;
    4.   set {
    5.     _enabled = value;
    6.     if(_enabled) SceneView.duringSceneGui += onSceneGui;
    7.       else SceneView.duringSceneGui -= onSceneGui;
    8.   }
    9. }
    And now we can do
    Code (csharp):
    1. int _windowId;
    2.  
    3. void onSceneGui(SceneView view) {
    4.   if(Visible) drawWindow();
    5. }
    6.  
    7. void drawWindow()
    8.   => GUI.Window(_windowId, new Rect(100f, 50f, 150f, 150f), drawWindowContent, Caption);
    9.  
    10. void drawWindowContent(int id) {
    11.   if(id != _windowId) return; // wrong handle
    12. }
    We're lacking the actual id and also Enabled needs to be set to true, so let's address this in the constructor.
    Code (csharp):
    1. public SimpleSceneViewPanel(string caption) {
    2.   Caption = caption;
    3.   _windowId = GUIUtility.GetControlID(FocusType.Passive);
    4.   Enabled = true;
    5.   Visible = true;
    6. }
    This should now work. That's the basic set up. (We'll leave the Refresh method for later.)

    Let's think about the basic featureset. It would be nice if:
    1) we could fetch text data as individual rows from UserScript
    2) this panel was sized to encompass the information we want to present
    3) this panel was tucked in the corner of the scene view

    To do this let's introduce an intermediate object, which can describe one full row of info. Initially, we'll support only labels, so there's not much going on here, but this will be expanded.
    Code (csharp):
    1. public class PanelRow {
    2.  
    3.   public string label;
    4.  
    5.   // and a simple factory
    6.   static public PanelRow Label(string label)
    7.     => new PanelRow() { label = label };
    8.  
    9. }
    Next we need three new bits of information stored somewhere: panel width/height, number of rows, as well as a callback reference. What's a callback? Well we want UserScript to locally handle the information that is supposed to be displayed, and we need a way for this object to fetch that data without explicitly knowing that UserScript exists.

    Instead, UserScript passes in a locally accepted callback reference through constructor, and then SimpleSceneViewPanel tries to work with this, one row at a time. This is why we also want to provide the number of rows.
    Code (csharp):
    1. public int Rows { get; private set; }
    Let's now introduce a delegate and modify the constructor
    Code (csharp):
    1. public delegate PanelRow CallbackDelegate(int row);
    2. CallbackDelegate _callback;
    3.  
    4. public SimpleSceneViewPanel(string caption, float width, int rows, CallbackDelegate callback) {
    5.   Caption = caption;
    6.   Rows = rows;
    7.   _callback = callback;
    8.  
    9.   _windowId = ...
    10.  
    11.   Enabled = true;
    12.   Visible = true;
    13. }

    Cue to UserScript.cs >>>
    It makes sense to prepare this callback method at this moment.
    First we need to make sure we call the constructor properly
    Code (csharp):
    1. void OnEnable() {
    2.   _panel = new SimpleSceneViewPanel("My Panel", width: 100f, rows: 3, callback: panelInfo);
    3.   SceneView.duringSceneGui += onSceneGui;
    4. }
    Then we make this
    panelInfo
    method (to shorten this code we can add
    using static EditorToolkit.SimpleSceneViewPanel;
    at the start)
    Code (csharp):
    1. PanelRow panelInfo(int row)
    2.   => row switch {
    3.        0 => PanelRow.Label("label #1"),
    4.        1 => PanelRow.Label("label #2"),
    5.        2 => PanelRow.Label("label #3"),
    6.        _ => null
    7.      };
    We have now fully defined three rows to be displayed in the panel.

    Cue back to SimpleSceneViewPanel.cs >>>
    Remember that we had
    float width
    in the constructor, but didn't handle it? Let's now add a new class member
    Code (csharp):
    1. Vector2 _panelSize; // width and height
    And we can add this right below
    _windowId = ...
    inside the constructor.
    Code (csharp):
    1. determinePanelSize(width);
    For this we now need some class constants. These are purely cosmetic and empirical, nothing special about them.
    Code (csharp):
    1. private const float VPAD = 6f, HPAD = 8f, SPAD = 12f, ROW_H = 16f;
    The idea behind determinePanelSize is to compute how tall it should be, by iterating through the rows and accumulating row height. Of course, it's better and easier to simply multiply two numbers, but we want to expand on this later, so let's make it "complicated" instead.
    Code (csharp):
    1. void determinePanelSize(float width) {
    2.   var h = ROW_H + VPAD / 2f - 1f; // takes the window caption into account
    3.  
    4.   if(_callback is not null) { // skips all of this if there was no callback method
    5.     for(int i = 0; i < Rows; i++) { // rolls through the row elements
    6.       var feedback = _callback(i); // UserScript may choose to return null
    7.       if(feedback is null) continue; // in that case, the row is skipped
    8.  
    9.       h += ROW_H; // adds one row height to total height
    10.     }
    11.   }
    12.  
    13.   _panelSize = new Vector2(width, 2f * VPAD + h); // some final padding
    14. }
    All that's left is to actually draw this. Let's modify drawWindow
    Code (csharp):
    1. void drawWindow()
    2.   => GUI.Window(_windowId, getWindowRect(), drawWindowContent, Caption);
    3.  
    4. Rect getWindowRect() => new Rect(50f, 50f, _panelSize.x, _panelSize.y);
    And finally we can render the contents
    Code (csharp):
    1. void drawWindowContent(int id) {
    2.   if(id != _windowId || _callback is null) return;
    3.  
    4.   var rect = new Rect(HPAD, VPAD + ROW_H, _panelSize.x - 2f * HPAD, ROW_H);
    5.  
    6.   for(int i = 0; i < Rows; i++) {
    7.     var feedback = _callback(i);
    8.     if(feedback is null) continue;
    9.  
    10.     GUI.Label(rect, feedback.label);
    11.     rect.y += ROW_H;
    12.   }
    13. }
    This should be working now.

    The only remaining feature is to make this panel align with the corner of the view. It is currently hardcoded to sit at (50, 50), so we need a way to actually compute these coordinates. Remember that the scene view might change, and so its dimensions may vary. To scan for width and height (in pixels), we need to interrogate the scene camera.

    The panel coordinates are showing its local top left corner, but because I want this panel in the bottom left corner of the scene view, we should find the bottom left coordinates, then move the window upward by its full height. Additionally we want to nudge the window slightly so that there is a visible gap from the edge.

    In
    onSceneGui
    we can use
    view
    argument to pass current camera to
    drawWindow

    Code (csharp):
    1. void onSceneGui(SceneView view) {
    2.   if(Visible) drawWindow(view.camera);
    3. }
    4.  
    5. void drawWindow(Camera sceneCam)
    6.   => GUI.Window(_windowId, getWindowRect(sceneCam.pixelRect.size), drawWindowContent, Caption);
    7.  
    8. Rect getWindowRect(Vector2 screenSize)
    9.   => new Rect(SPAD, screenSize.y - SPAD - _panelSize.y, _panelSize.x, _panelSize.y);
    If we now save this camera, we can also implement Refresh
    Code (csharp):
    1. Camera _lastCamera;
    2.  
    3. void onSceneGui(SceneView view) {
    4.   if(Visible) drawWindow(_lastCamera = view.camera);
    5. }
    6.  
    7. public void Refresh() {
    8.   if(_lastCamera != null) drawWindow(_lastCamera);
    9. }
    This is what the final code looks like
    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using EditorToolkit;
    4. using static EditorToolkit.SimpleSceneViewPanel;
    5.  
    6. [ExecuteInEditMode]
    7. public class UserScript : MonoBehaviour {
    8.  
    9.   SimpleSceneViewPanel _panel;
    10.  
    11.   void OnEnable() {
    12.     _panel = new SimpleSceneViewPanel("My Panel", 100f, rows: 3, panelInfo);
    13.     SceneView.duringSceneGui += onSceneGui;
    14.   }
    15.  
    16.   void OnDisable() {
    17.     if(_panel is not null) {
    18.       _panel.Enabled = false;
    19.       _panel = null;
    20.     }
    21.     SceneView.duringSceneGui -= onSceneGui;
    22.   }
    23.  
    24.   Vector2 _mp;
    25.   Vector3 _mpw;
    26.  
    27.   void onSceneGui(SceneView view) {
    28.     var e = Event.current;
    29.  
    30.     if(e.isMouse) {
    31.       _mp = e.mousePosition;
    32.       _mpw = toWorldPoint(_mp);
    33.     }
    34.   }
    35.  
    36.   Vector3 toWorldPoint(Vector2 pos) {
    37.     var ray = HandleUtility.GUIPointToWorldRay(pos);
    38.     var plane = new Plane(Vector3.back, Vector3.zero);
    39.     plane.Raycast(ray, out var result);
    40.     return ray.origin + result * ray.direction;
    41.   }
    42.  
    43.   PanelRow panelInfo(int row)
    44.     => row switch {
    45.          0 => PanelRow.Label("label #1"),
    46.          1 => PanelRow.Label("label #2"),
    47.          2 => PanelRow.Label("label #3"),
    48.          _ => null
    49.        };
    50.  
    51. }
    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEditor;
    4.  
    5. namespace EditorToolkit {
    6.  
    7.   public class SimpleSceneViewPanel {
    8.  
    9.     private const float VPAD = 6f, HPAD = 8f, SPAD = 12f, ROW_H = 16f;
    10.  
    11.     public string Caption { get; private set; }
    12.     public int Rows { get; private set; }
    13.     public bool Visible { get; set; }
    14.  
    15.     bool _enabled;
    16.     public bool Enabled {
    17.       get => _enabled;
    18.       set {
    19.         _enabled = value;
    20.         if(_enabled) SceneView.duringSceneGui += onSceneGui;
    21.           else SceneView.duringSceneGui -= onSceneGui;
    22.       }
    23.     }
    24.  
    25.     public delegate PanelRow CallbackDelegate(int row);
    26.     CallbackDelegate _callback;
    27.  
    28.     Vector2 _panelSize;
    29.  
    30.     Camera _lastCamera;
    31.     int _windowId = -1;
    32.  
    33.     public SimpleSceneViewPanel(string caption, float width, int rows, CallbackDelegate callback) {
    34.       Caption = caption;
    35.       Rows = rows;
    36.       _callback = callback;
    37.  
    38.       _windowId = GUIUtility.GetControlID(FocusType.Passive);
    39.       determinePanelSize(width);
    40.  
    41.       Enabled = true;
    42.       Visible = true;
    43.     }
    44.  
    45.     void onSceneGui(SceneView view) {
    46.       if(Visible) drawWindow(_lastCamera = view.camera);
    47.     }
    48.  
    49.     public void Refresh() {
    50.       if(_lastCamera != null) drawWindow(_lastCamera);
    51.     }
    52.  
    53.     void drawWindow(Camera sceneCam)
    54.       => GUI.Window(_windowId, getWindowRect(sceneCam.pixelRect.size), drawWindowContent, Caption);
    55.  
    56.     Rect getWindowRect(Vector2 screenSize)
    57.       => new Rect(SPAD, screenSize.y - SPAD - _panelSize.y, _panelSize.x, _panelSize.y);
    58.  
    59.     void drawWindowContent(int id) {
    60.       if(id != _windowId || _callback is null) return;
    61.  
    62.       var rect = new Rect(HPAD, VPAD + ROW_H, _panelSize.x - 2f * HPAD, ROW_H);
    63.  
    64.       for(int i = 0; i < Rows; i++) {
    65.         var feedback = _callback(i);
    66.         if(feedback is null) continue;
    67.  
    68.         GUI.Label(rect, feedback.label);
    69.         rect.y += ROW_H;
    70.       }
    71.     }
    72.  
    73.     void determinePanelSize(float width) {
    74.       var h = ROW_H + VPAD / 2f - 1f;
    75.  
    76.       if(_callback is not null) {
    77.         for(int i = 0; i < Rows; i++) {
    78.           var feedback = _callback(i);
    79.           if(feedback is null) continue;
    80.  
    81.           h += ROW_H;
    82.         }
    83.       }
    84.  
    85.       _panelSize = new Vector2(width, 2f * VPAD + h);
    86.     }
    87.  
    88.     public class PanelRow {
    89.  
    90.       public string label;
    91.  
    92.       static public PanelRow Label(string label)
    93.         => new PanelRow() { label = label };
    94.  
    95.     }
    96.  
    97.   }
    98.  
    99. }

    Now let's look at the extended featureset. It would be super nice if:
    1) we could specify the exact corner of the scene view,
    2) we could add support for buttons and checkboxes, amirite?

    So let's do the corners first
    Code (csharp):
    1. public ScreenCorner Corner { get; set; }
    2.  
    3. public enum ScreenCorner {
    4.   TopLeft,
    5.   TopRight,
    6.   BottomLeft,
    7.   BottomRight
    8. }
    And in constructor
    Code (csharp):
    1. public SimpleSceneViewPanel(string caption, float width, int rows, CallbackDelegate callback, ScreenCorner corner = ScreenCorner.BottomLeft) {
    2.   ...
    3.   Corner = corner;
    4.   ...
    5. }
    We could write down all the formulas like this
    Code (csharp):
    1. Rect getWindowRect(ScreenCorner corner, Vector2 screenSize)
    2.    => corner switch {
    3.         ScreenCorner.TopLeft => new Rect(SPAD, SPAD, _panelSize.x, _panelSize.y),
    4.         ScreenCorner.TopRight => new Rect(screenSize.x - SPAD - _panelSize.x, SPAD, _panelSize.x, _panelSize.y),
    5.         ScreenCorner.BottomRight => new Rect(screenSize.x - SPAD - _panelSize.x, screenSize.y - SPAD - _panelSize.y, _panelSize.x, _panelSize.y),
    6.         _ => new Rect(SPAD, screenSize.y - SPAD - _panelSize.y, _panelSize.x, _panelSize.y)
    7.       };
    But they all look rather messy, so let's do this differently
    Code (csharp):
    1. Rect getWindowRect(ScreenCorner corner, Vector2 screenSize) {
    2.   return new Rect(getPos(corner, screenSize), _panelSize);
    3.  
    4.   Vector2 getPos(ScreenCorner corner, Vector2 screenSize)
    5.     => corner switch {
    6.          ScreenCorner.TopLeft     => vec(-1, -1),
    7.          ScreenCorner.TopRight    => vec(+1, -1),
    8.          ScreenCorner.BottomRight => vec(+1, +1),
    9.          _                        => vec(-1, +1)
    10.        };
    11.  
    12.   Vector2 vec(int sign1, int sign2) => new Vector2(coord(0, sign1), coord(1, sign2));
    13.   float coord(int axis, int sign) => sign < 0? SPAD : screenSize[axis] - _panelSize[axis] - SPAD;
    14. }
    Here we cleverly set up local functions so that we don't have to repeat the same basic operations over and over again. It's slightly more cryptic at a first glance, but actually it's very readable at a high level as we can clearly see the intent behind it, through the method's name and the main switch expression. And because the same code is reused and tangled to all cases, we can kind of tell that it works for all of them. Also there is a fair bit of symmetry being exploited. What works for the width is reused for the height.

    Obviously this is highly subjective, but I prefer when a method looks like someone has invested some effort into making sure it's reliable. From my experience, these are less likely to cause bugs on their own. Compare this to the first version directly above. Which version would you trust more?

    To finish the corners set up, we need to pass the argument in the call site
    Code (csharp):
    1. void drawWindow(Camera sceneCam)
    2.   => GUI.Window(_windowId, getWindowRect(Corner, sceneCam.pixelRect.size), drawWindowContent, Caption);
    Try it.

    To introduce buttons, we can expand PanelRow we made earlier. Because these components are interactive, there is bit more to them then just label, and we need to add the actual type as well, to differentiate between them.
    Code (csharp):
    1. public enum RowClass {
    2.   Label,
    3.   Button,
    4.   Check
    5. }
    6.  
    7. public class PanelRow {
    8.  
    9.   public RowClass rowClass;
    10.   public string label;
    11.   public bool enabled;
    12.   public bool state; // for checkboxes
    13.  
    14.   // a generic delegate is fine
    15.   // we get back row index and current state
    16.   public Action<int, bool> onClick;
    17.  
    18.   static public PanelRow Label(string label)
    19.     => new PanelRow() {
    20.          rowClass = RowClass.Label,
    21.          label = label,
    22.          enabled = true,
    23.          onClick = null
    24.        };
    25.  
    26.   static public PanelRow Button(string label, Action<int, bool> onClick, bool enabled = true)
    27.     => new PanelRow() {
    28.          rowClass = RowClass.Button,
    29.          label = label,
    30.          enabled = enabled,
    31.          onClick = onClick
    32.        };
    33.  
    34.   static public PanelRow Check(string label, bool value, Action<int, bool> onClick, bool enabled = true)
    35.     => new PanelRow() {
    36.          rowClass = RowClass.Check,
    37.          label = label,
    38.          state = value,
    39.          enabled = enabled,
    40.          onClick = onClick
    41.        };
    42.  
    43.   }
    44.  
    45. }
    determinePanelSize should now accommodate for the differently sized controls, hence
    Code (csharp):
    1. void determinePanelSize(float width) {
    2.   var h = ROW_H + VPAD / 2f - 1f;
    3.  
    4.   if(_callback is not null) {
    5.     for(int i = 0; i < Rows; i++) {
    6.       var feedback = _callback(i);
    7.       if(feedback is null) continue;
    8.  
    9.       h += feedback.rowClass switch {
    10.         RowClass.Button => ROW_H + 6f,
    11.         RowClass.Check => ROW_H + 6f,
    12.         _ => ROW_H
    13.       };
    14.     }
    15.   }
    16.  
    17.   _panelSize = new Vector2(width, 2f * VPAD + h);
    18. }
    and finally, for the actual drawing and control feedback
    Code (csharp):
    1. void drawWindowContent(int id) {
    2.   if(id != _windowId || _callback is null) return;
    3.  
    4.   var rect = new Rect(HPAD, VPAD + ROW_H, _panelSize.x - 2f * HPAD, ROW_H);
    5.  
    6.   for(int i = 0; i < Rows; i++) {
    7.     var feedback = _callback(i);
    8.     if(feedback is null) continue;
    9.  
    10.     var saved = GUI.enabled;
    11.     GUI.enabled = feedback.enabled;
    12.  
    13.     Rect nr;
    14.  
    15.     switch(feedback.rowClass) {
    16.  
    17.       case RowClass.Label:
    18.         GUI.Label(rect, feedback.label);
    19.         rect.y += ROW_H;
    20.         break;
    21.  
    22.       case RowClass.Button:
    23.         rect.y += 4f;
    24.         nr = new Rect(rect);
    25.         nr.height += 1f;
    26.         GUI.enabled &= feedback.onClick is not null;
    27.         if(GUI.Button(nr, feedback.label)) feedback.onClick.Invoke(i, true);
    28.         rect.y += ROW_H + 2f;
    29.         break;
    30.  
    31.       case RowClass.Check:
    32.         rect.y += 4f;
    33.         nr = new Rect(rect);
    34.         nr.height += 1f;
    35.         GUI.enabled &= feedback.onClick is not null;
    36.         var oldState = feedback.state;
    37.         var newState = GUI.Toggle(nr, oldState, feedback.label, EditorStyles.miniButton);
    38.         if(newState != oldState) feedback.onClick.Invoke(i, newState);
    39.         rect.y += ROW_H + 2f;
    40.         break;
    41.  
    42.     }
    43.  
    44.     GUI.enabled = saved;
    45.   }
    46. }
    Here's the complete code with UserScript modified to showcase the features.
    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using EditorToolkit;
    4. using static EditorToolkit.SimpleSceneViewPanel;
    5.  
    6. [ExecuteInEditMode]
    7. public class SceneViewPanelTest : MonoBehaviour {
    8.  
    9.   [SerializeField] bool _showPanel = true;
    10.   [SerializeField] ScreenCorner _corner = ScreenCorner.BottomRight;
    11.  
    12.   SimpleSceneViewPanel _panel;
    13.  
    14.   void OnValidate() {
    15.     if(_panel is not null) {
    16.       _panel.Visible = _showPanel;
    17.       _panel.Corner = _corner;
    18.     }
    19.   }
    20.  
    21.   void OnEnable() {
    22.     _panel = new SimpleSceneViewPanel("My Panel", 160f, rows: 4, panelInfo, _corner);
    23.     SceneView.duringSceneGui += onSceneGui;
    24.   }
    25.  
    26.   void OnDisable() {
    27.     if(_panel is not null) {
    28.       _panel.Enabled = false;
    29.       _panel = null;
    30.     }
    31.     SceneView.duringSceneGui -= onSceneGui;
    32.   }
    33.  
    34.   Vector2 _mp;
    35.   Vector3 _mpw;
    36.   bool _snap;
    37.   int _button;
    38.  
    39.   void onSceneGui(SceneView view) {
    40.     var e = Event.current;
    41.  
    42.     if(e.isMouse) {
    43.       _mp = e.mousePosition;
    44.       _mpw = toWorldPoint(_mp, _snap);
    45.  
    46.       if(_panel.Visible) view.Repaint();
    47.     }
    48.   }
    49.  
    50.   Vector3 toWorldPoint(Vector2 pos, bool snap) {
    51.     var ray = HandleUtility.GUIPointToWorldRay(pos);
    52.     var plane = new Plane(Vector3.back, Vector3.zero);
    53.     plane.Raycast(ray, out var result);
    54.     var point = ray.origin + result * ray.direction;
    55.     if(snap) return (Vector3)snapToGrid((Vector2)point);
    56.     return point;
    57.   }
    58.  
    59.   Vector2 snapToGrid(Vector2 point)
    60.     => new Vector2(Mathf.Round(point.x), Mathf.Round(point.y));
    61.  
    62.   PanelRow panelInfo(int row)
    63.     => row switch {
    64.          0 => PanelRow.Label($"Mouse: {(Vector2)_mpw:F3}"),
    65.          1 => PanelRow.Check("Snap to integer", _snap, (i,f) => _snap = f ),
    66.          2 => PanelRow.Button("Press me", (i,f) => buttonPress(0), _button != 0),
    67.          3 => PanelRow.Button("Press me", (i,f) => buttonPress(1), _button == 0),
    68.          _ => null
    69.        };
    70.  
    71.   void buttonPress(int index) {
    72.     Debug.Log($"Hello world {index}");
    73.     _button = index;
    74.   }
    75.  
    76. }
    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. #if UNITY_EDITOR
    5. using UnityEditor;
    6. #endif
    7.  
    8. namespace EditorToolkit {
    9.  
    10.   public class SimpleSceneViewPanel {
    11.  
    12. #if UNITY_EDITOR
    13.     private const float VPAD = 6f, HPAD = 8f, SPAD = 12f, ROW_H = 16f;
    14. #endif
    15.  
    16.     public string Caption { get; private set; }
    17.     public ScreenCorner Corner { get; set; }
    18.     public int Rows { get; private set; }
    19.  
    20.     public bool Visible { get; set; }
    21.  
    22.     bool _enabled;
    23.     public bool Enabled {
    24.       get => _enabled;
    25.       set {
    26.         _enabled = value;
    27. #if UNITY_EDITOR
    28.         if(_enabled) SceneView.duringSceneGui += onSceneGui;
    29.           else SceneView.duringSceneGui -= onSceneGui;
    30. #endif
    31.       }
    32.     }
    33.  
    34.     public delegate PanelRow CallbackDelegate(int row);
    35.     CallbackDelegate _callback;
    36.  
    37.     int _windowId = -1;
    38.  
    39.     public SimpleSceneViewPanel(string caption, float width, int rows, CallbackDelegate callback, ScreenCorner corner = ScreenCorner.BottomLeft) {
    40.       Caption = caption;
    41.       Rows = rows;
    42.       _callback = callback;
    43.       Corner = corner;
    44.  
    45. #if UNITY_EDITOR
    46.       _windowId = GUIUtility.GetControlID(FocusType.Passive);
    47.       determinePanelSize(width);
    48. #endif
    49.  
    50.       Enabled = true;
    51.       Visible = true;
    52.     }
    53.  
    54.     public void Refresh() {
    55. #if UNITY_EDITOR
    56.       if(_lastCamera != null) drawWindow(_lastCamera);
    57. #endif
    58.     }
    59.  
    60. #if UNITY_EDITOR
    61.     Vector2 _panelSize;
    62.     Camera _lastCamera;
    63.  
    64.     void onSceneGui(SceneView view) {
    65.       if(Visible) drawWindow(_lastCamera = view.camera);
    66.     }
    67.  
    68.     void drawWindow(Camera sceneCam)
    69.       => GUI.Window(_windowId, getWindowRect(Corner, sceneCam.pixelRect.size), drawWindowContent, Caption);
    70.  
    71.     Rect getWindowRect(ScreenCorner corner, Vector2 screenSize) {
    72.       return new Rect(getPos(corner, screenSize), _panelSize);
    73.  
    74.       Vector2 getPos(ScreenCorner corner, Vector2 screenSize)
    75.         => corner switch {
    76.              ScreenCorner.TopLeft     => vec(-1, -1),
    77.              ScreenCorner.TopRight    => vec(+1, -1),
    78.              ScreenCorner.BottomRight => vec(+1, +1),
    79.              _                        => vec(-1, +1)
    80.            };
    81.  
    82.       Vector2 vec(int sign1, int sign2) => new Vector2(coord(0, sign1), coord(1, sign2));
    83.       float coord(int axis, int sign) => sign < 0? SPAD : screenSize[axis] - _panelSize[axis] - SPAD;
    84.     }
    85.  
    86.     void drawWindowContent(int id) {
    87.       if(id != _windowId || _callback is null) return;
    88.  
    89.       var rect = new Rect(HPAD, VPAD + ROW_H, _panelSize.x - 2f * HPAD, ROW_H);
    90.  
    91.       for(int i = 0; i < Rows; i++) {
    92.         var feedback = _callback(i);
    93.         if(feedback is null) continue;
    94.  
    95.         var saved = GUI.enabled;
    96.         GUI.enabled = feedback.enabled;
    97.  
    98.         Rect nr;
    99.  
    100.         switch(feedback.rowClass) {
    101.  
    102.           case RowClass.Label:
    103.             GUI.Label(rect, feedback.label);
    104.             rect.y += ROW_H;
    105.             break;
    106.  
    107.           case RowClass.Button:
    108.             rect.y += 4f;
    109.             nr = new Rect(rect);
    110.             nr.height += 1f;
    111.             GUI.enabled &= feedback.onClick is not null;
    112.             if(GUI.Button(nr, feedback.label)) feedback.onClick.Invoke(i, true);
    113.             rect.y += ROW_H + 2f;
    114.             break;
    115.  
    116.           case RowClass.Check:
    117.             rect.y += 4f;
    118.             nr = new Rect(rect);
    119.             nr.height += 1f;
    120.             GUI.enabled &= feedback.onClick is not null;
    121.             var oldState = feedback.state;
    122.             var newState = GUI.Toggle(nr, oldState, feedback.label, EditorStyles.miniButton);
    123.             if(newState != oldState) feedback.onClick.Invoke(i, newState);
    124.             rect.y += ROW_H + 2f;
    125.             break;
    126.  
    127.         }
    128.  
    129.         GUI.enabled = saved;
    130.       }
    131.     }
    132.  
    133.     void determinePanelSize(float width) {
    134.       var h = ROW_H + VPAD / 2f - 1f;
    135.  
    136.       if(_callback is not null) {
    137.         for(int i = 0; i < Rows; i++) {
    138.           var feedback = _callback(i);
    139.           if(feedback is null) continue;
    140.  
    141.           h += feedback.rowClass switch {
    142.             RowClass.Button => ROW_H + 6f,
    143.             RowClass.Check => ROW_H + 6f,
    144.             _ => ROW_H
    145.           };
    146.         }
    147.       }
    148.  
    149.       _panelSize = new Vector2(width, 2f * VPAD + h);
    150.     }
    151. #endif
    152.  
    153.     public enum ScreenCorner {
    154.       TopLeft,
    155.       TopRight,
    156.       BottomLeft,
    157.       BottomRight
    158.     }
    159.  
    160.     public enum RowClass {
    161.       Label,
    162.       Button,
    163.       Check
    164.     }
    165.  
    166.     public class PanelRow {
    167.  
    168.       public RowClass rowClass;
    169.       public string label;
    170.       public bool enabled;
    171.       public bool state;
    172.       public Action<int, bool> onClick;
    173.  
    174.       static public PanelRow Label(string label)
    175.         => new PanelRow() {
    176.             rowClass = RowClass.Label,
    177.             label = label,
    178.             enabled = true,
    179.             onClick = null
    180.           };
    181.  
    182.       static public PanelRow Button(string label, Action<int, bool> onClick, bool enabled = true)
    183.         => new PanelRow() {
    184.             rowClass = RowClass.Button,
    185.             label = label,
    186.             enabled = enabled,
    187.             onClick = onClick
    188.           };
    189.  
    190.       static public PanelRow Check(string label, bool value, Action<int, bool> onClick, bool enabled = true)
    191.         => new PanelRow() {
    192.             rowClass = RowClass.Check,
    193.             label = label,
    194.             state = value,
    195.             enabled = enabled,
    196.             onClick = onClick
    197.           };
    198.  
    199.     }
    200.  
    201.   }
    202.  
    203. }

    UserScript automation looks like this
    Code (csharp):
    1. PanelRow panelInfo(int row)
    2.   => row switch {
    3.        0 => PanelRow.Label($"Mouse: {(Vector2)_mpw:F3}"),
    4.        1 => PanelRow.Check("Snap to integer", _snap, (i,f) => _snap = f ),
    5.        2 => PanelRow.Button("Press me", (i,f) => buttonPress(0), _button != 0),
    6.        3 => PanelRow.Button("Press me", (i,f) => buttonPress(1), _button == 0),
    7.        _ => null
    8.      };
    9.  
    10. void buttonPress(int index) {
    11.   Debug.Log($"Hello world {index}");
    12.   _button = index;
    13. }


    Of course to prevent these objects (in
    UserScript.panelInfo
    method) being constantly allocated on the heap, they ought to be pre-allocated in
    OnEnable
    and served cold. Alternatively
    PanelRow
    can be switched to
    struct
    , which is much better if you're lazy to properly set it up (like I am in this demo).

    I find this utility mighty useful. Tell me what you think and what would you like to see added, or if you have any questions. I wanted to share this for a while now, but now I want to wrap up my other (huge) tutorial and I kind of need this for the final demo.

    Edit:
    Added
    #if UNITY_EDITOR
    safeguards as per Kurt's suggestion below. Now you don't have to pay much attention to where and when you're using this, it should shut itself off in builds. (Also no need to keep any of the classes in the Editor folder, but make sure you understand the implications of having a plain MonoBehaviour interact with the editor, because it may break your build.)

    Edit2:
    Fixed a typo in code.
     
    Last edited: May 17, 2023
    Kurt-Dekker likes this.
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,689
    Wouldn't there need to be some #if UNITY_EDITOR guarding somewhere in all of this??

    Or am I misunderstanding where it is supposed to live? I presume at least the MonoBehaviour would not be in an Editor/ folder, correct?
     
    orionsyndrome likes this.
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,106
    Originally I've made this with the safeguards all over the place, and it really hurts the eyes, so for the purpose of this tutorial it should all be in the Editor folder (both classes). However, the actual UserScript can be made more custom obviously, and just guard against the panel usage, namely in OnEnable, OnDisable, and the actual callback.

    In this particular case, something like this (assuming the remaining code actually does something else useful)
    Code (csharp):
    1. using UnityEngine;
    2.  
    3. #if UNITY_EDITOR
    4. using UnityEditor;
    5. using EditorToolkit;
    6. using static EditorToolkit.SimpleSceneViewPanel;
    7. #endif
    8.  
    9. public class SceneViewPanelTest : MonoBehaviour {
    10.  
    11.   [SerializeField] bool _showPanel = true;
    12.   [SerializeField] ScreenCorner _corner = ScreenCorner.BottomRight;
    13.  
    14. #if UNITY_EDITOR
    15.   SimpleSceneViewPanel _panel;
    16.  
    17.   void OnValidate() {
    18.     if(_panel is not null) {
    19.       _panel.Visible = _showPanel;
    20.       _panel.Corner = _corner;
    21.     }
    22.   }
    23.  
    24.   void OnEnable() {
    25.     _panel = new SimpleSceneViewPanel("My Panel", 160f, rows: 4, panelInfo, ScreenCorner.BottomRight);
    26.     SceneView.duringSceneGui += onSceneGui;
    27.   }
    28.  
    29.   void OnDisable() {
    30.     if(_panel is not null) {
    31.       _panel.Enabled = false;
    32.       _panel = null;
    33.     }
    34.     SceneView.duringSceneGui -= onSceneGui;
    35.   }
    36.  
    37.   Vector2 _mp;
    38.   Vector3 _mpw;
    39.   bool _snap;
    40.   int _button;
    41.  
    42.   void onSceneGui(SceneView view) {
    43.     var e = Event.current;
    44.  
    45.     if(e.isMouse) {
    46.       _mp = e.mousePosition;
    47.       _mpw = toWorldPoint(_mp, _snap);
    48.  
    49.       if(_panel.Visible) view.Repaint();
    50.     }
    51.   }
    52. #endif
    53.  
    54.   Vector3 toWorldPoint(Vector2 pos, bool snap) {
    55.     var ray = HandleUtility.GUIPointToWorldRay(pos);
    56.     var plane = new Plane(Vector3.back, Vector3.zero);
    57.     plane.Raycast(ray, out var result);
    58.     var point = ray.origin + result * ray.direction;
    59.     if(snap) return (Vector3)snapToGrid((Vector2)point);
    60.     return point;
    61.   }
    62.  
    63.   Vector2 snapToGrid(Vector2 point)
    64.     => new Vector2(Mathf.Round(point.x), Mathf.Round(point.y));
    65.  
    66. #if UNITY_EDITOR
    67.   PanelRow panelInfo(int row)
    68.     => row switch {
    69.          0 => PanelRow.Label($"Mouse: {(Vector2)_mpw:F3}"),
    70.          1 => PanelRow.Check("Snap to integer", _snap, (i,f) => _snap = f ),
    71.          2 => PanelRow.Button("Press me", (i,f) => buttonPress(0), _button != 0),
    72.          3 => PanelRow.Button("Press me", (i,f) => buttonPress(1), _button == 0),
    73.          _ => null
    74.        };
    75.  
    76.   void buttonPress(int index) {
    77.     Debug.Log($"Hello world {index}");
    78.     _button = index;
    79.   }
    80. #endif
    81.  
    82. }
    Edit: But I might modify the core class to be more friendly in this regard. The original version of mine was like this. It would just censor itself almost completely. Which is probably smarter now that I think about it. Sure, I'll modify the final code. Edit2: Done!
     
    Last edited: May 16, 2023
  4. unUmGong

    unUmGong

    Joined:
    May 13, 2023
    Posts:
    11
    Orion why set the corners?

    Why you prefer corner setting method over screen width method.

    Corner bottom left begins 0,0 top left screen. So top left add a value traverse matrix style down the screen. So Y, 0 + content Height. X, 0 + content Width
    Draw from top left corner down and right to the width and height of content size.

    You don't need to write a snap to int button because there one in editor shift or Ctrl shift one of the two do you a snap to int. On object movement.
     
    Last edited by a moderator: May 17, 2023
  5. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,106
    @unUmGong
    Regarding the first part: That doesn't make any sense. I am creating a custom Rect object on the spot, and all I have are two pairs of widths and heights. There is no "matrix style" that can magically know which values to subtract and even if I build the matrices to accomplish this, it would take the same amount of code.

    There is a trick with splitting the screen into quadrants, and moving the 2D origin to the center of the view. I could then theoretically solve just one corner/quadrant, but that's not really a good solution when the screen space coordinates are in pixels. Because of rounding the positioning would probably jiggle. And besides you would still have to mirror the Rect around.

    Regarding the second part: Obviously that's not the purpose of the script. If you don't understand much, the least you can do is to either ask proper questions or to refrain from posting.
     
    Last edited: May 16, 2023