Understanding sprite copy followed by set image oddity

SDK 1.9.3 - Linux

Hi all,

I am creating a game in which I require a lot of similar sprites. Ideally, I would create a single original sprite with all it's properties and attributes pre-set, then when a similar sprite is needed, copy the original into a new sprite and making any changes where necessary instead of recreating the whole sprite completely from scratch.

Lucky for me, the documentation does show a copy method for a sprite, but it has wording I didn't expect:

[M] playdate.graphics.sprite:copy()
Returns a copy of the caller.

Is it a typo? Should it say "Returns a copy of the sprite?". Other copy methods in the documentation seems to have consistency, so is this one somehow different?

In any case I attempted to use it and it seemed to work as expected. :playdate:

However :playdate_dead: when attempting to change the newly copied sprite's image as part of an animation, I start to see some issues.

To demonstrate, here is some working test code:

function setup()
  imgs = {}
  imagTbl = gfx.imagetable.new("body")
  for i = 1, #imagTbl do
    imgs[i] = imagTbl[i]
  end

  sprite1 = gfx.sprite.new(imgs[1])
  sprite1:setCollideRect(0, 0, sprite1:getSize())
  sprite1:moveTo(50, 50)
  sprite1:add()

  sprite2 = gfx.sprite.new(imgs[1])
  sprite2:setCollideRect(0, 0, sprite2:getSize())
  sprite2:moveTo(75, 50)
  sprite2:add()

  aniTimer = playdate.timer.new(500)
  aniTimer.repeats = true
  aniTimer.timerEndedCallback = function ()
    if sprite1:getImage() == imgs[1] then
      sprite1:setImage(imgs[2])
      sprite2:setImage(imgs[2])
    else
      sprite1:setImage(imgs[1])
      sprite2:setImage(imgs[1])
    end
  end
end

Here I simply create 2 sprites each with a set image (from an image table consisting of 2 different images for animation), set collide bounds (for easier demonstration) and set location. I have also created an animation timer that will change the images of both sprites every half a second.

The working code looks something like this:

working-yes

Now if I change the sprite creation from a sprite .new([image]) to a sprite :copy() like so:

sprite2 = sprite1:copy()

I then get something like this:

working-no

As you can see the second sprite is there, but it's bounds have offset to the sprites centre and also the original image from the sprite is replaced with a blank image (I'm assuming this is nil or something like that).

This happens every time a different image is being set to the sprite. I know this because if I comment out the animation timer completely, the second sprite will be showing on screen alongside the first, albeit not animating.

I see this oddity in my game too where sprites that are copied and don't have their images changed worked perfectly fine, but the second they do, they exhibit the same behaviour as explained above.

Now I have created other posts on this forum and others have graciously pointed out that I have either misunderstood what a given method is doing or that I didn't read the documentation correctly of which I'm happy to be told the same again.

I do understand that this is clearly something to do with the :copy() method as it is the clear outlier, but even with the strangely worded documentation, I still don't quite understand what is going on and why the change of it's image makes it behave this way.

Short of implementing my own copy method in my game (or being confident enough to say that this is some sort of a bug), I was wondering if anyone had any insight that might help me get to the bottom of this?

For reference here is the same code packaged up for you, including the image table file.

example.zip (881 Bytes)

Thanks in advance :playdate_goofy:

1 Like

This looks like a bug to me on macOS. Specifically, that calling :setImage() on a sprite created with sprite:copy() sets that sprite's size to (0,0). It looks like the copied sprite still has a valid image with size (16,16), but for whatever reason that's not translating to the sprite's size anymore.

I left sprite2 alone, and made sprite3 = sprite1:copy(), and you can detect when the size changes to something you don't expect:

Might be worth filing this in the Bug Reports section.

1 Like

Thanks @downie,

I went back to my example code in Linux and replicated your console output and I can confirm the same does happens on my end. I will be in a bit of a rush this week and I was hoping to dive deeper to see if I'm are able to force the size as a workaround fix, or even dive deeper into the copy method. I will see if I can do something a bit later in the day or week.

For now I will just mention this in the bug report section and tie it back round to this post, hopefully we'll get some more traction.

Thanks.

