Ok. With my extremely limited coding knowledge, I've gotten far further than I thought I'd be able to, but I've been bashing my head against this math on and off for a couple of weeks now and it's a complete blocker for continued dev on my game, so I'm appealing for some help
I've been avoiding asking here due in part to the amount of code I might need to paste, and being a bit embarrassed about the likely terrible code quality, but here goes.
The collision detection in my game is working great for a moving ball colliding with static bricks (pictured below).
I'm using the Playdate's sprite collision library, that sits within my ball's update function, below:
function Ball:update(paddleX, paddleY, isMagnetActive) -- stick ball to paddle if it hasn't been fired yet self:updateUnfiredPosition() -- calculate anticipated position based on direction, speed, and angular velocity (if any) self.currentDirection = atan2(self.velocityY, self.velocityX) self.currentDirection = self.currentDirection + self.angularVelocity self.velocityX = self.speed * cos(self.currentDirection) self.velocityY = self.speed * sin(self.currentDirection) self.targetX = self.x + self.velocityX * gameSpeed self.targetY = self.y + self.velocityY * gameSpeed -- reduce angular velocity(spin) a little each frame self.angularVelocity = self.angularVelocity * angularVelocityDecay -- check for collisions local actualX, actualY, collisions, length = self:moveWithCollisions(self.targetX, self.targetY) -- handle collision rebounds (poorly?) for i = 1, length do local collision = collisions[i] -- Only care about these 3 types of objects for now if collision.other.type == "brick" or collision.other.type == "sand" or collision.other.type == "ice" then -- trigger hit function that handles vfx and sounds collision.other:hit(self, collision.normal) -- Passing collision normal here -- collision normal local normalX = collision.normal.x local normalY = collision.normal.y -- calculate brick velocity local brickVelX = collision.other.velocityX or 0 local brickVelY = collision.other.velocityY or 0 print("Brick vel", brickVelX, brickVelY, "Collision normal", normalX, normalY) print("Ball x/y:", self.x, self.y, "Actual x/y:", actualX, actualY, "Target x/y", self.targetX, self.targetY) -- Only use the brick's velocity component perpendicular to the collision normal if normalX ~= 0 then self.velocityX = -self.velocityX -- brickVelX * normalX actualX = actualX + brickVelX end if normalY ~= 0 then self.velocityY = -self.velocityY -- brickVelY * normalY actualY = actualY + brickVelY end break end end -- curve trajectory towards paddle when magnet is active if isMagnetActive then self:magnetInfluence(paddleX, paddleY) end -- set actual position based on the above self.x = actualX self.y = actualY if debug then self:trackHistory() end end function Ball:collisionResponse(other) return "bounce" end
The moving bricks are handled by an update function that runs every frame (warning, this may be rough):
function updatePositions(objList) for _, obj in ipairs(objList) do if obj.path and obj.currentPathPoint and obj.currentLerpValue then if obj.waitTime and obj.waitTime > 0 then obj.waitTime = obj.waitTime - gameSpeed -- Subtract gameSpeed from waitTime else local prevPoint = obj.path[obj.currentPathPoint] local nextPoint = obj.path[obj.currentPathPoint % #obj.path + 1] obj.currentLerpValue = obj.currentLerpValue + (obj.speed[obj.currentPathPoint] * gameSpeed) obj.currentLerpValue = clamp(obj.currentLerpValue, 0, 1) local x = lerp(prevPoint, nextPoint, obj.currentLerpValue) local y = lerp(prevPoint, nextPoint, obj.currentLerpValue) x = round(x) y = round(y) if obj.prevX and obj.prevY then obj.velocityX = (x - obj.prevX) --/ gameSpeed obj.velocityY = (y - obj.prevY) --/ gameSpeed end obj.prevX = x obj.prevY = y if obj.type == "coin" then obj:moveTo(x, y) else obj:moveTo(x, y) end if obj.currentLerpValue >= 1 then obj.currentLerpValue = 0 obj.currentPathPoint = obj.currentPathPoint % #obj.path + 1 if obj.path[obj.currentPathPoint] then obj.waitTime = obj.path[obj.currentPathPoint] -- waitTime now directly set in seconds else obj.waitTime = 0 end -- Here, we explicitly set the object's velocity to zero when it reaches its target position. obj.velocityX = 0 obj.velocityY = 0 end end end end end
So I'm having two problems, likely related:
When a brick is moving quite fast, and a collision occurs, the ball tunnels through the brick in an unwanted way, as shown here:
When the brick is moving slower (which it does on the down motion), the problem doesn't occur, presumably as the ball's speed is greater than the brick, causing it not to trigger multiple collisions:
I know the collision detection in the Playdate SDK handles tunnelling, and when I comment out my collision handling code, there's no bounce, but no tunnelling either, so the problems are squarely with my code.
One of the things I thought might be an issue, is that I'm moving the brick with moveTo() rather than moveWithCollisions(). (hence the weird redundant if statement in updatePositions, ignore that please) Problem was when I was using moveWithCollisions for the brick, it would cause the brick to fail to get to its actual end position much of the time. I want the brick's position to be completely unaffected by the ball. Unsure if this is the right approach or not.
So that's problem 1.
Problem 2 is that I'm trying to get the brick's impact to affect the velocity/direction of the brick, which is most noticeable when the hit is perpendicular to the ball's direction:
The green here is sorta what I expect to happen, the red is what actually happens.
Not expecting a working solution (though that'd be lovely), but if anyone can point me in a direction even, or point out clear and obvious flaws, that'd be fantastic. Thank you all so much.