I’m making a custom camera system and I want to lock the camera’s rotation within a cone shape. The way it works is by using UserInputService.InputChanged to update two angle values, angleX and angleY with the input’s delta. Here’s a demonstration:
local UserInputService = game:GetService("UserInputService")
local angleX = 0
local angleY = 0
function onInputChanged(input, gameProcessedEvent)
local delta = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
angleX = math.clamp((angleX - delta.Y), -45, 45)
angleY = math.clamp((angleY - delta.X), -45, 45)
end
UserInputService.InputChanged:Connect(onInputChanged)
Then inside of a RenderStepped function I rotate the camera using those angles. I do this so I can lerp the angle values to get smoother movements. Now I want to only allow the camera to rotate within a cone shape.
I’ve tried using :Lerp() to only move the camera a certain amount of the way to the target CFrame which partially works, but for some reason the camera rotates sideways.
However that only locks the rotation of the camera and not the angle values, which causes the camera to “freeze” if you look around the corners. It’s also not restricting it to the correct angle. (Probably because I barely understand how it works) I’ve thought of locking the camera’s rotation and then extracting them by using :ToEulerAnglesYXZ() and updating the values, but I want to know if there is a better way to do this?
OP, can you elaborate more on what the problem is, or are you simply trying to find a better way to do this?
I don’t see a problem with just clamping the angles as you are doing. Assuming there are no additional calculations being made while Lerping, it should work fine.
If you want to reduce the cone’s angular size you can just reduce the positive and increase the negative limits on your clamps.
Currently the camera isn’t clamped to a cone, it’s allowed to move within a square shape.
This is the code I have currently in a LocalScript:
--!strict
local CAMERA_LERP_TIME: number = 1 / 10^12
local Players: Players = game:GetService("Players")
local RunService: RunService = game:GetService("RunService")
local UserInputService: UserInputService = game:GetService("UserInputService")
local camera: Camera = workspace.CurrentCamera
local player: Player? = Players.LocalPlayer
local targetAngleX: number = 0
local targetAngleY: number = 0
local angleX: number = 0
local angleY: number = 0
function onRenderStepped(deltaTime: number)
camera.CameraType = Enum.CameraType.Scriptable
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
if not player then return end
local character: Model? = player.Character
if not character then return end
local head: Instance = character:FindFirstChild("Head")
if not head or not head:IsA("BasePart") then return end
local faceFrontAttachment: Instance = head:FindFirstChild("FaceFrontAttachment")
if not faceFrontAttachment or not faceFrontAttachment:IsA("Attachment") then return end
local lerpTime: number = 1 - CAMERA_LERP_TIME ^ deltaTime
angleX = math.lerp(angleX, targetAngleX, lerpTime)
angleY = math.lerp(angleY, targetAngleY, lerpTime)
camera.CFrame = faceFrontAttachment.WorldCFrame * CFrame.fromEulerAnglesYXZ(math.rad(angleX), math.rad(angleY), 0)
end
function onInputChanged(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent then return end
local delta: Vector2 = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
targetAngleX = math.clamp((targetAngleX - delta.Y), -45, 45)
targetAngleY = math.clamp((targetAngleY - delta.X), -45, 45)
end
RunService:BindToRenderStep("Camera", Enum.RenderPriority.Camera.Value + 1, onRenderStepped)
UserInputService.InputChanged:Connect(onInputChanged)
Yes it is a square because your clamps are on -45 and 45. The difference between these numbers is 90 degrees which means you have a full square range of motion. Just reduce the difference between two and you should have a more “conical” result
local UserInputService = game:GetService("UserInputService")
local angleX = 0
local angleY = 0
local maxAngleLimit = 30 -- smaller = more "conical"
function onInputChanged(input, gameProcessedEvent)
local delta = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
angleX = math.clamp((angleX - delta.Y), -maxAngleLimit, maxAngleLimit)
angleY = math.clamp((angleY - delta.X), -maxAngleLimit, maxAngleLimit)
end
UserInputService.InputChanged:Connect(onInputChanged)
That doesn’t restrict it to a cone shape, just a smaller square. It’s allowing the camera to rotate 30 degrees left and right, and 30 degrees up and down.
Ah alright now I realize you want a circular thing.
So for a circle you are going to have to use dynamic limits and calculate them using trigonometry.
Its late at night so I’m not going to brainstorm right now, I’ll help you when I wake up.
Nevermind, I’m insomniac so might as well figure it out.
So I just tested this out in studio and it worked.
The logic behind this is to create a new limit for both pitch and yaw based on the other component. (pitch affects yaw limit and yaw affects pitch limit). It’s essentially trying to circularize the limits.
We know that cos(1) = 0. If we divide our angles by our angular limit, we can get them as a ratio. At fully left/right, yawRatio is going to be -1/1. Because of how cosine works, we can now limit the pitch to a fraction of the angular limit, which specifically is maxAngularLimit * cos(yawRatio). This is really just how a circle works.
Imagine you’re trying to plot points on the circumference of a unit circle. When you plot a point at x = 1/-1, the y coordinate of that point is always going to be 0.
Here is my implementation:
local UserInputService = game:GetService("UserInputService")
local camPitch = 0 -- equivalent to angleY
local camYaw = 0 -- equivalent to angleX
local maxAngularLimit = 15
function onInputChanged(input, gameProcessedEvent)
local delta = Vector2.new(input.Delta.X, input.Delta.Y) * 0.5
local pitchRatio = camPitch / maxAngularLimit
local yawRatio = camYaw / maxAngularLimit
local newPitchLimit = math.cos(yawRatio) * maxAngularLimit
local newYawLimit = math.cos(pitchRatio) * maxAngularLimit
camYaw = clamp(camYaw, -newYawLimit, newYawLimit)
camPitch = clamp(camPitch, -newPitchLimit, newPitchLimit)
end
UserInputService.InputChanged:Connect(onInputChanged)
It’s important to note here that the true range of motion of the camera is actually twice our angular limit because the camera can move maxAngularLimit degrees positively and also negatively.