Collide and Slide Algorithm

I’ve been trying to create a custom capsule collider, but so far I’ve encountered a bug in which you seem to phase through objects at random times. I’m not going to use Roblox’s default physics because I want more control.

NOTE: This is a for a custom collider, not a gameplay mechanic.

Here’s the following code that I’ve been using.

--// Services
local RunService = game:GetService("RunService");
local UserInputService = game:GetService("UserInputService");

--// Variables
local camera = workspace.CurrentCamera;
local capsule = script.Capsule:Clone();
local collider = {
	Velocity = Vector3.zero,
	Position = Vector3.new(0, 50, 0),
	LastPosition = Vector3.new(0, 50, 0),
	Gravity = 64,
	TerminalVelocity = 240,
};

local zoom = 0;
local userRotation = Vector2.zero;
local inputAxis = {
	[Enum.KeyCode.W] = -Vector3.zAxis,
	[Enum.KeyCode.S] = Vector3.zAxis;
	[Enum.KeyCode.A] = -Vector3.xAxis;
	[Enum.KeyCode.D] = Vector3.xAxis;
};

local limits = {
	pitch = math.rad(89),
};

local isGrounded = false;
local maxBounces = 5;
local skinWidth = 0.1;
local maxSlopeAngle = 55;

local raycastParams = RaycastParams.new();
raycastParams.FilterDescendantsInstances = {capsule, camera};

local overlapParams = OverlapParams.new();
overlapParams.FilterDescendantsInstances = {capsule};

--// Local Functions
local function ProjectOnPlane(v,n)	
	return v - (((v:Dot(n))/(n.Magnitude)^2)*n)
end

local function ProjectAndScale(vector, normal)
	local magnitude = vector.Magnitude;
	vector = ProjectOnPlane(vector, normal);
	return vector;
end

local function getMouseRotation(input: InputObject) --// Roblox Camera Rotation Feature
	local inputDelta = (input.Delta * (Vector3.new(1, 1, 0) * math.rad(0.5)));
	local mouseDelta = userRotation + Vector2.new(inputDelta.X, inputDelta.Y);
	return Vector2.new(mouseDelta.X, math.clamp(mouseDelta.Y, -limits.pitch, limits.pitch));
end

local function getMouseZoom(input: InputObject) --// Roblox Zoom Feature
	local curZoom = zoom;
	local zoomDelta = -input.Position.Z;
	local sensCurvature = 0.2;

	if (math.abs(zoomDelta) > 0) then
		local newZoom;

		if zoomDelta > 0 then
			newZoom = curZoom + zoomDelta * (1 + curZoom * sensCurvature);
			newZoom = math.max(newZoom, 1);
		else
			newZoom = (curZoom + zoomDelta)/(1 - zoomDelta * sensCurvature);
			newZoom = math.max(newZoom, 0.5);
		end

		if newZoom < 1 then
			newZoom = 0.5;
		end

		return math.clamp(newZoom, 0, 12);
	end

	return math.clamp(curZoom, 0, 12);
end

--// Main Function
local function CollideAndSlide(velocity, position, depth, gravityPass, velInit)
	if (depth >= maxBounces) then
		return Vector3.zero;
	end

	local dist = velocity.Magnitude + skinWidth;
	local unitVel = if (velocity.Magnitude > 0) then velocity.Unit else Vector3.zero;

	local savedCFrame = capsule.CFrame;
	capsule.CFrame = CFrame.new(position);
	
	local collisionCast = workspace:Shapecast(capsule, unitVel * dist, raycastParams);
	if (collisionCast) then
		local snapToSurface = unitVel * (collisionCast.Distance - skinWidth);
		local leftover = velocity - snapToSurface;
		local angle = Vector3.yAxis:Angle(collisionCast.Normal);

		if (snapToSurface.Magnitude <= skinWidth) then
			snapToSurface = Vector3.zero;
		end

		--// Normal Ground/Slope
		if (angle <= maxSlopeAngle) then
			if (gravityPass) then
				capsule.CFrame = savedCFrame;
				return snapToSurface;
			end

			leftover = ProjectAndScale(leftover, collisionCast.Normal);
		else
			--// wall or Steep slope
			local scale = 1 - Vector3.new(collisionCast.Normal.X, 0, collisionCast.Normal.Z).Unit:Dot(
				-Vector3.new(velInit.X, 0, velInit.Z).Unit
			);
			
			if (isGrounded and not gravityPass) then
				leftover = ProjectAndScale(
					Vector3.new(leftover.X, 0, leftover.Z),
					Vector3.new(collisionCast.Normal.X, 0, collisionCast.Normal.Z)
				).Unit;

				leftover *= scale;
			else
				leftover = ProjectAndScale(leftover, collisionCast.Normal) * scale;
			end
		end

		capsule.CFrame = savedCFrame;
		return snapToSurface + CollideAndSlide(leftover, position + snapToSurface, depth + 1, gravityPass, velInit);
	end
	
	capsule.CFrame = savedCFrame;
	return velocity;
