Intermittent error in __gc metamethod

,

SDK v3.0.1 on Windows

I’m seeing these errors intermittently in the playdate simulator console (yellow text)
error in
__gc metamethod
(
lua_release called on object with retainCount == 0
)

The condition is whenever there is a sprite collision between a moving sprite (projectile) and an animated one using moveWithCollisions. I’ve checked the usual suspects I could find elsewhere trying to dig into this issue, collisionResponse is set to a constant kCollisionTypeOverlap, not accidentally setting globals where they should be locals, accessing items after they’re marked for removal, ect..

Once a projectile sprite is colliding with the animated one an extra alphaCollision() is checked and if they are in contact the projectile sprite is marked for removal with self:remove()

Changing the collisionResponse to not kCollisionTypeOverlap removes the error, however the projectile is never then found inside the animated sprite which seems to be the key here. A solid body sprite will never trigger this error either, even a moving one. However a more complex sprite image will trigger it with varying consistency. A rotating image with two parallel lines is complex enough to trigger this error. Interesting to note that it only occurs when the projectile touches the rear line, and not when the projectile touches the line closest to the source.

I’m curious what is happening inside GC. I haven’t been able to find any reference to what lua_release is doing or how retainCount relates to sprites. If the source C code is available to developers that may be helpful here.

Is there’s a better approach to handle projectile like sprites other than with moveWithCollisions? Maybe some combination of checkCollisions and moveTo

I’ve simplified my project into a single file that can be run to demonstrate the error here:

import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"

local gfx <const> = playdate.graphics
local geom <const> = playdate.geometry

fps = 30
dt = 1 / fps

class('Ammunition').extends(gfx.sprite)
id = 0

function Ammunition:init(source)
    Ammunition.super.init(self)
    self.source = source
    self.damage = source.damage
    self.traveled = geom.point.new(0, 0)
    self.position = geom.point.new(source.x, source.y)  -- Put the proj at the source origin, fine tune init position in calling function
    self.velocity = geom.vector2D.new(0, 0)      -- Initial velocity
    self.acceleration = geom.vector2D.new(0, 0)  -- External acceleration ie. like gravity
    self.range = source.range
    if self.source:isa(Unit) then self.targetType = Enemy end
    if self.source:isa(Enemy) then self.targetType = Unit end
    
    self:setZIndex(self.source:getZIndex() - 1)    
    self.collisionResponse = gfx.sprite.kCollisionTypeOverlap
    self:add()
    self.id = id
    id += 1
    
    print('shooting '..self.id)
end

function Ammunition:update()
    if not self:isVisible() then return end
    -- dx = v_i*dt + 1/2a(dt^2)
    local deltaP = self.velocity * dt + (self.acceleration * (dt ^ 2) * (1/2))
    self.position += deltaP
    -- v_f = v_i + a(dt)
    self.velocity += self.acceleration * dt
    self.traveled += deltaP

    if self.traveled.x >= self.range then
        self:remove()
        return
    end

    local collisions, len, x, y
    self.x, self.y, collisions, len = self:moveWithCollisions(self.position)
    
    for i = 1, len do
        local c = collisions[i]
        if c.other:isa(self.targetType) and self:alphaCollision(c.other) then
            c.other.health -= self.damage
            self:remove()
            break
        end
    end
end

class('Projectile').extends(Ammunition)

-- class variables
local projectiles = {
    sml = geom.polygon.new(0,0, 2,0, 2,2, 0,2, 0,0)
}
function Projectile:init(projType, source)
    Projectile.super.init(self, source)

    local projPoly = projectiles[projType]
    local polyRect = projPoly:getBoundsRect()
    self.img = gfx.image.new(polyRect.width, polyRect.height, gfx.kColorClear)
    gfx.pushContext(self.img)
        gfx.setColor(gfx.kColorBlack)
        gfx.fillPolygon(projPoly)
    gfx.popContext()

    self.source = source

    self:setImage(self.img)
    self:setCollideRect(0, 0, self.width, self.height)
end


class('Unit').extends(gfx.sprite)
function Unit:init(unitType, location, version)
    Unit.super.init(self)
    self.range = 200
    self.damage = 1
    self:setImage(gfx.image.new(16, 16, gfx.kColorBlack))
    self:setCenter(1, 1)
    self:setCollideRect(0, 0, self.width, self.height)
    self:moveTo(100, 100)
    self:setZIndex(0)
    self.ready = false
    self.timer = playdate.timer.new(250)
    self.timer.discardOnCompletion = false
    self.timer.timerEndedCallback = function(timer) self.ready = true end
    self:add()
end

function Unit:update()
    if self.ready then
        self.ready = false
        self.timer:reset()
        self.timer:start()
        local proj = Projectile('sml', self)
        proj.position.y -= 8
        proj.velocity = geom.vector2D.new(60, 0)
        proj.range = self.range
        proj:add()
    end
end

class('Enemy').extends(gfx.sprite)
function Enemy:init()
    Enemy.super.init(self)
    self.speed = 0
    self.health = 500
    self.damage = 1
    self.attackSpeed = 1000
    self.location = 160
    self.currentSpeed = self.speed
    self.img = gfx.image.new(16, 16, gfx.kColorClear)
    gfx.pushContext(self.img)
        gfx.setStrokeLocation(gfx.kStrokeInside)
        gfx.setLineWidth(2)
        gfx.setColor(gfx.kColorBlack)
        gfx.drawLine(2, 2, 2, 14)
        gfx.drawLine(14, 2, 14, 14)
    gfx.popContext()
    self:setImage(self.img)
    self:setCollideRect(0, 0, self.width, self.height)
    self:addSprite()
    self.angle = 0

    self.newY = function(self)
        return self.y
    end
    self.newX = function(self)
        return self.x - (self.currentSpeed * dt)
    end

    self:moveTo(self.location, 90)
    self:setZIndex(math.random(0, 10))
    self:setCenter(0.5, 0.5)
    self.collisionResponse = gfx.sprite.kCollisionTypeOverlap
end

function Enemy:addSprite()
    self:add()
end

function Enemy:update()
    if self.health <= 0 then self:remove() return end
    self.angle = (self.angle + 8) % 360
    self:setImage(self.img:rotatedImage(self.angle))
    self:setCollideRect(0, 0, self.width, self.height)
end

function myGameSetUp()
    Unit()
    Enemy()
end


myGameSetUp()


function playdate.update()

    gfx.sprite.update()
    playdate.timer.updateTimers()

end