Achieving better mouse input in Godot 4: The perfect camera controller

Achieving better mouse input in Godot 4: The perfect camera controller

Input accumulation, mouse events, raw data, stretch independent sensitivity… and why you never multiply mouse input by delta

Hello there! I’m Freeman! I’m a game developer who uses a full open source workflow and tries to help the community in whatever I can.

As a long time Godot user I often see issues related with mouse input in games, tutorials, and demos. From clear frame dependent input because the wrong use of Delta, to the infamous problem of your mouse sensitivity changing if you use Stretching or Scaling in your projects.

To help with this issues i wrote the article featured at the top of the post and a companion asset. The first part of the article is a deep dive into mouse input, how Godot approaches it, the key elements that affect it, why these issues happen and how to solve them. The second part is a detailed tutorial to put all that we learned in practice by creating a responsive, stable, and reliable first person camera. You can download the full open source project too.

Hope it can be useful for you! I’ll be around here to answer questions! hope you have a good day!

PS: The article has no paywall, no ads and no cookies.

12 Likes

Thanks for the article! I definitely learned from it :slight_smile:

I see that you recommend turning off input accumulation, processing in _process, and using something like this in the event handler: mouse_input += event.relative.

Just to make sure I am understanding things correctly, this would be the same as turning on input accumulation, processing in _process, and using something like this in the event handler: mouse_input = event.relative.

Does that sound right?

1 Like

Hi @allenwp ! Thank you for reading it, I’m glad it was useful!

For your question:

Yes, they should be equivalent!

For capturing the mouse to use in process instead of using it directly:

You can enable or disable input accumulation depending on your needs and intended hardware. The important part is that is always safer to do mouse_input += event.relative, because it will work as intended whatever or not input accumulation is enabled.

However, i recommend moving the camera outside of _process from the input methods, but of course this totally depends on your final intended use!

Cool, thanks!

1 Like

Thanks for the guide! Helped clear some stuff up, cheers

1 Like

Very nice article! Cheers! :ok_hand:

1 Like

Clearly it works, but why doesn’t get_final_transform() double the stretch effect instead of neutralising it? The values it contains are equal to its effect, so unless xformed_by divides

I can’t brain it

Hi! Let’s focus on the horizontal dimensions:

If your original window width is 1920 and you move your mouse 1920 pixels you have covered 1/1 of the original window size (The full extension). If you make your window double this size, those 1080 px from the mouse only cover 1/2 of the distance. This is 0.5. Because get_final_transform() will report 2 in this case, 0.5 * 2 is 1, leaving you again with your unscaled input.

Edit: To be more clear, by the moment we scale it by 2 the value was already scaled to be half of it’s real value. So making it double fixes it.

Thanks!

I see your viewpoint, wanting to handle camera movement in the _process function, but instead of creating a new variable, could you just directly move the camera in the _unhandled_input function? You wouldn’t need to worry about multiple calls per frame, as whenever the mouse moves, the camera moves that much. You wouldn’t have to set anything to zero, as there is no variable to begin with. This just seems like the better option, unless there is a reason for handling it in _process.

Hi!

We are not moving the camera in process, we are using unhandled input to do so. You will or will not have multiple calls per frame depending how you set use accumulated input and as you say, whatever the mouse is moved the camera moves that much. There are no calls to process in the script and i recommend NOT to move the camera in process.

Back again. I did all your things (except QoL stuff, which I’ll do later) but I’d like to know, is there a way to make it compatible with this?

DevSer2

Currently looking downward fast enough makes the camera go through the ‘legs’ and come up upside-down on the other side. I could just child it, but I like what I have going with the lerp.

Correction: it’s not the other side, it’s the same one, except the camera is upside down. By setting DummyCam.rotation.z to %Dummy.Rotation.z every frame, it’s fixed - except now the screen jitters when looking down as the downward camera movement translates upwards and then down again, which could probably be fixed with some kind of halt screen refresh…

What have I done

Fixed! (yes I did rename everything)

Names:
ismouse = event
window_shape = viewport_transform
camera_motion = motion
mouselook_horiz = add_yaw
mouselook_verti = add_pitch
necklock_verti = clamp_pitch

Thanks again for the amazing guide! ☆⸜(⑉˙ᗜ˙⑉)⸝♡

Arg! Sorry, i been deeply into understanding GJK and some other stuff lately and i forgot this. I’m glad you found a solution.

For clarification, you are able to go down you legs because we are just clamping, so if the actual rotation + the new input wraps around 360 you end up on the top.

This can be fixed by instead clamping the added input to the range that goes from the current rotation to the lowest allowed and the current one to the bigger allowed.