Checking if sprites are 'covered' in 2D game

Godot Version

4.3

Question

Still new to Godot and this kind of coding in general, so hoping there’s a simple solution to this. This is just a simple test project for now, but I’m trying to make it so that you can ‘erase’ sprites on a screen by drawing over them. For now, I’m doing this by letting the player draw circles with the mouse to a blank node like so:

…and then using clipping to reveal a copy of the background in the same place to make it look ‘erased’:

So my question is this: how could I test to see if the sprite is fully covered/‘erased’?

Ideally I’d even like a way of checking how ‘much’ of the sprite is covered at any given time, but I can’t immediately think of a way that doesn’t involving checking every single pixel in the area. Any help with this would be greatly appreciated!

Bonus question: right now I’m storing the drawn-over area as just… well, a list of circles, as shown below. This works, but I’m concerned that this involves redrawing all of them every time the mouse is clicked. Is there a way of, say, taking everything drawn every frame and storing it as an actual texture? That seems like it would be more efficient in the long run…

Thanks in advance!

extends Node2D

var white : Color = Color.WHITE
var _circ_cent : Vector2
var _circ_array: Array

func _process(_delta):
	var mouse_position = get_viewport().get_mouse_position()
	_circ_cent = mouse_position
	if Input.is_action_pressed("mouse_click"):
		_circ_array.append(_circ_cent)
		queue_redraw()

func _draw():
	for circ in _circ_array:
		draw_circle(circ, 20, white)

Image processing is fun, had to do some 20yrs ago when I learned some Java (thankfully long forgetten) at Uni. Edge detection and stuff like that. You have more than one way here, depending on what your long-term goal is.

First of all Godot’s Image has get_pixel/set_pixel. That allows you to manipulate the image directly. It’s two loops and you might get a very long way as you only do it on user input and even then, circles? you can limit the area you loop over. You can even blend images into a composite as shown in this tutorial video

You could also use Viewport, but your’s doesn’t sound like it needs that.

Secondly find yourself a nice simple image processing library written in C++ like libvips, OpenCV imgproc or CImg and bind it into your game. This offers the most features, future image manipulation will be easier to add and binding a library into your game is a fantastic exercise.

Thirdly if you need more speed that Godot’s Image but not the features of an external library: Shaders are not just for making games look nice, they are also for useless crap like crypto. A computer shader will allow you to loop over your image’s pixel much faster and return the finished image to you.

1 Like

Thank you, this is very helpful!! Looks like I’ve got some reading to do :))

Personally, I’d be tempted to keep a PackedByteArray with one byte for every 8 pixels in a sprite. When you make the sprite, init the byte array with a bit set for every opaque(ish) pixel in the sprite. When the player obscures a pixel in the sprite, zero the corresponding bit in the byte array. When there are no bits set in the array (that is, every byte is zero), the sprite is completely obscured:

#NOTE: Pseudocode

var Mask: PackedByteArray

func _ready():
    Mask = PackedByteArray()

    # ideally you'd precompute this and load it rather than generate it...

    for len in ((sprite_x * sprite_y) / 8): # might need to round this up...
        Mask.append(0)

    for y in sprite_y:
        for x in sprite_x:
            val pos = (y * sprite_x) + x  # Bit offset of the pixel
            val idx = pos >> 3            # Offset of the byte containing the bit.
            val bit = 1 << (pos & 7)      # Set the appropriate bit in our byte 

            if opaque(sprite_pixels[pos]):
                Mask[idx] |= bit          # Set the bit

func completely_erased() -> bool:
    for i in Mask.size():
        if Mask[i]: return false # If any bit in any byte is set, something remains.

    return true

func erase_pixel(x, y):
    if x < 0 || x >= sprite_x || y < 0 || y > sprite_y: return  # Bounds check.

    var pos = (y * sprite_x) + x # Bit offset of the pixel
    var idx = pos >> 3           # Offset of the byte containing the bit
    var bit = 1 << (pos & 7)     # Set the appropriate bit in our byte

    Mask[idx] &= ~bit            # Invert our byte, AND with it to remove the appropriate bit
1 Like