Linear Velocity Constraint Causes Undesired Behavior During Collisions

bumping bc hour has passed; experimenting w/ linear velocity but still no good solution. My current collision-handling code now shoots raycasts top-to-bottom along the scooter’s velocity to cover edge cases. However, if the scooter is tilted enough, the raycasts won’t hit but I don’t want the raycasts to be too long because otherwise the collisions will be treated improperly in “normal” cases where the scooter hits the obstacle head-on. Current code:

local function handleCollisions()
    -- get current velocity in world space
	local velocity = scooterPhysicsBox.CFrame.LookVector * linearVelocity.PlaneVelocity.X
	
    -- shoot raycasts top-to-bottom
	for y = -1, 1, 1 do
        -- calculate raycast origin depending on offset
		local origin = scooterPhysicsBox.Position + Vector3.new(0, y * scooterPhysicsBox.Size.Y * 0.5 * 0.9, 0)

        -- get shoot raycast just past the edge of the scooter
        -- (hence the 0.55 which should be interpreted as 55%)
        -- maxDimension is just the maximum dimension of the scooter size (X, Y, or Z)
		local result = workspace:Raycast(origin, velocity.Unit * maxDimension * 0.55, raycastParams)
		
		-- Ensure result exists and surface is steep enough to be an obstacle
		if result ~= nil and result.Instance ~= nil and result.Normal.Y <= 0.5 then
            -- calculate reflected velocity along surface normal
			local reflectedWorldVelocity = scooterPhysicsBox.CFrame:PointToObjectSpace(velocity - 2 * velocity:Dot(result.Normal) * result.Normal)
			
            -- plane velocity is a two-dimensional vector always parallel with the scooter look vector
			linearVelocity.PlaneVelocity = Vector2.new(reflectedWorldVelocity.X, reflectedWorldVelocity.X)
			return
		end
	end
end

RunService.Heartbeat:Connect(handleCollisions)

How about trying to raycast in multiple different directions? I am assuming the only raycast in that code won’t hit the wall if angled.

Something similar like this:

local function handleCollisions()
	local velocity = scooterPhysicsBox.CFrame.LookVector * linearVelocity.PlaneVelocity.X
	local origin = scooterPhysicsBox.Position

	local directions = {
		velocity.Unit,
		(velocity + scooterPhysicsBox.CFrame.RightVector).Unit,
		(velocity - scooterPhysicsBox.CFrame.RightVector).Unit,
	}

	for _, direction in ipairs(directions) do
		
		-- multiply by 0.55 to make ray go a little past the scooter
		local rayDirection = direction * maxDimension * 0.55
		local result = workspace:Raycast(origin, rayDirection, raycastParams)

		if result ~= nil and result.Instance ~= nil then
			local incidentVector = velocity.Unit
			local normalVector = result.Normal
			local speed = linearVelocity.PlaneVelocity.X

			local dampingFactor = 0.5

			local reflectedVector = incidentVector - 2 * incidentVector:Dot(normalVector) * normalVector
			local reflectedVelocity = reflectedVector * math.abs(speed) * dampingFactor

			linearVelocity.PlaneVelocity = Vector2.new(reflectedVelocity.X, reflectedVelocity.Z)
			break
		end
	end
end
1 Like

Thanks for your reply! Great thinking! I tweaked it to fit with my code and that almost got me the result I am looking for. Although it does handle collisions correctly when going at a sufficient speed, it does not resolve the overall issue when colliding at a very low speed and causes the scooter to get stuck for some reason. Essentially what’s happening is when the scooter is already in contact with the wall and it just starts moving against the wall, the reflected velocity does not reflect much and this causes the scooter to eventually touch the wall and start having a “lesser” version of the original behavior; it just stays stuck to the wall as if there is friction. And btw I made sure to have zero friction on the physics box via custom physical properties.
Video of scooter physics box getting stuck on wall

Here’s the current code:

local function handleCollisions()
	local velocity = scooterPhysicsBox.CFrame.LookVector * linearVelocity.PlaneVelocity.X
	local dampingFactor = 0.5
	
    -- Shoot raycasts in multiple directions
	for x = -1, 1, 1 do
		for y = -1, 1, 1 do
            -- Offset and cast ray as appropriate
			local origin = scooterPhysicsBox.Position + Vector3.new(x * scooterPhysicsBox.Size.X * 0.5 * 1.1, y * scooterPhysicsBox.Size.Y * 0.5 * 0.9, 0)
			local result = workspace:Raycast(origin, velocity.Unit * maxDimension * 0.55, raycastParams)

			-- Ensure result exists and surface is steep enough to be an obstacle
			if result ~= nil and result.Instance ~= nil and result.Normal.Y <= 0.5 then
                -- Get unit vector representing direction of reflected vector
				local reflectedVector = velocity.Unit - 2 * velocity.Unit:Dot(result.Normal) * result.Normal
               
                -- Apply a magnitude with damping to reflected vector to get reflected velocity
				local reflectedVelocity = reflectedVector * velocity.Magnitude * dampingFactor

				linearVelocity.PlaneVelocity = Vector2.new(reflectedVelocity.X, reflectedVelocity.Z)
				return
			end
		end
	end
