Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

How to create a mouse-like cursor to be controlled by the keyboard or a joystick

Discussion in 'Unity UI (uGUI) & TextMesh Pro' started by Kaze_Senshi, Feb 21, 2015.

  1. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
    Hello here guys I want to create a cursor that is controlled by keyboard or a joystick, not by the mouse, like that hand-cursor used in Super Smash Bros character menu.
    Also if possible I want to get more than one cursor in the screen, like 3 players 3 hand-cursors for each player and it goes on. Each cursor should interact with the UI like the mouse cursor and being compatible with the UI's event manager events.
    So I did a search on Google and found a lot of material and then... I got lost. There are a lot of material talking about it but it seems to be too abstract, at least for me, none of them show the custom input system being integrated with the Scene's game Objects. Some links that I found:

    Input Controllers and uGUI: http://seanodowd.me/unity3d-input-and-ugui/

    Raycasting to create interaction between itens and inventory slots: http://forum.unity3d.com/threads/raycast-towards-ui-elements.284264/

    More Raycasting into GUI: http://forum.unity3d.com/threads/raycast-into-gui.263397/

    So in my head I think that I have to follow the following steps:

    1- Create the hand-cursor game object
    2- Create a script to control the cursor's movement like a common player moved by keyboard
    3- Create a script to handle a game object like a mouse cursor
    =3.1- Be able to convert 3D-space coords to UI canvas coords
    =3.2- Be able to get keyboard inputs as mouse clicks
    =3.3- Send these data to Unity's UI Event System to simulate mouse or touch interaction
    4- Attach the script from step 3 to the hand
    5- Profit

    Then my question is, how should I proceed to implement step 3? What do I really need to implement and what is already ready to use in Unity3D? Thanks for the attention :)
     
  2. rakkarage

    rakkarage

    Joined:
    Feb 3, 2014
    Posts:
    682
  3. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
  4. rakkarage

    rakkarage

    Joined:
    Feb 3, 2014
    Posts:
    682
    ya but it applies it on all platforms (even non cursor platforms, like ios and android) (seems like a bug?)
    but ya idk if there is some way to fake position that cursor, i guess not sorry idk
     
  5. Feaver1968

    Feaver1968

    Joined:
    Nov 16, 2014
    Posts:
    70
    I think you could start with a canvas with Render Mode set to Screen Space - Camera http://docs.unity3d.com/Manual/class-Canvas.html. The 3d hand would be closer to the camera and draw above the UI. I haven't tried out this type of UI myself, but I think you would need to do some kind of raycasting to do selection and interaction stuff.
     
  6. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
    This seems to be interesting, when I find more information about how to raycast from a 3D object I'll post it here.
     
  7. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
    Ookay today I think that I advanced a bit. Here is the code that I have until now:

    Code (csharp):
    1.  
    2. /*
    3.    Interesting links
    4.    https://gist.github.com/stramit/c98b992c43f7313084ac
    5.    https://gist.github.com/flarb/052467190b84657f10d2
    6.    http://forum.unity3d.com/threads/custom-cursor-how-can-i-simulate-mouse-clicks.268513/
    7. */
    8.  
    9. using UnityEngine;
    10. using System.Collections;
    11. using System.Collections.Generic;
    12. using UnityEngine.EventSystems;
    13.  
    14. // Attach this script on your canvas's EventSystem game object
    15. // For while it glitches the StandaloneInputModule if both are active at the same time
    16.  
    17. public class CursorInputModule : BaseInputModule {
    18.  
    19.    // Cursor object is some 3D-object inside of the canvas
    20.    // It moves only on X and Y axis using a common player movement script
    21.    public GameObject cursorObject = null;
    22.  
    23.    // The same event system used on the Canvas
    24.    public EventSystem eventSystem = null;
    25.    //private GameObject targetObject = null;
    26.  
    27.    // Use this for initialization
    28.    void Start () {
    29.      base.Start ();
    30.      if( cursorObject == null || eventSystem == null )
    31.      {
    32.        Debug.LogError( "Set the game objects in the cursor module." );
    33.        GameObject.Destroy( gameObject );
    34.      }
    35.    }
    36.  
    37.    // Process is called once per tick
    38.    public override void Process ()
    39.    {
    40.      // Converting the 3D-coords to Canvas-coords (it is giving wrong results, how to do this??)
    41.      Vector3 screenPos = Camera.main.WorldToScreenPoint( cursorObject.transform.position );
    42.      List<RaycastResult> rayResults = new List<RaycastResult>();
    43.    
    44.      float scaleFactorX = 1280.0f / Screen.width;
    45.      float scaleFactorY = 720.0f / Screen.height;
    46.      //float scaleFactorX = canvasScaler.referenceResolution.x / Screen.width;
    47.      //float scaleFactorY = canvasScaler.referenceResolution.y / Screen.height;
    48.  
    49.      Vector2 rectPosition =  new Vector2( screenPos.x * scaleFactorX, screenPos.y * scaleFactorY );
    50.  
    51.      // Raycasting
    52.      PointerEventData pointer = new PointerEventData(eventSystem);
    53.      pointer.position = rectPosition;
    54.      eventSystem.RaycastAll( pointer, rayResults );
    55.  
    56.      if( rayResults.Count > 0 )
    57.      {
    58.        for( int i = 0; i < rayResults.Count; i++ )
    59.        {
    60.          Debug.Log ( rayResults[i].gameObject.name );
    61.        }
    62.      }
    63.    }
    64. }
    65.  

    The debug log seems to print correctly the canvas elements names but it seems to not be exactly aligned with my 3d object, so I have the following questions:

    1- How to do the World-space to Canvas-space convertion?
    2- Is there a way to debug the canvas raycasts to see if I am hitting the correct things?
    3- Do someone knows if Unity3D UI has some intern vector of selected objects or I have to manage it on my InputModule script?
     
  8. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    457
    I have created a similar Input module so I could detect UI elements in a world canvas under a reticle(an arbitrary point on the screen more so.)

    It doesn't play well with the default StandaloneInputModule as it uses the mouse and conflicts so it needs to be removed or disabled. It works with what ever is setup as Submit in the input settings to "click" the button.

    You need to modify the position of your cursor so it is in Screen space to work, so try this. It doesn't use canvas space so forget about that.

    http://forum.unity3d.com/threads/use-reticle-like-mouse-for-worldspace-uis.295271/#post-1961818
    This might not be bug free. I haven't been working on that project, so it hasn't been tested fully.
     
  9. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
    Thank you Chris, with your tip I was able to do the following code that works for a single cursor, only for single clicks:

    Code (csharp):
    1.  
    2. /*
    3.    Interesting links
    4.    https://gist.github.com/stramit/c98b992c43f7313084ac
    5.    https://gist.github.com/flarb/052467190b84657f10d2
    6.    http://forum.unity3d.com/threads/custom-cursor-how-can-i-simulate-mouse-clicks.268513/
    7. */
    8.  
    9. using UnityEngine;
    10. using System.Collections;
    11. using System.Collections.Generic;
    12. using UnityEngine.EventSystems;
    13.  
    14. // Attach this script on your canvas's EventSystem game object
    15. // For while it glitches the StandaloneInputModule if both are active at the same time
    16.  
    17. public class CursorInputModule : BaseInputModule {
    18.  
    19.    // Cursor object is some 3D-object inside of the canvas
    20.    // It moves only on X and Y axis
    21.    public GameObject cursorObject = null;
    22.  
    23.    // The same event system used on the Canvas
    24.    public EventSystem eventSystem = null;
    25.  
    26.    private Vector2 auxVec2;
    27.  
    28.    // Use this for initialization
    29.    void Start () {
    30.      base.Start ();
    31.      if( cursorObject == null || eventSystem == null )
    32.      {
    33.        Debug.LogError( "Set the game objects in the cursor module." );
    34.        GameObject.Destroy( gameObject );
    35.      }
    36.    }
    37.  
    38.    // Process is called once per tick
    39.    public override void Process ()
    40.    {
    41.      // Converting the 3D-coords to Screen-coords
    42.      Vector3 screenPos = Camera.main.WorldToScreenPoint( cursorObject.transform.position );
    43.      List<RaycastResult> rayResults = new List<RaycastResult>();
    44.  
    45.      auxVec2.x = screenPos.x;
    46.      auxVec2.y = screenPos.y;
    47.  
    48.      // Raycasting
    49.      PointerEventData pointer = new PointerEventData(eventSystem);
    50.      pointer.position = auxVec2;
    51.      eventSystem.RaycastAll( pointer, rayResults );
    52.    
    53.      // Cursor click
    54.      if( Input.GetButtonDown( "Jump" ) )
    55.      {
    56.        if( rayResults.Count > 0 )
    57.        {  
    58.          Debug.Log ( Time.time + "s Number of objects : "  + rayResults.Count );
    59.          RaycastResult raycastResult = FindFirstRaycast ( rayResults );
    60.          pointer.pointerCurrentRaycast = raycastResult;
    61.          ExecuteEvents.ExecuteHierarchy ( raycastResult.gameObject, pointer, ExecuteEvents.submitHandler );
    62.        }
    63.      }
    64.    }
    65. }
    66.  
    Now I have to solve 2 problems:

    1- I want to simulate the other events like OnPointerEnter and OnPointerExit to use elements like dropdrown menus, so I was thinking to have an internal List of the selected GameObjects that are being hit by the raycast and remove them when there is no more rays hitting them. However I noticed that if you have an hierachy of UI objects and you are hovering the child object, the Unity UI system considers that you stays inside the parent one too (even if the parent isn't intersecting the child element). Do you know some smart way of implementing this system?

    2- To simulate player input I need to identify who clicked where. So the PointerEventData should have an extra field. Something like this:

    Code (csharp):
    1.  
    2. public class PlayerPointerEventData : PointerEventData {
    3.    public int playerNumber = 0;
    4. }
    5.  
    Then I should attach some script like this one to the button to make it work only for some specific player:

    Code (csharp):
    1.  
    2. public class PointerEventsController : MonoBehaviour,IPointerEnterHandler, IPointerExitHandler {
    3.  
    4.     public int buttonOwnedByPlayer= 0;
    5.     public void OnPointerEnter(PointerEventData eventData)
    6.     {
    7.            PlayerPointerEventData  pped = (PlayerPointerEventData ) eventData;
    8.            if( buttonOwnedByPlayer != pped.playerNumber ) return;
    9.            else
    10.                      /* normal flow */
    11.     }
    12. }
    13.  
    Am I going through the right path?
     
  10. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    457
    That is your internal list. If you use PointerInputModule instead of BaseInputModule to inherit from, it already has one for you: m_RaycastResultCache.

    I think that the whole parent being hovered, as well as the child, is a bug or working as intended.

    PointerEventData needs to be per pointer and persistent as long as the module is active and the pointer associated with it is being used. So you need to use different id's to track them. You could then associate a player with a pointerId seeing as you need one per player anyway, and not need to worry about making another PointerEventData class, with another id for nothing. Your new button code would look like this.
    Code (CSharp):
    1. public class PointerEventsController : [URL='http://unity3d.com/support/documentation/ScriptReference/30_search.html?q=MonoBehaviour']MonoBehaviour[/URL],IPointerEnterHandler, IPointerExitHandler
    2. {
    3.     public int buttonOwnedByPlayer = 0
    4.     public void OnPointerEnter(PointerEventData eventData)
    5.     {
    6.         if (eventData.pointerId == buttonOwnedByPlayer) return;
    7.         else
    8.             //normal flow
    9.     }
    10. }
    You can inherit from PointerInputModule and use its built in pointer tracking to make it much easier on you.

    Look at the StandAloneInputModule(outdated version, I can't find a newer one.) It may help you understand how it works better.
     
  11. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
    This m_RaycastResultCache is just an usual list right? I would need to control the events of entering and exiting anyway right?


    I think that it is supposed to work this way, it is useful to create things like dropdown menus.


    So I need to create a new PointerEventData only on the creation of the InputModule instead of creating it every frame?

    I took a look at this new version code but it doesn't look too useful because it uses a lot of Unity3d internal functions like
    GetMousePointerEventData, ProcessMove and HandlePointExitAndEnter that have a mediocre documentation on Unity3D's site.
     
  12. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    457
    Yes.
    That is done in the module using HandlePointExitAndEnter.

    Yes, and when you use GetPointerData it creates one if one doesn't exist for the pointerId you provide. If one does exist it returns the one you want.

    Those functions are internal to the input modules of which you can override for your own means.
     
  13. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
    Ahh I got it, while trying to look for information about those function I discovered that you can see their implementation by clicking with the left button and click to see their definition. With that I noticed that ProcessMove calls only HandlePointExitAndEnter based on the current raycast.

    Code (csharp):
    1.  
    2. protected virtual void ProcessMove (PointerEventData pointerEvent)
    3. {
    4.    GameObject gameObject = pointerEvent.pointerCurrentRaycast.gameObject;
    5.    base.HandlePointerExitAndEnter (pointerEvent, gameObject);
    6. }
    7.  
    Then I adapted my code to call this function and it worked amazing well, now it supports clicks, OnPointerEnter and OnPointerExit. I think that it is enough to me.

    Code (csharp):
    1.  
    2. public class CursorInputModule : PointerInputModule  {
    3.  
    4.    // Cursor object is some 3D-object inside of the canvas
    5.    // It moves only on X and Y axis
    6.    public GameObject cursorObject = null;
    7.  
    8.    // The same event system used on the Canvas
    9.    public EventSystem eventSystem = null;
    10.    public int playerNumber = 0;
    11.  
    12.    private Vector2 auxVec2;
    13.    private PointerEventData pointer;
    14.  
    15.    // Use this for initialization
    16.    void Start () {
    17.      base.Start ();
    18.      if( cursorObject == null || eventSystem == null )
    19.      {
    20.        Debug.LogError( "Set the game objects in the cursor module." );
    21.        GameObject.Destroy( gameObject );
    22.      }
    23.  
    24.      pointer = new PointerEventData(eventSystem);
    25.      pointer.pointerId = playerNumber;
    26.    }
    27.    
    28.    // Process is called once per tick
    29.    public override void Process ()
    30.    {
    31.      // Converting the 3D-coords to Screen-coords
    32.      Vector3 screenPos = Camera.main.WorldToScreenPoint( cursorObject.transform.position );
    33.      List<RaycastResult> rayResults = new List<RaycastResult>();
    34.  
    35.      auxVec2.x = screenPos.x;
    36.      auxVec2.y = screenPos.y;
    37.  
    38.      // Raycasting
    39.      pointer.position = auxVec2;
    40.      eventSystem.RaycastAll( pointer, this.m_RaycastResultCache );
    41.      RaycastResult raycastResult = FindFirstRaycast ( this.m_RaycastResultCache );
    42.      pointer.pointerCurrentRaycast = raycastResult;
    43.      this.ProcessMove( pointer );
    44.  
    45.      pointer.clickCount = 0;
    46.      // Cursor click
    47.      if( Input.GetButtonDown( "Jump" ) )
    48.      {
    49.        pointer.pressPosition = auxVec2;
    50.        pointer.clickTime = Time.unscaledTime;
    51.        pointer.pointerPressRaycast = raycastResult;
    52.  
    53.        pointer.clickCount = 1;
    54.        pointer.eligibleForClick = true;
    55.  
    56.        if( this.m_RaycastResultCache.Count > 0 )
    57.        {  
    58.          Debug.Log ( Time.time + "s Number of objects : "  + rayResults.Count );
    59.  
    60.          pointer.selectedObject = raycastResult.gameObject;
    61.          pointer.pointerPress = ExecuteEvents.ExecuteHierarchy ( raycastResult.gameObject, pointer, ExecuteEvents.submitHandler );
    62.          pointer.rawPointerPress = raycastResult.gameObject;
    63.        }
    64.        else
    65.        {
    66.          pointer.rawPointerPress = null;
    67.        }
    68.      }
    69.  
    70.      else
    71.      {
    72.        pointer.clickCount = 0;
    73.        pointer.eligibleForClick = false;
    74.        pointer.pointerPress = null;
    75.        pointer.rawPointerPress = null;
    76.      }
    77.    }
    78. }
    79.  
    Now I think that I'll need an array of PointerEventData and call each of these functions multiple times for each cursor. Also I think that I can optimize it by don't raycasting when the cursor isn't moving. Do you have some suggestion or see something wrong with my function?
     
    Thomas-Mountainborn likes this.
  14. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    457
    Get rid of this line.
    You are not using it anymore and its creating a new object every frame which will stress the GC for nothing.

    You don't need to store this.
    Instead use GetPointerData() and store the pointerId for the cursor. The event data is already stored for you in a list of PointerEventData. When you call it just make a local variable in the function for reference.
    Something like this.
    Code (CSharp):
    1. public override void Process ()
    2. {
    3.     PointerEventData pData;
    4.     GetPointerData(storedPointerId, out pData, true);
    5.  
    6.    //process the event data.
    7. }
    Wouldn't take too much to add multiple cursors to this. Make a list of pointerId's to loop through, or a list of players that have a unique pointerId so you can get the position of each players cursor, and apply that to the event data.
     
  15. Kaze_Senshi

    Kaze_Senshi

    Joined:
    Feb 19, 2012
    Posts:
    243
    Roger that, I followed your suggestions and make it work with multiple cursors:

    Code (csharp):
    1.  
    2.  
    3. using UnityEngine;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6. using UnityEngine.EventSystems;
    7.  
    8. // Attach this script on your canvas's EventSystem game object
    9. // It glitches the StandaloneInputModule if both are active at the same time
    10.  
    11. public class CursorInputModule : PointerInputModule  {
    12.    
    13.    // The same event system used on the Canvas
    14.    public EventSystem eventSystem = null;
    15.  
    16.    // A list of cursor objects inside the canvas
    17.    // It moves only on X and Y axis
    18.    public List<GameObject> cursorObjects ;
    19.  
    20.    private Vector2 auxVec2;
    21.    private PointerEventData pointer;
    22.  
    23.    // Use this for initialization
    24.    void Start () {
    25.      base.Start ();
    26.      if( cursorObjects == null || eventSystem == null )
    27.      {
    28.        Debug.LogError( "Set the game objects in the cursor module." );
    29.        GameObject.Destroy( gameObject );
    30.      }
    31.    }
    32.    
    33.    // Process is called once per tick
    34.    public override void Process ()
    35.    {
    36.      // For each player
    37.      for( int i = 0; i < cursorObjects.Count; i++ )
    38.      {
    39.        // Getting objects related to player (i+1)
    40.        GameObject cursorObject = cursorObjects[i];
    41.        GetPointerData( i, out pointer, true );
    42.  
    43.        // Converting the 3D-coords to Screen-coords
    44.        Vector3 screenPos = Camera.main.WorldToScreenPoint( cursorObject.transform.position );
    45.  
    46.        auxVec2.x = screenPos.x;
    47.        auxVec2.y = screenPos.y;
    48.  
    49.        // Raycasting
    50.        pointer.position = auxVec2;
    51.        eventSystem.RaycastAll( pointer, this.m_RaycastResultCache );
    52.        RaycastResult raycastResult = FindFirstRaycast ( this.m_RaycastResultCache );
    53.        pointer.pointerCurrentRaycast = raycastResult;
    54.        this.ProcessMove( pointer );
    55.  
    56.        pointer.clickCount = 0;
    57.        // Cursor click - adapt for detect input for player (i+1) only
    58.        if( Input.GetButtonDown( "Jump" ) )
    59.        {
    60.          pointer.pressPosition = auxVec2;
    61.          pointer.clickTime = Time.unscaledTime;
    62.          pointer.pointerPressRaycast = raycastResult;
    63.  
    64.          pointer.clickCount = 1;
    65.          pointer.eligibleForClick = true;
    66.  
    67.          if( this.m_RaycastResultCache.Count > 0 )
    68.          {  
    69.            //Debug.Log ( Time.time + "s Number of objects : "  + rayResults.Count );
    70.            pointer.selectedObject = raycastResult.gameObject;
    71.            pointer.pointerPress = ExecuteEvents.ExecuteHierarchy ( raycastResult.gameObject, pointer, ExecuteEvents.submitHandler );
    72.            pointer.rawPointerPress = raycastResult.gameObject;
    73.          }
    74.          else
    75.          {
    76.            pointer.selectedObject = null;
    77.            pointer.pointerPress = null;
    78.            pointer.rawPointerPress = null;
    79.          }
    80.        } // if( Input.GetButtonDown( "Jump" ) )
    81.  
    82.        else
    83.        {
    84.          pointer.clickCount = 0;
    85.          pointer.eligibleForClick = false;
    86.          pointer.pointerPress = null;
    87.          pointer.rawPointerPress = null;
    88.        }
    89.      } // for( int i; i < cursorObjects.Count; i++ )
    90.    }
    91. }
    92.  
    93.  
    It is working nice, you can even see each pointer's information on Inspector.
    ScreenShot.jpg

    Thank you Chris :)
     
    Lentaq, jjbish and Pamagon like this.
  16. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    457
    Nice! Your welcome, always glad to help.
     
  17. Mr.Stein

    Mr.Stein

    Joined:
    Dec 4, 2013
    Posts:
    169
  18. Carlotes247

    Carlotes247

    Joined:
    Jun 12, 2014
    Posts:
    8
    Thank you very much for this! I am adapting a Wiimote to use the eventsystem in unity and I was having some problems to fully understand how the process worked. After reading everything here, it looks much clearer :) :):)
     
  19. rowena_indiesoft

    rowena_indiesoft

    Joined:
    Jun 27, 2015
    Posts:
    5
    @Kaze_Senshi ... Im looking to do something similiar for my menu system but I only require input from one player.. anyhow I'd like to see a more detail look at your solution if you can upload a copy of it, I did tried to recreate it from the chunks of code pasted but with no luck...
     
  20. Carlotes247

    Carlotes247

    Joined:
    Jun 12, 2014
    Posts:
    8
    @rowena_indiesoft how is your code looking? I managed to get to work my wiimote code using this thread and the official UI source code on BitBucket. If you want you can upload your code and I can have a look at it ;)
     
    rowena_indiesoft likes this.
  21. jjbish

    jjbish

    Joined:
    Jun 16, 2016
    Posts:
    4
    Thanks sooooo much for this. Very helpful thread!
     
  22. bonkersdeluxe

    bonkersdeluxe

    Joined:
    Sep 13, 2013
    Posts:
    3
    Hi Kaze_Senshi and Chris-Trueman,
    Thank you! Works great.
    But i need help for implementation drag and drop.
    I dont´t understand it. at my scenario kecode.b ist mouseclick. But when i hold down button it is a short click. How get i get a button press?

    if (inputkey.getbutton(kecode.B))

    With GetButotnDown i understand that is triggered once. I have an Invetar that supports drag and drop of items.
    It would be fine to get it work.
    Sorry for my bad english.
    I hope someone can help me.

    Thank you!
     
  23. Lentaq

    Lentaq

    Joined:
    Apr 8, 2015
    Posts:
    56
    I found this thread an incredibly useful starting point, despite its age, so I figure I'll chip in and share my own learning experiments with this. Ironically, I wasn't even doing anything multiplayer related, but I got to tinkering and just kept messing with stuff. I was really only trying to set up a reticle cursor that worked with World Space UIs, but kept going with it.
    Using a 2017 version of Unity, fyi.

    Warning: My script is much longer than @Kaze_Senshi 's since I added some other features(listed below) and also left in some inactive debug checks, etc.

    A few additions:
    • His script didn't allow for dragging UI sliders and such, I fixed that for my use
    • I ripped out the "Cursor.lockState == CursorLockMode.Locked" bool checks in ProcessMove and ProcessDrag so that I could use these properly with cursor in a locked state. I did this since I was actually making a single-player project and wanted to be able to toggle lock the reticle in the center.
    • Had to hack something together so dragging would work with a locked cursor, as well.
    • Set up an input button array. Note: Required for couch co-op unless you want all players clicking at the same time. This is just a simple example, however.
    • Set up array of timers for multi-clicks / double-clicks for each player. Timeout tweakable in editor.
    • Being able to click on UI elements from across the scene was driving me insane, so I set a distance limit. Range limit tweakable in editor.
    • Cleaned up some of those error messages, etc.
    • Cached camera so that it wasn't doing that horrible Camera.main call every Process().
    Also, check out: https://forum.unity.com/threads/fake-mouse-position-in-4-6-ui-answered.283748/
    @EmmaEwert has a nice, much simpler hack for dragging with locked cursors, as well.

    As seen in the comments, split-screen cameras and virtual cursor lockstates for each player, etc., are on my TODO list, but I don't plan to come back and update this post. Just thought I'd share what I learned.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine.EventSystems;
    5. // Attach this script on your canvas's EventSystem game object
    6. // It glitches the StandaloneInputModule if both are active at the same time
    7. public class ReticleCursorInputModule : PointerInputModule
    8. {
    9.     // The same event system used on the Canvas
    10.     public new EventSystem eventSystem = null;
    11.  
    12.     //TODO: Setup multiple cameras and camera switching - Need an array of cameras for splitscreen games
    13.     // Cache Camera in place of Camera.main call
    14.     public Camera eventCamera;
    15.  
    16.     // A list of cursor objects inside the canvas
    17.     // It moves only on X and Y axis
    18.     public List<GameObject> cursorObjects;
    19.  
    20.     // Multiple players on same system need multiple
    21.     // buttons or all player cursors click at the same time
    22.     // Names of buttons in Input settings in Editor
    23.     private string[] submitButtons = {
    24.         "P1Click",
    25.         "P2Click",
    26.         "P3Click",
    27.         "P4Click"
    28.     };
    29.  
    30.     private Vector2 auxVec2 = Vector2.zero;
    31.     private Vector2 dragLockedPointerPos = Vector2.zero;
    32.     private Vector3 screenPos = Vector3.zero;
    33.     private Vector3 draggedObjectPos = Vector3.zero;
    34.  
    35.     private bool cursorLocked = true;
    36.     private float[] lastClickTime = new float[4];
    37.  
    38.     [SerializeField]
    39.     private float multipleClicksTimeout = 0.5f;
    40.     [SerializeField]
    41.     private float maxInteractDistance = 1.5f;
    42.  
    43.     private PointerEventData pointer;
    44.     private RaycastResult raycastResult;
    45.  
    46.     // Use this for initialization
    47.     protected override void Start()
    48.     {
    49.         base.Start();
    50.         if (cursorObjects == null || eventSystem == null)
    51.         {
    52.             Debug.LogError("Set the game objects in the cursor module.");
    53.             GameObject.Destroy(gameObject);
    54.         }
    55.         if (eventCamera == null)
    56.         {
    57.             Debug.LogWarning("Event Camera unassigned.  Camera.main will be used instead.");
    58.             eventCamera = Camera.main;
    59.         }
    60.     }
    61.  
    62.     private bool ShouldStartDrag(Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold)
    63.     {
    64.         if (!useDragThreshold)
    65.             return true;
    66.  
    67.         return (pressPos - currentPos).sqrMagnitude >= threshold * threshold;
    68.     }
    69.  
    70.     protected override void ProcessDrag(PointerEventData pointerEvent)
    71.     {
    72.         // Needed to override to cut out CursorLockMode.Locked check, etc. from base code
    73.         if (pointerEvent.pointerDrag == null)
    74.             return;
    75.  
    76.         if (!pointerEvent.dragging
    77.             && ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
    78.         {
    79.             ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
    80.             pointerEvent.dragging = true;
    81.         }
    82.  
    83.         // Drag notification
    84.         if (pointerEvent.dragging)
    85.         {
    86.             // Before doing drag we should cancel any pointer down state
    87.             // And clear selection!
    88.             if (pointerEvent.pointerPress != pointerEvent.pointerDrag)
    89.             {
    90.                 ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
    91.  
    92.                 pointerEvent.eligibleForClick = false;
    93.                 pointerEvent.pointerPress = null;
    94.                 pointerEvent.rawPointerPress = null;
    95.             }
    96.             ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);
    97.         }
    98.     }
    99.  
    100.     protected override void ProcessMove(PointerEventData pointerEvent)
    101.     {
    102.         var targetGO = pointerEvent.pointerCurrentRaycast.gameObject;
    103.         HandlePointerExitAndEnter(pointerEvent, targetGO);
    104.     }
    105.  
    106.     // Process is called once per tick
    107.     public override void Process()
    108.     {
    109.         // For each player
    110.         for (int i = 0; i < cursorObjects.Count; i++)
    111.         {
    112.             // Getting objects related to player (i+1)
    113.             GameObject cursorObject = cursorObjects[i];
    114.             GetPointerData(i, out pointer, true);
    115.  
    116.             if (Cursor.lockState == CursorLockMode.None || Cursor.lockState == CursorLockMode.Confined)
    117.             {
    118.                 cursorLocked = false;
    119.             }
    120.  
    121.             //TODO: Setup array of individual virtual lockstates for multiple players in split-screen
    122.             if (!cursorLocked && Cursor.lockState == CursorLockMode.Locked)
    123.             {
    124.                 cursorLocked = true;
    125.  
    126.                 //Actual ScreenPos never changes while Cursor is locked in center, so only need to set this one time per lock
    127.                 //"cursorLocked" bool prevents this from being processed every frame while locked
    128.                 screenPos = eventCamera.WorldToScreenPoint(cursorObject.transform.position);
    129.  
    130.                 auxVec2.x = screenPos.x;
    131.                 auxVec2.y = screenPos.y;
    132.             }
    133.  
    134.             // If using cursor locking in game, this translates from locked cursor's pos to pos of drag item to enable dragging,
    135.             // since by default dragging with a locked cursor is impossible.  Reticle stays in center of screen.
    136.             if (cursorLocked && pointer.pointerDrag)
    137.             {
    138.                 draggedObjectPos = eventCamera.WorldToScreenPoint(pointer.pointerDrag.transform.position);
    139.  
    140.                 dragLockedPointerPos = new Vector2(auxVec2.x - draggedObjectPos.x, auxVec2.y - draggedObjectPos.y);
    141.  
    142.                 pointer.position = auxVec2 + dragLockedPointerPos;
    143.                 //Debug.Log("RealPos: " + auxVec2 + " TranslatedPos: " + pointer.position);
    144.             }
    145.             else
    146.             {
    147.                 screenPos = eventCamera.WorldToScreenPoint(cursorObject.transform.position);
    148.                 auxVec2.x = screenPos.x;
    149.                 auxVec2.y = screenPos.y;
    150.  
    151.                 pointer.position = auxVec2;
    152.             }
    153.  
    154.             // Raycasting
    155.             eventSystem.RaycastAll(pointer, this.m_RaycastResultCache);
    156.             raycastResult = FindFirstRaycast(this.m_RaycastResultCache);
    157.             pointer.pointerCurrentRaycast = raycastResult;
    158.  
    159.             this.ProcessDrag(pointer);
    160.             this.ProcessMove(pointer);
    161.  
    162.             //if (raycastResult.distance > 0.0f)
    163.             //{
    164.             //    Debug.Log(raycastResult.distance);
    165.             //}
    166.  
    167.             // This ends the Update for this frame if the object hit by raycast is beyond user-defined interact range
    168.             // By default Graphics Raycaster of canvases has unlimited(?) range. This corrects that for world space canvases.
    169.             if (raycastResult.distance > maxInteractDistance)
    170.             {
    171.                 //Debug.Log("Distance too great");
    172.                 return;
    173.             }
    174.  
    175.             if (Input.GetButtonDown(submitButtons[i]))
    176.             {
    177.                 pointer.pressPosition = auxVec2;
    178.                 pointer.clickTime = Time.unscaledTime;
    179.                 pointer.pointerPressRaycast = raycastResult;
    180.                 pointer.eligibleForClick = true;
    181.  
    182.                 //-------------------------------------------------------------
    183.                 //Setup to allow for double-click / multiple clicks.  Interval between clicks user-defined
    184.                 float timeBetweenClicks = pointer.clickTime - lastClickTime[i];
    185.  
    186.                 if (timeBetweenClicks > multipleClicksTimeout)
    187.                 {
    188.                     pointer.clickCount = 0;
    189.                 }
    190.  
    191.                 pointer.clickCount++;
    192.                 lastClickTime[i] = Time.unscaledTime;
    193.              
    194.                 //Debug.Log("ClickCount: " + pointer.clickCount + " -- " + lastClickTime[i]);
    195.                 //-------------------------------------------------------------
    196.  
    197.                 if (this.m_RaycastResultCache.Count > 0)
    198.                 {
    199.                     //Debug.Log ( Time.time + "s Number of objects : "  + rayResults.Count );
    200.                     pointer.selectedObject = raycastResult.gameObject;
    201.                     pointer.pointerPress = raycastResult.gameObject;
    202.                     pointer.rawPointerPress = raycastResult.gameObject;
    203.                     pointer.pointerDrag = ExecuteEvents.ExecuteHierarchy(raycastResult.gameObject, pointer, ExecuteEvents.pointerDownHandler);
    204.  
    205.                     dragLockedPointerPos = pointer.position;
    206.                 }
    207.                 else
    208.                 {
    209.                     pointer.selectedObject = null;
    210.                     pointer.pointerPress = null;
    211.                     pointer.rawPointerPress = null;
    212.                 }
    213.             }
    214.             else if (Input.GetButtonUp(submitButtons[i]))
    215.             {
    216.                 pointer.pointerPress = ExecuteEvents.ExecuteHierarchy(raycastResult.gameObject, pointer, ExecuteEvents.submitHandler);
    217.  
    218.                 pointer.pointerPress = null;
    219.                 pointer.rawPointerPress = null;
    220.                 pointer.pointerDrag = null;
    221.                 pointer.dragging = false;
    222.                 pointer.eligibleForClick = false;
    223.             }
    224.         }
    225.     }
    226. }
     
  24. ashulgach

    ashulgach

    Joined:
    Aug 8, 2013
    Posts:
    27
    Last edited: Aug 14, 2019 at 2:44 AM
  25. danbg

    danbg

    Joined:
    May 1, 2017
    Posts:
    3
    I tried to use your scripts in a multidisplay setup and they are not working in the second or third, just in the first... :(

    Anyone knows how to create cursors with a selectable display? It seems that it is very difficult to get the UI working with a second or third display... Thanks
     
  26. ashulgach

    ashulgach

    Joined:
    Aug 8, 2013
    Posts:
    27
    Are you trying to use one cursor across multiple monitors?