Parts of sprite rendering after moving it

Hello there,

In a 2d top down game im working on, a player picks up items off the ground, they animate moving to the player before disappearing via remove(). I noticed a bug recently where sometimes parts of the sprite of the item on the ground still render in the place it dropped after the item is picked up. It will disappear upon the player moving, which triggers the whole scene to update due to camera implementation.

One thing i thought it might be is the "miss" text as that was a recent change, but after removing that feature the bug still occurs.

The sprite itself is relatively simple:

class('ItemDrop').extends(gfx.sprite)

local pickupVelocity = 4

function ItemDrop:init(item)
  self:setTag(ITEM_DROP_TAG)

  local frame = item.animation:image()
  self.item = item
  self:setSize(frame.width, frame.height)
  self:setCollideRect(0, 0, self:getSize())
  self:setCollidesWithGroups({ITEM_DROP_GROUP})
  self:setGroups(ITEM_DROP_GROUP)
  self.isPickedUp = false
end

function ItemDrop:update()
  self:setImage(self.item.animation:image())

  if self.isPickedUp then
    local dirX = self.pickupTarget.x - self.x
    local dirY = self.pickupTarget.y - self.y
    local x = pickupVelocity
    local y = pickupVelocity
    if math.abs(dirX) < x then
      x = dirX
    elseif dirX < 0 then
      x = x * -1
    end

    if math.abs(dirY) < y then
      y = dirY
    elseif dirY < 0 then
      y = y * -1
    end

    self:moveBy(x, y)
    if self.x == self.pickupTarget.x and self.y == self.pickupTarget.y then
      self:remove()
      inventory_addItem(self.item)
    end
  else
    local overlappingSprites = self:overlappingSprites()
    for _, sprite in pairs(overlappingSprites) do
      if sprite:getTag() == ITEM_DROP_TAG then
        local r1 = self:getCollideRect()
        r1.x = r1.x + self.x
        r1.y = r1.y + self.y
        local r2 = sprite:getCollideRect()
        r2.x = r2.x + sprite.x
        r2.y = r2.y + sprite.y
        local intersection = r1:intersection(r2)
        local x1 = math.random(2, 4)
        local x2 = math.random(2, 4)
        local y1 = math.random(2, 4)
        local y2 = math.random(2, 4)
        self:moveBy(intersection.width / 2 + x1, intersection.height / 2 + y1)
        sprite:moveBy(-(intersection.width / 2 + x2), -(intersection.height / 2 + y2))
      end
    end
  end
end

function ItemDrop:print()
  local type = ''
  if self.item.type == WEAPON_ITEM then
    type = 'Weapon'
  elseif self.item.type == ARMOR_ITEM then
    type = 'Armor'
  elseif self.item.type == SHIELD_ITEM then
    type = 'Shield'
  elseif self.item.type == POTION_ITEM then
    type = 'Potion'
  end
  print('Dropped', type, self.x, self.y)
end

function ItemDrop:getRect()
  local rect = self:getCollideRect()

  rect.x = rect.x + self.x
  rect.y = rect.y + self.y

  return rect
end

function ItemDrop:pickedUpBy(x, y)
  self.isPickedUp = true
  self.pickupTarget = playdate.geometry.vector2D.new(x, y)
  self:clearCollideRect()
end

Here's a screenshot where you can see the full sword close the player, and the bits of the hilt below them:

Likewise here's the full video: https://youtube.com/shorts/JbP6nTr7Kkc?feature=share

This looks like the sprite's dirty rect hasn't been adjusted for setDrawOffset() (which I assume you're using to move the camera around?) but I know we fixed a bug like that a while ago. I make a quick test, moving a sprite after changing the draw offset, couldn't reproduce this. What SDK version are you running? Can you post a pdx build which demonstrates this bug (or DM it to me if you don't want it public)?

Screen Shot 2023-03-29 at 1.16.47 PM

TheDepths.pdx.zip (126.9 KB)
Attached a zip of the PDF for you. I'm using version 1.13.2 of the SDK. And yes, I'm using setDrawOffset for moving the camera around. If you need me to share additional code, let me know and I can DM you a zip of it.

As far as testing it, you can move up to an enemy sprite and attack them with the A button. Each enemy will always drop 3 items, there's 3 enemies in the small map there total.

Awesome, thanks so much! You found an interesting bug! :slight_smile: What's happening is I've got a shortcut where it says "if we're already redrawing the whole screen, don't bother checking the individual sprite dirty rects" and that's somehow keeping it from updating that sprite's dirty rect to reflect the offset change. I'm guessing that full-screen sprite marking the entire screen dirty is your HUD containing the health meter? If so you can use sprite:setIgnoresDrawOffset(true) to keep it anchored to the screen so you don't have to move it around. (You should also use sprite:setZIndex(<some large number>) to put it above the other sprites--right now the dropped items will be drawn above it.) I'll get this fixed as soon as I can, seems like it's pretty easy to accidentally trigger.

Here's another example showing the bug. Press A twice and you get two black squares:

gfx = playdate.graphics
image = gfx.image.new(40,40,gfx.kColorBlack)
s = gfx.sprite.new(image)
s:add()
s:moveTo(150,70)

n = 0

function playdate.AButtonDown()
	if n == 0 then
		gfx.setDrawOffset(50, 50)
		gfx.sprite.addDirtyRect(0, 0, 400, 240)
		n = 1
	else
		s:moveTo(200,120)
	end
end

function playdate.update()
	gfx.sprite.update()
end

Thanks Dave, that did the trick. I updated the sprite that i set to screen size to setIgnoresDrawOffset(true), and set its position to center screen.

function UiContainer:init()
  UiContainer.super.init(self)

  self:setIgnoresDrawOffset(true)
  self:moveTo(screenWidth / 2, screenHeight / 2)

  self:setSize(screenWidth, screenHeight)
  self:setZIndex(100)
  self.healthPercentage = 1
end
1 Like