Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get_viewport().get_mouse_position() does not work with InputSender #646

Open
murilosr opened this issue Jul 25, 2024 · 5 comments
Open

get_viewport().get_mouse_position() does not work with InputSender #646

murilosr opened this issue Jul 25, 2024 · 5 comments

Comments

@murilosr
Copy link

Versions

(list all versions where you have replicated the bug)

  • Godot: 4.2.2-stable
  • GUT: 9.2.1
  • OS: Pop_OS 22.04

  • Godot: 4.3.beta3
  • GUT: 9.2.1
  • OS: Pop_OS 22.04

The Bug

In my script, I use the method Node.get_viewport().get_mouse_position() to get the coordinates and position another node near the mouse.

Using the InputSender's mouse_set_position and mouse_relative_motion methods, it doesn't propagate the mouse position to the Node.get_viewport().get_mouse_position(). In the docs, I haven't found anything about this method also.

Steps To Reproduce

1. Scene setup

Create a scene with a Node2D and a Label.
Set any text to the Label.

Screenshot from 2024-07-25 10-12-38

2. Node2D script

Attach this script to Node2D node:

extends Node2D

@onready var label: Label = $Label
var offset: Vector2 = Vector2(10,50)

func _process(delta: float) -> void:
	var mouse_pos = get_viewport().get_mouse_position()
	var label_pos = mouse_pos + offset
	label.position = label_pos

