checkCollisions only returns a single collision, even if there should be multiple

(Found using Lua SDK on Mac)

Expected behavior:
playdate.graphics.sprite:checkCollisions(x, y) should return an array of collisions with the sprite's collision rect.

Observed behavior:
when there are multiple overlapping collision rects, only a single one is returned by checkCollisions

You can see this in the following gif, which queries checkCollisions every frame and should display all collisions on the screen:
box_collisions

additionally, it should print a message to the console when if there are multiple collisions in the response, but this message is never printed.

The issue can be reproduced with the following main.lua:

-- main.lua for collision test

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

local gfx <const> = playdate.graphics

-- Here's our  sprite declaration

-- pre-render "player" image
local playerImage = gfx.image.new(20, 20)

gfx.lockFocus(playerImage)
	-- black outline
	gfx.setColor(gfx.kColorBlack)
	gfx.fillCircleAtPoint(10, 10, 10)
	-- white box in the middle
	gfx.setColor(gfx.kColorWhite)
	gfx.fillRect(8, 8, 4, 4)
gfx.unlockFocus()


local playerSprite = gfx.sprite.new( playerImage )
playerSprite:setCenter( 0.5, 0.5 )
playerSprite:moveTo( 200, 120 )
playerSprite:setCollideRect( 0, 0, playerSprite:getSize() )	
playerSprite:add()


gfx.setLineWidth(3)


-- define box 1
local box_image = gfx.image.new(50, 50)
gfx.lockFocus(box_image)
	gfx.setColor(gfx.kColorBlack)
	gfx.drawRect(0, 0, 50, 50)	
	gfx.drawText("box1", 10, 10)
gfx.unlockFocus()

local box1 = gfx.sprite.new(box_image )
box1:setCenter( 0.5, 0.5 )
box1:moveTo( 100, 120 )
box1.name = "box1"
box1:setCollideRect( 0, 0, box1:getSize() )	
box1:add()

-- define box 2
local box_image = gfx.image.new(50, 50)
gfx.lockFocus(box_image)
	gfx.setColor(gfx.kColorBlack)
	gfx.drawRect(0, 0, 50, 50)	
	gfx.drawText("box2", 10, 10)
gfx.unlockFocus()

local box2 = gfx.sprite.new(box_image )
box2:setCenter( 0.5, 0.5 )
box2:moveTo( 300, 120 )
box2.name = "box2"
box2:setCollideRect( 0, 0, box2:getSize() )	
box2:add()

-- define box 3
local box_image = gfx.image.new(100, 100)
gfx.lockFocus(box_image)
	gfx.setColor(gfx.kColorBlack)
	gfx.drawRect(0, 0, 100, 100)	
	gfx.drawText("box3", 10, 10)
gfx.unlockFocus()

local box3 = gfx.sprite.new(box_image )
box3:setCenter( 0.5, 0.5 )
box3:moveTo( 180, 130 )
box3.name = "box3"
box3:setCollideRect( 0, 0, box3:getSize() )	
box3:add()

-- define box 4
local box_image = gfx.image.new(100, 100)
gfx.lockFocus(box_image)
	gfx.setColor(gfx.kColorBlack)
	gfx.drawRect(0, 0, 100, 100)	
	gfx.drawText("box4", 10, 10)
gfx.unlockFocus()

local box4 = gfx.sprite.new(box_image )
box4:setCenter( 0.5, 0.5 )
box4:moveTo( 220, 180 )
box4.name = "box4"
box4:setCollideRect( 0, 0, box4:getSize() )	
box4:add()



-- main update function
function playdate.update()
	
	-- Poll the d-pad and move our player accordingly.
	if playdate.buttonIsPressed( playdate.kButtonUp ) then
		playerSprite:moveBy( 0, -2 )
	end
	if playdate.buttonIsPressed( playdate.kButtonRight ) then
		playerSprite:moveBy( 2, 0 )
	end
	if playdate.buttonIsPressed( playdate.kButtonDown ) then
		playerSprite:moveBy( 0, 2 )
	end
	if playdate.buttonIsPressed( playdate.kButtonLeft ) then
		playerSprite:moveBy( -2, 0 )
	end

	-- update sprites
	gfx.sprite.update()
	
	gfx.drawText("Collision test", 10, 10)

	-- collision detection
	local positionX,positionY,collisionsP,lengthP = playerSprite:checkCollisions(playerSprite.x,playerSprite.y)

	-- print the name of all objects on screen that the player is colliding with
	if lengthP>0 then
				
		for i=1,lengthP do
			local object = collisionsP[i].other
			gfx.drawText("Collision with " .. object.name , 250, 20 * i)			
		end

	end
	
	-- the condition below should trigger if the player's hitbox overlaps with several hitboxres, but as we'll see this never triggers!
	if lengthP > 1 or #collisionsP > 1 then
		print("PLAYER IS OVERLAPPING WITH MULTIPLE OBJECTS!")
	end
	

end
1 Like

I was experimenting some more, and i noticed that everything works as expected when defining the player's collision response function:

function playerSprite:collisionResponse(other)
	return "overlap"
end

collisions3

After looking at this for bit, I believe this is actually correct, if somewhat confusing, behavior. And it will only happen for "freeze", which is the default. The other collision types all do something more like what you're expecting.

The reason is that playdate.graphics.sprite:checkCollisions() returns the same results as the equivalent call to playdate.graphics.sprite:moveWithCollisions() would return, but without moving the sprite. It's sort of like a dry-run for moving a sprite.

So, when the collision response type is "freeze", the sprite will stop moving entirely as soon as it collides with another sprite, meaning that only one collision will ever occur. For the other collision types the sprite may slide, bounce, or overlap to keep moving and potentially collide with further sprites, meaning that more collisions might be returned.

If what you're looking for is just a list of all of sprites that a sprite is currently "colliding with", you would probably want to use playerSprite:overlappingSprites() instead:

  local collisionsP = playerSprite:overlappingSprites()

  -- print the name of all objects on screen that the player is colliding with
  if #collisionsP>0 then
        
    for i=1,#collisionsP do
      local object = collisionsP[i]
      gfx.drawText("Collision with " .. object.name , 250, 20 * i)			
    end

  end
  
  -- the condition below should trigger if the player's hitbox overlaps with several hitboxres, but as we'll see this never triggers!
  if #collisionsP > 1 then
    print("PLAYER IS OVERLAPPING WITH MULTIPLE OBJECTS!")
  end

As as aside, I also wanted to mention you can set the collision response for a sprite this way if you don't need any logic in the response:

playerSprite.collisionResponse = gfx.sprite.kCollisionTypeOverlap

1 Like

Ahhh, makes sense now. Thanks for the detailed explanation!

Hi! I encountered the same problem just writing some code for a couple of days. It looks very confusing for me. Does your explanation mean that playdate.graphics.sprite:checkCollisions() always return only one object and not an array as stated in documentation?
Or there is some setting to change it from "freeze" to something else? Unfortunately, I haven't found this in docs too.

Yes, you can change it from "freeze" to something else - the four options are "slide", "freeze", "overlap", and "bounce". The documentation in question can be found here: Inside Playdate

I noted this above, but I think it's worth mentioning again that instead of providing a callback you can also set the value of a collision response on a sprite directly, like playerSprite.collisionResponse = gfx.sprite.kCollisionTypeOverlap.