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.