--[[ -- Original By Kip Turner, Copyright Roblox 2014 -- Updated by Garnold to utilize the new PathfindingService API, 2017 -- 2018 PlayerScripts Update - AllYourBlox --]] --[[ Roblox Services ]]-- local UserInputService = game:GetService("UserInputService") local ContextActionService = game:GetService("ContextActionService") local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") local RunService = game:GetService("RunService") local DebrisService = game:GetService('Debris') local ReplicatedStorage = game:GetService('ReplicatedStorage') local TweenService = game:GetService("TweenService") --[[ Constants ]]-- local ZERO_VECTOR3 = Vector3.new(0,0,0) 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 FFlagUserNavigationFixClickToMoveInterruptionSuccess, FFlagUserNavigationFixClickToMoveInterruptionResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationFixClickToMoveInterruption") end) local FFlagUserNavigationFixClickToMoveInterruption = FFlagUserNavigationFixClickToMoveInterruptionSuccess and FFlagUserNavigationFixClickToMoveInterruptionResult local Player = Players.LocalPlayer local PlayerScripts = Player.PlayerScripts local TouchJump = nil local SHOW_PATH = true local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList local CurrentSeatPart = nil local DrivingTo = nil local XZ_VECTOR3 = Vector3.new(1,0,1) local ZERO_VECTOR3 = Vector3.new(0,0,0) local ZERO_VECTOR2 = Vector2.new(0,0) local BindableEvent_OnFailStateChanged = nil if UserInputService.TouchEnabled then -- BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged() end --------------------------UTIL LIBRARY------------------------------- local Utility = {} do local function ViewSizeX() local camera = workspace.CurrentCamera local x = camera and camera.ViewportSize.X or 0 local y = camera and camera.ViewportSize.Y or 0 if x == 0 then return 1024 else if x > y then return x else return y end end end Utility.ViewSizeX = ViewSizeX local function ViewSizeY() local camera = workspace.CurrentCamera local x = camera and camera.ViewportSize.X or 0 local y = camera and camera.ViewportSize.Y or 0 if y == 0 then return 768 else if x > y then return y else return x end end end Utility.ViewSizeY = ViewSizeY local function FindCharacterAncestor(part) if part then local humanoid = part:FindFirstChild("Humanoid") if humanoid then return part, humanoid else return FindCharacterAncestor(part.Parent) end end end Utility.FindCharacterAncestor = FindCharacterAncestor local function Raycast(ray, ignoreNonCollidable, ignoreList) local ignoreList = ignoreList or {} local hitPart, hitPos, hitNorm, hitMat = RayCastIgnoreList(workspace, ray, ignoreList) if hitPart then if ignoreNonCollidable and hitPart.CanCollide == false then table.insert(ignoreList, hitPart) return Raycast(ray, ignoreNonCollidable, ignoreList) end return hitPart, hitPos, hitNorm, hitMat end return nil, nil end Utility.Raycast = Raycast local function AveragePoints(positions) local avgPos = ZERO_VECTOR2 if #positions > 0 then for i = 1, #positions do avgPos = avgPos + positions[i] end avgPos = avgPos / #positions end return avgPos end Utility.AveragePoints = AveragePoints 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 function GetCharacter() return Player and Player.Character end local function GetTorso() local humanoid = findPlayerHumanoid(Player) return humanoid and humanoid.RootPart end local function getIgnoreList() if CurrentIgnoreList then return CurrentIgnoreList end CurrentIgnoreList = {} table.insert(CurrentIgnoreList, GetCharacter()) return CurrentIgnoreList end -----------------------------------PATHER-------------------------------------- local popupAdornee local function getPopupAdorneePart() --Handle the case of the adornee part getting deleted (camera changed, maybe) if popupAdornee and not popupAdornee.Parent then popupAdornee = nil end --If the adornee doesn't exist yet, create it if not popupAdornee then popupAdornee = Instance.new("Part") popupAdornee.Name = "ClickToMovePopupAdornee" popupAdornee.Transparency = 1 popupAdornee.CanCollide = false popupAdornee.Anchored = true popupAdornee.Size = Vector3.new(2, 2, 2) popupAdornee.CFrame = CFrame.new() popupAdornee.Parent = workspace.CurrentCamera end return popupAdornee end local activePopups = {} local function createNewPopup(popupType) local newModel = Instance.new("ImageHandleAdornment") newModel.AlwaysOnTop = false newModel.Transparency = 1 newModel.Size = ZERO_VECTOR2 newModel.SizeRelativeOffset = ZERO_VECTOR3 newModel.Image = "rbxasset://textures/ui/move.png" newModel.ZIndex = 20 local radius = 0 if popupType == "DestinationPopup" then newModel.Color3 = Color3.fromRGB(0, 175, 255) radius = 1.25 elseif popupType == "DirectWalkPopup" then newModel.Color3 = Color3.fromRGB(0, 175, 255) radius = 1.25 elseif popupType == "FailurePopup" then newModel.Color3 = Color3.fromRGB(255, 100, 100) radius = 1.25 elseif popupType == "PatherPopup" then newModel.Color3 = Color3.fromRGB(255, 255, 255) radius = 1 newModel.ZIndex = 10 end newModel.Size = Vector2.new(5, 0.1) * radius local dataStructure = {} dataStructure.Model = newModel activePopups[#activePopups + 1] = newModel function dataStructure:TweenIn() local tweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out) local tween1 = TweenService:Create(newModel, tweenInfo, { Size = Vector2.new(2,2) * radius }) tween1:Play() TweenService:Create(newModel, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 0, false, 0.1), { Transparency = 0, SizeRelativeOffset = Vector3.new(0, radius * 1.5, 0) }):Play() return tween1 end function dataStructure:TweenOut() local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In) local tween1 = TweenService:Create(newModel, tweenInfo, { Size = ZERO_VECTOR2 }) tween1:Play() coroutine.wrap(function() tween1.Completed:Wait() for i = 1, #activePopups do if activePopups[i] == newModel then table.remove(activePopups, i) break end end end)() return tween1 end function dataStructure:Place(position, dest) -- place the model at position if not self.Model.Parent then local popupAdorneePart = getPopupAdorneePart() self.Model.Parent = popupAdorneePart self.Model.Adornee = popupAdorneePart --Start the 10-stud long ray 2.5 studs above where the tap happened and point straight down to try to find --the actual ground 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, Player.Character }) self.Model.CFrame = CFrame.new(hitPoint) + Vector3.new(0, -radius,0) end end return dataStructure end local function createPopupPath(points, numCircles) -- creates a path with the provided points, using the path and number of circles provided local popups = {} local stopTraversing = false local function killPopup(i) -- kill all popups before and at i for iter, v in pairs(popups) do if iter <= i then local tween = v:TweenOut() spawn(function() tween.Completed:Wait() v.Model:Destroy() end) popups[iter] = nil end end end local function stopFunction() stopTraversing = true killPopup(#points) end spawn(function() for i = 1, #points do if stopTraversing then break end local includeWaypoint = i % numCircles == 0 and i < #points and (points[#points].Position - points[i].Position).magnitude > 4 if includeWaypoint then local popup = createNewPopup("PatherPopup") popups[i] = popup local nextPopup = points[i+1] popup:Place(points[i].Position, nextPopup and nextPopup.Position or points[#points].Position) local tween = popup:TweenIn() wait(0.2) end end end) return stopFunction, killPopup end local function Pather(character, endPoint, surfaceNormal) local this = {} this.Cancelled = false this.Started = false this.Finished = Instance.new("BindableEvent") this.PathFailed = Instance.new("BindableEvent") this.PathComputing = false this.PathComputed = false this.TargetPoint = endPoint this.TargetSurfaceNormal = surfaceNormal this.DiedConn = nil this.SeatedConn = nil this.MoveToConn = nil this.CurrentPoint = 0 function this:Cleanup() if this.stopTraverseFunc then this.stopTraverseFunc() end if this.MoveToConn then this.MoveToConn:Disconnect() this.MoveToConn = nil end if this.DiedConn then this.DiedConn:Disconnect() this.DiedConn = nil end if this.SeatedConn then this.SeatedConn:Disconnect() this.SeatedConn = nil end this.humanoid = nil end function this:Cancel() this.Cancelled = true this:Cleanup() end function this:OnPathInterrupted() -- Stop moving this.Cancelled = true this:OnPointReached(false) end function this:ComputePath() local humanoid = findPlayerHumanoid(Player) local torso = humanoid and humanoid.Torso local success = false if torso then if this.PathComputed or this.PathComputing then return end this.PathComputing = true success = pcall(function() this.pathResult = PathfindingService:FindPathAsync(torso.CFrame.p, this.TargetPoint) end) this.pointList = this.pathResult and this.pathResult:GetWaypoints() this.PathComputing = false this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false end return true end function this:IsValidPath() if not this.pathResult then this:ComputePath() end return this.pathResult.Status == Enum.PathStatus.Success end function this:OnPointReached(reached) if reached and not this.Cancelled then this.CurrentPoint = this.CurrentPoint + 1 if this.CurrentPoint > #this.pointList then -- End of path reached if this.stopTraverseFunc then this.stopTraverseFunc() end this.Finished:Fire() this:Cleanup() else -- If next action == Jump, but the humanoid -- is still jumping from a previous action -- wait until it gets to the ground if this.CurrentPoint + 1 <= #this.pointList then local nextAction = this.pointList[this.CurrentPoint + 1].Action if nextAction == Enum.PathWaypointAction.Jump then local currentState = this.humanoid:GetState() if currentState == Enum.HumanoidStateType.FallingDown or currentState == Enum.HumanoidStateType.Freefall or currentState == Enum.HumanoidStateType.Jumping 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 end -- Move to the next point if this.setPointFunc then this.setPointFunc(this.CurrentPoint) end local nextWaypoint = this.pointList[this.CurrentPoint] if nextWaypoint.Action == Enum.PathWaypointAction.Jump then this.humanoid.Jump = true end this.humanoid:MoveTo(nextWaypoint.Position) end else this.PathFailed:Fire() this:Cleanup() end end function this:Start() if CurrentSeatPart then return end this.humanoid = findPlayerHumanoid(Player) if FFlagUserNavigationFixClickToMoveInterruption and not this.humanoid then this.PathFailed:Fire() return end if this.Started then return end this.Started = true if SHOW_PATH then -- choose whichever one Mike likes best this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4) end if #this.pointList > 0 then if FFlagUserNavigationFixClickToMoveInterruption then this.SeatedConn = this.humanoid.Seated:Connect(function(reached) this:OnPathInterrupted() end) this.DiedConn = this.humanoid.Died:Connect(function(reached) this:OnPathInterrupted() end) end this.MoveToConn = this.humanoid.MoveToFinished:Connect(function(reached) this:OnPointReached(reached) end) 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 this:ComputePath() if not this.PathComputed then -- set the end point towards the camera and raycasted towards the ground in case we hit a wall local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5 local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50) local newHitPart, newHitPos = RayCastIgnoreList(workspace, ray, getIgnoreList()) if newHitPart then this.TargetPoint = newHitPos end -- try again this:ComputePath() end return this end ------------------------------------------------------------------------- local function IsInBottomLeft(pt) local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) local joystickWidth = joystickHeight return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight end local function IsInBottomRight(pt) local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250) local joystickWidth = joystickHeight return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight end local function CheckAlive(character) 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() DrivingTo = nil if ExistingPather then ExistingPather:Cancel() end if PathCompleteListener then PathCompleteListener:Disconnect() PathCompleteListener = nil end if PathFailedListener then PathFailedListener:Disconnect() PathFailedListener = nil end if ExistingIndicator then local obj = ExistingIndicator local tween = obj:TweenOut() local tweenCompleteEvent = nil tweenCompleteEvent = tween.Completed:connect(function() tweenCompleteEvent:Disconnect() obj.Model:Destroy() end) ExistingIndicator = nil end end local function getExtentsSize(Parts) local maxX,maxY,maxZ = -math.huge,-math.huge,-math.huge local minX,minY,minZ = math.huge,math.huge,math.huge for i = 1, #Parts do maxX,maxY,maxZ = math.max(maxX, Parts[i].Position.X), math.max(maxY, Parts[i].Position.Y), math.max(maxZ, Parts[i].Position.Z) minX,minY,minZ = math.min(minX, Parts[i].Position.X), math.min(minY, Parts[i].Position.Y), math.min(minZ, Parts[i].Position.Z) end return Region3.new(Vector3.new(minX, minY, minZ), Vector3.new(maxX, maxY, maxZ)) end local function inExtents(Extents, Position) if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then return false end if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then return false end --ignoring Y for now return true end local function showQuickPopupAsync(position, popupType) local popup = createNewPopup(popupType) popup:Place(position, Vector3.new(0,position.y,0)) local tweenIn = popup:TweenIn() tweenIn.Completed:Wait() local tweenOut = popup:TweenOut() tweenOut.Completed:Wait() popup.Model:Destroy() popup = nil end local FailCount = 0 local function OnTap(tapPositions, goToPoint) -- Good to remember if this is the latest tap event local camera = workspace.CurrentCamera local character = Player.Character if not CheckAlive(character) 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) -- inivisicam stuff local initIgnore = getIgnoreList() local invisicamParts = {} --InvisicamModule and InvisicamModule:GetObscuredParts() or {} local ignoreTab = {} -- add to the ignore list for i, v in pairs(invisicamParts) do ignoreTab[#ignoreTab+1] = i end for i = 1, #initIgnore do ignoreTab[#ignoreTab+1] = initIgnore[i] end -- local myHumanoid = findPlayerHumanoid(Player) local hitPart, hitPt, hitNormal, hitMat = Utility.Raycast(ray, true, ignoreTab) local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart) local torso = GetTorso() local startPos = torso.CFrame.p if goToPoint then hitPt = goToPoint hitChar = nil end if hitChar and hitHumanoid and hitHumanoid.RootPart and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then CleanupPath() if myHumanoid then myHumanoid:MoveTo(hitPt) end -- Do shoot local currentWeapon = GetEquippedTool(character) if currentWeapon then currentWeapon:Activate() LastFired = tick() end elseif hitPt and character and not CurrentSeatPart then local thisPather = Pather(character, hitPt, hitNormal) if thisPather:IsValidPath() then FailCount = 0 thisPather:Start() if BindableEvent_OnFailStateChanged then BindableEvent_OnFailStateChanged:Fire(false) end CleanupPath() local destinationPopup = createNewPopup("DestinationPopup") destinationPopup:Place(hitPt, Vector3.new(0,hitPt.y,0)) local failurePopup = createNewPopup("FailurePopup") local currentTween = destinationPopup:TweenIn() ExistingPather = thisPather ExistingIndicator = destinationPopup PathCompleteListener = thisPather.Finished.Event:Connect(function() if destinationPopup then if ExistingIndicator == destinationPopup then ExistingIndicator = nil end local tween = destinationPopup:TweenOut() local tweenCompleteEvent = nil tweenCompleteEvent = tween.Completed:Connect(function() tweenCompleteEvent:Disconnect() destinationPopup.Model:Destroy() destinationPopup = nil end) end if hitChar then local humanoid = findPlayerHumanoid(Player) local currentWeapon = GetEquippedTool(character) if currentWeapon then currentWeapon:Activate() LastFired = tick() end if humanoid then humanoid:MoveTo(hitPt) end end end) PathFailedListener = thisPather.PathFailed.Event:Connect(function() if FFlagUserNavigationFixClickToMoveInterruption then CleanupPath() end if failurePopup then failurePopup:Place(hitPt, Vector3.new(0,hitPt.y,0)) local failTweenIn = failurePopup:TweenIn() failTweenIn.Completed:Wait() local failTweenOut = failurePopup:TweenOut() failTweenOut.Completed:Wait() failurePopup.Model:Destroy() failurePopup = nil end end) else if hitPt then -- Feedback here for when we don't have a good path local foundDirectPath = false if (hitPt-startPos).Magnitude < 25 and (startPos.y-hitPt.y > -3) then -- move directly here if myHumanoid then if myHumanoid.Sit then myHumanoid.Jump = true end myHumanoid:MoveTo(hitPt) foundDirectPath = true end end coroutine.wrap(showQuickPopupAsync)(hitPt, foundDirectPath and "DirectWalkPopup" or "FailurePopup") end end elseif hitPt and character and CurrentSeatPart then local destinationPopup = createNewPopup("DestinationPopup") ExistingIndicator = destinationPopup destinationPopup:Place(hitPt, Vector3.new(0,hitPt.y,0)) destinationPopup:TweenIn() DrivingTo = hitPt local ConnectedParts = CurrentSeatPart:GetConnectedParts(true) while wait() do if CurrentSeatPart and ExistingIndicator == destinationPopup then local ExtentsSize = getExtentsSize(ConnectedParts) if inExtents(ExtentsSize, hitPt) then local popup = destinationPopup spawn(function() local tweenOut = popup:TweenOut() tweenOut.Completed:Wait() popup.Model:Destroy() end) destinationPopup = nil DrivingTo = nil break end else if CurrentSeatPart == nil and destinationPopup == ExistingIndicator then DrivingTo = nil OnTap(tapPositions, hitPt) end local popup = destinationPopup spawn(function() local tweenOut = popup:TweenOut() tweenOut.Completed:Wait() popup.Model:Destroy() end) destinationPopup = nil break end end end end elseif #tapPositions >= 2 then if camera then -- Do shoot local avgPoint = Utility.AveragePoints(tapPositions) local unitRay = camera:ScreenPointToRay(avgPoint.x, avgPoint.y) local currentWeapon = GetEquippedTool(character) if currentWeapon then currentWeapon:Activate() LastFired = tick() end end end end local function IsFinite(num) return num == num and num ~= 1/0 and num ~= -1/0 end local function findAngleBetweenXZVectors(vec2, vec1) return math.atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z) end local function DisconnectEvent(event) if event then event:Disconnect() end end --[[ The ClickToMove Controller Class ]]-- local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController")) local ClickToMove = setmetatable({}, BaseCharacterController) ClickToMove.__index = ClickToMove function ClickToMove.new() print("Instantiating Keyboard Controller") local self = setmetatable(BaseCharacterController.new(), 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.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.humanoidSeatedConn = nil self.running = 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) -- TODO: Resolve with ControlScript handling of seating for vehicles DisconnectEvent(self.humanoidSeatedConn) pcall(function() RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate") end) 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) -- Give back controls when they tap both sticks local wasInBottomLeft = IsInBottomLeft(input.Position) local wasInBottomRight = IsInBottomRight(input.Position) if wasInBottomRight or wasInBottomLeft then for otherInput, _ in pairs(self.fingerTouches) do if otherInput ~= input then local otherInputInLeft = IsInBottomLeft(otherInput.Position) local otherInputInRight = IsInBottomRight(otherInput.Position) if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then if BindableEvent_OnFailStateChanged then BindableEvent_OnFailStateChanged:Fire(true) end return end end end end end -- Cancel path when you use the keyboard controls. if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then CleanupPath() 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 if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 then local positions = {currPos} OnTap(positions) end end end) self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed) if not processed then OnTap(touchPositions) end end) local function computeThrottle(dist) if dist > .2 then return 0.5+(dist^2)/2 else return 0 end end local lastSteer = 0 --kP = how much the steering corrects for the current error in driving angle --kD = how much the steering corrects for how quickly the error in driving angle is changing local kP = 1 local kD = 0.5 local function getThrottleAndSteer(object, point) local throttle, steer = 0, 0 local oCF = object.CFrame local relativePosition = oCF:pointToObjectSpace(point) local relativeZDirection = -relativePosition.z local relativeDistance = relativePosition.magnitude -- throttle quadratically increases from 0-1 as distance from the selected point goes from 0-50, after 50, throttle is 1. -- this allows shorter distance travel to have more fine-tuned control. throttle = computeThrottle(math.min(1,relativeDistance/50))*math.sign(relativeZDirection) local steerAngle = -math.atan2(-relativePosition.x, -relativePosition.z) steer = steerAngle/(math.pi/4) local steerDelta = steer - lastSteer lastSteer = steer local pdSteer = kP * steer + kD * steer return throttle, pdSteer end local function Update() if CurrentSeatPart then if DrivingTo then local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo) CurrentSeatPart.ThrottleFloat = throttle CurrentSeatPart.SteerFloat = steer else CurrentSeatPart.ThrottleFloat = 0 CurrentSeatPart.SteerFloat = 0 end end local cameraPos = workspace.CurrentCamera.CFrame.p for i = 1, #activePopups do local popup = activePopups[i] popup.CFrame = CFrame.new(popup.CFrame.p, cameraPos) end end RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update) -- TODO: Resolve with control script seating functionality -- local function onSeated(child, active, currentSeatPart) -- if active then -- if TouchJump and UserInputService.TouchEnabled then -- TouchJump:Enable() -- end -- if currentSeatPart and currentSeatPart.ClassName == "VehicleSeat" then -- CurrentSeatPart = currentSeatPart -- end -- else -- CurrentSeatPart = nil -- if TouchJump and UserInputService.TouchEnabled then -- TouchJump:Disable() -- end -- end -- 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) -- self.humanoidSeatedConn = child.Seated:Connect(function(active, seat) onSeated(child, active, seat) end) -- if child.SeatPart then -- onSeated(child, true, child.SeatPart) -- 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:Enable(enable) 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 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 DrivingTo = nil self.running = false end end self.enabled = enable end return ClickToMove