In a camera look script, I can't turn the camera if the cursor is on the edge of the screen - how to fix?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By threenuc

I’m creating a simple camera look script that moves the camera based on mouse movement.

It works like this (the following is called after an InputEventMouseMotion event)

  1. Take mouse position from last frame
  2. Take current mouse position
  3. Rotate the camera by the difference between mouse positions
  4. Save current mouse position so it can be reused as “mouse position from last frame” in the next frame.

The problem is that when the cursor is on the edge of the screen, the difference of mouse positions between two frames will be the same. The camera wont rotate.

What I’ve tried is adding another step to the above algorithm:

  1. Reset the mouse cursor to the center of the screen
    (using get_viewport.warp_mouse(Vector2(windowWidth/2, windowHeight/2))

The problem is: a warp_mouse() call triggers the InputEventMouseMotion event.
Here’s an example frame by frame, assuming we start from the center of the screen:

  1. Move mouse 1 pixel to the left (MouseMotion event triggers)
  2. Last mousepos is center of the screen → (screenwidth/2, screenheight/2)
  3. Turn camera 1 degree left
  4. Current mousepos is 1 pixel to the left from center
  5. Warp mouse back to center with warp_mouse() (MouseMotion event still triggers)
  6. Last mousepos is 1 pixel to the left from center
  7. Current mousepos is center
  8. Turn camera 1 degree right

So it will always turn the camera back to center after 1 frame (which is visible after I limit Godot FPS to 2 also).

Here’s the code:

var lastFrameMouseposX=0;
var lastFrameMouseposY=0;

func _input(event):
    if event is InputEventMouseMotion:
	    _moveCameraWithMouse(event);
	pass

 func _moveCameraWithMouse(event):
    var mousepos = event.position;
	var sensitivity = 0.01;
    #var lastFrameMouseposX; these are global variables defined at the beginning of this script
    #var lastFrameMouseposY;

    var moveCameraOnX=(lastFrameMouseposX - mousepos.x) * sensitivity;
    var moveCameraOnY=(lastFrameMouseposY - mousepos.y) * sensitivity;

    var potentialCameraXAngleOnNextFrame = rad2deg(rotation.x+moveCameraOnY);
    if (_isBetween(potentialCameraXAngleOnNextFrame, -90, 90)):
	    rotation += Vector3(moveCameraOnY, moveCameraOnX,0);
	
    lastFrameMouseposX=mousepos.x;
    lastFrameMouseposY=mousepos.y;

    var windowsize=get_viewport().size;
    get_viewport().warp_mouse(Vector2(windowsize.x/2, windowsize.y/2));
    pass

The fact that warp_mouse triggers a mousemove is pretty weird in itself. I’m just learning but isn’t it something one could file an github issue for (to create a ignore flag for warp_mouse)? Obviously I’m asking here to make sure I’m not making a mistake here.

What do I do to make edge look work?

:bust_in_silhouette: Reply From: threenuc

Fixed it.

I removed the part of my code that holds the last mouse position (lastFrameMouseposX/Y) and used the event.relative variable from the InputEventMouseMotion event instead (I’ll explain why later).

Then, in func _ready() I added

Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED);

Seems like this mouse mode resets the cursor position to center each time the mouse moves BUT event.relative does not recalculate the mouse displacement for when MOUSE_MODE_CAPTURED resets the cursor back to center (so you don’t go back and forth like in the code from my question).

Maybe this will help you also
Here’s what my code looks right now (fully working)

Thanks to the Godot Discord for pointing me in the right direction.

:bust_in_silhouette: Reply From: Azai

One solution could be to forego events entirely. Something like:

func _process(delta):
    move_camera_with_mouse()

func move_camera_with_mouse():
    var sensitivity = 0.02
    
    #Get the displacement from the center
    var mouse_diff = get_mouse_position() - get_viewport().size / 2.0

    #Apply the rotation and clamp the vertical rotation
    rotation += Vector3(-mouse_diff.y, -mouse_diff.x, 0) * sensitivity
    rotation.x = clamp(rotation.x, deg2rad(-80), deg2rad(80))

    #Return the mouse to the center 
    warp_mouse(get_viewport().size / 2.0)

Keep in mind that rotation.y is rotation around the y-axis, meaning it’s horizontal rotation and should be based on the horizontal movement, aka mouse_diff.x.
rotation.x is rotation around the x-axis, which goes from left to right, meaning it’s used to tilt up and down.

Ah, and in GDScript, you don’t need to put semicolons at the end of your lines. It just uses spacing and parentheses to figure out where your lines end. If you do want to spread a single statement out of several lines, for structural clarity, you can just do something like:

var my_sum = (
    number_a +
    number_b +
    number_c
)

Hope this helps.

—Side note—

It’s also worthy to note that without the clamp on the vertical rotation, the camera starts glitching out when you tilt up or down further than 90°. This is because the camera automatically adjusts so that the x rotation never goes outside of the range from -90° to +90°.
In most games this isn’t really a problem, since you clamp vertical rotation anyway. But if you want your player to be able to look up so far that they’re looking back and upside down, you have to make a separate rotation variable in your script that the automatic correction doesn’t touch.