Skip to content

Instantly share code, notes, and snippets.

@unitycoder
Created April 22, 2024 10:05
Show Gist options
  • Save unitycoder/3f2c6988f415d5b34e0ce3f5138c3cb7 to your computer and use it in GitHub Desktop.
Save unitycoder/3f2c6988f415d5b34e0ce3f5138c3cb7 to your computer and use it in GitHub Desktop.
2D mouse world coordinates in scene view (unity)
// https://forum.unity.com/threads/2d-mouse-world-coordinates-in-scene-view.1581912/
#nullable enable
#if UNITY_EDITOR
 
using System.Globalization;
 
using UnityEditor;
 
using UnityEngine;
using UnityEngine.InputSystem;
 
namespace My.Common.Scripts.Editor
{
  /// <summary>
  /// A window in the scene view that shows the current world coordinates of the mouse cursor in 2D mode.
  /// </summary>
  [InitializeOnLoad]
  public class MouseWorldSceneWindow
  {
    private static Vector3 _currentMouseWorldPosition;
    private static GUISkin? _customSkin;
    private static bool _isWindowVisible;
    private static Rect _windowRect = new(0, 0, 150, 20);
    private static readonly Vector2 DefaultPosition = new(50f, 10f);
    private const string MouseWorldSceneWindowName = nameof(MouseWorldSceneWindow);
    private const string ResetSceneWindowMenuName = "Tools/Reset " + MouseWorldSceneWindow.MouseWorldSceneWindowName;
    private const string ToggleSceneWindowMenuName = "Tools/Toggle " + MouseWorldSceneWindow.MouseWorldSceneWindowName;
    private const string WindowPositionXEditorPrefsName = MouseWorldSceneWindow.MouseWorldSceneWindowName + "/SceneWindowPositionX";
    private const string WindowPositionYEditorPrefsName = MouseWorldSceneWindow.MouseWorldSceneWindowName + "/SceneWindowPositionY";
    private const string WindowVisibilityEditorPrefsName = MouseWorldSceneWindow.MouseWorldSceneWindowName + "/ShowWindow";
 
    /// <summary>
    /// Static constructor that initializes the window.
    /// </summary>
    static MouseWorldSceneWindow()
    {
      // Load preferences
      MouseWorldSceneWindow._isWindowVisible = EditorPrefs.GetBool(MouseWorldSceneWindow.WindowVisibilityEditorPrefsName, true);
      MouseWorldSceneWindow._windowRect.x = EditorPrefs.GetFloat(MouseWorldSceneWindow.WindowPositionXEditorPrefsName, MouseWorldSceneWindow.DefaultPosition.x);
      MouseWorldSceneWindow._windowRect.y = EditorPrefs.GetFloat(MouseWorldSceneWindow.WindowPositionYEditorPrefsName, MouseWorldSceneWindow.DefaultPosition.y);
 
      // Update the menu item checked state
      Menu.SetChecked(MouseWorldSceneWindow.ToggleSceneWindowMenuName, MouseWorldSceneWindow._isWindowVisible);
 
      // Register events
      SceneView.duringSceneGui += MouseWorldSceneWindow.OnSceneGUI;
      EditorApplication.update += MouseWorldSceneWindow.Update;
    }
 
    /// <summary>
    /// Create the GUI skin for the scene window.
    /// </summary>
    private static void CreateGUISkin()
    {
      if (MouseWorldSceneWindow._customSkin == null)
      {
        Color semiTransparentBackground = new Color32(0, 0, 0, 160);
        Color textColor = new Color32(224, 224, 224, 255);
 
        var windowTexture = MouseWorldSceneWindow.CreateWindowBackgroundTexture((int)MouseWorldSceneWindow._windowRect.width, (int)MouseWorldSceneWindow._windowRect.height,
          semiTransparentBackground, 5);
 
        var customSkin = ScriptableObject.CreateInstance<GUISkin>();
        customSkin.window.normal.background = windowTexture;
        customSkin.window.onNormal.background = windowTexture;
        customSkin.window.focused.background = windowTexture;
        customSkin.window.onFocused.background = windowTexture;
 
        var labelStyle = new GUIStyle();
        labelStyle.fontSize = 16;
        labelStyle.normal.textColor = textColor;
        labelStyle.onFocused.textColor = textColor;
        labelStyle.hover.textColor = textColor;
        customSkin.label = labelStyle;
 
        MouseWorldSceneWindow._customSkin = customSkin;
      }
    }
 