end

--// Init
capsule.Parent = workspace;

UserInputService.InputBegan:Connect(function(input)
	if (input.KeyCode == Enum.KeyCode.E) then
		collider.Position = Vector3.new(0, 50, 0);
	end
end);

UserInputService.InputChanged:Connect(function(input, GPE)
	if (GPE) then return end;

	if (input.UserInputType == Enum.UserInputType.MouseMovement) then
		userRotation = getMouseRotation(input);
	end
	
	if (input.UserInputType == Enum.UserInputType.MouseWheel) then
		zoom = getMouseZoom(input);
	end
end);

workspace:WaitForChild("Baseplate");
RunService:BindToRenderStep("Collider", Enum.RenderPriority.Camera.Value, function(dt)
	camera.CameraType = Enum.CameraType.Scriptable;
	UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter;
	
	local movementVector = Vector3.zero;
	for key, vector in pairs(inputAxis) do
		if (UserInputService:IsKeyDown(key)) then
			movementVector += vector;
		end
	end

	if (movementVector.Magnitude > 0) then
		local lookVector = camera.CFrame.LookVector;
		local cameraCFrame = CFrame.new(Vector3.zero, Vector3.new(lookVector.X, 0, lookVector.Z));

		movementVector = (cameraCFrame:ToWorldSpace() * movementVector).Unit;
	end
	
	local gravity = Vector3.new(0, -collider.Gravity * dt, 0);

	local groundcast = workspace:Raycast(collider.Position, Vector3.yAxis * -2.1, raycastParams);
	isGrounded = (groundcast) ~= nil;
	local moveAmount = (movementVector * 16) * dt;

	moveAmount = CollideAndSlide(moveAmount, collider.Position, 0, false, moveAmount);

	if not (isGrounded) then
		moveAmount += CollideAndSlide(gravity, collider.Position + moveAmount, 0, true, gravity);
	end
	
	collider.Position = collider.Position + moveAmount;
	capsule.CFrame = CFrame.new(collider.Position);
	
	camera.CFrame = CFrame.new(collider.Position) * CFrame.fromEulerAnglesYXZ(-userRotation.Y, -userRotation.X, 0) * CFrame.new(0, 0, zoom);
end);

If you want to see the entire place file, I can link it.

1 Like

I understand you don’t want to use roblox’s physics but the sliding would definitely work alot better if you were using a prismatic constraint. Which allows two constraints to slide on one another, if you really don’t want to use a prismatic then you can use tweens which don’t respect roblox physics at all (but it may not be that smooth due to tweening a character and it may be very difficult). Up to you.

Sorry, maybe I should’ve specified that it’s a form of collision response, not an actual sliding mechanic. I’m trying to make a custom capsule collider, not any movement mechanics.

i’ve already been trying to make a character controller a few months ago and the exact same issue happened to me

after hours of trying to figure out what’s wrong in my code i found out that roblox’s shapecast functions, in some cases, aren’t that accurate, which makes your character clip into the wall

so unfortunately the shapecast functions aren’t reliable enough for this type of stuff for now

That really sucks, should I just go the AABB route then?

1 Like

to be honest i wont really be able to give you advice for this because i just didnt bother and made it physics based after that, i wanted it to work on ramps, meshparts, terrain and stuff so i had no choice really

That’s fine, I think I’ll just try different methods for a little bit and if they don’t really work I’ll just go physics based like you did. Thank you for the help!

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