Linear Velocity Constraint Causes Undesired Behavior During Collisions

Thanks for the reply! I’m thinking of a modified version: shooting multiple raycasts top to bottom along the direction of the velocity so that it not only works when the physics box is moving forward but also backward and also in edge cases where the obstacle is only blocking the bottom and/or top of the physics box. And if so, i will have to determine what component of the velocity will result in closing the distance further between the physics box and the obstacle and nullify that component.

I’ll post my testing results once I am able. In the meantime, I would appreciate if anyone posted an alternative solution because this is quite the hairy workaround.

1 Like

I think this Method is going into the right direction, this is how I would have done it:

local physicsBox = --[[ your physics box ]]
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
raycastParams.FilterDescendantsInstances = {physicsBox}

local function checkCollision()
    local raycastResult = workspace:Raycast(physicsBox.Position, physicsBox.Velocity.Unit * 10, raycastParams)

    if raycastResult then
        local normal = raycastResult.Normal
        local velocity = physicsBox.Velocity
        local reflectedVelocity = velocity - 2 * (velocity:Dot(normal)) * normal
        physicsBox.Velocity = reflectedVelocity
    end
end

game:GetService("RunService").Heartbeat:Connect(checkCollision)

It should prevent the jittering and glitching since we reflect the velocity off the wall. This is only an idea though and I don’t know it will work for sure.

2 Likes

Thanks for the reply! I modified the code and it works beautifully when colliding straight into the wall, but issues arise again when colliding at certain angles:

Video of scooter physics box colliding with wall at different angles

A little bit of context: the linearVelocity I keep referencing is a LinearVelocity constraint whose constraint mode is Plane. Assume that the constraint is constantly being updated such that the X component of the linearVelocity is always collinear with the look vector of the physicsBox, while the Y component is always collinear with the right vector of the physicsBox. There is no Z component since the constraint mode is Plane.

Here’s the code:

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

  local origin = scooterPhysicsBox.Position

  -- multiply by 0.55 to make ray go a little past the scooter
  local direction = velocity.Unit * maxDimension * 0.55
  local result = workspace:Raycast(origin, direction, 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)
	end
end

RunService.Heartbeat:Connect(handleCollisions)

bumping since I haven’t found a solution yet ;-;

I don’t have very much experience with making vehicles, but have you tried limiting the max velocity?

Thank you for your response! Yes, I have limited the max velocity. The problem in this case, though is with how Roblox treats the collision between the obstacle (e.g., a wall) and the physics box (an invisible rectangular prism that represents the hitbox of the scooter). No matter what the speed is (even if it’s as small as 1), the same issue occurs with the physics box jittering and sliding up the wall. The linear velocity constraint keeps pushing the physics box in the direction the player is trying to move (even when touching the obstacle) and so I guess this causes issues, but I would think that the rectangular prism would simply not move if it’s already pressed against the wall

After some brief testing, it seems that a VectorForce is much more stable than a LinearVelocity. Have you tried using those?

No because it applies a constant force and therefore constant acceleration but I want constant velocity (no acceleration)

Bumping as I have not yet found a working solution

two days later and still no solution… ;-;

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.