Pixel-Perfect Sprite Clicks

JoeStrout - Oct 13 '23 - - Dev Community

A common need in any graphical program is to detect clicks on a button or other sprite. The Mini Micro wiki contains an article on "How to detect a click on a sprite," but (at least at the time of this writing), it assumes a simple rectangular click area. What if your sprite is an odd shape, and you really need to detect clicks only on the visible part of the sprite?

In this case, we need pixel-perfect click detection. The basic idea is: first do your simple rectangular-bounds check; and then if that passes, check the pixel color of the sprite image at the location of the mouse. Consider it a valid click only if the pixel there is not transparent. (Or, if your sprite has some solid background color, then check for that color instead.)

Let's start by reviewing the simple rectangular check first. Launch Mini Micro, edit a fresh program, and paste in this short demo code:

clear
sp = new Sprite
sp.image = file.loadImage("/sys/pics/Wumpus.png")
sp.localBounds = new Bounds
sp.localBounds.width = sp.image.width
sp.localBounds.height = sp.image.height
sp.x = 480; sp.y = 320; sp.scale = 2
display(4).sprites.push sp

while true
    if sp.contains(mouse) then
        sp.tint = color.green
    else
        sp.tint = color.white
    end if
end while
Enter fullscreen mode Exit fullscreen mode

This sets up a sprite, loading our beloved Wumpus from the system pictures. It assigns a localBounds based on the image size, so that we can use the contains method to check whether the mouse is over those bounds. (This accounts for the position, scale, and rotation of the sprite automatically.) In our main loop, we tint the sprite green when the mouse is within the sprite bounds, and white when it is not.

Mouse-over test with simple rectangular bounds check

Now let's extend this to pixel perfection! Press control-C to break out of the previous demo, and edit again. Modify the code as follows, with a new overSolidPixel function to check whether a given x,y position (such as the mouse) is over a non-transparent pixel in the sprite image.

clear
sp = new Sprite
sp.image = file.loadImage("/sys/pics/Wumpus.png")
sp.localBounds = new Bounds
sp.localBounds.width = sp.image.width
sp.localBounds.height = sp.image.height
sp.x = 480; sp.y = 320; sp.scale = 2
display(4).sprites.push sp

Sprite.overSolidPixel = function(pos)
    x = (pos.x - self.x) / self.scale + self.image.width/2
    y = (pos.y - self.y) / self.scale + self.image.height/2
    c = self.image.pixel(x,y)
    text.row = 25; print c  // (for debugging)
    return c[-2:] > "88"
end function

while true
    if sp.contains(mouse) and sp.overSolidPixel(mouse) then
        sp.tint = color.green
    else
        sp.tint = color.white
    end if
end while
Enter fullscreen mode Exit fullscreen mode

This overSolidPixel function starts by converting the global x/y position given into coordinates local to the sprite image. (The code as shown assumes a non-rotated sprite with uniform scale. Extending it to support-non uniform scale and rotation is left as an exercise for the reader.) Then we look up the pixel color at that point in the sprite, and return whether the last two digits (the alpha channel) are greater than "88", i.e., are closer to fully solid (FF) than transparent (00).

Now when you run it, you'll find you can mouse over the corners or gaps in the sprite without it turning green. Only when the mouse is over a solid part of the sprite image, do we consider it really "over" the sprite.

Mouse-over test with pixel perfect check

So there you have it! The next time you find a rectangular bounds isn't good enough for click detection on your sprite, remember this technique, and you'll have pixel perfection in no time.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .