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.
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.
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:
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)
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
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
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
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
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 . I really appreciate your time and effort