Begginer here using Godot Engine 3.5.2. I want to detect if the player clicks on the enemy image.

The enemy is composed of a Node2D that has an AnimatedSprite as a child, which is responsible for representing the enemy on the screen by playing the enemy's animations. So when the player clicks, I need to detect which image is being played by the AnimatedSprite and if the enemy is drawn at the mouse position, that is, avoiding detecting the enemy if the player clicks on the transparent area of the enemy image.

The hardest part for me is calculating the position of the image on the screen, since all of these variables can change during the game and influence it:

The scale and position values of the Node2D can vary.

The scale, position, offset, and flip_h values of the AnimatedSprite can vary.

I would also have to take into accound that the value of the centered property of the AnimatedSprite is always set to "1" or "True". That doesn't change.

I have tried all day with no success. This is the latest code I've tried, that is able to detect the enemy (opaque pixels), the transparent pixels and the "out of sprite" clicks but everything in the wrong positions:

				var mouse_pos = get_global_mouse_position()
				var local_pos = enemy_animated_sprite.get_parent().to_local(mouse_pos)
				var enemy_pos = enemy_animated_sprite.get_global_transform().xform_inv(local_pos)
				var size = enemy_animated_sprite.frames.get_frame(enemy_animated_sprite.animation, enemy_animated_sprite.frame).get_size()
				var pos = enemy_animated_sprite.offset
				if enemy_animated_sprite.centered:
					pos -= 0.5 * size
				var frame_rect = Rect2(pos, size)
				if frame_rect.has_point(enemy_pos):
					var frame_texture = enemy_animated_sprite.frames.get_frame(enemy_animated_sprite.animation, enemy_animated_sprite.frame)
					var frame_image = frame_texture.get_data()
					frame_image.lock()
					var pixel_pos = enemy_pos - pos
					var parent_scale = enemy_animated_sprite.get_parent().get_scale()
					var enemy_scale = enemy_animated_sprite.get_scale()
					pixel_pos.x /= parent_scale.x * enemy_scale.x
					pixel_pos.y /= parent_scale.y * enemy_scale.y
					if enemy_animated_sprite.flip_h:
						pixel_pos.x = size.x - pixel_pos.x
					var pixel_color = frame_image.get_pixel(pixel_pos.x, pixel_pos.y)
					frame_image.unlock()
					if pixel_color.a > 0:
						print("CLICKED ON ENEMY")
					else:
						print("clicked on transparent part of sprite")
				else:
					print("clicked out of sprite")
					pass
  • How about something like this:

    extends AnimatedSprite
    
    
    const HIT_ALPHA_THRESHOLD = .1
    
    
    func mouse_hit():
    	# get mouse local position (in sprite's local space)
    	var pos = get_local_mouse_position()
    	
    	# get current frame texture and its size
    	var texture = frames.get_frame(animation, frame)
    	var size = texture.get_size()
    	
    	# adjust local position if sprite is centered or flipped
    	if centered:
    		pos = pos + size*.5
    	if flip_v:
    		pos.y = size.y - pos.y
    	if flip_h:
    		pos.x = size.x - pos.x
    
    	# check if mouse position is out of sprite's texture bounds
    	if pos.x < 0 or pos.x >= size.x or pos.y < 0 or pos.y >= size.y:
    		return false
    	
    	# read the pixel under mouse from image data
    	var img = texture.get_data()
    	img.lock()
    	var alpha = img.get_pixel(pos.x, pos.y).a
    	img.unlock()
    	
    	# check if pixel is opaque
    	return alpha > HIT_ALPHA_THRESHOLD
    
    	
    	
    func _process(delta):
    	if mouse_hit():
    		print("hit")
    	else:
    		print("miss")

Sorry it looks like I also need help pasting code the right way. I copied from godot's editor, clicked on "insert code" button here and pasted the code but it lost the identation for some reason.

