Alright, so I have been trying to make a custom character controller with slope alignment/ gravity controller.
The movement is great and I am proud of it, but everything completely breaks after calculating normals above or around 90 degrees.
I assume the issue is in the way I calculate which way the character/ blocky part moves but I have no idea how to rotate the camera around the Y-Orientation of the part or if that even will solve the issue since I know how difficult roblox is with 360 movement like this.
here is the code:
-- Create a new part
local part = Instance.new("Part")
part.Size = Vector3.new(4, 4, 4)
part.Anchored = true
part.Position = Vector3.new(0, 5, 0)
part.Parent = workspace
-- Camera setup
local camera = workspace.CurrentCamera
camera.CameraType = Enum.CameraType.Scriptable
camera.CFrame = CFrame.new(0, 5, 0)
wait(2)
-- Parameters
local moveSpeed = 20
local sensitivity = 0.2
local zoomSpeed = 2
local minZoom = 5
local maxZoom = 50
local jumpPower = 50
local gravity = -100
local hoverHeight = 1.8
local currentZoom = 10
local yaw, pitch = 0, 0
local runService = game:GetService("RunService")
local userInputService = game:GetService("UserInputService")
-- Input state
local inputState = { W = false, A = false, S = false, D = false, Space = false }
local isJumping = false
local verticalVelocity = 0
-- Input handling
userInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.W then inputState.W = true
elseif input.KeyCode == Enum.KeyCode.A then inputState.A = true
elseif input.KeyCode == Enum.KeyCode.S then inputState.S = true
elseif input.KeyCode == Enum.KeyCode.D then inputState.D = true
elseif input.KeyCode == Enum.KeyCode.Space then inputState.Space = true
end
end)
userInputService.InputEnded:Connect(function(input)
if input.KeyCode == Enum.KeyCode.W then inputState.W = false
elseif input.KeyCode == Enum.KeyCode.A then inputState.A = false
elseif input.KeyCode == Enum.KeyCode.S then inputState.S = false
elseif input.KeyCode == Enum.KeyCode.D then inputState.D = false
elseif input.KeyCode == Enum.KeyCode.Space then inputState.Space = false
end
end)
userInputService.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
local delta = input.Delta
yaw = yaw - delta.X * sensitivity
pitch = math.clamp(pitch + delta.Y * sensitivity, -45, 45)
elseif input.UserInputType == Enum.UserInputType.MouseWheel then
currentZoom = math.clamp(currentZoom - input.Position.Z * zoomSpeed, minZoom, maxZoom)
end
end)
-- Raycast parameters
local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances = {part}
rayParams.FilterType = Enum.RaycastFilterType.Exclude
-- Function to get average ground below part
local function getGroundPosition()
local origin = part.Position
local halfSize = part.Size / 2
local directions = {
Vector3.new(halfSize.X, 0, halfSize.Z),
Vector3.new(-halfSize.X, 0, halfSize.Z),
Vector3.new(halfSize.X, 0, -halfSize.Z),
Vector3.new(-halfSize.X, 0, -halfSize.Z),
Vector3.new(0, 0, 0),
}
local averagePosition = Vector3.zero
local hitCount = 0
for _, dir in ipairs(directions) do
local rayOrigin = origin + dir
local rayDirection = -part.CFrame.UpVector * (hoverHeight + 2)
local result = workspace:Raycast(rayOrigin, rayDirection, rayParams)
if result then
averagePosition += result.Position
hitCount += 1
end
end
if hitCount > 0 then
return averagePosition / hitCount
else
return nil
end
end
-- Function to align part to surface normal
local function alignPart(normal)
if normal then
local upVec = normal
local rightVec = upVec:Cross(part.CFrame.LookVector).Unit
local forwardVec = rightVec:Cross(upVec).Unit
part.CFrame = CFrame.fromMatrix(part.Position, rightVec, upVec, forwardVec)
end
end
-- Main update loop
runService.RenderStepped:Connect(function(deltaTime)
-- Raycast down to get surface normal
local downRay = workspace:Raycast(part.Position, -part.CFrame.UpVector * 6, rayParams)
local surfaceNormal = downRay and downRay.Normal or Vector3.new(0,1,0)
-- Camera rotation
local cameraRotation = CFrame.Angles(0, math.rad(yaw), 0) * CFrame.Angles(math.rad(pitch), 0, 0)
-- Movement direction projected onto surface plane
local moveDirection = Vector3.zero
local camForward = camera.CFrame.LookVector
local camRight = camera.CFrame.RightVector
camForward = (camForward - camForward:Dot(surfaceNormal) * surfaceNormal)
camRight = (camRight - camRight:Dot(surfaceNormal) * surfaceNormal)
if inputState.W then moveDirection += camForward end
if inputState.S then moveDirection -= camForward end
if inputState.A then moveDirection -= camRight end
if inputState.D then moveDirection += camRight end
if moveDirection.Magnitude > 0 then
moveDirection = moveDirection.Unit
part.Position += moveDirection * moveSpeed * deltaTime
part.CFrame = CFrame.new(part.Position, part.Position + moveDirection)
end
-- Jump logic
if inputState.Space and not isJumping then
isJumping = true
verticalVelocity = jumpPower
end
-- Gravity, Collision detection
local groundPos = getGroundPosition()
if groundPos then
local targetHeight = groundPos.Y + hoverHeight
if not isJumping then
part.Position = Vector3.new(part.Position.X, targetHeight, part.Position.Z)
else
part.Position = Vector3.new(part.Position.X, math.max(part.Position.Y + verticalVelocity * deltaTime, targetHeight), part.Position.Z)
end
if part.Position.Y <= targetHeight then
isJumping = false
verticalVelocity = 0
end
else
verticalVelocity += gravity * deltaTime
part.Position += Vector3.new(0, verticalVelocity * deltaTime, 0)
end
-- Camera follow
local cameraOffset = Vector3.new(0, 5, -currentZoom)
local cameraPosition = part.Position + cameraRotation * cameraOffset
camera.CFrame = CFrame.new(cameraPosition, part.Position)
-- Align part to surface
alignPart(surfaceNormal)
end)
Please if anyone could check this out and give me some help with this it would mean the world to me.