    /// <summary>
    /// Creates the background texture for the window.
    /// </summary>
    private static Texture2D CreateWindowBackgroundTexture(int width, int height, Color col, int borderRadius)
    {
      var tex = new Texture2D(width, height);
      var colors = new Color[width * height];
      var cornerReferencePoints = new[]
      {
        new Vector2(borderRadius, borderRadius),
        new Vector2(width - borderRadius, borderRadius),
        new Vector2(borderRadius, height - borderRadius),
        new Vector2(width - borderRadius, height - borderRadius),
      };
 
      for (var y = 0; y < height; y++)
      {
        for (var x = 0; x < width; x++)
        {
          bool isBorder;
 
          var coord = new Vector2(x, y);
 
          if (coord.x < cornerReferencePoints[0].x && coord.y < cornerReferencePoints[0].y)
          {
            isBorder = Vector2.Distance(coord, cornerReferencePoints[0]) > borderRadius;
          }
          else if (coord.x > cornerReferencePoints[1].x && coord.y < cornerReferencePoints[1].y)
          {
            isBorder = Vector2.Distance(coord, cornerReferencePoints[1]) > borderRadius;
          }
          else if (coord.x < cornerReferencePoints[2].x && coord.y > cornerReferencePoints[2].y)
          {
            isBorder = Vector2.Distance(coord, cornerReferencePoints[2]) > borderRadius;
          }
          else if (coord.x > cornerReferencePoints[3].x && coord.y > cornerReferencePoints[3].y)
          {
            isBorder = Vector2.Distance(coord, cornerReferencePoints[3]) > borderRadius;
          }
          else
          {
            isBorder = false;
          }
 
          colors[y * width + x] = isBorder ? Color.clear : col;
        }
      }
 
      tex.SetPixels(colors);
      tex.Apply();
 
      return tex;
    }
 
    private static void DrawWindow(int windowID)
    {
      // Show the mouse world position
      const string FormatString = "F2";
      GUILayout.Space(3);
      GUILayout.BeginHorizontal();
      GUILayout.Space(5);
      GUILayout.Label(MouseWorldSceneWindow._currentMouseWorldPosition.x.ToString(FormatString, CultureInfo.CurrentCulture), GUILayout.Width(60));
      GUILayout.Space(5);
      GUILayout.Label("/", GUILayout.Width(5));
      GUILayout.Space(5);
      GUILayout.Label(MouseWorldSceneWindow._currentMouseWorldPosition.y.ToString(FormatString, CultureInfo.CurrentCulture));
      GUILayout.EndHorizontal();
      GUILayout.Space(3);
 
      // Make the window draggable
      GUI.DragWindow();
    }
 
    /// <summary>
    /// Draws the scene view window.
    /// </summary>
    private static void OnSceneGUI(SceneView sceneView)
    {
      if (MouseWorldSceneWindow._isWindowVisible)
      {
        // Has to be done in the OnSceneGUI method, static ctor does not work
        MouseWorldSceneWindow.CreateGUISkin();
 
        // Draw the scene window
        Handles.BeginGUI();
        GUI.skin = MouseWorldSceneWindow._customSkin;
        MouseWorldSceneWindow._windowRect =
          GUILayout.Window(0, MouseWorldSceneWindow._windowRect, MouseWorldSceneWindow.DrawWindow, string.Empty, MouseWorldSceneWindow._customSkin?.window);
        GUI.skin = null;
        Handles.EndGUI();
 
        // Save the window positions
        EditorPrefs.SetFloat(MouseWorldSceneWindow.WindowPositionXEditorPrefsName, MouseWorldSceneWindow._windowRect.x);
        EditorPrefs.SetFloat(MouseWorldSceneWindow.WindowPositionYEditorPrefsName, MouseWorldSceneWindow._windowRect.y);
      }
    }
 
