The CameraOffset property of Humanoid probably isn’t the best way to tackle this problem because it is so limited in terms of what you can do with it and how you can modify it. A better solution would be to update the CFrame of the camera after every frame.
I actually had a similar problem a while back, when I was trying to create a 3rd person shooter game. When studying camera manipulation, I found that Roblox created their own compact (albeit basic) 3rd person over-the-shoulder camera script as a demonstration of camera manipulation.
Here is the code for it:
local Players = game:GetService("Players")
local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local camera = workspace.CurrentCamera
local cameraOffset = Vector3.new(2, 2, 8)
local player = Players.LocalPlayer
player.CharacterAdded:Connect(function(character)
local humanoid = character:WaitForChild("Humanoid")
local rootPart = character:WaitForChild("HumanoidRootPart")
humanoid.AutoRotate = false
local cameraAngleX = 0
local cameraAngleY = 0
local function playerInput(actionName, inputState, inputObject)
-- Calculate camera/player rotation on input change
if inputState == Enum.UserInputState.Change then
cameraAngleX = cameraAngleX - inputObject.Delta.X
-- Reduce vertical mouse/touch sensitivity and clamp vertical axis
cameraAngleY = math.clamp(cameraAngleY-inputObject.Delta.Y*0.4, -75, 75)
-- Rotate root part CFrame by X delta
rootPart.CFrame = rootPart.CFrame * CFrame.Angles(0, math.rad(-inputObject.Delta.X), 0)
end
end
ContextActionService:BindAction("PlayerInput", playerInput, false, Enum.UserInputType.MouseMovement, Enum.UserInputType.Touch)
RunService.RenderStepped:Connect(function()
if camera.CameraType ~= Enum.CameraType.Scriptable then
camera.CameraType = Enum.CameraType.Scriptable
end
local startCFrame = CFrame.new((rootPart.CFrame.Position)) * CFrame.Angles(0, math.rad(cameraAngleX), 0) * CFrame.Angles(math.rad(cameraAngleY), 0, 0)
local cameraCFrame = startCFrame:ToWorldSpace(CFrame.new(cameraOffset.X, cameraOffset.Y, cameraOffset.Z))
local cameraFocus = startCFrame:ToWorldSpace(CFrame.new(cameraOffset.X, cameraOffset.Y, -10000))
camera.CFrame = CFrame.new(cameraCFrame.Position, cameraFocus.Position)
end)
end)
local function focusControl(actionName, inputState, inputObject)
-- Lock and hide mouse icon on input began
if inputState == Enum.UserInputState.Begin then
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
UserInputService.MouseIconEnabled = false
ContextActionService:UnbindAction("FocusControl", focusControl, false, Enum.UserInputType.MouseButton1, Enum.UserInputType.Touch, Enum.UserInputType.Focus)
end
end
ContextActionService:BindAction("FocusControl", focusControl, false, Enum.UserInputType.MouseButton1, Enum.UserInputType.Touch, Enum.UserInputType.Focus)
Here is the article from which I got this code. None of this was made by me.
You can look at the article for more information, but here are some key parts of it that are probably relevant to your issue.
- You’ll notice that the character turns based off of user input rather than on each frame. This system should be more efficient than having the HRP CFrame be updated for every frame. Though, this is a small change from your design, and I believe your implementation of this mechanic would work just fine.
- The camera’s CFrame property is set every frame. Hardcoding a camera position is more work, but allows for more flexibility and tinkering. In turn, this allows for you to use math.clamp() to constrain the camera in a reasonable Y-direction range as seen in the code. Essentially, there is a function that updates two cameraAngle variables on userInput (the playerInput function) and a RenderStepped function that just uses that information to constantly set the CFrame of the camera correctly. There is a lot of complex CFrame operations that are difficult to visualize in this code snippet, so if you need help understanding them, please do not hesitate to ask.
- The focusControl function is just there to initiate the third person camera. It shouldn’t be relevant to you at all since it seems like you already have a plan for this.
Hopefully, this answers your question. I know there’s a lot in this script I didn’t address, but much of what is done here should be pretty self explanatory. Just remember that the camera’s CFrame is essentially updated every frame. The article should provide some more insight if you’re still confused.
Let me know if you have any further questions.