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