--BROUGHT TO YOU BY RSCRIPTS.NET!-- --[[ local _p = game:WaitForChild("Players") local _plr = _p.ChildAdded:Wait() if _plr == _p.LocalPlayer then _plr.ChildAdded:Connect(function(cccc) if c.Name == "PlayerScriptsLoader" then c.Disabled = true end end) end ]] repeat wait() a = pcall(function() game:WaitForChild("Players").LocalPlayer:WaitForChild("PlayerScripts").ChildAdded:Connect(function(c) if c.Name == "PlayerScriptsLoader"then c.Disabled = true end end) end) if a == true then break end until true == false game:WaitForChild("Players").LocalPlayer:WaitForChild("PlayerScripts").ChildAdded:Connect(function(c) if c.Name == "PlayerScriptsLoader"then c.Disabled = true end end) function _CameraUI() local Players = game:GetService("Players") local TweenService = game:GetService("TweenService") local LocalPlayer = Players.LocalPlayer if not LocalPlayer then Players:GetPropertyChangedSignal("LocalPlayer"):Wait() LocalPlayer = Players.LocalPlayer end local function waitForChildOfClass(parent, class) local child = parent:FindFirstChildOfClass(class) while not child or child.ClassName ~= class do child = parent.ChildAdded:Wait() end return child end local PlayerGui = waitForChildOfClass(LocalPlayer, "PlayerGui") local TOAST_OPEN_SIZE = UDim2.new(0, 326, 0, 58) local TOAST_CLOSED_SIZE = UDim2.new(0, 80, 0, 58) local TOAST_BACKGROUND_COLOR = Color3.fromRGB(32, 32, 32) local TOAST_BACKGROUND_TRANS = 0.4 local TOAST_FOREGROUND_COLOR = Color3.fromRGB(200, 200, 200) local TOAST_FOREGROUND_TRANS = 0 -- Convenient syntax for creating a tree of instanes local function create(className) return function(props) local inst = Instance.new(className) local parent = props.Parent props.Parent = nil for name, val in pairs(props) do if type(name) == "string" then inst[name] = val else val.Parent = inst end end -- Only set parent after all other properties are initialized inst.Parent = parent return inst end end local initialized = false local uiRoot local toast local toastIcon local toastUpperText local toastLowerText local function initializeUI() assert(not initialized) uiRoot = create("ScreenGui"){ Name = "RbxCameraUI", AutoLocalize = false, Enabled = true, DisplayOrder = -1, -- Appears behind default developer UI IgnoreGuiInset = false, ResetOnSpawn = false, ZIndexBehavior = Enum.ZIndexBehavior.Sibling, create("ImageLabel"){ Name = "Toast", Visible = false, AnchorPoint = Vector2.new(0.5, 0), BackgroundTransparency = 1, BorderSizePixel = 0, Position = UDim2.new(0.5, 0, 0, 8), Size = TOAST_CLOSED_SIZE, Image = "rbxasset://textures/ui/Camera/CameraToast9Slice.png", ImageColor3 = TOAST_BACKGROUND_COLOR, ImageRectSize = Vector2.new(6, 6), ImageTransparency = 1, ScaleType = Enum.ScaleType.Slice, SliceCenter = Rect.new(3, 3, 3, 3), ClipsDescendants = true, create("Frame"){ Name = "IconBuffer", BackgroundTransparency = 1, BorderSizePixel = 0, Position = UDim2.new(0, 0, 0, 0), Size = UDim2.new(0, 80, 1, 0), create("ImageLabel"){ Name = "Icon", AnchorPoint = Vector2.new(0.5, 0.5), BackgroundTransparency = 1, Position = UDim2.new(0.5, 0, 0.5, 0), Size = UDim2.new(0, 48, 0, 48), ZIndex = 2, Image = "rbxasset://textures/ui/Camera/CameraToastIcon.png", ImageColor3 = TOAST_FOREGROUND_COLOR, ImageTransparency = 1, } }, create("Frame"){ Name = "TextBuffer", BackgroundTransparency = 1, BorderSizePixel = 0, Position = UDim2.new(0, 80, 0, 0), Size = UDim2.new(1, -80, 1, 0), ClipsDescendants = true, create("TextLabel"){ Name = "Upper", AnchorPoint = Vector2.new(0, 1), BackgroundTransparency = 1, Position = UDim2.new(0, 0, 0.5, 0), Size = UDim2.new(1, 0, 0, 19), Font = Enum.Font.GothamSemibold, Text = "Camera control enabled", TextColor3 = TOAST_FOREGROUND_COLOR, TextTransparency = 1, TextSize = 19, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Center, }, create("TextLabel"){ Name = "Lower", AnchorPoint = Vector2.new(0, 0), BackgroundTransparency = 1, Position = UDim2.new(0, 0, 0.5, 3), Size = UDim2.new(1, 0, 0, 15), Font = Enum.Font.Gotham, Text = "Right mouse button to toggle", TextColor3 = TOAST_FOREGROUND_COLOR, TextTransparency = 1, TextSize = 15, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Center, }, }, }, Parent = PlayerGui, } toast = uiRoot.Toast toastIcon = toast.IconBuffer.Icon toastUpperText = toast.TextBuffer.Upper toastLowerText = toast.TextBuffer.Lower initialized = true end local CameraUI = {} do -- Instantaneously disable the toast or enable for opening later on. Used when switching camera modes. function CameraUI.setCameraModeToastEnabled(enabled) if not enabled and not initialized then return end if not initialized then initializeUI() end toast.Visible = enabled if not enabled then CameraUI.setCameraModeToastOpen(false) end end local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) -- Tween the toast in or out. Toast must be enabled with setCameraModeToastEnabled. function CameraUI.setCameraModeToastOpen(open) assert(initialized) TweenService:Create(toast, tweenInfo, { Size = open and TOAST_OPEN_SIZE or TOAST_CLOSED_SIZE, ImageTransparency = open and TOAST_BACKGROUND_TRANS or 1, }):Play() TweenService:Create(toastIcon, tweenInfo, { ImageTransparency = open and TOAST_FOREGROUND_TRANS or 1, }):Play() TweenService:Create(toastUpperText, tweenInfo, { TextTransparency = open and TOAST_FOREGROUND_TRANS or 1, }):Play() TweenService:Create(toastLowerText, tweenInfo, { TextTransparency = open and TOAST_FOREGROUND_TRANS or 1, }):Play() end end return CameraUI end function _CameraToggleStateController() local Players = game:GetService("Players") local UserInputService = game:GetService("UserInputService") local GameSettings = UserSettings():GetService("UserGameSettings") local LocalPlayer = Players.LocalPlayer if not LocalPlayer then Players:GetPropertyChangedSignal("LocalPlayer"):Wait() LocalPlayer = Players.LocalPlayer end local Mouse = LocalPlayer:GetMouse() local Input = _CameraInput() local CameraUI = _CameraUI() local lastTogglePan = false local lastTogglePanChange = tick() local CROSS_MOUSE_ICON = "rbxasset://textures/Cursors/CrossMouseIcon.png" local lockStateDirty = false local wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = false local lastFirstPerson = false CameraUI.setCameraModeToastEnabled(false) return function(isFirstPerson) local togglePan = Input.getTogglePan() local toastTimeout = 3 if isFirstPerson and togglePan ~= lastTogglePan then lockStateDirty = true end if lastTogglePan ~= togglePan or tick() - lastTogglePanChange > toastTimeout then local doShow = togglePan and tick() - lastTogglePanChange < toastTimeout CameraUI.setCameraModeToastOpen(doShow) if togglePan then lockStateDirty = false end lastTogglePanChange = tick() lastTogglePan = togglePan end if isFirstPerson ~= lastFirstPerson then if isFirstPerson then wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = Input.getTogglePan() Input.setTogglePan(true) elseif not lockStateDirty then Input.setTogglePan(wasTogglePanOnTheLastTimeYouWentIntoFirstPerson) end end if isFirstPerson then if Input.getTogglePan() then Mouse.Icon = CROSS_MOUSE_ICON UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter --GameSettings.RotationType = Enum.RotationType.CameraRelative else Mouse.Icon = "" UserInputService.MouseBehavior = Enum.MouseBehavior.Default --GameSettings.RotationType = Enum.RotationType.CameraRelative end elseif Input.getTogglePan() then Mouse.Icon = CROSS_MOUSE_ICON UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter GameSettings.RotationType = Enum.RotationType.MovementRelative elseif Input.getHoldPan() then Mouse.Icon = "" UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition GameSettings.RotationType = Enum.RotationType.MovementRelative else Mouse.Icon = "" UserInputService.MouseBehavior = Enum.MouseBehavior.Default GameSettings.RotationType = Enum.RotationType.MovementRelative end lastFirstPerson = isFirstPerson end end function _CameraInput() local UserInputService = game:GetService("UserInputService") local MB_TAP_LENGTH = 0.3 -- length of time for a short mouse button tap to be registered local rmbDown, rmbUp do local rmbDownBindable = Instance.new("BindableEvent") local rmbUpBindable = Instance.new("BindableEvent") rmbDown = rmbDownBindable.Event rmbUp = rmbUpBindable.Event UserInputService.InputBegan:Connect(function(input, gpe) if not gpe and input.UserInputType == Enum.UserInputType.MouseButton2 then rmbDownBindable:Fire() end end) UserInputService.InputEnded:Connect(function(input, gpe) if input.UserInputType == Enum.UserInputType.MouseButton2 then rmbUpBindable:Fire() end end) end local holdPan = false local togglePan = false local lastRmbDown = 0 -- tick() timestamp of the last right mouse button down event local CameraInput = {} function CameraInput.getHoldPan() return holdPan end function CameraInput.getTogglePan() return togglePan end function CameraInput.getPanning() return togglePan or holdPan end function CameraInput.setTogglePan(value) togglePan = value end local cameraToggleInputEnabled = false local rmbDownConnection local rmbUpConnection function CameraInput.enableCameraToggleInput() if cameraToggleInputEnabled then return end cameraToggleInputEnabled = true holdPan = false togglePan = false if rmbDownConnection then rmbDownConnection:Disconnect() end if rmbUpConnection then rmbUpConnection:Disconnect() end rmbDownConnection = rmbDown:Connect(function() holdPan = true lastRmbDown = tick() end) rmbUpConnection = rmbUp:Connect(function() holdPan = false if tick() - lastRmbDown < MB_TAP_LENGTH and (togglePan or UserInputService:GetMouseDelta().Magnitude < 2) then togglePan = not togglePan end end) end function CameraInput.disableCameraToggleInput() if not cameraToggleInputEnabled then return end cameraToggleInputEnabled = false if rmbDownConnection then rmbDownConnection:Disconnect() rmbDownConnection = nil end if rmbUpConnection then rmbUpConnection:Disconnect() rmbUpConnection = nil end end return CameraInput end function _BaseCamera() --[[ BaseCamera - Abstract base class for camera control modules 2018 Camera Update - AllYourBlox --]] --[[ Local Constants ]]-- local UNIT_Z = Vector3.new(0,0,1) local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane local THUMBSTICK_DEADZONE = 0.2 local DEFAULT_DISTANCE = 12.5 -- Studs local PORTRAIT_DEFAULT_DISTANCE = 25 -- Studs local FIRST_PERSON_DISTANCE_THRESHOLD = 1.0 -- Below this value, snap into first person local CAMERA_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value -- Note: DotProduct check in CoordinateFrame::lookAt() prevents using values within about -- 8.11 degrees of the +/- Y axis, that's why these limits are currently 80 degrees local MIN_Y = math.rad(-80) local MAX_Y = math.rad(80) local TOUCH_ADJUST_AREA_UP = math.rad(30) local TOUCH_ADJUST_AREA_DOWN = math.rad(-15) local TOUCH_SENSITIVTY_ADJUST_MAX_Y = 2.1 local TOUCH_SENSITIVTY_ADJUST_MIN_Y = 0.5 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(0.00945 * math.pi, 0.003375 * math.pi) local MOUSE_SENSITIVITY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi ) 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, 1.5, 0) local R15_HEAD_OFFSET_NO_SCALING = Vector3.new(0, 2, 0) local HUMANOID_ROOT_PART_SIZE = Vector3.new(2, 2, 1) local GAMEPAD_ZOOM_STEP_1 = 0 local GAMEPAD_ZOOM_STEP_2 = 10 local GAMEPAD_ZOOM_STEP_3 = 20 local PAN_SENSITIVITY = 20 local ZOOM_SENSITIVITY_CURVATURE = 0.5 local abs = math.abs local sign = math.sign local FFlagUserCameraToggle do local success, result = pcall(function() return UserSettings():IsUserFeatureEnabled("UserCameraToggle") end) FFlagUserCameraToggle = success and result end local FFlagUserDontAdjustSensitvityForPortrait do local success, result = pcall(function() return UserSettings():IsUserFeatureEnabled("UserDontAdjustSensitvityForPortrait") end) FFlagUserDontAdjustSensitvityForPortrait = success and result end local FFlagUserFixZoomInZoomOutDiscrepancy do local success, result = pcall(function() return UserSettings():IsUserFeatureEnabled("UserFixZoomInZoomOutDiscrepancy") end) FFlagUserFixZoomInZoomOutDiscrepancy = success and result end local Util = _CameraUtils() local ZoomController = _ZoomController() local CameraToggleStateController = _CameraToggleStateController() local CameraInput = _CameraInput() local CameraUI = _CameraUI() --[[ Roblox Services ]]-- local Players = 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 UserGameSettings = UserSettings():GetService("UserGameSettings") local player = Players.LocalPlayer --[[ The Module ]]-- local BaseCamera = {} BaseCamera.__index = BaseCamera function BaseCamera.new() local self = setmetatable({}, BaseCamera) -- So that derived classes have access to this self.FIRST_PERSON_DISTANCE_THRESHOLD = FIRST_PERSON_DISTANCE_THRESHOLD self.cameraType = nil self.cameraMovementMode = nil self.lastCameraTransform = nil self.rotateInput = ZERO_VECTOR2 self.userPanningCamera = false self.lastUserPanCamera = tick() self.humanoidRootPart = nil self.humanoidCache = {} -- Subject and position on last update call self.lastSubject = nil self.lastSubjectPosition = Vector3.new(0,5,0) -- These subject distance members refer to the nominal camera-to-subject follow distance that the camera -- is trying to maintain, not the actual measured value. -- The default is updated when screen orientation or the min/max distances change, -- to be sure the default is always in range and appropriate for the orientation. self.defaultSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) self.currentSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) self.inFirstPerson = false self.inMouseLockedMode = false self.portraitMode = false self.isSmallTouchScreen = false -- Used by modules which want to reset the camera angle on respawn. self.resetCameraAngle = true self.enabled = false -- Input Event Connections self.inputBeganConn = nil self.inputChangedConn = nil self.inputEndedConn = nil self.startPos = nil self.lastPos = nil self.panBeginLook = nil self.panEnabled = true self.keyPanEnabled = true self.distanceChangeEnabled = true self.PlayerGui = nil self.cameraChangedConn = nil self.viewportSizeChangedConn = nil self.boundContextActions = {} -- VR Support self.shouldUseVRRotation = false self.VRRotationIntensityAvailable = false self.lastVRRotationIntensityCheckTime = 0 self.lastVRRotationTime = 0 self.vrRotateKeyCooldown = {} self.cameraTranslationConstraints = Vector3.new(1, 1, 1) self.humanoidJumpOrigin = nil self.trackingHumanoid = nil self.cameraFrozen = false self.subjectStateChangedConn = nil -- Gamepad support self.activeGamepad = nil self.gamepadPanningCamera = false self.lastThumbstickRotate = nil self.numOfSeconds = 0.7 self.currentSpeed = 0 self.maxSpeed = 6 self.vrMaxSpeed = 4 self.lastThumbstickPos = Vector2.new(0,0) self.ySensitivity = 0.65 self.lastVelocity = nil self.gamepadConnectedConn = nil self.gamepadDisconnectedConn = nil self.currentZoomSpeed = 1.0 self.L3ButtonDown = false self.dpadLeftDown = false self.dpadRightDown = false -- Touch input support self.isDynamicThumbstickEnabled = false self.fingerTouches = {} self.dynamicTouchInput = nil self.numUnsunkTouches = 0 self.inputStartPositions = {} self.inputStartTimes = {} self.startingDiff = nil self.pinchBeginZoom = nil self.userPanningTheCamera = false self.touchActivateConn = nil -- Mouse locked formerly known as shift lock mode self.mouseLockOffset = ZERO_VECTOR3 -- [[ NOTICE ]] -- -- Initialization things used to always execute at game load time, but now these camera modules are instantiated -- when needed, so the code here may run well after the start of the game if player.Character then self:OnCharacterAdded(player.Character) end player.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char) end) if self.cameraChangedConn then self.cameraChangedConn:Disconnect() end self.cameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function() self:OnCurrentCameraChanged() end) self:OnCurrentCameraChanged() if self.playerCameraModeChangeConn then self.playerCameraModeChangeConn:Disconnect() end self.playerCameraModeChangeConn = player:GetPropertyChangedSignal("CameraMode"):Connect(function() self:OnPlayerCameraPropertyChange() end) if self.minDistanceChangeConn then self.minDistanceChangeConn:Disconnect() end self.minDistanceChangeConn = player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(function() self:OnPlayerCameraPropertyChange() end) if self.maxDistanceChangeConn then self.maxDistanceChangeConn:Disconnect() end self.maxDistanceChangeConn = player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(function() self:OnPlayerCameraPropertyChange() end) if self.playerDevTouchMoveModeChangeConn then self.playerDevTouchMoveModeChangeConn:Disconnect() end self.playerDevTouchMoveModeChangeConn = player:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function() self:OnDevTouchMovementModeChanged() end) self:OnDevTouchMovementModeChanged() -- Init if self.gameSettingsTouchMoveMoveChangeConn then self.gameSettingsTouchMoveMoveChangeConn:Disconnect() end self.gameSettingsTouchMoveMoveChangeConn = UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function() self:OnGameSettingsTouchMovementModeChanged() end) self:OnGameSettingsTouchMovementModeChanged() -- Init UserGameSettings:SetCameraYInvertVisible() UserGameSettings:SetGamepadCameraSensitivityVisible() self.hasGameLoaded = game:IsLoaded() if not self.hasGameLoaded then self.gameLoadedConn = game.Loaded:Connect(function() self.hasGameLoaded = true self.gameLoadedConn:Disconnect() self.gameLoadedConn = nil end) end self:OnPlayerCameraPropertyChange() return self end function BaseCamera:GetModuleName() return "BaseCamera" end function BaseCamera:OnCharacterAdded(char) self.resetCameraAngle = self.resetCameraAngle or self:GetEnabled() self.humanoidRootPart = nil if UserInputService.TouchEnabled then self.PlayerGui = player:WaitForChild("PlayerGui") for _, child in ipairs(char:GetChildren()) do if child:IsA("Tool") then self.isAToolEquipped = true end end char.ChildAdded:Connect(function(child) if child:IsA("Tool") then self.isAToolEquipped = true end end) char.ChildRemoved:Connect(function(child) if child:IsA("Tool") then self.isAToolEquipped = false end end) end end function BaseCamera:GetHumanoidRootPart() if not self.humanoidRootPart then if player.Character then local humanoid = player.Character:FindFirstChildOfClass("Humanoid") if humanoid then self.humanoidRootPart = humanoid.RootPart end end end return self.humanoidRootPart end function BaseCamera:GetBodyPartToFollow(humanoid, isDead) -- If the humanoid is dead, prefer the head part if one still exists as a sibling of the humanoid if humanoid:GetState() == Enum.HumanoidStateType.Dead then local character = humanoid.Parent if character and character:IsA("Model") then return character:FindFirstChild("Head") or humanoid.RootPart end end return humanoid.RootPart end function BaseCamera:GetSubjectPosition() local result = self.lastSubjectPosition local camera = game.Workspace.CurrentCamera local cameraSubject = camera and camera.CameraSubject if cameraSubject then if cameraSubject:IsA("Humanoid") then local humanoid = cameraSubject local humanoidIsDead = humanoid:GetState() == Enum.HumanoidStateType.Dead if VRService.VREnabled and humanoidIsDead and humanoid == self.lastSubject then result = self.lastSubjectPosition else local bodyPartToFollow = humanoid.RootPart -- If the humanoid is dead, prefer their head part as a follow target, if it exists if humanoidIsDead then if humanoid.Parent and humanoid.Parent:IsA("Model") then bodyPartToFollow = humanoid.Parent:FindFirstChild("Head") or bodyPartToFollow end end if bodyPartToFollow and bodyPartToFollow:IsA("BasePart") then local heightOffset if humanoid.RigType == Enum.HumanoidRigType.R15 then if humanoid.AutomaticScalingEnabled then heightOffset = R15_HEAD_OFFSET if bodyPartToFollow == humanoid.RootPart then local rootPartSizeOffset = (humanoid.RootPart.Size.Y/2) - (HUMANOID_ROOT_PART_SIZE.Y/2) heightOffset = heightOffset + Vector3.new(0, rootPartSizeOffset, 0) end else heightOffset = R15_HEAD_OFFSET_NO_SCALING end else heightOffset = HEAD_OFFSET end if humanoidIsDead then heightOffset = ZERO_VECTOR3 end result = bodyPartToFollow.CFrame.p + bodyPartToFollow.CFrame:vectorToWorldSpace(heightOffset + humanoid.CameraOffset) end end elseif cameraSubject:IsA("VehicleSeat") then local offset = SEAT_OFFSET if VRService.VREnabled then offset = VR_SEAT_OFFSET end result = cameraSubject.CFrame.p + cameraSubject.CFrame:vectorToWorldSpace(offset) elseif cameraSubject:IsA("SkateboardPlatform") then result = cameraSubject.CFrame.p + SEAT_OFFSET elseif cameraSubject:IsA("BasePart") then result = cameraSubject.CFrame.p elseif cameraSubject:IsA("Model") then if cameraSubject.PrimaryPart then result = cameraSubject:GetPrimaryPartCFrame().p else result = cameraSubject:GetModelCFrame().p end end else -- cameraSubject is nil -- Note: Previous RootCamera did not have this else case and let self.lastSubject and self.lastSubjectPosition -- both get set to nil in the case of cameraSubject being nil. This function now exits here to preserve the -- last set valid values for these, as nil values are not handled cases return end self.lastSubject = cameraSubject self.lastSubjectPosition = result return result end function BaseCamera:UpdateDefaultSubjectDistance() if self.portraitMode then self.defaultSubjectDistance = math.clamp(PORTRAIT_DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) else self.defaultSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) end end function BaseCamera:OnViewportSizeChanged() local camera = game.Workspace.CurrentCamera local size = camera.ViewportSize self.portraitMode = size.X < size.Y self.isSmallTouchScreen = UserInputService.TouchEnabled and (size.Y < 500 or size.X < 700) self:UpdateDefaultSubjectDistance() end -- Listener for changes to workspace.CurrentCamera function BaseCamera:OnCurrentCameraChanged() if UserInputService.TouchEnabled then if self.viewportSizeChangedConn then self.viewportSizeChangedConn:Disconnect() self.viewportSizeChangedConn = nil end local newCamera = game.Workspace.CurrentCamera if newCamera then self:OnViewportSizeChanged() self.viewportSizeChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function() self:OnViewportSizeChanged() end) end end -- VR support additions if self.cameraSubjectChangedConn then self.cameraSubjectChangedConn:Disconnect() self.cameraSubjectChangedConn = nil end local camera = game.Workspace.CurrentCamera if camera then self.cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):Connect(function() self:OnNewCameraSubject() end) self:OnNewCameraSubject() end end function BaseCamera:OnDynamicThumbstickEnabled() if UserInputService.TouchEnabled then self.isDynamicThumbstickEnabled = true end end function BaseCamera:OnDynamicThumbstickDisabled() self.isDynamicThumbstickEnabled = false end function BaseCamera:OnGameSettingsTouchMovementModeChanged() if player.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then if (UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.DynamicThumbstick or UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.Default) then self:OnDynamicThumbstickEnabled() else self:OnDynamicThumbstickDisabled() end end end function BaseCamera:OnDevTouchMovementModeChanged() if player.DevTouchMovementMode.Name == "DynamicThumbstick" then self:OnDynamicThumbstickEnabled() else self:OnGameSettingsTouchMovementModeChanged() end end function BaseCamera:OnPlayerCameraPropertyChange() -- This call forces re-evaluation of player.CameraMode and clamping to min/max distance which may have changed self:SetCameraToSubjectDistance(self.currentSubjectDistance) end function BaseCamera:GetCameraHeight() if VRService.VREnabled and not self.inFirstPerson then return math.sin(VR_ANGLE) * self.currentSubjectDistance end return 0 end function BaseCamera:InputTranslationToCameraAngleChange(translationVector, sensitivity) if not FFlagUserDontAdjustSensitvityForPortrait then local camera = game.Workspace.CurrentCamera if camera and camera.ViewportSize.X > 0 and camera.ViewportSize.Y > 0 and (camera.ViewportSize.Y > camera.ViewportSize.X) then -- Screen has portrait orientation, swap X and Y sensitivity return translationVector * Vector2.new( sensitivity.Y, sensitivity.X) end end return translationVector * sensitivity end function BaseCamera:Enable(enable) if self.enabled ~= enable then self.enabled = enable if self.enabled then self:ConnectInputEvents() self:BindContextActions() if player.CameraMode == Enum.CameraMode.LockFirstPerson then self.currentSubjectDistance = 0.5 if not self.inFirstPerson then self:EnterFirstPerson() end end else self:DisconnectInputEvents() self:UnbindContextActions() -- Clean up additional event listeners and reset a bunch of properties self:Cleanup() end end end function BaseCamera:GetEnabled() return self.enabled end function BaseCamera:OnInputBegan(input, processed) if input.UserInputType == Enum.UserInputType.Touch then self:OnTouchBegan(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton2 then self:OnMouse2Down(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton3 then self:OnMouse3Down(input, processed) end end function BaseCamera:OnInputChanged(input, processed) if input.UserInputType == Enum.UserInputType.Touch then self:OnTouchChanged(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseMovement then self:OnMouseMoved(input, processed) end end function BaseCamera:OnInputEnded(input, processed) if input.UserInputType == Enum.UserInputType.Touch then self:OnTouchEnded(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton2 then self:OnMouse2Up(input, processed) elseif input.UserInputType == Enum.UserInputType.MouseButton3 then self:OnMouse3Up(input, processed) end end function BaseCamera:OnPointerAction(wheel, pan, pinch, processed) if processed then return end if pan.Magnitude > 0 then local inversionVector = Vector2.new(1, UserGameSettings:GetCameraYInvertValue()) local rotateDelta = self:InputTranslationToCameraAngleChange(PAN_SENSITIVITY*pan, MOUSE_SENSITIVITY)*inversionVector self.rotateInput = self.rotateInput + rotateDelta end local zoom = self.currentSubjectDistance local zoomDelta = -(wheel + pinch) if abs(zoomDelta) > 0 then local newZoom if self.inFirstPerson and zoomDelta > 0 then newZoom = FIRST_PERSON_DISTANCE_THRESHOLD else if FFlagUserFixZoomInZoomOutDiscrepancy then if (zoomDelta > 0) then newZoom = zoom + zoomDelta*(1 + zoom*ZOOM_SENSITIVITY_CURVATURE) else newZoom = (zoom + zoomDelta) / (1 - zoomDelta*ZOOM_SENSITIVITY_CURVATURE) end else newZoom = zoom + zoomDelta*(1 + zoom*ZOOM_SENSITIVITY_CURVATURE) end end self:SetCameraToSubjectDistance(newZoom) end end function BaseCamera:ConnectInputEvents() self.pointerActionConn = UserInputService.PointerAction:Connect(function(wheel, pan, pinch, processed) self:OnPointerAction(wheel, pan, pinch, processed) end) self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed) self:OnInputBegan(input, processed) end) self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed) self:OnInputChanged(input, processed) end) self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed) self:OnInputEnded(input, processed) end) self.menuOpenedConn = GuiService.MenuOpened:connect(function() self:ResetInputStates() end) self.gamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) if self.activeGamepad ~= gamepadEnum then return end self.activeGamepad = nil self:AssignActivateGamepad() end) self.gamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum) if self.activeGamepad == nil then self:AssignActivateGamepad() end end) self:AssignActivateGamepad() if not FFlagUserCameraToggle then self:UpdateMouseBehavior() end end function BaseCamera:BindContextActions() self:BindGamepadInputActions() self:BindKeyboardInputActions() end function BaseCamera:AssignActivateGamepad() local connectedGamepads = UserInputService:GetConnectedGamepads() if #connectedGamepads > 0 then for i = 1, #connectedGamepads do if self.activeGamepad == nil then self.activeGamepad = connectedGamepads[i] elseif connectedGamepads[i].Value < self.activeGamepad.Value then self.activeGamepad = connectedGamepads[i] end end end if self.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1 self.activeGamepad = Enum.UserInputType.Gamepad1 end end function BaseCamera:DisconnectInputEvents() if self.inputBeganConn then self.inputBeganConn:Disconnect() self.inputBeganConn = nil end if self.inputChangedConn then self.inputChangedConn:Disconnect() self.inputChangedConn = nil end if self.inputEndedConn then self.inputEndedConn:Disconnect() self.inputEndedConn = nil end end function BaseCamera:UnbindContextActions() for i = 1, #self.boundContextActions do ContextActionService:UnbindAction(self.boundContextActions[i]) end self.boundContextActions = {} end function BaseCamera:Cleanup() if self.pointerActionConn then self.pointerActionConn:Disconnect() self.pointerActionConn = nil end if self.menuOpenedConn then self.menuOpenedConn:Disconnect() self.menuOpenedConn = nil end if self.mouseLockToggleConn then self.mouseLockToggleConn:Disconnect() self.mouseLockToggleConn = nil end if self.gamepadConnectedConn then self.gamepadConnectedConn:Disconnect() self.gamepadConnectedConn = nil end if self.gamepadDisconnectedConn then self.gamepadDisconnectedConn:Disconnect() self.gamepadDisconnectedConn = nil end if self.subjectStateChangedConn then self.subjectStateChangedConn:Disconnect() self.subjectStateChangedConn = nil end if self.viewportSizeChangedConn then self.viewportSizeChangedConn:Disconnect() self.viewportSizeChangedConn = nil end if self.touchActivateConn then self.touchActivateConn:Disconnect() self.touchActivateConn = nil end self.turningLeft = false self.turningRight = false self.lastCameraTransform = nil self.lastSubjectCFrame = nil self.userPanningTheCamera = false self.rotateInput = Vector2.new() self.gamepadPanningCamera = Vector2.new(0,0) -- Reset input states self.startPos = nil self.lastPos = nil self.panBeginLook = nil self.isRightMouseDown = false self.isMiddleMouseDown = false self.fingerTouches = {} self.dynamicTouchInput = nil self.numUnsunkTouches = 0 self.startingDiff = nil self.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 -- This is called when settings menu is opened function BaseCamera:ResetInputStates() self.isRightMouseDown = false self.isMiddleMouseDown = false self: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 in pairs(self.fingerTouches) do self.fingerTouches[inputObject] = nil end self.dynamicTouchInput = nil self.panBeginLook = nil self.startPos = nil self.lastPos = nil self.userPanningTheCamera = false self.startingDiff = nil self.pinchBeginZoom = nil self.numUnsunkTouches = 0 end end function BaseCamera:GetGamepadPan(name, state, input) if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then -- if self.L3ButtonDown then -- -- L3 Thumbstick is depressed, right stick controls dolly in/out -- if (input.Position.Y > THUMBSTICK_DEADZONE) then -- self.currentZoomSpeed = 0.96 -- elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then -- self.currentZoomSpeed = 1.04 -- else -- self.currentZoomSpeed = 1.00 -- end -- else if state == Enum.UserInputState.Cancel then self.gamepadPanningCamera = ZERO_VECTOR2 return end local inputVector = Vector2.new(input.Position.X, -input.Position.Y) if inputVector.magnitude > THUMBSTICK_DEADZONE then self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y) else self.gamepadPanningCamera = ZERO_VECTOR2 end --end return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass end function BaseCamera:DoKeyboardPanTurn(name, state, input) if not self.hasGameLoaded and VRService.VREnabled then return Enum.ContextActionResult.Pass end if state == Enum.UserInputState.Cancel then self.turningLeft = false self.turningRight = false return Enum.ContextActionResult.Sink end if self.panBeginLook == nil and self.keyPanEnabled then if input.KeyCode == Enum.KeyCode.Left then self.turningLeft = state == Enum.UserInputState.Begin elseif input.KeyCode == Enum.KeyCode.Right then self.turningRight = state == Enum.UserInputState.Begin end return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass end function BaseCamera:DoPanRotateCamera(rotateAngle) local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), rotateAngle, math.pi*0.25) if angle ~= 0 then self.rotateInput = self.rotateInput + Vector2.new(angle, 0) self.lastUserPanCamera = tick() self.lastCameraTransform = nil end end function BaseCamera:DoGamepadZoom(name, state, input) if input.UserInputType == self.activeGamepad then if input.KeyCode == Enum.KeyCode.ButtonR3 then if state == Enum.UserInputState.Begin then if self.distanceChangeEnabled then local dist = self:GetCameraToSubjectDistance() if dist > (GAMEPAD_ZOOM_STEP_2 + GAMEPAD_ZOOM_STEP_3)/2 then self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_2) elseif dist > (GAMEPAD_ZOOM_STEP_1 + GAMEPAD_ZOOM_STEP_2)/2 then self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_1) else self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_3) end end end elseif input.KeyCode == Enum.KeyCode.DPadLeft then self.dpadLeftDown = (state == Enum.UserInputState.Begin) elseif input.KeyCode == Enum.KeyCode.DPadRight then self.dpadRightDown = (state == Enum.UserInputState.Begin) end if self.dpadLeftDown then self.currentZoomSpeed = 1.04 elseif self.dpadRightDown then self.currentZoomSpeed = 0.96 else self.currentZoomSpeed = 1.00 end return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass -- elseif input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonL3 then -- if (state == Enum.UserInputState.Begin) then -- self.L3ButtonDown = true -- elseif (state == Enum.UserInputState.End) then -- self.L3ButtonDown = false -- self.currentZoomSpeed = 1.00 -- end -- end end function BaseCamera:DoKeyboardZoom(name, state, input) if not self.hasGameLoaded and VRService.VREnabled then return Enum.ContextActionResult.Pass end if state ~= Enum.UserInputState.Begin then return Enum.ContextActionResult.Pass end if self.distanceChangeEnabled and player.CameraMode ~= Enum.CameraMode.LockFirstPerson then if input.KeyCode == Enum.KeyCode.I then self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 ) elseif input.KeyCode == Enum.KeyCode.O then self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 ) end return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass end function BaseCamera:BindAction(actionName, actionFunc, createTouchButton, ...) table.insert(self.boundContextActions, actionName) ContextActionService:BindActionAtPriority(actionName, actionFunc, createTouchButton, CAMERA_ACTION_PRIORITY, ...) end function BaseCamera:BindGamepadInputActions() self:BindAction("BaseCameraGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2) self:BindAction("BaseCameraGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight, Enum.KeyCode.ButtonR3) end function BaseCamera:BindKeyboardInputActions() self:BindAction("BaseCameraKeyboardPanArrowKeys", function(name, state, input) return self:DoKeyboardPanTurn(name, state, input) end, false, Enum.KeyCode.Left, Enum.KeyCode.Right) self:BindAction("BaseCameraKeyboardZoom", function(name, state, input) return self:DoKeyboardZoom(name, state, input) end, false, Enum.KeyCode.I, Enum.KeyCode.O) end local function isInDynamicThumbstickArea(input) local playerGui = player:FindFirstChildOfClass("PlayerGui") local touchGui = playerGui and playerGui:FindFirstChild("TouchGui") local touchFrame = touchGui and touchGui:FindFirstChild("TouchControlFrame") local thumbstickFrame = touchFrame and touchFrame:FindFirstChild("DynamicThumbstickFrame") if not thumbstickFrame then return false end local frameCornerTopLeft = thumbstickFrame.AbsolutePosition local frameCornerBottomRight = frameCornerTopLeft + thumbstickFrame.AbsoluteSize if input.Position.X >= frameCornerTopLeft.X and input.Position.Y >= frameCornerTopLeft.Y then if input.Position.X <= frameCornerBottomRight.X and input.Position.Y <= frameCornerBottomRight.Y then return true end end return false end ---Adjusts the camera Y touch Sensitivity when moving away from the center and in the TOUCH_SENSITIVTY_ADJUST_AREA function BaseCamera:AdjustTouchSensitivity(delta, sensitivity) local cameraCFrame = game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame if not cameraCFrame then return sensitivity end local currPitchAngle = cameraCFrame:ToEulerAnglesYXZ() local multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y if currPitchAngle > TOUCH_ADJUST_AREA_UP and delta.Y < 0 then local fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_UP)/(MAX_Y - TOUCH_ADJUST_AREA_UP) fractionAdjust = 1 - (1 - fractionAdjust)^3 multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * ( TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y) elseif currPitchAngle < TOUCH_ADJUST_AREA_DOWN and delta.Y > 0 then local fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_DOWN)/(MIN_Y - TOUCH_ADJUST_AREA_DOWN) fractionAdjust = 1 - (1 - fractionAdjust)^3 multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * ( TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y) end return Vector2.new( sensitivity.X, sensitivity.Y * multiplierY ) end function BaseCamera:OnTouchBegan(input, processed) local canUseDynamicTouch = self.isDynamicThumbstickEnabled and not processed if canUseDynamicTouch then if self.dynamicTouchInput == nil and isInDynamicThumbstickArea(input) then -- First input in the dynamic thumbstick area should always be ignored for camera purposes -- Even if the dynamic thumbstick does not process it immediately self.dynamicTouchInput = input return end self.fingerTouches[input] = processed self.inputStartPositions[input] = input.Position self.inputStartTimes[input] = tick() self.numUnsunkTouches = self.numUnsunkTouches + 1 end end function BaseCamera:OnTouchChanged(input, processed) if self.fingerTouches[input] == nil then if self.isDynamicThumbstickEnabled then return end self.fingerTouches[input] = processed if not processed then self.numUnsunkTouches = self.numUnsunkTouches + 1 end end if self.numUnsunkTouches == 1 then if self.fingerTouches[input] == false then self.panBeginLook = self.panBeginLook or self:GetCameraLookVector() self.startPos = self.startPos or input.Position self.lastPos = self.lastPos or self.startPos self.userPanningTheCamera = true local delta = input.Position - self.lastPos delta = Vector2.new(delta.X, delta.Y * UserGameSettings:GetCameraYInvertValue()) if self.panEnabled then local adjustedTouchSensitivity = TOUCH_SENSITIVTY self:AdjustTouchSensitivity(delta, TOUCH_SENSITIVTY) local desiredXYVector = self:InputTranslationToCameraAngleChange(delta, adjustedTouchSensitivity) self.rotateInput = self.rotateInput + desiredXYVector end self.lastPos = input.Position end else self.panBeginLook = nil self.startPos = nil self.lastPos = nil self.userPanningTheCamera = false end if self.numUnsunkTouches == 2 then local unsunkTouches = {} for touch, wasSunk in pairs(self.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 self.startingDiff and self.pinchBeginZoom then local scale = difference / math.max(0.01, self.startingDiff) local clampedScale = math.clamp(scale, 0.1, 10) if self.distanceChangeEnabled then self:SetCameraToSubjectDistance(self.pinchBeginZoom / clampedScale) end else self.startingDiff = difference self.pinchBeginZoom = self:GetCameraToSubjectDistance() end end else self.startingDiff = nil self.pinchBeginZoom = nil end end function BaseCamera:OnTouchEnded(input, processed) if input == self.dynamicTouchInput then self.dynamicTouchInput = nil return end if self.fingerTouches[input] == false then if self.numUnsunkTouches == 1 then self.panBeginLook = nil self.startPos = nil self.lastPos = nil self.userPanningTheCamera = false elseif self.numUnsunkTouches == 2 then self.startingDiff = nil self.pinchBeginZoom = nil end end if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then self.numUnsunkTouches = self.numUnsunkTouches - 1 end self.fingerTouches[input] = nil self.inputStartPositions[input] = nil self.inputStartTimes[input] = nil end function BaseCamera:OnMouse2Down(input, processed) if processed then return end self.isRightMouseDown = true self:OnMousePanButtonPressed(input, processed) end function BaseCamera:OnMouse2Up(input, processed) self.isRightMouseDown = false self:OnMousePanButtonReleased(input, processed) end function BaseCamera:OnMouse3Down(input, processed) if processed then return end self.isMiddleMouseDown = true self:OnMousePanButtonPressed(input, processed) end function BaseCamera:OnMouse3Up(input, processed) self.isMiddleMouseDown = false self:OnMousePanButtonReleased(input, processed) end function BaseCamera:OnMouseMoved(input, processed) if not self.hasGameLoaded and VRService.VREnabled then return end local inputDelta = input.Delta inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * UserGameSettings:GetCameraYInvertValue()) local isInputPanning = FFlagUserCameraToggle and CameraInput.getPanning() local isBeginLook = self.startPos and self.lastPos and self.panBeginLook local isPanning = isBeginLook or self.inFirstPerson or self.inMouseLockedMode or isInputPanning if self.panEnabled and isPanning then local desiredXYVector = self:InputTranslationToCameraAngleChange(inputDelta, MOUSE_SENSITIVITY) self.rotateInput = self.rotateInput + desiredXYVector end if self.startPos and self.lastPos and self.panBeginLook then self.lastPos = self.lastPos + input.Delta end end function BaseCamera:OnMousePanButtonPressed(input, processed) if processed then return end if not FFlagUserCameraToggle then self:UpdateMouseBehavior() end self.panBeginLook = self.panBeginLook or self:GetCameraLookVector() self.startPos = self.startPos or input.Position self.lastPos = self.lastPos or self.startPos self.userPanningTheCamera = true end function BaseCamera:OnMousePanButtonReleased(input, processed) if not FFlagUserCameraToggle then self:UpdateMouseBehavior() end if not (self.isRightMouseDown or self.isMiddleMouseDown) then self.panBeginLook = nil self.startPos = nil self.lastPos = nil self.userPanningTheCamera = false end end function BaseCamera:UpdateMouseBehavior() if FFlagUserCameraToggle and self.isCameraToggle then CameraUI.setCameraModeToastEnabled(true) CameraInput.enableCameraToggleInput() CameraToggleStateController(self.inFirstPerson) else if FFlagUserCameraToggle then CameraUI.setCameraModeToastEnabled(false) CameraInput.disableCameraToggleInput() end -- first time transition to first person mode or mouse-locked third person if self.inFirstPerson or self.inMouseLockedMode then --UserGameSettings.RotationType = Enum.RotationType.CameraRelative UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter else UserGameSettings.RotationType = Enum.RotationType.MovementRelative if self.isRightMouseDown or self.isMiddleMouseDown then UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition else UserInputService.MouseBehavior = Enum.MouseBehavior.Default end end end end function BaseCamera:UpdateForDistancePropertyChange() -- Calling this setter with the current value will force checking that it is still -- in range after a change to the min/max distance limits self:SetCameraToSubjectDistance(self.currentSubjectDistance) end function BaseCamera:SetCameraToSubjectDistance(desiredSubjectDistance) local lastSubjectDistance = self.currentSubjectDistance -- By default, camera modules will respect LockFirstPerson and override the currentSubjectDistance with 0 -- regardless of what Player.CameraMinZoomDistance is set to, so that first person can be made -- available by the developer without needing to allow players to mousewheel dolly into first person. -- Some modules will override this function to remove or change first-person capability. if player.CameraMode == Enum.CameraMode.LockFirstPerson then self.currentSubjectDistance = 0.5 if not self.inFirstPerson then self:EnterFirstPerson() end else local newSubjectDistance = math.clamp(desiredSubjectDistance, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) if newSubjectDistance < FIRST_PERSON_DISTANCE_THRESHOLD then self.currentSubjectDistance = 0.5 if not self.inFirstPerson then self:EnterFirstPerson() end else self.currentSubjectDistance = newSubjectDistance if self.inFirstPerson then self:LeaveFirstPerson() end end end -- Pass target distance and zoom direction to the zoom controller ZoomController.SetZoomParameters(self.currentSubjectDistance, math.sign(desiredSubjectDistance - lastSubjectDistance)) -- Returned only for convenience to the caller to know the outcome return self.currentSubjectDistance end function BaseCamera:SetCameraType( cameraType ) --Used by derived classes self.cameraType = cameraType end function BaseCamera:GetCameraType() return self.cameraType end -- Movement mode standardized to Enum.ComputerCameraMovementMode values function BaseCamera:SetCameraMovementMode( cameraMovementMode ) self.cameraMovementMode = cameraMovementMode end function BaseCamera:GetCameraMovementMode() return self.cameraMovementMode end function BaseCamera:SetIsMouseLocked(mouseLocked) self.inMouseLockedMode = mouseLocked if not FFlagUserCameraToggle then self:UpdateMouseBehavior() end end function BaseCamera:GetIsMouseLocked() return self.inMouseLockedMode end function BaseCamera:SetMouseLockOffset(offsetVector) self.mouseLockOffset = offsetVector end function BaseCamera:GetMouseLockOffset() return self.mouseLockOffset end function BaseCamera:InFirstPerson() return self.inFirstPerson end function BaseCamera:EnterFirstPerson() -- Overridden in ClassicCamera, the only module which supports FirstPerson end function BaseCamera:LeaveFirstPerson() -- Overridden in ClassicCamera, the only module which supports FirstPerson end -- Nominal distance, set by dollying in and out with the mouse wheel or equivalent, not measured distance function BaseCamera:GetCameraToSubjectDistance() return self.currentSubjectDistance end -- Actual measured distance to the camera Focus point, which may be needed in special circumstances, but should -- never be used as the starting point for updating the nominal camera-to-subject distance (self.currentSubjectDistance) -- since that is a desired target value set only by mouse wheel (or equivalent) input, PopperCam, and clamped to min max camera distance function BaseCamera:GetMeasuredDistanceToFocus() local camera = game.Workspace.CurrentCamera if camera then return (camera.CoordinateFrame.p - camera.Focus.p).magnitude end return nil end function BaseCamera:GetCameraLookVector() return game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame.lookVector or UNIT_Z end -- Replacements for RootCamera:RotateCamera() which did not actually rotate the camera -- suppliedLookVector is not normally passed in, it's used only by Watch camera function BaseCamera:CalculateNewLookCFrame(suppliedLookVector) local currLookVector = suppliedLookVector or self:GetCameraLookVector() local currPitchAngle = math.asin(currLookVector.y) local yTheta = math.clamp(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle) local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta) local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0) return newLookCFrame end function BaseCamera:CalculateNewLookVector(suppliedLookVector) local newLookCFrame = self:CalculateNewLookCFrame(suppliedLookVector) return newLookCFrame.lookVector end function BaseCamera:CalculateNewLookVectorVR() local subjectPosition = self:GetSubjectPosition() local vecToSubject = (subjectPosition - game.Workspace.CurrentCamera.CFrame.p) local currLookVector = (vecToSubject * X1_Y0_Z1).unit local vrRotateInput = Vector2.new(self.rotateInput.x, 0) local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) local yawRotatedVector = (CFrame.Angles(0, -vrRotateInput.x, 0) * startCFrame * CFrame.Angles(-vrRotateInput.y,0,0)).lookVector return (yawRotatedVector * X1_Y0_Z1).unit end function BaseCamera:GetHumanoid() local character = player and player.Character if character then local resultHumanoid = self.humanoidCache[player] if resultHumanoid and resultHumanoid.Parent == character then return resultHumanoid else self.humanoidCache[player] = nil -- Bust Old Cache local humanoid = character:FindFirstChildOfClass("Humanoid") if humanoid then self.humanoidCache[player] = humanoid end return humanoid end end return nil end function BaseCamera:GetHumanoidPartToFollow(humanoid, humanoidStateType) if humanoidStateType == Enum.HumanoidStateType.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 function BaseCamera:UpdateGamepad() local gamepadPan = self.gamepadPanningCamera if gamepadPan and (self.hasGameLoaded or not VRService.VREnabled) then gamepadPan = Util.GamepadLinearToCurve(gamepadPan) local currentTime = tick() if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then self.userPanningTheCamera = true elseif gamepadPan == ZERO_VECTOR2 then self.lastThumbstickRotate = nil if self.lastThumbstickPos == ZERO_VECTOR2 then self.currentSpeed = 0 end end local finalConstant = 0 if self.lastThumbstickRotate then if VRService.VREnabled then self.currentSpeed = self.vrMaxSpeed else local elapsedTime = (currentTime - self.lastThumbstickRotate) * 10 self.currentSpeed = self.currentSpeed + (self.maxSpeed * ((elapsedTime*elapsedTime)/self.numOfSeconds)) if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end if self.lastVelocity then local velocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate) local velocityDeltaMag = (velocity - self.lastVelocity).magnitude if velocityDeltaMag > 12 then self.currentSpeed = self.currentSpeed * (20/velocityDeltaMag) if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end end end end finalConstant = UserGameSettings.GamepadCameraSensitivity * self.currentSpeed self.lastVelocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate) end self.lastThumbstickPos = gamepadPan self.lastThumbstickRotate = currentTime return Vector2.new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * self.ySensitivity * UserGameSettings:GetCameraYInvertValue()) end return ZERO_VECTOR2 end -- [[ VR Support Section ]] -- function BaseCamera:ApplyVRTransform() if not VRService.VREnabled then return end --we only want this to happen in first person VR local rootJoint = self.humanoidRootPart and self.humanoidRootPart:FindFirstChild("RootJoint") if not rootJoint then return end local cameraSubject = game.Workspace.CurrentCamera.CameraSubject local isInVehicle = cameraSubject and cameraSubject:IsA("VehicleSeat") if self.inFirstPerson and not isInVehicle then local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head) local vrRotation = vrFrame - vrFrame.p rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) else rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) end end function BaseCamera:IsInFirstPerson() return self.inFirstPerson end function BaseCamera:ShouldUseVRRotation() if not VRService.VREnabled then return false end if not self.VRRotationIntensityAvailable and tick() - self.lastVRRotationIntensityCheckTime < 1 then return false end local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) self.VRRotationIntensityAvailable = success and vrRotationIntensity ~= nil self.lastVRRotationIntensityCheckTime = tick() self.shouldUseVRRotation = success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth" return self.shouldUseVRRotation end function BaseCamera:GetVRRotationInput() local vrRotateSum = ZERO_VECTOR2 local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) if not success then return end local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2 local delayExpired = (tick() - self.lastVRRotationTime) >= self:GetRepeatDelayValue(vrRotationIntensity) if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then local sign = 1 if vrGamepadRotation.x < 0 then sign = -1 end vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true end elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil end if self.turningLeft then if delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Left] then vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity) self.vrRotateKeyCooldown[Enum.KeyCode.Left] = true end else self.vrRotateKeyCooldown[Enum.KeyCode.Left] = nil end if self.turningRight then if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Right]) then vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) self.vrRotateKeyCooldown[Enum.KeyCode.Right] = true end else self.vrRotateKeyCooldown[Enum.KeyCode.Right] = nil end if vrRotateSum ~= ZERO_VECTOR2 then self.lastVRRotationTime = tick() end return vrRotateSum end function BaseCamera:CancelCameraFreeze(keepConstraints) if not keepConstraints then self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 1, self.cameraTranslationConstraints.z) end if self.cameraFrozen then self.trackingHumanoid = nil self.cameraFrozen = false end end function BaseCamera:StartCameraFreeze(subjectPosition, humanoidToTrack) if not self.cameraFrozen then self.humanoidJumpOrigin = subjectPosition self.trackingHumanoid = humanoidToTrack self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 0, self.cameraTranslationConstraints.z) self.cameraFrozen = true end end function BaseCamera:OnNewCameraSubject() if self.subjectStateChangedConn then self.subjectStateChangedConn:Disconnect() self.subjectStateChangedConn = nil end local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject if self.trackingHumanoid ~= humanoid then self:CancelCameraFreeze() end if humanoid and humanoid:IsA("Humanoid") then self.subjectStateChangedConn = humanoid.StateChanged:Connect(function(oldState, newState) if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not self.inFirstPerson then self:StartCameraFreeze(self:GetSubjectPosition(), humanoid) elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then self:CancelCameraFreeze(true) end end) end end function BaseCamera:GetVRFocus(subjectPosition, timeDelta) local lastFocus = self.LastCameraFocus or subjectPosition if not self.cameraFrozen then self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, math.min(1, self.cameraTranslationConstraints.y + 0.42 * timeDelta), self.cameraTranslationConstraints.z) end local newFocus if self.cameraFrozen and self.humanoidJumpOrigin and self.humanoidJumpOrigin.y > lastFocus.y then newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(self.humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z)) else newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, self.cameraTranslationConstraints.y)) end if self.cameraFrozen then -- No longer in 3rd person if self.inFirstPerson then -- not VRService.VREnabled self: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 self.humanoidJumpOrigin and subjectPosition.y < (self.humanoidJumpOrigin.y - 0.5) then self:CancelCameraFreeze() end end return newFocus end function BaseCamera: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 BaseCamera: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 function BaseCamera:Update(dt) error("BaseCamera:Update() This is a virtual function that should never be getting called.", 2) end BaseCamera.UpCFrame = CFrame.new() function BaseCamera:UpdateUpCFrame(cf) self.UpCFrame = cf end local ZERO = Vector3.new(0, 0, 0) function BaseCamera:CalculateNewLookCFrame(suppliedLookVector) local currLookVector = suppliedLookVector or self:GetCameraLookVector() currLookVector = self.UpCFrame:VectorToObjectSpace(currLookVector) local currPitchAngle = math.asin(currLookVector.y) local yTheta = math.clamp(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle) local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta) local startCFrame = CFrame.new(ZERO, currLookVector) local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0) return newLookCFrame end return BaseCamera end function _BaseOcclusion() --[[ The Module ]]-- local BaseOcclusion = {} BaseOcclusion.__index = BaseOcclusion setmetatable(BaseOcclusion, { __call = function(_, ...) return BaseOcclusion.new(...) end }) function BaseOcclusion.new() local self = setmetatable({}, BaseOcclusion) return self end -- Called when character is added function BaseOcclusion:CharacterAdded(char, player) end -- Called when character is about to be removed function BaseOcclusion:CharacterRemoving(char, player) end function BaseOcclusion:OnCameraSubjectChanged(newSubject) end --[[ Derived classes are required to override and implement all of the following functions ]]-- function BaseOcclusion:GetOcclusionMode() -- Must be overridden in derived classes to return an Enum.DevCameraOcclusionMode value warn("BaseOcclusion GetOcclusionMode must be overridden by derived classes") return nil end function BaseOcclusion:Enable(enabled) warn("BaseOcclusion Enable must be overridden by derived classes") end function BaseOcclusion:Update(dt, desiredCameraCFrame, desiredCameraFocus) warn("BaseOcclusion Update must be overridden by derived classes") return desiredCameraCFrame, desiredCameraFocus end return BaseOcclusion end function _Popper() local Players = game:GetService("Players") local camera = game.Workspace.CurrentCamera local min = math.min local tan = math.tan local rad = math.rad local inf = math.huge local ray = Ray.new local function getTotalTransparency(part) return 1 - (1 - part.Transparency)*(1 - part.LocalTransparencyModifier) end local function eraseFromEnd(t, toSize) for i = #t, toSize + 1, -1 do t[i] = nil end end local nearPlaneZ, projX, projY do local function updateProjection() local fov = rad(camera.FieldOfView) local view = camera.ViewportSize local ar = view.X/view.Y projY = 2*tan(fov/2) projX = ar*projY end camera:GetPropertyChangedSignal("FieldOfView"):Connect(updateProjection) camera:GetPropertyChangedSignal("ViewportSize"):Connect(updateProjection) updateProjection() nearPlaneZ = camera.NearPlaneZ camera:GetPropertyChangedSignal("NearPlaneZ"):Connect(function() nearPlaneZ = camera.NearPlaneZ end) end local blacklist = {} do local charMap = {} local function refreshIgnoreList() local n = 1 blacklist = {} for _, character in pairs(charMap) do blacklist[n] = character n = n + 1 end end local function playerAdded(player) local function characterAdded(character) charMap[player] = character refreshIgnoreList() end local function characterRemoving() charMap[player] = nil refreshIgnoreList() end player.CharacterAdded:Connect(characterAdded) player.CharacterRemoving:Connect(characterRemoving) if player.Character then characterAdded(player.Character) end end local function playerRemoving(player) charMap[player] = nil refreshIgnoreList() end Players.PlayerAdded:Connect(playerAdded) Players.PlayerRemoving:Connect(playerRemoving) for _, player in ipairs(Players:GetPlayers()) do playerAdded(player) end refreshIgnoreList() end -------------------------------------------------------------------------------------------- -- Popper uses the level geometry find an upper bound on subject-to-camera distance. -- -- Hard limits are applied immediately and unconditionally. They are generally caused -- when level geometry intersects with the near plane (with exceptions, see below). -- -- Soft limits are only applied under certain conditions. -- They are caused when level geometry occludes the subject without actually intersecting -- with the near plane at the target distance. -- -- Soft limits can be promoted to hard limits and hard limits can be demoted to soft limits. -- We usually don"t want the latter to happen. -- -- A soft limit will be promoted to a hard limit if an obstruction -- lies between the current and target camera positions. -------------------------------------------------------------------------------------------- local subjectRoot local subjectPart camera:GetPropertyChangedSignal("CameraSubject"):Connect(function() local subject = camera.CameraSubject if subject:IsA("Humanoid") then subjectPart = subject.RootPart elseif subject:IsA("BasePart") then subjectPart = subject else subjectPart = nil end end) local function canOcclude(part) -- Occluders must be: -- 1. Opaque -- 2. Interactable -- 3. Not in the same assembly as the subject return getTotalTransparency(part) < 0.25 and part.CanCollide and subjectRoot ~= (part:GetRootPart() or part) and not part:IsA("TrussPart") end -- Offsets for the volume visibility test local SCAN_SAMPLE_OFFSETS = { Vector2.new( 0.4, 0.0), Vector2.new(-0.4, 0.0), Vector2.new( 0.0,-0.4), Vector2.new( 0.0, 0.4), Vector2.new( 0.0, 0.2), } -------------------------------------------------------------------------------- -- Piercing raycasts local function getCollisionPoint(origin, dir) local originalSize = #blacklist repeat local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList( ray(origin, dir), blacklist, false, true ) if hitPart then if hitPart.CanCollide then eraseFromEnd(blacklist, originalSize) return hitPoint, true end blacklist[#blacklist + 1] = hitPart end until not hitPart eraseFromEnd(blacklist, originalSize) return origin + dir, false end -------------------------------------------------------------------------------- local function queryPoint(origin, unitDir, dist, lastPos) debug.profilebegin("queryPoint") local originalSize = #blacklist dist = dist + nearPlaneZ local target = origin + unitDir*dist local softLimit = inf local hardLimit = inf local movingOrigin = origin repeat local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true) if entryPart then if canOcclude(entryPart) then local wl = {entryPart} local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true) local lim = (entryPos - origin).Magnitude if exitPart then local promote = false if lastPos then promote = workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true) end if promote then -- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit. hardLimit = lim elseif dist < softLimit then -- Trivial soft limit softLimit = lim end else -- Trivial hard limit hardLimit = lim end end blacklist[#blacklist + 1] = entryPart movingOrigin = entryPos - unitDir*1e-3 end until hardLimit < inf or not entryPart eraseFromEnd(blacklist, originalSize) debug.profileend() return softLimit - nearPlaneZ, hardLimit - nearPlaneZ end local function queryViewport(focus, dist) debug.profilebegin("queryViewport") local fP = focus.p local fX = focus.rightVector local fY = focus.upVector local fZ = -focus.lookVector local viewport = camera.ViewportSize local hardBoxLimit = inf local softBoxLimit = inf -- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits for viewX = 0, 1 do local worldX = fX*((viewX - 0.5)*projX) for viewY = 0, 1 do local worldY = fY*((viewY - 0.5)*projY) local origin = fP + nearPlaneZ*(worldX + worldY) local lastPos = camera:ViewportPointToRay( viewport.x*viewX, viewport.y*viewY ).Origin local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos) if hardPointLimit < hardBoxLimit then hardBoxLimit = hardPointLimit end if softPointLimit < softBoxLimit then softBoxLimit = softPointLimit end end end debug.profileend() return softBoxLimit, hardBoxLimit end local function testPromotion(focus, dist, focusExtrapolation) debug.profilebegin("testPromotion") local fP = focus.p local fX = focus.rightVector local fY = focus.upVector local fZ = -focus.lookVector do -- Dead reckoning the camera rotation and focus debug.profilebegin("extrapolate") local SAMPLE_DT = 0.0625 local SAMPLE_MAX_T = 1.25 local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude -- Metric that decides how many samples to take local combinedSpeed = focusExtrapolation.posVelocity.magnitude for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then return false end end debug.profileend() end do -- Test screen-space offsets from the focus for the presence of soft limits debug.profilebegin("testOffsets") for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do local scaledOffset = offset local pos = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y) if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then return false end end debug.profileend() end debug.profileend() return true end local function Popper(focus, targetDist, focusExtrapolation) debug.profilebegin("popper") subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart local dist = targetDist local soft, hard = queryViewport(focus, targetDist) if hard < dist then dist = hard end if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then dist = soft end subjectRoot = nil debug.profileend() return dist end return Popper end function _ZoomController() local ZOOM_STIFFNESS = 4.5 local ZOOM_DEFAULT = 12.5 local ZOOM_ACCELERATION = 0.0375 local MIN_FOCUS_DIST = 0.5 local DIST_OPAQUE = 1 local Popper = _Popper() local clamp = math.clamp local exp = math.exp local min = math.min local max = math.max local pi = math.pi local cameraMinZoomDistance, cameraMaxZoomDistance do local Player = game:GetService("Players").LocalPlayer local function updateBounds() cameraMinZoomDistance = Player.CameraMinZoomDistance cameraMaxZoomDistance = Player.CameraMaxZoomDistance end updateBounds() Player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(updateBounds) Player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(updateBounds) end local ConstrainedSpring = {} do ConstrainedSpring.__index = ConstrainedSpring function ConstrainedSpring.new(freq, x, minValue, maxValue) x = clamp(x, minValue, maxValue) return setmetatable({ freq = freq, -- Undamped frequency (Hz) x = x, -- Current position v = 0, -- Current velocity minValue = minValue, -- Minimum bound maxValue = maxValue, -- Maximum bound goal = x, -- Goal position }, ConstrainedSpring) end function ConstrainedSpring:Step(dt) local freq = self.freq*2*pi -- Convert from Hz to rad/s local x = self.x local v = self.v local minValue = self.minValue local maxValue = self.maxValue local goal = self.goal -- Solve the spring ODE for position and velocity after time t, assuming critical damping: -- 2*f*x'[t] + x''[t] = f^2*(g - x[t]) -- Knowns are x[0] and x'[0]. -- Solve for x[t] and x'[t]. local offset = goal - x local step = freq*dt local decay = exp(-step) local x1 = goal + (v*dt - offset*(step + 1))*decay local v1 = ((offset*freq - v)*step + v)*decay -- Constrain if x1 < minValue then x1 = minValue v1 = 0 elseif x1 > maxValue then x1 = maxValue v1 = 0 end self.x = x1 self.v = v1 return x1 end end local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, MIN_FOCUS_DIST, cameraMaxZoomDistance) local function stepTargetZoom(z, dz, zoomMin, zoomMax) z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax) if z < DIST_OPAQUE then z = dz <= 0 and zoomMin or DIST_OPAQUE end return z end local zoomDelta = 0 local Zoom = {} do function Zoom.Update(renderDt, focus, extrapolation) local poppedZoom = math.huge if zoomSpring.goal > DIST_OPAQUE then -- Make a pessimistic estimate of zoom distance for this step without accounting for poppercam local maxPossibleZoom = max( zoomSpring.x, stepTargetZoom(zoomSpring.goal, zoomDelta, cameraMinZoomDistance, cameraMaxZoomDistance) ) -- Run the Popper algorithm on the feasible zoom range, [MIN_FOCUS_DIST, maxPossibleZoom] poppedZoom = Popper( focus*CFrame.new(0, 0, MIN_FOCUS_DIST), maxPossibleZoom - MIN_FOCUS_DIST, extrapolation ) + MIN_FOCUS_DIST end zoomSpring.minValue = MIN_FOCUS_DIST zoomSpring.maxValue = min(cameraMaxZoomDistance, poppedZoom) return zoomSpring:Step(renderDt) end function Zoom.SetZoomParameters(targetZoom, newZoomDelta) zoomSpring.goal = targetZoom zoomDelta = newZoomDelta end end return Zoom end function _MouseLockController() --[[ Constants ]]-- local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png" local CONTEXT_ACTION_NAME = "MouseLockSwitchAction" local MOUSELOCK_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value --[[ Services ]]-- local PlayersService = game:GetService("Players") local ContextActionService = game:GetService("ContextActionService") local Settings = UserSettings() -- ignore warning local GameSettings = Settings.GameSettings local Mouse = PlayersService.LocalPlayer:GetMouse() --[[ The Module ]]-- local MouseLockController = {} MouseLockController.__index = MouseLockController function MouseLockController.new() local self = setmetatable({}, MouseLockController) self.isMouseLocked = false self.savedMouseCursor = nil self.boundKeys = {Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift} -- defaults self.mouseLockToggledEvent = Instance.new("BindableEvent") local boundKeysObj = script:FindFirstChild("BoundKeys") if (not boundKeysObj) or (not boundKeysObj:IsA("StringValue")) then -- If object with correct name was found, but it's not a StringValue, destroy and replace if boundKeysObj then boundKeysObj:Destroy() end boundKeysObj = Instance.new("StringValue") boundKeysObj.Name = "BoundKeys" boundKeysObj.Value = "LeftShift,RightShift" boundKeysObj.Parent = script end if boundKeysObj then boundKeysObj.Changed:Connect(function(value) self:OnBoundKeysObjectChanged(value) end) self:OnBoundKeysObjectChanged(boundKeysObj.Value) -- Initial setup call end -- Watch for changes to user's ControlMode and ComputerMovementMode settings and update the feature availability accordingly GameSettings.Changed:Connect(function(property) if property == "ControlMode" or property == "ComputerMovementMode" then self:UpdateMouseLockAvailability() end end) -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly PlayersService.LocalPlayer:GetPropertyChangedSignal("DevEnableMouseLock"):Connect(function() self:UpdateMouseLockAvailability() end) -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly PlayersService.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function() self:UpdateMouseLockAvailability() end) self:UpdateMouseLockAvailability() return self end function MouseLockController:GetIsMouseLocked() return self.isMouseLocked end function MouseLockController:GetBindableToggleEvent() return self.mouseLockToggledEvent.Event end function MouseLockController:GetMouseLockOffset() local offsetValueObj = script:FindFirstChild("CameraOffset") if offsetValueObj and offsetValueObj:IsA("Vector3Value") then return offsetValueObj.Value else -- If CameraOffset object was found but not correct type, destroy if offsetValueObj then offsetValueObj:Destroy() end offsetValueObj = Instance.new("Vector3Value") offsetValueObj.Name = "CameraOffset" offsetValueObj.Value = Vector3.new(1.75,0,0) -- Legacy Default Value offsetValueObj.Parent = script end if offsetValueObj and offsetValueObj.Value then return offsetValueObj.Value end return Vector3.new(1.75,0,0) end function MouseLockController:UpdateMouseLockAvailability() local devAllowsMouseLock = PlayersService.LocalPlayer.DevEnableMouseLock local devMovementModeIsScriptable = PlayersService.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable local userHasMouseLockModeEnabled = GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch local userHasClickToMoveEnabled = GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove local MouseLockAvailable = devAllowsMouseLock and userHasMouseLockModeEnabled and not userHasClickToMoveEnabled and not devMovementModeIsScriptable if MouseLockAvailable~=self.enabled then self:EnableMouseLock(MouseLockAvailable) end end function MouseLockController:OnBoundKeysObjectChanged(newValue) self.boundKeys = {} -- Overriding defaults, note: possibly with nothing at all if boundKeysObj.Value is "" or contains invalid values for token in string.gmatch(newValue,"[^%s,]+") do for _, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do if token == keyEnum.Name then self.boundKeys[#self.boundKeys+1] = keyEnum break end end end self:UnbindContextActions() self:BindContextActions() end --[[ Local Functions ]]-- function MouseLockController:OnMouseLockToggled() self.isMouseLocked = not self.isMouseLocked if self.isMouseLocked then local cursorImageValueObj = script:FindFirstChild("CursorImage") if cursorImageValueObj and cursorImageValueObj:IsA("StringValue") and cursorImageValueObj.Value then self.savedMouseCursor = Mouse.Icon Mouse.Icon = cursorImageValueObj.Value else if cursorImageValueObj then cursorImageValueObj:Destroy() end cursorImageValueObj = Instance.new("StringValue") cursorImageValueObj.Name = "CursorImage" cursorImageValueObj.Value = DEFAULT_MOUSE_LOCK_CURSOR cursorImageValueObj.Parent = script self.savedMouseCursor = Mouse.Icon Mouse.Icon = DEFAULT_MOUSE_LOCK_CURSOR end else if self.savedMouseCursor then Mouse.Icon = self.savedMouseCursor self.savedMouseCursor = nil end end self.mouseLockToggledEvent:Fire() end function MouseLockController:DoMouseLockSwitch(name, state, input) if state == Enum.UserInputState.Begin then self:OnMouseLockToggled() return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass end function MouseLockController:BindContextActions() ContextActionService:BindActionAtPriority(CONTEXT_ACTION_NAME, function(name, state, input) return self:DoMouseLockSwitch(name, state, input) end, false, MOUSELOCK_ACTION_PRIORITY, unpack(self.boundKeys)) end function MouseLockController:UnbindContextActions() ContextActionService:UnbindAction(CONTEXT_ACTION_NAME) end function MouseLockController:IsMouseLocked() return self.enabled and self.isMouseLocked end function MouseLockController:EnableMouseLock(enable) if enable ~= self.enabled then self.enabled = enable if self.enabled then -- Enabling the mode self:BindContextActions() else -- Disabling -- Restore mouse cursor if Mouse.Icon~="" then Mouse.Icon = "" end self:UnbindContextActions() -- If the mode is disabled while being used, fire the event to toggle it off if self.isMouseLocked then self.mouseLockToggledEvent:Fire() end self.isMouseLocked = false end end end return MouseLockController end function _TransparencyController() local MAX_TWEEN_RATE = 2.8 -- per second local Util = _CameraUtils() --[[ The Module ]]-- local TransparencyController = {} TransparencyController.__index = TransparencyController function TransparencyController.new() local self = setmetatable({}, TransparencyController) self.lastUpdate = tick() self.transparencyDirty = false self.enabled = false self.lastTransparency = nil self.descendantAddedConn, self.descendantRemovingConn = nil, nil self.toolDescendantAddedConns = {} self.toolDescendantRemovingConns = {} self.cachedParts = {} return self end function TransparencyController:HasToolAncestor(object) if object.Parent == nil then return false end return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent) end function TransparencyController:IsValidPartToModify(part) if part:IsA('BasePart') or part:IsA('Decal') then return not self:HasToolAncestor(part) end return false end function TransparencyController:CachePartsRecursive(object) if object then if self:IsValidPartToModify(object) then self.cachedParts[object] = true self.transparencyDirty = true end for _, child in pairs(object:GetChildren()) do self:CachePartsRecursive(child) end end end function TransparencyController:TeardownTransparency() for child, _ in pairs(self.cachedParts) do child.LocalTransparencyModifier = 0 end self.cachedParts = {} self.transparencyDirty = true self.lastTransparency = nil if self.descendantAddedConn then self.descendantAddedConn:disconnect() self.descendantAddedConn = nil end if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() self.descendantRemovingConn = nil end for object, conn in pairs(self.toolDescendantAddedConns) do conn:Disconnect() self.toolDescendantAddedConns[object] = nil end for object, conn in pairs(self.toolDescendantRemovingConns) do conn:Disconnect() self.toolDescendantRemovingConns[object] = nil end end function TransparencyController:SetupTransparency(character) self:TeardownTransparency() if self.descendantAddedConn then self.descendantAddedConn:disconnect() end self.descendantAddedConn = character.DescendantAdded:Connect(function(object) -- This is a part we want to invisify if self:IsValidPartToModify(object) then self.cachedParts[object] = true self.transparencyDirty = true -- There is now a tool under the character elseif object:IsA('Tool') then if self.toolDescendantAddedConns[object] then self.toolDescendantAddedConns[object]:Disconnect() end self.toolDescendantAddedConns[object] = object.DescendantAdded:Connect(function(toolChild) self.cachedParts[toolChild] = nil if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then -- Reset the transparency toolChild.LocalTransparencyModifier = 0 end end) if self.toolDescendantRemovingConns[object] then self.toolDescendantRemovingConns[object]:disconnect() end self.toolDescendantRemovingConns[object] = object.DescendantRemoving:Connect(function(formerToolChild) wait() -- wait for new parent if character and formerToolChild and formerToolChild:IsDescendantOf(character) then if self:IsValidPartToModify(formerToolChild) then self.cachedParts[formerToolChild] = true self.transparencyDirty = true end end end) end end) if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() end self.descendantRemovingConn = character.DescendantRemoving:connect(function(object) if self.cachedParts[object] then self.cachedParts[object] = nil -- Reset the transparency object.LocalTransparencyModifier = 0 end end) self:CachePartsRecursive(character) end function TransparencyController:Enable(enable) if self.enabled ~= enable then self.enabled = enable self:Update() end end function TransparencyController:SetSubject(subject) local character = nil if subject and subject:IsA("Humanoid") then character = subject.Parent end if subject and subject:IsA("VehicleSeat") and subject.Occupant then character = subject.Occupant.Parent end if character then self:SetupTransparency(character) else self:TeardownTransparency() end end function TransparencyController:Update() local instant = false local now = tick() local currentCamera = workspace.CurrentCamera if currentCamera then local transparency = 0 if not self.enabled then instant = true else local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude transparency = (distance<2) and (1.0-(distance-0.5)/1.5) or 0 --(7 - distance) / 5 if transparency < 0.5 then transparency = 0 end if self.lastTransparency then local deltaTransparency = transparency - self.lastTransparency -- Don't tween transparency if it is instant or your character was fully invisible last frame if not instant and transparency < 1 and self.lastTransparency < 0.95 then local maxDelta = MAX_TWEEN_RATE * (now - self.lastUpdate) deltaTransparency = math.clamp(deltaTransparency, -maxDelta, maxDelta) end transparency = self.lastTransparency + deltaTransparency else self.transparencyDirty = true end transparency = math.clamp(Util.Round(transparency, 2), 0, 1) end if self.transparencyDirty or self.lastTransparency ~= transparency then for child, _ in pairs(self.cachedParts) do child.LocalTransparencyModifier = transparency end self.transparencyDirty = false self.lastTransparency = transparency end end self.lastUpdate = now end return TransparencyController end function _Poppercam() local ZoomController = _ZoomController() local TransformExtrapolator = {} do TransformExtrapolator.__index = TransformExtrapolator local CF_IDENTITY = CFrame.new() local function cframeToAxis(cframe) local axis, angle = cframe:toAxisAngle() return axis*angle end local function axisToCFrame(axis) local angle = axis.magnitude if angle > 1e-5 then return CFrame.fromAxisAngle(axis, angle) end return CF_IDENTITY end local function extractRotation(cf) local _, _, _, xx, yx, zx, xy, yy, zy, xz, yz, zz = cf:components() return CFrame.new(0, 0, 0, xx, yx, zx, xy, yy, zy, xz, yz, zz) end function TransformExtrapolator.new() return setmetatable({ lastCFrame = nil, }, TransformExtrapolator) end function TransformExtrapolator:Step(dt, currentCFrame) local lastCFrame = self.lastCFrame or currentCFrame self.lastCFrame = currentCFrame local currentPos = currentCFrame.p local currentRot = extractRotation(currentCFrame) local lastPos = lastCFrame.p local lastRot = extractRotation(lastCFrame) -- Estimate velocities from the delta between now and the last frame -- This estimation can be a little noisy. local dp = (currentPos - lastPos)/dt local dr = cframeToAxis(currentRot*lastRot:inverse())/dt local function extrapolate(t) local p = dp*t + currentPos local r = axisToCFrame(dr*t)*currentRot return r + p end return { extrapolate = extrapolate, posVelocity = dp, rotVelocity = dr, } end function TransformExtrapolator:Reset() self.lastCFrame = nil end end --[[ The Module ]]-- local BaseOcclusion = _BaseOcclusion() local Poppercam = setmetatable({}, BaseOcclusion) Poppercam.__index = Poppercam function Poppercam.new() local self = setmetatable(BaseOcclusion.new(), Poppercam) self.focusExtrapolator = TransformExtrapolator.new() return self end function Poppercam:GetOcclusionMode() return Enum.DevCameraOcclusionMode.Zoom end function Poppercam:Enable(enable) self.focusExtrapolator:Reset() end function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController) local rotatedFocus = CFrame.new(desiredCameraFocus.p, desiredCameraCFrame.p)*CFrame.new( 0, 0, 0, -1, 0, 0, 0, 1, 0, 0, 0, -1 ) local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus) local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation) return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus end -- Called when character is added function Poppercam:CharacterAdded(character, player) end -- Called when character is about to be removed function Poppercam:CharacterRemoving(character, player) end function Poppercam:OnCameraSubjectChanged(newSubject) end local ZoomController = _ZoomController() function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController) local rotatedFocus = desiredCameraFocus * (desiredCameraCFrame - desiredCameraCFrame.p) local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus) local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation) return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus end return Poppercam end function _Invisicam() --[[ Top Level Roblox Services ]]-- local PlayersService = game:GetService("Players") --[[ Constants ]]-- local ZERO_VECTOR3 = Vector3.new(0,0,0) local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays local MODE = { --CUSTOM = 1, -- Retired, unused LIMBS = 2, -- Track limbs MOVEMENT = 3, -- Track movement CORNERS = 4, -- Char model corners CIRCLE1 = 5, -- Circle of casts around character CIRCLE2 = 6, -- Circle of casts around character, camera relative LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode SMART_CIRCLE = 8, -- More sample points on and around character CHAR_OUTLINE = 9, -- Dynamic outline around the character } local LIMB_TRACKING_SET = { -- Body parts common to R15 and R6 ['Head'] = true, -- Body parts unique to R6 ['Left Arm'] = true, ['Right Arm'] = true, ['Left Leg'] = true, ['Right Leg'] = true, -- Body parts unique to R15 ['LeftLowerArm'] = true, ['RightLowerArm'] = true, ['LeftUpperLeg'] = true, ['RightUpperLeg'] = true } local CORNER_FACTORS = { Vector3.new(1,1,-1), Vector3.new(1,-1,-1), Vector3.new(-1,-1,-1), Vector3.new(-1,1,-1) } local CIRCLE_CASTS = 10 local MOVE_CASTS = 3 local SMART_CIRCLE_CASTS = 24 local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS local CHAR_OUTLINE_CASTS = 24 -- Used to sanitize user-supplied functions local function AssertTypes(param, ...) local allowedTypes = {} local typeString = '' for _, typeName in pairs({...}) do allowedTypes[typeName] = true typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName end local theType = type(param) assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType) end -- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons local function Det3x3(a,b,c,d,e,f,g,h,i) return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g)) end -- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane -- because they are generated from cross products with a common vector. This function is computing -- that intersection, but it's actually the general solution for the point halfway between where -- two skew lines come nearest to each other, which is more forgiving. local function RayIntersection(p0, v0, p1, v1) local v2 = v0:Cross(v1) local d1 = p1.x - p0.x local d2 = p1.y - p0.y local d3 = p1.z - p0.z local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z) if (denom == 0) then return ZERO_VECTOR3 -- No solution (rays are parallel) end local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom local s0 = p0 + t0 * v0 local s1 = p1 + t1 * v1 local s = s0 + 0.5 * ( s1 - s0 ) -- 0.25 studs is a threshold for deciding if the rays are -- close enough to be considered intersecting, found through testing if (s1-s0).Magnitude < 0.25 then return s else return ZERO_VECTOR3 end end --[[ The Module ]]-- local BaseOcclusion = _BaseOcclusion() local Invisicam = setmetatable({}, BaseOcclusion) Invisicam.__index = Invisicam function Invisicam.new() local self = setmetatable(BaseOcclusion.new(), Invisicam) self.char = nil self.humanoidRootPart = nil self.torsoPart = nil self.headPart = nil self.childAddedConn = nil self.childRemovedConn = nil self.behaviors = {} -- Map of modes to behavior fns self.behaviors[MODE.LIMBS] = self.LimbBehavior self.behaviors[MODE.MOVEMENT] = self.MoveBehavior self.behaviors[MODE.CORNERS] = self.CornerBehavior self.behaviors[MODE.CIRCLE1] = self.CircleBehavior self.behaviors[MODE.CIRCLE2] = self.CircleBehavior self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior self.mode = MODE.SMART_CIRCLE self.behaviorFunction = self.SmartCircleBehavior self.savedHits = {} -- Objects currently being faded in/out self.trackedLimbs = {} -- Used in limb-tracking casting modes self.camera = game.Workspace.CurrentCamera self.enabled = false return self end function Invisicam:Enable(enable) self.enabled = enable if not enable then self:Cleanup() end end function Invisicam:GetOcclusionMode() return Enum.DevCameraOcclusionMode.Invisicam end --[[ Module functions ]]-- function Invisicam:LimbBehavior(castPoints) for limb, _ in pairs(self.trackedLimbs) do castPoints[#castPoints + 1] = limb.Position end end function Invisicam:MoveBehavior(castPoints) for i = 1, MOVE_CASTS do local position, velocity = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2 local offsetVector = (i - 1) * self.humanoidRootPart.CFrame.lookVector * horizontalSpeed castPoints[#castPoints + 1] = position + offsetVector end end function Invisicam:CornerBehavior(castPoints) local cframe = self.humanoidRootPart.CFrame local centerPoint = cframe.p local rotation = cframe - centerPoint local halfSize = self.char:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations castPoints[#castPoints + 1] = centerPoint for i = 1, #CORNER_FACTORS do castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i])) end end function Invisicam:CircleBehavior(castPoints) local cframe if self.mode == MODE.CIRCLE1 then cframe = self.humanoidRootPart.CFrame else local camCFrame = self.camera.CoordinateFrame cframe = camCFrame - camCFrame.p + self.humanoidRootPart.Position end castPoints[#castPoints + 1] = cframe.p for i = 0, CIRCLE_CASTS - 1 do local angle = (2 * math.pi / CIRCLE_CASTS) * i local offset = 3 * Vector3.new(math.cos(angle), math.sin(angle), 0) castPoints[#castPoints + 1] = cframe * offset end end function Invisicam:LimbMoveBehavior(castPoints) self:LimbBehavior(castPoints) self:MoveBehavior(castPoints) end function Invisicam:CharacterOutlineBehavior(castPoints) local torsoUp = self.torsoPart.CFrame.upVector.unit local torsoRight = self.torsoPart.CFrame.rightVector.unit -- Torso cross of points for interior coverage castPoints[#castPoints + 1] = self.torsoPart.CFrame.p castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight if self.headPart then castPoints[#castPoints + 1] = self.headPart.CFrame.p end local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z)) local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position) local partsWhitelist = {self.torsoPart} if self.headPart then partsWhitelist[#partsWhitelist + 1] = self.headPart end for i = 1, CHAR_OUTLINE_CASTS do local angle = (2 * math.pi * i / CHAR_OUTLINE_CASTS) local offset = cframe * (3 * Vector3.new(math.cos(angle), math.sin(angle), 0)) offset = Vector3.new(offset.X, math.max(offset.Y, -2.25), offset.Z) local ray = Ray.new(centerPoint + offset, -3 * offset) local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false) if hit then -- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against -- walls is less likely to cause a transparency glitch castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit end end end function Invisicam:SmartCircleBehavior(castPoints) local torsoUp = self.torsoPart.CFrame.upVector.unit local torsoRight = self.torsoPart.CFrame.rightVector.unit -- SMART_CIRCLE mode includes rays to head and 5 to the torso. -- Hands, arms, legs and feet are not included since they -- are not canCollide and can therefore go inside of parts castPoints[#castPoints + 1] = self.torsoPart.CFrame.p castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight if self.headPart then castPoints[#castPoints + 1] = self.headPart.CFrame.p end local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position) local radius = 2.5 -- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the -- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within -- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that -- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the -- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are -- behind, below or beside the character and not really obstructing view of the character. This minimizes -- the undesirable situation where the character walks up to an exterior wall and it is made invisible even -- though it is behind the character. for i = 1, SMART_CIRCLE_CASTS do local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi local offset = radius * Vector3.new(math.cos(angle), math.sin(angle), 0) local circlePoint = torsoPoint + cameraOrientation * offset -- Vector from camera to point on the circle being tested local vp = circlePoint - self.camera.CFrame.p local ray = Ray.new(torsoPoint, circlePoint - torsoPoint) local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false ) local castPoint = circlePoint if hit then local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface local v0 = hprime - torsoPoint -- Vector from torso to offset hit point local perp = (v0:Cross(vp)).unit -- Vector from the offset hit point, along the hit surface local v1 = (perp:Cross(hitNormal)).unit -- Vector from camera to offset hit local vprime = (hprime - self.camera.CFrame.p).unit -- This dot product checks to see if the vector along the hit surface would hit the correct -- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then castPoint = RayIntersection(hprime, v1, circlePoint, vp) if castPoint.Magnitude > 0 then local ray = Ray.new(hprime, castPoint - hprime) local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false ) if hit then local hprime2 = hitPoint + 0.1 * hitNormal.unit castPoint = hprime2 end else castPoint = hprime end else castPoint = hprime end local ray = Ray.new(torsoPoint, (castPoint - torsoPoint)) local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false ) if hit then local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit castPoint = castPoint2 end end castPoints[#castPoints + 1] = castPoint end end function Invisicam:CheckTorsoReference() if self.char then self.torsoPart = self.char:FindFirstChild("Torso") if not self.torsoPart then self.torsoPart = self.char:FindFirstChild("UpperTorso") if not self.torsoPart then self.torsoPart = self.char:FindFirstChild("HumanoidRootPart") end end self.headPart = self.char:FindFirstChild("Head") end end function Invisicam:CharacterAdded(char, player) -- We only want the LocalPlayer's character if player~=PlayersService.LocalPlayer then return end if self.childAddedConn then self.childAddedConn:Disconnect() self.childAddedConn = nil end if self.childRemovedConn then self.childRemovedConn:Disconnect() self.childRemovedConn = nil end self.char = char self.trackedLimbs = {} local function childAdded(child) if child:IsA("BasePart") then if LIMB_TRACKING_SET[child.Name] then self.trackedLimbs[child] = true end if child.Name == "Torso" or child.Name == "UpperTorso" then self.torsoPart = child end if child.Name == "Head" then self.headPart = child end end end local function childRemoved(child) self.trackedLimbs[child] = nil -- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use self:CheckTorsoReference() end self.childAddedConn = char.ChildAdded:Connect(childAdded) self.childRemovedConn = char.ChildRemoved:Connect(childRemoved) for _, child in pairs(self.char:GetChildren()) do childAdded(child) end end function Invisicam:SetMode(newMode) AssertTypes(newMode, 'number') for _, modeNum in pairs(MODE) do if modeNum == newMode then self.mode = newMode self.behaviorFunction = self.behaviors[self.mode] return end end error("Invalid mode number") end function Invisicam:GetObscuredParts() return self.savedHits end -- Want to turn off Invisicam? Be sure to call this after. function Invisicam:Cleanup() for hit, originalFade in pairs(self.savedHits) do hit.LocalTransparencyModifier = originalFade end end function Invisicam:Update(dt, desiredCameraCFrame, desiredCameraFocus) -- Bail if there is no Character if not self.enabled or not self.char then return desiredCameraCFrame, desiredCameraFocus end self.camera = game.Workspace.CurrentCamera -- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference -- Make sure we still have a HumanoidRootPart if not self.humanoidRootPart then local humanoid = self.char:FindFirstChildOfClass("Humanoid") if humanoid and humanoid.RootPart then self.humanoidRootPart = humanoid.RootPart else -- Not set up with Humanoid? Try and see if there's one in the Character at all: self.humanoidRootPart = self.char:FindFirstChild("HumanoidRootPart") if not self.humanoidRootPart then -- Bail out, since we're relying on HumanoidRootPart existing return desiredCameraCFrame, desiredCameraFocus end end -- TODO: Replace this with something more sensible local ancestryChangedConn ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent) if child == self.humanoidRootPart and not parent then self.humanoidRootPart = nil if ancestryChangedConn and ancestryChangedConn.Connected then ancestryChangedConn:Disconnect() ancestryChangedConn = nil end end end) end if not self.torsoPart then self:CheckTorsoReference() if not self.torsoPart then -- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso return desiredCameraCFrame, desiredCameraFocus end end -- Make a list of world points to raycast to local castPoints = {} self.behaviorFunction(self, castPoints) -- Cast to get a list of objects between the camera and the cast points local currentHits = {} local ignoreList = {self.char} local function add(hit) currentHits[hit] = true if not self.savedHits[hit] then self.savedHits[hit] = hit.LocalTransparencyModifier end end local hitParts local hitPartCount = 0 -- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays -- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled local headTorsoRayHitParts = {} local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY local perPartTransparencyOtherHits = TARGET_TRANSPARENCY if USE_STACKING_TRANSPARENCY then -- This first call uses head and torso rays to find out how many parts are stacked up -- for the purpose of calculating required per-part transparency local headPoint = self.headPart and self.headPart.CFrame.p or castPoints[1] local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2] hitParts = self.camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList) -- Count how many things the sample rays passed through, including decals. This should only -- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals, -- so my compromise for now is to just let any decal increase the part count by 1. Only one -- decal per part will be considered. for i = 1, #hitParts do local hitPart = hitParts[i] hitPartCount = hitPartCount + 1 -- count the part itself headTorsoRayHitParts[hitPart] = true for _, child in pairs(hitPart:GetChildren()) do if child:IsA('Decal') or child:IsA('Texture') then hitPartCount = hitPartCount + 1 -- count first decal hit, then break break end end end if (hitPartCount > 0) then perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount ) perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount ) end end -- Now get all the parts hit by all the rays hitParts = self.camera:GetPartsObscuringTarget(castPoints, ignoreList) local partTargetTransparency = {} -- Include decals and textures for i = 1, #hitParts do local hitPart = hitParts[i] partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits -- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of -- parts to be modified by invisicam if hitPart.Transparency < partTargetTransparency[hitPart] then add(hitPart) end -- Check all decals and textures on the part for _, child in pairs(hitPart:GetChildren()) do if child:IsA('Decal') or child:IsA('Texture') then if (child.Transparency < partTargetTransparency[hitPart]) then partTargetTransparency[child] = partTargetTransparency[hitPart] add(child) end end end end -- Invisibilize objects that are in the way, restore those that aren't anymore for hitPart, originalLTM in pairs(self.savedHits) do if currentHits[hitPart] then -- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0 else -- Restore original pre-invisicam value of LTM hitPart.LocalTransparencyModifier = originalLTM self.savedHits[hitPart] = nil end end -- Invisicam does not change the camera values return desiredCameraCFrame, desiredCameraFocus end return Invisicam end function _LegacyCamera() local ZERO_VECTOR2 = Vector2.new(0,0) local Util = _CameraUtils() --[[ Services ]]-- local PlayersService = game:GetService('Players') --[[ The Module ]]-- local BaseCamera = _BaseCamera() local LegacyCamera = setmetatable({}, BaseCamera) LegacyCamera.__index = LegacyCamera function LegacyCamera.new() local self = setmetatable(BaseCamera.new(), LegacyCamera) self.cameraType = Enum.CameraType.Fixed self.lastUpdate = tick() self.lastDistanceToSubject = nil return self end function LegacyCamera:GetModuleName() return "LegacyCamera" end --[[ Functions overridden from BaseCamera ]]-- function LegacyCamera:SetCameraToSubjectDistance(desiredSubjectDistance) return BaseCamera.SetCameraToSubjectDistance(self,desiredSubjectDistance) end function LegacyCamera:Update(dt) -- Cannot update until cameraType has been set if not self.cameraType then return end local now = tick() local timeDelta = (now - self.lastUpdate) local camera = workspace.CurrentCamera local newCameraCFrame = camera.CFrame local newCameraFocus = camera.Focus local player = PlayersService.LocalPlayer if self.lastUpdate == nil or timeDelta > 1 then self.lastDistanceToSubject = nil end local subjectPosition = self:GetSubjectPosition() if self.cameraType == Enum.CameraType.Fixed then if self.lastUpdate then -- 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, now - self.lastUpdate) local gamepadRotation = self:UpdateGamepad() self.rotateInput = self.rotateInput + (gamepadRotation * delta) end if subjectPosition and player and camera then local distanceToSubject = self:GetCameraToSubjectDistance() local newLookVector = self:CalculateNewLookVector() self.rotateInput = ZERO_VECTOR2 newCameraFocus = camera.Focus -- Fixed camera does not change focus newCameraCFrame = CFrame.new(camera.CFrame.p, camera.CFrame.p + (distanceToSubject * newLookVector)) end elseif self.cameraType == Enum.CameraType.Attach then if subjectPosition and camera then local distanceToSubject = self:GetCameraToSubjectDistance() local humanoid = self:GetHumanoid() if self.lastUpdate and humanoid and humanoid.RootPart then -- 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, now - self.lastUpdate) local gamepadRotation = self:UpdateGamepad() self.rotateInput = self.rotateInput + (gamepadRotation * delta) local forwardVector = humanoid.RootPart.CFrame.lookVector local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector()) if Util.IsFinite(y) then -- Preserve vertical rotation from user input self.rotateInput = Vector2.new(y, self.rotateInput.Y) end end local newLookVector = self:CalculateNewLookVector() self.rotateInput = ZERO_VECTOR2 newCameraFocus = CFrame.new(subjectPosition) newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition) end elseif self.cameraType == Enum.CameraType.Watch then if subjectPosition and player and camera then local cameraLook = nil local humanoid = self:GetHumanoid() if humanoid and humanoid.RootPart then local diffVector = subjectPosition - camera.CFrame.p cameraLook = diffVector.unit if self.lastDistanceToSubject and self.lastDistanceToSubject == self:GetCameraToSubjectDistance() then -- Don't clobber the zoom if they zoomed the camera local newDistanceToSubject = diffVector.magnitude self:SetCameraToSubjectDistance(newDistanceToSubject) end end local distanceToSubject = self:GetCameraToSubjectDistance() local newLookVector = self:CalculateNewLookVector(cameraLook) self.rotateInput = ZERO_VECTOR2 newCameraFocus = CFrame.new(subjectPosition) newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition) self.lastDistanceToSubject = distanceToSubject end else -- Unsupported type, return current values unchanged return camera.CFrame, camera.Focus end self.lastUpdate = now return newCameraCFrame, newCameraFocus end return LegacyCamera end function _OrbitalCamera() -- Local private variables and constants local UNIT_Z = Vector3.new(0,0,1) local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane local ZERO_VECTOR3 = Vector3.new(0,0,0) local ZERO_VECTOR2 = Vector2.new(0,0) local TAU = 2 * math.pi --[[ Gamepad Support ]]-- local THUMBSTICK_DEADZONE = 0.2 -- Do not edit these values, they are not the developer-set limits, they are limits -- to the values the camera system equations can correctly handle local MIN_ALLOWED_ELEVATION_DEG = -80 local MAX_ALLOWED_ELEVATION_DEG = 80 local externalProperties = {} externalProperties["InitialDistance"] = 25 externalProperties["MinDistance"] = 10 externalProperties["MaxDistance"] = 100 externalProperties["InitialElevation"] = 35 externalProperties["MinElevation"] = 35 externalProperties["MaxElevation"] = 35 externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default local Util = _CameraUtils() --[[ Services ]]-- local PlayersService = game:GetService('Players') local VRService = game:GetService("VRService") --[[ The Module ]]-- local BaseCamera = _BaseCamera() local OrbitalCamera = setmetatable({}, BaseCamera) OrbitalCamera.__index = OrbitalCamera function OrbitalCamera.new() local self = setmetatable(BaseCamera.new(), OrbitalCamera) self.lastUpdate = tick() -- OrbitalCamera-specific members self.changedSignalConnections = {} self.refAzimuthRad = nil self.curAzimuthRad = nil self.minAzimuthAbsoluteRad = nil self.maxAzimuthAbsoluteRad = nil self.useAzimuthLimits = nil self.curElevationRad = nil self.minElevationRad = nil self.maxElevationRad = nil self.curDistance = nil self.minDistance = nil self.maxDistance = nil -- Gamepad self.r3ButtonDown = false self.l3ButtonDown = false self.gamepadDollySpeedMultiplier = 1 self.lastUserPanCamera = tick() self.externalProperties = {} self.externalProperties["InitialDistance"] = 25 self.externalProperties["MinDistance"] = 10 self.externalProperties["MaxDistance"] = 100 self.externalProperties["InitialElevation"] = 35 self.externalProperties["MinElevation"] = 35 self.externalProperties["MaxElevation"] = 35 self.externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally self.externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above self.externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above self.externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default self:LoadNumberValueParameters() return self end function OrbitalCamera:LoadOrCreateNumberValueParameter(name, valueType, updateFunction) local valueObj = script:FindFirstChild(name) if valueObj and valueObj:isA(valueType) then -- Value object exists and is the correct type, use its value self.externalProperties[name] = valueObj.Value elseif self.externalProperties[name] ~= nil then -- Create missing (or replace incorrectly-typed) valueObject with default value valueObj = Instance.new(valueType) valueObj.Name = name valueObj.Parent = script valueObj.Value = self.externalProperties[name] else print("externalProperties table has no entry for ",name) return end if updateFunction then if self.changedSignalConnections[name] then self.changedSignalConnections[name]:Disconnect() end self.changedSignalConnections[name] = valueObj.Changed:Connect(function(newValue) self.externalProperties[name] = newValue updateFunction(self) end) end end function OrbitalCamera:SetAndBoundsCheckAzimuthValues() self.minAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(self.externalProperties["CWAzimuthTravel"])) self.maxAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(self.externalProperties["CCWAzimuthTravel"])) self.useAzimuthLimits = self.externalProperties["UseAzimuthLimits"] if self.useAzimuthLimits then self.curAzimuthRad = math.max(self.curAzimuthRad, self.minAzimuthAbsoluteRad) self.curAzimuthRad = math.min(self.curAzimuthRad, self.maxAzimuthAbsoluteRad) end end function OrbitalCamera:SetAndBoundsCheckElevationValues() -- These degree values are the direct user input values. It is deliberate that they are -- ranged checked only against the extremes, and not against each other. Any time one -- is changed, both of the internal values in radians are recalculated. This allows for -- A developer to change the values in any order and for the end results to be that the -- internal values adjust to match intent as best as possible. local minElevationDeg = math.max(self.externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG) local maxElevationDeg = math.min(self.externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG) -- Set internal values in radians self.minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg)) self.maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg)) self.curElevationRad = math.max(self.curElevationRad, self.minElevationRad) self.curElevationRad = math.min(self.curElevationRad, self.maxElevationRad) end function OrbitalCamera:SetAndBoundsCheckDistanceValues() self.minDistance = self.externalProperties["MinDistance"] self.maxDistance = self.externalProperties["MaxDistance"] self.curDistance = math.max(self.curDistance, self.minDistance) self.curDistance = math.min(self.curDistance, self.maxDistance) end -- This loads from, or lazily creates, NumberValue objects for exposed parameters function OrbitalCamera:LoadNumberValueParameters() -- These initial values do not require change listeners since they are read only once self:LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil) self:LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil) -- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits self:LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", self.SetAndBoundsCheckAzimuthValue) self:LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues) self:LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues) self:LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", self.SetAndBoundsCheckElevationValues) self:LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", self.SetAndBoundsCheckElevationValues) self:LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues) self:LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues) self:LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", self.SetAndBoundsCheckAzimuthValues) -- Internal values set (in radians, from degrees), plus sanitization self.curAzimuthRad = math.rad(self.externalProperties["ReferenceAzimuth"]) self.curElevationRad = math.rad(self.externalProperties["InitialElevation"]) self.curDistance = self.externalProperties["InitialDistance"] self:SetAndBoundsCheckAzimuthValues() self:SetAndBoundsCheckElevationValues() self:SetAndBoundsCheckDistanceValues() end function OrbitalCamera:GetModuleName() return "OrbitalCamera" end function OrbitalCamera:SetInitialOrientation(humanoid) if not humanoid or not humanoid.RootPart then warn("OrbitalCamera could not set initial orientation due to missing humanoid") return end local newDesiredLook = (humanoid.RootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, self:GetCameraLookVector()) local vertShift = math.asin(self:GetCameraLookVector().y) - math.asin(newDesiredLook.y) if not Util.IsFinite(horizontalShift) then horizontalShift = 0 end if not Util.IsFinite(vertShift) then vertShift = 0 end self.rotateInput = Vector2.new(horizontalShift, vertShift) end --[[ Functions of BaseCamera that are overridden by OrbitalCamera ]]-- function OrbitalCamera:GetCameraToSubjectDistance() return self.curDistance end function OrbitalCamera:SetCameraToSubjectDistance(desiredSubjectDistance) print("OrbitalCamera SetCameraToSubjectDistance ",desiredSubjectDistance) local player = PlayersService.LocalPlayer if player then self.currentSubjectDistance = math.clamp(desiredSubjectDistance, self.minDistance, self.maxDistance) -- OrbitalCamera is not allowed to go into the first-person range self.currentSubjectDistance = math.max(self.currentSubjectDistance, self.FIRST_PERSON_DISTANCE_THRESHOLD) end self.inFirstPerson = false self:UpdateMouseBehavior() return self.currentSubjectDistance end function OrbitalCamera:CalculateNewLookVector(suppliedLookVector, xyRotateVector) local currLookVector = suppliedLookVector or self:GetCameraLookVector() local currPitchAngle = math.asin(currLookVector.y) local yTheta = math.clamp(xyRotateVector.y, currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG)) local constrainedRotateInput = Vector2.new(xyRotateVector.x, yTheta) local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) local newLookVector = (CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)).lookVector return newLookVector end function OrbitalCamera:GetGamepadPan(name, state, input) if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then if self.r3ButtonDown or self.l3ButtonDown then -- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out if (input.Position.Y > THUMBSTICK_DEADZONE) then self.gamepadDollySpeedMultiplier = 0.96 elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then self.gamepadDollySpeedMultiplier = 1.04 else self.gamepadDollySpeedMultiplier = 1.00 end else if state == Enum.UserInputState.Cancel then self.gamepadPanningCamera = ZERO_VECTOR2 return end local inputVector = Vector2.new(input.Position.X, -input.Position.Y) if inputVector.magnitude > THUMBSTICK_DEADZONE then self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y) else self.gamepadPanningCamera = ZERO_VECTOR2 end end return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass end function OrbitalCamera:DoGamepadZoom(name, state, input) if input.UserInputType == self.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then if (state == Enum.UserInputState.Begin) then self.r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3 self.l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3 elseif (state == Enum.UserInputState.End) then if (input.KeyCode == Enum.KeyCode.ButtonR3) then self.r3ButtonDown = false elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then self.l3ButtonDown = false end if (not self.r3ButtonDown) and (not self.l3ButtonDown) then self.gamepadDollySpeedMultiplier = 1.00 end end return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass end function OrbitalCamera:BindGamepadInputActions() self:BindAction("OrbitalCamGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2) self:BindAction("OrbitalCamGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonL3) end -- [[ Update ]]-- function OrbitalCamera:Update(dt) local now = tick() local timeDelta = (now - self.lastUpdate) local userPanningTheCamera = (self.UserPanningTheCamera == true) local camera = workspace.CurrentCamera local newCameraCFrame = camera.CFrame local newCameraFocus = camera.Focus local player = PlayersService.LocalPlayer local cameraSubject = camera and camera.CameraSubject local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform') if self.lastUpdate == nil or timeDelta > 1 then self.lastCameraTransform = nil end if self.lastUpdate 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 userPanningTheCamera = true 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) userPanningTheCamera = true end end end -- Reset tween speed if user is panning if userPanningTheCamera then self.lastUserPanCamera = tick() end local subjectPosition = self:GetSubjectPosition() if subjectPosition and player and camera then -- Process any dollying being done by gamepad -- TODO: Move this if self.gamepadDollySpeedMultiplier ~= 1 then self:SetCameraToSubjectDistance(self.currentSubjectDistance * self.gamepadDollySpeedMultiplier) end local VREnabled = VRService.VREnabled newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition) local cameraFocusP = newCameraFocus.p if VREnabled and not self:IsInFirstPerson() then local cameraHeight = self:GetCameraHeight() local vecToSubject = (subjectPosition - camera.CFrame.p) local distToSubject = vecToSubject.magnitude -- Only move the camera if it exceeded a maximum distance to the subject in VR if distToSubject > self.currentSubjectDistance or self.rotateInput.x ~= 0 then local desiredDist = math.min(distToSubject, self.currentSubjectDistance) -- Note that CalculateNewLookVector is overridden from BaseCamera vecToSubject = self:CalculateNewLookVector(vecToSubject.unit * X1_Y0_Z1, Vector2.new(self.rotateInput.x, 0)) * desiredDist local newPos = cameraFocusP - vecToSubject local desiredLookDir = camera.CFrame.lookVector if self.rotateInput.x ~= 0 then desiredLookDir = vecToSubject end local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z) self.RotateInput = ZERO_VECTOR2 newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0) end else -- self.RotateInput is a Vector2 of mouse movement deltas since last update self.curAzimuthRad = self.curAzimuthRad - self.rotateInput.x if self.useAzimuthLimits then self.curAzimuthRad = math.clamp(self.curAzimuthRad, self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad) else self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0 end self.curElevationRad = math.clamp(self.curElevationRad + self.rotateInput.y, self.minElevationRad, self.maxElevationRad) local cameraPosVector = self.currentSubjectDistance * ( CFrame.fromEulerAnglesYXZ( -self.curElevationRad, self.curAzimuthRad, 0 ) * UNIT_Z ) local camPos = subjectPosition + cameraPosVector newCameraCFrame = CFrame.new(camPos, subjectPosition) self.rotateInput = ZERO_VECTOR2 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 return OrbitalCamera end function _ClassicCamera() -- 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.0 --Seconds, used when auto-aligning camera with vehicles local INITIAL_CAMERA_ANGLE = CFrame.fromOrientation(math.rad(-15), 0, 0) local FFlagUserCameraToggle do local success, result = pcall(function() return UserSettings():IsUserFeatureEnabled("UserCameraToggle") end) FFlagUserCameraToggle = success and result end --[[ Services ]]-- local PlayersService = game:GetService('Players') local VRService = game:GetService("VRService") local CameraInput = _CameraInput() local Util = _CameraUtils() --[[ The Module ]]-- local BaseCamera = _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 if self.lastUpdate 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 self.userPanningTheCamera then tweenSpeed = 0 self.lastUserPanCamera = tick() 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 = self:CalculateNewLookCFrame(overrideCameraLookVector) 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 if not self.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 self.rotateInput = self.rotateInput + Vector2.new(y, 0) end tweenSpeed = 0 end elseif not userRecentlyPannedCamera then local forwardVector = humanoid.Torso.CFrame.lookVector if isOnASkateboard 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 self.rotateInput = self.rotateInput + Vector2.new(y * percent, 0) 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 self.rotateInput = self.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 -- Only move the camera if it exceeded a maximum distance to the subject in VR if distToSubject > zoom or self.rotateInput.x ~= 0 then local desiredDist = math.min(distToSubject, zoom) vecToSubject = self:CalculateNewLookVectorVR() * desiredDist local newPos = cameraFocusP - vecToSubject local desiredLookDir = camera.CFrame.lookVector if self.rotateInput.x ~= 0 then desiredLookDir = vecToSubject end local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z) self.rotateInput = ZERO_VECTOR2 newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0) end else local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector) self.rotateInput = ZERO_VECTOR2 newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP) end else -- is FollowCamera local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector) self.rotateInput = ZERO_VECTOR2 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 end function _CameraUtils() local CameraUtils = {} local FFlagUserCameraToggle do local success, result = pcall(function() return UserSettings():IsUserFeatureEnabled("UserCameraToggle") end) FFlagUserCameraToggle = success and result end local function round(num) return math.floor(num + 0.5) end -- Critically damped spring class for fluid motion effects local Spring = {} do Spring.__index = Spring -- Initialize to a given undamped frequency and default position function Spring.new(freq, pos) return setmetatable({ freq = freq, goal = pos, pos = pos, vel = 0, }, Spring) end -- Advance the spring simulation by `dt` seconds function Spring:step(dt) local f = self.freq*2*math.pi local g = self.goal local p0 = self.pos local v0 = self.vel local offset = p0 - g local decay = math.exp(-f*dt) local p1 = (offset*(1 + f*dt) + v0*dt)*decay + g local v1 = (v0*(1 - f*dt) - offset*(f*f*dt))*decay self.pos = p1 self.vel = v1 return p1 end end CameraUtils.Spring = Spring -- map a value from one range to another function CameraUtils.map(x, inMin, inMax, outMin, outMax) return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin end -- From TransparencyController function CameraUtils.Round(num, places) local decimalPivot = 10^places return math.floor(num * decimalPivot + 0.5) / decimalPivot end function CameraUtils.IsFinite(val) return val == val and val ~= math.huge and val ~= -math.huge end function CameraUtils.IsFiniteVector3(vec3) return CameraUtils.IsFinite(vec3.X) and CameraUtils.IsFinite(vec3.Y) and CameraUtils.IsFinite(vec3.Z) end -- Legacy implementation renamed function CameraUtils.GetAngleBetweenXZVectors(v1, v2) return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z) end function CameraUtils.RotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount) if camLook.Magnitude > 0 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 -- 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 = math.clamp(t, -1, 1) if t >= 0 then return (k*t) / (k - t + 1) end return -((lowerK*-t) / (lowerK + t + 1)) end 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 function CameraUtils.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 math.clamp(point, -1, 1) end return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y)) end -- This function converts 4 different, redundant enumeration types to one standard so the values can be compared function CameraUtils.ConvertCameraModeEnumToStandard(enumValue) if enumValue == Enum.TouchCameraMovementMode.Default then return Enum.ComputerCameraMovementMode.Follow end if enumValue == Enum.ComputerCameraMovementMode.Default then return Enum.ComputerCameraMovementMode.Classic end if enumValue == Enum.TouchCameraMovementMode.Classic or enumValue == Enum.DevTouchCameraMovementMode.Classic or enumValue == Enum.DevComputerCameraMovementMode.Classic or enumValue == Enum.ComputerCameraMovementMode.Classic then return Enum.ComputerCameraMovementMode.Classic end if enumValue == Enum.TouchCameraMovementMode.Follow or enumValue == Enum.DevTouchCameraMovementMode.Follow or enumValue == Enum.DevComputerCameraMovementMode.Follow or enumValue == Enum.ComputerCameraMovementMode.Follow then return Enum.ComputerCameraMovementMode.Follow end if enumValue == Enum.TouchCameraMovementMode.Orbital or enumValue == Enum.DevTouchCameraMovementMode.Orbital or enumValue == Enum.DevComputerCameraMovementMode.Orbital or enumValue == Enum.ComputerCameraMovementMode.Orbital then return Enum.ComputerCameraMovementMode.Orbital end if FFlagUserCameraToggle then if enumValue == Enum.ComputerCameraMovementMode.CameraToggle or enumValue == Enum.DevComputerCameraMovementMode.CameraToggle then return Enum.ComputerCameraMovementMode.CameraToggle end end -- Note: Only the Dev versions of the Enums have UserChoice as an option if enumValue == Enum.DevTouchCameraMovementMode.UserChoice or enumValue == Enum.DevComputerCameraMovementMode.UserChoice then return Enum.DevComputerCameraMovementMode.UserChoice end -- For any unmapped options return Classic camera return Enum.ComputerCameraMovementMode.Classic end return CameraUtils end function _CameraModule() local CameraModule = {} CameraModule.__index = CameraModule local FFlagUserCameraToggle do local success, result = pcall(function() return UserSettings():IsUserFeatureEnabled("UserCameraToggle") end) FFlagUserCameraToggle = success and result end local FFlagUserRemoveTheCameraApi do local success, result = pcall(function() return UserSettings():IsUserFeatureEnabled("UserRemoveTheCameraApi") end) FFlagUserRemoveTheCameraApi = success and result end -- NOTICE: Player property names do not all match their StarterPlayer equivalents, -- with the differences noted in the comments on the right local PLAYER_CAMERA_PROPERTIES = { "CameraMinZoomDistance", "CameraMaxZoomDistance", "CameraMode", "DevCameraOcclusionMode", "DevComputerCameraMode", -- Corresponds to StarterPlayer.DevComputerCameraMovementMode "DevTouchCameraMode", -- Corresponds to StarterPlayer.DevTouchCameraMovementMode -- Character movement mode "DevComputerMovementMode", "DevTouchMovementMode", "DevEnableMouseLock", -- Corresponds to StarterPlayer.EnableMouseLockOption } local USER_GAME_SETTINGS_PROPERTIES = { "ComputerCameraMovementMode", "ComputerMovementMode", "ControlMode", "GamepadCameraSensitivity", "MouseSensitivity", "RotationType", "TouchCameraMovementMode", "TouchMovementMode", } --[[ Roblox Services ]]-- local Players = game:GetService("Players") local RunService = game:GetService("RunService") local UserInputService = game:GetService("UserInputService") local UserGameSettings = UserSettings():GetService("UserGameSettings") -- Camera math utility library local CameraUtils = _CameraUtils() -- Load Roblox Camera Controller Modules local ClassicCamera = _ClassicCamera() local OrbitalCamera = _OrbitalCamera() local LegacyCamera = _LegacyCamera() -- Load Roblox Occlusion Modules local Invisicam = _Invisicam() local Poppercam = _Poppercam() -- Load the near-field character transparency controller and the mouse lock "shift lock" controller local TransparencyController = _TransparencyController() local MouseLockController = _MouseLockController() -- Table of camera controllers that have been instantiated. They are instantiated as they are used. local instantiatedCameraControllers = {} local instantiatedOcclusionModules = {} -- Management of which options appear on the Roblox User Settings screen do local PlayerScripts = Players.LocalPlayer:WaitForChild("PlayerScripts") PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default) PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Follow) PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Classic) PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Default) PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Follow) PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Classic) if FFlagUserCameraToggle then PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.CameraToggle) end end CameraModule.FFlagUserCameraToggle = FFlagUserCameraToggle function CameraModule.new() local self = setmetatable({},CameraModule) -- Current active controller instances self.activeCameraController = nil self.activeOcclusionModule = nil self.activeTransparencyController = nil self.activeMouseLockController = nil self.currentComputerCameraMovementMode = nil -- Connections to events self.cameraSubjectChangedConn = nil self.cameraTypeChangedConn = nil -- Adds CharacterAdded and CharacterRemoving event handlers for all current players for _,player in pairs(Players:GetPlayers()) do self:OnPlayerAdded(player) end -- Adds CharacterAdded and CharacterRemoving event handlers for all players who join in the future Players.PlayerAdded:Connect(function(player) self:OnPlayerAdded(player) end) self.activeTransparencyController = TransparencyController.new() self.activeTransparencyController:Enable(true) if not UserInputService.TouchEnabled then self.activeMouseLockController = MouseLockController.new() local toggleEvent = self.activeMouseLockController:GetBindableToggleEvent() if toggleEvent then toggleEvent:Connect(function() self:OnMouseLockToggled() end) end end self:ActivateCameraController(self:GetCameraControlChoice()) self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode) self:OnCurrentCameraChanged() -- Does initializations and makes first camera controller RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, function(dt) self:Update(dt) end) -- Connect listeners to camera-related properties for _, propertyName in pairs(PLAYER_CAMERA_PROPERTIES) do Players.LocalPlayer:GetPropertyChangedSignal(propertyName):Connect(function() self:OnLocalPlayerCameraPropertyChanged(propertyName) end) end for _, propertyName in pairs(USER_GAME_SETTINGS_PROPERTIES) do UserGameSettings:GetPropertyChangedSignal(propertyName):Connect(function() self:OnUserGameSettingsPropertyChanged(propertyName) end) end game.Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function() self:OnCurrentCameraChanged() end) self.lastInputType = UserInputService:GetLastInputType() UserInputService.LastInputTypeChanged:Connect(function(newLastInputType) self.lastInputType = newLastInputType end) return self end function CameraModule:GetCameraMovementModeFromSettings() local cameraMode = Players.LocalPlayer.CameraMode -- Lock First Person trumps all other settings and forces ClassicCamera if cameraMode == Enum.CameraMode.LockFirstPerson then return CameraUtils.ConvertCameraModeEnumToStandard(Enum.ComputerCameraMovementMode.Classic) end local devMode, userMode if UserInputService.TouchEnabled then devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevTouchCameraMode) userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.TouchCameraMovementMode) else devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevComputerCameraMode) userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode) end if devMode == Enum.DevComputerCameraMovementMode.UserChoice then -- Developer is allowing user choice, so user setting is respected return userMode end return devMode end function CameraModule:ActivateOcclusionModule( occlusionMode ) local newModuleCreator if occlusionMode == Enum.DevCameraOcclusionMode.Zoom then newModuleCreator = Poppercam elseif occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then newModuleCreator = Invisicam else warn("CameraScript ActivateOcclusionModule called with unsupported mode") return end -- First check to see if there is actually a change. If the module being requested is already -- the currently-active solution then just make sure it's enabled and exit early if self.activeOcclusionModule and self.activeOcclusionModule:GetOcclusionMode() == occlusionMode then if not self.activeOcclusionModule:GetEnabled() then self.activeOcclusionModule:Enable(true) end return end -- Save a reference to the current active module (may be nil) so that we can disable it if -- we are successful in activating its replacement local prevOcclusionModule = self.activeOcclusionModule -- If there is no active module, see if the one we need has already been instantiated self.activeOcclusionModule = instantiatedOcclusionModules[newModuleCreator] -- If the module was not already instantiated and selected above, instantiate it if not self.activeOcclusionModule then self.activeOcclusionModule = newModuleCreator.new() if self.activeOcclusionModule then instantiatedOcclusionModules[newModuleCreator] = self.activeOcclusionModule end end -- If we were successful in either selecting or instantiating the module, -- enable it if it's not already the currently-active enabled module if self.activeOcclusionModule then local newModuleOcclusionMode = self.activeOcclusionModule:GetOcclusionMode() -- Sanity check that the module we selected or instantiated actually supports the desired occlusionMode if newModuleOcclusionMode ~= occlusionMode then warn("CameraScript ActivateOcclusionModule mismatch: ",self.activeOcclusionModule:GetOcclusionMode(),"~=",occlusionMode) end -- Deactivate current module if there is one if prevOcclusionModule then -- Sanity check that current module is not being replaced by itself (that should have been handled above) if prevOcclusionModule ~= self.activeOcclusionModule then prevOcclusionModule:Enable(false) else warn("CameraScript ActivateOcclusionModule failure to detect already running correct module") end end -- Occlusion modules need to be initialized with information about characters and cameraSubject -- Invisicam needs the LocalPlayer's character -- Poppercam needs all player characters and the camera subject if occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then -- Optimization to only send Invisicam what we know it needs if Players.LocalPlayer.Character then self.activeOcclusionModule:CharacterAdded(Players.LocalPlayer.Character, Players.LocalPlayer ) end else -- When Poppercam is enabled, we send it all existing player characters for its raycast ignore list for _, player in pairs(Players:GetPlayers()) do if player and player.Character then self.activeOcclusionModule:CharacterAdded(player.Character, player) end end self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject) end -- Activate new choice self.activeOcclusionModule:Enable(true) end end -- When supplied, legacyCameraType is used and cameraMovementMode is ignored (should be nil anyways) -- Next, if userCameraCreator is passed in, that is used as the cameraCreator function CameraModule:ActivateCameraController(cameraMovementMode, legacyCameraType) local newCameraCreator = nil if legacyCameraType~=nil then --[[ This function has been passed a CameraType enum value. Some of these map to the use of the LegacyCamera module, the value "Custom" will be translated to a movementMode enum value based on Dev and User settings, and "Scriptable" will disable the camera controller. --]] if legacyCameraType == Enum.CameraType.Scriptable then if self.activeCameraController then self.activeCameraController:Enable(false) self.activeCameraController = nil return end elseif legacyCameraType == Enum.CameraType.Custom then cameraMovementMode = self:GetCameraMovementModeFromSettings() elseif legacyCameraType == Enum.CameraType.Track then -- Note: The TrackCamera module was basically an older, less fully-featured -- version of ClassicCamera, no longer actively maintained, but it is re-implemented in -- case a game was dependent on its lack of ClassicCamera's extra functionality. cameraMovementMode = Enum.ComputerCameraMovementMode.Classic elseif legacyCameraType == Enum.CameraType.Follow then cameraMovementMode = Enum.ComputerCameraMovementMode.Follow elseif legacyCameraType == Enum.CameraType.Orbital then cameraMovementMode = Enum.ComputerCameraMovementMode.Orbital elseif legacyCameraType == Enum.CameraType.Attach or legacyCameraType == Enum.CameraType.Watch or legacyCameraType == Enum.CameraType.Fixed then newCameraCreator = LegacyCamera else warn("CameraScript encountered an unhandled Camera.CameraType value: ",legacyCameraType) end end if not newCameraCreator then if cameraMovementMode == Enum.ComputerCameraMovementMode.Classic or cameraMovementMode == Enum.ComputerCameraMovementMode.Follow or cameraMovementMode == Enum.ComputerCameraMovementMode.Default or (FFlagUserCameraToggle and cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle) then newCameraCreator = ClassicCamera elseif cameraMovementMode == Enum.ComputerCameraMovementMode.Orbital then newCameraCreator = OrbitalCamera else warn("ActivateCameraController did not select a module.") return end end -- Create the camera control module we need if it does not already exist in instantiatedCameraControllers local newCameraController if not instantiatedCameraControllers[newCameraCreator] then newCameraController = newCameraCreator.new() instantiatedCameraControllers[newCameraCreator] = newCameraController else newCameraController = instantiatedCameraControllers[newCameraCreator] end -- If there is a controller active and it's not the one we need, disable it, -- if it is the one we need, make sure it's enabled if self.activeCameraController then if self.activeCameraController ~= newCameraController then self.activeCameraController:Enable(false) self.activeCameraController = newCameraController self.activeCameraController:Enable(true) elseif not self.activeCameraController:GetEnabled() then self.activeCameraController:Enable(true) end elseif newCameraController ~= nil then self.activeCameraController = newCameraController self.activeCameraController:Enable(true) end if self.activeCameraController then if cameraMovementMode~=nil then self.activeCameraController:SetCameraMovementMode(cameraMovementMode) elseif legacyCameraType~=nil then -- Note that this is only called when legacyCameraType is not a type that -- was convertible to a ComputerCameraMovementMode value, i.e. really only applies to LegacyCamera self.activeCameraController:SetCameraType(legacyCameraType) end end end -- Note: The active transparency controller could be made to listen for this event itself. function CameraModule:OnCameraSubjectChanged() if self.activeTransparencyController then self.activeTransparencyController:SetSubject(game.Workspace.CurrentCamera.CameraSubject) end if self.activeOcclusionModule then self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject) end end function CameraModule:OnCameraTypeChanged(newCameraType) if newCameraType == Enum.CameraType.Scriptable then if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCenter then UserInputService.MouseBehavior = Enum.MouseBehavior.Default end end -- Forward the change to ActivateCameraController to handle self:ActivateCameraController(nil, newCameraType) end -- Note: Called whenever workspace.CurrentCamera changes, but also on initialization of this script function CameraModule:OnCurrentCameraChanged() local currentCamera = game.Workspace.CurrentCamera if not currentCamera then return end if self.cameraSubjectChangedConn then self.cameraSubjectChangedConn:Disconnect() end if self.cameraTypeChangedConn then self.cameraTypeChangedConn:Disconnect() end self.cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):Connect(function() self:OnCameraSubjectChanged(currentCamera.CameraSubject) end) self.cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):Connect(function() self:OnCameraTypeChanged(currentCamera.CameraType) end) self:OnCameraSubjectChanged(currentCamera.CameraSubject) self:OnCameraTypeChanged(currentCamera.CameraType) end function CameraModule:OnLocalPlayerCameraPropertyChanged(propertyName) if propertyName == "CameraMode" then -- CameraMode is only used to turn on/off forcing the player into first person view. The -- Note: The case "Classic" is used for all other views and does not correspond only to the ClassicCamera module if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then -- Locked in first person, use ClassicCamera which supports this if not self.activeCameraController or self.activeCameraController:GetModuleName() ~= "ClassicCamera" then self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(Enum.DevComputerCameraMovementMode.Classic)) end if self.activeCameraController then self.activeCameraController:UpdateForDistancePropertyChange() end elseif Players.LocalPlayer.CameraMode == Enum.CameraMode.Classic then -- Not locked in first person view local cameraMovementMode =self: GetCameraMovementModeFromSettings() self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode)) else warn("Unhandled value for property player.CameraMode: ",Players.LocalPlayer.CameraMode) end elseif propertyName == "DevComputerCameraMode" or propertyName == "DevTouchCameraMode" then local cameraMovementMode = self:GetCameraMovementModeFromSettings() self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode)) elseif propertyName == "DevCameraOcclusionMode" then self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode) elseif propertyName == "CameraMinZoomDistance" or propertyName == "CameraMaxZoomDistance" then if self.activeCameraController then self.activeCameraController:UpdateForDistancePropertyChange() end elseif propertyName == "DevTouchMovementMode" then elseif propertyName == "DevComputerMovementMode" then elseif propertyName == "DevEnableMouseLock" then -- This is the enabling/disabling of "Shift Lock" mode, not LockFirstPerson (which is a CameraMode) -- Note: Enabling and disabling of MouseLock mode is normally only a publish-time choice made via -- the corresponding EnableMouseLockOption checkbox of StarterPlayer, and this script does not have -- support for changing the availability of MouseLock at runtime (this would require listening to -- Player.DevEnableMouseLock changes) end end function CameraModule:OnUserGameSettingsPropertyChanged(propertyName) if propertyName == "ComputerCameraMovementMode" then local cameraMovementMode = self:GetCameraMovementModeFromSettings() self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode)) end end --[[ Main RenderStep Update. The camera controller and occlusion module both have opportunities to set and modify (respectively) the CFrame and Focus before it is set once on CurrentCamera. The camera and occlusion modules should only return CFrames, not set the CFrame property of CurrentCamera directly. --]] function CameraModule:Update(dt) if self.activeCameraController then if FFlagUserCameraToggle then self.activeCameraController:UpdateMouseBehavior() end local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt) self.activeCameraController:ApplyVRTransform() if self.activeOcclusionModule then newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus) end -- Here is where the new CFrame and Focus are set for this render frame game.Workspace.CurrentCamera.CFrame = newCameraCFrame game.Workspace.CurrentCamera.Focus = newCameraFocus -- Update to character local transparency as needed based on camera-to-subject distance if self.activeTransparencyController then self.activeTransparencyController:Update() end end end -- Formerly getCurrentCameraMode, this function resolves developer and user camera control settings to -- decide which camera control module should be instantiated. The old method of converting redundant enum types function CameraModule:GetCameraControlChoice() local player = Players.LocalPlayer if player then if self.lastInputType == Enum.UserInputType.Touch or UserInputService.TouchEnabled then -- Touch if player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then return CameraUtils.ConvertCameraModeEnumToStandard( UserGameSettings.TouchCameraMovementMode ) else return CameraUtils.ConvertCameraModeEnumToStandard( player.DevTouchCameraMode ) end else -- Computer if player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then local computerMovementMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode) return CameraUtils.ConvertCameraModeEnumToStandard(computerMovementMode) else return CameraUtils.ConvertCameraModeEnumToStandard(player.DevComputerCameraMode) end end end end function CameraModule:OnCharacterAdded(char, player) if self.activeOcclusionModule then self.activeOcclusionModule:CharacterAdded(char, player) end end function CameraModule:OnCharacterRemoving(char, player) if self.activeOcclusionModule then self.activeOcclusionModule:CharacterRemoving(char, player) end end function CameraModule:OnPlayerAdded(player) player.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char, player) end) player.CharacterRemoving:Connect(function(char) self:OnCharacterRemoving(char, player) end) end function CameraModule:OnMouseLockToggled() if self.activeMouseLockController then local mouseLocked = self.activeMouseLockController:GetIsMouseLocked() local mouseLockOffset = self.activeMouseLockController:GetMouseLockOffset() if self.activeCameraController then self.activeCameraController:SetIsMouseLocked(mouseLocked) self.activeCameraController:SetMouseLockOffset(mouseLockOffset) end end end --begin edit local Camera = CameraModule local IDENTITYCF = CFrame.new() local lastUpCFrame = IDENTITYCF Camera.UpVector = Vector3.new(0, 1, 0) Camera.TransitionRate = 0.15 Camera.UpCFrame = IDENTITYCF function Camera:GetUpVector(oldUpVector) return oldUpVector end local function getRotationBetween(u, v, axis) local dot, uxv = u:Dot(v), u:Cross(v) if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot) end function Camera:CalculateUpCFrame() local oldUpVector = self.UpVector local newUpVector = self:GetUpVector(oldUpVector) local backup = game.Workspace.CurrentCamera.CFrame.RightVector local transitionCF = getRotationBetween(oldUpVector, newUpVector, backup) local vecSlerpCF = IDENTITYCF:Lerp(transitionCF, self.TransitionRate) self.UpVector = vecSlerpCF * oldUpVector self.UpCFrame = vecSlerpCF * self.UpCFrame lastUpCFrame = self.UpCFrame end function Camera:Update(dt) if self.activeCameraController then if Camera.FFlagUserCameraToggle then self.activeCameraController:UpdateMouseBehavior() end local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt) self.activeCameraController:ApplyVRTransform() self:CalculateUpCFrame() self.activeCameraController:UpdateUpCFrame(self.UpCFrame) -- undo shift-lock offset local lockOffset = Vector3.new(0, 0, 0) if (self.activeMouseLockController and self.activeMouseLockController:GetIsMouseLocked()) then lockOffset = self.activeMouseLockController:GetMouseLockOffset() end local offset = newCameraFocus:ToObjectSpace(newCameraCFrame) local camRotation = self.UpCFrame * offset newCameraFocus = newCameraFocus - newCameraCFrame:VectorToWorldSpace(lockOffset) + camRotation:VectorToWorldSpace(lockOffset) newCameraCFrame = newCameraFocus * camRotation --local offset = newCameraFocus:Inverse() * newCameraCFrame --newCameraCFrame = newCameraFocus * self.UpCFrame * offset if (self.activeCameraController.lastCameraTransform) then self.activeCameraController.lastCameraTransform = newCameraCFrame self.activeCameraController.lastCameraFocus = newCameraFocus end if self.activeOcclusionModule then newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus) end game.Workspace.CurrentCamera.CFrame = newCameraCFrame game.Workspace.CurrentCamera.Focus = newCameraFocus if self.activeTransparencyController then self.activeTransparencyController:Update() end end end function Camera:IsFirstPerson() if self.activeCameraController then return self.activeCameraController:InFirstPerson() end return false end function Camera:IsMouseLocked() if self.activeCameraController then return self.activeCameraController:GetIsMouseLocked() end return false end function Camera:IsToggleMode() if self.activeCameraController then return self.activeCameraController.isCameraToggle end return false end function Camera:IsCamRelative() return self:IsMouseLocked() or self:IsFirstPerson() --return self:IsToggleMode(), self:IsMouseLocked(), self:IsFirstPerson() end -- local Utils = _CameraUtils() function Utils.GetAngleBetweenXZVectors(v1, v2) local upCFrame = lastUpCFrame v1 = upCFrame:VectorToObjectSpace(v1) v2 = upCFrame:VectorToObjectSpace(v2) return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z) end --end edit local cameraModuleObject = CameraModule.new() local cameraApi = {} return cameraModuleObject end function _ClickToMoveDisplay() local ClickToMoveDisplay = {} local FAILURE_ANIMATION_ID = "rbxassetid://2874840706" local TrailDotIcon = "rbxasset://textures/ui/traildot.png" local EndWaypointIcon = "rbxasset://textures/ui/waypoint.png" local WaypointsAlwaysOnTop = false local WAYPOINT_INCLUDE_FACTOR = 2 local LAST_DOT_DISTANCE = 3 local WAYPOINT_BILLBOARD_SIZE = UDim2.new(0, 1.68 * 25, 0, 2 * 25) local ENDWAYPOINT_SIZE_OFFSET_MIN = Vector2.new(0, 0.5) local ENDWAYPOINT_SIZE_OFFSET_MAX = Vector2.new(0, 1) local FAIL_WAYPOINT_SIZE_OFFSET_CENTER = Vector2.new(0, 0.5) local FAIL_WAYPOINT_SIZE_OFFSET_LEFT = Vector2.new(0.1, 0.5) local FAIL_WAYPOINT_SIZE_OFFSET_RIGHT = Vector2.new(-0.1, 0.5) local FAILURE_TWEEN_LENGTH = 0.125 local FAILURE_TWEEN_COUNT = 4 local TWEEN_WAYPOINT_THRESHOLD = 5 local TRAIL_DOT_PARENT_NAME = "ClickToMoveDisplay" local TrailDotSize = Vector2.new(1.5, 1.5) local TRAIL_DOT_MIN_SCALE = 1 local TRAIL_DOT_MIN_DISTANCE = 10 local TRAIL_DOT_MAX_SCALE = 2.5 local TRAIL_DOT_MAX_DISTANCE = 100 local PlayersService = game:GetService("Players") local TweenService = game:GetService("TweenService") local RunService = game:GetService("RunService") local Workspace = game:GetService("Workspace") local LocalPlayer = PlayersService.LocalPlayer local function CreateWaypointTemplates() local TrailDotTemplate = Instance.new("Part") TrailDotTemplate.Size = Vector3.new(1, 1, 1) TrailDotTemplate.Anchored = true TrailDotTemplate.CanCollide = false TrailDotTemplate.Name = "TrailDot" TrailDotTemplate.Transparency = 1 local TrailDotImage = Instance.new("ImageHandleAdornment") TrailDotImage.Name = "TrailDotImage" TrailDotImage.Size = TrailDotSize TrailDotImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1) TrailDotImage.AlwaysOnTop = WaypointsAlwaysOnTop TrailDotImage.Image = TrailDotIcon TrailDotImage.Adornee = TrailDotTemplate TrailDotImage.Parent = TrailDotTemplate local EndWaypointTemplate = Instance.new("Part") EndWaypointTemplate.Size = Vector3.new(2, 2, 2) EndWaypointTemplate.Anchored = true EndWaypointTemplate.CanCollide = false EndWaypointTemplate.Name = "EndWaypoint" EndWaypointTemplate.Transparency = 1 local EndWaypointImage = Instance.new("ImageHandleAdornment") EndWaypointImage.Name = "TrailDotImage" EndWaypointImage.Size = TrailDotSize EndWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1) EndWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop EndWaypointImage.Image = TrailDotIcon EndWaypointImage.Adornee = EndWaypointTemplate EndWaypointImage.Parent = EndWaypointTemplate local EndWaypointBillboard = Instance.new("BillboardGui") EndWaypointBillboard.Name = "EndWaypointBillboard" EndWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE EndWaypointBillboard.LightInfluence = 0 EndWaypointBillboard.SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MIN EndWaypointBillboard.AlwaysOnTop = true EndWaypointBillboard.Adornee = EndWaypointTemplate EndWaypointBillboard.Parent = EndWaypointTemplate local EndWaypointImageLabel = Instance.new("ImageLabel") EndWaypointImageLabel.Image = EndWaypointIcon EndWaypointImageLabel.BackgroundTransparency = 1 EndWaypointImageLabel.Size = UDim2.new(1, 0, 1, 0) EndWaypointImageLabel.Parent = EndWaypointBillboard local FailureWaypointTemplate = Instance.new("Part") FailureWaypointTemplate.Size = Vector3.new(2, 2, 2) FailureWaypointTemplate.Anchored = true FailureWaypointTemplate.CanCollide = false FailureWaypointTemplate.Name = "FailureWaypoint" FailureWaypointTemplate.Transparency = 1 local FailureWaypointImage = Instance.new("ImageHandleAdornment") FailureWaypointImage.Name = "TrailDotImage" FailureWaypointImage.Size = TrailDotSize FailureWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1) FailureWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop FailureWaypointImage.Image = TrailDotIcon FailureWaypointImage.Adornee = FailureWaypointTemplate FailureWaypointImage.Parent = FailureWaypointTemplate local FailureWaypointBillboard = Instance.new("BillboardGui") FailureWaypointBillboard.Name = "FailureWaypointBillboard" FailureWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE FailureWaypointBillboard.LightInfluence = 0 FailureWaypointBillboard.SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER FailureWaypointBillboard.AlwaysOnTop = true FailureWaypointBillboard.Adornee = FailureWaypointTemplate FailureWaypointBillboard.Parent = FailureWaypointTemplate local FailureWaypointFrame = Instance.new("Frame") FailureWaypointFrame.BackgroundTransparency = 1 FailureWaypointFrame.Size = UDim2.new(0, 0, 0, 0) FailureWaypointFrame.Position = UDim2.new(0.5, 0, 1, 0) FailureWaypointFrame.Parent = FailureWaypointBillboard local FailureWaypointImageLabel = Instance.new("ImageLabel") FailureWaypointImageLabel.Image = EndWaypointIcon FailureWaypointImageLabel.BackgroundTransparency = 1 FailureWaypointImageLabel.Position = UDim2.new( 0, -WAYPOINT_BILLBOARD_SIZE.X.Offset/2, 0, -WAYPOINT_BILLBOARD_SIZE.Y.Offset ) FailureWaypointImageLabel.Size = WAYPOINT_BILLBOARD_SIZE FailureWaypointImageLabel.Parent = FailureWaypointFrame return TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate end local TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() local function getTrailDotParent() local camera = Workspace.CurrentCamera local trailParent = camera:FindFirstChild(TRAIL_DOT_PARENT_NAME) if not trailParent then trailParent = Instance.new("Model") trailParent.Name = TRAIL_DOT_PARENT_NAME trailParent.Parent = camera end return trailParent end local function placePathWaypoint(waypointModel, position) local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0)) local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList( ray, { Workspace.CurrentCamera, LocalPlayer.Character } ) if hitPart then waypointModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal) waypointModel.Parent = getTrailDotParent() end end local TrailDot = {} TrailDot.__index = TrailDot function TrailDot:Destroy() self.DisplayModel:Destroy() end function TrailDot:NewDisplayModel(position) local newDisplayModel = TrailDotTemplate:Clone() placePathWaypoint(newDisplayModel, position) return newDisplayModel end function TrailDot.new(position, closestWaypoint) local self = setmetatable({}, TrailDot) self.DisplayModel = self:NewDisplayModel(position) self.ClosestWayPoint = closestWaypoint return self end local EndWaypoint = {} EndWaypoint.__index = EndWaypoint function EndWaypoint:Destroy() self.Destroyed = true self.Tween:Cancel() self.DisplayModel:Destroy() end function EndWaypoint:NewDisplayModel(position) local newDisplayModel = EndWaypointTemplate:Clone() placePathWaypoint(newDisplayModel, position) return newDisplayModel end function EndWaypoint:CreateTween() local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, -1, true) local tween = TweenService:Create( self.DisplayModel.EndWaypointBillboard, tweenInfo, { SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MAX } ) tween:Play() return tween end function EndWaypoint:TweenInFrom(originalPosition) local currentPositon = self.DisplayModel.Position local studsOffset = originalPosition - currentPositon self.DisplayModel.EndWaypointBillboard.StudsOffset = Vector3.new(0, studsOffset.Y, 0) local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out) local tween = TweenService:Create( self.DisplayModel.EndWaypointBillboard, tweenInfo, { StudsOffset = Vector3.new(0, 0, 0) } ) tween:Play() return tween end function EndWaypoint.new(position, closestWaypoint, originalPosition) local self = setmetatable({}, EndWaypoint) self.DisplayModel = self:NewDisplayModel(position) self.Destroyed = false if originalPosition and (originalPosition - position).magnitude > TWEEN_WAYPOINT_THRESHOLD then self.Tween = self:TweenInFrom(originalPosition) coroutine.wrap(function() self.Tween.Completed:Wait() if not self.Destroyed then self.Tween = self:CreateTween() end end)() else self.Tween = self:CreateTween() end self.ClosestWayPoint = closestWaypoint return self end local FailureWaypoint = {} FailureWaypoint.__index = FailureWaypoint function FailureWaypoint:Hide() self.DisplayModel.Parent = nil end function FailureWaypoint:Destroy() self.DisplayModel:Destroy() end function FailureWaypoint:NewDisplayModel(position) local newDisplayModel = FailureWaypointTemplate:Clone() placePathWaypoint(newDisplayModel, position) local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0)) local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList( ray, { Workspace.CurrentCamera, LocalPlayer.Character } ) if hitPart then newDisplayModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal) newDisplayModel.Parent = getTrailDotParent() end return newDisplayModel end function FailureWaypoint:RunFailureTween() wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore starting tweening -- Tween out from center local tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out) local tweenLeft = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo, { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_LEFT }) tweenLeft:Play() local tweenLeftRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo, { Rotation = 10 }) tweenLeftRoation:Play() tweenLeft.Completed:wait() -- Tween back and forth tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, FAILURE_TWEEN_COUNT - 1, true) local tweenSideToSide = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo, { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_RIGHT}) tweenSideToSide:Play() -- Tween flash dark and roate left and right tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, FAILURE_TWEEN_COUNT - 1, true) local tweenFlash = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame.ImageLabel, tweenInfo, { ImageColor3 = Color3.new(0.75, 0.75, 0.75)}) tweenFlash:Play() local tweenRotate = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo, { Rotation = -10 }) tweenRotate:Play() tweenSideToSide.Completed:wait() -- Tween back to center tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out) local tweenCenter = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo, { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER }) tweenCenter:Play() local tweenRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo, { Rotation = 0 }) tweenRoation:Play() tweenCenter.Completed:wait() wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore removing end function FailureWaypoint.new(position) local self = setmetatable({}, FailureWaypoint) self.DisplayModel = self:NewDisplayModel(position) return self end local failureAnimation = Instance.new("Animation") failureAnimation.AnimationId = FAILURE_ANIMATION_ID local lastHumanoid = nil local lastFailureAnimationTrack = nil local function getFailureAnimationTrack(myHumanoid) if myHumanoid == lastHumanoid then return lastFailureAnimationTrack end lastFailureAnimationTrack = myHumanoid:LoadAnimation(failureAnimation) lastFailureAnimationTrack.Priority = Enum.AnimationPriority.Action lastFailureAnimationTrack.Looped = false return lastFailureAnimationTrack end local function findPlayerHumanoid() local character = LocalPlayer.Character if character then return character:FindFirstChildOfClass("Humanoid") end end local function createTrailDots(wayPoints, originalEndWaypoint) local newTrailDots = {} local count = 1 for i = 1, #wayPoints - 1 do local closeToEnd = (wayPoints[i].Position - wayPoints[#wayPoints].Position).magnitude < LAST_DOT_DISTANCE local includeWaypoint = i % WAYPOINT_INCLUDE_FACTOR == 0 and not closeToEnd if includeWaypoint then local trailDot = TrailDot.new(wayPoints[i].Position, i) newTrailDots[count] = trailDot count = count + 1 end end local newEndWaypoint = EndWaypoint.new(wayPoints[#wayPoints].Position, #wayPoints, originalEndWaypoint) table.insert(newTrailDots, newEndWaypoint) local reversedTrailDots = {} count = 1 for i = #newTrailDots, 1, -1 do reversedTrailDots[count] = newTrailDots[i] count = count + 1 end return reversedTrailDots end local function getTrailDotScale(distanceToCamera, defaultSize) local rangeLength = TRAIL_DOT_MAX_DISTANCE - TRAIL_DOT_MIN_DISTANCE local inRangePoint = math.clamp(distanceToCamera - TRAIL_DOT_MIN_DISTANCE, 0, rangeLength)/rangeLength local scale = TRAIL_DOT_MIN_SCALE + (TRAIL_DOT_MAX_SCALE - TRAIL_DOT_MIN_SCALE)*inRangePoint return defaultSize * scale end local createPathCount = 0 -- originalEndWaypoint is optional, causes the waypoint to tween from that position. function ClickToMoveDisplay.CreatePathDisplay(wayPoints, originalEndWaypoint) createPathCount = createPathCount + 1 local trailDots = createTrailDots(wayPoints, originalEndWaypoint) local function removePathBeforePoint(wayPointNumber) -- kill all trailDots before and at wayPointNumber for i = #trailDots, 1, -1 do local trailDot = trailDots[i] if trailDot.ClosestWayPoint <= wayPointNumber then trailDot:Destroy() trailDots[i] = nil else break end end end local reiszeTrailDotsUpdateName = "ClickToMoveResizeTrail" ..createPathCount local function resizeTrailDots() if #trailDots == 0 then RunService:UnbindFromRenderStep(reiszeTrailDotsUpdateName) return end local cameraPos = Workspace.CurrentCamera.CFrame.p for i = 1, #trailDots do local trailDotImage = trailDots[i].DisplayModel:FindFirstChild("TrailDotImage") if trailDotImage then local distanceToCamera = (trailDots[i].DisplayModel.Position - cameraPos).magnitude trailDotImage.Size = getTrailDotScale(distanceToCamera, TrailDotSize) end end end RunService:BindToRenderStep(reiszeTrailDotsUpdateName, Enum.RenderPriority.Camera.Value - 1, resizeTrailDots) local function removePath() removePathBeforePoint(#wayPoints) end return removePath, removePathBeforePoint end local lastFailureWaypoint = nil function ClickToMoveDisplay.DisplayFailureWaypoint(position) if lastFailureWaypoint then lastFailureWaypoint:Hide() end local failureWaypoint = FailureWaypoint.new(position) lastFailureWaypoint = failureWaypoint coroutine.wrap(function() failureWaypoint:RunFailureTween() failureWaypoint:Destroy() failureWaypoint = nil end)() end function ClickToMoveDisplay.CreateEndWaypoint(position) return EndWaypoint.new(position) end function ClickToMoveDisplay.PlayFailureAnimation() local myHumanoid = findPlayerHumanoid() if myHumanoid then local animationTrack = getFailureAnimationTrack(myHumanoid) animationTrack:Play() end end function ClickToMoveDisplay.CancelFailureAnimation() if lastFailureAnimationTrack ~= nil and lastFailureAnimationTrack.IsPlaying then lastFailureAnimationTrack:Stop() end end function ClickToMoveDisplay.SetWaypointTexture(texture) TrailDotIcon = texture TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() end function ClickToMoveDisplay.GetWaypointTexture() return TrailDotIcon end function ClickToMoveDisplay.SetWaypointRadius(radius) TrailDotSize = Vector2.new(radius, radius) TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() end function ClickToMoveDisplay.GetWaypointRadius() return TrailDotSize.X end function ClickToMoveDisplay.SetEndWaypointTexture(texture) EndWaypointIcon = texture TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() end function ClickToMoveDisplay.GetEndWaypointTexture() return EndWaypointIcon end function ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop) WaypointsAlwaysOnTop = alwaysOnTop TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() end function ClickToMoveDisplay.GetWaypointsAlwaysOnTop() return WaypointsAlwaysOnTop end return ClickToMoveDisplay end function _BaseCharacterController() local ZERO_VECTOR3 = Vector3.new(0,0,0) --[[ The Module ]]-- local BaseCharacterController = {} BaseCharacterController.__index = BaseCharacterController function BaseCharacterController.new() local self = setmetatable({}, BaseCharacterController) self.enabled = false self.moveVector = ZERO_VECTOR3 self.moveVectorIsCameraRelative = true self.isJumping = false return self end function BaseCharacterController:OnRenderStepped(dt) -- By default, nothing to do end function BaseCharacterController:GetMoveVector() return self.moveVector end function BaseCharacterController:IsMoveVectorCameraRelative() return self.moveVectorIsCameraRelative end function BaseCharacterController:GetIsJumping() return self.isJumping end -- Override in derived classes to set self.enabled and return boolean indicating -- whether Enable/Disable was successful. Return true if controller is already in the requested state. function BaseCharacterController:Enable(enable) error("BaseCharacterController:Enable must be overridden in derived classes and should not be called.") return false end return BaseCharacterController end function _VehicleController() local ContextActionService = game:GetService("ContextActionService") --[[ Constants ]]-- -- Set this to true if you want to instead use the triggers for the throttle local useTriggersForThrottle = true -- Also set this to true if you want the thumbstick to not affect throttle, only triggers when a gamepad is conected local onlyTriggersForThrottle = false local ZERO_VECTOR3 = Vector3.new(0,0,0) local AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE = 35 -- Note that VehicleController does not derive from BaseCharacterController, it is a special case local VehicleController = {} VehicleController.__index = VehicleController function VehicleController.new(CONTROL_ACTION_PRIORITY) local self = setmetatable({}, VehicleController) self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY self.enabled = false self.vehicleSeat = nil self.throttle = 0 self.steer = 0 self.acceleration = 0 self.decceleration = 0 self.turningRight = 0 self.turningLeft = 0 self.vehicleMoveVector = ZERO_VECTOR3 self.autoPilot = {} self.autoPilot.MaxSpeed = 0 self.autoPilot.MaxSteeringAngle = 0 return self end function VehicleController:BindContextActions() if useTriggersForThrottle then ContextActionService:BindActionAtPriority("throttleAccel", (function(actionName, inputState, inputObject) self:OnThrottleAccel(actionName, inputState, inputObject) return Enum.ContextActionResult.Pass end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonR2) ContextActionService:BindActionAtPriority("throttleDeccel", (function(actionName, inputState, inputObject) self:OnThrottleDeccel(actionName, inputState, inputObject) return Enum.ContextActionResult.Pass end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonL2) end ContextActionService:BindActionAtPriority("arrowSteerRight", (function(actionName, inputState, inputObject) self:OnSteerRight(actionName, inputState, inputObject) return Enum.ContextActionResult.Pass end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Right) ContextActionService:BindActionAtPriority("arrowSteerLeft", (function(actionName, inputState, inputObject) self:OnSteerLeft(actionName, inputState, inputObject) return Enum.ContextActionResult.Pass end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Left) end function VehicleController:Enable(enable, vehicleSeat) if enable == self.enabled and vehicleSeat == self.vehicleSeat then return end self.enabled = enable self.vehicleMoveVector = ZERO_VECTOR3 if enable then if vehicleSeat then self.vehicleSeat = vehicleSeat self:SetupAutoPilot() self:BindContextActions() end else if useTriggersForThrottle then ContextActionService:UnbindAction("throttleAccel") ContextActionService:UnbindAction("throttleDeccel") end ContextActionService:UnbindAction("arrowSteerRight") ContextActionService:UnbindAction("arrowSteerLeft") self.vehicleSeat = nil end end function VehicleController:OnThrottleAccel(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then self.acceleration = 0 else self.acceleration = -1 end self.throttle = self.acceleration + self.decceleration end function VehicleController:OnThrottleDeccel(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then self.decceleration = 0 else self.decceleration = 1 end self.throttle = self.acceleration + self.decceleration end function VehicleController:OnSteerRight(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then self.turningRight = 0 else self.turningRight = 1 end self.steer = self.turningRight + self.turningLeft end function VehicleController:OnSteerLeft(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then self.turningLeft = 0 else self.turningLeft = -1 end self.steer = self.turningRight + self.turningLeft end -- Call this from a function bound to Renderstep with Input Priority function VehicleController:Update(moveVector, cameraRelative, usingGamepad) if self.vehicleSeat then if cameraRelative then -- This is the default steering mode moveVector = moveVector + Vector3.new(self.steer, 0, self.throttle) if usingGamepad and onlyTriggersForThrottle and useTriggersForThrottle then self.vehicleSeat.ThrottleFloat = -self.throttle else self.vehicleSeat.ThrottleFloat = -moveVector.Z end self.vehicleSeat.SteerFloat = moveVector.X return moveVector, true else -- This is the path following mode local localMoveVector = self.vehicleSeat.Occupant.RootPart.CFrame:VectorToObjectSpace(moveVector) self.vehicleSeat.ThrottleFloat = self:ComputeThrottle(localMoveVector) self.vehicleSeat.SteerFloat = self:ComputeSteer(localMoveVector) return ZERO_VECTOR3, true end end return moveVector, false end function VehicleController:ComputeThrottle(localMoveVector) if localMoveVector ~= ZERO_VECTOR3 then local throttle = -localMoveVector.Z return throttle else return 0.0 end end function VehicleController:ComputeSteer(localMoveVector) if localMoveVector ~= ZERO_VECTOR3 then local steerAngle = -math.atan2(-localMoveVector.x, -localMoveVector.z) * (180 / math.pi) return steerAngle / self.autoPilot.MaxSteeringAngle else return 0.0 end end function VehicleController:SetupAutoPilot() -- Setup default self.autoPilot.MaxSpeed = self.vehicleSeat.MaxSpeed self.autoPilot.MaxSteeringAngle = AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE -- VehicleSeat should have a MaxSteeringAngle as well. -- Or we could look for a child "AutoPilotConfigModule" to find these values -- Or allow developer to set them through the API as like the CLickToMove customization API end return VehicleController end function _TouchJump() local Players = game:GetService("Players") local GuiService = game:GetService("GuiService") --[[ Constants ]]-- local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png" --[[ The Module ]]-- local BaseCharacterController = _BaseCharacterController() local TouchJump = setmetatable({}, BaseCharacterController) TouchJump.__index = TouchJump function TouchJump.new() local self = setmetatable(BaseCharacterController.new(), TouchJump) self.parentUIFrame = nil self.jumpButton = nil self.characterAddedConn = nil self.humanoidStateEnabledChangedConn = nil self.humanoidJumpPowerConn = nil self.humanoidParentConn = nil self.externallyEnabled = false self.jumpPower = 0 self.jumpStateEnabled = true self.isJumping = false self.humanoid = nil -- saved reference because property change connections are made using it return self end function TouchJump:EnableButton(enable) if enable then if not self.jumpButton then self:Create() end local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") if humanoid and self.externallyEnabled then if self.externallyEnabled then if humanoid.JumpPower > 0 then self.jumpButton.Visible = true end end end else self.jumpButton.Visible = false self.isJumping = false self.jumpButton.ImageRectOffset = Vector2.new(1, 146) end end function TouchJump:UpdateEnabled() if self.jumpPower > 0 and self.jumpStateEnabled then self:EnableButton(true) else self:EnableButton(false) end end function TouchJump:HumanoidChanged(prop) local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") if humanoid then if prop == "JumpPower" then self.jumpPower = humanoid.JumpPower self:UpdateEnabled() elseif prop == "Parent" then if not humanoid.Parent then self.humanoidChangeConn:Disconnect() end end end end function TouchJump:HumanoidStateEnabledChanged(state, isEnabled) if state == Enum.HumanoidStateType.Jumping then self.jumpStateEnabled = isEnabled self:UpdateEnabled() end end function TouchJump:CharacterAdded(char) if self.humanoidChangeConn then self.humanoidChangeConn:Disconnect() self.humanoidChangeConn = nil end self.humanoid = char:FindFirstChildOfClass("Humanoid") while not self.humanoid do char.ChildAdded:wait() self.humanoid = char:FindFirstChildOfClass("Humanoid") end self.humanoidJumpPowerConn = self.humanoid:GetPropertyChangedSignal("JumpPower"):Connect(function() self.jumpPower = self.humanoid.JumpPower self:UpdateEnabled() end) self.humanoidParentConn = self.humanoid:GetPropertyChangedSignal("Parent"):Connect(function() if not self.humanoid.Parent then self.humanoidJumpPowerConn:Disconnect() self.humanoidJumpPowerConn = nil self.humanoidParentConn:Disconnect() self.humanoidParentConn = nil end end) self.humanoidStateEnabledChangedConn = self.humanoid.StateEnabledChanged:Connect(function(state, enabled) self:HumanoidStateEnabledChanged(state, enabled) end) self.jumpPower = self.humanoid.JumpPower self.jumpStateEnabled = self.humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping) self:UpdateEnabled() end function TouchJump:SetupCharacterAddedFunction() self.characterAddedConn = Players.LocalPlayer.CharacterAdded:Connect(function(char) self:CharacterAdded(char) end) if Players.LocalPlayer.Character then self:CharacterAdded(Players.LocalPlayer.Character) end end function TouchJump:Enable(enable, parentFrame) if parentFrame then self.parentUIFrame = parentFrame end self.externallyEnabled = enable self:EnableButton(enable) end function TouchJump:Create() if not self.parentUIFrame then return end if self.jumpButton then self.jumpButton:Destroy() self.jumpButton = nil end local minAxis = math.min(self.parentUIFrame.AbsoluteSize.x, self.parentUIFrame.AbsoluteSize.y) local isSmallScreen = minAxis <= 500 local jumpButtonSize = isSmallScreen and 70 or 120 self.jumpButton = Instance.new("ImageButton") self.jumpButton.Name = "JumpButton" self.jumpButton.Visible = false self.jumpButton.BackgroundTransparency = 1 self.jumpButton.Image = TOUCH_CONTROL_SHEET self.jumpButton.ImageRectOffset = Vector2.new(1, 146) self.jumpButton.ImageRectSize = Vector2.new(144, 144) self.jumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize) self.jumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75) local touchObject = nil self.jumpButton.InputBegan:connect(function(inputObject) --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch or inputObject.UserInputState ~= Enum.UserInputState.Begin then return end touchObject = inputObject self.jumpButton.ImageRectOffset = Vector2.new(146, 146) self.isJumping = true end) local OnInputEnded = function() touchObject = nil self.isJumping = false self.jumpButton.ImageRectOffset = Vector2.new(1, 146) end self.jumpButton.InputEnded:connect(function(inputObject) if inputObject == touchObject then OnInputEnded() end end) GuiService.MenuOpened:connect(function() if touchObject then OnInputEnded() end end) if not self.characterAddedConn then self:SetupCharacterAddedFunction() end self.jumpButton.Parent = self.parentUIFrame end return TouchJump end function _ClickToMoveController() --[[ Roblox Services ]]-- local UserInputService = game:GetService("UserInputService") local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") local DebrisService = game:GetService('Debris') local StarterGui = game:GetService("StarterGui") local Workspace = game:GetService("Workspace") local CollectionService = game:GetService("CollectionService") local GuiService = game:GetService("GuiService") --[[ Configuration ]] local ShowPath = true local PlayFailureAnimation = true local UseDirectPath = false local UseDirectPathForVehicle = true local AgentSizeIncreaseFactor = 1.0 local UnreachableWaypointTimeout = 8 --[[ Constants ]]-- local movementKeys = { [Enum.KeyCode.W] = true; [Enum.KeyCode.A] = true; [Enum.KeyCode.S] = true; [Enum.KeyCode.D] = true; [Enum.KeyCode.Up] = true; [Enum.KeyCode.Down] = true; } local FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess, FFlagUserNavigationClickToMoveSkipPassedWaypointsResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationClickToMoveSkipPassedWaypoints") end) local FFlagUserNavigationClickToMoveSkipPassedWaypoints = FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess and FFlagUserNavigationClickToMoveSkipPassedWaypointsResult local Player = Players.LocalPlayer local ClickToMoveDisplay = _ClickToMoveDisplay() local ZERO_VECTOR3 = Vector3.new(0,0,0) local ALMOST_ZERO = 0.000001 --------------------------UTIL LIBRARY------------------------------- local Utility = {} do local function FindCharacterAncestor(part) if part then local humanoid = part:FindFirstChildOfClass("Humanoid") if humanoid then return part, humanoid else return FindCharacterAncestor(part.Parent) end end end Utility.FindCharacterAncestor = FindCharacterAncestor local function Raycast(ray, ignoreNonCollidable, ignoreList) ignoreList = ignoreList or {} local hitPart, hitPos, hitNorm, hitMat = Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList) if hitPart then if ignoreNonCollidable and hitPart.CanCollide == false then -- We always include character parts so a user can click on another character -- to walk to them. local _, humanoid = FindCharacterAncestor(hitPart) if humanoid == nil then table.insert(ignoreList, hitPart) return Raycast(ray, ignoreNonCollidable, ignoreList) end end return hitPart, hitPos, hitNorm, hitMat end return nil, nil end Utility.Raycast = Raycast end 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 --------------------------CHARACTER CONTROL------------------------------- local CurrentIgnoreList local CurrentIgnoreTag = nil local TaggedInstanceAddedConnection = nil local TaggedInstanceRemovedConnection = nil local function GetCharacter() return Player and Player.Character end local function UpdateIgnoreTag(newIgnoreTag) if newIgnoreTag == CurrentIgnoreTag then return end if TaggedInstanceAddedConnection then TaggedInstanceAddedConnection:Disconnect() TaggedInstanceAddedConnection = nil end if TaggedInstanceRemovedConnection then TaggedInstanceRemovedConnection:Disconnect() TaggedInstanceRemovedConnection = nil end CurrentIgnoreTag = newIgnoreTag CurrentIgnoreList = {GetCharacter()} if CurrentIgnoreTag ~= nil then local ignoreParts = CollectionService:GetTagged(CurrentIgnoreTag) for _, ignorePart in ipairs(ignoreParts) do table.insert(CurrentIgnoreList, ignorePart) end TaggedInstanceAddedConnection = CollectionService:GetInstanceAddedSignal( CurrentIgnoreTag):Connect(function(ignorePart) table.insert(CurrentIgnoreList, ignorePart) end) TaggedInstanceRemovedConnection = CollectionService:GetInstanceRemovedSignal( CurrentIgnoreTag):Connect(function(ignorePart) for i = 1, #CurrentIgnoreList do if CurrentIgnoreList[i] == ignorePart then CurrentIgnoreList[i] = CurrentIgnoreList[#CurrentIgnoreList] table.remove(CurrentIgnoreList) break end end end) end end local function getIgnoreList() if CurrentIgnoreList then return CurrentIgnoreList end CurrentIgnoreList = {} table.insert(CurrentIgnoreList, GetCharacter()) return CurrentIgnoreList end -----------------------------------PATHER-------------------------------------- local function Pather(endPoint, surfaceNormal, overrideUseDirectPath) local this = {} local directPathForHumanoid local directPathForVehicle if overrideUseDirectPath ~= nil then directPathForHumanoid = overrideUseDirectPath directPathForVehicle = overrideUseDirectPath else directPathForHumanoid = UseDirectPath directPathForVehicle = UseDirectPathForVehicle end this.Cancelled = false this.Started = false this.Finished = Instance.new("BindableEvent") this.PathFailed = Instance.new("BindableEvent") this.PathComputing = false this.PathComputed = false this.OriginalTargetPoint = endPoint this.TargetPoint = endPoint this.TargetSurfaceNormal = surfaceNormal this.DiedConn = nil this.SeatedConn = nil this.BlockedConn = nil this.TeleportedConn = nil this.CurrentPoint = 0 this.HumanoidOffsetFromPath = ZERO_VECTOR3 this.CurrentWaypointPosition = nil this.CurrentWaypointPlaneNormal = ZERO_VECTOR3 this.CurrentWaypointPlaneDistance = 0 this.CurrentWaypointNeedsJump = false; this.CurrentHumanoidPosition = ZERO_VECTOR3 this.CurrentHumanoidVelocity = 0 this.NextActionMoveDirection = ZERO_VECTOR3 this.NextActionJump = false this.Timeout = 0 this.Humanoid = findPlayerHumanoid(Player) this.OriginPoint = nil this.AgentCanFollowPath = false this.DirectPath = false this.DirectPathRiseFirst = false local rootPart = this.Humanoid and this.Humanoid.RootPart if rootPart then -- Setup origin this.OriginPoint = rootPart.CFrame.p -- Setup agent local agentRadius = 2 local agentHeight = 5 local agentCanJump = true local seat = this.Humanoid.SeatPart if seat and seat:IsA("VehicleSeat") then -- Humanoid is seated on a vehicle local vehicle = seat:FindFirstAncestorOfClass("Model") if vehicle then -- Make sure the PrimaryPart is set to the vehicle seat while we compute the extends. local tempPrimaryPart = vehicle.PrimaryPart vehicle.PrimaryPart = seat -- For now, only direct path if directPathForVehicle then local extents = vehicle:GetExtentsSize() agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z) agentHeight = AgentSizeIncreaseFactor * extents.Y agentCanJump = false this.AgentCanFollowPath = true this.DirectPath = directPathForVehicle end -- Reset PrimaryPart vehicle.PrimaryPart = tempPrimaryPart end else local extents = GetCharacter():GetExtentsSize() agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z) agentHeight = AgentSizeIncreaseFactor * extents.Y agentCanJump = (this.Humanoid.JumpPower > 0) this.AgentCanFollowPath = true this.DirectPath = directPathForHumanoid this.DirectPathRiseFirst = this.Humanoid.Sit end -- Build path object this.pathResult = PathfindingService:CreatePath({AgentRadius = agentRadius, AgentHeight = agentHeight, AgentCanJump = agentCanJump}) end function this:Cleanup() if this.stopTraverseFunc then this.stopTraverseFunc() this.stopTraverseFunc = nil end if this.MoveToConn then this.MoveToConn:Disconnect() this.MoveToConn = nil end if this.BlockedConn then this.BlockedConn:Disconnect() this.BlockedConn = nil end if this.DiedConn then this.DiedConn:Disconnect() this.DiedConn = nil end if this.SeatedConn then this.SeatedConn:Disconnect() this.SeatedConn = nil end if this.TeleportedConn then this.TeleportedConn:Disconnect() this.TeleportedConn = nil end this.Started = false end function this:Cancel() this.Cancelled = true this:Cleanup() end function this:IsActive() return this.AgentCanFollowPath and this.Started and not this.Cancelled end function this:OnPathInterrupted() -- Stop moving this.Cancelled = true this:OnPointReached(false) end function this:ComputePath() if this.OriginPoint then if this.PathComputed or this.PathComputing then return end this.PathComputing = true if this.AgentCanFollowPath then if this.DirectPath then this.pointList = { PathWaypoint.new(this.OriginPoint, Enum.PathWaypointAction.Walk), PathWaypoint.new(this.TargetPoint, this.DirectPathRiseFirst and Enum.PathWaypointAction.Jump or Enum.PathWaypointAction.Walk) } this.PathComputed = true else this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint) this.pointList = this.pathResult:GetWaypoints() this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end) this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success end end this.PathComputing = false end end function this:IsValidPath() this:ComputePath() return this.PathComputed and this.AgentCanFollowPath end this.Recomputing = false function this:OnPathBlocked(blockedWaypointIdx) local pathBlocked = blockedWaypointIdx >= this.CurrentPoint if not pathBlocked or this.Recomputing then return end this.Recomputing = true if this.stopTraverseFunc then this.stopTraverseFunc() this.stopTraverseFunc = nil end this.OriginPoint = this.Humanoid.RootPart.CFrame.p this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint) this.pointList = this.pathResult:GetWaypoints() if #this.pointList > 0 then this.HumanoidOffsetFromPath = this.pointList[1].Position - this.OriginPoint end this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success if ShowPath then this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList) end if this.PathComputed then this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it. this:OnPointReached(true) -- Move to first point else this.PathFailed:Fire() this:Cleanup() end this.Recomputing = false end function this:OnRenderStepped(dt) if this.Started and not this.Cancelled then -- Check for Timeout (if a waypoint is not reached within the delay, we fail) this.Timeout = this.Timeout + dt if this.Timeout > UnreachableWaypointTimeout then this:OnPointReached(false) return end -- Get Humanoid position and velocity this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity -- Check if it has reached some waypoints while this.Started and this:IsCurrentWaypointReached() do this:OnPointReached(true) end -- If still started, update actions if this.Started then -- Move action this.NextActionMoveDirection = this.CurrentWaypointPosition - this.CurrentHumanoidPosition if this.NextActionMoveDirection.Magnitude > ALMOST_ZERO then this.NextActionMoveDirection = this.NextActionMoveDirection.Unit else this.NextActionMoveDirection = ZERO_VECTOR3 end -- Jump action if this.CurrentWaypointNeedsJump then this.NextActionJump = true this.CurrentWaypointNeedsJump = false -- Request jump only once else this.NextActionJump = false end end end end function this:IsCurrentWaypointReached() local reached = false -- Check we do have a plane, if not, we consider the waypoint reached if this.CurrentWaypointPlaneNormal ~= ZERO_VECTOR3 then -- Compute distance of Humanoid from destination plane local dist = this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidPosition) - this.CurrentWaypointPlaneDistance -- Compute the component of the Humanoid velocity that is towards the plane local velocity = -this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidVelocity) -- Compute the threshold from the destination plane based on Humanoid velocity local threshold = math.max(1.0, 0.0625 * velocity) -- If we are less then threshold in front of the plane (between 0 and threshold) or if we are behing the plane (less then 0), we consider we reached it reached = dist < threshold else reached = true end if reached then this.CurrentWaypointPosition = nil this.CurrentWaypointPlaneNormal = ZERO_VECTOR3 this.CurrentWaypointPlaneDistance = 0 end return reached end function this:OnPointReached(reached) if reached and not this.Cancelled then -- First, destroyed the current displayed waypoint if this.setPointFunc then this.setPointFunc(this.CurrentPoint) end local nextWaypointIdx = this.CurrentPoint + 1 if nextWaypointIdx > #this.pointList then -- End of path reached if this.stopTraverseFunc then this.stopTraverseFunc() end this.Finished:Fire() this:Cleanup() else local currentWaypoint = this.pointList[this.CurrentPoint] local nextWaypoint = this.pointList[nextWaypointIdx] -- If airborne, only allow to keep moving -- if nextWaypoint.Action ~= Jump, or path mantains a direction -- Otherwise, wait until the humanoid gets to the ground local currentState = this.Humanoid:GetState() local isInAir = currentState == Enum.HumanoidStateType.FallingDown or currentState == Enum.HumanoidStateType.Freefall or currentState == Enum.HumanoidStateType.Jumping if isInAir then local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump if not shouldWaitForGround and this.CurrentPoint > 1 then local prevWaypoint = this.pointList[this.CurrentPoint - 1] local prevDir = currentWaypoint.Position - prevWaypoint.Position local currDir = nextWaypoint.Position - currentWaypoint.Position local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit local THRESHOLD_COS = 0.996 -- ~cos(5 degrees) shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS end if shouldWaitForGround then this.Humanoid.FreeFalling:Wait() -- Give time to the humanoid's state to change -- Otherwise, the jump flag in Humanoid -- will be reset by the state change wait(0.1) end end -- Move to the next point if FFlagUserNavigationClickToMoveSkipPassedWaypoints then this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx) else if this.setPointFunc then this.setPointFunc(nextWaypointIdx) end if nextWaypoint.Action == Enum.PathWaypointAction.Jump then this.Humanoid.Jump = true end this.Humanoid:MoveTo(nextWaypoint.Position) this.CurrentPoint = nextWaypointIdx end end else this.PathFailed:Fire() this:Cleanup() end end function this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx) -- Build next destination plane -- (plane normal is perpendicular to the y plane and is from next waypoint towards current one (provided the two waypoints are not at the same location)) -- (plane location is at next waypoint) this.CurrentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position this.CurrentWaypointPlaneNormal = Vector3.new(this.CurrentWaypointPlaneNormal.X, 0, this.CurrentWaypointPlaneNormal.Z) if this.CurrentWaypointPlaneNormal.Magnitude > ALMOST_ZERO then this.CurrentWaypointPlaneNormal = this.CurrentWaypointPlaneNormal.Unit this.CurrentWaypointPlaneDistance = this.CurrentWaypointPlaneNormal:Dot(nextWaypoint.Position) else -- Next waypoint is the same as current waypoint so no plane this.CurrentWaypointPlaneNormal = ZERO_VECTOR3 this.CurrentWaypointPlaneDistance = 0 end -- Should we jump this.CurrentWaypointNeedsJump = nextWaypoint.Action == Enum.PathWaypointAction.Jump; -- Remember next waypoint position this.CurrentWaypointPosition = nextWaypoint.Position -- Move to next point this.CurrentPoint = nextWaypointIdx -- Finally reset Timeout this.Timeout = 0 end function this:Start(overrideShowPath) if not this.AgentCanFollowPath then this.PathFailed:Fire() return end if this.Started then return end this.Started = true ClickToMoveDisplay.CancelFailureAnimation() if ShowPath then if overrideShowPath == nil or overrideShowPath then this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList, this.OriginalTargetPoint) end end if #this.pointList > 0 then -- Determine the humanoid offset from the path's first point -- Offset of the first waypoint from the path's origin point this.HumanoidOffsetFromPath = Vector3.new(0, this.pointList[1].Position.Y - this.OriginPoint.Y, 0) -- As well as its current position and velocity this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity -- Connect to events this.SeatedConn = this.Humanoid.Seated:Connect(function(isSeated, seat) this:OnPathInterrupted() end) this.DiedConn = this.Humanoid.Died:Connect(function() this:OnPathInterrupted() end) this.TeleportedConn = this.Humanoid.RootPart:GetPropertyChangedSignal("CFrame"):Connect(function() this:OnPathInterrupted() end) -- Actually start this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it. this:OnPointReached(true) -- Move to first point else this.PathFailed:Fire() if this.stopTraverseFunc then this.stopTraverseFunc() end end end --We always raycast to the ground in the case that the user clicked a wall. local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5 local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50) local newHitPart, newHitPos = Workspace:FindPartOnRayWithIgnoreList(ray, getIgnoreList()) if newHitPart then this.TargetPoint = newHitPos end this:ComputePath() return this end ------------------------------------------------------------------------- local function CheckAlive() local humanoid = findPlayerHumanoid(Player) return humanoid ~= nil and humanoid.Health > 0 end local function GetEquippedTool(character) if character ~= nil then for _, child in pairs(character:GetChildren()) do if child:IsA('Tool') then return child end end end end local ExistingPather = nil local ExistingIndicator = nil local PathCompleteListener = nil local PathFailedListener = nil local function CleanupPath() if ExistingPather then ExistingPather:Cancel() ExistingPather = nil end if PathCompleteListener then PathCompleteListener:Disconnect() PathCompleteListener = nil end if PathFailedListener then PathFailedListener:Disconnect() PathFailedListener = nil end if ExistingIndicator then ExistingIndicator:Destroy() end end local function HandleMoveTo(thisPather, hitPt, hitChar, character, overrideShowPath) if ExistingPather then CleanupPath() end ExistingPather = thisPather thisPather:Start(overrideShowPath) PathCompleteListener = thisPather.Finished.Event:Connect(function() CleanupPath() if hitChar then local currentWeapon = GetEquippedTool(character) if currentWeapon then currentWeapon:Activate() end end end) PathFailedListener = thisPather.PathFailed.Event:Connect(function() CleanupPath() if overrideShowPath == nil or overrideShowPath then local shouldPlayFailureAnim = PlayFailureAnimation and not (ExistingPather and ExistingPather:IsActive()) if shouldPlayFailureAnim then ClickToMoveDisplay.PlayFailureAnimation() end ClickToMoveDisplay.DisplayFailureWaypoint(hitPt) end end) end local function ShowPathFailedFeedback(hitPt) if ExistingPather and ExistingPather:IsActive() then ExistingPather:Cancel() end if PlayFailureAnimation then ClickToMoveDisplay.PlayFailureAnimation() end ClickToMoveDisplay.DisplayFailureWaypoint(hitPt) end function OnTap(tapPositions, goToPoint, wasTouchTap) -- Good to remember if this is the latest tap event local camera = Workspace.CurrentCamera local character = Player.Character if not CheckAlive() then return end -- This is a path tap position if #tapPositions == 1 or goToPoint then if camera then local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y) local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000) local myHumanoid = findPlayerHumanoid(Player) local hitPart, hitPt, hitNormal = Utility.Raycast(ray, true, getIgnoreList()) local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart) if wasTouchTap and hitHumanoid and StarterGui:GetCore("AvatarContextMenuEnabled") then local clickedPlayer = Players:GetPlayerFromCharacter(hitHumanoid.Parent) if clickedPlayer then CleanupPath() return end end if goToPoint then hitPt = goToPoint hitChar = nil end if hitPt and character then -- Clean up current path CleanupPath() local thisPather = Pather(hitPt, hitNormal) if thisPather:IsValidPath() then HandleMoveTo(thisPather, hitPt, hitChar, character) else -- Clean up thisPather:Cleanup() -- Feedback here for when we don't have a good path ShowPathFailedFeedback(hitPt) end end end elseif #tapPositions >= 2 then if camera then -- Do shoot local currentWeapon = GetEquippedTool(character) if currentWeapon then currentWeapon:Activate() end end end end local function DisconnectEvent(event) if event then event:Disconnect() end end --[[ The ClickToMove Controller Class ]]-- local KeyboardController = _Keyboard() local ClickToMove = setmetatable({}, KeyboardController) ClickToMove.__index = ClickToMove function ClickToMove.new(CONTROL_ACTION_PRIORITY) local self = setmetatable(KeyboardController.new(CONTROL_ACTION_PRIORITY), ClickToMove) self.fingerTouches = {} self.numUnsunkTouches = 0 -- PC simulation self.mouse1Down = tick() self.mouse1DownPos = Vector2.new() self.mouse2DownTime = tick() self.mouse2DownPos = Vector2.new() self.mouse2UpTime = tick() self.keyboardMoveVector = ZERO_VECTOR3 self.tapConn = nil self.inputBeganConn = nil self.inputChangedConn = nil self.inputEndedConn = nil self.humanoidDiedConn = nil self.characterChildAddedConn = nil self.onCharacterAddedConn = nil self.characterChildRemovedConn = nil self.renderSteppedConn = nil self.menuOpenedConnection = nil self.running = false self.wasdEnabled = false return self end function ClickToMove:DisconnectEvents() DisconnectEvent(self.tapConn) DisconnectEvent(self.inputBeganConn) DisconnectEvent(self.inputChangedConn) DisconnectEvent(self.inputEndedConn) DisconnectEvent(self.humanoidDiedConn) DisconnectEvent(self.characterChildAddedConn) DisconnectEvent(self.onCharacterAddedConn) DisconnectEvent(self.renderSteppedConn) DisconnectEvent(self.characterChildRemovedConn) DisconnectEvent(self.menuOpenedConnection) end function ClickToMove:OnTouchBegan(input, processed) if self.fingerTouches[input] == nil and not processed then self.numUnsunkTouches = self.numUnsunkTouches + 1 end self.fingerTouches[input] = processed end function ClickToMove:OnTouchChanged(input, processed) if self.fingerTouches[input] == nil then self.fingerTouches[input] = processed if not processed then self.numUnsunkTouches = self.numUnsunkTouches + 1 end end end function ClickToMove:OnTouchEnded(input, processed) if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then self.numUnsunkTouches = self.numUnsunkTouches - 1 end self.fingerTouches[input] = nil end function ClickToMove:OnCharacterAdded(character) self:DisconnectEvents() self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed) if input.UserInputType == Enum.UserInputType.Touch then self:OnTouchBegan(input, processed) end -- Cancel path when you use the keyboard controls if wasd is enabled. if self.wasdEnabled and processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then CleanupPath() ClickToMoveDisplay.CancelFailureAnimation() end if input.UserInputType == Enum.UserInputType.MouseButton1 then self.mouse1DownTime = tick() self.mouse1DownPos = input.Position end if input.UserInputType == Enum.UserInputType.MouseButton2 then self.mouse2DownTime = tick() self.mouse2DownPos = input.Position end end) self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed) if input.UserInputType == Enum.UserInputType.Touch then self:OnTouchChanged(input, processed) end end) self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed) if input.UserInputType == Enum.UserInputType.Touch then self:OnTouchEnded(input, processed) end if input.UserInputType == Enum.UserInputType.MouseButton2 then self.mouse2UpTime = tick() local currPos = input.Position -- We allow click to move during path following or if there is no keyboard movement local allowed = ExistingPather or self.keyboardMoveVector.Magnitude <= 0 if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 and allowed then local positions = {currPos} OnTap(positions) end end end) self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed) if not processed then OnTap(touchPositions, nil, true) end end) self.menuOpenedConnection = GuiService.MenuOpened:Connect(function() CleanupPath() end) local function OnCharacterChildAdded(child) if UserInputService.TouchEnabled then if child:IsA('Tool') then child.ManualActivationOnly = true end end if child:IsA('Humanoid') then DisconnectEvent(self.humanoidDiedConn) self.humanoidDiedConn = child.Died:Connect(function() if ExistingIndicator then DebrisService:AddItem(ExistingIndicator.Model, 1) end end) end end self.characterChildAddedConn = character.ChildAdded:Connect(function(child) OnCharacterChildAdded(child) end) self.characterChildRemovedConn = character.ChildRemoved:Connect(function(child) if UserInputService.TouchEnabled then if child:IsA('Tool') then child.ManualActivationOnly = false end end end) for _, child in pairs(character:GetChildren()) do OnCharacterChildAdded(child) end end function ClickToMove:Start() self:Enable(true) end function ClickToMove:Stop() self:Enable(false) end function ClickToMove:CleanupPath() CleanupPath() end function ClickToMove:Enable(enable, enableWASD, touchJumpController) if enable then if not self.running then if Player.Character then -- retro-listen self:OnCharacterAdded(Player.Character) end self.onCharacterAddedConn = Player.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char) end) self.running = true end self.touchJumpController = touchJumpController if self.touchJumpController then self.touchJumpController:Enable(self.jumpEnabled) end else if self.running then self:DisconnectEvents() CleanupPath() -- Restore tool activation on shutdown if UserInputService.TouchEnabled then local character = Player.Character if character then for _, child in pairs(character:GetChildren()) do if child:IsA('Tool') then child.ManualActivationOnly = false end end end end self.running = false end if self.touchJumpController and not self.jumpEnabled then self.touchJumpController:Enable(true) end self.touchJumpController = nil end -- Extension for initializing Keyboard input as this class now derives from Keyboard if UserInputService.KeyboardEnabled and enable ~= self.enabled then self.forwardValue = 0 self.backwardValue = 0 self.leftValue = 0 self.rightValue = 0 self.moveVector = ZERO_VECTOR3 if enable then self:BindContextActions() self:ConnectFocusEventListeners() else self:UnbindContextActions() self:DisconnectFocusEventListeners() end end self.wasdEnabled = enable and enableWASD or false self.enabled = enable end function ClickToMove:OnRenderStepped(dt) -- Reset jump self.isJumping = false -- Handle Pather if ExistingPather then -- Let the Pather update ExistingPather:OnRenderStepped(dt) -- If we still have a Pather, set the resulting actions if ExistingPather then -- Setup move (NOT relative to camera) self.moveVector = ExistingPather.NextActionMoveDirection self.moveVectorIsCameraRelative = false -- Setup jump (but do NOT prevent the base Keayboard class from requesting jumps as well) if ExistingPather.NextActionJump then self.isJumping = true end else self.moveVector = self.keyboardMoveVector self.moveVectorIsCameraRelative = true end else self.moveVector = self.keyboardMoveVector self.moveVectorIsCameraRelative = true end -- Handle Keyboard's jump if self.jumpRequested then self.isJumping = true end end -- Overrides Keyboard:UpdateMovement(inputState) to conditionally consider self.wasdEnabled and let OnRenderStepped handle the movement function ClickToMove:UpdateMovement(inputState) if inputState == Enum.UserInputState.Cancel then self.keyboardMoveVector = ZERO_VECTOR3 elseif self.wasdEnabled then self.keyboardMoveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue) end end -- Overrides Keyboard:UpdateJump() because jump is handled in OnRenderStepped function ClickToMove:UpdateJump() -- Nothing to do (handled in OnRenderStepped) end --Public developer facing functions function ClickToMove:SetShowPath(value) ShowPath = value end function ClickToMove:GetShowPath() return ShowPath end function ClickToMove:SetWaypointTexture(texture) ClickToMoveDisplay.SetWaypointTexture(texture) end function ClickToMove:GetWaypointTexture() return ClickToMoveDisplay.GetWaypointTexture() end function ClickToMove:SetWaypointRadius(radius) ClickToMoveDisplay.SetWaypointRadius(radius) end function ClickToMove:GetWaypointRadius() return ClickToMoveDisplay.GetWaypointRadius() end function ClickToMove:SetEndWaypointTexture(texture) ClickToMoveDisplay.SetEndWaypointTexture(texture) end function ClickToMove:GetEndWaypointTexture() return ClickToMoveDisplay.GetEndWaypointTexture() end function ClickToMove:SetWaypointsAlwaysOnTop(alwaysOnTop) ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop) end function ClickToMove:GetWaypointsAlwaysOnTop() return ClickToMoveDisplay.GetWaypointsAlwaysOnTop() end function ClickToMove:SetFailureAnimationEnabled(enabled) PlayFailureAnimation = enabled end function ClickToMove:GetFailureAnimationEnabled() return PlayFailureAnimation end function ClickToMove:SetIgnoredPartsTag(tag) UpdateIgnoreTag(tag) end function ClickToMove:GetIgnoredPartsTag() return CurrentIgnoreTag end function ClickToMove:SetUseDirectPath(directPath) UseDirectPath = directPath end function ClickToMove:GetUseDirectPath() return UseDirectPath end function ClickToMove:SetAgentSizeIncreaseFactor(increaseFactorPercent) AgentSizeIncreaseFactor = 1.0 + (increaseFactorPercent / 100.0) end function ClickToMove:GetAgentSizeIncreaseFactor() return (AgentSizeIncreaseFactor - 1.0) * 100.0 end function ClickToMove:SetUnreachableWaypointTimeout(timeoutInSec) UnreachableWaypointTimeout = timeoutInSec end function ClickToMove:GetUnreachableWaypointTimeout() return UnreachableWaypointTimeout end function ClickToMove:SetUserJumpEnabled(jumpEnabled) self.jumpEnabled = jumpEnabled if self.touchJumpController then self.touchJumpController:Enable(jumpEnabled) end end function ClickToMove:GetUserJumpEnabled() return self.jumpEnabled end function ClickToMove:MoveTo(position, showPath, useDirectPath) local character = Player.Character if character == nil then return false end local thisPather = Pather(position, Vector3.new(0, 1, 0), useDirectPath) if thisPather and thisPather:IsValidPath() then HandleMoveTo(thisPather, position, nil, character, showPath) return true end return false end return ClickToMove end function _TouchThumbstick() local Players = game:GetService("Players") local GuiService = game:GetService("GuiService") local UserInputService = game:GetService("UserInputService") --[[ Constants ]]-- local ZERO_VECTOR3 = Vector3.new(0,0,0) local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png" --[[ The Module ]]-- local BaseCharacterController = _BaseCharacterController() local TouchThumbstick = setmetatable({}, BaseCharacterController) TouchThumbstick.__index = TouchThumbstick function TouchThumbstick.new() local self = setmetatable(BaseCharacterController.new(), TouchThumbstick) self.isFollowStick = false self.thumbstickFrame = nil self.moveTouchObject = nil self.onTouchMovedConn = nil self.onTouchEndedConn = nil self.screenPos = nil self.stickImage = nil self.thumbstickSize = nil -- Float return self end function TouchThumbstick:Enable(enable, uiParentFrame) if enable == nil then return false end -- If nil, return false (invalid argument) enable = enable and true or false -- Force anything non-nil to boolean before comparison if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state self.moveVector = ZERO_VECTOR3 self.isJumping = false if enable then -- Enable if not self.thumbstickFrame then self:Create(uiParentFrame) end self.thumbstickFrame.Visible = true else -- Disable self.thumbstickFrame.Visible = false self:OnInputEnded() end self.enabled = enable end function TouchThumbstick:OnInputEnded() self.thumbstickFrame.Position = self.screenPos self.stickImage.Position = UDim2.new(0, self.thumbstickFrame.Size.X.Offset/2 - self.thumbstickSize/4, 0, self.thumbstickFrame.Size.Y.Offset/2 - self.thumbstickSize/4) self.moveVector = ZERO_VECTOR3 self.isJumping = false self.thumbstickFrame.Position = self.screenPos self.moveTouchObject = nil end function TouchThumbstick:Create(parentFrame) if self.thumbstickFrame then self.thumbstickFrame:Destroy() self.thumbstickFrame = nil if self.onTouchMovedConn then self.onTouchMovedConn:Disconnect() self.onTouchMovedConn = nil end if self.onTouchEndedConn then self.onTouchEndedConn:Disconnect() self.onTouchEndedConn = nil end end local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y) local isSmallScreen = minAxis <= 500 self.thumbstickSize = isSmallScreen and 70 or 120 self.screenPos = isSmallScreen and UDim2.new(0, (self.thumbstickSize/2) - 10, 1, -self.thumbstickSize - 20) or UDim2.new(0, self.thumbstickSize/2, 1, -self.thumbstickSize * 1.75) self.thumbstickFrame = Instance.new("Frame") self.thumbstickFrame.Name = "ThumbstickFrame" self.thumbstickFrame.Active = true self.thumbstickFrame.Visible = false self.thumbstickFrame.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize) self.thumbstickFrame.Position = self.screenPos self.thumbstickFrame.BackgroundTransparency = 1 local outerImage = Instance.new("ImageLabel") outerImage.Name = "OuterImage" outerImage.Image = TOUCH_CONTROL_SHEET outerImage.ImageRectOffset = Vector2.new() outerImage.ImageRectSize = Vector2.new(220, 220) outerImage.BackgroundTransparency = 1 outerImage.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize) outerImage.Position = UDim2.new(0, 0, 0, 0) outerImage.Parent = self.thumbstickFrame self.stickImage = Instance.new("ImageLabel") self.stickImage.Name = "StickImage" self.stickImage.Image = TOUCH_CONTROL_SHEET self.stickImage.ImageRectOffset = Vector2.new(220, 0) self.stickImage.ImageRectSize = Vector2.new(111, 111) self.stickImage.BackgroundTransparency = 1 self.stickImage.Size = UDim2.new(0, self.thumbstickSize/2, 0, self.thumbstickSize/2) self.stickImage.Position = UDim2.new(0, self.thumbstickSize/2 - self.thumbstickSize/4, 0, self.thumbstickSize/2 - self.thumbstickSize/4) self.stickImage.ZIndex = 2 self.stickImage.Parent = self.thumbstickFrame local centerPosition = nil local deadZone = 0.05 local function DoMove(direction) local currentMoveVector = direction / (self.thumbstickSize/2) -- Scaled Radial Dead Zone local inputAxisMagnitude = currentMoveVector.magnitude if inputAxisMagnitude < deadZone then currentMoveVector = Vector3.new() else currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone)) -- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed -- must check for zero length vector is using unit currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y) end self.moveVector = currentMoveVector end local function MoveStick(pos) local relativePosition = Vector2.new(pos.x - centerPosition.x, pos.y - centerPosition.y) local length = relativePosition.magnitude local maxLength = self.thumbstickFrame.AbsoluteSize.x/2 if self.isFollowStick and length > maxLength then local offset = relativePosition.unit * maxLength self.thumbstickFrame.Position = UDim2.new( 0, pos.x - self.thumbstickFrame.AbsoluteSize.x/2 - offset.x, 0, pos.y - self.thumbstickFrame.AbsoluteSize.y/2 - offset.y) else length = math.min(length, maxLength) relativePosition = relativePosition.unit * length end self.stickImage.Position = UDim2.new(0, relativePosition.x + self.stickImage.AbsoluteSize.x/2, 0, relativePosition.y + self.stickImage.AbsoluteSize.y/2) end -- input connections self.thumbstickFrame.InputBegan:Connect(function(inputObject) --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin) if self.moveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch or inputObject.UserInputState ~= Enum.UserInputState.Begin then return end self.moveTouchObject = inputObject self.thumbstickFrame.Position = UDim2.new(0, inputObject.Position.x - self.thumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.y - self.thumbstickFrame.Size.Y.Offset/2) centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2, self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2) local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) end) self.onTouchMovedConn = UserInputService.TouchMoved:Connect(function(inputObject, isProcessed) if inputObject == self.moveTouchObject then centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2, self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2) local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y) DoMove(direction) MoveStick(inputObject.Position) end end) self.onTouchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject, isProcessed) if inputObject == self.moveTouchObject then self:OnInputEnded() end end) GuiService.MenuOpened:Connect(function() if self.moveTouchObject then self:OnInputEnded() end end) self.thumbstickFrame.Parent = parentFrame end return TouchThumbstick end function _DynamicThumbstick() local ZERO_VECTOR3 = Vector3.new(0,0,0) local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png" local DYNAMIC_THUMBSTICK_ACTION_NAME = "DynamicThumbstickAction" local DYNAMIC_THUMBSTICK_ACTION_PRIORITY = Enum.ContextActionPriority.High.Value local MIDDLE_TRANSPARENCIES = { 1 - 0.89, 1 - 0.70, 1 - 0.60, 1 - 0.50, 1 - 0.40, 1 - 0.30, 1 - 0.25 } local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES local FADE_IN_OUT_BACKGROUND = true local FADE_IN_OUT_MAX_ALPHA = 0.35 local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3 local FADE_IN_OUT_BALANCE_DEFAULT = 0.5 local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) local Players = game:GetService("Players") local GuiService = game:GetService("GuiService") local UserInputService = game:GetService("UserInputService") local ContextActionService = game:GetService("ContextActionService") local RunService = game:GetService("RunService") local TweenService = game:GetService("TweenService") local LocalPlayer = Players.LocalPlayer if not LocalPlayer then Players:GetPropertyChangedSignal("LocalPlayer"):Wait() LocalPlayer = Players.LocalPlayer end --[[ The Module ]]-- local BaseCharacterController = _BaseCharacterController() local DynamicThumbstick = setmetatable({}, BaseCharacterController) DynamicThumbstick.__index = DynamicThumbstick function DynamicThumbstick.new() local self = setmetatable(BaseCharacterController.new(), DynamicThumbstick) self.moveTouchObject = nil self.moveTouchLockedIn = false self.moveTouchFirstChanged = false self.moveTouchStartPosition = nil self.startImage = nil self.endImage = nil self.middleImages = {} self.startImageFadeTween = nil self.endImageFadeTween = nil self.middleImageFadeTweens = {} self.isFirstTouch = true self.thumbstickFrame = nil self.onRenderSteppedConn = nil self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT self.hasFadedBackgroundInPortrait = false self.hasFadedBackgroundInLandscape = false self.tweenInAlphaStart = nil self.tweenOutAlphaStart = nil return self end -- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump -- rather than sustained jumping. This is only to preserve the current behavior through the refactor. function DynamicThumbstick:GetIsJumping() local wasJumping = self.isJumping self.isJumping = false return wasJumping end function DynamicThumbstick:Enable(enable, uiParentFrame) if enable == nil then return false end -- If nil, return false (invalid argument) enable = enable and true or false -- Force anything non-nil to boolean before comparison if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state if enable then -- Enable if not self.thumbstickFrame then self:Create(uiParentFrame) end self:BindContextActions() else ContextActionService:UnbindAction(DYNAMIC_THUMBSTICK_ACTION_NAME) -- Disable self:OnInputEnded() -- Cleanup end self.enabled = enable self.thumbstickFrame.Visible = enable end -- Was called OnMoveTouchEnded in previous version function DynamicThumbstick:OnInputEnded() self.moveTouchObject = nil self.moveVector = ZERO_VECTOR3 self:FadeThumbstick(false) end function DynamicThumbstick:FadeThumbstick(visible) if not visible and self.moveTouchObject then return end if self.isFirstTouch then return end if self.startImageFadeTween then self.startImageFadeTween:Cancel() end if self.endImageFadeTween then self.endImageFadeTween:Cancel() end for i = 1, #self.middleImages do if self.middleImageFadeTweens[i] then self.middleImageFadeTweens[i]:Cancel() end end if visible then self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 }) self.startImageFadeTween:Play() self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 }) self.endImageFadeTween:Play() for i = 1, #self.middleImages do self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] }) self.middleImageFadeTweens[i]:Play() end else self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) self.startImageFadeTween:Play() self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) self.endImageFadeTween:Play() for i = 1, #self.middleImages do self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 }) self.middleImageFadeTweens[i]:Play() end end end function DynamicThumbstick:FadeThumbstickFrame(fadeDuration, fadeRatio) self.fadeInAndOutHalfDuration = fadeDuration * 0.5 self.fadeInAndOutBalance = fadeRatio self.tweenInAlphaStart = tick() end function DynamicThumbstick:InputInFrame(inputObject) local frameCornerTopLeft = self.thumbstickFrame.AbsolutePosition local frameCornerBottomRight = frameCornerTopLeft + self.thumbstickFrame.AbsoluteSize local inputPosition = inputObject.Position if inputPosition.X >= frameCornerTopLeft.X and inputPosition.Y >= frameCornerTopLeft.Y then if inputPosition.X <= frameCornerBottomRight.X and inputPosition.Y <= frameCornerBottomRight.Y then return true end end return false end function DynamicThumbstick:DoFadeInBackground() local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui") local hasFadedBackgroundInOrientation = false -- only fade in/out the background once per orientation if playerGui then if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then hasFadedBackgroundInOrientation = self.hasFadedBackgroundInLandscape self.hasFadedBackgroundInLandscape = true elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then hasFadedBackgroundInOrientation = self.hasFadedBackgroundInPortrait self.hasFadedBackgroundInPortrait = true end end if not hasFadedBackgroundInOrientation then self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT self.tweenInAlphaStart = tick() end end function DynamicThumbstick:DoMove(direction) local currentMoveVector = direction -- Scaled Radial Dead Zone local inputAxisMagnitude = currentMoveVector.magnitude if inputAxisMagnitude < self.radiusOfDeadZone then currentMoveVector = ZERO_VECTOR3 else currentMoveVector = currentMoveVector.unit*( 1 - math.max(0, (self.radiusOfMaxSpeed - currentMoveVector.magnitude)/self.radiusOfMaxSpeed) ) currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y) end self.moveVector = currentMoveVector end function DynamicThumbstick:LayoutMiddleImages(startPos, endPos) local startDist = (self.thumbstickSize / 2) + self.middleSize local vector = endPos - startPos local distAvailable = vector.magnitude - (self.thumbstickRingSize / 2) - self.middleSize local direction = vector.unit local distNeeded = self.middleSpacing * NUM_MIDDLE_IMAGES local spacing = self.middleSpacing if distNeeded < distAvailable then spacing = distAvailable / NUM_MIDDLE_IMAGES end for i = 1, NUM_MIDDLE_IMAGES do local image = self.middleImages[i] local distWithout = startDist + (spacing * (i - 2)) local currentDist = startDist + (spacing * (i - 1)) if distWithout < distAvailable then local pos = endPos - direction * currentDist local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1) image.Visible = true image.Position = UDim2.new(0, pos.X, 0, pos.Y) image.Size = UDim2.new(0, self.middleSize * exposedFraction, 0, self.middleSize * exposedFraction) else image.Visible = false end end end function DynamicThumbstick:MoveStick(pos) local vector2StartPosition = Vector2.new(self.moveTouchStartPosition.X, self.moveTouchStartPosition.Y) local startPos = vector2StartPosition - self.thumbstickFrame.AbsolutePosition local endPos = Vector2.new(pos.X, pos.Y) - self.thumbstickFrame.AbsolutePosition self.endImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y) self:LayoutMiddleImages(startPos, endPos) end function DynamicThumbstick:BindContextActions() local function inputBegan(inputObject) if self.moveTouchObject then return Enum.ContextActionResult.Pass end if not self:InputInFrame(inputObject) then return Enum.ContextActionResult.Pass end if self.isFirstTouch then self.isFirstTouch = false local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0) TweenService:Create(self.startImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play() TweenService:Create( self.endImage, tweenInfo, {Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize), ImageColor3 = Color3.new(0,0,0)} ):Play() end self.moveTouchLockedIn = false self.moveTouchObject = inputObject self.moveTouchStartPosition = inputObject.Position self.moveTouchFirstChanged = true if FADE_IN_OUT_BACKGROUND then self:DoFadeInBackground() end return Enum.ContextActionResult.Pass end local function inputChanged(inputObject) if inputObject == self.moveTouchObject then if self.moveTouchFirstChanged then self.moveTouchFirstChanged = false local startPosVec2 = Vector2.new( inputObject.Position.X - self.thumbstickFrame.AbsolutePosition.X, inputObject.Position.Y - self.thumbstickFrame.AbsolutePosition.Y ) self.startImage.Visible = true self.startImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y) self.endImage.Visible = true self.endImage.Position = self.startImage.Position self:FadeThumbstick(true) self:MoveStick(inputObject.Position) end self.moveTouchLockedIn = true local direction = Vector2.new( inputObject.Position.x - self.moveTouchStartPosition.x, inputObject.Position.y - self.moveTouchStartPosition.y ) if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then self:DoMove(direction) self:MoveStick(inputObject.Position) end return Enum.ContextActionResult.Sink end return Enum.ContextActionResult.Pass end local function inputEnded(inputObject) if inputObject == self.moveTouchObject then self:OnInputEnded() if self.moveTouchLockedIn then return Enum.ContextActionResult.Sink end end return Enum.ContextActionResult.Pass end local function handleInput(actionName, inputState, inputObject) if inputState == Enum.UserInputState.Begin then return inputBegan(inputObject) elseif inputState == Enum.UserInputState.Change then return inputChanged(inputObject) elseif inputState == Enum.UserInputState.End then return inputEnded(inputObject) elseif inputState == Enum.UserInputState.Cancel then self:OnInputEnded() end end ContextActionService:BindActionAtPriority( DYNAMIC_THUMBSTICK_ACTION_NAME, handleInput, false, DYNAMIC_THUMBSTICK_ACTION_PRIORITY, Enum.UserInputType.Touch) end function DynamicThumbstick:Create(parentFrame) if self.thumbstickFrame then self.thumbstickFrame:Destroy() self.thumbstickFrame = nil if self.onRenderSteppedConn then self.onRenderSteppedConn:Disconnect() self.onRenderSteppedConn = nil end end self.thumbstickSize = 45 self.thumbstickRingSize = 20 self.middleSize = 10 self.middleSpacing = self.middleSize + 4 self.radiusOfDeadZone = 2 self.radiusOfMaxSpeed = 20 local screenSize = parentFrame.AbsoluteSize local isBigScreen = math.min(screenSize.x, screenSize.y) > 500 if isBigScreen then self.thumbstickSize = self.thumbstickSize * 2 self.thumbstickRingSize = self.thumbstickRingSize * 2 self.middleSize = self.middleSize * 2 self.middleSpacing = self.middleSpacing * 2 self.radiusOfDeadZone = self.radiusOfDeadZone * 2 self.radiusOfMaxSpeed = self.radiusOfMaxSpeed * 2 end local function layoutThumbstickFrame(portraitMode) if portraitMode then self.thumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0) self.thumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0) else self.thumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0) self.thumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0) end end self.thumbstickFrame = Instance.new("Frame") self.thumbstickFrame.BorderSizePixel = 0 self.thumbstickFrame.Name = "DynamicThumbstickFrame" self.thumbstickFrame.Visible = false self.thumbstickFrame.BackgroundTransparency = 1.0 self.thumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0) self.thumbstickFrame.Active = false layoutThumbstickFrame(false) self.startImage = Instance.new("ImageLabel") self.startImage.Name = "ThumbstickStart" self.startImage.Visible = true self.startImage.BackgroundTransparency = 1 self.startImage.Image = TOUCH_CONTROLS_SHEET self.startImage.ImageRectOffset = Vector2.new(1,1) self.startImage.ImageRectSize = Vector2.new(144, 144) self.startImage.ImageColor3 = Color3.new(0, 0, 0) self.startImage.AnchorPoint = Vector2.new(0.5, 0.5) self.startImage.Position = UDim2.new(0, self.thumbstickRingSize * 3.3, 1, -self.thumbstickRingSize * 2.8) self.startImage.Size = UDim2.new(0, self.thumbstickRingSize * 3.7, 0, self.thumbstickRingSize * 3.7) self.startImage.ZIndex = 10 self.startImage.Parent = self.thumbstickFrame self.endImage = Instance.new("ImageLabel") self.endImage.Name = "ThumbstickEnd" self.endImage.Visible = true self.endImage.BackgroundTransparency = 1 self.endImage.Image = TOUCH_CONTROLS_SHEET self.endImage.ImageRectOffset = Vector2.new(1,1) self.endImage.ImageRectSize = Vector2.new(144, 144) self.endImage.AnchorPoint = Vector2.new(0.5, 0.5) self.endImage.Position = self.startImage.Position self.endImage.Size = UDim2.new(0, self.thumbstickSize * 0.8, 0, self.thumbstickSize * 0.8) self.endImage.ZIndex = 10 self.endImage.Parent = self.thumbstickFrame for i = 1, NUM_MIDDLE_IMAGES do self.middleImages[i] = Instance.new("ImageLabel") self.middleImages[i].Name = "ThumbstickMiddle" self.middleImages[i].Visible = false self.middleImages[i].BackgroundTransparency = 1 self.middleImages[i].Image = TOUCH_CONTROLS_SHEET self.middleImages[i].ImageRectOffset = Vector2.new(1,1) self.middleImages[i].ImageRectSize = Vector2.new(144, 144) self.middleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i] self.middleImages[i].AnchorPoint = Vector2.new(0.5, 0.5) self.middleImages[i].ZIndex = 9 self.middleImages[i].Parent = self.thumbstickFrame end local CameraChangedConn = nil local function onCurrentCameraChanged() if CameraChangedConn then CameraChangedConn:Disconnect() CameraChangedConn = nil end local newCamera = workspace.CurrentCamera if newCamera then local function onViewportSizeChanged() local size = newCamera.ViewportSize local portraitMode = size.X < size.Y layoutThumbstickFrame(portraitMode) end CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged) onViewportSizeChanged() end end workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged) if workspace.CurrentCamera then onCurrentCameraChanged() end self.moveTouchStartPosition = nil self.startImageFadeTween = nil self.endImageFadeTween = nil self.middleImageFadeTweens = {} self.onRenderSteppedConn = RunService.RenderStepped:Connect(function() if self.tweenInAlphaStart ~= nil then local delta = tick() - self.tweenInAlphaStart local fadeInTime = (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance) self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeInTime, 1) if delta > fadeInTime then self.tweenOutAlphaStart = tick() self.tweenInAlphaStart = nil end elseif self.tweenOutAlphaStart ~= nil then local delta = tick() - self.tweenOutAlphaStart local fadeOutTime = (self.fadeInAndOutHalfDuration * 2) - (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance) self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA + FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeOutTime, 1) if delta > fadeOutTime then self.tweenOutAlphaStart = nil end end end) self.onTouchEndedConn = UserInputService.TouchEnded:connect(function(inputObject) if inputObject == self.moveTouchObject then self:OnInputEnded() end end) GuiService.MenuOpened:connect(function() if self.moveTouchObject then self:OnInputEnded() end end) local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui") while not playerGui do LocalPlayer.ChildAdded:wait() playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui") end local playerGuiChangedConn = nil local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight local function longShowBackground() self.fadeInAndOutHalfDuration = 2.5 self.fadeInAndOutBalance = 0.05 self.tweenInAlphaStart = tick() end playerGuiChangedConn = playerGui:GetPropertyChangedSignal("CurrentScreenOrientation"):Connect(function() if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or (not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then playerGuiChangedConn:disconnect() longShowBackground() if originalScreenOrientationWasLandscape then self.hasFadedBackgroundInPortrait = true else self.hasFadedBackgroundInLandscape = true end end end) self.thumbstickFrame.Parent = parentFrame if game:IsLoaded() then longShowBackground() else coroutine.wrap(function() game.Loaded:Wait() longShowBackground() end)() end end return DynamicThumbstick end function _Gamepad() local UserInputService = game:GetService("UserInputService") local ContextActionService = game:GetService("ContextActionService") --[[ Constants ]]-- local ZERO_VECTOR3 = Vector3.new(0,0,0) local NONE = Enum.UserInputType.None local thumbstickDeadzone = 0.2 --[[ The Module ]]-- local BaseCharacterController = _BaseCharacterController() local Gamepad = setmetatable({}, BaseCharacterController) Gamepad.__index = Gamepad function Gamepad.new(CONTROL_ACTION_PRIORITY) local self = setmetatable(BaseCharacterController.new(), Gamepad) self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY self.forwardValue = 0 self.backwardValue = 0 self.leftValue = 0 self.rightValue = 0 self.activeGamepad = NONE -- Enum.UserInputType.Gamepad1, 2, 3... self.gamepadConnectedConn = nil self.gamepadDisconnectedConn = nil return self end function Gamepad:Enable(enable) if not UserInputService.GamepadEnabled then return false end if enable == self.enabled then -- Module is already in the state being requested. True is returned here since the module will be in the state -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate -- no action was necessary. False indicates failure to be in requested/expected state. return true end self.forwardValue = 0 self.backwardValue = 0 self.leftValue = 0 self.rightValue = 0 self.moveVector = ZERO_VECTOR3 self.isJumping = false if enable then self.activeGamepad = self:GetHighestPriorityGamepad() if self.activeGamepad ~= NONE then self:BindContextActions() self:ConnectGamepadConnectionListeners() else -- No connected gamepads, failure to enable return false end else self:UnbindContextActions() self:DisconnectGamepadConnectionListeners() self.activeGamepad = NONE end self.enabled = enable return true end -- This function selects the lowest number gamepad from the currently-connected gamepad -- and sets it as the active gamepad function Gamepad:GetHighestPriorityGamepad() local connectedGamepads = UserInputService:GetConnectedGamepads() local bestGamepad = NONE -- Note that this value is higher than all valid gamepad values for _, gamepad in pairs(connectedGamepads) do if gamepad.Value < bestGamepad.Value then bestGamepad = gamepad end end return bestGamepad end function Gamepad:BindContextActions() if self.activeGamepad == NONE then -- There must be an active gamepad to set up bindings return false end local handleJumpAction = function(actionName, inputState, inputObject) self.isJumping = (inputState == Enum.UserInputState.Begin) return Enum.ContextActionResult.Sink end local handleThumbstickInput = function(actionName, inputState, inputObject) if inputState == Enum.UserInputState.Cancel then self.moveVector = ZERO_VECTOR3 return Enum.ContextActionResult.Sink end if self.activeGamepad ~= inputObject.UserInputType then return Enum.ContextActionResult.Pass end if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end if inputObject.Position.magnitude > thumbstickDeadzone then self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y) else self.moveVector = ZERO_VECTOR3 end return Enum.ContextActionResult.Sink end ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA) ContextActionService:BindActionAtPriority("moveThumbstick", handleThumbstickInput, false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Thumbstick1) return true end function Gamepad:UnbindContextActions() if self.activeGamepad ~= NONE then ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) end ContextActionService:UnbindAction("moveThumbstick") ContextActionService:UnbindAction("jumpAction") end function Gamepad:OnNewGamepadConnected() -- A new gamepad has been connected. local bestGamepad = self:GetHighestPriorityGamepad() if bestGamepad == self.activeGamepad then -- A new gamepad was connected, but our active gamepad is not changing return end if bestGamepad == NONE then -- There should be an active gamepad when GamepadConnected fires, so this should not -- normally be hit. If there is no active gamepad, unbind actions but leave -- the module enabled and continue to listen for a new gamepad connection. warn("Gamepad:OnNewGamepadConnected found no connected gamepads") self:UnbindContextActions() return end if self.activeGamepad ~= NONE then -- Switching from one active gamepad to another self:UnbindContextActions() end self.activeGamepad = bestGamepad self:BindContextActions() end function Gamepad:OnCurrentGamepadDisconnected() if self.activeGamepad ~= NONE then ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) end local bestGamepad = self:GetHighestPriorityGamepad() if self.activeGamepad ~= NONE and bestGamepad == self.activeGamepad then warn("Gamepad:OnCurrentGamepadDisconnected found the supposedly disconnected gamepad in connectedGamepads.") self:UnbindContextActions() self.activeGamepad = NONE return end if bestGamepad == NONE then -- No active gamepad, unbinding actions but leaving gamepad connection listener active self:UnbindContextActions() self.activeGamepad = NONE else -- Set new gamepad as active and bind to tool activation self.activeGamepad = bestGamepad ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) end end function Gamepad:ConnectGamepadConnectionListeners() self.gamepadConnectedConn = UserInputService.GamepadConnected:Connect(function(gamepadEnum) self:OnNewGamepadConnected() end) self.gamepadDisconnectedConn = UserInputService.GamepadDisconnected:Connect(function(gamepadEnum) if self.activeGamepad == gamepadEnum then self:OnCurrentGamepadDisconnected() end end) end function Gamepad:DisconnectGamepadConnectionListeners() if self.gamepadConnectedConn then self.gamepadConnectedConn:Disconnect() self.gamepadConnectedConn = nil end if self.gamepadDisconnectedConn then self.gamepadDisconnectedConn:Disconnect() self.gamepadDisconnectedConn = nil end end return Gamepad end function _Keyboard() --[[ Roblox Services ]]-- local UserInputService = game:GetService("UserInputService") local ContextActionService = game:GetService("ContextActionService") --[[ Constants ]]-- local ZERO_VECTOR3 = Vector3.new(0,0,0) --[[ The Module ]]-- local BaseCharacterController = _BaseCharacterController() local Keyboard = setmetatable({}, BaseCharacterController) Keyboard.__index = Keyboard function Keyboard.new(CONTROL_ACTION_PRIORITY) local self = setmetatable(BaseCharacterController.new(), Keyboard) self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY self.textFocusReleasedConn = nil self.textFocusGainedConn = nil self.windowFocusReleasedConn = nil self.forwardValue = 0 self.backwardValue = 0 self.leftValue = 0 self.rightValue = 0 self.jumpEnabled = true return self end function Keyboard:Enable(enable) if not UserInputService.KeyboardEnabled then return false end if enable == self.enabled then -- Module is already in the state being requested. True is returned here since the module will be in the state -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate -- no action was necessary. False indicates failure to be in requested/expected state. return true end self.forwardValue = 0 self.backwardValue = 0 self.leftValue = 0 self.rightValue = 0 self.moveVector = ZERO_VECTOR3 self.jumpRequested = false self:UpdateJump() if enable then self:BindContextActions() self:ConnectFocusEventListeners() else self:UnbindContextActions() self:DisconnectFocusEventListeners() end self.enabled = enable return true end function Keyboard:UpdateMovement(inputState) if inputState == Enum.UserInputState.Cancel then self.moveVector = ZERO_VECTOR3 else self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue) end end function Keyboard:UpdateJump() self.isJumping = self.jumpRequested end function Keyboard:BindContextActions() -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are, -- which fixes them from getting stuck on. -- We return ContextActionResult.Pass here for legacy reasons. -- Many games rely on gameProcessedEvent being false on UserInputService.InputBegan for these control actions. local handleMoveForward = function(actionName, inputState, inputObject) self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 self:UpdateMovement(inputState) return Enum.ContextActionResult.Pass end local handleMoveBackward = function(actionName, inputState, inputObject) self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 self:UpdateMovement(inputState) return Enum.ContextActionResult.Pass end local handleMoveLeft = function(actionName, inputState, inputObject) self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 self:UpdateMovement(inputState) return Enum.ContextActionResult.Pass end local handleMoveRight = function(actionName, inputState, inputObject) self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 self:UpdateMovement(inputState) return Enum.ContextActionResult.Pass end local handleJumpAction = function(actionName, inputState, inputObject) self.jumpRequested = self.jumpEnabled and (inputState == Enum.UserInputState.Begin) self:UpdateJump() return Enum.ContextActionResult.Pass end -- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to -- movement direction is done in Lua ContextActionService:BindActionAtPriority("moveForwardAction", handleMoveForward, false, self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterForward) ContextActionService:BindActionAtPriority("moveBackwardAction", handleMoveBackward, false, self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterBackward) ContextActionService:BindActionAtPriority("moveLeftAction", handleMoveLeft, false, self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterLeft) ContextActionService:BindActionAtPriority("moveRightAction", handleMoveRight, false, self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterRight) ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false, self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterJump) end function Keyboard:UnbindContextActions() ContextActionService:UnbindAction("moveForwardAction") ContextActionService:UnbindAction("moveBackwardAction") ContextActionService:UnbindAction("moveLeftAction") ContextActionService:UnbindAction("moveRightAction") ContextActionService:UnbindAction("jumpAction") end function Keyboard:ConnectFocusEventListeners() local function onFocusReleased() self.moveVector = ZERO_VECTOR3 self.forwardValue = 0 self.backwardValue = 0 self.leftValue = 0 self.rightValue = 0 self.jumpRequested = false self:UpdateJump() end local function onTextFocusGained(textboxFocused) self.jumpRequested = false self:UpdateJump() end self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased) self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained) self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased) end function Keyboard:DisconnectFocusEventListeners() if self.textFocusReleasedCon then self.textFocusReleasedCon:Disconnect() self.textFocusReleasedCon = nil end if self.textFocusGainedConn then self.textFocusGainedConn:Disconnect() self.textFocusGainedConn = nil end if self.windowFocusReleasedConn then self.windowFocusReleasedConn:Disconnect() self.windowFocusReleasedConn = nil end end return Keyboard end function _ControlModule() local ControlModule = {} ControlModule.__index = ControlModule --[[ Roblox Services ]]-- local Players = game:GetService("Players") local RunService = game:GetService("RunService") local UserInputService = game:GetService("UserInputService") local Workspace = game:GetService("Workspace") local UserGameSettings = UserSettings():GetService("UserGameSettings") -- Roblox User Input Control Modules - each returns a new() constructor function used to create controllers as needed local Keyboard = _Keyboard() local Gamepad = _Gamepad() local DynamicThumbstick = _DynamicThumbstick() local FFlagUserMakeThumbstickDynamic do local success, value = pcall(function() return UserSettings():IsUserFeatureEnabled("UserMakeThumbstickDynamic") end) FFlagUserMakeThumbstickDynamic = success and value end local TouchThumbstick = FFlagUserMakeThumbstickDynamic and DynamicThumbstick or _TouchThumbstick() -- These controllers handle only walk/run movement, jumping is handled by the -- TouchJump controller if any of these are active local ClickToMove = _ClickToMoveController() local TouchJump = _TouchJump() local VehicleController = _VehicleController() local CONTROL_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value -- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching local movementEnumToModuleMap = { [Enum.TouchMovementMode.DPad] = DynamicThumbstick, [Enum.DevTouchMovementMode.DPad] = DynamicThumbstick, [Enum.TouchMovementMode.Thumbpad] = DynamicThumbstick, [Enum.DevTouchMovementMode.Thumbpad] = DynamicThumbstick, [Enum.TouchMovementMode.Thumbstick] = TouchThumbstick, [Enum.DevTouchMovementMode.Thumbstick] = TouchThumbstick, [Enum.TouchMovementMode.DynamicThumbstick] = DynamicThumbstick, [Enum.DevTouchMovementMode.DynamicThumbstick] = DynamicThumbstick, [Enum.TouchMovementMode.ClickToMove] = ClickToMove, [Enum.DevTouchMovementMode.ClickToMove] = ClickToMove, -- Current default [Enum.TouchMovementMode.Default] = DynamicThumbstick, [Enum.ComputerMovementMode.Default] = Keyboard, [Enum.ComputerMovementMode.KeyboardMouse] = Keyboard, [Enum.DevComputerMovementMode.KeyboardMouse] = Keyboard, [Enum.DevComputerMovementMode.Scriptable] = nil, [Enum.ComputerMovementMode.ClickToMove] = ClickToMove, [Enum.DevComputerMovementMode.ClickToMove] = ClickToMove, } -- Keyboard controller is really keyboard and mouse controller local computerInputTypeToModuleMap = { [Enum.UserInputType.Keyboard] = Keyboard, [Enum.UserInputType.MouseButton1] = Keyboard, [Enum.UserInputType.MouseButton2] = Keyboard, [Enum.UserInputType.MouseButton3] = Keyboard, [Enum.UserInputType.MouseWheel] = Keyboard, [Enum.UserInputType.MouseMovement] = Keyboard, [Enum.UserInputType.Gamepad1] = Gamepad, [Enum.UserInputType.Gamepad2] = Gamepad, [Enum.UserInputType.Gamepad3] = Gamepad, [Enum.UserInputType.Gamepad4] = Gamepad, } local lastInputType function ControlModule.new() local self = setmetatable({},ControlModule) -- The Modules above are used to construct controller instances as-needed, and this -- table is a map from Module to the instance created from it self.controllers = {} self.activeControlModule = nil -- Used to prevent unnecessarily expensive checks on each input event self.activeController = nil self.touchJumpController = nil self.moveFunction = Players.LocalPlayer.Move self.humanoid = nil self.lastInputType = Enum.UserInputType.None -- For Roblox self.vehicleController self.humanoidSeatedConn = nil self.vehicleController = nil self.touchControlFrame = nil self.vehicleController = VehicleController.new(CONTROL_ACTION_PRIORITY) Players.LocalPlayer.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char) end) Players.LocalPlayer.CharacterRemoving:Connect(function(char) self:OnCharacterRemoving(char) end) if Players.LocalPlayer.Character then self:OnCharacterAdded(Players.LocalPlayer.Character) end RunService:BindToRenderStep("ControlScriptRenderstep", Enum.RenderPriority.Input.Value, function(dt) self:OnRenderStepped(dt) end) UserInputService.LastInputTypeChanged:Connect(function(newLastInputType) self:OnLastInputTypeChanged(newLastInputType) end) UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function() self:OnTouchMovementModeChange() end) Players.LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function() self:OnTouchMovementModeChange() end) UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(function() self:OnComputerMovementModeChange() end) Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function() self:OnComputerMovementModeChange() end) --[[ Touch Device UI ]]-- self.playerGui = nil self.touchGui = nil self.playerGuiAddedConn = nil if UserInputService.TouchEnabled then self.playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui") if self.playerGui then self:CreateTouchGuiContainer() self:OnLastInputTypeChanged(UserInputService:GetLastInputType()) else self.playerGuiAddedConn = Players.LocalPlayer.ChildAdded:Connect(function(child) if child:IsA("PlayerGui") then self.playerGui = child self:CreateTouchGuiContainer() self.playerGuiAddedConn:Disconnect() self.playerGuiAddedConn = nil self:OnLastInputTypeChanged(UserInputService:GetLastInputType()) end end) end else self:OnLastInputTypeChanged(UserInputService:GetLastInputType()) end return self end -- Convenience function so that calling code does not have to first get the activeController -- and then call GetMoveVector on it. When there is no active controller, this function returns -- nil so that this case can be distinguished from no current movement (which returns zero vector). function ControlModule:GetMoveVector() if self.activeController then return self.activeController:GetMoveVector() end return Vector3.new(0,0,0) end function ControlModule:GetActiveController() return self.activeController end function ControlModule:EnableActiveControlModule() if self.activeControlModule == ClickToMove then -- For ClickToMove, when it is the player's choice, we also enable the full keyboard controls. -- When the developer is forcing click to move, the most keyboard controls (WASD) are not available, only jump. self.activeController:Enable( true, Players.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice, self.touchJumpController ) elseif self.touchControlFrame then self.activeController:Enable(true, self.touchControlFrame) else self.activeController:Enable(true) end end function ControlModule:Enable(enable) if not self.activeController then return end if enable == nil then enable = true end if enable then self:EnableActiveControlModule() else self:Disable() end end -- For those who prefer distinct functions function ControlModule:Disable() if self.activeController then self.activeController:Enable(false) if self.moveFunction then self.moveFunction(Players.LocalPlayer, Vector3.new(0,0,0), true) end end end -- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable function ControlModule:SelectComputerMovementModule() if not (UserInputService.KeyboardEnabled or UserInputService.GamepadEnabled) then return nil, false end local computerModule local DevMovementMode = Players.LocalPlayer.DevComputerMovementMode if DevMovementMode == Enum.DevComputerMovementMode.UserChoice then computerModule = computerInputTypeToModuleMap[lastInputType] if UserGameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove and computerModule == Keyboard then -- User has ClickToMove set in Settings, prefer ClickToMove controller for keyboard and mouse lastInputTypes computerModule = ClickToMove end else -- Developer has selected a mode that must be used. computerModule = movementEnumToModuleMap[DevMovementMode] -- computerModule is expected to be nil here only when developer has selected Scriptable if (not computerModule) and DevMovementMode ~= Enum.DevComputerMovementMode.Scriptable then warn("No character control module is associated with DevComputerMovementMode ", DevMovementMode) end end if computerModule then return computerModule, true elseif DevMovementMode == Enum.DevComputerMovementMode.Scriptable then -- Special case where nil is returned and we actually want to set self.activeController to nil for Scriptable return nil, true else -- This case is for when computerModule is nil because of an error and no suitable control module could -- be found. return nil, false end end -- Choose current Touch control module based on settings (user, dev) -- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable function ControlModule:SelectTouchModule() if not UserInputService.TouchEnabled then return nil, false end local touchModule local DevMovementMode = Players.LocalPlayer.DevTouchMovementMode if DevMovementMode == Enum.DevTouchMovementMode.UserChoice then touchModule = movementEnumToModuleMap[UserGameSettings.TouchMovementMode] elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then return nil, true else touchModule = movementEnumToModuleMap[DevMovementMode] end return touchModule, true end local function calculateRawMoveVector(humanoid, cameraRelativeMoveVector) local camera = Workspace.CurrentCamera if not camera then return cameraRelativeMoveVector end if humanoid:GetState() == Enum.HumanoidStateType.Swimming then return camera.CFrame:VectorToWorldSpace(cameraRelativeMoveVector) end local c, s local _, _, _, R00, R01, R02, _, _, R12, _, _, R22 = camera.CFrame:GetComponents() if R12 < 1 and R12 > -1 then -- X and Z components from back vector. c = R22 s = R02 else -- In this case the camera is looking straight up or straight down. -- Use X components from right and up vectors. c = R00 s = -R01*math.sign(R12) end local norm = math.sqrt(c*c + s*s) return Vector3.new( (c*cameraRelativeMoveVector.x + s*cameraRelativeMoveVector.z)/norm, 0, (c*cameraRelativeMoveVector.z - s*cameraRelativeMoveVector.x)/norm ) end function ControlModule:OnRenderStepped(dt) if self.activeController and self.activeController.enabled and self.humanoid then -- Give the controller a chance to adjust its state self.activeController:OnRenderStepped(dt) -- Now retrieve info from the controller local moveVector = self.activeController:GetMoveVector() local cameraRelative = self.activeController:IsMoveVectorCameraRelative() local clickToMoveController = self:GetClickToMoveController() if self.activeController ~= clickToMoveController then if moveVector.magnitude > 0 then -- Clean up any developer started MoveTo path clickToMoveController:CleanupPath() else -- Get move vector for developer started MoveTo clickToMoveController:OnRenderStepped(dt) moveVector = clickToMoveController:GetMoveVector() cameraRelative = clickToMoveController:IsMoveVectorCameraRelative() end end -- Are we driving a vehicle ? local vehicleConsumedInput = false if self.vehicleController then moveVector, vehicleConsumedInput = self.vehicleController:Update(moveVector, cameraRelative, self.activeControlModule==Gamepad) end -- If not, move the player -- Verification of vehicleConsumedInput is commented out to preserve legacy behavior, -- in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat --if not vehicleConsumedInput then if cameraRelative then moveVector = calculateRawMoveVector(self.humanoid, moveVector) end self.moveFunction(Players.LocalPlayer, moveVector, false) --end -- And make them jump if needed self.humanoid.Jump = self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping()) end end function ControlModule:OnHumanoidSeated(active, currentSeatPart) if active then if currentSeatPart and currentSeatPart:IsA("VehicleSeat") then if not self.vehicleController then self.vehicleController = self.vehicleController.new(CONTROL_ACTION_PRIORITY) end self.vehicleController:Enable(true, currentSeatPart) end else if self.vehicleController then self.vehicleController:Enable(false, currentSeatPart) end end end function ControlModule:OnCharacterAdded(char) self.humanoid = char:FindFirstChildOfClass("Humanoid") while not self.humanoid do char.ChildAdded:wait() self.humanoid = char:FindFirstChildOfClass("Humanoid") end if self.touchGui then self.touchGui.Enabled = true end if self.humanoidSeatedConn then self.humanoidSeatedConn:Disconnect() self.humanoidSeatedConn = nil end self.humanoidSeatedConn = self.humanoid.Seated:Connect(function(active, currentSeatPart) self:OnHumanoidSeated(active, currentSeatPart) end) end function ControlModule:OnCharacterRemoving(char) self.humanoid = nil if self.touchGui then self.touchGui.Enabled = false end end -- Helper function to lazily instantiate a controller if it does not yet exist, -- disable the active controller if it is different from the on being switched to, -- and then enable the requested controller. The argument to this function must be -- a reference to one of the control modules, i.e. Keyboard, Gamepad, etc. function ControlModule:SwitchToController(controlModule) if not controlModule then if self.activeController then self.activeController:Enable(false) end self.activeController = nil self.activeControlModule = nil else if not self.controllers[controlModule] then self.controllers[controlModule] = controlModule.new(CONTROL_ACTION_PRIORITY) end if self.activeController ~= self.controllers[controlModule] then if self.activeController then self.activeController:Enable(false) end self.activeController = self.controllers[controlModule] self.activeControlModule = controlModule -- Only used to check if controller switch is necessary if self.touchControlFrame and (self.activeControlModule == ClickToMove or self.activeControlModule == TouchThumbstick or self.activeControlModule == DynamicThumbstick) then if not self.controllers[TouchJump] then self.controllers[TouchJump] = TouchJump.new() end self.touchJumpController = self.controllers[TouchJump] self.touchJumpController:Enable(true, self.touchControlFrame) else if self.touchJumpController then self.touchJumpController:Enable(false) end end self:EnableActiveControlModule() end end end function ControlModule:OnLastInputTypeChanged(newLastInputType) if lastInputType == newLastInputType then warn("LastInputType Change listener called with current type.") end lastInputType = newLastInputType if lastInputType == Enum.UserInputType.Touch then -- TODO: Check if touch module already active local touchModule, success = self:SelectTouchModule() if success then while not self.touchControlFrame do wait() end self:SwitchToController(touchModule) end elseif computerInputTypeToModuleMap[lastInputType] ~= nil then local computerModule = self:SelectComputerMovementModule() if computerModule then self:SwitchToController(computerModule) end end end -- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of -- current control scheme function ControlModule:OnComputerMovementModeChange() local controlModule, success = self:SelectComputerMovementModule() if success then self:SwitchToController(controlModule) end end function ControlModule:OnTouchMovementModeChange() local touchModule, success = self:SelectTouchModule() if success then while not self.touchControlFrame do wait() end self:SwitchToController(touchModule) end end function ControlModule:CreateTouchGuiContainer() if self.touchGui then self.touchGui:Destroy() end -- Container for all touch device guis self.touchGui = Instance.new("ScreenGui") self.touchGui.Name = "TouchGui" self.touchGui.ResetOnSpawn = false self.touchGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling self.touchGui.Enabled = self.humanoid ~= nil self.touchControlFrame = Instance.new("Frame") self.touchControlFrame.Name = "TouchControlFrame" self.touchControlFrame.Size = UDim2.new(1, 0, 1, 0) self.touchControlFrame.BackgroundTransparency = 1 self.touchControlFrame.Parent = self.touchGui self.touchGui.Parent = self.playerGui end function ControlModule:GetClickToMoveController() if not self.controllers[ClickToMove] then self.controllers[ClickToMove] = ClickToMove.new(CONTROL_ACTION_PRIORITY) end return self.controllers[ClickToMove] end function ControlModule:IsJumping() if self.activeController then return self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping()) end return false end return ControlModule.new() end function _PlayerModule() local PlayerModule = {} PlayerModule.__index = PlayerModule function PlayerModule.new() local self = setmetatable({},PlayerModule) self.cameras = _CameraModule() self.controls = _ControlModule() return self end function PlayerModule:GetCameras() return self.cameras end function PlayerModule:GetControls() return self.controls end function PlayerModule:GetClickToMoveController() return self.controls:GetClickToMoveController() end return PlayerModule.new() end function _sounds() local SetState = Instance.new("BindableEvent",script) local Players = game:GetService("Players") local RunService = game:GetService("RunService") local SOUND_DATA = { Climbing = { SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3", Looped = true, }, Died = { SoundId = "rbxasset://sounds/uuhhh.mp3", }, FreeFalling = { SoundId = "rbxasset://sounds/action_falling.mp3", Looped = true, }, GettingUp = { SoundId = "rbxasset://sounds/action_get_up.mp3", }, Jumping = { SoundId = "rbxasset://sounds/action_jump.mp3", }, Landing = { SoundId = "rbxasset://sounds/action_jump_land.mp3", }, Running = { SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3", Looped = true, Pitch = 1.85, }, Splash = { SoundId = "rbxasset://sounds/impact_water.mp3", }, Swimming = { SoundId = "rbxasset://sounds/action_swim.mp3", Looped = true, Pitch = 1.6, }, } -- wait for the first of the passed signals to fire local function waitForFirst(...) local shunt = Instance.new("BindableEvent") local slots = {...} local function fire(...) for i = 1, #slots do slots[i]:Disconnect() end return shunt:Fire(...) end for i = 1, #slots do slots[i] = slots[i]:Connect(fire) end return shunt.Event:Wait() end -- map a value from one range to another local function map(x, inMin, inMax, outMin, outMax) return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin end local function playSound(sound) sound.TimePosition = 0 sound.Playing = true end local function stopSound(sound) sound.Playing = false sound.TimePosition = 0 end local function shallowCopy(t) local out = {} for k, v in pairs(t) do out[k] = v end return out end local function initializeSoundSystem(player, humanoid, rootPart) local sounds = {} -- initialize sounds for name, props in pairs(SOUND_DATA) do local sound = Instance.new("Sound") sound.Name = name -- set default values sound.Archivable = false sound.EmitterSize = 5 sound.MaxDistance = 150 sound.Volume = 0.65 for propName, propValue in pairs(props) do sound[propName] = propValue end sound.Parent = rootPart sounds[name] = sound end local playingLoopedSounds = {} local function stopPlayingLoopedSounds(except) for sound in pairs(shallowCopy(playingLoopedSounds)) do if sound ~= except then sound.Playing = false playingLoopedSounds[sound] = nil end end end -- state transition callbacks local stateTransitions = { [Enum.HumanoidStateType.FallingDown] = function() stopPlayingLoopedSounds() end, [Enum.HumanoidStateType.GettingUp] = function() stopPlayingLoopedSounds() playSound(sounds.GettingUp) end, [Enum.HumanoidStateType.Jumping] = function() stopPlayingLoopedSounds() playSound(sounds.Jumping) end, [Enum.HumanoidStateType.Swimming] = function() local verticalSpeed = math.abs(rootPart.Velocity.Y) if verticalSpeed > 0.1 then sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1) playSound(sounds.Splash) end stopPlayingLoopedSounds(sounds.Swimming) sounds.Swimming.Playing = true playingLoopedSounds[sounds.Swimming] = true end, [Enum.HumanoidStateType.Freefall] = function() sounds.FreeFalling.Volume = 0 stopPlayingLoopedSounds(sounds.FreeFalling) playingLoopedSounds[sounds.FreeFalling] = true end, [Enum.HumanoidStateType.Landed] = function() stopPlayingLoopedSounds() local verticalSpeed = math.abs(rootPart.Velocity.Y) if verticalSpeed > 75 then sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1) playSound(sounds.Landing) end end, [Enum.HumanoidStateType.Running] = function() stopPlayingLoopedSounds(sounds.Running) sounds.Running.Playing = true playingLoopedSounds[sounds.Running] = true end, [Enum.HumanoidStateType.Climbing] = function() local sound = sounds.Climbing if math.abs(rootPart.Velocity.Y) > 0.1 then sound.Playing = true stopPlayingLoopedSounds(sound) else stopPlayingLoopedSounds() end playingLoopedSounds[sound] = true end, [Enum.HumanoidStateType.Seated] = function() stopPlayingLoopedSounds() end, [Enum.HumanoidStateType.Dead] = function() stopPlayingLoopedSounds() playSound(sounds.Died) end, } -- updaters for looped sounds local loopedSoundUpdaters = { [sounds.Climbing] = function(dt, sound, vel) sound.Playing = vel.Magnitude > 0.1 end, [sounds.FreeFalling] = function(dt, sound, vel) if vel.Magnitude > 75 then sound.Volume = math.clamp(sound.Volume + 0.9*dt, 0, 1) else sound.Volume = 0 end end, [sounds.Running] = function(dt, sound, vel) sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5 end, } -- state substitutions to avoid duplicating entries in the state table local stateRemap = { [Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running, } local activeState = stateRemap[humanoid:GetState()] or humanoid:GetState() local activeConnections = {} local stateChangedConn = humanoid.StateChanged:Connect(function(_, state) state = stateRemap[state] or state if state ~= activeState then local transitionFunc = stateTransitions[state] if transitionFunc then transitionFunc() end activeState = state end end) local customStateChangedConn = SetState.Event:Connect(function(state) state = stateRemap[state] or state if state ~= activeState then local transitionFunc = stateTransitions[state] if transitionFunc then transitionFunc() end activeState = state end end) local steppedConn = RunService.Stepped:Connect(function(_, worldDt) -- update looped sounds on stepped for sound in pairs(playingLoopedSounds) do local updater = loopedSoundUpdaters[sound] if updater then updater(worldDt, sound, rootPart.Velocity) end end end) local humanoidAncestryChangedConn local rootPartAncestryChangedConn local characterAddedConn local function terminate() stateChangedConn:Disconnect() customStateChangedConn:Disconnect() steppedConn:Disconnect() humanoidAncestryChangedConn:Disconnect() rootPartAncestryChangedConn:Disconnect() characterAddedConn:Disconnect() end humanoidAncestryChangedConn = humanoid.AncestryChanged:Connect(function(_, parent) if not parent then terminate() end end) rootPartAncestryChangedConn = rootPart.AncestryChanged:Connect(function(_, parent) if not parent then terminate() end end) characterAddedConn = player.CharacterAdded:Connect(terminate) end local function playerAdded(player) local function characterAdded(character) -- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications: -- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically. -- ** must use a waitForFirst on everything and listen for hierarchy changes. -- * the character might not be in the dm by the time CharacterAdded fires -- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again -- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented. -- * RootPart probably won't exist immediately. -- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented. if not character.Parent then waitForFirst(character.AncestryChanged, player.CharacterAdded) end if player.Character ~= character or not character.Parent then return end local humanoid = character:FindFirstChildOfClass("Humanoid") while character:IsDescendantOf(game) and not humanoid do waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded) humanoid = character:FindFirstChildOfClass("Humanoid") end if player.Character ~= character or not character:IsDescendantOf(game) then return end -- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals local rootPart = character:FindFirstChild("HumanoidRootPart") while character:IsDescendantOf(game) and not rootPart do waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded) rootPart = character:FindFirstChild("HumanoidRootPart") end if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then initializeSoundSystem(player, humanoid, rootPart) end end if player.Character then characterAdded(player.Character) end player.CharacterAdded:Connect(characterAdded) end Players.PlayerAdded:Connect(playerAdded) for _, player in ipairs(Players:GetPlayers()) do playerAdded(player) end return SetState end function _StateTracker() local EPSILON = 0.1 local SPEED = { ["onRunning"] = true, ["onClimbing"] = true } local INAIR = { ["onFreeFall"] = true, ["onJumping"] = true } local STATEMAP = { ["onRunning"] = Enum.HumanoidStateType.Running, ["onJumping"] = Enum.HumanoidStateType.Jumping, ["onFreeFall"] = Enum.HumanoidStateType.Freefall } local StateTracker = {} StateTracker.__index = StateTracker function StateTracker.new(humanoid, soundState) local self = setmetatable({}, StateTracker) self.Humanoid = humanoid self.HRP = humanoid.RootPart self.Speed = 0 self.State = "onRunning" self.Jumped = false self.JumpTick = tick() self.SoundState = soundState self._ChangedEvent = Instance.new("BindableEvent") self.Changed = self._ChangedEvent.Event return self end function StateTracker:Destroy() self._ChangedEvent:Destroy() end function StateTracker:RequestedJump() self.Jumped = true self.JumpTick = tick() end function StateTracker:OnStep(gravityUp, grounded, isMoving) local cVelocity = self.HRP.Velocity local gVelocity = cVelocity:Dot(gravityUp) local oldState, oldSpeed = self.State, self.Speed local newState local newSpeed = cVelocity.Magnitude if (not grounded) then if (gVelocity > 0) then if (self.Jumped) then newState = "onJumping" else newState = "onFreeFall" end else if (self.Jumped) then self.Jumped = false end newState = "onFreeFall" end else if (self.Jumped and tick() - self.JumpTick > 0.1) then self.Jumped = false end newSpeed = (cVelocity - gVelocity*gravityUp).Magnitude newState = "onRunning" end newSpeed = isMoving and newSpeed or 0 if (oldState ~= newState or (SPEED[newState] and math.abs(oldSpeed - newSpeed) > EPSILON)) then self.State = newState self.Speed = newSpeed self.SoundState:Fire(STATEMAP[newState]) self._ChangedEvent:Fire(self.State, self.Speed) end end return StateTracker end function _InitObjects() local model = workspace:FindFirstChild("objects") or game:GetObjects("rbxassetid://5045408489")[1] local SPHERE = model:WaitForChild("Sphere") local FLOOR = model:WaitForChild("Floor") local VFORCE = model:WaitForChild("VectorForce") local BGYRO = model:WaitForChild("BodyGyro") local function initObjects(self) local hrp = self.HRP local humanoid = self.Humanoid local sphere = SPHERE:Clone() sphere.Parent = self.Character local floor = FLOOR:Clone() floor.Parent = self.Character local isR15 = (humanoid.RigType == Enum.HumanoidRigType.R15) local height = isR15 and (humanoid.HipHeight + 0.05) or 2 local weld = Instance.new("Weld") weld.C0 = CFrame.new(0, -height, 0.1) weld.Part0 = hrp weld.Part1 = sphere weld.Parent = sphere local weld2 = Instance.new("Weld") weld2.C0 = CFrame.new(0, -(height + 1.5), 0) weld2.Part0 = hrp weld2.Part1 = floor weld2.Parent = floor local gyro = BGYRO:Clone() gyro.CFrame = hrp.CFrame gyro.Parent = hrp local vForce = VFORCE:Clone() vForce.Attachment0 = isR15 and hrp:WaitForChild("RootRigAttachment") or hrp:WaitForChild("RootAttachment") vForce.Parent = hrp return sphere, gyro, vForce, floor end return initObjects end local plr = game.Players.LocalPlayer local ms = plr:GetMouse() local char plr.CharacterAdded:Connect(function(c) char = c end) function _R6() function r6() local Figure = char local Torso = Figure:WaitForChild("Torso") local RightShoulder = Torso:WaitForChild("Right Shoulder") local LeftShoulder = Torso:WaitForChild("Left Shoulder") local RightHip = Torso:WaitForChild("Right Hip") local LeftHip = Torso:WaitForChild("Left Hip") local Neck = Torso:WaitForChild("Neck") local Humanoid = Figure:WaitForChild("Humanoid") local pose = "Standing" local currentAnim = "" local currentAnimInstance = nil local currentAnimTrack = nil local currentAnimKeyframeHandler = nil local currentAnimSpeed = 1.0 local animTable = {} local animNames = { idle = { { id = "http://www.roblox.com/asset/?id=180435571", weight = 9 }, { id = "http://www.roblox.com/asset/?id=180435792", weight = 1 } }, walk = { { id = "http://www.roblox.com/asset/?id=180426354", weight = 10 } }, run = { { id = "run.xml", weight = 10 } }, jump = { { id = "http://www.roblox.com/asset/?id=125750702", weight = 10 } }, fall = { { id = "http://www.roblox.com/asset/?id=180436148", weight = 10 } }, climb = { { id = "http://www.roblox.com/asset/?id=180436334", weight = 10 } }, sit = { { id = "http://www.roblox.com/asset/?id=178130996", weight = 10 } }, toolnone = { { id = "http://www.roblox.com/asset/?id=182393478", weight = 10 } }, toolslash = { { id = "http://www.roblox.com/asset/?id=129967390", weight = 10 } -- { id = "slash.xml", weight = 10 } }, toollunge = { { id = "http://www.roblox.com/asset/?id=129967478", weight = 10 } }, wave = { { id = "http://www.roblox.com/asset/?id=128777973", weight = 10 } }, point = { { id = "http://www.roblox.com/asset/?id=128853357", weight = 10 } }, dance1 = { { id = "http://www.roblox.com/asset/?id=182435998", weight = 10 }, { id = "http://www.roblox.com/asset/?id=182491037", weight = 10 }, { id = "http://www.roblox.com/asset/?id=182491065", weight = 10 } }, dance2 = { { id = "http://www.roblox.com/asset/?id=182436842", weight = 10 }, { id = "http://www.roblox.com/asset/?id=182491248", weight = 10 }, { id = "http://www.roblox.com/asset/?id=182491277", weight = 10 } }, dance3 = { { id = "http://www.roblox.com/asset/?id=182436935", weight = 10 }, { id = "http://www.roblox.com/asset/?id=182491368", weight = 10 }, { id = "http://www.roblox.com/asset/?id=182491423", weight = 10 } }, laugh = { { id = "http://www.roblox.com/asset/?id=129423131", weight = 10 } }, cheer = { { id = "http://www.roblox.com/asset/?id=129423030", weight = 10 } }, } local dances = {"dance1", "dance2", "dance3"} -- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote local emoteNames = { wave = false, point = false, dance1 = true, dance2 = true, dance3 = true, laugh = false, cheer = false} function configureAnimationSet(name, fileList) if (animTable[name] ~= nil) then for _, connection in pairs(animTable[name].connections) do connection:disconnect() end end animTable[name] = {} animTable[name].count = 0 animTable[name].totalWeight = 0 animTable[name].connections = {} -- check for config values local config = script:FindFirstChild(name) if (config ~= nil) then -- print("Loading anims " .. name) table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end)) table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end)) local idx = 1 for _, childPart in pairs(config:GetChildren()) do if (childPart:IsA("Animation")) then table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end)) animTable[name][idx] = {} animTable[name][idx].anim = childPart local weightObject = childPart:FindFirstChild("Weight") if (weightObject == nil) then animTable[name][idx].weight = 1 else animTable[name][idx].weight = weightObject.Value end animTable[name].count = animTable[name].count + 1 animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight -- print(name .. " [" .. idx .. "] " .. animTable[name][idx].anim.AnimationId .. " (" .. animTable[name][idx].weight .. ")") idx = idx + 1 end end end -- fallback to defaults if (animTable[name].count <= 0) then for idx, anim in pairs(fileList) do animTable[name][idx] = {} animTable[name][idx].anim = Instance.new("Animation") animTable[name][idx].anim.Name = name animTable[name][idx].anim.AnimationId = anim.id animTable[name][idx].weight = anim.weight animTable[name].count = animTable[name].count + 1 animTable[name].totalWeight = animTable[name].totalWeight + anim.weight -- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")") end end end -- Setup animation objects function scriptChildModified(child) local fileList = animNames[child.Name] if (fileList ~= nil) then configureAnimationSet(child.Name, fileList) end end script.ChildAdded:connect(scriptChildModified) script.ChildRemoved:connect(scriptChildModified) for name, fileList in pairs(animNames) do configureAnimationSet(name, fileList) end -- ANIMATION -- declarations local toolAnim = "None" local toolAnimTime = 0 local jumpAnimTime = 0 local jumpAnimDuration = 0.3 local toolTransitionTime = 0.1 local fallTransitionTime = 0.3 local jumpMaxLimbVelocity = 0.75 -- functions function stopAllAnimations() local oldAnim = currentAnim -- return to idle if finishing an emote if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then oldAnim = "idle" end currentAnim = "" currentAnimInstance = nil if (currentAnimKeyframeHandler ~= nil) then currentAnimKeyframeHandler:disconnect() end if (currentAnimTrack ~= nil) then currentAnimTrack:Stop() currentAnimTrack:Destroy() currentAnimTrack = nil end return oldAnim end function setAnimationSpeed(speed) if speed ~= currentAnimSpeed then currentAnimSpeed = speed currentAnimTrack:AdjustSpeed(currentAnimSpeed) end end function keyFrameReachedFunc(frameName) if (frameName == "End") then local repeatAnim = currentAnim -- return to idle if finishing an emote if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then repeatAnim = "idle" end local animSpeed = currentAnimSpeed playAnimation(repeatAnim, 0.0, Humanoid) setAnimationSpeed(animSpeed) end end -- Preload animations function playAnimation(animName, transitionTime, humanoid) local roll = math.random(1, animTable[animName].totalWeight) local origRoll = roll local idx = 1 while (roll > animTable[animName][idx].weight) do roll = roll - animTable[animName][idx].weight idx = idx + 1 end -- print(animName .. " " .. idx .. " [" .. origRoll .. "]") local anim = animTable[animName][idx].anim -- switch animation if (anim ~= currentAnimInstance) then if (currentAnimTrack ~= nil) then currentAnimTrack:Stop(transitionTime) currentAnimTrack:Destroy() end currentAnimSpeed = 1.0 -- load it to the humanoid; get AnimationTrack currentAnimTrack = humanoid:LoadAnimation(anim) currentAnimTrack.Priority = Enum.AnimationPriority.Core -- play the animation currentAnimTrack:Play(transitionTime) currentAnim = animName currentAnimInstance = anim -- set up keyframe name triggers if (currentAnimKeyframeHandler ~= nil) then currentAnimKeyframeHandler:disconnect() end currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) end end ------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------- local toolAnimName = "" local toolAnimTrack = nil local toolAnimInstance = nil local currentToolAnimKeyframeHandler = nil function toolKeyFrameReachedFunc(frameName) if (frameName == "End") then -- print("Keyframe : ".. frameName) playToolAnimation(toolAnimName, 0.0, Humanoid) end end function playToolAnimation(animName, transitionTime, humanoid, priority) local roll = math.random(1, animTable[animName].totalWeight) local origRoll = roll local idx = 1 while (roll > animTable[animName][idx].weight) do roll = roll - animTable[animName][idx].weight idx = idx + 1 end -- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") local anim = animTable[animName][idx].anim if (toolAnimInstance ~= anim) then if (toolAnimTrack ~= nil) then toolAnimTrack:Stop() toolAnimTrack:Destroy() transitionTime = 0 end -- load it to the humanoid; get AnimationTrack toolAnimTrack = humanoid:LoadAnimation(anim) if priority then toolAnimTrack.Priority = priority end -- play the animation toolAnimTrack:Play(transitionTime) toolAnimName = animName toolAnimInstance = anim currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) end end function stopToolAnimations() local oldAnim = toolAnimName if (currentToolAnimKeyframeHandler ~= nil) then currentToolAnimKeyframeHandler:disconnect() end toolAnimName = "" toolAnimInstance = nil if (toolAnimTrack ~= nil) then toolAnimTrack:Stop() toolAnimTrack:Destroy() toolAnimTrack = nil end return oldAnim end ------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------- function onRunning(speed) if speed > 0.01 then playAnimation("walk", 0.1, Humanoid) if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then setAnimationSpeed(speed / 14.5) end pose = "Running" else if emoteNames[currentAnim] == nil then playAnimation("idle", 0.1, Humanoid) pose = "Standing" end end end function onDied() pose = "Dead" end function onJumping() playAnimation("jump", 0.1, Humanoid) jumpAnimTime = jumpAnimDuration pose = "Jumping" end function onClimbing(speed) playAnimation("climb", 0.1, Humanoid) setAnimationSpeed(speed / 12.0) pose = "Climbing" end function onGettingUp() pose = "GettingUp" end function onFreeFall() if (jumpAnimTime <= 0) then playAnimation("fall", fallTransitionTime, Humanoid) end pose = "FreeFall" end function onFallingDown() pose = "FallingDown" end function onSeated() pose = "Seated" end function onPlatformStanding() pose = "PlatformStanding" end function onSwimming(speed) if speed > 0 then pose = "Running" else pose = "Standing" end end function getTool() for _, kid in ipairs(Figure:GetChildren()) do if kid.className == "Tool" then return kid end end return nil end function getToolAnim(tool) for _, c in ipairs(tool:GetChildren()) do if c.Name == "toolanim" and c.className == "StringValue" then return c end end return nil end function animateTool() if (toolAnim == "None") then playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) return end if (toolAnim == "Slash") then playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) return end if (toolAnim == "Lunge") then playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) return end end function moveSit() RightShoulder.MaxVelocity = 0.15 LeftShoulder.MaxVelocity = 0.15 RightShoulder:SetDesiredAngle(3.14 /2) LeftShoulder:SetDesiredAngle(-3.14 /2) RightHip:SetDesiredAngle(3.14 /2) LeftHip:SetDesiredAngle(-3.14 /2) end local lastTick = 0 function move(time) local amplitude = 1 local frequency = 1 local deltaTime = time - lastTick lastTick = time local climbFudge = 0 local setAngles = false if (jumpAnimTime > 0) then jumpAnimTime = jumpAnimTime - deltaTime end if (pose == "FreeFall" and jumpAnimTime <= 0) then playAnimation("fall", fallTransitionTime, Humanoid) elseif (pose == "Seated") then playAnimation("sit", 0.5, Humanoid) return elseif (pose == "Running") then playAnimation("walk", 0.1, Humanoid) elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then -- print("Wha " .. pose) stopAllAnimations() amplitude = 0.1 frequency = 1 setAngles = true end if (setAngles) then local desiredAngle = amplitude * math.sin(time * frequency) RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) RightHip:SetDesiredAngle(-desiredAngle) LeftHip:SetDesiredAngle(-desiredAngle) end -- Tool Animation handling local tool = getTool() if tool and tool:FindFirstChild("Handle") then local animStringValueObject = getToolAnim(tool) if animStringValueObject then toolAnim = animStringValueObject.Value -- message recieved, delete StringValue animStringValueObject.Parent = nil toolAnimTime = time + .3 end if time > toolAnimTime then toolAnimTime = 0 toolAnim = "None" end animateTool() else stopToolAnimations() toolAnim = "None" toolAnimInstance = nil toolAnimTime = 0 end end local events = {} local eventHum = Humanoid local function onUnhook() for i = 1, #events do events[i]:Disconnect() end events = {} end local function onHook() onUnhook() pose = eventHum.Sit and "Seated" or "Standing" events = { eventHum.Died:connect(onDied), eventHum.Running:connect(onRunning), eventHum.Jumping:connect(onJumping), eventHum.Climbing:connect(onClimbing), eventHum.GettingUp:connect(onGettingUp), eventHum.FreeFalling:connect(onFreeFall), eventHum.FallingDown:connect(onFallingDown), eventHum.Seated:connect(onSeated), eventHum.PlatformStanding:connect(onPlatformStanding), eventHum.Swimming:connect(onSwimming) } end onHook() -- setup emote chat hook game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) local emote = "" if msg == "/e dance" then emote = dances[math.random(1, #dances)] elseif (string.sub(msg, 1, 3) == "/e ") then emote = string.sub(msg, 4) elseif (string.sub(msg, 1, 7) == "/emote ") then emote = string.sub(msg, 8) end if (pose == "Standing" and emoteNames[emote] ~= nil) then playAnimation(emote, 0.1, Humanoid) end end) -- main program -- initialize to idle playAnimation("idle", 0.1, Humanoid) pose = "Standing" spawn(function() while Figure.Parent ~= nil do local _, time = wait(0.1) move(time) end end) return { onRunning = onRunning, onDied = onDied, onJumping = onJumping, onClimbing = onClimbing, onGettingUp = onGettingUp, onFreeFall = onFreeFall, onFallingDown = onFallingDown, onSeated = onSeated, onPlatformStanding = onPlatformStanding, onHook = onHook, onUnhook = onUnhook } end return r6() end function _R15() local function r15() local Character = char local Humanoid = Character:WaitForChild("Humanoid") local pose = "Standing" local userNoUpdateOnLoopSuccess, userNoUpdateOnLoopValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoUpdateOnLoop") end) local userNoUpdateOnLoop = userNoUpdateOnLoopSuccess and userNoUpdateOnLoopValue local userAnimationSpeedDampeningSuccess, userAnimationSpeedDampeningValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserAnimationSpeedDampening") end) local userAnimationSpeedDampening = userAnimationSpeedDampeningSuccess and userAnimationSpeedDampeningValue local animateScriptEmoteHookFlagExists, animateScriptEmoteHookFlagEnabled = pcall(function() return UserSettings():IsUserFeatureEnabled("UserAnimateScriptEmoteHook") end) local FFlagAnimateScriptEmoteHook = animateScriptEmoteHookFlagExists and animateScriptEmoteHookFlagEnabled local AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent") local HumanoidHipHeight = 2 local EMOTE_TRANSITION_TIME = 0.1 local currentAnim = "" local currentAnimInstance = nil local currentAnimTrack = nil local currentAnimKeyframeHandler = nil local currentAnimSpeed = 1.0 local runAnimTrack = nil local runAnimKeyframeHandler = nil local animTable = {} local animNames = { idle = { { id = "http://www.roblox.com/asset/?id=507766666", weight = 1 }, { id = "http://www.roblox.com/asset/?id=507766951", weight = 1 }, { id = "http://www.roblox.com/asset/?id=507766388", weight = 9 } }, walk = { { id = "http://www.roblox.com/asset/?id=507777826", weight = 10 } }, run = { { id = "http://www.roblox.com/asset/?id=507767714", weight = 10 } }, swim = { { id = "http://www.roblox.com/asset/?id=507784897", weight = 10 } }, swimidle = { { id = "http://www.roblox.com/asset/?id=507785072", weight = 10 } }, jump = { { id = "http://www.roblox.com/asset/?id=507765000", weight = 10 } }, fall = { { id = "http://www.roblox.com/asset/?id=507767968", weight = 10 } }, climb = { { id = "http://www.roblox.com/asset/?id=507765644", weight = 10 } }, sit = { { id = "http://www.roblox.com/asset/?id=2506281703", weight = 10 } }, toolnone = { { id = "http://www.roblox.com/asset/?id=507768375", weight = 10 } }, toolslash = { { id = "http://www.roblox.com/asset/?id=522635514", weight = 10 } }, toollunge = { { id = "http://www.roblox.com/asset/?id=522638767", weight = 10 } }, wave = { { id = "http://www.roblox.com/asset/?id=507770239", weight = 10 } }, point = { { id = "http://www.roblox.com/asset/?id=507770453", weight = 10 } }, dance = { { id = "http://www.roblox.com/asset/?id=507771019", weight = 10 }, { id = "http://www.roblox.com/asset/?id=507771955", weight = 10 }, { id = "http://www.roblox.com/asset/?id=507772104", weight = 10 } }, dance2 = { { id = "http://www.roblox.com/asset/?id=507776043", weight = 10 }, { id = "http://www.roblox.com/asset/?id=507776720", weight = 10 }, { id = "http://www.roblox.com/asset/?id=507776879", weight = 10 } }, dance3 = { { id = "http://www.roblox.com/asset/?id=507777268", weight = 10 }, { id = "http://www.roblox.com/asset/?id=507777451", weight = 10 }, { id = "http://www.roblox.com/asset/?id=507777623", weight = 10 } }, laugh = { { id = "http://www.roblox.com/asset/?id=507770818", weight = 10 } }, cheer = { { id = "http://www.roblox.com/asset/?id=507770677", weight = 10 } }, } -- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote local emoteNames = { wave = false, point = false, dance = true, dance2 = true, dance3 = true, laugh = false, cheer = false} local PreloadAnimsUserFlag = false local PreloadedAnims = {} local successPreloadAnim, msgPreloadAnim = pcall(function() PreloadAnimsUserFlag = UserSettings():IsUserFeatureEnabled("UserPreloadAnimations") end) if not successPreloadAnim then PreloadAnimsUserFlag = false end math.randomseed(tick()) function findExistingAnimationInSet(set, anim) if set == nil or anim == nil then return 0 end for idx = 1, set.count, 1 do if set[idx].anim.AnimationId == anim.AnimationId then return idx end end return 0 end function configureAnimationSet(name, fileList) if (animTable[name] ~= nil) then for _, connection in pairs(animTable[name].connections) do connection:disconnect() end end animTable[name] = {} animTable[name].count = 0 animTable[name].totalWeight = 0 animTable[name].connections = {} local allowCustomAnimations = true local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end) if not success then allowCustomAnimations = true end -- check for config values local config = script:FindFirstChild(name) if (allowCustomAnimations and config ~= nil) then table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end)) table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end)) local idx = 0 for _, childPart in pairs(config:GetChildren()) do if (childPart:IsA("Animation")) then local newWeight = 1 local weightObject = childPart:FindFirstChild("Weight") if (weightObject ~= nil) then newWeight = weightObject.Value end animTable[name].count = animTable[name].count + 1 idx = animTable[name].count animTable[name][idx] = {} animTable[name][idx].anim = childPart animTable[name][idx].weight = newWeight animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end)) table.insert(animTable[name].connections, childPart.ChildAdded:connect(function(property) configureAnimationSet(name, fileList) end)) table.insert(animTable[name].connections, childPart.ChildRemoved:connect(function(property) configureAnimationSet(name, fileList) end)) end end end -- fallback to defaults if (animTable[name].count <= 0) then for idx, anim in pairs(fileList) do animTable[name][idx] = {} animTable[name][idx].anim = Instance.new("Animation") animTable[name][idx].anim.Name = name animTable[name][idx].anim.AnimationId = anim.id animTable[name][idx].weight = anim.weight animTable[name].count = animTable[name].count + 1 animTable[name].totalWeight = animTable[name].totalWeight + anim.weight end end -- preload anims if PreloadAnimsUserFlag then for i, animType in pairs(animTable) do for idx = 1, animType.count, 1 do if PreloadedAnims[animType[idx].anim.AnimationId] == nil then Humanoid:LoadAnimation(animType[idx].anim) PreloadedAnims[animType[idx].anim.AnimationId] = true end end end end end ------------------------------------------------------------------------------------------------------------ function configureAnimationSetOld(name, fileList) if (animTable[name] ~= nil) then for _, connection in pairs(animTable[name].connections) do connection:disconnect() end end animTable[name] = {} animTable[name].count = 0 animTable[name].totalWeight = 0 animTable[name].connections = {} local allowCustomAnimations = true local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end) if not success then allowCustomAnimations = true end -- check for config values local config = script:FindFirstChild(name) if (allowCustomAnimations and config ~= nil) then table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end)) table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end)) local idx = 1 for _, childPart in pairs(config:GetChildren()) do if (childPart:IsA("Animation")) then table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end)) animTable[name][idx] = {} animTable[name][idx].anim = childPart local weightObject = childPart:FindFirstChild("Weight") if (weightObject == nil) then animTable[name][idx].weight = 1 else animTable[name][idx].weight = weightObject.Value end animTable[name].count = animTable[name].count + 1 animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight idx = idx + 1 end end end -- fallback to defaults if (animTable[name].count <= 0) then for idx, anim in pairs(fileList) do animTable[name][idx] = {} animTable[name][idx].anim = Instance.new("Animation") animTable[name][idx].anim.Name = name animTable[name][idx].anim.AnimationId = anim.id animTable[name][idx].weight = anim.weight animTable[name].count = animTable[name].count + 1 animTable[name].totalWeight = animTable[name].totalWeight + anim.weight -- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")") end end -- preload anims if PreloadAnimsUserFlag then for i, animType in pairs(animTable) do for idx = 1, animType.count, 1 do Humanoid:LoadAnimation(animType[idx].anim) end end end end -- Setup animation objects function scriptChildModified(child) local fileList = animNames[child.Name] if (fileList ~= nil) then configureAnimationSet(child.Name, fileList) end end script.ChildAdded:connect(scriptChildModified) script.ChildRemoved:connect(scriptChildModified) for name, fileList in pairs(animNames) do configureAnimationSet(name, fileList) end -- ANIMATION -- declarations local toolAnim = "None" local toolAnimTime = 0 local jumpAnimTime = 0 local jumpAnimDuration = 0.31 local toolTransitionTime = 0.1 local fallTransitionTime = 0.2 local currentlyPlayingEmote = false -- functions function stopAllAnimations() local oldAnim = currentAnim -- return to idle if finishing an emote if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then oldAnim = "idle" end if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then oldAnim = "idle" currentlyPlayingEmote = false end currentAnim = "" currentAnimInstance = nil if (currentAnimKeyframeHandler ~= nil) then currentAnimKeyframeHandler:disconnect() end if (currentAnimTrack ~= nil) then currentAnimTrack:Stop() currentAnimTrack:Destroy() currentAnimTrack = nil end -- clean up walk if there is one if (runAnimKeyframeHandler ~= nil) then runAnimKeyframeHandler:disconnect() end if (runAnimTrack ~= nil) then runAnimTrack:Stop() runAnimTrack:Destroy() runAnimTrack = nil end return oldAnim end function getHeightScale() if Humanoid then if not Humanoid.AutomaticScalingEnabled then return 1 end local scale = Humanoid.HipHeight / HumanoidHipHeight if userAnimationSpeedDampening then if AnimationSpeedDampeningObject == nil then AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent") end if AnimationSpeedDampeningObject ~= nil then scale = 1 + (Humanoid.HipHeight - HumanoidHipHeight) * AnimationSpeedDampeningObject.Value / HumanoidHipHeight end end return scale end return 1 end local smallButNotZero = 0.0001 function setRunSpeed(speed) local speedScaled = speed * 1.25 local heightScale = getHeightScale() local runSpeed = speedScaled / heightScale if runSpeed ~= currentAnimSpeed then if runSpeed < 0.33 then currentAnimTrack:AdjustWeight(1.0) runAnimTrack:AdjustWeight(smallButNotZero) elseif runSpeed < 0.66 then local weight = ((runSpeed - 0.33) / 0.33) currentAnimTrack:AdjustWeight(1.0 - weight + smallButNotZero) runAnimTrack:AdjustWeight(weight + smallButNotZero) else currentAnimTrack:AdjustWeight(smallButNotZero) runAnimTrack:AdjustWeight(1.0) end currentAnimSpeed = runSpeed runAnimTrack:AdjustSpeed(runSpeed) currentAnimTrack:AdjustSpeed(runSpeed) end end function setAnimationSpeed(speed) if currentAnim == "walk" then setRunSpeed(speed) else if speed ~= currentAnimSpeed then currentAnimSpeed = speed currentAnimTrack:AdjustSpeed(currentAnimSpeed) end end end function keyFrameReachedFunc(frameName) if (frameName == "End") then if currentAnim == "walk" then if userNoUpdateOnLoop == true then if runAnimTrack.Looped ~= true then runAnimTrack.TimePosition = 0.0 end if currentAnimTrack.Looped ~= true then currentAnimTrack.TimePosition = 0.0 end else runAnimTrack.TimePosition = 0.0 currentAnimTrack.TimePosition = 0.0 end else local repeatAnim = currentAnim -- return to idle if finishing an emote if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then repeatAnim = "idle" end if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then if currentAnimTrack.Looped then -- Allow the emote to loop return end repeatAnim = "idle" currentlyPlayingEmote = false end local animSpeed = currentAnimSpeed playAnimation(repeatAnim, 0.15, Humanoid) setAnimationSpeed(animSpeed) end end end function rollAnimation(animName) local roll = math.random(1, animTable[animName].totalWeight) local origRoll = roll local idx = 1 while (roll > animTable[animName][idx].weight) do roll = roll - animTable[animName][idx].weight idx = idx + 1 end return idx end local function switchToAnim(anim, animName, transitionTime, humanoid) -- switch animation if (anim ~= currentAnimInstance) then if (currentAnimTrack ~= nil) then currentAnimTrack:Stop(transitionTime) currentAnimTrack:Destroy() end if (runAnimTrack ~= nil) then runAnimTrack:Stop(transitionTime) runAnimTrack:Destroy() if userNoUpdateOnLoop == true then runAnimTrack = nil end end currentAnimSpeed = 1.0 -- load it to the humanoid; get AnimationTrack currentAnimTrack = humanoid:LoadAnimation(anim) currentAnimTrack.Priority = Enum.AnimationPriority.Core -- play the animation currentAnimTrack:Play(transitionTime) currentAnim = animName currentAnimInstance = anim -- set up keyframe name triggers if (currentAnimKeyframeHandler ~= nil) then currentAnimKeyframeHandler:disconnect() end currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) -- check to see if we need to blend a walk/run animation if animName == "walk" then local runAnimName = "run" local runIdx = rollAnimation(runAnimName) runAnimTrack = humanoid:LoadAnimation(animTable[runAnimName][runIdx].anim) runAnimTrack.Priority = Enum.AnimationPriority.Core runAnimTrack:Play(transitionTime) if (runAnimKeyframeHandler ~= nil) then runAnimKeyframeHandler:disconnect() end runAnimKeyframeHandler = runAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) end end end function playAnimation(animName, transitionTime, humanoid) local idx = rollAnimation(animName) local anim = animTable[animName][idx].anim switchToAnim(anim, animName, transitionTime, humanoid) currentlyPlayingEmote = false end function playEmote(emoteAnim, transitionTime, humanoid) switchToAnim(emoteAnim, emoteAnim.Name, transitionTime, humanoid) currentlyPlayingEmote = true end ------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------- local toolAnimName = "" local toolAnimTrack = nil local toolAnimInstance = nil local currentToolAnimKeyframeHandler = nil function toolKeyFrameReachedFunc(frameName) if (frameName == "End") then playToolAnimation(toolAnimName, 0.0, Humanoid) end end function playToolAnimation(animName, transitionTime, humanoid, priority) local idx = rollAnimation(animName) local anim = animTable[animName][idx].anim if (toolAnimInstance ~= anim) then if (toolAnimTrack ~= nil) then toolAnimTrack:Stop() toolAnimTrack:Destroy() transitionTime = 0 end -- load it to the humanoid; get AnimationTrack toolAnimTrack = humanoid:LoadAnimation(anim) if priority then toolAnimTrack.Priority = priority end -- play the animation toolAnimTrack:Play(transitionTime) toolAnimName = animName toolAnimInstance = anim currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) end end function stopToolAnimations() local oldAnim = toolAnimName if (currentToolAnimKeyframeHandler ~= nil) then currentToolAnimKeyframeHandler:disconnect() end toolAnimName = "" toolAnimInstance = nil if (toolAnimTrack ~= nil) then toolAnimTrack:Stop() toolAnimTrack:Destroy() toolAnimTrack = nil end return oldAnim end ------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------- -- STATE CHANGE HANDLERS function onRunning(speed) if speed > 0.75 then local scale = 16.0 playAnimation("walk", 0.2, Humanoid) setAnimationSpeed(speed / scale) pose = "Running" else if emoteNames[currentAnim] == nil and not currentlyPlayingEmote then playAnimation("idle", 0.2, Humanoid) pose = "Standing" end end end function onDied() pose = "Dead" end function onJumping() playAnimation("jump", 0.1, Humanoid) jumpAnimTime = jumpAnimDuration pose = "Jumping" end function onClimbing(speed) local scale = 5.0 playAnimation("climb", 0.1, Humanoid) setAnimationSpeed(speed / scale) pose = "Climbing" end function onGettingUp() pose = "GettingUp" end function onFreeFall() if (jumpAnimTime <= 0) then playAnimation("fall", fallTransitionTime, Humanoid) end pose = "FreeFall" end function onFallingDown() pose = "FallingDown" end function onSeated() pose = "Seated" end function onPlatformStanding() pose = "PlatformStanding" end ------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------- function onSwimming(speed) if speed > 1.00 then local scale = 10.0 playAnimation("swim", 0.4, Humanoid) setAnimationSpeed(speed / scale) pose = "Swimming" else playAnimation("swimidle", 0.4, Humanoid) pose = "Standing" end end function animateTool() if (toolAnim == "None") then playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) return end if (toolAnim == "Slash") then playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) return end if (toolAnim == "Lunge") then playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) return end end function getToolAnim(tool) for _, c in ipairs(tool:GetChildren()) do if c.Name == "toolanim" and c.className == "StringValue" then return c end end return nil end local lastTick = 0 function stepAnimate(currentTime) local amplitude = 1 local frequency = 1 local deltaTime = currentTime - lastTick lastTick = currentTime local climbFudge = 0 local setAngles = false if (jumpAnimTime > 0) then jumpAnimTime = jumpAnimTime - deltaTime end if (pose == "FreeFall" and jumpAnimTime <= 0) then playAnimation("fall", fallTransitionTime, Humanoid) elseif (pose == "Seated") then playAnimation("sit", 0.5, Humanoid) return elseif (pose == "Running") then playAnimation("walk", 0.2, Humanoid) elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then stopAllAnimations() amplitude = 0.1 frequency = 1 setAngles = true end -- Tool Animation handling local tool = Character:FindFirstChildOfClass("Tool") if tool and tool:FindFirstChild("Handle") then local animStringValueObject = getToolAnim(tool) if animStringValueObject then toolAnim = animStringValueObject.Value -- message recieved, delete StringValue animStringValueObject.Parent = nil toolAnimTime = currentTime + .3 end if currentTime > toolAnimTime then toolAnimTime = 0 toolAnim = "None" end animateTool() else stopToolAnimations() toolAnim = "None" toolAnimInstance = nil toolAnimTime = 0 end end -- connect events local events = {} local eventHum = Humanoid local function onUnhook() for i = 1, #events do events[i]:Disconnect() end events = {} end local function onHook() onUnhook() pose = eventHum.Sit and "Seated" or "Standing" events = { eventHum.Died:connect(onDied), eventHum.Running:connect(onRunning), eventHum.Jumping:connect(onJumping), eventHum.Climbing:connect(onClimbing), eventHum.GettingUp:connect(onGettingUp), eventHum.FreeFalling:connect(onFreeFall), eventHum.FallingDown:connect(onFallingDown), eventHum.Seated:connect(onSeated), eventHum.PlatformStanding:connect(onPlatformStanding), eventHum.Swimming:connect(onSwimming) } end onHook() -- setup emote chat hook game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) local emote = "" if (string.sub(msg, 1, 3) == "/e ") then emote = string.sub(msg, 4) elseif (string.sub(msg, 1, 7) == "/emote ") then emote = string.sub(msg, 8) end if (pose == "Standing" and emoteNames[emote] ~= nil) then playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid) end end) --[[ emote bindable hook if FFlagAnimateScriptEmoteHook then script:WaitForChild("PlayEmote").OnInvoke = function(emote) -- Only play emotes when idling if pose ~= "Standing" then return end if emoteNames[emote] ~= nil then -- Default emotes playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid) return true elseif typeof(emote) == "Instance" and emote:IsA("Animation") then -- Non-default emotes playEmote(emote, EMOTE_TRANSITION_TIME, Humanoid) return true end -- Return false to indicate that the emote could not be played return false end end ]] -- initialize to idle playAnimation("idle", 0.1, Humanoid) pose = "Standing" -- loop to handle timed state transitions and tool animations spawn(function() while Character.Parent ~= nil do local _, currentGameTime = wait(0.1) stepAnimate(currentGameTime) end end) return { onRunning = onRunning, onDied = onDied, onJumping = onJumping, onClimbing = onClimbing, onGettingUp = onGettingUp, onFreeFall = onFreeFall, onFallingDown = onFallingDown, onSeated = onSeated, onPlatformStanding = onPlatformStanding, onHook = onHook, onUnhook = onUnhook } end return r15() end while true do wait(.1) if plr.Character ~= nil then char = plr.Character break end end function _Controller() local humanoid = char:WaitForChild("Humanoid") local animFuncs = {} if (humanoid.RigType == Enum.HumanoidRigType.R6) then animFuncs = _R6() else animFuncs = _R15() end print("Animation succes") return animFuncs end function _AnimationHandler() local AnimationHandler = {} AnimationHandler.__index = AnimationHandler function AnimationHandler.new(humanoid, animate) local self = setmetatable({}, AnimationHandler) self._AnimFuncs = _Controller() self.Humanoid = humanoid return self end function AnimationHandler:EnableDefault(bool) if (bool) then self._AnimFuncs.onHook() else self._AnimFuncs.onUnhook() end end function AnimationHandler:Run(name, ...) self._AnimFuncs[name](...) end return AnimationHandler end function _GravityController() local ZERO = Vector3.new(0, 0, 0) local UNIT_X = Vector3.new(1, 0, 0) local UNIT_Y = Vector3.new(0, 1, 0) local UNIT_Z = Vector3.new(0, 0, 1) local VEC_XY = Vector3.new(1, 0, 1) local IDENTITYCF = CFrame.new() local JUMPMODIFIER = 1.2 local TRANSITION = 0.15 local WALKF = 200 / 3 local UIS = game:GetService("UserInputService") local RUNSERVICE = game:GetService("RunService") local InitObjects = _InitObjects() local AnimationHandler = _AnimationHandler() local StateTracker = _StateTracker() -- Class local GravityController = {} GravityController.__index = GravityController -- Private Functions local function getRotationBetween(u, v, axis) local dot, uxv = u:Dot(v), u:Cross(v) if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot) end local function lookAt(pos, forward, up) local r = forward:Cross(up) local u = r:Cross(forward) return CFrame.fromMatrix(pos, r.Unit, u.Unit) end local function getMass(array) local mass = 0 for _, part in next, array do if (part:IsA("BasePart")) then mass = mass + part:GetMass() end end return mass end -- Public Constructor local ExecutedPlayerModule = _PlayerModule() local ExecutedSounds = _sounds() function GravityController.new(player) local self = setmetatable({}, GravityController) --[[ Camera local loaded = player.PlayerScripts:WaitForChild("PlayerScriptsLoader"):WaitForChild("Loaded") if (not loaded.Value) then --loaded.Changed:Wait() end ]] local playerModule = ExecutedPlayerModule self.Controls = playerModule:GetControls() self.Camera = playerModule:GetCameras() -- Player and character self.Player = player self.Character = player.Character self.Humanoid = player.Character:WaitForChild("Humanoid") self.HRP = player.Character:WaitForChild("HumanoidRootPart") -- Animation self.AnimationHandler = AnimationHandler.new(self.Humanoid, self.Character:WaitForChild("Animate")) self.AnimationHandler:EnableDefault(false) local ssss = game:GetService("Players").LocalPlayer.PlayerScripts:FindFirstChild("SetState") or Instance.new("BindableEvent",game:GetService("Players").LocalPlayer.PlayerScripts) local soundState = ExecutedSounds ssss.Name = "SetState" self.StateTracker = StateTracker.new(self.Humanoid, soundState) self.StateTracker.Changed:Connect(function(name, speed) self.AnimationHandler:Run(name, speed) end) -- Collider and forces local collider, gyro, vForce, floor = InitObjects(self) floor.Touched:Connect(function() end) collider.Touched:Connect(function() end) self.Collider = collider self.VForce = vForce self.Gyro = gyro self.Floor = floor -- Attachment to parts self.LastPart = workspace.Terrain self.LastPartCFrame = IDENTITYCF -- Gravity properties self.GravityUp = UNIT_Y self.Ignores = {self.Character} function self.Camera.GetUpVector(this, oldUpVector) return self.GravityUp end -- Events etc self.Humanoid.PlatformStand = true self.CharacterMass = getMass(self.Character:GetDescendants()) self.Character.AncestryChanged:Connect(function() self.CharacterMass = getMass(self.Character:GetDescendants()) end) self.JumpCon = RUNSERVICE.RenderStepped:Connect(function(dt) if (self.Controls:IsJumping()) then self:OnJumpRequest() end end) self.DeathCon = self.Humanoid.Died:Connect(function() self:Destroy() end) self.SeatCon = self.Humanoid.Seated:Connect(function(active) if (active) then self:Destroy() end end) self.HeartCon = RUNSERVICE.Heartbeat:Connect(function(dt) self:OnHeartbeatStep(dt) end) RUNSERVICE:BindToRenderStep("GravityStep", Enum.RenderPriority.Input.Value + 1, function(dt) self:OnGravityStep(dt) end) return self end -- Public Methods function GravityController:Destroy() self.JumpCon:Disconnect() self.DeathCon:Disconnect() self.SeatCon:Disconnect() self.HeartCon:Disconnect() RUNSERVICE:UnbindFromRenderStep("GravityStep") self.Collider:Destroy() self.VForce:Destroy() self.Gyro:Destroy() self.StateTracker:Destroy() self.Humanoid.PlatformStand = false self.AnimationHandler:EnableDefault(true) self.GravityUp = UNIT_Y end function GravityController:GetGravityUp(oldGravity) return oldGravity end function GravityController:IsGrounded(isJumpCheck) if (not isJumpCheck) then local parts = self.Floor:GetTouchingParts() for _, part in next, parts do if (not part:IsDescendantOf(self.Character)) then return true end end else if (self.StateTracker.Jumped) then return false end -- 1. check we are touching something with the collider local valid = {} local parts = self.Collider:GetTouchingParts() for _, part in next, parts do if (not part:IsDescendantOf(self.Character)) then table.insert(valid, part) end end if (#valid > 0) then -- 2. do a decently long downwards raycast local max = math.cos(self.Humanoid.MaxSlopeAngle) local ray = Ray.new(self.Collider.Position, -10 * self.GravityUp) local hit, pos, normal = workspace:FindPartOnRayWithWhitelist(ray, valid, true) -- 3. use slope to decide on jump if (hit and max <= self.GravityUp:Dot(normal)) then return true end end end return false end function GravityController:OnJumpRequest() if (not self.StateTracker.Jumped and self:IsGrounded(true)) then local hrpVel = self.HRP.Velocity self.HRP.Velocity = hrpVel + self.GravityUp*self.Humanoid.JumpPower*JUMPMODIFIER self.StateTracker:RequestedJump() end end function GravityController:GetMoveVector() return self.Controls:GetMoveVector() end function GravityController:OnHeartbeatStep(dt) local ray = Ray.new(self.Collider.Position, -1.1*self.GravityUp) local hit, pos, normal = workspace:FindPartOnRayWithIgnoreList(ray, self.Ignores) local lastPart = self.LastPart if (hit and lastPart and lastPart == hit) then local offset = self.LastPartCFrame:ToObjectSpace(self.HRP.CFrame) self.HRP.CFrame = hit.CFrame:ToWorldSpace(offset) end self.LastPart = hit self.LastPartCFrame = hit and hit.CFrame end function GravityController:OnGravityStep(dt) -- update gravity up vector local oldGravity = self.GravityUp local newGravity = self:GetGravityUp(oldGravity) local rotation = getRotationBetween(oldGravity, newGravity, workspace.CurrentCamera.CFrame.RightVector) rotation = IDENTITYCF:Lerp(rotation, TRANSITION) self.GravityUp = rotation * oldGravity -- get world move vector local camCF = workspace.CurrentCamera.CFrame local fDot = camCF.LookVector:Dot(newGravity) local cForward = math.abs(fDot) > 0.5 and -math.sign(fDot)*camCF.UpVector or camCF.LookVector local left = cForward:Cross(-newGravity).Unit local forward = -left:Cross(newGravity).Unit local move = self:GetMoveVector() local worldMove = forward*move.z - left*move.x worldMove = worldMove:Dot(worldMove) > 1 and worldMove.Unit or worldMove local isInputMoving = worldMove:Dot(worldMove) > 0 -- get the desired character cframe local hrpCFLook = self.HRP.CFrame.LookVector local charF = hrpCFLook:Dot(forward)*forward + hrpCFLook:Dot(left)*left local charR = charF:Cross(newGravity).Unit local newCharCF = CFrame.fromMatrix(ZERO, charR, newGravity, -charF) local newCharRotation = IDENTITYCF if (isInputMoving) then newCharRotation = IDENTITYCF:Lerp(getRotationBetween(charF, worldMove, newGravity), 0.7) end -- calculate forces local g = workspace.Gravity local gForce = g * self.CharacterMass * (UNIT_Y - newGravity) local cVelocity = self.HRP.Velocity local tVelocity = self.Humanoid.WalkSpeed * worldMove local gVelocity = cVelocity:Dot(newGravity)*newGravity local hVelocity = cVelocity - gVelocity if (hVelocity:Dot(hVelocity) < 1) then hVelocity = ZERO end local dVelocity = tVelocity - hVelocity local walkForceM = math.min(10000, WALKF * self.CharacterMass * dVelocity.Magnitude / (dt*60)) local walkForce = walkForceM > 0 and dVelocity.Unit*walkForceM or ZERO -- mouse lock local charRotation = newCharRotation * newCharCF if (self.Camera:IsCamRelative()) then local lv = workspace.CurrentCamera.CFrame.LookVector local hlv = lv - charRotation.UpVector:Dot(lv)*charRotation.UpVector charRotation = lookAt(ZERO, hlv, charRotation.UpVector) end -- get state self.StateTracker:OnStep(self.GravityUp, self:IsGrounded(), isInputMoving) -- update values self.VForce.Force = walkForce + gForce self.Gyro.CFrame = charRotation end return GravityController end function _Draw3D() local module = {} -- Style Guide module.StyleGuide = { Point = { Thickness = 0.5; Color = Color3.new(0, 1, 0); }, Line = { Thickness = 0.1; Color = Color3.new(1, 1, 0); }, Ray = { Thickness = 0.1; Color = Color3.new(1, 0, 1); }, Triangle = { Thickness = 0.05; }; CFrame = { Thickness = 0.1; RightColor3 = Color3.new(1, 0, 0); UpColor3 = Color3.new(0, 1, 0); BackColor3 = Color3.new(0, 0, 1); PartProperties = { Material = Enum.Material.SmoothPlastic; }; } } -- CONSTANTS local WEDGE = Instance.new("WedgePart") WEDGE.Material = Enum.Material.SmoothPlastic WEDGE.Anchored = true WEDGE.CanCollide = false local PART = Instance.new("Part") PART.Size = Vector3.new(0.1, 0.1, 0.1) PART.Anchored = true PART.CanCollide = false PART.TopSurface = Enum.SurfaceType.Smooth PART.BottomSurface = Enum.SurfaceType.Smooth PART.Material = Enum.Material.SmoothPlastic -- Functions local function draw(properties, style) local part = PART:Clone() for k, v in next, properties do part[k] = v end if (style) then for k, v in next, style do if (k ~= "Thickness") then part[k] = v end end end return part end function module.Draw(parent, properties) properties.Parent = parent return draw(properties, nil) end function module.Point(parent, cf_v3) local thickness = module.StyleGuide.Point.Thickness return draw({ Size = Vector3.new(thickness, thickness, thickness); CFrame = (typeof(cf_v3) == "CFrame" and cf_v3 or CFrame.new(cf_v3)); Parent = parent; }, module.StyleGuide.Point) end function module.Line(parent, a, b) local thickness = module.StyleGuide.Line.Thickness return draw({ CFrame = CFrame.new((a + b)/2, b); Size = Vector3.new(thickness, thickness, (b - a).Magnitude); Parent = parent; }, module.StyleGuide.Line) end function module.Ray(parent, origin, direction) local thickness = module.StyleGuide.Ray.Thickness return draw({ CFrame = CFrame.new(origin + direction/2, origin + direction); Size = Vector3.new(thickness, thickness, direction.Magnitude); Parent = parent; }, module.StyleGuide.Ray) end function module.Triangle(parent, a, b, c) local ab, ac, bc = b - a, c - a, c - b local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc) if (abd > acd and abd > bcd) then c, a = a, c elseif (acd > bcd and acd > abd) then a, b = b, a end ab, ac, bc = b - a, c - a, c - b local right = ac:Cross(ab).Unit local up = bc:Cross(right).Unit local back = bc.Unit local height = math.abs(ab:Dot(up)) local width1 = math.abs(ab:Dot(back)) local width2 = math.abs(ac:Dot(back)) local thickness = module.StyleGuide.Triangle.Thickness local w1 = WEDGE:Clone() w1.Size = Vector3.new(thickness, height, width1) w1.CFrame = CFrame.fromMatrix((a + b)/2, right, up, back) w1.Parent = parent local w2 = WEDGE:Clone() w2.Size = Vector3.new(thickness, height, width2) w2.CFrame = CFrame.fromMatrix((a + c)/2, -right, up, -back) w2.Parent = parent for k, v in next, module.StyleGuide.Triangle do if (k ~= "Thickness") then w1[k] = v w2[k] = v end end return w1, w2 end function module.CFrame(parent, cf) local origin = cf.Position local r = cf.RightVector local u = cf.UpVector local b = -cf.LookVector local thickness = module.StyleGuide.CFrame.Thickness local right = draw({ CFrame = CFrame.new(origin + r/2, origin + r); Size = Vector3.new(thickness, thickness, r.Magnitude); Color = module.StyleGuide.CFrame.RightColor3; Parent = parent; }, module.StyleGuide.CFrame.PartProperties) local up = draw({ CFrame = CFrame.new(origin + u/2, origin + u); Size = Vector3.new(thickness, thickness, r.Magnitude); Color = module.StyleGuide.CFrame.UpColor3; Parent = parent; }, module.StyleGuide.CFrame.PartProperties) local back = draw({ CFrame = CFrame.new(origin + b/2, origin + b); Size = Vector3.new(thickness, thickness, u.Magnitude); Color = module.StyleGuide.CFrame.BackColor3; Parent = parent; }, module.StyleGuide.CFrame.PartProperties) return right, up, back end -- Return return module end function _Draw2D() local module = {} -- Style Guide module.StyleGuide = { Point = { BorderSizePixel = 0; Size = UDim2.new(0, 4, 0, 4); BorderColor3 = Color3.new(0, 0, 0); BackgroundColor3 = Color3.new(0, 1, 0); }, Line = { Thickness = 1; BorderSizePixel = 0; BorderColor3 = Color3.new(0, 0, 0); BackgroundColor3 = Color3.new(0, 1, 0); }, Ray = { Thickness = 1; BorderSizePixel = 0; BorderColor3 = Color3.new(0, 0, 0); BackgroundColor3 = Color3.new(0, 1, 0); }, Triangle = { ImageTransparency = 0; ImageColor3 = Color3.new(0, 1, 0); } } -- CONSTANTS local HALF = Vector2.new(0.5, 0.5) local RIGHT = "rbxassetid://2798177521" local LEFT = "rbxassetid://2798177955" local IMG = Instance.new("ImageLabel") IMG.BackgroundTransparency = 1 IMG.AnchorPoint = HALF IMG.BorderSizePixel = 0 local FRAME = Instance.new("Frame") FRAME.BorderSizePixel = 0 FRAME.Size = UDim2.new(0, 0, 0, 0) FRAME.BackgroundColor3 = Color3.new(1, 1, 1) -- Functions function draw(properties, style) local frame = FRAME:Clone() for k, v in next, properties do frame[k] = v end if (style) then for k, v in next, style do if (k ~= "Thickness") then frame[k] = v end end end return frame end function module.Draw(parent, properties) properties.Parent = parent return draw(properties, nil) end function module.Point(parent, v2) return draw({ AnchorPoint = HALF; Position = UDim2.new(0, v2.x, 0, v2.y); Parent = parent; }, module.StyleGuide.Point) end function module.Line(parent, a, b) local v = (b - a) local m = (a + b)/2 return draw({ AnchorPoint = HALF; Position = UDim2.new(0, m.x, 0, m.y); Size = UDim2.new(0, module.StyleGuide.Line.Thickness, 0, v.magnitude); Rotation = math.deg(math.atan2(v.y, v.x)) - 90; BackgroundColor3 = Color3.new(1, 1, 0); Parent = parent; }, module.StyleGuide.Line) end function module.Ray(parent, origin, direction) local a, b = origin, origin + direction local v = (b - a) local m = (a + b)/2 return draw({ AnchorPoint = HALF; Position = UDim2.new(0, m.x, 0, m.y); Size = UDim2.new(0, module.StyleGuide.Ray.Thickness, 0, v.magnitude); Rotation = math.deg(math.atan2(v.y, v.x)) - 90; Parent = parent; }, module.StyleGuide.Ray) end function module.Triangle(parent, a, b, c) local ab, ac, bc = b - a, c - a, c - b local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc) if (abd > acd and abd > bcd) then c, a = a, c elseif (acd > bcd and acd > abd) then a, b = b, a end ab, ac, bc = b - a, c - a, c - b local unit = bc.unit local height = unit:Cross(ab) local flip = (height >= 0) local theta = math.deg(math.atan2(unit.y, unit.x)) + (flip and 0 or 180) local m1 = (a + b)/2 local m2 = (a + c)/2 local w1 = IMG:Clone() w1.Image = flip and RIGHT or LEFT w1.AnchorPoint = HALF w1.Size = UDim2.new(0, math.abs(unit:Dot(ab)), 0, height) w1.Position = UDim2.new(0, m1.x, 0, m1.y) w1.Rotation = theta w1.Parent = parent local w2 = IMG:Clone() w2.Image = flip and LEFT or RIGHT w2.AnchorPoint = HALF w2.Size = UDim2.new(0, math.abs(unit:Dot(ac)), 0, height) w2.Position = UDim2.new(0, m2.x, 0, m2.y) w2.Rotation = theta w2.Parent = parent for k, v in next, module.StyleGuide.Triangle do w1[k] = v w2[k] = v end return w1, w2 end -- Return return module end function _DrawClass() local Draw2DModule = _Draw2D() local Draw3DModule = _Draw3D() -- local DrawClass = {} local DrawClassStorage = setmetatable({}, {__mode = "k"}) DrawClass.__index = DrawClass function DrawClass.new(parent) local self = setmetatable({}, DrawClass) self.Parent = parent DrawClassStorage[self] = {} self.Draw3D = {} for key, func in next, Draw3DModule do self.Draw3D[key] = function(...) local returns = {func(self.Parent, ...)} for i = 1, #returns do table.insert(DrawClassStorage[self], returns[i]) end return unpack(returns) end end self.Draw2D = {} for key, func in next, Draw2DModule do self.Draw2D[key] = function(...) local returns = {func(self.Parent, ...)} for i = 1, #returns do table.insert(DrawClassStorage[self], returns[i]) end return unpack(returns) end end return self end -- function DrawClass:Clear() local t = DrawClassStorage[self] while (#t > 0) do local part = table.remove(t) if (part) then part:Destroy() end end DrawClassStorage[self] = {} end -- return DrawClass end --END TEST local PLAYERS = game:GetService("Players") local GravityController = _GravityController() local Controller = GravityController.new(PLAYERS.LocalPlayer) local DrawClass = _DrawClass() local PI2 = math.pi*2 local ZERO = Vector3.new(0, 0, 0) local LOWER_RADIUS_OFFSET = 3 local NUM_DOWN_RAYS = 24 local ODD_DOWN_RAY_START_RADIUS = 3 local EVEN_DOWN_RAY_START_RADIUS = 2 local ODD_DOWN_RAY_END_RADIUS = 1.66666 local EVEN_DOWN_RAY_END_RADIUS = 1 local NUM_FEELER_RAYS = 9 local FEELER_LENGTH = 2 local FEELER_START_OFFSET = 2 local FEELER_RADIUS = 3.5 local FEELER_APEX_OFFSET = 1 local FEELER_WEIGHTING = 8 function GetGravityUp(self, oldGravityUp) local ignoreList = {} for i, player in next, PLAYERS:GetPlayers() do ignoreList[i] = player.Character end -- get the normal local hrpCF = self.HRP.CFrame local isR15 = (self.Humanoid.RigType == Enum.HumanoidRigType.R15) local origin = isR15 and hrpCF.p or hrpCF.p + 0.35*oldGravityUp local radialVector = math.abs(hrpCF.LookVector:Dot(oldGravityUp)) < 0.999 and hrpCF.LookVector:Cross(oldGravityUp) or hrpCF.RightVector:Cross(oldGravityUp) local centerRayLength = 25 local centerRay = Ray.new(origin, -centerRayLength * oldGravityUp) local centerHit, centerHitPoint, centerHitNormal = workspace:FindPartOnRayWithIgnoreList(centerRay, ignoreList) --[[disable DrawClass:Clear() DrawClass.Draw3D.Ray(centerRay.Origin, centerRay.Direction) ]] local downHitCount = 0 local totalHitCount = 0 local centerRayHitCount = 0 local evenRayHitCount = 0 local oddRayHitCount = 0 local mainDownNormal = ZERO if (centerHit) then mainDownNormal = centerHitNormal centerRayHitCount = 0 end local downRaySum = ZERO for i = 1, NUM_DOWN_RAYS do local dtheta = PI2 * ((i-1)/NUM_DOWN_RAYS) local angleWeight = 0.25 + 0.75 * math.abs(math.cos(dtheta)) local isEvenRay = (i%2 == 0) local startRadius = isEvenRay and EVEN_DOWN_RAY_START_RADIUS or ODD_DOWN_RAY_START_RADIUS local endRadius = isEvenRay and EVEN_DOWN_RAY_END_RADIUS or ODD_DOWN_RAY_END_RADIUS local downRayLength = centerRayLength local offset = CFrame.fromAxisAngle(oldGravityUp, dtheta) * radialVector local dir = (LOWER_RADIUS_OFFSET * -oldGravityUp + (endRadius - startRadius) * offset) local ray = Ray.new(origin + startRadius * offset, downRayLength * dir.unit) local hit, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList) --[[disable DrawClass.Draw3D.Ray(ray.Origin, ray.Direction) ]] if (hit) then downRaySum = downRaySum + angleWeight * hitNormal downHitCount = downHitCount + 1 if isEvenRay then evenRayHitCount = evenRayHitCount + 1 else oddRayHitCount = oddRayHitCount + 1 end end end local feelerHitCount = 0 local feelerNormalSum = ZERO for i = 1, NUM_FEELER_RAYS do local dtheta = 2 * math.pi * ((i-1)/NUM_FEELER_RAYS) local angleWeight = 0.25 + 0.75 * math.abs(math.cos(dtheta)) local offset = CFrame.fromAxisAngle(oldGravityUp, dtheta) * radialVector local dir = (FEELER_RADIUS * offset + LOWER_RADIUS_OFFSET * -oldGravityUp).unit local feelerOrigin = origin - FEELER_APEX_OFFSET * -oldGravityUp + FEELER_START_OFFSET * dir local ray = Ray.new(feelerOrigin, FEELER_LENGTH * dir) local hit, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList) --[[disable DrawClass.Draw3D.Ray(ray.Origin, ray.Direction) ]] if (hit) then feelerNormalSum = feelerNormalSum + FEELER_WEIGHTING * angleWeight * hitNormal --* hitDistSqInv feelerHitCount = feelerHitCount + 1 end end if (centerRayHitCount + downHitCount + feelerHitCount > 0) then local normalSum = mainDownNormal + downRaySum + feelerNormalSum if (normalSum ~= ZERO) then return normalSum.unit end end return oldGravityUp end Controller.GetGravityUp = GetGravityUp -- E is toggle game:GetService("ContextActionService"):BindAction("Toggle", function(action, state, input) if not (state == Enum.UserInputState.Begin) then return end if (Controller) then Controller:Destroy() Controller = nil else Controller = GravityController.new(PLAYERS.LocalPlayer) Controller.GetGravityUp = GetGravityUp end end, false, Enum.KeyCode.Z) print("end")