end

Maybe try using a push factor that always applies no matter the speed?

local function handleCollisions()
	local velocity = scooterPhysicsBox.CFrame.LookVector * linearVelocity.PlaneVelocity.X
	local dampingFactor = 0.5
	local pushFactor = 0.1 -- your push factor
	
	for x = -1, 1, 1 do
		for y = -1, 1, 1 do
			local origin = scooterPhysicsBox.Position + Vector3.new(x * scooterPhysicsBox.Size.X * 0.5 * 1.1, y * scooterPhysicsBox.Size.Y * 0.5 * 0.9, 0)
			local result = workspace:Raycast(origin, velocity.Unit * maxDimension * 0.55, raycastParams)

			if result ~= nil and result.Instance ~= nil and result.Normal.Y <= 0.5 then
				local reflectedVector = velocity.Unit - 2 * velocity.Unit:Dot(result.Normal) * result.Normal
			   
				local reflectedVelocity = reflectedVector * velocity.Magnitude * dampingFactor
				reflectedVelocity = reflectedVelocity + (-result.Normal * pushFactor) -- apply the small push factor here

				linearVelocity.PlaneVelocity = Vector2.new(reflectedVelocity.X, reflectedVelocity.Z)
				return
			end
		end
	end
end

Although that did make it take longer, the scooter still eventually made its way to the wall. I added a ternary operator that checks if the magnitude of the current velocity is below some threshold and if so, it just reflects the entirety of the velocity rather than damping:

local function handleCollisions()
	local velocity = scooterPhysicsBox.CFrame.LookVector * linearVelocity.PlaneVelocity.X
	local velocityThreshold = 5
	local dampingFactor = 0.5
	
	for x = -1, 1, 1 do
		for y = -1, 1, 1 do
			local origin = scooterPhysicsBox.CFrame:PointToWorldSpace(Vector3.new(x * scooterPhysicsBox.Size.X * 0.5 * 1.1, y * scooterPhysicsBox.Size.Y * 0.5 * 0.9, 0))
			local result = workspace:Raycast(origin, velocity.Unit * maxDimension * 0.525, raycastParams)

			-- Ensure result exists and surface is steep enough to be an obstacle
			if result ~= nil and result.Instance ~= nil and result.Normal.Y <= 0.5 then
				local reflectedVector = velocity.Unit - 2 * velocity.Unit:Dot(result.Normal) * result.Normal
				local reflectedVelocity = reflectedVector * velocity.Magnitude * (if velocity.Magnitude <= velocityThreshold then 1 else dampingFactor)

				linearVelocity.PlaneVelocity = Vector2.new(reflectedVelocity.X, reflectedVelocity.Z).Unit * reflectedVelocity.Magnitude
				return
			end
		end
	end
end

;-; Overall this does resolve the issue… But I found a case in which this does not work. When the scooter bumps into the wall backward, it for some reason still gets stuck. And even sometimes when going in forward…
Video of scooter getting stuck

Hmm after some testing, I found that this hardly works when the scooter’s back part touches the wall. It doesn’t even bounce back ;-;

Adjusting the code a little, I implemented a check to see if the scooter is driving backwards into the wall. And I also added a velocityThreshold to check if the scooter is too slow, and if so, we will just reverse the velocity.

local function handleCollisions()
	local velocity = scooterPhysicsBox.CFrame.LookVector * linearVelocity.PlaneVelocity.X
	local velocityThreshold = 5
	local dampingFactor = 0.5
	
	for x = -1, 1, 1 do
		for y = -1, 1, 1 do
			local origin = scooterPhysicsBox.CFrame:PointToWorldSpace(Vector3.new(x * scooterPhysicsBox.Size.X * 0.5 * 1.1, y * scooterPhysicsBox.Size.Y * 0.5 * 0.9, 0))
			local result = workspace:Raycast(origin, velocity.Unit * maxDimension * 0.525, raycastParams)

			if result ~= nil and result.Instance ~= nil and result.Normal.Y <= 0.5 then
				local reflectedVector = velocity.Unit - 2 * velocity.Unit:Dot(result.Normal) * result.Normal
				local reflectedVelocity

				local angle = math.acos(velocity.Unit:Dot(scooterPhysicsBox.CFrame.LookVector))
				if angle > math.pi / 2 then
					reflectedVelocity = -reflectedVector * velocity.Magnitude
				elseif velocity.Magnitude <= velocityThreshold then
					reflectedVelocity = -velocity
				else
					reflectedVelocity = reflectedVector * velocity.Magnitude * dampingFactor
				end

				linearVelocity.PlaneVelocity = Vector2.new(reflectedVelocity.X, reflectedVelocity.Z).Unit * reflectedVelocity.Magnitude
				return
			end
		end
	end
end
1 Like

Nice! It works as expected! Thank you very much! Hopefully in the future, this won’t be an issue with Linear Velocity Constraints; it doesn’t seem very natural to slide up walls :woozy_face:. I really appreciate your time and effort :pray:

1 Like

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