Place ~~~ on a line before and after the code to display it properly here. That's more reliable than using the insert-code widget.

    Three.

    Then shalt thou count to three, no more no less. Three shalt be the number thou shalt count, and the number of the counting shalt be three. Four shalt thou not count, neither count thou two, excepting that thou then proceed to three.

    How about something like this:

    extends AnimatedSprite
    
    
    const HIT_ALPHA_THRESHOLD = .1
    
    
    func mouse_hit():
    	# get mouse local position (in sprite's local space)
    	var pos = get_local_mouse_position()
    	
    	# get current frame texture and its size
    	var texture = frames.get_frame(animation, frame)
    	var size = texture.get_size()
    	
    	# adjust local position if sprite is centered or flipped
    	if centered:
    		pos = pos + size*.5
    	if flip_v:
    		pos.y = size.y - pos.y
    	if flip_h:
    		pos.x = size.x - pos.x
    
    	# check if mouse position is out of sprite's texture bounds
    	if pos.x < 0 or pos.x >= size.x or pos.y < 0 or pos.y >= size.y:
    		return false
    	
    	# read the pixel under mouse from image data
    	var img = texture.get_data()
    	img.lock()
    	var alpha = img.get_pixel(pos.x, pos.y).a
    	img.unlock()
    	
    	# check if pixel is opaque
    	return alpha > HIT_ALPHA_THRESHOLD
    
    	
    	
    func _process(delta):
    	if mouse_hit():
    		print("hit")
    	else:
    		print("miss")

      There's no reason to check pixel positions manually. Place an Area2D in the node and just check for collision with that.

        cybereality Sadly I need precision, the drawing I want to detect is too complex, changes each frame and has many different animations, so if I'm able to get the pixel detection working, it would be nice.

        xyz Thank you! I'm going to try to understand it and try it.

        xyz
        Alright that's it! Honor! Honor on your whole family. Make a note of this. Honor on you, honor on your cow...

        It worked flawlesly and I'm loving it. Now even if add a new enemy, a new animation, flip it, scale it, lick it, put it on fire and throw it out of the window, it's still being detected. Thank you.

        • xyz replied to this.

          Denxel With great power comes great responsibility, so use this wisely 😉
          But seriously, what @cybereality said; it's better to use colliders in most cases. For the player, pixel perfect collisions often feel too strict and possibly annoying. Well placed colliders with some margin for mistakes make interactions more forgiving.

          If you simply must go pixel perfect... well then, my cows shall take note of honors bestowed upon them 😃

            xyz Yes I must. I indeed use colliders in most cases. This is a special one and it's working very well so honor on you again.

            3 months later

            xyz Hi again! Your solution worked perfectly. But now I'm porting this little game to Godot 4 and I'm getting an error that I can't solve. So far I solved everything until this part:

            # read the pixel under mouse from image data
            	var img = texture.get_data() ------> **error: Invalid get index 'get_data' (on base: 'CompressedTexture2D').**
            	img.lock()
            	var alpha = img.get_pixel(pos.x, pos.y).a
            	img.unlock()

            The object was previously StreamTexture running your code on Godot 3.5, but now it is CompressedTexture2D on Godot 4, and that seems to cause the error. I should mention that I had to fix the part that gets the texture because it gives an error in Godot 4:
            I had to change this:

            # get current frame texture and its size
            	var texture = frames.get_frame(animation, frame)

            For this:

            var texture = sprite_frames.get_frame_texture(animation, frame)

              Denxel okay I solved it! In case someone ends here looking for the solution, what I did was changing this lines:

              	var img = texture.get_data() 
              	img.lock()
              	var alpha = img.get_pixel(pos.x, pos.y).a
              	img.unlock()

              for this:

              var img = texture.get_image()
              var alpha = img.get_pixel(pos.x, pos.y).a

              So basically removing the lock and unlock (not needed in G4) and geting the image through get_image() instead of get_data().