local PlayersService = game:GetService('Players') local UserInputService = game:GetService('UserInputService') local StarterGui = game:GetService('StarterGui') local GuiService = game:GetService('GuiService') local ContextActionService = game:GetService('ContextActionService') local VRService = game:GetService("VRService") local LocalPlayer = PlayersService.LocalPlayer local PlayerGui = nil if LocalPlayer then PlayerGui = PlayersService.LocalPlayer:WaitForChild("PlayerGui") end local PortraitMode = false local CameraScript = script.Parent local ShiftLockController = require(CameraScript:WaitForChild('ShiftLockController')) local Settings = UserSettings() local GameSettings = Settings.GameSettings local function clamp(low, high, num) return (num > high and high or num < low and low or num) end local math_atan2 = math.atan2 local function findAngleBetweenXZVectors(vec2, vec1) return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) end local function IsFinite(num) return num == num and num ~= 1/0 and num ~= -1/0 end local THUMBSTICK_DEADZONE = 0.2 local LANDSCAPE_DEFAULT_ZOOM = 12.5 local PORTRAIT_DEFAULT_ZOOM = 25 local humanoidCache = {} local function findPlayerHumanoid(player) local character = player and player.Character if character then local resultHumanoid = humanoidCache[player] if resultHumanoid and resultHumanoid.Parent == character then return resultHumanoid else humanoidCache[player] = nil -- Bust Old Cache local humanoid = character:FindFirstChildOfClass("Humanoid") if humanoid then humanoidCache[player] = humanoid end return humanoid end end end local MIN_Y = math.rad(-80) local MAX_Y = math.rad(80) local DEFAULT_CAMERA_ANGLE = 25 local VR_ANGLE = math.rad(15) local VR_LOW_INTENSITY_ROTATION = Vector2.new(math.rad(15), 0) local VR_HIGH_INTENSITY_ROTATION = Vector2.new(math.rad(45), 0) local VR_LOW_INTENSITY_REPEAT = 0.1 local VR_HIGH_INTENSITY_REPEAT = 0.4 local ZERO_VECTOR2 = Vector2.new(0, 0) local ZERO_VECTOR3 = Vector3.new(0, 0, 0) local TOUCH_SENSITIVTY = Vector2.new(math.pi*2.25, math.pi*2) local MOUSE_SENSITIVITY = Vector2.new(math.pi*4, math.pi*1.9) local MAX_TIME_FOR_DOUBLE_TAP = 1.5 local MAX_TAP_POS_DELTA = 15 local MAX_TAP_TIME_DELTA = 0.75 local SEAT_OFFSET = Vector3.new(0,5,0) local VR_SEAT_OFFSET = Vector3.new(0, 4, 0) local HEAD_OFFSET = Vector3.new(0, 1.5, 0) local R15_HEAD_OFFSET = Vector3.new(0, 2.0, 0) local PORTRAIT_MODE_CAMERA_OFFSET = 2 -- Reset the camera look vector when the camera is enabled for the first time local SetCameraOnSpawn = true local hasGameLoaded = false local GestureArea = nil local GestureAreaManagedByControlScript = false local function layoutGestureArea(portraitMode) if GestureArea and not GestureAreaManagedByControlScript then if portraitMode then GestureArea.Size = UDim2.new(1, 0, .6, 0) GestureArea.Position = UDim2.new(0, 0, 0, 0) else GestureArea.Size = UDim2.new(1, 0, .5, -18) GestureArea.Position = UDim2.new(0, 0, 0, 0) end end end -- Setup gesture area that camera uses while DynamicThumbstick is enabled local function OnCharacterAdded(character) if UserInputService.TouchEnabled then for _, child in ipairs(LocalPlayer.Character:GetChildren()) do if child:IsA("Tool") then IsAToolEquipped = true end end character.ChildAdded:Connect(function(child) if child:IsA("Tool") then IsAToolEquipped = true end end) character.ChildRemoved:Connect(function(child) if child:IsA("Tool") then IsAToolEquipped = false end end) if PlayerGui then local TouchGui = PlayerGui:FindFirstChild("TouchGui") if TouchGui and TouchGui:WaitForChild("GestureArea", 0.5) then GestureArea = TouchGui.GestureArea GestureAreaManagedByControlScript = true else GestureAreaManagedByControlScript = false local ScreenGui = Instance.new("ScreenGui") ScreenGui.Name = "GestureArea" ScreenGui.Parent = PlayerGui GestureArea = Instance.new("Frame") GestureArea.BackgroundTransparency = 1.0 GestureArea.Visible = true GestureArea.BackgroundColor3 = Color3.fromRGB(0, 0, 0) layoutGestureArea(PortraitMode) GestureArea.Parent = ScreenGui end end end end if LocalPlayer then if LocalPlayer.Character ~= nil then OnCharacterAdded(LocalPlayer.Character) end LocalPlayer.CharacterAdded:connect(function(character) OnCharacterAdded(character) end) end local function positionIntersectsGuiObject(position, guiObject) if position.X < guiObject.AbsolutePosition.X + guiObject.AbsoluteSize.X and position.X > guiObject.AbsolutePosition.X and position.Y < guiObject.AbsolutePosition.Y + guiObject.AbsoluteSize.Y and position.Y > guiObject.AbsolutePosition.Y then return true end return false end local function GetRenderCFrame(part) return part:GetRenderCFrame() end local function CreateCamera() local this = {} local R15HeadHeight = R15_HEAD_OFFSET function this:GetActivateValue() return 0.7 end function this:IsPortraitMode() return PortraitMode end function this:GetRotateAmountValue(vrRotationIntensity) vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") if vrRotationIntensity then if vrRotationIntensity == "Low" then return VR_LOW_INTENSITY_ROTATION elseif vrRotationIntensity == "High" then return VR_HIGH_INTENSITY_ROTATION end end return ZERO_VECTOR2 end function this:GetRepeatDelayValue(vrRotationIntensity) vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") if vrRotationIntensity then if vrRotationIntensity == "Low" then return VR_LOW_INTENSITY_REPEAT elseif vrRotationIntensity == "High" then return VR_HIGH_INTENSITY_REPEAT end end return 0 end this.ShiftLock = false this.Enabled = false local isFirstPerson = false local isRightMouseDown = false local isMiddleMouseDown = false this.RotateInput = ZERO_VECTOR2 this.DefaultZoom = LANDSCAPE_DEFAULT_ZOOM this.activeGamepad = nil local tweens = {} this.lastSubject = nil this.lastSubjectPosition = Vector3.new(0, 5, 0) local lastVRRotation = 0 local vrRotateKeyCooldown = {} local isDynamicThumbstickEnabled = false local dynamicThumbstickFrame = nil local function getDynamicThumbstickFrame() if dynamicThumbstickFrame and dynamicThumbstickFrame:IsDescendantOf(game) then return dynamicThumbstickFrame else local touchGui = PlayerGui:FindFirstChild("TouchGui") if not touchGui then return nil end local touchControlFrame = touchGui:FindFirstChild("TouchControlFrame") if not touchControlFrame then return nil end dynamicThumbstickFrame = touchControlFrame:FindFirstChild("DynamicThumbstickFrame") return dynamicThumbstickFrame end end -- Check for changes in ViewportSize to decide if PortraitMode local CameraChangedConn = nil local workspaceCameraChangedConn = nil local function onWorkspaceCameraChanged() if UserInputService.TouchEnabled then if CameraChangedConn then CameraChangedConn:Disconnect() CameraChangedConn = nil end local newCamera = workspace.CurrentCamera if newCamera then local size = newCamera.ViewportSize PortraitMode = size.X < size.Y layoutGestureArea(PortraitMode) DefaultZoom = PortraitMode and PORTRAIT_DEFAULT_ZOOM or LANDSCAPE_DEFAULT_ZOOM CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function() size = newCamera.ViewportSize PortraitMode = size.X < size.Y layoutGestureArea(PortraitMode) DefaultZoom = PortraitMode and PORTRAIT_DEFAULT_ZOOM or LANDSCAPE_DEFAULT_ZOOM end) end end end workspaceCameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onWorkspaceCameraChanged) if workspace.CurrentCamera then onWorkspaceCameraChanged() end function this:GetShiftLock() return ShiftLockController:IsShiftLocked() end function this:GetHumanoid() local player = PlayersService.LocalPlayer return findPlayerHumanoid(player) end function this:GetHumanoidRootPart() local humanoid = this:GetHumanoid() return humanoid and humanoid.Torso end function this:GetRenderCFrame(part) GetRenderCFrame(part) end local STATE_DEAD = Enum.HumanoidStateType.Dead -- HumanoidRootPart when alive, Head part when dead local function getHumanoidPartToFollow(humanoid, humanoidStateType) if humanoidStateType == STATE_DEAD then local character = humanoid.Parent if character then return character:FindFirstChild("Head") or humanoid.Torso else return humanoid.Torso end else return humanoid.Torso end end local HUMANOID_STATE_DEAD = Enum.HumanoidStateType.Dead function this:GetSubjectPosition() local result = nil local camera = workspace.CurrentCamera local cameraSubject = camera and camera.CameraSubject if cameraSubject then if cameraSubject:IsA('Humanoid') then local humanoidStateType = cameraSubject:GetState() if VRService.VREnabled and humanoidStateType == STATE_DEAD and cameraSubject == this.lastSubject then result = this.lastSubjectPosition else local humanoidRootPart = getHumanoidPartToFollow(cameraSubject, humanoidStateType) if humanoidRootPart and humanoidRootPart:IsA('BasePart') then local subjectCFrame = GetRenderCFrame(humanoidRootPart) local heightOffset = ZERO_VECTOR3 if humanoidStateType ~= STATE_DEAD then heightOffset = cameraSubject.RigType == Enum.HumanoidRigType.R15 and R15HeadHeight or HEAD_OFFSET end if PortraitMode then heightOffset = heightOffset + Vector3.new(0, PORTRAIT_MODE_CAMERA_OFFSET, 0) end result = subjectCFrame.p + subjectCFrame:vectorToWorldSpace(heightOffset + cameraSubject.CameraOffset) end end elseif cameraSubject:IsA('VehicleSeat') then local subjectCFrame = GetRenderCFrame(cameraSubject) local offset = SEAT_OFFSET if VRService.VREnabled then offset = VR_SEAT_OFFSET end result = subjectCFrame.p + subjectCFrame:vectorToWorldSpace(offset) elseif cameraSubject:IsA('SkateboardPlatform') then local subjectCFrame = GetRenderCFrame(cameraSubject) result = subjectCFrame.p + SEAT_OFFSET elseif cameraSubject:IsA('BasePart') then local subjectCFrame = GetRenderCFrame(cameraSubject) result = subjectCFrame.p elseif cameraSubject:IsA('Model') then result = cameraSubject:GetModelCFrame().p end end this.lastSubject = cameraSubject this.lastSubjectPosition = result return result end function this:ResetCameraLook() end function this:GetCameraLook() return workspace.CurrentCamera and workspace.CurrentCamera.CoordinateFrame.lookVector or Vector3.new(0,0,1) end function this:GetCameraZoom() if this.currentZoom == nil then local player = PlayersService.LocalPlayer this.currentZoom = player and clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, this.DefaultZoom) or this.DefaultZoom end return this.currentZoom end function this:GetCameraActualZoom() local camera = workspace.CurrentCamera if camera then return (camera.CoordinateFrame.p - camera.Focus.p).magnitude end end function this:GetCameraHeight() if VRService.VREnabled and not this:IsInFirstPerson() then local zoom = this:GetCameraZoom() return math.sin(VR_ANGLE) * zoom end return 0 end function this:ViewSizeX() local result = 1024 local camera = workspace.CurrentCamera if camera then result = camera.ViewportSize.X end return result end function this:ViewSizeY() local result = 768 local camera = workspace.CurrentCamera if camera then result = camera.ViewportSize.Y end return result end local math_asin = math.asin local math_atan2 = math.atan2 local math_floor = math.floor local math_max = math.max local math_pi = math.pi local Vector2_new = Vector2.new local Vector3_new = Vector3.new local CFrame_Angles = CFrame.Angles local CFrame_new = CFrame.new function this:ScreenTranslationToAngle(translationVector) local screenX = this:ViewSizeX() local screenY = this:ViewSizeY() local xTheta = (translationVector.x / screenX) local yTheta = (translationVector.y / screenY) return Vector2_new(xTheta, yTheta) end function this:MouseTranslationToAngle(translationVector) local xTheta = (translationVector.x / 1920) local yTheta = (translationVector.y / 1200) return Vector2_new(xTheta, yTheta) end function this:RotateVector(startVector, xyRotateVector) local startCFrame = CFrame_new(ZERO_VECTOR3, startVector) local resultLookVector = (CFrame_Angles(0, -xyRotateVector.x, 0) * startCFrame * CFrame_Angles(-xyRotateVector.y,0,0)).lookVector return resultLookVector, Vector2_new(xyRotateVector.x, xyRotateVector.y) end function this:RotateCamera(startLook, xyRotateVector) if VRService.VREnabled then local yawRotatedVector, xyRotateVector = self:RotateVector(startLook, Vector2.new(xyRotateVector.x, 0)) return Vector3_new(yawRotatedVector.x, 0, yawRotatedVector.z).unit, xyRotateVector else local startVertical = math_asin(startLook.y) local yTheta = clamp(-MAX_Y + startVertical, -MIN_Y + startVertical, xyRotateVector.y) return self:RotateVector(startLook, Vector2_new(xyRotateVector.x, yTheta)) end end function this:IsInFirstPerson() return isFirstPerson end -- there are several cases to consider based on the state of input and camera rotation mode function this:UpdateMouseBehavior() -- first time transition to first person mode or shiftlock local camera = workspace.CurrentCamera if camera.CameraType == Enum.CameraType.Scriptable then return end if isFirstPerson or self:GetShiftLock() then pcall(function() GameSettings.RotationType = Enum.RotationType.CameraRelative end) if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter end else pcall(function() GameSettings.RotationType = Enum.RotationType.MovementRelative end) if isRightMouseDown or isMiddleMouseDown then UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition else UserInputService.MouseBehavior = Enum.MouseBehavior.Default end end end function this:ZoomCamera(desiredZoom) local player = PlayersService.LocalPlayer if player then if player.CameraMode == Enum.CameraMode.LockFirstPerson then this.currentZoom = 0 else this.currentZoom = clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, desiredZoom) end end isFirstPerson = self:GetCameraZoom() < 2 ShiftLockController:SetIsInFirstPerson(isFirstPerson) -- set mouse behavior self:UpdateMouseBehavior() return self:GetCameraZoom() end function this:rk4Integrator(position, velocity, t) local direction = velocity < 0 and -1 or 1 local function acceleration(p, v) local accel = direction * math_max(1, (p / 3.3) + 0.5) return accel end local p1 = position local v1 = velocity local a1 = acceleration(p1, v1) local p2 = p1 + v1 * (t / 2) local v2 = v1 + a1 * (t / 2) local a2 = acceleration(p2, v2) local p3 = p1 + v2 * (t / 2) local v3 = v1 + a2 * (t / 2) local a3 = acceleration(p3, v3) local p4 = p1 + v3 * t local v4 = v1 + a3 * t local a4 = acceleration(p4, v4) local positionResult = position + (v1 + 2 * v2 + 2 * v3 + v4) * (t / 6) local velocityResult = velocity + (a1 + 2 * a2 + 2 * a3 + a4) * (t / 6) return positionResult, velocityResult end function this:ZoomCameraBy(zoomScale) local zoom = this:GetCameraActualZoom() if zoom then -- Can break into more steps to get more accurate integration zoom = self:rk4Integrator(zoom, zoomScale, 1) self:ZoomCamera(zoom) end return self:GetCameraZoom() end function this:ZoomCameraFixedBy(zoomIncrement) return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement) end function this:Update() end ----- VR STUFF ------ function this:ApplyVRTransform() if not VRService.VREnabled then return end --we only want this to happen in first person VR local player = PlayersService.LocalPlayer if not (player and player.Character and player.Character:FindFirstChild("HumanoidRootPart") and player.Character.HumanoidRootPart:FindFirstChild("RootJoint")) then return end local camera = workspace.CurrentCamera local cameraSubject = camera.CameraSubject local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') if this:IsInFirstPerson() and not isInVehicle then local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head) local vrRotation = vrFrame - vrFrame.p local rootJoint = player.Character.HumanoidRootPart.RootJoint rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) else local rootJoint = player.Character.HumanoidRootPart.RootJoint rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) end end local vrRotationIntensityExists = true local lastVrRotationCheck = 0 function this:ShouldUseVRRotation() if not VRService.VREnabled then return false end if not vrRotationIntensityExists and tick() - lastVrRotationCheck < 1 then return false end local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) vrRotationIntensityExists = success and vrRotationIntensity ~= nil lastVrRotationCheck = tick() return success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth" end function this:GetVRRotationInput() local vrRotateSum = ZERO_VECTOR2 local vrRotationIntensity = StarterGui:GetCore("VRRotationIntensity") local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2 local delayExpired = (tick() - lastVRRotation) >= self:GetRepeatDelayValue(vrRotationIntensity) if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then if (delayExpired or not vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then local sign = 1 if vrGamepadRotation.x < 0 then sign = -1 end vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true end elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil end if self.TurningLeft then if delayExpired or not vrRotateKeyCooldown[Enum.KeyCode.Left] then vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity) vrRotateKeyCooldown[Enum.KeyCode.Left] = true end else vrRotateKeyCooldown[Enum.KeyCode.Left] = nil end if self.TurningRight then if (delayExpired or not vrRotateKeyCooldown[Enum.KeyCode.Right]) then vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) vrRotateKeyCooldown[Enum.KeyCode.Right] = true end else vrRotateKeyCooldown[Enum.KeyCode.Right] = nil end if vrRotateSum ~= ZERO_VECTOR2 then lastVRRotation = tick() end return vrRotateSum end local cameraTranslationConstraints = Vector3.new(1, 1, 1) local humanoidJumpOrigin = nil local trackingHumanoid = nil local cameraFrozen = false local subjectStateChangedConn = nil local cameraSubjectChangedConn = nil local workspaceChangedConn = nil local humanoidChildAddedConn = nil local humanoidChildRemovedConn = nil local function cancelCameraFreeze(keepConstraints) if not keepConstraints then cameraTranslationConstraints = Vector3.new(cameraTranslationConstraints.x, 1, cameraTranslationConstraints.z) end if cameraFrozen then trackingHumanoid = nil cameraFrozen = false end end local function startCameraFreeze(subjectPosition, humanoidToTrack) if not cameraFrozen then humanoidJumpOrigin = subjectPosition trackingHumanoid = humanoidToTrack cameraTranslationConstraints = Vector3.new(cameraTranslationConstraints.x, 0, cameraTranslationConstraints.z) cameraFrozen = true end end local function rescaleCameraOffset(newScaleFactor) R15HeadHeight = R15_HEAD_OFFSET*newScaleFactor end local function onHumanoidSubjectChildAdded(child) if child.Name == "BodyHeightScale" and child:IsA("NumberValue") then if heightScaleChangedConn then heightScaleChangedConn:disconnect() end heightScaleChangedConn = child.Changed:connect(rescaleCameraOffset) rescaleCameraOffset(child.Value) end end local function onHumanoidSubjectChildRemoved(child) if child.Name == "BodyHeightScale" then rescaleCameraOffset(1) if heightScaleChangedConn then heightScaleChangedConn:disconnect() heightScaleChangedConn = nil end end end local function onNewCameraSubject() if subjectStateChangedConn then subjectStateChangedConn:disconnect() subjectStateChangedConn = nil end if humanoidChildAddedConn then humanoidChildAddedConn:disconnect() humanoidChildAddedConn = nil end if humanoidChildRemovedConn then humanoidChildRemovedConn:disconnect() humanoidChildRemovedConn = nil end if heightScaleChangedConn then heightScaleChangedConn:disconnect() heightScaleChangedConn = nil end local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject if trackingHumanoid ~= humanoid then cancelCameraFreeze() end if humanoid and humanoid:IsA('Humanoid') then humanoidChildAddedConn = humanoid.ChildAdded:connect(onHumanoidSubjectChildAdded) humanoidChildRemovedConn = humanoid.ChildRemoved:connect(onHumanoidSubjectChildRemoved) for _, child in pairs(humanoid:GetChildren()) do onHumanoidSubjectChildAdded(child) end subjectStateChangedConn = humanoid.StateChanged:connect(function(oldState, newState) if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not this:IsInFirstPerson() then startCameraFreeze(this:GetSubjectPosition(), humanoid) elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then cancelCameraFreeze(true) end end) end end local function onCurrentCameraChanged() if cameraSubjectChangedConn then cameraSubjectChangedConn:disconnect() cameraSubjectChangedConn = nil end local camera = workspace.CurrentCamera if camera then cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):connect(onNewCameraSubject) onNewCameraSubject() end end function this:GetVRFocus(subjectPosition, timeDelta) local newFocus = nil local camera = workspace.CurrentCamera local lastFocus = self.LastCameraFocus or subjectPosition if not cameraFrozen then cameraTranslationConstraints = Vector3.new(cameraTranslationConstraints.x, math.min(1, cameraTranslationConstraints.y + 0.42 * timeDelta), cameraTranslationConstraints.z) end if cameraFrozen and humanoidJumpOrigin and humanoidJumpOrigin.y > lastFocus.y then newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z)) else newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, cameraTranslationConstraints.y)) end if cameraFrozen then -- No longer in 3rd person if self:IsInFirstPerson() then -- not VRService.VREnabled cancelCameraFreeze() end -- This case you jumped off a cliff and want to keep your character in view -- 0.5 is to fix floating point error when not jumping off cliffs if humanoidJumpOrigin and subjectPosition.y < (humanoidJumpOrigin.y - 0.5) then cancelCameraFreeze() end end return newFocus end ------------------------ ---- Input Events ---- local startPos = nil local lastPos = nil local panBeginLook = nil local lastTapTime = nil local fingerTouches = {} local NumUnsunkTouches = 0 local inputStartPositions = {} local inputStartTimes = {} local StartingDiff = nil local pinchBeginZoom = nil this.ZoomEnabled = true this.PanEnabled = true this.KeyPanEnabled = true local function OnTouchBegan(input, processed) --If isDynamicThumbstickEnabled, then only process TouchBegan event if it starts in GestureArea local dtFrame = getDynamicThumbstickFrame() local isDynamicThumbstickUsingThisInput = false if isDynamicThumbstickEnabled then local ControlScript = CameraScript.Parent:FindFirstChild("ControlScript") if ControlScript then local MasterControl = ControlScript:FindFirstChild("MasterControl") if MasterControl then local DynamicThumbstickModule = MasterControl:FindFirstChild("DynamicThumbstick") if DynamicThumbstickModule then DynamicThumbstickModule = require(DynamicThumbstickModule) local dynamicInputObject = DynamicThumbstickModule:GetInputObject() isDynamicThumbstickUsingThisInput = (dynamicInputObject == input) end end end end if not isDynamicThumbstickUsingThisInput then fingerTouches[input] = processed if not processed then inputStartPositions[input] = input.Position inputStartTimes[input] = tick() NumUnsunkTouches = NumUnsunkTouches + 1 end end end local function OnTouchChanged(input, processed) if fingerTouches[input] == nil then if isDynamicThumbstickEnabled then return end fingerTouches[input] = processed if not processed then NumUnsunkTouches = NumUnsunkTouches + 1 end end if NumUnsunkTouches == 1 then if fingerTouches[input] == false then panBeginLook = panBeginLook or this:GetCameraLook() startPos = startPos or input.Position lastPos = lastPos or startPos this.UserPanningTheCamera = true local delta = input.Position - lastPos delta = Vector2.new(delta.X, delta.Y * GameSettings:GetCameraYInvertValue()) if this.PanEnabled then local desiredXYVector = this:ScreenTranslationToAngle(delta) * TOUCH_SENSITIVTY this.RotateInput = this.RotateInput + desiredXYVector end lastPos = input.Position end else panBeginLook = nil startPos = nil lastPos = nil this.UserPanningTheCamera = false end if NumUnsunkTouches == 2 then local unsunkTouches = {} for touch, wasSunk in pairs(fingerTouches) do if not wasSunk then table.insert(unsunkTouches, touch) end end if #unsunkTouches == 2 then local difference = (unsunkTouches[1].Position - unsunkTouches[2].Position).magnitude if StartingDiff and pinchBeginZoom then local scale = difference / math_max(0.01, StartingDiff) local clampedScale = clamp(0.1, 10, scale) if this.ZoomEnabled then this:ZoomCamera(pinchBeginZoom / clampedScale) end else StartingDiff = difference pinchBeginZoom = this:GetCameraActualZoom() end end else StartingDiff = nil pinchBeginZoom = nil end end local function calcLookBehindRotateInput(torso) if torso then local newDesiredLook = (torso.CFrame.lookVector - Vector3.new(0, math.sin(math.rad(DEFAULT_CAMERA_ANGLE), 0))).unit local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, this:GetCameraLook()) local vertShift = math.asin(this:GetCameraLook().y) - math.asin(newDesiredLook.y) if not IsFinite(horizontalShift) then horizontalShift = 0 end if not IsFinite(vertShift) then vertShift = 0 end return Vector2.new(horizontalShift, vertShift) end return nil end local function IsTouchTap(input) -- We can't make the assumption that the input exists in the inputStartPositions because we may have switched from a different camera type. if inputStartPositions[input] then local posDelta = (inputStartPositions[input] - input.Position).magnitude if posDelta < MAX_TAP_POS_DELTA then local timeDelta = inputStartTimes[input] - tick() if timeDelta < MAX_TAP_TIME_DELTA then return true end end end return false end local function OnTouchEnded(input, processed) if fingerTouches[input] == false then if NumUnsunkTouches == 1 then panBeginLook = nil startPos = nil lastPos = nil this.UserPanningTheCamera = false elseif NumUnsunkTouches == 2 then StartingDiff = nil pinchBeginZoom = nil end end if fingerTouches[input] ~= nil and fingerTouches[input] == false then NumUnsunkTouches = NumUnsunkTouches - 1 end fingerTouches[input] = nil inputStartPositions[input] = nil inputStartTimes[input] = nil end local function OnMousePanButtonPressed(input, processed) if processed then return end this:UpdateMouseBehavior() panBeginLook = panBeginLook or this:GetCameraLook() startPos = startPos or input.Position lastPos = lastPos or startPos this.UserPanningTheCamera = true end local function OnMousePanButtonReleased(input, processed) this:UpdateMouseBehavior() if not (isRightMouseDown or isMiddleMouseDown) then panBeginLook = nil startPos = nil lastPos = nil this.UserPanningTheCamera = false end end local function OnMouse2Down(input, processed) if processed then return end isRightMouseDown = true OnMousePanButtonPressed(input, processed) end local function OnMouse2Up(input, processed) isRightMouseDown = false OnMousePanButtonReleased(input, processed) end local function OnMouse3Down(input, processed) if processed then return end isMiddleMouseDown = true OnMousePanButtonPressed(input, processed) end local function OnMouse3Up(input, processed) isMiddleMouseDown = false OnMousePanButtonReleased(input, processed) end local function OnMouseMoved(input, processed) if not hasGameLoaded and VRService.VREnabled then return end local inputDelta = input.Delta inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * GameSettings:GetCameraYInvertValue()) if startPos and lastPos and panBeginLook then local currPos = lastPos + input.Delta local totalTrans = currPos - startPos if this.PanEnabled then local desiredXYVector = this:MouseTranslationToAngle(inputDelta) * MOUSE_SENSITIVITY this.RotateInput = this.RotateInput + desiredXYVector end lastPos = currPos elseif this:IsInFirstPerson() or this:GetShiftLock() then if this.PanEnabled then local desiredXYVector = this:MouseTranslationToAngle(inputDelta) * MOUSE_SENSITIVITY this.RotateInput = this.RotateInput + desiredXYVector end end end local function OnMouseWheel(input, processed) if not hasGameLoaded and VRService.VREnabled then return end if not processed then if this.ZoomEnabled then this:ZoomCameraBy(clamp(-1, 1, -input.Position.Z) * 1.4) end end end local function round(num) return math_floor(num + 0.5) end local eight2Pi = math_pi / 4 local function rotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount) if camLook ~= ZERO_VECTOR3 then camLook = camLook.unit local currAngle = math_atan2(camLook.z, camLook.x) local newAngle = round((math_atan2(camLook.z, camLook.x) + rotateAngle) / roundAmount) * roundAmount return newAngle - currAngle end return 0 end local function OnKeyDown(input, processed) if not hasGameLoaded and VRService.VREnabled then return end if processed then return end if this.ZoomEnabled then if input.KeyCode == Enum.KeyCode.I then this:ZoomCameraBy(-5) elseif input.KeyCode == Enum.KeyCode.O then this:ZoomCameraBy(5) end end if panBeginLook == nil and this.KeyPanEnabled then if input.KeyCode == Enum.KeyCode.Left then this.TurningLeft = true elseif input.KeyCode == Enum.KeyCode.Right then this.TurningRight = true elseif input.KeyCode == Enum.KeyCode.Comma then local angle = rotateVectorByAngleAndRound(this:GetCameraLook() * Vector3.new(1,0,1), -eight2Pi * (3/4), eight2Pi) if angle ~= 0 then this.RotateInput = this.RotateInput + Vector2.new(angle, 0) this.LastUserPanCamera = tick() this.LastCameraTransform = nil end elseif input.KeyCode == Enum.KeyCode.Period then local angle = rotateVectorByAngleAndRound(this:GetCameraLook() * Vector3.new(1,0,1), eight2Pi * (3/4), eight2Pi) if angle ~= 0 then this.RotateInput = this.RotateInput + Vector2.new(angle, 0) this.LastUserPanCamera = tick() this.LastCameraTransform = nil end elseif input.KeyCode == Enum.KeyCode.PageUp then --elseif input.KeyCode == Enum.KeyCode.Home then this.RotateInput = this.RotateInput + Vector2.new(0,math.rad(15)) this.LastCameraTransform = nil elseif input.KeyCode == Enum.KeyCode.PageDown then --elseif input.KeyCode == Enum.KeyCode.End then this.RotateInput = this.RotateInput + Vector2.new(0,math.rad(-15)) this.LastCameraTransform = nil end end end local function OnKeyUp(input, processed) if input.KeyCode == Enum.KeyCode.Left then this.TurningLeft = false elseif input.KeyCode == Enum.KeyCode.Right then this.TurningRight = false end end local function onWindowFocusReleased() this:ResetInputStates() end local lastThumbstickRotate = nil local numOfSeconds = 0.7 local currentSpeed = 0 local maxSpeed = 6 local vrMaxSpeed = 4 local lastThumbstickPos = Vector2.new(0,0) local ySensitivity = 0.65 local lastVelocity = nil -- K is a tunable parameter that changes the shape of the S-curve -- the larger K is the more straight/linear the curve gets local k = 0.35 local lowerK = 0.8 local function SCurveTranform(t) t = clamp(-1,1,t) if t >= 0 then return (k*t) / (k - t + 1) end return -((lowerK*-t) / (lowerK + t + 1)) end -- DEADZONE local DEADZONE = 0.1 local function toSCurveSpace(t) return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE end local function fromSCurveSpace(t) return t/2 + 0.5 end local function gamepadLinearToCurve(thumbstickPosition) local function onAxis(axisValue) local sign = 1 if axisValue < 0 then sign = -1 end local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue)))) point = point * sign return clamp(-1, 1, point) end return Vector2_new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y)) end function this:UpdateGamepad() local gamepadPan = this.GamepadPanningCamera if gamepadPan and (hasGameLoaded or not VRService.VREnabled) then gamepadPan = gamepadLinearToCurve(gamepadPan) local currentTime = tick() if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then this.userPanningTheCamera = true elseif gamepadPan == ZERO_VECTOR2 then lastThumbstickRotate = nil if lastThumbstickPos == ZERO_VECTOR2 then currentSpeed = 0 end end local finalConstant = 0 if lastThumbstickRotate then if VRService.VREnabled then currentSpeed = vrMaxSpeed else local elapsedTime = (currentTime - lastThumbstickRotate) * 10 currentSpeed = currentSpeed + (maxSpeed * ((elapsedTime*elapsedTime)/numOfSeconds)) if currentSpeed > maxSpeed then currentSpeed = maxSpeed end if lastVelocity then local velocity = (gamepadPan - lastThumbstickPos)/(currentTime - lastThumbstickRotate) local velocityDeltaMag = (velocity - lastVelocity).magnitude if velocityDeltaMag > 12 then currentSpeed = currentSpeed * (20/velocityDeltaMag) if currentSpeed > maxSpeed then currentSpeed = maxSpeed end end end end local success, gamepadCameraSensitivity = pcall(function() return GameSettings.GamepadCameraSensitivity end) finalConstant = success and (gamepadCameraSensitivity * currentSpeed) or currentSpeed lastVelocity = (gamepadPan - lastThumbstickPos)/(currentTime - lastThumbstickRotate) end lastThumbstickPos = gamepadPan lastThumbstickRotate = currentTime return Vector2_new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * ySensitivity * GameSettings:GetCameraYInvertValue()) end return ZERO_VECTOR2 end local InputBeganConn, InputChangedConn, InputEndedConn, WindowUnfocusConn, MenuOpenedConn, ShiftLockToggleConn, GamepadConnectedConn, GamepadDisconnectedConn, TouchActivateConn = nil, nil, nil, nil, nil, nil, nil, nil, nil function this:DisconnectInputEvents() if InputBeganConn then InputBeganConn:disconnect() InputBeganConn = nil end if InputChangedConn then InputChangedConn:disconnect() InputChangedConn = nil end if InputEndedConn then InputEndedConn:disconnect() InputEndedConn = nil end if WindowUnfocusConn then WindowUnfocusConn:disconnect() WindowUnfocusConn = nil end if MenuOpenedConn then MenuOpenedConn:disconnect() MenuOpenedConn = nil end if ShiftLockToggleConn then ShiftLockToggleConn:disconnect() ShiftLockToggleConn = nil end if GamepadConnectedConn then GamepadConnectedConn:disconnect() GamepadConnectedConn = nil end if GamepadDisconnectedConn then GamepadDisconnectedConn:disconnect() GamepadDisconnectedConn = nil end if subjectStateChangedConn then subjectStateChangedConn:disconnect() subjectStateChangedConn = nil end if workspaceChangedConn then workspaceChangedConn:disconnect() workspaceChangedConn = nil end if TouchActivateConn then TouchActivateConn:disconnect() TouchActivateConn = nil end this.TurningLeft = false this.TurningRight = false this.LastCameraTransform = nil self.LastSubjectCFrame = nil this.UserPanningTheCamera = false this.RotateInput = Vector2.new() this.GamepadPanningCamera = Vector2.new(0,0) -- Reset input states startPos = nil lastPos = nil panBeginLook = nil isRightMouseDown = false isMiddleMouseDown = false fingerTouches = {} NumUnsunkTouches = 0 StartingDiff = nil pinchBeginZoom = nil -- Unlock mouse for example if right mouse button was being held down if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then UserInputService.MouseBehavior = Enum.MouseBehavior.Default end end function this:ResetInputStates() isRightMouseDown = false isMiddleMouseDown = false this.TurningRight = false this.TurningLeft = false OnMousePanButtonReleased() -- this function doesn't seem to actually need parameters if UserInputService.TouchEnabled then --[[menu opening was causing serious touch issues this should disable all active touch events if they're active when menu opens.]] for inputObject, value in pairs(fingerTouches) do fingerTouches[inputObject] = nil end panBeginLook = nil startPos = nil lastPos = nil this.UserPanningTheCamera = false StartingDiff = nil pinchBeginZoom = nil NumUnsunkTouches = 0 end end function this.getGamepadPan(name, state, input) if state == Enum.UserInputState.Cancel then this.GamepadPanningCamera = ZERO_VECTOR2 return end if input.UserInputType == this.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then local inputVector = Vector2.new(input.Position.X, -input.Position.Y) if inputVector.magnitude > THUMBSTICK_DEADZONE then this.GamepadPanningCamera = Vector2_new(input.Position.X, -input.Position.Y) else this.GamepadPanningCamera = ZERO_VECTOR2 end end end function this.doGamepadZoom(name, state, input) if input.UserInputType == this.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonR3 and state == Enum.UserInputState.Begin then if this.ZoomEnabled then if this:GetCameraZoom() > 0.5 then this:ZoomCamera(0) else this:ZoomCamera(10) end end end end function this:BindGamepadInputActions() ContextActionService:BindAction("RootCamGamepadPan", this.getGamepadPan, false, Enum.KeyCode.Thumbstick2) ContextActionService:BindAction("RootCamGamepadZoom", this.doGamepadZoom, false, Enum.KeyCode.ButtonR3) end function this:ConnectInputEvents() InputBeganConn = UserInputService.InputBegan:connect(function(input, processed) if input.UserInputType == Enum.UserInputType.Touch then OnTouchBegan(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton2 then OnMouse2Down(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton3 then OnMouse3Down(input, processed) end -- Keyboard if input.UserInputType == Enum.UserInputType.Keyboard then OnKeyDown(input, processed) end end) InputChangedConn = UserInputService.InputChanged:connect(function(input, processed) if input.UserInputType == Enum.UserInputType.Touch then OnTouchChanged(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseMovement then OnMouseMoved(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseWheel then OnMouseWheel(input, processed) end end) InputEndedConn = UserInputService.InputEnded:connect(function(input, processed) if input.UserInputType == Enum.UserInputType.Touch then OnTouchEnded(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton2 then OnMouse2Up(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton3 then OnMouse3Up(input, processed) end -- Keyboard if input.UserInputType == Enum.UserInputType.Keyboard then OnKeyUp(input, processed) end end) WindowUnfocusConn = UserInputService.WindowFocusReleased:connect(onWindowFocusReleased) MenuOpenedConn = GuiService.MenuOpened:connect(function() this:ResetInputStates() end) workspaceChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged) if workspace.CurrentCamera then onCurrentCameraChanged() end ShiftLockToggleConn = ShiftLockController.OnShiftLockToggled.Event:connect(function() this:UpdateMouseBehavior() end) this.RotateInput = Vector2.new() this.activeGamepad = nil local function assignActivateGamepad() local connectedGamepads = UserInputService:GetConnectedGamepads() if #connectedGamepads > 0 then for i = 1, #connectedGamepads do if this.activeGamepad == nil then this.activeGamepad = connectedGamepads[i] elseif connectedGamepads[i].Value < this.activeGamepad.Value then this.activeGamepad = connectedGamepads[i] end end end if this.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1 this.activeGamepad = Enum.UserInputType.Gamepad1 end end GamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) if this.activeGamepad ~= gamepadEnum then return end this.activeGamepad = nil assignActivateGamepad() end) GamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum) if this.activeGamepad == nil then assignActivateGamepad() end end) self:BindGamepadInputActions() assignActivateGamepad() -- set mouse behavior self:UpdateMouseBehavior() end --Process tweens related to tap-to-recenter and double-tap-to-zoom --Needs to be called from specific cameras on each update function this:ProcessTweens() for name, tween in pairs(tweens) do local alpha = math.min(1.0, (tick() - tween.start)/tween.duration) tween.to = tween.func(tween.from, tween.to, alpha) if math.abs(1 - alpha) < 0.0001 then tweens[name] = nil end end end function this:SetEnabled(newState) if newState ~= self.Enabled then self.Enabled = newState if self.Enabled then self:ConnectInputEvents() self.cframe = workspace.CurrentCamera.CFrame else self:DisconnectInputEvents() end end end local function OnPlayerAdded(player) player.Changed:connect(function(prop) if this.Enabled then if prop == "CameraMode" or prop == "CameraMaxZoomDistance" or prop == "CameraMinZoomDistance" then this:ZoomCameraFixedBy(0) end end end) local function OnCharacterAdded(newCharacter) local humanoid = findPlayerHumanoid(player) local start = tick() while tick() - start < 0.3 and (humanoid == nil or humanoid.Torso == nil) do wait() humanoid = findPlayerHumanoid(player) end if humanoid and humanoid.Torso and player.Character == newCharacter then local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0, math.sin(math.rad(DEFAULT_CAMERA_ANGLE)), 0)).unit local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, this:GetCameraLook()) local vertShift = math.asin(this:GetCameraLook().y) - math.asin(newDesiredLook.y) if not IsFinite(horizontalShift) then horizontalShift = 0 end if not IsFinite(vertShift) then vertShift = 0 end this.RotateInput = Vector2.new(horizontalShift, vertShift) -- reset old camera info so follow cam doesn't rotate us this.LastCameraTransform = nil end -- Need to wait for camera cframe to update before we zoom in -- Not waiting will force camera to original cframe wait() this:ZoomCamera(this.DefaultZoom) end player.CharacterAdded:connect(function(character) if this.Enabled or SetCameraOnSpawn then OnCharacterAdded(character) SetCameraOnSpawn = false end end) if player.Character then spawn(function() OnCharacterAdded(player.Character) end) end end if PlayersService.LocalPlayer then OnPlayerAdded(PlayersService.LocalPlayer) end PlayersService.ChildAdded:connect(function(child) if child and PlayersService.LocalPlayer == child then OnPlayerAdded(PlayersService.LocalPlayer) end end) local function OnGameLoaded() hasGameLoaded = true end spawn(function() if game:IsLoaded() then OnGameLoaded() else game.Loaded:wait() OnGameLoaded() end end) local function OnDynamicThumbstickEnabled() if UserInputService.TouchEnabled then isDynamicThumbstickEnabled = true end end local function OnDynamicThumbstickDisabled() isDynamicThumbstickEnabled = false end local function OnGameSettingsTouchMovementModeChanged() if LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then if GameSettings.TouchMovementMode.Name == "DynamicThumbstick" then OnDynamicThumbstickEnabled() else OnDynamicThumbstickDisabled() end end end local function OnDevTouchMovementModeChanged() if LocalPlayer.DevTouchMovementMode.Name == "DynamicThumbstick" then OnDynamicThumbstickEnabled() else OnGameSettingsTouchMovementModeChanged() end end if PlayersService.LocalPlayer then PlayersService.LocalPlayer.Changed:Connect(function(prop) if prop == "DevTouchMovementMode" then OnDevTouchMovementModeChanged() end end) OnDevTouchMovementModeChanged() end GameSettings.Changed:Connect(function(prop) if prop == "TouchMovementMode" then OnGameSettingsTouchMovementModeChanged() end end) OnGameSettingsTouchMovementModeChanged() GameSettings:SetCameraYInvertVisible() pcall(function() GameSettings:SetGamepadCameraSensitivityVisible() end) return this end return CreateCamera