How do I check for corners? [MATH]

Basically what the title says, if you want more context; I am trying to detect corners (the two-walls-that-intersect-to-create-a-corner) in my custom movement script to avoid bugging out, or unnecessary tweaking.

Diagnosis

Here’s how my code works for right now; I first constantly Spherecast in front of my character to find a wall with collisions. Next, I’ll get the normal of the wall relative to my character (like, getting your shadow but you are perfectly obstructing the light). Finally, you get the angle your character corresponding to the dot vector (e.g looking straight at the wall gives 0 degrees, looking sideways at the wall gives 90 degrees)

When my spherecast reaches a corner, or a two walls no-clipping to make a corner, the spherecast rapidly flips between the two, causing me to inch forward in the process. This then, bugs me out.

SCR/CODE SNIPPET
-- random code prior, __stateresults have normal params
	__StateResults = workspace:Spherecast(RootPart.Position, 1, (RootPart.CFrame * CFrame.Angles(0, 0, 0)).LookVector * FirstCheckDist, __StateConditions)
	if __StateResults then
		local Divider = RootPart.CFrame.LookVector
		local Object: Part, Offset = __StateResults.Instance, __StateResults.Instance.CFrame:PointToObjectSpace(RootPart.Position)

		local PositionRelativeTo = Vector3.new(math.clamp(Offset.X, -(Object.Size/2) .X, (Object.Size/2).X), math.clamp(Offset.Y, -(Object.Size/2).Y, (Object.Size/2).Y), math.clamp(Offset.Z, -(Object.Size/2).Z, (Object.Size/2).Z))
		local PreciseSurfaceDirection3 = (Object.CFrame * CFrame.new(PositionRelativeTo)) - RootPart.Position

		Divider = Vector3.new(Divider.X, 0, Divider.Z).Unit
		local PreciseSurfaceDirection = Vector3.new(PreciseSurfaceDirection3.X, 0, PreciseSurfaceDirection3.Z).Unit
		local Intersect = Divider:Cross(PreciseSurfaceDirection)
		__WallCorrection = 180 - math.deg(math.acos(RootPart.CFrame.LookVector.Unit:Dot(__StateResults.Normal.Unit)))

		local __FutureCorrection
		if math.deg(Intersect.Y) < 0 then -- Left
			__FutureCorrection = __WallCorrection
			__FinalDiam = math.rad(math.abs(math.round(__WallCorrection - 90)))
		else -- Right
			__FutureCorrection = -__WallCorrection
			__FinalDiam = math.rad(math.round(__WallCorrection - 90))
		end
		-- random code later
2 Likes

Do you want them to be detected when touched by the player or?

1 Like

I want them to be detected by raycast, and then calculate their dot product to determine what should my speed & trajectory when running into a corner.
(Sorry if this is a bit hard to understand, and another thing is that I am not actually touching the walll)

I tried to do water droplet physics simulation. You may look into it’s code and try to find how I did similar thing. (I can’t talk a lot now, sorry, if you will need more help, I’ll try later)

1 Like

I’ll look into it further, overall a good community resource!

If the initial raycast hits, you could fire another raycast parallel to the wall (and away from the player) and see if that one hits something. In case it does, you push back with the angle formed by the two walls. I’m not too sure on what the math on this would look like though.

Can I also ask why you’re using a custom movement system? In one of my own projects, I tried my hand at creating a custom movement system, but it proved to be very unstable and I eventually realized there wasn’t even that much of a difference between it and the default movement system, so I just switched back to the default.

I already tried your solution a day ago, but half way through it; since even 1 degree (& short) corners can easily invalidate the equation, it won’t work. But I am currently trying a variation of said solution, which after brainstorming it & simulating parts of it, seems to work.

The purpose of a custom movement system is to deviate from the default to provide a more diverse & possibly better experience for players. Games like Apeirophobia, Dingus, & etc all have custom movement systems which makes their gameplay more unique. Even games outside of Roblox like gmod, & bo3 have objectively better movement physics & opportunities (wallrunning, wallclimbing, etc…)

It’s kind of hard to breakdown the source code of your script for me… (no src comments were made)
I added my script code (& a short summary) that handles all my non sense under a “thread” or detail.

Hm, never thought about adding comments about what and why I did something… I’ll try to help you.

1 Like

