-----------------UPDATE-----------------
Update: I found a solution, it requires significantly more effort but here’s how I went about doing this. I used a Triangle-Capsule collision formula and utilized creating map assets in blender. I created a blender plugin to capture the triangle data to a .txt file on my computer and then made a converter to properly convert the blender data to readable Roblox data. There I would check for and resolve any collision detected by the Triangle-Capsule formula.
Cons: It’s not the best way by any means because it ends up creating huge module files and forces you to a chunk system if you want smooth gameplay. It ONLY allows for static maps, so any moving platforms you’d have to change the way the system works.
The reason why raycast colliders don’t work is because Shape Casts don’t query anything that’s already inside the origin point. They’d also not be amazingly optimized since you’d be moving a part per collider every frame because of the way Shape Cast works. A method that could possibly work but I couldn’t find a way would be using GetPartsInPart() and the :GetClosestPointOnSurface().
If you have any questions just DM me.
-----------------OLD POST-----------------
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.