Page 1 of 1

[Solved] Rectangle collision resolution only works on one axis

Posted: Thu May 09, 2024 1:51 am
by autumnDev
Hello, I am creating the bones of a 2D top-down game, and am trying to make my 'basic' code something I can reuse for other projects. I originally based my code off the Sheepolution tutorial here https://sheepolution.com/learn/book/23. However, I encountered some strange behavior where colliding from the vertical directions (up or down) causes it to act like its colliding from the horizontal directions. Left and right collisions still work perfectly fine. When I switched whether it was vertically and horizontally collided, it would instead properly collide up and down, and treat left and right like a down collision. I ended up copying the code I used when doing the tutorial, and when that didn't work, from the tutorial itself. I thought that fixed it, and I moved on. However, while I was working on having actors collide, while the collision process itself works, the previous bug has come back, and nothing I do seems to fix it. It seems to be the only issue I'm having with my collision code otherwise.

I'm hoping that someone has a solution to this problem. I'm trying to find a way to do this without using libraries, more so I can understand how it works, and because this game is supposed to be extremely simple physics-wise.

Collisions:

Code: Select all

function Collision:new(x,y,width,height,strength)
	Collision.super.new(self,x,y,width,height)
	--Collsions
	if strength == nil then strength = 0 end
	self.strength = strength
	self.tempStr = strength
	self.last = {}
	self.last.x = x
	self.last.y = y
end
function Collision:setStrength(val)
	if val < 0 then val = 0 end
	self.strength = val
end
function Collision:update(dt)
	self.last.x = self.x
	self.last.y = self.y
	self.tempStr = self.strength
end
function Collision:check(col)
	if col:is(Collision) then return self:overlap(col) end
	return false
end
function Collision:collide(e,dir)
	if dir == "left" then
		local pushback = self.x + self.width - e.x
		self.x = self.x - pushback
	elseif dir == "right" then
		local pushback = e.x + e.width - self.x
		self.x = self.x + pushback
	elseif dir == "up" then
		local pushback = e.y + e.height - self.y
		self.y = self.y + pushback
	elseif dir == "down" then
		local pushback = self.y + self.height - e.y
		self.y = self.y - pushback
	end
end
function Collision:wasVerticalAligned(e)
	return self.last.y < e.last.y + e.height and self.last.y + self.height > e.last.y
end
function Collision:wasHorizontalAligned(e)
	 return self.last.x < e.last.x + e.width and self.last.x + self.width > e.last.x
end
--Overwritten by other functions
function Collision:checkResolve(e,dir)
	return true
end

function Collision:resolve(e)
	--Check if stronger; if true, reverse
	if self.tempStr > e.tempStr then
		return e:resolve(self)
	end
	--Check if colliding
	if self:check(e) then
		self.tempStr = e.tempStr
		if self:wasVerticalAligned(e) then
			if self.x + (self.width/2) < e.x + (e.width/2) then
				self:collide(e,"left")
			else
				self:collide(e,"right")
			end
			debugStrings[2] = "Horizontal Collision"
		elseif self:wasHorizontalAligned(e) then
			if self.y + (self.height/2) < e.y + (e.height/2) then
				self:collide(e,"down")
			else
				self:collide(e,"up")
			end
			debugStrings[2] = "Vertical Collision"
		end
		return true
	end
	return false
end
Relevant Actor snippet

Code: Select all

function Actor:canCollide(b)
	if b == nil then b = false end
	self.canCollide = b
end

function Actor:collide(a)
	if a == nil then return false end
	if a:is(Actor) then
		if a:hasCollision() then 
		--Resolve
		local t = self.col:resolve(a.col) 
		--Set position
			if t then
				self.x = self.col.x
				self.y = self.col.y
				a.x = a.col.x
				a.y = a.col.y
			end
		return t
		end
	end
	return false
end

function BasicData.collisions(dt,loops)
	if loops == nil then loops = 100 end
	
	--Sort in priority order
	table.sort(BasicData.actors,function(a1,a2)
		if a1.priority ~= a2.priority  then
			return a1.priority > a2.priority
		end
		return a1.id < a2.id
	end)
	
	--Check collide
	local can_loop = true
	local limit = 0
	while can_loop do
		--Breaking the loop
		can_loop = false
		limit = limit + 1
		if limit >= loops then break end
		--Collisions
		for i=1,#BasicData.actors-1 do
			for j=i+1,#BasicData.actors do
				local col = BasicData.actors[i]:collide(BasicData.actors[j])
				if col then can_loop = true end
			end
		end
	end
end
I also provided the .love file so you can see the behavior.
EDIT: Forgot to add the file.
CollisionHelp.love
Love File
(13.97 MiB) Downloaded 55 times
[EDIT2] Was solved by pgimeno! Thank you for your help!

Re: Rectangle collision resolution only works on one axis

Posted: Thu May 09, 2024 9:40 pm
by knorke
autumnDev wrote: Thu May 09, 2024 1:51 amI also provided the .love file so you can see the behavior.
It seems you forget to attach the file.
Good luck..such kind of bugs are extremely annoying to find.

Re: Rectangle collision resolution only works on one axis

Posted: Sat May 11, 2024 8:45 am
by pgimeno
Once the shapes are interpenetrating, both wasHorizontalAligned and wasVerticalAligned are going to be true, because they only check for interpenetration in one axis, and the shapes are known to be interpenetrating anyway, and the interpenetration affects both axes, so both checks will be true. This is confirmed by the fact that swapping the order of checking for wasHorizontalAligned and wasVerticalAligned, the code says that all collisions are vertical, as you have noted.

Therefore the criteria to determine whether the collision happens horizontally or vertically have to be different than what you have. Swept collisions would be ideal, but they are tricky to implement; lacking that, a simpler approach is to compare the X penetration with the Y penetration and decide which one is smaller, and take that as the collision direction. The rationale can be explained with this ASCII "art":

Code: Select all

+-------+
|       |
|     +-|---+
|     | |   |
|     | |   |
+-----+-+   |
      |     |
      +-----+
In the above case, since the X penetration is smaller than the Y penetration, the collision probably happened horizontally. The box-circle collision is more complicated, since both the corners and the sides need to be checked.

Re: Rectangle collision resolution only works on one axis

Posted: Sat May 11, 2024 6:27 pm
by autumnDev
pgimeno wrote: Sat May 11, 2024 8:45 am Once the shapes are interpenetrating, both wasHorizontalAligned and wasVerticalAligned are going to be true, because they only check for interpenetration in one axis, and the shapes are known to be interpenetrating anyway, and the interpenetration affects both axes, so both checks will be true. This is confirmed by the fact that swapping the order of checking for wasHorizontalAligned and wasVerticalAligned, the code says that all collisions are vertical, as you have noted.
Thank you! I was able to implement the simpler solution and it works properly now. While I was refactoring, when both 'branches' were true, it gave me the same behavior, so thank you for finding the initial problem as well.