An experiment in animating a hardware cursor in Godot
Source code for this experiment
After looking at custom cursors last week, the question naturally came up on whether or not you can have a hardware cursor that’s also animated. At first glance, the answer is no. The project settings menu only lets you choose a PNG or a WebP image as your cursor, and the Godot documentation for Input.set_custom_mouse_cursor clearly says
Note: AnimatedTextures aren’t supported as custom mouse cursors. If using an AnimatedTexture, only the first frame will be displayed.
But I got curious as to whether or not you could hack something together on your own using the Input.set_custom_mouse_cursor function without any appreciable impact on responsiveness or weirdness in the UI. The answer appears to be - kind of. Let’s take a look.
The design
We know that we can’t use an AnimatedTexture as our cursor, as it’s not supported, and we can’t use an AnimatedSprite and sync it each frame without sacrificing responsiveness. The only option we have, then, is to essentially make our own equivalent of those two, storing each frame as a separate image and managing the animation state on our own, calling Input.set_custom_mouse_cursor whenever we have a frame change.
So I went and whipped up a quick cursor:
And then made another version of it with a loading animation (I’m showing it as a spritesheet here but in my project, as you’ll soon see, each frame is a separate PNG):
Next, I made a project in Godot and a scene with a few items:
- A Button that can trigger the loading animation
- A LineEdit so that I can test that the animation can work well with other cursor shapes
- A Timer so I know when to switch frames
- A script on the root node to manage everything
The code for this scene is as follows:
extends Control
# Preload our cursor graphics so they're ready to go
const CURSOR = preload("res://cursor/cursor.png")
const LOADING_FRAMES = [
preload("res://cursor/loading_1.png"),
preload("res://cursor/loading_2.png"),
preload("res://cursor/loading_3.png"),
preload("res://cursor/loading_4.png"),
preload("res://cursor/loading_5.png"),
preload("res://cursor/loading_6.png"),
preload("res://cursor/loading_7.png"),
preload("res://cursor/loading_8.png")
]
export (float) var frames_per_second = 16.0
var current_frame = 0
func _ready():
# Connect our signals and default to a cursor with no loading animation
$trigger_button.connect("pressed", self, "begin_load")
$animation_timer.connect("timeout", self, "update_frame")
Input.set_custom_mouse_cursor(CURSOR, Input.CURSOR_ARROW, Vector2(64, 64))
func begin_load():
# When the loading button is pressed, we start our animation timer
# and go to the first frame of the loading animation
$animation_timer.start(1.0 / frames_per_second)
current_frame = 0
Input.set_custom_mouse_cursor(LOADING_FRAMES[current_frame], Input.CURSOR_ARROW, Vector2(64, 64))
func update_frame():
# If we have a cursor shape other than the default, don't try to override it
if Input.get_current_cursor_shape() != Input.CURSOR_ARROW:
return
# Increment our loading animation, cycling through the array indefinitely
current_frame += 1
if current_frame >= LOADING_FRAMES.size():
current_frame = 0
Input.set_custom_mouse_cursor(LOADING_FRAMES[current_frame], Input.CURSOR_ARROW, Vector2(64, 64))
If you’re familiar with basic coding in Godot and followed the tutorial last week on setting the cursor to a static image then there’s not much here that should throw you off. I’m doing the same thing I did last week by using Input.set_custom_mouse_cursor to set my default cursor (Input.CURSOR_ARROW) to a graphic with an offset (64x64 in this case since my image is 128x128, centered on the tip of the mouse), except now I also set a timer to go off at my desired frame rate and step through the array of animation frames whenever the timeout
signal is emitted by the timer.
That’s enough to manage the animation itself, but I also have to make sure that I don’t step on the toes of the other parts of my UI by making sure that the current arrow shape, provided by Input.get_current_cursor_shape, is the default Input.CURSOR_ARROW shape. Godot doesn’t have a signal for when the cursor shape changes (how often would that be useful anyways?), so I just wait for the timer to step the animation and see what the current state is before updating the cursor graphic.
If you really wanted to, you could set up a custom signal that pings Input.get_current_cursor_shape every frame and emits a signal whenever the cursor changes, and if you were doing a complex set of cursors, such as an animation for every cursor shape, it could be worth doing, especially if combined with some sort of state machine to manage the chaos at that point, rather than the simple script I’ve done here.
The results
So what do I get when I run the scene?
Overall not bad! It swaps between the loading animation and IBeam shape smoothly and as needed. And since I’m using a hardware cursor, it feels smooth and responsive.
The catch
There are a few quirks, though. First of all, when we hover over the button to close the game window, the native cursor glitches in and out (look at the end of the above video to see this).
Secondly, the animation is also somewhat framerate dependent. I tested this by limiting the framerate in the project settings under Debug > Settings to both 30fps, as a reasonable example of minor slowdown in a game (you are targeting 60fps, right?) and 5fps, as a more extreme example to better see the behavior of the cursor.
At 30 fps
At 5 fps
For one, that glitchiness on the close button does occasionally appear in the game, it’s just hidden by the higher frame rate (and can still be hard to catch). For another, the animation update itself is effectively tied to the framerate (since the timer can’t fire timeout signals faster than the framerate), which means that if your framerate drops below your animation rate the mouse will animate slower as well. The cursor will continue to update its position at the same rate as the native cursor, though, and that’s still an improvement over the alternative - a software cursor using an AnimatedSprite whose position update is also locked to the framerate.
So it’s not a perfect solution by any means, but it does show potential if you want the responsiveness of a hardware cursor but the freedom of a software cursor. You could probably take the concepts demonstrated here and make something a bit more robust and polished to give your game a really unique look and feel, though the glitchiness of this method would be something to watch out for and try to figure out the root cause of. I wouldn’t go overboard with the animated cursors, though, as you don’t want to make the very item your player is using to interact with the game too distracting, confusing, or even just ugly!