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[1], nextPoint[1], obj.currentLerpValue)
local y = lerp(prevPoint[2], nextPoint[2], 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][3] then
obj.waitTime = obj.path[obj.currentPathPoint][3] -- 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.