After a significant amount of research and testing, I think I might have solved it. However, there are two issues that I have not been able to fix.
- Sometimes the camera will glitch and roll on the Z-Asix, which is what I do not want. I want a flat horizon at all times.
- The camera position itself seems to move on a circle when moving up and down. Run the below script and you will see what I mean. I want the camera position to follow the position of the character’s head.
The things that I have done to try to correct the above issues are:
- Manually set the Z-Axis to 0 before assignment to
currentCameraCFrame
.
- Take the CFrame apart and put it back together again with Z-Axis set to 0 in the render step.
- Different combinations of positions, offsets,
CFrame:ToWorldSpace()
, CFrame:ToObjectSpace()
I’m at a loss as to how to correct these issues. Does anyone have any suggestions that I might try?
The following posts were helpful in getting to this point:
How do I stop camera from rotating on Z axis?
Camera has strange behavior when rotating in more than 1 direction
How to make camera never orientate based on Z axis
Camera View Rolling/Tilting not Working
The below code is a local script.
--[[
Created by Roblox
Modified by Maelstorm_1973
This code was adapted from Roblox's reference implementation of
binoculars which can be found at the following URL:
https://create.roblox.com/docs/reference/engine/classes/InputObject#Delta
NOTES:
This is a test script for taking control of the user's camera
and make it follow the mouse. Eventually, this will be adapted
to also use Touch and Gamepad Thumbstick inputs as well.
All references to the mouse was excised from the script. The
target location is now calculated from the player's head position
and look vector when the zoom is initiated. Eventually, the
sensitivity settings will be separated into different categories
depending on what the input device is.
BUGS:
* There is a glitch that sometimes the camera will still roll
on the Z-Axis, but it seems to self correct after a few mouse
movements.
* The camera position itself seems to move on a circle when
moving up and down. Not sure how to correct this one.
* Performance of the camera update in the render stepped event
is sluggish. Possibly due to the tween that adjusts the
camera from the previous CFrame to the current CFrame.
--]]
-- ******** Requirements
-- Required Game Services and Facilities
local userInputService = game:GetService("UserInputService")
local playerService = game:GetService("Players")
local runService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- ******** Local Data
-- Player
local player = playerService.LocalPlayer
local character = player.CharacterAdded:Wait()
local head = character:WaitForChild("Head", false)
local root = character:WaitForChild("HumanoidRootPart")
-- Camera
local zoomed = false
local camera = game.Workspace.CurrentCamera
local target = nil
local screenCenter = camera.ViewportSize / 2
-- Camera Original Parameters
local originalProperties = {
fieldOfView = nil;
cframe = nil;
mouseBehavior = nil;
mouseDeltaSensitivity = nil;
}
-- Camera Orientation
local angleLimit = 80
local angleX, targetAngleX = 0, 0
local angleY, targetAngleY = 0, 0
local currentCameraCFrame = nil
local previousCameraCFrame = nil
-- Camera Field Of View
local minFieldOfView = 70
local maxFieldOfView = 1
local minSensitivity = 1.8
local maxSensitivity = 0.1
local currentFieldOfView
-- Sensitivity Scale
local currentScale = 0
-- Event References
local renderSteppedEvent = nil
-- ******** Functions / Methods
-- Convert number from one scale into a different scale.
local function convertScale(x, fromLow, fromHigh, toLow, toHigh, invert)
x = math.clamp(x, fromLow, fromHigh)
local percent = (x - fromLow) / (fromHigh - fromLow)
if invert == true then
percent = 1 - percent
end
return math.clamp(percent * (toHigh - toLow) + toLow, toLow, toHigh)
end
-- Reset camera back to CFrame and FieldOfView before zoom
local function resetCamera()
if renderSteppedEvent ~= nil then
renderSteppedEvent:Disconnect()
renderSteppedEvent = nil
end
-- Clear Target
target = nil
-- Restore Original Parameters
camera.CameraType = Enum.CameraType.Custom
camera.CFrame = originalProperties.cframe
camera.FieldOfView = originalProperties.fieldOfView
userInputService.MouseBehavior = originalProperties.mouseBehavior
userInputService.MouseDeltaSensitivity = originalProperties.mouseDeltaSensitivity
end
local function zoomCamera()
-- Allow camera to be changed by script.
camera.CameraType = Enum.CameraType.Scriptable
-- Store camera properties before zoom.
originalProperties.cframe = camera.CFrame
originalProperties.fieldOfView = camera.FieldOfView
originalProperties.mouseBehavior = userInputService.MouseBehavior
originalProperties.mouseDeltaSensitivity = userInputService.MouseDeltaSensitivity
-- Zoom camera
target = head.Position + head.CFrame.LookVector * 20
currentCameraCFrame = CFrame.new(head.Position, target) * CFrame.new(0, 1, -5)
currentFieldOfView = 40
currentScale = convertScale(currentFieldOfView, maxFieldOfView, minFieldOfView, maxSensitivity, minSensitivity, true)
camera.Focus = CFrame.new(target)
-- Lock and slow down mouse
userInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
userInputService.MouseDeltaSensitivity = 1
-- Reset zoom angles
angleX, targetAngleX = 0, 0
angleY, targetAngleY = 0, 0
-- Update camera every frame if needed.
-- XXX: This is a bit sluggish, probably due to the tween. This
-- will need to be adjusted or even recoded to use a different
-- method.
renderSteppedEvent = runService.RenderStepped:Connect(function(deltaTime)
if currentCameraCFrame ~= nil then
if previousCameraCFrame ~= currentCameraCFrame then
local twinfo = TweenInfo.new(deltaTime)
local goal = {}
goal.CFrame = currentCameraCFrame
local tween = TweenService:Create(camera, twinfo, goal)
tween:Play()
previousCameraCFrame = currentCameraCFrame
end
end
camera.FieldOfView = currentFieldOfView
end)
end
-- Toggle camera zoom/unzoom
local function mouseClick()
if zoomed then
-- Unzoom camera
resetCamera()
zoomed = false
else
-- Zoom in camera
zoomCamera()
zoomed = true
end
end
-- Called when the mouse is moved.
local function mouseMoved(input)
if zoomed then
--local sensitivity = .6 -- anything higher would make looking up and down harder; recommend anything between 0~1
local smoothness = 0.05 -- recommend anything between 0~1
local sensitivity = currentScale
print(currentScale)
-- Compute new delta based on sensitivity and smoothness
local delta = Vector2.new(input.Delta.x / sensitivity, input.Delta.y / sensitivity) * smoothness
-- I'm not sure why the delta and targetAngle axes are
-- crossed like this, but undoing this makes the camera
-- go insane.
local X = targetAngleX - delta.Y
local Y = targetAngleY - delta.X
-- Apply Angle Limits
targetAngleX = (X >= angleLimit and angleLimit) or (X <= -angleLimit and -angleLimit) or X
targetAngleY = (Y >= angleLimit and angleLimit) or (Y <= -angleLimit and -angleLimit) or Y
-- Not entirely sure what the significance of this
-- strange scaling method is, but it seems to be
-- reqired.
angleX = angleX + (targetAngleX - angleX) * 0.35
angleY = angleY + (targetAngleY - angleY) * 0.15
-- Calculate a new CFrame for the camera.
-- **** ORDER IS IMPORTANT ****
local cframe = CFrame.new()
cframe *= cframe:ToObjectSpace(CFrame.Angles(0, math.rad(angleY), 0))
cframe *= CFrame.Angles(math.rad(angleX), 0, 0)
cframe *= CFrame.new(head.Position, target)
cframe *= CFrame.new(0, 1, -5)
currentCameraCFrame = cframe
-- Calculate a new camera focus.
camera.Focus = CFrame.new(cframe.Position + cframe.LookVector * 20)
end
end
-- Mouse wheel event handler.
local function mouseWheel(input)
if input.Position.Z == 1 then
local temp = currentFieldOfView
temp = math.clamp(temp - 3, maxFieldOfView, minFieldOfView)
currentScale = convertScale(temp, maxFieldOfView, minFieldOfView, maxSensitivity, minSensitivity, true)
currentFieldOfView = temp
elseif input.Position.Z == -1 then
local temp = currentFieldOfView
temp = math.clamp(temp + 3, maxFieldOfView, minFieldOfView)
currentScale = convertScale(temp, maxFieldOfView, minFieldOfView, maxSensitivity, minSensitivity, true)
currentFieldOfView = temp
end
end
-- Function call lookup table for input begin event handlers.
local inputBeginCallTable = {
[Enum.UserInputType.MouseButton1] = mouseClick;
[Enum.UserInputType.MouseWheel] = mouseWheel;
}
-- Function call lookup table for input changed event handlers.
local inputChangedCallTable = {
[Enum.UserInputType.MouseMovement] = mouseMoved;
[Enum.UserInputType.MouseWheel] = mouseWheel;
}
-- Input begin event handler dispatcher.
local function inputBegan(input, _gameProcessedEvent)
if inputBeginCallTable[input.UserInputType] ~= nil then
inputBeginCallTable[input.UserInputType](input)
end
end
-- Input changed event handler dispatcher.
local function inputChanged(input, _gameProcessedEvent)
if inputChangedCallTable[input.UserInputType] ~= nil then
inputChangedCallTable[input.UserInputType](input)
end
end
-- ******** Events
-- Enable mouse events if a mouse is detected.
if userInputService.MouseEnabled then
userInputService.InputBegan:Connect(inputBegan)
userInputService.InputChanged:Connect(inputChanged)
end