Change Mouse Cursor Graphic


#1

Howdy guys,

Working on my own Raycasting demonstration, would like to change the mouse cursor to a custom sprite. Thought it would be fairly straight forward but not finding anything in help. I’m going to press on with the default mouse and get raycasting working, any way to easily update the mouse cursor graphic?

I’ll post a video once its complete.

Thanks,
Jarmo


#2

Bump.

I could see making the mouse invisible and then moving a sprite or UI along the mouse X, Y coordinates, though I’m not sure how I would do that without repackaging my scene into a different view, seems like I tough way to do it. Is there any easier way to do it?

Thanks,
Jarmo


#3

Did reading on this, looks like you can make a cursor file and load it through Input.Mouse. In terms of using a sprite based item I’m not sure, but would think you could turn the mouse invisible and move the sprite based off mouse X/Y in a repacked scene. If I ever do that I’ll try.


#4

You can use a sprite based cursor if you use a Custom Scene Renderer. I did it by creating a texture asset and the script below and adding it to the scene. This script could use some improvements. See comments for more details:

public class SpriteCursor : SyncScript
{
    private SpriteBatch spriteBatch;
    private DelegateSceneRenderer delegateRenderer;

    //In my example used a 16x16 texture with following settings
    // Generate mipmaps false
    // Compress false
    // Stream false
    public Texture Cursor { get; set; }

    public override void Start()
    {
        //If you are using a resizeable window and want to keep cursor size relative to window size,
        //should probably set VirtualResolution property.
        spriteBatch = new SpriteBatch(GraphicsDevice);
        
        //Add renderer so we can draw cursor sprite last.
        delegateRenderer = new DelegateSceneRenderer(Draw);
        var renderers = GetSceneRenderers();
        renderers?.Add(delegateRenderer);


        Game.IsMouseVisible = false; //hide windows cursor.
    }

    public override void Update()
    {
        //Make sure SpriteCursor is the last renderer.
        //We really shouldn't do this each frame. Should setup game so this can be done in Start method, once.
        //If we don't need to do this and we don't need to do any other update logic should 
        //change script to inherit from StartupScript and delete this method.
        var renderers = GetSceneRenderers();
        if (renderers.Last() != delegateRenderer)
        {
            renderers.Remove(delegateRenderer);
            renderers.Add(delegateRenderer);
        }

        
    }
    private void Draw(RenderDrawContext context)
    {
        //Should probably only draw cursor if mouse is over window.
        //Have not worked out how to do that.
        if (Cursor != null)
        {
            spriteBatch.Begin(context.GraphicsContext,
                samplerState:context.GraphicsDevice.SamplerStates.PointClamp, //I wanted to make sure sprite stayed crisp/pixley
                depthStencilState: DepthStencilStates.None //Make sure cursor is always on top
                );

            //This assumes you are not using a VirtualResolution for the SpriteBatch. 
            //If using a VirtualResolution should probably use that rather than ViewSize.
            var cursorPosition = Input.MousePosition * context.RenderContext.RenderView.ViewSize;

            //Using whole texture as cursor. Assumes top-left corner of texture is cursor position.
            //If using cursor like a crosshair would have to do some off-setting.
            spriteBatch.Draw(Cursor, cursorPosition);

            spriteBatch.End();
        }
    }

    public override void Cancel()
    {
        //Remove from renderer.
        var renderers = GetSceneRenderers();
        renderers?.Remove(delegateRenderer);
        
        //clean up
        spriteBatch.Dispose();
        spriteBatch = null;

        //Opionally undo any game changes we made.
        Game.IsMouseVisible = true; //show windows cursor.
    }

    private List<ISceneRenderer> GetSceneRenderers()
    {
        //This asumes you are using the default Graphics Composition 
        var cameraRenderer = this.SceneSystem.GraphicsCompositor.Game as SceneCameraRenderer;
        var rendererCollection = cameraRenderer?.Child as SceneRendererCollection;
        return rendererCollection?.Children;
    }
}


#5

Regarding your question about where the Draw method is called. (Thought I would “answer” it here.):

The script adds a renderer to the graphics composition (rendering pipeline). For more details you should have a look at the documentation:

My first implementation was actually done via a custom renderer inheriting from SiliconStudio.Xenko.Rendering.Compositing.SceneRendererBase and adding it to the graphics composition via Game Studio. Like:

[Display("Sprite Cursor Renderer")]
public class SpriteCursorRenderer : SceneRendererBase
{
    private SpriteBatch spriteBatch;
    private InputManager inputManager;

    public Texture Cursor { get; set; }

