Removing a previously set background drawing callback results in a crash

The SDK provides a convenience for drawing a background via playdate.graphics.sprite.setBackgroundDrawingCallback(drawCallback). However, there is no documented method for removing it once set. I can achieve the result I'm after by setting it to a no-op function (function() end) but that feels like a hack. Given that it's a callback, I expect to be able to just set it to nil, as most executors of optional callbacks will check for nil before attempting to call them. However, calling setBackgroundDrawingCallback(nil) results in a runtime crash.

I'm honestly not sure if this is a bug, a feature request, or just a misuse of the API on my part, so please let me know if this belongs under another topic. My use case is setting a background for various levels, but sometimes a background isn't needed and I'm looking for the recommended way to clear it.

This method is often misunderstood as being a little more magical than it really is; all it does is create and configure a new sprite, and assigns drawCallback to the new sprite's drawing callback. (it's defined in CoreLibs/sprite.lua, and actually the entire source is right in the documentation if you would like to see what it does (See: Inside Playdate).

The newly-created sprite is returned from the function, so to remove it you can call :remove():

local bgSprite = playdate.graphics.sprite.setBackgroundDrawingCallback(drawCallback)
...
bgSprite:remove()

That said, I think you're right that passing nil should remove a previously-set background drawing callback. Looking at this also made me realize that if this method is called more than once, you'll end up with multiple background sprites, which feels like a bug to me. I'll file an issue and get a fix ready for this stuff as soon as possible!

4 Likes

Thanks Dan, that's clarifying. And I apologize, I should have dug a little deeper to check my assumptions. In fact, I did go read the docs, and the main documentation (in bright white text) actually supported my assumption that this would just configure the inner draw loop for the background object. I then glossed over the implementation details section, which would have corrected my seemingly reinforced but ultimately incorrect understanding.

That said, the behavior does go somewhat against my expectations! I think naming plays a part in that, because the side effect of creating a new sprite instance is non-obvious. My mental model was that this would set, overwrite, and hopefully "unset" the background drawing routine for a singular, static background object. The only "magic" I assumed was that the background object would live outside of the standard sprite list, as a singular special-case object which you could update the drawing routine for at any time.

Even if the functionality still manifests as a regular sprite, updating the behavior to avoid creating multiple instances, and to support passing nil (either to remove it, or at least result in a no-op draw handler that appears like a cleared background) sounds like an improvement. Thanks again!

1 Like

I agree that the naming of the method suggests the usage you were expecting, and that's how it should behave.

I think this will be the new implementation, if you want to replace the implementation in CoreLibs before it makes it into an SDK release:

function spritelib.setBackgroundDrawingCallback(drawCallback)
	
	if drawCallback == nil then
		if bgsprite ~= nil then
			bgsprite:remove()
			bgsprite = nil
		end
		return nil
	end
	
	if bgsprite == nil then
		bgsprite = gfx.sprite.new()
		bgsprite:setSize(playdate.display.getSize())
		bgsprite:setCenter(0, 0)
		bgsprite:moveTo(0, 0)
		bgsprite:setZIndex(-32768)
		bgsprite:setIgnoresDrawOffset(true)
		bgsprite:setUpdatesEnabled(false)
		bgsprite:add()
	end
	
	bgsprite.draw = function(s, x, y, w, h)
		drawCallback(x, y, w, h)
	end
	
	return bgsprite
end
4 Likes