Search Unity

Stay cursor! Stay! Keeping the cursor @ the same world position.

Discussion in 'Scripting' started by FeastSC2, Apr 8, 2022.

  1. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    Hey, I'm stuck on a problem related to cursors.
    Here's the conundrum.

    The setup
    In my game, the camera follows my character.
    And by default, as the camera moves in the world, the cursor is locked at the same screen position (unless the player moves the camera mouse).
    This is the default, and it's typical for games, and for mouse cursors in general.
    See the gif here, the cursor remains at the same screen position, but moves in terms of world position.

    The objective
    However, my game needs the cursor to stay at the same WORLD position (and not the same screen position like many other games). That's my objective.
    I've managed to do so for example when I'm switching from character to character.
    Notice the cursor is now translated in screen space such that it remains @ the same world position

    How did I manage to keep the cursor locked to the world position during a character switch?
    I do this by saving the worldPosition (casting a ray to the ground from the cursor) before the characters switch.
    And then throughout the character's switch I update the cursor to remain at the same worldPosition using:
    Code (CSharp):
    1. Mouse.current.WarpCursorPosition(Camera.main.WorldToScreenPoint(worldPositionPreCharacterSwitch));
    I do this for as long as the duration of switching from character to character takes.

    As a sidenote: In order to allow input from the user, I also add to this worldPosition the delta of my mouse so that if the player moves the mouse during a character switch, the cursor is offset from that worldPosition.
    The mouseDelta is the amount of movement received by the mouse, it doesn't take into account movements of the screen.
    Code (CSharp):
    1. Mouse.current.WarpCursorPosition(Camera.main.WorldToScreenPoint(worldPositionPreCharacterSwitch + mouseDelta));
    And that's perfect, it's exactly what I want and it achieves the result of the 2nd gif shown above.

    The next step...
    Now I need to figure out how to apply this idea to my game constantly.
    If you look back at very first gif: when my character moves, the camera moves.
    And because the camera moves, the cursor moves (in terms of world position).

    I would like that the cursor stays at the same worldPosition, even when the camera is moving.
    And when the cursor reaches the borders of the screen, it would get dragged to remain within the bounds of the camera.

    If I could have that throughout my game, my objective would be fulfilled.

    Do you guys have any idea how to do this?
     
    Last edited: Apr 8, 2022
  2. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    Not sure I really understand why you would want this functionality in a game. I would find it frustrating that my cursor moves when my character moves. I will constantly need to move the mouse from one side of the screen to the other because my movement pushed the cursor to the opposite side of the direction of movement. Alas I don't know what type of gameplay you have and I would check out your site but I am told it isn't secure.

    With that being said. The performed event should only fire for Delta Position when the mouse is moved by the player. However moving the mouse via code may also fire that event seeing as it was moved. I would test that to see if you can use that to force the mouse to keep it's world position as long as the player isn't moving the mouse.
     
  3. RadRedPanda

    RadRedPanda

    Joined:
    May 9, 2018
    Posts:
    1,647
    Maybe not the best way to do it, and would probably need a lot of rewriting, but you could create a "fake cursor" as a Canvas element rendered in World Space, and you manually move it when the mouse is moved. It would probably be best to move it based on Viewport position and render it above everything else.

    Since it's in World Space, it would automatically stay in place whenever the camera moved. Raycasting shouldn't be too difficult, as you would just need to Raycast from the Camera to the tip of the cursor, ignoring the cursor itself.

    To check whether it's on screen or not, you could use Camera.WorldToViewportPoint to check whether it's on the screen, and move it to the closest Viewport Point when it's off.

    Lastly, another problem you would face is that the cursor would get further or closer to the camera when it moved, but I think that could be solved by setting its Viewport z axis to the same value every frame.

    Again, just theory crafting here.
     
    FeastSC2 likes this.
  4. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    I would most likely activate this during combat only. It's a test, but I'm quite sure it has potential.

    The website is out of date. I removed the link.

    I don't really understand what you're saying.
    I'm not sure I have access to "when the camera is moving or when the player is moving" by code easily.

    I think that's a good start of an idea.
    I'm not sure how to move the fake cursor (with world space canvas) in a way that's similar to the "real" screen space cursor however. And if possible i'd like to keep the sensitivity the user has from his native OS as well.

    Actually... maybe.. it could be as simple as to create a new camera that never moves.
    This camera would only render the fake cursor with a screen space canvas.
    Then it's as simple as to move the fake cursor with the mouse's movement (deltaMove).
    After that I can overlay the cursor camera with the main camera.
    And finally use the viewport function to keep the fake cursor inside the bounds.

    Mhh.. actually that doesn't work. The fake cursor just won't stay @ the same world position like this. It's hard to wrap my head around this. :D
     
    Last edited: Apr 8, 2022
  5. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    I'm not sure how your getting the current mouse position, but seeing in the screenshot of your actions you have DeltaPosition which will have a performed event that you can hook into. I was suggesting to use that to see if the player is moving the mouse or not as it will only trigger when the mouse is moving. However moving the mouse with Mouse.current.WarpCursorPosition() (this is moving the mouse via code) may also trigger the performed event.

    Let me explain with an example.
    Code (CSharp):
    1. public InputAction mouseDeltaAction;
    2.  
    3. private void Awake()
    4. {
    5.     mouseDeltaAction.performed += MouseDelta_Performed;
    6. }
    7.  
    8. private void MouseDelta_Performed(InputAction.CallbackContext context)
    9. {
    10.     worldMousePosition = GetMouseWorldPos();//I will leave this method up to you.
    11. }
    12.  
    13. private void Update()
    14. {
    15.     Vector3 viewportPos = Camera.main.WorldToViewportPoint(worldMousePositon);
    16.     bool outOfBounds = false;
    17.     if (viewportPos.x < 0)
    18.     {
    19.         viewportPos.x = 0;
    20.         outOfBounds = true;
    21.     }
    22.     else if (viewportPos.x > 1)
    23.     {
    24.         viewportPos.x = 1;
    25.         outOfBounds = true;
    26.     }
    27.  
    28.     if (viewportPos.y < 0)
    29.     {
    30.         viewportPos.y = 0;
    31.         outOfBounds = true;
    32.     }
    33.     else if (viewportPos.y > 1)
    34.     {
    35.         viewportPos.y = 1;
    36.         outOfBounds = true;
    37.     }
    38.  
    39.     if (outOfBounds)
    40.     {
    41.         worldMousePosition = Camera.main.VewportToWorldPoint(viewportPos);
    42.     }
    43.  
    44.     Mouse.current.WarpCursorPosition(Camera.main.WorldToScreenPoint(worldMousePosition));
    45. }
    46.  
    Well turns out I pretty much wrote what you needed.
     
    FeastSC2 likes this.
  6. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    Fortunately it doesn't trigger the event.
    I'll try this right now
     
  7. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    Ok, the mouse stays in world position now: it works :)
    Thanks @Chris-Trueman

    The result is definitely interesting haha.
    It take some getting used to.
    Having the cursor hidden and replaced with "world" cursor helps to ground the cursor in the world and forces the player to think differently about the cursor.

    It's still probably not viable for my game. I'm not sure.
    Anyway, here's the code I'm using if someone's interested. It's straight out of my game so, you'd have to do some small modifications to use it yourself.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.InputSystem;
    3.  
    4. /// <summary>
    5. /// This class replaces the normal cursor as soon as we are in gameplay mode.
    6. /// Menus and UI will use the normal mouse.
    7. /// Compared to normal mouses that move with the screen. This cursor only moves when the user moves the mouse.
    8. /// This way, the camera can freely move and the cursor will remain in its location.
    9. /// The only exception will be whenever the cursor would be out of bounds of the frustrum of the camera.
    10. /// When that is the case, we will make the cursor stick within the bounds of the camera's frustrum.
    11. /// </summary>
    12. public class CursorWorld : Singleton<CursorWorld>, IPausable
    13. {
    14.     [SerializeField] private Transform WorldCursor;
    15.     private Vector2 DeltaMove;
    16.     public Vector3 WorldMousePosition { get; private set; }
    17.  
    18.     void Start()
    19.     {
    20.         Inputer.Controls.Gameplay.MouseMovement.started += OnMouseMovementAction;
    21.         Inputer.Controls.Gameplay.MouseMovement.performed += OnMouseMovementAction;
    22.         Inputer.Controls.Gameplay.MouseMovement.canceled += OnMouseMovementAction;
    23.    
    24.         Pauser.Query.AddMe(this);
    25.     }
    26.  
    27.     private void OnDestroy()
    28.     {
    29.         Inputer.Controls.Gameplay.MouseMovement.started -= OnMouseMovementAction;
    30.         Inputer.Controls.Gameplay.MouseMovement.performed -= OnMouseMovementAction;
    31.         Inputer.Controls.Gameplay.MouseMovement.canceled -= OnMouseMovementAction;
    32.    
    33.         Pauser.Remove(this);
    34.     }
    35.  
    36.     private Vector2 KeepViewPositionWithinScreenBounds(Vector2 _viewportPosition, out bool _isOutOfBounds)
    37.     {
    38.         _isOutOfBounds = false;
    39.         if (_viewportPosition.x < 0)
    40.         {
    41.             _viewportPosition.x = 0;
    42.             _isOutOfBounds = true;
    43.         }
    44.         else if (_viewportPosition.x > 1)
    45.         {
    46.             _viewportPosition.x = 1;
    47.             _isOutOfBounds = true;
    48.         }
    49.         if (_viewportPosition.y < 0)
    50.         {
    51.             _viewportPosition.y = 0;
    52.             _isOutOfBounds = true;
    53.         }
    54.         else if (_viewportPosition.y > 1)
    55.         {
    56.             _viewportPosition.y = 1;
    57.             _isOutOfBounds = true;
    58.         }
    59.         return _viewportPosition;
    60.     }
    61.  
    62.     void OnEnable()
    63.     {
    64.         // On the first frame, we want to set the world mouse position.
    65.         ReadWorldPositionFromCursor();
    66.     }
    67.  
    68.     void ReadWorldPositionFromCursor()
    69.     {
    70.         WorldMousePosition = Inputer.GetScreenToWorldPositionOrAtHeight(Input.mousePosition, TeamManager.ActivePlayCharacter.transform.position);
    71.     }
    72.  
    73.     void Update()
    74.     {
    75.         if (IsPaused) return;
    76.    
    77.         Cursor.visible = false;
    78.         CursorStaysAtSameWorldPosition();
    79.         WorldCursor.position = WorldMousePosition;
    80.     }
    81.  
    82.     void CursorStaysAtSameWorldPosition()
    83.     {
    84.         var mainCamera = Camera.main;
    85.         if (mainCamera == null) return;
    86.  
    87.         var heightOfCursorIfNoGroundIsFound = TeamManager.ActivePlayCharacter.transform.position;
    88.    
    89.         var screenOffset = new Vector3(DeltaMove.x, DeltaMove.y, 0f);
    90.         DeltaMove = Vector2.zero;
    91.  
    92.         var worldPosInScreen = mainCamera.WorldToScreenPoint(WorldMousePosition);
    93.  
    94.         // this is the offset screen movement.
    95.         worldPosInScreen += screenOffset;
    96.         WorldMousePosition = Inputer.GetScreenToWorldPositionOrAtHeight(worldPosInScreen, heightOfCursorIfNoGroundIsFound);
    97.  
    98.         // let's keep the cursor's position within the viewport's bounds.
    99.         var worldPosInViewport = mainCamera.WorldToViewportPoint(WorldMousePosition);
    100.         worldPosInViewport = KeepViewPositionWithinScreenBounds(worldPosInViewport, out var isOutOfBounds);
    101.  
    102.         if (isOutOfBounds)
    103.         {
    104.             var boundScreenPoint = mainCamera.ViewportToScreenPoint(worldPosInViewport);
    105.             WorldMousePosition = Inputer.GetScreenToWorldPositionOrAtHeight(boundScreenPoint, heightOfCursorIfNoGroundIsFound);
    106.         }
    107.    
    108.         // we now have the final world mouse position.
    109.         // we can transform it to a screen position to become the cursor.
    110.         var finalScreenPoint = mainCamera.WorldToScreenPoint(WorldMousePosition);
    111.         Mouse.current.WarpCursorPosition(finalScreenPoint);
    112.     }
    113.  
    114.     private void OnMouseMovementAction(InputAction.CallbackContext _obj)
    115.     {
    116.         if (CameraManager.Single.IsPressingRotateCameraWithMouse)
    117.         {
    118.             DeltaMove = Vector2.zero;
    119.         }
    120.         else
    121.         {
    122.             DeltaMove += _obj.ReadValue<Vector2>();
    123.         }
    124.     }
    125.  
    126.     private bool IsPaused;
    127.     public PauseObject PauseObj { get; set; }
    128.     public void OnPaused(PauseType _type)
    129.     {
    130.         IsPaused = true;
    131.         Cursor.visible = true;
    132.         Debug.Log("cursor is paused! We entered a menu.");
    133.     }
    134.  
    135.     public void OnUnpaused(PauseType _type)
    136.     {
    137.         IsPaused = false;
    138.         ReadWorldPositionFromCursor();
    139.     }
    140. }
     
    Last edited: Apr 8, 2022
    Chris-Trueman likes this.