    protected override void InitializeCore()
    {
        base.InitializeCore();

        spriteBatch = new SpriteBatch(GraphicsDevice);

        inputManager = this.Services.GetService<InputManager>();

        this.Services.GetService<IGame>().IsMouseVisible = false;
    }

    protected override void DrawCore(RenderContext context, RenderDrawContext drawContext)
    {
        if(Cursor != null)
        {
            spriteBatch.Begin(drawContext.GraphicsContext, depthStencilState: DepthStencilStates.None);

            spriteBatch.Draw(Cursor, inputManager.MousePosition * context.RenderView.ViewSize);

            spriteBatch.End();
        }
    }

    protected override void Destroy()
    {
        base.Destroy();

        spriteBatch.Dispose();
    }
}


#6

Thanks. I’m learning some things here from you but working with the engine framework hasn’t fully clicked yet. I’m currently trying to override the OnExiting() method in the GameBase so that I can put some network handling in and encourage network game clients to gracefully disconnect on exit. I’ve created the derived class and overriden the OnExiting() method, but I can’t figure out where to go from there. How do I add this override to the game? Do I need to register it somewhere?

Any help is appreciated, going to table the item and work on connection handling inside the multiplayer instance.


#7

Sent you a message regarding this, but placing it here for the community:

One question I have, do I need to instantiate this class for it to be added to the game rendering pipeline? Or–how do I add it to the game? In the example you provided I see that you “enabled” it via Game Studio, how can I add/remove this renderer at runtime? I can think of workarounds (as in having it run the whole time but not render any sprites) but I’d prefer my minimap class to add and the renderer at run time so that it is not present the entire time.


#8

You can either modify the composition at run time (add/remove renderer from the composition graph) or set the Enabled property to true or false. Not sure if there is any benefit to either option.

If you look my first example of the SpriteCursor script you will see how it adds a DelegateSceneRenderer to the composition. (You could just as easily create another renderer like the SpriteCursorRenderer and add that to the composition if you want to keep the render code separate.). If you keep a reference to the renderer you can then either remove/add it or update the enabled state as you desire.

If you add the custom renderer by modifying the composition asset then you can get it at run time and do the same (remove/add or update the enabled state). Now to get the asset defined renderer you have to know where it is in the graph and traverse the various properties casting objects to the correct type as you go. If you have a look at the SpriteCursor script example you will get a rough idea by looking at the GetSceneRenderers method. I generally work it out by debugging and seeing what I need to cast to etc. to get the renderer I am interested in.

But there is also another option Services. You could make the renderer register itself as a service at start up.

[Display("Sprite Cursor Renderer")]
public class SpriteCursorRenderer : SceneRendererBase
{

    protected override void InitializeCore()
    {
        base.InitializeCore();
       
        //Same setup as before......

        //Add this renderer as a service. 
        //You could also add it as a specific interface if you didn't want to expose all of a renderer 
        this.Services.AddService(this);
    }     

    public override bool Enabled
    {
        get => base.Enabled;
        set
        {
            base.Enabled = value;

            //Do some action on enabled changing
            //Game etc. might not exist when assets being compiled.
            if(this.Services?.GetService<IGame>() is IGame game)
            {
                //Enable normal mouse cursor if this renderer is disabled.
                game.IsMouseVisible = !value;
            }

        }
    }

    protected override void Destroy()
    {
        //Unregister this as a service if renderer removed from composition.
        this.Services.RemoveService<SpriteCursorRenderer>();
        base.Destroy();

        //Clean up stuff here.

    }
}

Then in your scripts get the service and change Enabled state:

public override void Update()
{
    //...

    if (Input.IsKeyPressed(Keys.M) &&
        Services.GetService<SpriteCursorRenderer>() is SpriteCursorRenderer scr)
    {
        scr.Enabled = !scr.Enabled;
    }
}

I believe there is also the option of not using the composition graph at all. I think you can create a GameSystem and add that to the Game.GameSystems Collection. And do the drawing in there. You would just have to make sure the draw order was after the built in rendering system. I have not attempted this myself though.

Hope that helps.


#9

I played with two approaches and found the fixed renderer with enable/disable and operational bool flags to be the most elegant. Thankyou greatly for your help. I will be rendering a Minimap fog of war as one large Sprite batch of 4096 tiles in a 320x320 area. I’ll let you know how it goes!

The delegate renderer due to adding removing from the Scene renderer enumeration was problematic for cleanup. I couldn’t modify the collection once it was added, it wouldn’t actually remove even though a call to Remove was made.

Thank you!!!
Jarmo/Jeremy