    /// <summary>
    /// Resets the scene view window to its default position.
    /// </summary>
    [MenuItem(MouseWorldSceneWindow.ResetSceneWindowMenuName)]
    public static void ResetWindowPosition()
    {
      MouseWorldSceneWindow._windowRect.x = MouseWorldSceneWindow.DefaultPosition.x;
      MouseWorldSceneWindow._windowRect.y = MouseWorldSceneWindow.DefaultPosition.y;
 
      EditorPrefs.SetFloat(MouseWorldSceneWindow.WindowPositionXEditorPrefsName, MouseWorldSceneWindow._windowRect.x);
      EditorPrefs.SetFloat(MouseWorldSceneWindow.WindowPositionYEditorPrefsName, MouseWorldSceneWindow._windowRect.y);
    }
 
    /// <summary>
    /// Shows the scene view window.
    /// </summary>
    [MenuItem(MouseWorldSceneWindow.ToggleSceneWindowMenuName)]
    public static void ToggleWindowVisibility()
    {
      MouseWorldSceneWindow._isWindowVisible = !MouseWorldSceneWindow._isWindowVisible;
      EditorPrefs.SetBool(MouseWorldSceneWindow.WindowVisibilityEditorPrefsName, MouseWorldSceneWindow._isWindowVisible);
      Menu.SetChecked(MouseWorldSceneWindow.ToggleSceneWindowMenuName, MouseWorldSceneWindow._isWindowVisible);
    }
 
    /// <summary>
    /// Updates the scene view window data.
    /// </summary>
    private static void Update()
    {
      // Update mouse position during design-time and play mode
      MouseWorldSceneWindow.UpdateCurrentMouseWorldPosition();
    }
 
    /// <summary>
    /// Update the mouse world position.
    /// </summary>
    private static void UpdateCurrentMouseWorldPosition()
    {
      // Gets the current scene view and scene view camera while design-time and play mode
      var sceneView = SceneView.sceneViews.Count > 0 ? (SceneView)SceneView.sceneViews[0] : null;
      var sceneViewCamera = sceneView?.camera;
 
      if (sceneView != null && sceneViewCamera != null)
      {
        // Determine current mouse position with new input system
        var mouseScreenPosition = Mouse.current.position.ReadValue();
 
        // Determine offsets and bars of the scene view
        var sceneViewTopBarsHeight = sceneView.rootVisualElement.worldBound.y;
        var sceneViewAbsoluteScreenOffset = sceneView.position.min;
 
        // Adjust y so it takes the top bars of the scene view into account and flip y-axis (cause of different coordinate systems)
        // and also adjust x/y regarding the scene view offset
        // (This is the correct code when calling from Update)
        mouseScreenPosition.y = sceneViewCamera.pixelHeight - (mouseScreenPosition.y - sceneViewTopBarsHeight - sceneViewAbsoluteScreenOffset.y);
        mouseScreenPosition.x -= sceneViewAbsoluteScreenOffset.x;
 
        // Adjust y so it takes the top bars of the scene view into account and flip y-axis (cause of different coordinate systems)
        // (This would be the correct code when calling from OnSceneGUI)
        //mouseScreenPosition.y = sceneViewCamera.pixelHeight - (mouseScreenPosition.y - sceneViewTopBarsHeight);
 
        MouseWorldSceneWindow._currentMouseWorldPosition = sceneViewCamera.ScreenToWorldPoint(mouseScreenPosition);
 
        // Force repaint the scene view, so the scene window is also repainted with the new data
        SceneView.RepaintAll();
      }
    }
  }
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment