Table of Contents

Custom Cursors are a great way to add a level of professionalism and polish to any project. A Cursor created specifically for a project can be used for player feedback and fits into the project universe better than the normal Mouse icon in most situations. There are several methods that can be used to add this functionality, all of which come with their own strengths and weaknesses.

NOTE: Recommended Reading This section covers topics that may not have been encountered yet. To learn or review those topics, please see: Mouse Input and {icon university}[[zilch_engine_documentation/zilch_editor_documentation/ZilchManual/Graphics/CamerasViewportsRenderers/|Cameras, Viewports and Renderers]]

Using a Custom Cursor

Hiding the Mouse

Pros Cons
Quick to implement Mouse Icon can leave window bounds
Retains all mouse functionality Will become visible if it does

The first option involves making the Mouse Cursor invisible while not affecting any of its functionality. This has two easy-to-see benefits: it's quick to implement and all functionality is retained.

class HiddenMouseExample : NadaComponent
{
     [Dependency]
     var Transform : Transform;
     function Initialize(init : CogInitializer)
    {
        // Set the Mouse to invisible
        Zilch.Mouse.Cursor = Cursor.Invisible;
         // Connect to MouseMove Event
        Zilch.Connect(this.Space, Events.MouseMove, this.OnMouseMove);
    }
// Code continued below

This would be the start of a component attached to the object that will act as the Custom Cursor. After making sure the object has a transform Component using the Dependency - Component using the Dependency attributes, it then sets the Mouse Cursor to be invisible. The final part of the code-block connects to the MouseMove event, which is fleshed out in the following code-block:

    // continued from above
        function OnMouseMove(event : ViewportMouseEvent)
        {
            // Get mouse WorldPosition on the Z-Plane at depth 0
            var mousePosition = event.ToWorldZPlane(0);
            // Set the MouseCursor Objects position to the Mouse position
            this.Transform.Translation = mousePosition;
        }
    }

The OnMouseMove function will fire every time the Mouse is moved, keeping the custom cursor in the same location as the invisible Mouse. It could also connect to other Mouse Events, such as MouseDown or MouseUp, in order to interact with these events visually (e.g., by changing the SpriteSource of the object). All other working functionality can then be given to the Mouse itself, so that it can react with objects that have the Reactive Component attached the them.

A large drawback to using this method is the fact that it allows the Mouse icon to leave the boundaries of the Game window, which will make the Mouse visible and not hide it again even if the Game is brought back into focus. Below is an example of how a Sprite with this script attached would act:

MouseCursorMove

It would also be possible to set the Mouse invisible from the LogicUpdate event, keeping the cursor invisible in within the editor even when leaving the game window.

Locking the Mouse

Pros Cons
Mouse stays within widow bounds Loses most mouse functionality
Allows use of multiple Spaces More complex the hiding the mouse

A second option, rather than hiding the Mouse, is to lock it to the center of the screen. It helps to solve the issue of the the Mouse staying within the the Game boundaries but also comes with its own set of problems.

class LockedMouseExample : NadaComponent
{
    // An Archetype of the Custom Cursor Object
    [Property] var CursorArchetype : Archetype;

    // An Archetype of the Space to be built
    [Property] var CursorSpaceArchetype : Archetype;

    // An Archetype of the GameCamera to use in the Cursor Level
    [Property] var CameraArchetype : Archetype;

    // Reference to the CameraViewport of the created Cursor Space
    var CurViewport : CameraViewport = null;

    // An empty Level used to create the Custom Cursor in
    [Property] var CursorLevel : Level;

    // Reference to the Custom Cursor Object once it's created
    var CursorObj : Cog = null;

    // The Space the Custom Cursor will inhabit
    var CursorSpace : Space = null;

    // Reference to the Translation of the Custom Cursor
    var CursorTranslation : Real3
    {
        get { return this.CursorObj.Transform.Translation; }
    }

    // Calls a function to give the Cursor Position in Screen Space
    var CursorScreenSpacePosition : Real2
    {
        get { return this.CurViewport.WorldToScreen(this.CursorTranslation); }
    }

    // Calls a function to give the Cursor Position in the Viewport
    var CursorViewportPosition : Real2
    {
        get { return this.CurViewport.ScreenToViewport(this.CursorScreenSpacePosition); }
    }
// Code continued below

Just from the initial setup it becomes obvious that this method is much more complex than simply making the Mouse invisible and having an Object follow it, but it also offers benefits.

NOTE: Get-Sets In order to reduce the amount of repetitive typing, a number of getters have been used to hold references to other functions. For further explanation of get-sets, see Get-Sets.

When this component is attached to the current level's LevelSettings object, it will create an Object in a new Space that is built on top of the GameSpace. This object allows it to function properly in a 3D game. As the getters have defined most of the variables, much of the script is ready to be used.

...
    function Initialize(init : CogInitializer)
    {
        // Traps the Mouse, locking it and making it invisible
        Zilch.Mouse.Trapped = true;

        // Creates a new Space for the Cursor to inhabit
        this.CursorSpace = this.GameSession.CreateNamedSpace("CursorSpace", this.CursorSpaceArchetype);
        // Loads the CursorLevel inside the newly made space
        this.CursorSpace.LoadLevel(this.CursorLevel);
        // Create the GameCamera
        var cam = this.CursorSpace.Create(this.CameraArchetype);
        // Set the viewport based on the GameCamera
        this.CurViewport = cam.CameraViewport;
        // Creates the Custom Cursor at (0,0,0) in the new Level
        this.CursorObj = this.CursorSpace.CreateAtPosition(this.CursorArchetype, Real3());
        // Connects to the MouseMove Event In the Cursor Space, NOT the Game Space
        Zilch.Connect(this.CursorSpace, Events.MouseMove, this.OnMouseMove);
    }
...

The Initialize function creates the required Space, Level and Object for the Custom Cursor. It's important to make sure that the objects are being made in the CursorSpace, not the main GameSpace.

...
    function OnMouseMove(event : ViewportMouseEvent)
    {
        // Get a new position by adding the distance of the MoveEvent to the Cursors current position
        var newPos = this.CursorViewportPosition + event.Movement;
         // Create a new position within the Viewport for the Cursor while also Clamping the range
        var newCurViewportPos = Math.Clamp(newPos, Real2(), this.CurViewport.ViewportResolution);
         // Adjust the Viewport position to the proper Screen position
        var newCurScreenPos = this.CurViewport.ViewportToScreen(newCurViewportPos);
         // Set the Cursor Objects position to the newly determined position in World Space
        this.CursorObj.Transform.Translation = 
        this.CurViewport.ScreenToWorldZPlane(newCurScreenPos, 0.0);
    }
}

Similar to hiding the mouse, the code-block above uses MouseMove Events to control the movement of the Custom Cursor object. This time, however, a few more steps are needed:

The intended new cursor position is found by adding the current viewport position of the Cursor and the Mouse Movement vector given in the MouseMove Event.

The new cursor viewport position must be clamped within the boundaries of the viewport.

The viewport position must be converted to screen position as there is no way to directly convert from viewport space coordinates directly to worldspace.

The cursor screenspace position must be converted to worldspace so the custom cursor can be moved.

The biggest problem with using this method is the fact that most Mouse functionality is lost, as it becomes locked to the center of the screen. All Cursor functionality will have to be determined using either collision events or Cursor position, which can become taxing on the system as it has to either check the position every frame or resolve various collisions.

Below is an example of this script attached to the LevelSettings object of an empty level, with the blue sprite given as the CursorArchetype and the white sprite following the same script as the hidden cursor example, to show how the mouse is attempting to move from the world origin.

LockedMouseCursorMove

Related Materials

Manual

  • Mouse Input
  • Getters and Setters
  • {icon university}[[zilch_engine_documentation/zilch_editor_documentation/ZilchManual/Graphics/CamerasViewportsRenderers/|Cameras, Viewports and Renderers]]

Code Reference