Crazy collisions when rotating sprite

,

I have a sprite that rotates as the user turns the crank and travels in that direction. It's only travelling at a speed of 1 pixel per update.

When I disable the rotating of the sprite, collisions seem correct.

No rotation on sprite

However, when I enable the rotation it goes what I can only describe as bat-shit crazy.

Sprite rotation enabled

The collision response function is:

function Harry:collisionResponse(other)
    if (other:isa(Crop)) then
        other:remove()
        return collisionType.overlap
    else
        return collisionType.freeze
    end
end

What's going on here? It's driving me mad.

[quote="BanksySan, post:1, topic:20755, full:true"]
I have a sprite that rotates as the user turns the crank and travels in that direction. It's only travelling at a speed of 1 pixel per update.

When I disable the rotating of the sprite, collisions seem correct.

No rotation on sprite

However, when I enable the rotation it goes what I can only describe as bat-shit crazy.

Sprite rotation enabled

The collision response function is:

function Harry:collisionResponse(other)
    if (other:isa(Crop)) then
        other:remove()
        return collisionType.overlap
    else
        return collisionType.freeze
    end
end

I get similar behaviour with slide and bounce collision types.

What's going on here? It's driving me mad.

This could be related to Rotating a sprite with a collider moves the collision rectangle away from the center point.

I've uploaded a demo project to GitHub.

Hi!
The problem is that you're not adjusting the sprite's collideRect to account for the sprite's increase in size when rotating. The collideRect is always at the top-left of your sprite's bounding box. Rotation is pushing this through your wall, causing the collision to resolve with the sprite on the other side.

Here's your sprite with a border and Show Collide Rects to illustrate:

You could do this to adjust the size of the collideRect when rotating:

local _, _, w, h = self:getBounds()
self:setCollideRect(0, 0, w, h)

But that doesn't fix the issue. Your player can still tunnel through walls by rotating. And you probably don't want a variable-sized sprite anyway.
So to maintain the sprite size and offset the position of the collideRect, try:

if (self.enableRotation) then
    self:setRotation(crankAngleDeg)
    local _, _, offsetX, offsetY = self:getBounds()
    offsetX, offsetY = offsetX * 0.5 - 16, offsetY * 0.5 - 16
    self:setCollideRect(offsetX, offsetY, 32, 32)
end

And now it behaves as expected.

Why do the sprite's bounds effect the collision? Surely only the colllide rect should matter and that doesn't change size.

Because the collideRect is at 0,0 in the sprites bounding box. So if the size of the bounding box changes (as it does when you rotate the sprite), the position of the collideRect moves.

CollideRects do move though, why would that be an issue? It moves when I don't rotate too.

When you rotate your sprite, this rotation shifts the collideRect (the red square) to a position that is overlapping your walls:
SpriteRotation2

Then you do moveWithCollisions which resolves the overlap in an unexpected (to you) way.
Adjusting the collideRect during rotation so it is centred on the sprite rather than at 0,0 fixes this.

1 Like

Thank you. Yes, that does make sense now!

1 Like

Fixed. I rotate I regenerate the image now, rather than rotating the sprite.

Working demo on the fix branch.

-- Setup here, not interesting.

--- Generate an image at set angle in degrees
--- @param angle number The angle in degrees
--- @return playdate.graphics.image
function generateImage(angle)
    local vectorOffset = geo.vector2D.new(16, 16)

    local vectorA = geo.vector2D.newPolar(16, angle) + vectorOffset
    local vectorB = geo.vector2D.newPolar(16, angle + 120) + vectorOffset
    local vectorC = geo.vector2D.newPolar(16, angle + 240) + vectorOffset

    local image <const>
    = gfx.image.new(64, 64)

    gfx.pushContext(image)
    gfx.drawPolygon(vectorA.x, vectorA.y, vectorB.x, vectorB.y, vectorC.x, vectorC.y)
    gfx.drawRect(0,0,32,32)
    gfx.popContext(image)

    return image
end

class('Player').extends(gfx.sprite)
    
function Player:init(x, y, speed, enableRotation)
    self.speed = speed
    self.enableRotation = enableRotation
    self.collisionResponseIndex = 1
    self:setImage(generateImage(0))
    self:setZIndex(0)
    self:setCollideRect(0, 0, 32, 32)
    self:moveTo(x, y)
    self:add()
end

function Player:update()
    -- Movement calculations here

    local crankAngleDeg = pd.getCrankPosition()
    local crankAngleRad = math.rad(crankAngleDeg);

    if (self.enableRotation) then
        self:setImage(generateImage(crankAngleDeg))
    end

    -- Do movement here
end

function Player:collisionResponse(_)
    -- Handle collision
end