Quick answer: get_viewport().get_mouse_position() returns coordinates in viewport space, not window-pixel space. After a resize, the viewport transform changes but your code may still assume a 1:1 mapping. Use get_global_mouse_position() for world-space queries, check your stretch mode settings, and use make_input_local() when handling input events in nested viewports.

You build your game, test it at 1920×1080, everything works perfectly. The mouse clicks exactly where you expect. Then you resize the window, or a player runs it at a different resolution, and suddenly your aim is off, your clicks land in the wrong spot, and your drag-and-drop system breaks completely. This is one of the most common Godot issues, and it comes down to understanding how Godot transforms coordinates between the OS window, the viewport, and the game world.

Understanding Godot’s Coordinate Spaces

Godot 4 has several distinct coordinate spaces, and mixing them up is the root cause of nearly every mouse position bug:

When you resize the window and stretch mode is active, the mapping between window coordinates and viewport coordinates changes. If your code uses viewport coordinates but treats them as if they were window coordinates (or vice versa), everything breaks.

Stretch Mode and How It Affects Mouse Position

The stretch mode setting in Project Settings > Display > Window > Stretch controls what happens when the window size doesn’t match your design resolution. Godot 4 offers two main modes:

With stretch mode set to disabled (the default), the viewport size equals the window size. No transform is applied, and get_mouse_position() returns raw pixel coordinates. But the moment a player resizes the window, your game world may clip or reflow, and any hardcoded position assumptions break.

Here is a common broken pattern:

# BAD: Assumes viewport coords = window coords
func _process(delta):
    var mouse_pos = get_viewport().get_mouse_position()
    # This only works if stretch mode is disabled AND no camera transform
    $Crosshair.position = mouse_pos

The Correct Approach for Each Use Case

The fix depends on what you are trying to do with the mouse position:

World-space targeting (aiming, clicking on game objects)

# CORRECT: Uses the canvas transform to account for camera
func _process(delta):
    var world_pos = get_global_mouse_position()
    $Crosshair.global_position = world_pos

get_global_mouse_position() automatically applies the inverse of the viewport’s canvas transform, which includes the camera’s transform. This means it correctly accounts for camera zoom, panning, and rotation — and critically, it also accounts for stretch mode scaling.

UI-space interactions (menus, HUD elements)

For Control nodes and UI elements, you rarely need to query mouse position directly. Instead, handle input events in _gui_input() or use signals like mouse_entered. The event coordinates are already in the correct local space:

func _gui_input(event):
    if event is InputEventMouseButton:
        # event.position is already in this Control's local space
        var local_pos = event.position
        print("Clicked at: ", local_pos)

Input events in _input or _unhandled_input

When handling InputEventMouseButton or InputEventMouseMotion in _input(), the event’s position property is in viewport coordinates (already adjusted for stretch mode). To convert to world space, use the canvas transform:

func _unhandled_input(event):
    if event is InputEventMouseButton and event.pressed:
        var viewport = get_viewport()
        var canvas_xform = viewport.get_canvas_transform()
        var world_pos = canvas_xform.affine_inverse() * event.position
        spawn_effect_at(world_pos)

SubViewport Headaches

If you use SubViewport nodes (for minimap rendering, split-screen, or render-to-texture effects), mouse coordinates get even more confusing. The mouse position reported inside a SubViewport is relative to that SubViewport’s own coordinate system, but the raw input events come from the root viewport.

Use make_input_local() to transform an input event from the parent viewport’s space into a child node’s local space:

# In a Control inside a SubViewportContainer
func _input(event):
    if event is InputEventMouse:
        var local_event = make_input_local(event)
        var local_mouse_pos = local_event.position
        # Now local_mouse_pos is relative to this node

Without make_input_local(), your SubViewport-based minimap or picture-in-picture display will report mouse positions that correspond to the main viewport instead of the sub-viewport’s content.

Testing Multiple Resolutions

The most insidious part of this bug is that it often works perfectly at your development resolution and only breaks at other sizes. Here is a testing workflow that catches these issues early:

  1. Set your project’s design resolution in Project Settings (e.g., 1920×1080).
  2. Test at your design resolution to confirm baseline behavior.
  3. Resize the window to half size (960×540). Does the mouse still hit the right targets?
  4. Test at a different aspect ratio (e.g., 2560×1080 ultrawide). Does the mouse track correctly near the edges?
  5. Test in fullscreen on a monitor with a different native resolution.
  6. If targeting mobile, test with touch events — InputEventScreenTouch uses a different coordinate space than InputEventMouseButton.

You can also override the window size at launch for quick testing by adding command-line arguments: --resolution 800x600.

“Our playtesters kept reporting that the click-to-move system was broken. It worked perfectly in the editor. Turns out every tester was running at 1366×768 and we’d only ever tested at 1080p. One line change from get_mouse_position to get_global_mouse_position fixed every report.”

Related Issues

Mouse position bugs often surface alongside other viewport issues. See Fix Godot Export Variable Dictionary Null at Runtime for another common Godot 4 gotcha, and Best Practices for Game Error Logging for how to log input coordinates during development so you can reproduce resolution-dependent bugs from player reports.

Tip: Add a debug label that displays both get_mouse_position() and get_global_mouse_position() simultaneously during testing. When they diverge unexpectedly, you’ve found the coordinate space mismatch.