How to make a sprite transparent to the non-sprite background without leaving a trail?

When I move my sprite (or assign a new image to it) and update it, the old version remains on the screen too.

This is despite doing gfx.clear(gfx.kColorBlack) before updating the sprite. (Clearing the whole screen is fine—everything is being redrawn anyway.)

I also tried doing the background drawing in setBackgroundDrawingCallback (which I think I don't need since I always redraw the whole screen), but it makes no difference.

What's my basic mistake?

(Previously I only ever had sprites over a big screen-sized sprite, which works fine. But now I have this sprite over the background—which gets erased and redrawn right before the sprite is changed. Yet the trace remains.)

TIA!

Whoa, that's really weird. I can't imagine how you're getting the previous state of the display leaking through if you're clearing the screen before the sprite update. Can you distill this down to a simple demo? Is there anything unusual that you're doing outside of the standard "move sprites around/change their image, then call gfx.sprite.update()" routine in playdate.update()?

1 Like

I can get rid of the "trail" by combining BOTH things I tried: putting the clear() inside a setBackgroundDrawingCallback.

I'm not sure how it all works to understand (yet) the "why" of it—I'm thinking I shouldn't even need a setBackgroundDrawingCallback since I'm clearing the entire screen every time.

But, knowing that this works, is it now less weird-sounding?

If it's still weird, yes, I will distill it down to something simpler to share here! (There isn't much else going on I don't think.)

Even though it now works for my current situation, I'd love to understand what's happening. (What if I didn't want to redraw the entire background every frame, but only draw changes as needed? How could I have transparent sprites over the background then?)

I haven't ever used setBackgroundDrawingCallback myself so I don't know if there are any gotchas there. Including an empty callback there doesn't cause the problem..

import "CoreLibs/sprites"

gfx = playdate.graphics
s = gfx.sprite.new()
s:setImage(gfx.getSystemFont():getGlyph("X")) -- getGlyph() is new in 1.12
s:moveTo(200,120)
s:add()

gfx.sprite.setBackgroundDrawingCallback(function() end)

function playdate.update()
	s:moveTo(s.x+math.random(-3,3), s.y+math.random(-3,3))
	gfx.sprite.update()
end

My first thought when you said you were getting trails was you'd set the background color to clear, but if you're calling gfx.clear() every frame that wouldn't matter. :thinking:

I'd rather not use setBackgroundDrawingCallback (doesn't seem necessary) it was just something I tried. So I've removed it now to keep troubleshooting.

I notice I only get trails if I change the sprite's image--as in the text on the right here:
Phi trails
And the trail only lasts ONE frame, not forever.

What the heck could cause these symptoms??

I am stumped! I've ported my background graphics and sprite code over to your example (many thanks!) as best I can and I do NOT see the problem there.

Structurally my app is complex (re-assigning paydate.update, running things on timers) but graphically it's not. I'm just drawing some circles, with a bit of text overlaid as sprites.

I have a theory: maybe it's all in the sprite's image: every time I change the image, it is drawing ON TOP of the previous image. Just one version prior, not cumulative. And the old image is being applied at the OLD x,y location, not "staying with the sprite" as it moves.

Why might that happen?

I'm using a brand new local image each frame:

	local textImage = gfx.image.new(textWidth, compTextH)
	
	gfx.pushContext(textImage)
		gfx.clear --(Shouldn't be needed on a new image, didn't help anyway)
		gfx.setFont(font)
		gfx.drawTextAligned(text, internalAlignX, 0, textAlign)
	gfx.popContext()
	
	textSprite:setImage(textImage)

But I moved that code to your example and it doesn't cause the problem there.

(I've also tried marking the sprite dirty--no help.)

Is there any chance you've got multiple sprites stacked there? This looks like it's got 9:15, 9:16, and 9:17 all together (though maybe it's just 16 instead of 9:16):

Screen Shot 2022-06-16 at 1.17.37 PM

One thing you could do here is use a solid background on textImage instead of clear by giving a gfx.kColorBlack argument to image.new(), but that's really just obscuring whatever the underlying problem is.

1 Like

Just one sprite for sure—I'm changing the text each frame (which is the trail I REALLY care about--the motion is just an extra test).

And you're right, there are at least THREE frames stacked. So odd.

And it's not limited to the sprite's image bounds: I increase the motion to ± 20 and there's no clipping of the "trail."

I'll keep trying things. A trail that only lasts a few frames is so specific, I can't imagine what could cause it! I'm not doing anything on a multi-frame cycle, the same code runs every frame.

OK! I've got a simple reproducible case... when I draw text into two sprites, the SECOND ONE in the code always has the trail/ghost effect. But the first one does not!

(Note: if I put another sprite in back, the problem goes away. Only happens over non-sprite backgrounds.)

This main.lua will show the problem:

import "CoreLibs/graphics"
import "CoreLibs/timer"
import "CoreLibs/sprites"
local pd <const> = playdate
local gfx <const> = pd.graphics

gfx.setBackgroundColor(gfx.kColorClear)
pd.display.setRefreshRate(10)

comp1 = gfx.sprite.new()
comp1:add()

comp2 = gfx.sprite.new()
comp2:add()

function pd.update()
	gfx.clear(gfx.kColorBlack)

	-- PROBLEM IS HERE:
	-- EVEN IF NEXT 2 LINES ARE SWAPPED, 2ND ONE ALWAYS MAKES TRAILS
	showText(comp1, 100,100, "Test 1")
	showText(comp2, 200,200, "Test 2")
end

function showText(textSprite, alignX, alignY, text)
	textSprite:setSize(64, 28)
	textSprite:moveTo(alignX + math.random(-10,10), alignY + math.random(-10,10))
	
	local textImage = gfx.image.new(64, 28)
	gfx.pushContext(textImage)
		gfx.setImageDrawMode(gfx.kDrawModeFillWhite)
		gfx.drawText(text, 0, 0)
	gfx.popContext()
	
	textSprite:setImage(textImage)
	textSprite:update()
end

That code should draw "Test 1" and "Test 2" in the same way, but instead...

Sprite Trail Test

Stranger and stranger... What am I missing? :exploding_head:

Full project attached—but that main.lua is the whole thing anyway
Phi Trails Test.zip (9.2 KB)

Ah! Instead of calling textSprite:update() in showText, you want a single gfx.sprite.update() call at the end of pd.update. Because Lua, textSprite:update() is the same as a call to gfx.sprite.update(textSprite), and the sprite.update function ignores the argument. What's going on is you're setting the image on the first sprite, calling sprite.update() which draws everything, then setting the image on the second and calling sprite.update() again, drawing the sprites in their second state. Since the background color is kColorClear, it doesn't erase the dirty rect between the two calls.

I'll file an issue to throw a warning if you call gfx.sprite.update() with an argument.

4 Likes

Many thanks! I 70% understand, and have 100% implemented the fix!

The docs here confuse me a bit:
graphics.sprite.update

In one sentence it sounds like sprite:update does nothing, but is called by sprite.update for you to do things with. In another, it sounds like it simply updates a single sprite instead of all of them (which sounded like good efficiency to me).

In any case, it looks like the issue wasn't even just with that, but WHERE I was updating the sprites. Even doing sprite.update within my showText() instead of sprite:update would cause the problem. But moving the sprite.update out of that function and into playdate.update did the trick!

(I have done sprite.update in places other than playdate.update before, and it has worked as I intended... is the problem in this case that I was doing two updates in the same frame?)

Oh man. It never crossed my mind that having the global sprite update function and the per-sprite update callback use the same name would be a problem, but there it is, no wonder you were confused. I should have used different names. The short of it is, you call gfx.sprite.update(), and that calls back to the individual sprites' update() functions if they exist. You shouldn't need to call those yourself. And if they don't exist, then calling them actually calls gfx.sprite.update() as we've discovered here.

Also amending that issue to also make the docs clearer about this..

2 Likes

Cool—now that I get it, sprite:update sounds very useful to keep in mind!