3. Create the test script

  • Save the scene to res://node_2d.tscn
  • Create the test script (res://tests/unit/test_bugreport.gd)
extends GutTest

func test_label():
	var _sender = InputSender.new(Input)
	
	var my_scene = load("res://node_2d.tscn")
	var doubled_scene: Node2D = partial_double(my_scene).instantiate() as Node2D

	add_child_autofree(doubled_scene)
	
	_sender = _sender.mouse_set_position(Vector2i(20,20)).wait_frames(1)
	await _sender.idle
	for i in range(0,20):
		_sender = _sender.mouse_relative_motion(Vector2(1,1)).wait_frames(2)
		await _sender.idle
	
	await wait_frames(1)
	assert_eq(doubled_scene.get_node("./Label").position, Vector2(40,40) + Vector2(10,50), "Wrong label position")
  • Running the test, the input is simulated (showing the target on the mouse position)

Screenshot from 2024-07-25 10-31-05

  • But the Node2D's get_viewport().get_mouse_position() get the position from the actual mouse pointer, not the simulated:

Screenshot from 2024-07-25 10-31-12

Expected result (Running the game)

Screenshot from 2024-07-25 10-15-10

It is expected that the Label follows the mouse pointer with the defined offset.

Workaround

If I use the function Input.warp_mouse, I can simulate the OS's mouse cursor moving, and the test scenario works:

func test_label_workaround():
	var _sender = InputSender.new(Input)
	
	var my_scene = load("res://node_2d.tscn")
	var doubled_scene: Node2D = partial_double(my_scene).instantiate() as Node2D

	add_child_autofree(doubled_scene)
	
	_sender = _sender.mouse_set_position(Vector2i(20,20)).wait_frames(1)
	await _sender.idle
	var x = 20
	Input.warp_mouse(Vector2i(20,20))
	for i in range(0,20):
		_sender = _sender.mouse_relative_motion(Vector2(1,1)).wait_frames(2)
		await _sender.idle
		x += 1
		Input.warp_mouse(Vector2i(x,x))
	
	await wait_frames(1)
	assert_eq(doubled_scene.get_node("./Label").position, Vector2(40,40) + Vector2(10,50), "Wrong label position")

Screenshot from 2024-07-25 10-36-02

Screenshot from 2024-07-25 10-36-12

@bitwes
Copy link
Owner

bitwes commented Jul 25, 2024

Try adding _sender.mouse_warp = true. Looks like this hasn't been documented yet, but it should make the mouse follow the input.

I can't remember if this is experimental (why it might not be documented) or not. I do know that you shouldn't touch the mouse while tests that use this are running...for fairly obvious reasons.

Since you are using a double, it might be easier to add a private _get_mouse_position that you could stub to return whatever you want. That way you don't have to mock the input to test how you are handling mouse position.

@murilosr
Copy link
Author

Try adding _sender.mouse_warp = true. Looks like this hasn't been documented yet, but it should make the mouse follow the input.

Yeah, this works! Thanks.

Also, thanks for the idea to stub the get_mouse_position using a private method. It works too.

But for the reported way to get the mouse position get_viewport().get_mouse_position(), will it be supported anytime?

@murilosr
Copy link
Author

murilosr commented Jul 25, 2024

I was playing with the suggestions of @bitwes and got a solution to what I was seeking.
If anyone needs it, here it is:

1. Add a private method to get the cursor position (as was suggested):

At Node2D's script:

extends Node2D

@onready var label: Label = $Label
var offset: Vector2 = Vector2(10,50)

# Added private method
func _get_mouse_position() -> Vector2:
	return self.get_viewport().get_mouse_position()

func _process(_delta: float) -> void:
	# Replaced by the private method call here
	var mouse_pos = _get_mouse_position()
	var label_pos = mouse_pos + offset
	label.position = label_pos

2. Stub the _get_mouse_position:

At test script:

func _get_inputsender_mouse_position(sender):
	return sender._mouse_draw._draw_at

func test_label_workaround_with_stub():
	var _sender = InputSender.new(Input)
	
	var my_scene = load("res://node_2d.tscn")
	var scene = partial_double(my_scene).instantiate()
	stub(scene.get_node("."), "_get_mouse_position").to_call(_get_inputsender_mouse_position.bind(_sender))

	add_child_autofree(scene)
	
	_sender = _sender.mouse_set_position(Vector2i(20,20)).wait_frames(1)
	await _sender.idle

	for i in range(0,20):
		_sender = _sender.mouse_relative_motion(Vector2(1,1)).wait_frames(2)
		await _sender.idle
	
	await wait_frames(1)
	assert_eq(scene.get_node("./Label").position, Vector2(40,40) + Vector2(10,50), "Wrong label position")

Here, the sender._mouse_draw._draw_at get where the fake mouse cursor is drawn by the InputSender. So, I use it to return the fake mouse position by the stub.

This way, moving the real mouse cursor will not impact on the test, but the "follow the fake cursor moving part" of the label also works without changing any code of the original Node2D (in addition to add the private method).

PS: If your Node2D is translated or rotated, you need to use it's Transform2D to get the real position, because the _draw_at is the position of the mouse cursor on screen, not in the viewport/transform of the Node. Mine was at origin (0,0), so I didn't need it.

PS2: I updated the GUT to 9.3.0 to use the to_call on stub. It was not present on 9.2.1 as I used on the original report.

@bitwes
Copy link
Owner

bitwes commented Jul 25, 2024

If I'm understanding all this right, you are trying to verify that the label moves with the cursor. If that is the case, then I think you can simplify things a bit and not use the input sender at all. This is a little more in-line with a unit test approach, where as what you posted would be considered an integration test.

In this code, I put all the code into a Label to simplify for demo purposes, and to keep all the required code in a single blob. Since we've made a wrapper for getting the mouse position, then we can just verify that _process is doing the right thing based on wherever that method tells us the mouse is. This tests "our" code with less "testing of the engine".

extends GutTest

class SuperLabel:
	extends Label

	var offset: Vector2 = Vector2(10,50)

	func _get_mouse_position() -> Vector2:
		return self.get_viewport().get_mouse_position()

	func _process(_delta: float) -> void:
		var mouse_pos = _get_mouse_position()
		var label_pos = mouse_pos + offset
		position = label_pos


func before_all():
	register_inner_classes(get_script())

func test_something():
	var _sender = InputSender.new(Input)

	var scene = partial_double(SuperLabel).new()
	stub(scene._get_mouse_position).to_return(Vector2(50, 50))
	add_child_autofree(scene)

	await wait_frames(10) # 5 didn't give _process time to kick in
	assert_eq(scene.position, Vector2(50, 50) + scene.offset)

I think I need to add a blurb to the Input Mocking documentation that tells you not to use it unless you absolutely have to. I always want to use it. It's cool, it's fun. I wanna watch the cursor move around...but most of the time you can simplify things and test the code without having to involve input. A great example is pushing a button. I want to write a test that moves the mouse over a button and pushes the button down and then releases it to test my button press logic...but it's way easier, and tests the same thing, to just make your test do my_button.pressed.emit().

@bitwes
Copy link
Owner

bitwes commented Jul 25, 2024

Thought of this right after I hit "comment". You don't even need to await. You an use simulate to run _process a number of times with a desired delta value. Here, it will be run once and passed .1 for delta.

func test_something_using_simulate():
	var scene = partial_double(SuperLabel).new()
	stub(scene._get_mouse_position).to_return(Vector2(50, 50))
	add_child_autofree(scene)

	simulate(scene, 1, .1)
	assert_eq(scene.position, Vector2(50, 50) + scene.offset)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants