1

I am trying to take a Ui object's screen space position and translate that to what I am calling 'monitor space'.

As far as I can tell, screen space, in Unity, is relative to the applications' window. That is, even if the app is not full screen, and moved around on your monitor, 0,0 will still be the lower left of the app window.

I need to translate one of those screen space values into the actual position within the user's monitor. This is especially important when considering that the user might have multiple monitors.

I am not finding anything to get this done, though.

I am hoping to find a platform agnostic solution, but if it must be Windows-only than I can make that work as well.

Any help on this would be greatly appreciated.

Thank you

FunnerSoft
  • 77
  • 8
  • Why would you want to do this? What if the user plays windowed and the indicator is now very off screen? You can get the [`resolution`](https://docs.unity3d.com/ScriptReference/Screen.html) of the monitor. Then translate space relative to the viewport extended to the resolution size. – TEEBQNE Sep 20 '21 at 16:53
  • @TEEBQNE sounds like a very specific use case where OP needs exactly this ;) make it an answer – derHugo Sep 20 '21 at 16:57
  • @derHugo I can write a detailed answer when back on my computer. Wanted to put out the idea in case OP can use it themselves or someone else beats me to posting. – TEEBQNE Sep 20 '21 at 17:03
  • @TEEBQNE, this is for a very specific tool that would be helpful for development of another internal project. I realize it's not a standard approach to things :) – FunnerSoft Sep 20 '21 at 17:20
  • @TEEBQNE, yes, if you are able to explain that translation in more detail. it would be greatly appreciated. It seems like I need to know the application's window rect in monitor space coordinates. However, I only see values like width and height. – FunnerSoft Sep 20 '21 at 17:23
  • @FunnerSoft If this is for development, would this just need to work in editor, or also in a standalone project? Specifically `game view` or `scene view` in editor? Or both? – TEEBQNE Sep 20 '21 at 17:28
  • @TEEBQNE the ultimate goal is for it to work in editor and debug builds on device (mobile, switch, etc). But, an editor only solution is better then no solution :) – FunnerSoft Sep 20 '21 at 17:35
  • You can also use [`Display`](https://docs.unity3d.com/ScriptReference/Display.html) to get the actual Native resolution of the device display as well as the current rendering resolution – derHugo Sep 20 '21 at 18:54
  • Yep. My solution was going to use `Display.main.systemWidth, Display.main.systemHeight` and find the normalized value of screen space then convert this to display or monitor space. Had linked to the wrong doc and noticed too late. – TEEBQNE Sep 20 '21 at 18:59
  • 1
    Actually if this is Windows specific anyway you can also just look at [Getting mouse position in c#](https://stackoverflow.com/questions/1316681/getting-mouse-position-in-c-sharp) which gives you the absolute pixel coordinates of the system cursor... Oh but just realized it's not about the mouse anyway ^^ – derHugo Sep 20 '21 at 19:05
  • That would work to get the mouse position in display space but could you convert another objects screen position to display position using it? – TEEBQNE Sep 20 '21 at 19:09
  • @TEEBQNE nah just forgot that it wasn't specifically about the mouse position ^^ Using the new Input system however you could use [`WarpCursorPosition`](https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Mouse.html) and set the system mouse to the target position (in unity screen coordinates) and then get the value(s) via the DLL .. not sure though how much overkill this is compared to calculating it ^^ If uses though anyway you somehow need native window pixel position anyway so you won't get around some hacky DLL usage – derHugo Sep 20 '21 at 19:11
  • Interesting haven't looked at the new input system too much. Seems like a valid way to get the position. If this system is only used as a debug tool I see no harm. One concern would be it moves the mouse position which could disturb the flow of testing. If its moved to the new position and back shouldn't be an issue though. – TEEBQNE Sep 20 '21 at 19:15

2 Answers2

1

Now after TEEBQNE's answer I also wanted to give it a shot using the native solution.

As mentioned this will be only for Windows PC Standalone and requires

  • Unity's new Input System (see Quick Start)

  • One of the solutions from Getting mouse position in c#

    For example if you want to use System.Windows.Forms then copy the according DLL from

     C:\Windows\Microsoft.NET\Framework64\v4.x.xx
    

    into your project under Assets/Plugins

    Then in code you can use

     using System.Windows.Forms;
    

If this is more efficient (or even works this way) I can't tell - only on the phone here - but I hope the idea gets clear ;)

So the idea is:

  • store initial cursor position
  • Set your cursor to certain positions of interest using WarpCursorPosition using Unity screen coordinates as input
  • read out the resulting absolute monitor coordinates using the native stuff
  • in the end reset the cursor to the original position

This might look somewhat like

using UnityEngine;
using UnityEngine.InputSystem;

public static class MonitorUtils
{
    // Store reference to main Camera (Camera.main is expensive)
    private static Camera _mainCamera;

    // persistent array to fetch rect corners
    // cheaper then everytime creating and throwing away a new array
    // especially when fetching them every frame
    private static readonly Vector3[] corners = new Vector3[4];

    // For getting the UI rect corners in Monitor pixel coordinates
    public static void GetMonitorRectCorners(this RectTransform rectTransform, Vector2Int[] output, bool isScreenSpaceCanvas = true, Camera camera = null)
    {
        // Lazy initialization of optional parameter
        if (!camera) camera = GetMainCamera();

        // Store initial mouse position
        var originalMousePosition = Mouse.current.position.ReadValue();

        // Get the four world space positions of your RectTtansform's corners
        // in the order bottom left, top left, top right, bottom right
        // See https://docs.unity3d.com/ScriptReference/RectTransform.GetWorldCorners.html
        rectTransform.GetWorldCorners(corners);

        // Iterate the four corners
        for (var i = 0; i < 4; i++)
        {
            if (!isScreenSpaceCanvas)
            {
                // Get the monitor position from the world position (see below)
                output[i] = WorldToMonitorPoint(corners[i], camera);
            }
            else
            {
                // Get the monitor position from the screen position (see below)
                output[i] = ScreenToMonitorPoint(corners[i], camera);
            }
        }

        // Restore mouse position
        Mouse.current.WarpCursorPosition(originalMousePosition);
    }
    
    // For getting a single Unity world space position in Monitor pixel coordinates
    public static Vector2Int WorldToMonitorPoint(Vector3 worldPoint, Camera camera = null)
    {
        // Lazy initialization of optional parameter
        if (!camera) camera = GetMainCamera();

        var screenPos = camera.WorldToScreenPoint(worldPoint);

        return ScreenToMonitorPoint(screenPos, camera);
    }

    // For getting a single Unity world space position in Monitor pixel coordinates
    public static Vector2Int ScreenToMonitorPoint(Vector3 screenPos, Camera camera = null)
    {
        // Lazy initialization of optional parameter
        if (!camera) camera = GetMainCamera();

        // Set the system cursor position there based on Unity screen space
        Mouse.current.WarpCursorPosition(screenPos);

        // Then get the actual system mouse position (see below)
        return GetSystemMousePosition();
    }

    // Get and store the main camera
    private static Camera GetMainCamera()
    {
        if (!_mainCamera) _mainCamera = Camera.main;

        return _mainCamera;
    }

    // Convert the system mouse position to Vector2Int for working 
    // with it in Unity
    private static Vector2Int GetSystemMousePosition()
    {
        var point = System.Windows.Forms.Cursor.Position;

        return new Vector2Int(point.X, point.Y);
    }
}

So you can either simply use

var monitorPosition = MonitorUtils.WorldToMonitorPoint(someUnityWorldPosition);
// or if you already have the `Camera` reference
//var monitorPosition = MonitorUtils.WorldToMonitorPoint(someUnityWorldPosition, someCamera);

or if you already have a screen space position like e.g. in a ScreenSpace Overlay canvas

var monitorPosition = MonitorUtils.ScreenToMonitorPoint(someUnityWorldPosition);
// or if you already have the `Camera` reference
//var monitorPosition = MonitorUtils.ScreenToMonitorPoint(someUnityWorldPosition, someCamera);

or you can get all four corners of a UI element at once using e.g.

 var monitorCorners = new Vector2Int [4];

 someRectTransform.GetMonitorRectCorners(monitorCorners, isScreenSpaceCanvas);
// or again if you already have a camera reference
//someRectTransform.GetMonitorRectCorners(monitorCorners, isScreenSpaceCanvas, someCamera);

Little example

public class Example : MonoBehaviour
{
    [Header("References")]
    [SerializeField] private Camera mainCamera;
    [SerializeField] private RectTransform _rectTransform;
    [SerializeField] private Canvas _canvas;

    [Header("Debugging")] 
    [SerializeField] private bool isScreenSpace;

    [Header("Output")] 
    [SerializeField] private Vector2Int bottomLeft;
    [SerializeField] private Vector2Int topLeft;
    [SerializeField] private Vector2Int topRight;
    [SerializeField] private Vector2Int bottomRight;

    private readonly Vector2Int[] _monitorPixelCornerCoordinates = new Vector2Int[4];

    private void Awake()
    {
        if (!mainCamera) mainCamera = Camera.main;
        if (!_canvas) _canvas = GetComponentInParent<Canvas>();
        isScreenSpace = _canvas.renderMode == RenderMode.ScreenSpaceOverlay;
    }

    private void Update()
    {
        if (Keyboard.current.spaceKey.isPressed)
        {
            _rectTransform.GetMonitorRectCorners(_monitorPixelCornerCoordinates, isScreenSpace);

            bottomLeft = _monitorPixelCornerCoordinates[0];
            topLeft = _monitorPixelCornerCoordinates[1];
            topRight = _monitorPixelCornerCoordinates[2];
            bottomRight = _monitorPixelCornerCoordinates[3];
        }
    }
}

You will see that moving your mouse each and every frame isn't a good idea though ^^

Now you can see the four corners being updated depending on the actual position on the screen.

enter image description here

Note: while Unity Screenspace is 0,0 at the bottom left in normal display pixels 0,0 is actually rather top-left. So you might need to invert these.

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • @TEEBQNE and derHugo, first off, thank you very much for the tremendous amount of effort and time you two have put into this already. I really appreciate it. However, I am not sure I see the solution yet. Check out this ![example image](https://ibb.co/wrkkBcf) The code listed above "var screenPos = camera.WorldToScreenPoint(worldPoint);" does not result in a equivalent monitor space value. And so, warping the cursor to that point does not go to the expected location. I must be missing something? – FunnerSoft Sep 21 '21 at 16:44
  • @FunnerSoft I don't fully understand .. my code uses far more than just WorldToScreenPoint ;) .. also have in mind that Unity Screenspace is bottom to top while display pixels go top to bottom so you might have to invert it depending on your needs – derHugo Sep 21 '21 at 17:53
  • Also want to mention there is maximum display resolution and current resolution. Monitors can support many different resolutions. My solution does not use the maximum size but the current used resolution the user has set their monitor to. My solution also uses more than just screen space. If you implement the dll for Windows it should produce the correct results on any windowed standalone build. – TEEBQNE Sep 21 '21 at 20:16
  • Thank you again to you both. The solutions, when used in my 'example image' setup, don't produce the results that I expect. There must be something I am missing; since it's working for you two. I think I will try and spin up a new, clean, empty project to work in. – FunnerSoft Sep 21 '21 at 22:42
  • @FunnerSoft we still don't know what exactly you are doing/expecting ... – derHugo Sep 22 '21 at 08:07
  • @TEEBQNE my solution doesn't use the maximum resolution either .. it sues the current system cursor position which of course only uses the resolution that the user is currently using ;) – derHugo Sep 22 '21 at 08:08
  • @derHugo, sorry for not making my issue very clear. Please, take a look at this ![image](https://ibb.co/wrkkBcf). If there was a Transform smack dead in the middle of that application's window, I would want my code to give me it's "monitor space" position as something like 2460,295. However, my code gives me a value that is closer to 70000,40000. So, something is totally off for sure =/ – FunnerSoft Sep 22 '21 at 13:58
  • What you're describing almost sounds like you're using a overlay canvas space and trying to get its screen position. When a canvas render mode is already in screen space you do not need to convert its space to the space it is already in. – TEEBQNE Sep 22 '21 at 14:52
  • @FunnerSoft what exactly did you use? `ScreenToMonitorPoint` or `WorldToMonitorPoint` ? – derHugo Sep 22 '21 at 14:54
  • @derHugo I should have clarified. I didn't mean to say your solution didn't use the current resolution and not the maximum possible resolution. I was more explaining to OP that their expected result could be different due to changing resolution sizes vs maximum resolution size of their monitor. But it seems something else is entirely off. – TEEBQNE Sep 22 '21 at 14:56
  • @TEEBQNE, I am, in fact, using a Screen Space - Overlay canvas. Will that not work? – FunnerSoft Sep 22 '21 at 15:58
  • @derHugo, I am using WorldToMonitorPoint – FunnerSoft Sep 22 '21 at 15:59
  • @FunnerSoft If you have a Screenspace overlay then your coordinates already **are** in Screenspace, not in worldSpace => use ScreeToMonitorPoint instead ;) – derHugo Sep 22 '21 at 16:00
  • @derHugo, are you saying that the GameObject.Transform.Position value of any Ui object within my Screen Space - Overlay canvas is already in screen space? – FunnerSoft Sep 22 '21 at 16:20
  • 1
    @FunnerSoft yep exactly that's the case in a Screenspace Overlay Canvas ... Therefore the name **Screenspace** ;) – derHugo Sep 22 '21 at 16:27
  • Hello again @derHugo. I tested with the correct screen space values now, but I still don't get proper monitor space values. I suspect it's related to my mixture of these 2 proposed solutions. My current project doesn't use the new Input System from Unity. It uses the old method. Therefore, I am using the imnported Windows DLL trick that TEEBQNE mentioned. I will create the the new, empty project I mentioned earlier and test in there. Thank you to the both of you. Cheers! – FunnerSoft Sep 23 '21 at 16:24
  • @derHugo, thank you! A quick test showed that this does work in a new, clean project with the new Input System. I apologize for all the back and forth this took. Thank you for taking so much of your time to help me with this. Now, just to get the new Input System working in our main project ..... – FunnerSoft Sep 23 '21 at 16:51
0

Alright first off - sorry for the late response just got back and was able to type up an answer.

From what I have found, this solution does not work in the editor and produces odd results on Mac with retina display. In the editor, the Screen and Display spaces appear to be exactly the same. There is probably a solution to fix this but I did not look into the specifics. As for Mac, for whatever reason, the internal resolution outputted is always half the actual resolution. I am not sure if this is just a retina display bug with Unity or a general Mac bug. I tested and ran this test script on both a Windows computer and Mac with a retina display. I have yet to test it on any mobile platform.

I do not know exactly what you would like to achieve with the values you wish to find, so I set up a demo scene displays the values instead of using them.

Here is the demo script:

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.UI;

public class TestScript : MonoBehaviour
{
    [SerializeField] private RectTransform rect = null;
    [SerializeField] private List<Text> text = new List<Text>();
    [SerializeField] private Canvas parentCanvas = null;
    [SerializeField] private Camera mainCam = null;

    private void Start()
    {
        // determine the canvas mode of our UI object
        if (parentCanvas == null)
            parentCanvas = GetComponentInParent<Canvas>();

        // only need a camera in the case of camera space canvas
        if (parentCanvas.renderMode == RenderMode.ScreenSpaceCamera && mainCam == null)
            mainCam = Camera.main;

        // generate initial data points
        GenerateData();
    }

    /// <summary>
    /// Onclick of our button to test generating data when the object moves
    /// </summary>
    public void GenerateData()
    {
        // the anchored position is relative to screen space if the canvas is an overlay - if not, it will need to be converted to screen space based on our camera
        Vector3 screenPos = parentCanvas.renderMode == RenderMode.ScreenSpaceCamera ? mainCam.WorldToScreenPoint(transform.position) : rect.transform.position;

        // our object relative to screen position
        text[0].text = "Screen Pos: " + screenPos;

        // the dimensions of our screen (The current window that is rendering our game)
        text[1].text = "Screen dimensions: " + Screen.width + " " + Screen.height;

        // find our width / height normalized relative to the screen space dimensions
        float x = Mathf.Clamp01(screenPos.x / Screen.width);
        float y = Mathf.Clamp01(screenPos.y / Screen.height);

        // our normalized screen positions
        text[2].text = "Normalized Screen Pos: " + x + " " + y;

        // grab the dimensions of the main renderer - the current monitor our game is rendered on
#if UNITY_STANDALONE_OSX
        text[3].text = "Display dimensions: " + (Display.main.systemWidth * 2f) + " " + (Display.main.systemHeight * 2f);

        // now find the coordinates our the UI object transcribed from screen space normalized coordinates to our monitor / resolution coordinates
        text[4].text = "Display relative pos: " + (Display.main.systemWidth * x * 2f) + " " + (Display.main.systemHeight * y * 2f);
#else
        text[3].text = "Display dimensions: " + Display.main.systemWidth + " " + Display.main.systemHeight;

        // now find the coordinates our the UI object transcribed from screen space normalized coordinates to our monitor / resolution coordinates
        text[4].text = "Display relative pos: " + (Display.main.systemWidth * x) + " " + (Display.main.systemHeight * y);
#endif
    }

    /// <summary>
    /// Just for debugging - can be deleted
    /// </summary>
    private void Update()
    {
        if (Input.GetKey(KeyCode.A))
        {
            rect.anchoredPosition += new Vector2(-10f, 0f);
        }

        if (Input.GetKey(KeyCode.W))
        {
            rect.anchoredPosition += new Vector2(0f, 10f);
        }

        if (Input.GetKey(KeyCode.S))
        {
            rect.anchoredPosition += new Vector2(0f, -10f);
        }

        if (Input.GetKey(KeyCode.D))
        {
            rect.anchoredPosition += new Vector2(10f, 0f);
        }
    }
}

I accounted for the parent canvas being either Overlay or Camera mode and put in a check for an OSX build to adjust to the proper screen dimensions.

Here is a gif of the build on OSX. I set the window to be 1680x1050 and my computer's current resolution is 2880x1800. I had also test it on Windows but did not record it as the example looks nearly identical. Example

Let me know if you have more questions about the implementation or if there are issues with other platforms I did not test.

Edit: Just realized you want the screen space coordinate relative to the monitor space. I will correct the snippet in a little bit - in a meeting right now.

Edit2: After a bit more looking, it will not be easy to get the exact coordinates without the window being centered or getting the standalone window's position. I do not believe there is an easy way to get this information without a dll, so here is a implementation for mac and a solution for windows.

Currently, the solution I have will only get the screen position if the standalone player is windowed and centered on your screen. If the player is centered on the screen, I know that the center of my monitor is half the dimensions of its resolution, and know that the center point of my window matches up to this point. I can now get the bottom left corner of my window relative to my monitor and not a (0,0) coordinate. As the screen space has the bottom left corner at (0,0), you can now adjust the position to monitor space by adding the position of the newly calculated bottom left position.

Here is the new new GenerateData method:

/// <summary>
/// Onclick of our button to test generating data when the object moves
/// </summary>
public void GenerateData()
{
    // the anchored position is relative to screen space if the canvas is an overlay - if not, it will need to be converted to screen space based on our camera
    Vector3 screenPos = parentCanvas.renderMode == RenderMode.ScreenSpaceCamera ? mainCam.WorldToScreenPoint(transform.position) : rect.transform.position;

    // grab the display dimensions
    Vector2 displayDimensions;

    // bug or something with mac or retina display on mac where the main.system dimensions are half of what they actually are
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
    displayDimensions = new Vector2(Display.main.systemWidth * 2f, Display.main.systemHeight * 2f);
#else
    displayDimensions = new Vector2(Display.main.systemWidth, Display.main.systemHeight);
#endif

    // the centerpoint of our display coordinates
    Vector2 displayCenter = new Vector2(displayDimensions.x / 2f, displayDimensions.y / 2f);

    // half our screen dimensions to find our screen space relative to monitor space
    Vector2 screenDimensionsHalf = new Vector2(Screen.width / 2f, Screen.height / 2f);

    // find the corners of our window relative to the monitor space
    Vector2[] displayCorners = new Vector2[] {
                                                    new Vector2(displayCenter.x - screenDimensionsHalf.x, displayCenter.y - screenDimensionsHalf.y),  // bottom left
                                                    new Vector2(displayCenter.x - screenDimensionsHalf.x, displayCenter.y + screenDimensionsHalf.y),  // top left
                                                    new Vector2(displayCenter.x + screenDimensionsHalf.x, displayCenter.y + screenDimensionsHalf.y),  // top right
                                                    new Vector2(displayCenter.x + screenDimensionsHalf.x, displayCenter.y - screenDimensionsHalf.y)   // bottom right
                                            };

    for (int z = 0; z < 4; ++z)
    {
        text[z].text = displayCorners[z].ToString();
    }

    // outputting our screen position relative to our monitor
    text[4].text = (new Vector2(screenPos.x, screenPos.y) + displayCorners[0]).ToString();
}

Once you are able to either get or set the windowed screen, you can properly re-orient the lower-left corner relative to the monitor dimensions or you can set the window back to the center point of your monitor. The above snippet would also work for a full-screen player. You would just need to determine how far off the aspect ratio of the player window is to your monitor, which allows you to find how large the black bars would be on the edges.

I assumed what you had wanted was straightforward but from what I can tell an OS-agnostic solution would be difficult. My above solution should work for any platform when the player is windowed if you can either get or set the standalone window position and for any platform that is full-screened with the theoretical approach I mentioned.

If you want more info on how to adjust the implementation for the full-screened window let me know.

TEEBQNE
  • 6,104
  • 3
  • 20
  • 37