357 lines
12 KiB
Lua
357 lines
12 KiB
Lua
--[[
|
|
ClassicCamera - Classic Roblox camera control module
|
|
2018 Camera Update - AllYourBlox
|
|
|
|
Note: This module also handles camera control types Follow and Track, the
|
|
latter of which is currently not distinguished from Classic
|
|
--]]
|
|
|
|
-- Local private variables and constants
|
|
local ZERO_VECTOR2 = Vector2.new(0,0)
|
|
|
|
local tweenAcceleration = math.rad(220) -- Radians/Second^2
|
|
local tweenSpeed = math.rad(0) -- Radians/Second
|
|
local tweenMaxSpeed = math.rad(250) -- Radians/Second
|
|
local TIME_BEFORE_AUTO_ROTATE = 2 -- Seconds, used when auto-aligning camera with vehicles
|
|
|
|
local INITIAL_CAMERA_ANGLE = CFrame.fromOrientation(math.rad(-15), 0, 0)
|
|
local ZOOM_SENSITIVITY_CURVATURE = 0.5
|
|
local FIRST_PERSON_DISTANCE_MIN = 0.5
|
|
|
|
local FFlagUserCameraToggle do
|
|
local success, result = pcall(function()
|
|
return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
|
|
end)
|
|
FFlagUserCameraToggle = success and result
|
|
end
|
|
|
|
local FFlagUserCameraInputRefactor do
|
|
local success, result = pcall(function()
|
|
return UserSettings():IsUserFeatureEnabled("UserCameraInputRefactor3")
|
|
end)
|
|
FFlagUserCameraInputRefactor = success and result
|
|
end
|
|
|
|
--[[ Services ]]--
|
|
local PlayersService = game:GetService("Players")
|
|
local VRService = game:GetService("VRService")
|
|
|
|
local CameraInput = require(script.Parent:WaitForChild("CameraInput"))
|
|
local Util = require(script.Parent:WaitForChild("CameraUtils"))
|
|
|
|
--[[ The Module ]]--
|
|
local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
|
|
local ClassicCamera = setmetatable({}, BaseCamera)
|
|
ClassicCamera.__index = ClassicCamera
|
|
|
|
function ClassicCamera.new()
|
|
local self = setmetatable(BaseCamera.new(), ClassicCamera)
|
|
|
|
self.isFollowCamera = false
|
|
self.isCameraToggle = false
|
|
self.lastUpdate = tick()
|
|
self.cameraToggleSpring = Util.Spring.new(5, 0)
|
|
|
|
return self
|
|
end
|
|
|
|
function ClassicCamera:GetCameraToggleOffset(dt)
|
|
assert(FFlagUserCameraToggle)
|
|
|
|
if self.isCameraToggle then
|
|
local zoom = self.currentSubjectDistance
|
|
|
|
if CameraInput.getTogglePan() then
|
|
self.cameraToggleSpring.goal = math.clamp(Util.map(zoom, 0.5, self.FIRST_PERSON_DISTANCE_THRESHOLD, 0, 1), 0, 1)
|
|
else
|
|
self.cameraToggleSpring.goal = 0
|
|
end
|
|
|
|
local distanceOffset = math.clamp(Util.map(zoom, 0.5, 64, 0, 1), 0, 1) + 1
|
|
return Vector3.new(0, self.cameraToggleSpring:step(dt)*distanceOffset, 0)
|
|
end
|
|
|
|
return Vector3.new()
|
|
end
|
|
|
|
-- Movement mode standardized to Enum.ComputerCameraMovementMode values
|
|
function ClassicCamera:SetCameraMovementMode(cameraMovementMode)
|
|
BaseCamera.SetCameraMovementMode(self, cameraMovementMode)
|
|
|
|
self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow
|
|
self.isCameraToggle = cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle
|
|
end
|
|
|
|
function ClassicCamera:Update()
|
|
local now = tick()
|
|
local timeDelta = now - self.lastUpdate
|
|
|
|
local camera = workspace.CurrentCamera
|
|
local newCameraCFrame = camera.CFrame
|
|
local newCameraFocus = camera.Focus
|
|
|
|
local overrideCameraLookVector = nil
|
|
if self.resetCameraAngle then
|
|
local rootPart = self:GetHumanoidRootPart()
|
|
if rootPart then
|
|
overrideCameraLookVector = (rootPart.CFrame * INITIAL_CAMERA_ANGLE).lookVector
|
|
else
|
|
overrideCameraLookVector = INITIAL_CAMERA_ANGLE.lookVector
|
|
end
|
|
self.resetCameraAngle = false
|
|
end
|
|
|
|
local player = PlayersService.LocalPlayer
|
|
local humanoid = self:GetHumanoid()
|
|
local cameraSubject = camera.CameraSubject
|
|
local isInVehicle = cameraSubject and cameraSubject:IsA("VehicleSeat")
|
|
local isOnASkateboard = cameraSubject and cameraSubject:IsA("SkateboardPlatform")
|
|
local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing
|
|
|
|
if self.lastUpdate == nil or timeDelta > 1 then
|
|
self.lastCameraTransform = nil
|
|
end
|
|
|
|
local rotateInput
|
|
if FFlagUserCameraInputRefactor then
|
|
rotateInput = CameraInput.getRotation()
|
|
else
|
|
rotateInput = self.rotateInput
|
|
end
|
|
|
|
if FFlagUserCameraInputRefactor then
|
|
self:StepZoom()
|
|
end
|
|
|
|
if self.lastUpdate and not FFlagUserCameraInputRefactor then
|
|
local gamepadRotation = self:UpdateGamepad()
|
|
|
|
if self:ShouldUseVRRotation() then
|
|
self.rotateInput = self.rotateInput + self:GetVRRotationInput()
|
|
else
|
|
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
|
|
local delta = math.min(0.1, timeDelta)
|
|
|
|
if gamepadRotation ~= ZERO_VECTOR2 then
|
|
self.rotateInput = self.rotateInput + (gamepadRotation * delta)
|
|
end
|
|
|
|
local angle = 0
|
|
if not (isInVehicle or isOnASkateboard) then
|
|
angle = angle + (self.turningLeft and -120 or 0)
|
|
angle = angle + (self.turningRight and 120 or 0)
|
|
end
|
|
|
|
if angle ~= 0 then
|
|
self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
local cameraHeight = self:GetCameraHeight()
|
|
|
|
-- Reset tween speed if user is panning
|
|
if FFlagUserCameraInputRefactor then
|
|
if CameraInput.getRotation() ~= Vector2.new() then
|
|
tweenSpeed = 0
|
|
self.lastUserPanCamera = tick()
|
|
end
|
|
else
|
|
if self.userPanningTheCamera then
|
|
tweenSpeed = 0
|
|
self.lastUserPanCamera = tick()
|
|
end
|
|
end
|
|
|
|
local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE
|
|
local subjectPosition = self:GetSubjectPosition()
|
|
|
|
if subjectPosition and player and camera then
|
|
local zoom = self:GetCameraToSubjectDistance()
|
|
if zoom < 0.5 then
|
|
zoom = 0.5
|
|
end
|
|
|
|
if self:GetIsMouseLocked() and not self:IsInFirstPerson() then
|
|
-- We need to use the right vector of the camera after rotation, not before
|
|
local newLookCFrame
|
|
if FFlagUserCameraInputRefactor then
|
|
newLookCFrame = self:CalculateNewLookCFrameFromArg(overrideCameraLookVector, rotateInput)
|
|
else
|
|
newLookCFrame = self:CalculateNewLookCFrame(overrideCameraLookVector)
|
|
end
|
|
|
|
local offset = self:GetMouseLockOffset()
|
|
local cameraRelativeOffset = offset.X * newLookCFrame.rightVector + offset.Y * newLookCFrame.upVector + offset.Z * newLookCFrame.lookVector
|
|
|
|
--offset can be NAN, NAN, NAN if newLookVector has only y component
|
|
if Util.IsFiniteVector3(cameraRelativeOffset) then
|
|
subjectPosition = subjectPosition + cameraRelativeOffset
|
|
end
|
|
else
|
|
local userPanningTheCamera
|
|
if FFlagUserCameraInputRefactor then
|
|
userPanningTheCamera = CameraInput.getRotation() ~= Vector2.new()
|
|
else
|
|
userPanningTheCamera = self.userPanningTheCamera == true
|
|
end
|
|
|
|
if not userPanningTheCamera and self.lastCameraTransform then
|
|
|
|
local isInFirstPerson = self:IsInFirstPerson()
|
|
|
|
if (isInVehicle or isOnASkateboard or (self.isFollowCamera and isClimbing)) and self.lastUpdate and humanoid and humanoid.Torso then
|
|
if isInFirstPerson then
|
|
if self.lastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA("BasePart") then
|
|
local y = -Util.GetAngleBetweenXZVectors(self.lastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
|
|
if Util.IsFinite(y) then
|
|
if FFlagUserCameraInputRefactor then
|
|
rotateInput = rotateInput + Vector2.new(y, 0)
|
|
else
|
|
self.rotateInput = self.rotateInput + Vector2.new(y, 0)
|
|
end
|
|
end
|
|
tweenSpeed = 0
|
|
end
|
|
elseif not userRecentlyPannedCamera then
|
|
local forwardVector = humanoid.Torso.CFrame.lookVector
|
|
if isOnASkateboard and not FFlagUserCameraInputRefactor then
|
|
forwardVector = cameraSubject.CFrame.lookVector
|
|
end
|
|
|
|
tweenSpeed = math.clamp(tweenSpeed + tweenAcceleration * timeDelta, 0, tweenMaxSpeed)
|
|
|
|
local percent = math.clamp(tweenSpeed * timeDelta, 0, 1)
|
|
if self:IsInFirstPerson() and not (self.isFollowCamera and self.isClimbing) then
|
|
percent = 1
|
|
end
|
|
|
|
local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
|
|
if Util.IsFinite(y) and math.abs(y) > 0.0001 then
|
|
if FFlagUserCameraInputRefactor then
|
|
rotateInput = rotateInput + Vector2.new(y * percent, 0)
|
|
else
|
|
self.rotateInput = self.rotateInput + Vector2.new(y * percent, 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
elseif self.isFollowCamera and (not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled) then
|
|
-- Logic that was unique to the old FollowCamera module
|
|
local lastVec = -(self.lastCameraTransform.p - subjectPosition)
|
|
|
|
local y = Util.GetAngleBetweenXZVectors(lastVec, self:GetCameraLookVector())
|
|
|
|
-- This cutoff is to decide if the humanoid's angle of movement,
|
|
-- relative to the camera's look vector, is enough that
|
|
-- we want the camera to be following them. The point is to provide
|
|
-- a sizable dead zone to allow more precise forward movements.
|
|
local thetaCutoff = 0.4
|
|
|
|
-- Check for NaNs
|
|
if Util.IsFinite(y) and math.abs(y) > 0.0001 and math.abs(y) > thetaCutoff * timeDelta then
|
|
rotateInput = rotateInput + Vector2.new(y, 0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not self.isFollowCamera then
|
|
local VREnabled = VRService.VREnabled
|
|
|
|
if VREnabled then
|
|
newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
|
|
else
|
|
newCameraFocus = CFrame.new(subjectPosition)
|
|
end
|
|
|
|
local cameraFocusP = newCameraFocus.p
|
|
if VREnabled and not self:IsInFirstPerson() then
|
|
local vecToSubject = (subjectPosition - camera.CFrame.p)
|
|
local distToSubject = vecToSubject.magnitude
|
|
|
|
local flaggedRotateInput
|
|
if FFlagUserCameraInputRefactor then
|
|
flaggedRotateInput = rotateInput
|
|
else
|
|
flaggedRotateInput = self.rotateInput
|
|
end
|
|
|
|
-- Only move the camera if it exceeded a maximum distance to the subject in VR
|
|
if distToSubject > zoom or flaggedRotateInput.x ~= 0 then
|
|
local desiredDist = math.min(distToSubject, zoom)
|
|
if FFlagUserCameraInputRefactor then
|
|
vecToSubject = self:CalculateNewLookVectorFromArg(nil, rotateInput) * desiredDist
|
|
else
|
|
vecToSubject = self:CalculateNewLookVector() * desiredDist
|
|
end
|
|
local newPos = cameraFocusP - vecToSubject
|
|
local desiredLookDir = camera.CFrame.lookVector
|
|
if flaggedRotateInput.x ~= 0 then
|
|
desiredLookDir = vecToSubject
|
|
end
|
|
local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
|
|
if not FFlagUserCameraInputRefactor then
|
|
self.rotateInput = ZERO_VECTOR2
|
|
end
|
|
|
|
newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
|
|
end
|
|
else
|
|
local newLookVector
|
|
if FFlagUserCameraInputRefactor then
|
|
newLookVector = self:CalculateNewLookVectorFromArg(overrideCameraLookVector, rotateInput)
|
|
else
|
|
newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
|
|
self.rotateInput = ZERO_VECTOR2
|
|
end
|
|
newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
|
|
end
|
|
else -- is FollowCamera
|
|
local newLookVector
|
|
if FFlagUserCameraInputRefactor then
|
|
newLookVector = self:CalculateNewLookVectorFromArg(overrideCameraLookVector, rotateInput)
|
|
else
|
|
newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
|
|
self.rotateInput = ZERO_VECTOR2
|
|
end
|
|
|
|
if VRService.VREnabled then
|
|
newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
|
|
else
|
|
newCameraFocus = CFrame.new(subjectPosition)
|
|
end
|
|
newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, cameraHeight, 0)
|
|
end
|
|
|
|
if FFlagUserCameraToggle then
|
|
local toggleOffset = self:GetCameraToggleOffset(timeDelta)
|
|
newCameraFocus = newCameraFocus + toggleOffset
|
|
newCameraCFrame = newCameraCFrame + toggleOffset
|
|
end
|
|
|
|
self.lastCameraTransform = newCameraCFrame
|
|
self.lastCameraFocus = newCameraFocus
|
|
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA("BasePart") then
|
|
self.lastSubjectCFrame = cameraSubject.CFrame
|
|
else
|
|
self.lastSubjectCFrame = nil
|
|
end
|
|
end
|
|
|
|
self.lastUpdate = now
|
|
return newCameraCFrame, newCameraFocus
|
|
end
|
|
|
|
function ClassicCamera:EnterFirstPerson()
|
|
self.inFirstPerson = true
|
|
self:UpdateMouseBehavior()
|
|
end
|
|
|
|
function ClassicCamera:LeaveFirstPerson()
|
|
self.inFirstPerson = false
|
|
self:UpdateMouseBehavior()
|
|
end
|
|
|
|
return ClassicCamera
|