I’m developing a turn-based strategy game and need to implement pixel-perfect unit hovering. The units use animated 2D sprites with spritesheets for frames. This seems like a common issue for classic sprite 2D , but I haven’t definitive solutions for animated sprite.
I’ve founds two main options:
Area2D with Collision Polygons
Classic method for mouse-sprite interaction. Variables to consider: runtime vs. preprocessed, specific animations vs. all animations, single frames vs. all frames.
As I will have hundreds of unit types, i’d like to not preprocess collision polygons but generate them at runtime
Generating polygons for every frame of current animation at runtime would be ideal but may be too resource-intensive for medium-spec computers.
An more optimized way is to generate polygons only for idle animations (hovering during unit movement is not usefull for the player).
An even more optimized way is to generation the collision polygon and rotate it based on unit orientation.
Pixel Alpha Check with Image.get_pixel()
- Detect hover by checking if the sprite pixel under the cursor is opaque.
- I am Less familiar with this method, so I have not much to say yet.
Here are several points against it needing to be “pixel perfect”
Sprites being animated means pixels inside sprites won’t be persistently opaque or transparent. What should happen if the player aims at an opaque pixel but it turns transparent when they click? The other way around?
Player really won’t be hunting individual pixels to click on, and in fact you expecting them to may make the interaction frustrating.
A formally opaque area will not in all cases coincide with what visually is perceived as a clickable area.
I think you should define meaningful areas for your units by hand. A little extra work on top of creating the units themselves. As a compromise, you could generate smaller transparency masks from the sprites, and test the value in those on the fly. Or a combination of the two, that would allow some manual adjustment for the tricky cases.
func _is_pixel_opaque(global_mouse: Vector2) → bool:
# Create or get cached image MAYBE TO REMOVE
if animation != _cached_animation or frame != _cached_frame:
_cached_animation = animation
_cached_frame = frame
var tex = sprite_frames.get_frame_texture(animation, frame)
_cached_image = tex.get_image() if tex else null
var image = _cached_image
if not image:
return false
var tex_size = Vector2(image.get_width(), image.get_height())
var local = to_local(global_mouse)
var pixel_pos = local - offset
# Flip
if flip_h: # PAS SUR QUE CE SOIT UTILE
pixel_pos.x = tex_size.x - pixel_pos.x
if flip_v:
pixel_pos.y = tex_size.y - pixel_pos.y
# Bounds check
if pixel_pos.x < 0 or pixel_pos.y < 0:
return false
if pixel_pos.x >= tex_size.x or pixel_pos.y >= tex_size.y:
return false
var a = image.get_pixel(int(pixel_pos.x), int(pixel_pos.y)).a
return a > 0.1