I have many overlapping Area2Ds. How can I detect which one is in the foreground? What's the best approach / design? (Includes video demo.)

Godot Version

v4.2.2.stable.fedora [15073afe3]

Question

I’ve made this small demo to help me learn Area2Ds.

Please just watch the video, it is short.

I have 3 overlapping shapes, like a Venn diagram, and I want them to react when the mouse hovers over them. I want only the shape that is in the foreground to react to the mouse hover.

This seems surprisingly complicated to do in Godot, but maybe I’m missing something? How would you approach this problem?

Here’s the code from the video if you want to attempt solving this yourself: godot_100/030_hover_effect_on_circles at main · DevJac/godot_100 · GitHub

K, I’ll give it a spin.

Thanks. I anticipate some custom input handling or something. I know it might not be the simplest problem to solve but I appreciate the help.

The main thing that needs to happen is keeping track of who is on top. Since they are are all on the same z index and canvas layer the rendering order is done by first ready, last rendered.

I added static class information to keep track of who is selected and who has rank. when ever the mouse enters/exits we need to notify all the cards.

This is my attempt. Since there was no code to reorder nodes I didn’t take that into consideration.

Code
extends Node2D
class_name Circle

#---------------------------------
static var selected : Array[Circle] = []
static func push(cir:Circle):
	selected.push_back(cir)
	selected.sort_custom(func(a:Circle,b:Circle) : return a.rank > b.rank )
	for c in selected:
		c.hover_changed()

static func remove(cir:Circle):
	selected.remove_at(selected.find(cir))
	cir.hover_changed()
	for c in selected:
		c.hover_changed()

static var count : int = 0
#---------------------------------

var rank : int = 0

func _ready() -> void:
	$Area2D.connect('mouse_entered', mouse_entered)
	$Area2D.connect('mouse_exited', mouse_exited)
	rank = Circle.count # first ready, last rendered
	Circle.count += 1;

func hover_changed():
	if not Circle.selected.is_empty() and self == Circle.selected.front():
		$Sprite2D.scale = Vector2.ONE * 1.15
	else:
		$Sprite2D.scale = Vector2.ONE


func mouse_entered():
	Circle.push(self)

func mouse_exited():
	Circle.remove(self)

If i were to do this myself I would explore a highlight manager outside of the card class. This would be a ray cast query into 2d space to get selected nearby areas, then do a simpler selected/deselected mechanism. ( this would be contingent on how the ray collides with the areas, but I assume it will be deterministic with either the appropriately rendered on first, or on top, in the ray query) I will explore this next.

Thank you. We’ve been discussing this on Discord as well: Discord

A big realization for me was PhysicsDirectSpaceState2D — Godot Engine (stable) documentation in English

PhysicsDirectSpaceState2D contains many of the functions I thought must exist, but I couldn’t find them anywhere. I was thinking the Godot API was just immature and missing these functions, but I just couldn’t find them. I feel a lot better about it now.

So i found out that the physics query is nondeterministic and would flipflop on different bodies on top.

here is my code

CardSelectorManager
extends Node2D

var selected : Circle = null
func _unhandled_input(event):
	if event is InputEventMouseMotion:
		var space_state = get_world_2d().direct_space_state
		var query = PhysicsPointQueryParameters2D.new()
		query.position = event.global_position
		var result = space_state.intersect_point(query)
		if result.is_empty():
			if is_instance_valid(selected):
				selected.highlight(false)
				selected = null
			return
		result = result.filter(func(r) : return r.collider is Circle)
		var top = result.pop_back().collider as Circle
		top.highlight(true)
		#if is_instance_valid(selected) and selected != top:
		#	breakpoint
		selected = top
		
		for res in result:
			res.collider.highlight(false)



Circle
extends StaticBody2D
class_name Circle


func highlight(is_highlted:bool):
	if is_highlted :
		$Sprite2D.scale = Vector2.ONE * 1.15
	else:
		$Sprite2D.scale = Vector2.ONE

image
for this to work easier i had to switch to static bodies.

Update: this is just a thought, since input is checked with each substep of the physics server it may be more stable if we did the query during the physics process. (Will test later)

I was unsuccessful with the ray cast approach it seems to me something similar to z fighting.

bonus: i also tried commanding an area2d selection point and use area overlap, it was more predictable but the order was based on contact order. every new contact would be pushed to the back of the array.

I think the only approach is to imbue the circles with some metadata that maps to their render order. this would most likely be the z index, which would also guarantee their render order.

If you didn’t want to do that, making a hybrid system that renders a 2d/3d scene with physical offsets could make raycast work.

I put this in the root node of my project (which is the parent of the 3 circles):

extends Node2D


func point_query(p: Vector2) -> PhysicsPointQueryParameters2D:
	var q := PhysicsPointQueryParameters2D.new()
	q.collide_with_areas = true
	q.collide_with_bodies = false
	q.position = p
	return q


func _process(_delta) -> void:
	var space_state := get_tree().root.world_2d.direct_space_state
	var ips := (
		space_state
		.intersect_point(point_query(get_global_mouse_position()))
		.map(func (ip): return ip.collider.get_parent()))
	ips.sort_custom(func (a, b):  return -a.get_index() < -b.get_index())
	for c in get_children():
		if ips and c == ips[0]:
			c.find_child('Sprite2D').scale = Vector2.ONE * 1.15
		else:
			c.find_child('Sprite2D').scale = Vector2.ONE

This is the only code in the project and demonstrates one solution.

The simplest for what this is just get the last node in the tree. Basically check who cam last, because will be displayed top. Another way would be to use z-index.

add this code in Main node (e.g. main.gd). Also check how you could use z_index.

extends Node2D

var last_id: int

func _ready():
	last_id = get_last()
	pass
		
func get_last() -> int:
	var last_id: int
	for i:Node2D in get_children():
		last_id=i.get_instance_id()
		print(last_id)
	return last_id

Then in circle.gd modify to this code:

func mouse_entered():
	if $"..".last_id==self.get_instance_id():
		print("Scale", self.name)
		$Sprite2D.scale = Vector2.ONE * 1.15

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.