So, after some re-reads of your code, I can say that your problem occurs because you use single shapecase only. I never used them, but as I know how they are working, they won’t detect everything within their radius. That’s may be important.
Next, when you get Normal of 1 wall, you calculate everything related to only that normal, but you also must account for vector which you receive by :Dot relatively to Wall’s normal, and then you must cast third ray if second succeed. (In my droplet simulation this situation cared in TestVelocity function, which called recursively up to 3 times, to detect all possiblities up to corner situation. After, I return in it resulting relative vector direction (may be wrong in saying that, but will give sample: If you give vector which is parallel to normal, function will return 0 meaning that no movement possible). Also, during the process of recursive normal calculations, I put them into Normals array, which is later used to calculate possible movement direction. And, finally, to calculate how much and at which direction character / droplet should move, I do smth like Normal * Distance and sum it with current character/droplet position.

Hope this explanation will help you. Otherwise, tell me that.

A bit of extra info:

Don’t do math with degrees. Try to learn radian math, it will be faster than constantly switching between degrees/radians.

__WallCorrection = math.pi - math.acos(RootPart.CFrame.LookVector.Unit:Dot(__StateResults.Normal.Unit))

And this part can be simplified into:

local __FutureCorrection = __WallCorrection * -math.sign(Intersect.Y)
__FinalDiam = math.abs(__WallCorrection - math.pi / 2)
-- IN RADIANS, 180 degrees = math.pi
-- And, please test this before implementing...
1 Like

That was also my exact line of reasoning when attempting this. But there wasn’t anything in my game that would really benefit from or look better with a custom movement sytem. I could still make the player look at a target, push them around, change movement speed, halt them, launch them, ect. Then again, my game isn’t a platformer or anything with a focus on movement tech.

I definitely didn’t think of the Normal * Distance thing, I’ll give that a shot. I also didn’t think constant degree/radian conversion had any consequences. A Normals array I didn’t think of either, so I’ll try and implement that.

Here were the three different times I tried to implement it with the purpose of detecting the second wall (Before you replied with a potential solution).

The Eyesore
-- 1, __incomingresults1 & 2 are raycast left & right from normal
if __IncomingResults1 or __IncomingResults2 then
			__WorkingResults = __IncomingResults1 or __IncomingResults2

			local Divider = RootPart.CFrame.LookVector
			local Object: Part, Offset = __StateResults.Instance, __StateResults.Instance.CFrame:PointToObjectSpace(RootPart.Position)

			local PositionRelativeTo = Vector3.new(math.clamp(Offset.X, -(Object.Size/2) .X, (Object.Size/2).X), math.clamp(Offset.Y, -(Object.Size/2).Y, (Object.Size/2).Y), math.clamp(Offset.Z, -(Object.Size/2).Z, (Object.Size/2).Z))
			local PreciseSurfaceDirection3 = (Object.CFrame * CFrame.new(PositionRelativeTo)) - RootPart.Position

			Divider = Vector3.new(Divider.X, 0, Divider.Z).Unit
			local PreciseSurfaceDirection = Vector3.new(PreciseSurfaceDirection3.X, 0, PreciseSurfaceDirection3.Z).Unit
			local Intersect = Divider:Cross(PreciseSurfaceDirection)
			__WallCorrection2 = 180 - math.deg(math.acos(RootPart.CFrame.LookVector.Unit:Dot(__StateResults.Normal.Unit)))

			local __FutureCorrection2
			if math.deg(Intersect.Y) < 0 then -- Left
				__FutureCorrection2 = __WallCorrection2
				__FinalDiam2 = math.rad(math.abs(math.round(__WallCorrection2 - 90)))
			else -- Right
				__FutureCorrection2 = -__WallCorrection2
				__FinalDiam2 = math.rad(math.round(__WallCorrection2 - 90))
			end
		end

		if math.deg(__FinalDiam - __FinalDiam2) == 0 and __StateResults ~= __WorkingResults then
			__WallCorrection = __WallCorrection - __WallCorrection2
			__FinalDiam = __FinalDiam - __FinalDiam2
		end
		local __IncomingResults
		for i = DirectionInt, DirectionInt2 do
			__IncomingResults = workspace:Raycast(RootPart.Position, (RootPart.CFrame * CFrame.Angles(0, i, 0)).LookVector * 5, __StateConditions)
			if __IncomingResults and __IncomingResults.Instance ~= __StateResults.Instance then
				print(DirectionInt, DirectionInt2, __IncomingResults.Instance, __StateResults.Instance)
				break
			end
		end
-- 2
		local __Part1Results = workspace:Raycast(RootPart.Position, (RootPart.CFrame * CFrame.Angles(0, math.rad(__FutureCorrection), 0)).LookVector * 5, __StateConditions)
		local __Part2Results

		local Alt1, Alt2 = 90, 0
		if __Part1Results then
			__Part2Results = workspace:Raycast(__Part1Results.Position, (CFrame.new(__Part1Results.Position) * CFrame.Angles(0, -__FutureCorrection, 0)).LookVector * 3, __StateConditions)
			if __Part2Results then
				print(__Part2Results.Instance)
				local Divider = RootPart.CFrame.LookVector
				local Object: Part, Offset = __Part2Results.Instance, __Part2Results.Instance.CFrame:PointToObjectSpace(RootPart.Position)

				local PositionRelativeTo = Vector3.new(math.clamp(Offset.X, -(Object.Size/2) .X, (Object.Size/2).X), math.clamp(Offset.Y, -(Object.Size/2).Y, (Object.Size/2).Y), math.clamp(Offset.Z, -(Object.Size/2).Z, (Object.Size/2).Z))
				local PreciseSurfaceDirection = (Object.CFrame * CFrame.new(PositionRelativeTo)) - RootPart.Position

				Divider = Vector3.new(Divider.X, 0, Divider.Z).Unit
				PreciseSurfaceDirection = Vector3.new(PreciseSurfaceDirection.X, 0, PreciseSurfaceDirection.Z).Unit
				local Intersect = Divider:Cross(PreciseSurfaceDirection)
				Alt1 = 180 - math.deg(math.acos(RootPart.CFrame.LookVector.Unit:Dot(__Part2Results.Normal.Unit)))

				if math.deg(Intersect.Y) < 0 then
					Alt2 = math.rad(math.abs(math.round(Alt1 - 90)))
					Direction2 = "Left"
				else 
					Alt2 = math.rad(math.round(Alt1 - 90))
					Direction2 = "Right"
				end
				
				--print(Direction, Direction2)
				--print(__FinalDiam - Alt1)
			end
		end
-- 3
		local __RoomResults = workspace:Raycast(RootPart.Position, ((RootPart.CFrame * CFrame.new(0, 0, -0.5)) * CFrame.Angles(0, __FinalDiam, 0)).LookVector * 2, __StateConditions)
		local Alt1, Alt2 = 90, 0
		if __RoomResults then
			local Divider, Direction2 = RootPart.CFrame.LookVector, nil
			local Object: Part, Offset = __RoomResults.Instance, __RoomResults.Instance.CFrame:PointToObjectSpace(RootPart.Position)

			local PositionRelativeTo = Vector3.new(math.clamp(Offset.X, -(Object.Size/2) .X, (Object.Size/2).X), math.clamp(Offset.Y, -(Object.Size/2).Y, (Object.Size/2).Y), math.clamp(Offset.Z, -(Object.Size/2).Z, (Object.Size/2).Z))
			local PreciseSurfaceDirection = (Object.CFrame * CFrame.new(PositionRelativeTo)) - RootPart.Position

			Divider = Vector3.new(Divider.X, 0, Divider.Z).Unit
			PreciseSurfaceDirection = Vector3.new(PreciseSurfaceDirection.X, 0, PreciseSurfaceDirection.Z).Unit
			local Intersect = Divider:Cross(PreciseSurfaceDirection)
			Alt1 = 180 - math.deg(math.acos(RootPart.CFrame.LookVector.Unit:Dot(__RoomResults.Normal.Unit)))

			if math.deg(Intersect.Y) < 0 then
				Alt2 = math.rad(math.abs(math.round(Alt1 - 90)))
				Direction2 = "Left"
			else 
				Alt2 = math.rad(math.round(Alt1 - 90))
				Direction2 = "Right"
			end

			if Direction == "Right" and Direction2 == "Left" then
				SpeedZ.Raw = 0
			elseif Direction2 == "Right" and Direction == "Left" then
				SpeedZ.Raw = 0
			end
		end

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.