This may be unrelated but while I was looking to repro this I also noticed that a sprites size is (0, 0) when you use playdate.graphics.sprite.new(image) which doesn't seem correct either. It is correct when you use setImage though. And +1 to being able to reproduce this issue.

2 Likes

Hi @ericlewis ,

I was not able to replicate your issue you describe in the first part of the post. Checking the size of the sprite after the use of new I could only get (0,0) if I don't specify the image in the first place which makes sense. Only after setting the image does it work correctly.

sprite1 = gfx.sprite.new(imgs[1])
print("sprite1 size", sprite1:getSize()) -- output: 16.0 16.0
sprite1:setImage(imgs[2])
print("sprite1 size", sprite1:getSize()) -- output: 16.0 16.0

sprite2 = gfx.sprite.new()
print("sprite2 size", sprite2:getSize()) -- output: 0.0 0.0
sprite2:setImage(imgs[2])
print("sprite2 size", sprite2:getSize()) -- output: 16.0 16.0

sprite3 = sprite1:copy()
print("sprite3 size", sprite3:getSize()) -- output: 16.0 16.0
sprite3:setImage(imgs[2])
print("sprite3 size", sprite3:getSize()) -- output: 16.0 16.0

Strangely enough looking at sprite 3 in this example, the size of the sprite is correct after using both the copy and the setImage which might look like I've invalidated my own original post, but I do want to point out that the code above is still done in some sort of an initialiser method.

Once the update loop kicks in and I wish to change the image of sprite 3, I get the same issue described by @downie.

timeCounter = 0

aniTimer = playdate.timer.new(500)
aniTimer.repeats = true
aniTimer.timerEndedCallback = function ()
  timeCounter += 1
  if sprite3:getImage() == imgs[1] then
    print("Sprite3 before size", timeCounter, sprite3:getSize())
    sprite3:setImage(imgs[2])
    print("Sprite3 after size", timeCounter, sprite3:getSize())
  else
    print("Sprite3 before size", timeCounter, sprite3:getSize())
    sprite3:setImage(imgs[1])
    print("Sprite3 after size", timeCounter, sprite3:getSize())
  end
end

output

16:52:36: Loading: OK
16:52:36: Sprite3 before size	1	16.0 16.0
16:52:36: Sprite3 after size	1	0.0	0.0
16:52:37: Sprite3 before size	2	0.0	0.0
16:52:37: Sprite3 after size	2	0.0	0.0
16:52:37: Sprite3 before size	3	0.0	0.0
16:52:37: Sprite3 after size	3	0.0	0.0
16:52:38: Sprite3 before size	4	0.0	0.0
16:52:38: Sprite3 after size	4	0.0	0.0
16:52:38: Sprite3 before size	5	0.0	0.0
16:52:38: Sprite3 after size	5	0.0	0.0

Hm. I'm seeing the correct sprite size with playdate.graphics.sprite.new(image). No idea what's special with your machine @ericlewis . :slight_smile:

In the meantime, I was able to get it working with my own sprite.copy implementation. No idea what the correct behavior is (should a copy preserve center? Be automatically added to the display list? Preserve the sprite's tag?) so I'm sure this is only a partial implementation and definitely slower than what a fixed SDK could provide. But hopefully it unblocks you @IGM0937

-- preserve the old method
local oldCopy = gfx.sprite.copy

-- define our own copy method
local function spriteCopy(self)
  local duplicate = gfx.sprite.new()
  local image = self:getImage()
  if image ~= nil then
    duplicate:setImage(image)
  else
    duplicate:setSize(self:getSize())
  end
  duplicate:setScale(self:getScale())
  duplicate:setZIndex(self:getZIndex())
  duplicate:setVisible(self:isVisible())
  duplicate:moveTo(self:getPosition())
  return duplicate
end

-- make our method THE copy method
gfx.sprite.copy = spriteCopy
1 Like

Thanks @downie for your help.

I had implemented a small method that ended up coping my properties and attributes that I cared about. I've made a couple of more changes based on your suggestion, so it will have to do as a work around until this is fixed.

Still haven't heard from anyone (either from moderators or from Panic) to confirm that this is being looked at.

I've made the post here.

1 Like

Sorry about that! Yes, I've filed it and we're looking into it. Thanks for the report!

2 Likes