I have an array of dynamically generated 'planets' that need to be able to interact with the mouse. I have been trying to use the new Godot 4 signals format, but no joy yet.
The following code works correctly:
func _ready():
for planet in planets:
var planet_collision_shape = CollisionShape2D.new()
planet_collision_shape.shape = CircleShape2D.new()
planet_collision_shape.shape.radius = 27.5
planet_collision_shape.position = Vector2(planet._coord[0], planet._coord[1])
add_child(planet_collision_shape)
planet._body = planet_collision_shape
But attempts to implement a signal with the following code generates an error code of:
Invalid get index 'mouse_entered' (on base: 'CircleShape2D').
planet._body.shape.mouse_entered.connect(self._on_mouse_entered)
The node tree is nested as follows, with the dynamically generated nodes attaching to the Camera2D.
Node2D -> CanvasLayer -> Camera2D
********** EDIT **********
@ Theraot: Well, I genuinely thought I was close to a solution before your comment, but it seems that my code has confused even you. Here is the full script:
extends Node2D
var mouse_entered
@onready var camera = $"."
var planets = Global.planets
var zoom_minimum = Vector2(.5, .5)
var zoom_maximum = Vector2(2.5, 2.5)
var zoom_speed = Vector2(.1, .1)
var velocity = Vector2.ZERO
func _ready():
for planet in planets:
var planet_collision_shape = CollisionShape2D.new()
planet_collision_shape.shape = CircleShape2D.new()
planet_collision_shape.shape.radius = 27.5
planet_collision_shape.position = Vector2(planet._coord[0], planet._coord[1])
add_child(planet_collision_shape)
planet._body = planet_collision_shape
# #planet._body.mouse_entered.connect(self._on_mouse_entered)
func _on_mouse_entered():
print("BUMP")
func _draw():
for planet in planets:
var center = Vector2(planet._coord[0] + velocity.x, planet._coord[1] + velocity.y)
var size = planet._texture.get_size()
var rect = Rect2(center - size * .5, size)
draw_texture_rect(planet._texture, rect, false)
planet._body.position = center
func update():
queue_redraw()
func _process(_delta):
if Input.is_physical_key_pressed(65):
velocity.x -= 10
if Input.is_physical_key_pressed(68):
velocity.x += 10
if Input.is_physical_key_pressed(87):
velocity.y -= 10
if Input.is_physical_key_pressed(83):
velocity.y += 10
update()
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.is_pressed():
if event.button_index == 4:
if camera.zoom > zoom_minimum:
camera.zoom -= zoom_speed
print(camera.zoom)
if event.button_index == 5:
if camera.zoom < zoom_maximum:
camera.zoom += zoom_speed
print(camera.zoom)
At runtime the tree looks as follows:
The above code results in planet icons that move with their respective collision shapes as follows:
Since I have the collision shapes where I want them I belived that my next step was to connect a signal so I could tell when the mouse was over a planet. Any advice on how to proceed from here greatly appreciated.
As you have surely already figured out by the errors, neither CollisionShape2D
nor CircleShape2D
. We can approach this a few ways.
You would make a Planet
scene, with these Node
s
Planet
├ CollisionShape2D
└ Sprite2D
The root of the scene (i.e. Planet
) would be a StaticBody2D
(or perhaps a some other kind of body), and it would have the mouse_entered
signal.
The CollisionShape2D
would have the CircleShape2D
you want set from the editor. But don't position it. You will position the StaticBody2D
instead.
And the Sprite2D
would be responsible for rendering the texture. So with this approach would not be using _draw
to render the textures.
Since both the CollisionShape2D
and Sprite2D
are children of the StaticBody2D
(Planet
), when it moves they move with it.
Once you have the scene, it would be saved to a scene file (e.g. a ".tscn" file) which you can instantiate multiple times, and set its properties (change its position and change the texture of the Sprite2D
) either from code or in the editor (you can set editable children from the context menu in the scene panel, which will allow you to change the Sprite2D
texture).
So, as you can see this approach requires no code for the planets.
Using _draw
has its advantages. For once you don't need to instantiate as many objects (since you don't have any Sprite2D
).
But what about physics? That is the issue here… You don't have a physics body that can do a point-shape intersection/collision which the mouse signals need…
So, you could do this the hard way. We can get mouse input events in _input
and check if they enter any planet. Thankfully you only need point-circle collision, which is easy. It would be something like this:
func _input(event:InputEvent) -> void:
if not event is InputEventMouse:
return
var mouse_event := event as InputEventMouse
for planet in planets:
var center = Vector2(planet._coord[0] + velocity.x, planet._coord[1] + velocity.y)
var radius = planet._texture.get_size().x * 0.5 # 27.5
if mouse_event.position.distance_to(center) < radius:
# the mouse pointer is over the planet
break
If you want actual signals you can do it like this:
signal mouse_entered()
signal mouse_exited()
var _mouse_is_inside:bool
func _input(event:InputEvent) -> void:
if not event is InputEventMouse:
return
var mouse_event := event as InputEventMouse
var is_inside := false
for planet in planets:
var center = Vector2(planet._coord[0] + velocity.x, planet._coord[1] + velocity.y)
var radius = planet._texture.get_size().x * 0.5 # 27.5
if mouse_event.position.distance_to(center) < radius:
is_inside = true
break
if _mouse_is_inside != is_inside:
_mouse_is_inside = is_inside
if _mouse_is_inside:
emit_signal("mouse_entered")
else:
emit_signal("mouse_exited")
If the motivation to use _draw
is optimization. Then we can do much better than the approaches above. And we do that by using the RenderingServer
for drawing and the PhysicsServer2D
et.al. for physics. In other words, we can use a lower level API. This approach does not use Node
s for the planets at all. You probably don't need this unless you will have planets in the thousands.
We can create a physics body like this:
body=PhysicsServer2D.body_create()
And you are responsible for freeing it like this:
PhysicsServer2D.free_rid(body)
My recommendation is to use _enter_tree
for initialization, and _exit_tree
for tear down.
You need to assign a space to your body, so we use the one from your Node2D
(where the script is running):
PhysicsServer2D.body_set_space(body, get_world_2d().space)
And you want to give a shape to the body, which is something like this (here shape
would be the CircleShape2D
):
var disabled := false
PhysicsServer2D.body_add_shape(body, shape.get_rid(), Transform2D.IDENTITY, disabled)
If you prefer to not use CircleShape2D
, you can use circle_shape_create
and configure it with shape_set_data
:
var radius := 27.5
var shape_rid := PhysicsServer2D.circle_shape_create()
PhysicsServer2D.shape_set_data(shape_rid, radius)
To position the body you do this:
PhysicsServer2D.body_set_state(body, PhysicsServer2D.BODY_STATE_TRANSFORM, global_transform)
And for the visual part, create a canvas item:
var ci_rid := RenderingServer.canvas_item_create()
We are going to make it a child of your Node2D
(where the script is running):
RenderingServer.canvas_item_set_parent(ci_rid, get_canvas_item())
We can load
our Texture
and set it:
var texture:Texture = load("res://icon.png")
texture.reference()
var texture_rid := texture.get_rid()
This is intended to be a workaround to prevent Texture
being freed due not being references to it, and thus invalidating the rid. Another way to go about it is to make a copy:
var texture:Texture = load("res://icon.png")
var image:Image = texture.get_data()
var texture_rid := RenderingServer.texture_2d_create(image)
And you can add the texture, and of course position the canvas item:
RenderingServer.canvas_item_add_texture_rect(ci_rid, rect, texture_rid)
RenderingServer.canvas_item_set_transform(ci_rid, Transform2D.IDENTITY)
And you can keep calling PhysicsServer2D.body_set_state
and RenderingServer.canvas_item_set_transform
to move the planet.
Ah, and you need to know if the mouse enters… Once again we will resource to _input
, this time we use intersect_point
to find what is below the mouse pointer, which gives us an Array
of objects, and we look for one that matches the rid of our body. If we find it then the mouse pointer is inside:
signal mouse_entered()
signal mouse_exited()
var _mouse_is_inside:bool
func _input(event: InputEvent) -> void:
if not event is InputEventMouse:
return
var mouse_event := event as InputEventMouse
var viewport := get_viewport()
var position:Vector2 = viewport.get_canvas_transform().affine_inverse() * mouse_event.position
var query := PhysicsPointQueryParameters2D.new()
query.position = position
var objects := get_world_2d().direct_space_state.intersect_point(query, 32)
var is_inside := false
for object in objects:
if object.rid == body:
is_inside = true
break
if _mouse_is_inside != is_inside:
_mouse_is_inside = is_inside
if _mouse_is_inside:
emit_signal("mouse_entered")
else:
emit_signal("mouse_exited")
Note: this code is adapted from Godot 3 to 4 because I haven't got that deep into Godot 4 yet. If it gives you problems let me know.