import "macros" as { $ } $load $FILE -- This is responsible for all touch controls we show (as of this writing, only on iOS) -- this includes character move thumbsticks, and buttons for jump, use of items, camera, etc. -- Written by Ben Tkacheff, Roblox 2013 -- Heliodex's basic New function (basically a simplified version of melt) New = (className, name, props) -> if not props? -- no name was provided props = name name = nil obj = Instance.new className obj.Name = name if name local parent for k, v in pairs props if type(k) == "string" if k == "Parent" parent = v else obj[k] = v elseif type(k) == "number" and type(v) == "userdata" v.Parent = obj obj.Parent = parent obj -- -- obligatory stuff to make sure we don't access nil data wait! until Game wait! until Game\FindFirstChild "Players" wait! until Game.Players.LocalPlayer wait! until Game\FindFirstChild "CoreGui" wait! until Game.CoreGui\FindFirstChild "RobloxGui" userInputService = Game\GetService "UserInputService" success = try userInputService\IsLuaTouchControls! if not success script\Destroy! ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- -- Variables screenResolution = Game\GetService"GuiService"\GetScreenResolution! isSmallScreenDevice = -> return screenResolution.y <= 320 localPlayer = Game.Players.LocalPlayer thumbstickSize = 120 if isSmallScreenDevice! thumbstickSize = 70 touchControlsSheet = "rbxasset://textures/ui/TouchControlsSheet.png" ThumbstickDeadZone = 5 ThumbstickMaxPercentGive = 0.92 thumbstickTouches = {} jumpButtonSize = 90 if isSmallScreenDevice! jumpButtonSize = 70 oldJumpTouches = {} local currentJumpTouch CameraRotateSensitivity = 0.007 CameraRotateDeadZone = CameraRotateSensitivity * 16 CameraZoomSensitivity = 0.03 PinchZoomDelay = 0.2 local cameraTouch -- make sure all of our images are good to go Game\GetService"ContentProvider"\Preload touchControlsSheet ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- -- Functions DistanceBetweenTwoPoints = (point1, point2) -> dx = point2.x - point1.x dy = point2.y - point1.y math.sqrt dx * dx + dy * dy transformFromCenterToTopLeft = (pointToTranslate, guiObject) -> UDim2.new( 0, pointToTranslate.x - guiObject.AbsoluteSize.x / 2, 0, pointToTranslate.y - guiObject.AbsoluteSize.y / 2 ) rotatePointAboutLocation = (pointToRotate, pointToRotateAbout, radians) -> sinAnglePercent = math.sin radians cosAnglePercent = math.cos radians transformedPoint = pointToRotate -- translate point back to origin: transformedPoint = Vector2.new transformedPoint.x - pointToRotateAbout.x, transformedPoint.y - pointToRotateAbout.y -- rotate point xNew = transformedPoint.x * cosAnglePercent - transformedPoint.y * sinAnglePercent yNew = transformedPoint.x * sinAnglePercent + transformedPoint.y * cosAnglePercent -- translate point back: transformedPoint = Vector2.new xNew + pointToRotateAbout.x, yNew + pointToRotateAbout.y transformedPoint dotProduct = (v1, v2) -> v1.x * v2.x + v1.y * v2.y stationaryThumbstickTouchMove = (thumbstickFrame, thumbstickOuter, touchLocation) -> thumbstickOuterCenterPosition = Vector2.new( thumbstickOuter.Position.X.Offset + thumbstickOuter.AbsoluteSize.x / 2, thumbstickOuter.Position.Y.Offset + thumbstickOuter.AbsoluteSize.y / 2 ) centerDiff = DistanceBetweenTwoPoints touchLocation, thumbstickOuterCenterPosition -- thumbstick is moving outside our region, need to cap its distance if centerDiff > (thumbstickSize / 2) thumbVector = Vector2.new( touchLocation.x - thumbstickOuterCenterPosition.x, touchLocation.y - thumbstickOuterCenterPosition.y ) normal = thumbVector.unit if normal.x == math.nan or normal.x == math.inf normal = Vector2.new 0, normal.y if normal.y == math.nan or normal.y == math.inf normal = Vector2.new normal.x, 0 newThumbstickInnerPosition = thumbstickOuterCenterPosition + (normal * (thumbstickSize / 2)) thumbstickFrame.Position = transformFromCenterToTopLeft newThumbstickInnerPosition, thumbstickFrame else thumbstickFrame.Position = transformFromCenterToTopLeft touchLocation, thumbstickFrame Vector2.new( thumbstickFrame.Position.X.Offset - thumbstickOuter.Position.X.Offset, thumbstickFrame.Position.Y.Offset - thumbstickOuter.Position.Y.Offset ) followThumbstickTouchMove = (thumbstickFrame, thumbstickOuter, touchLocation) -> thumbstickOuterCenter = Vector2.new( thumbstickOuter.Position.X.Offset + thumbstickOuter.AbsoluteSize.x / 2, thumbstickOuter.Position.Y.Offset + thumbstickOuter.AbsoluteSize.y / 2 ) -- thumbstick is moving outside our region, need to position outer thumbstick texture carefully (to make look and feel like actual joystick controller) if DistanceBetweenTwoPoints(touchLocation, thumbstickOuterCenter) > thumbstickSize / 2 thumbstickInnerCenter = Vector2.new( thumbstickFrame.Position.X.Offset + thumbstickFrame.AbsoluteSize.x / 2, thumbstickFrame.Position.Y.Offset + thumbstickFrame.AbsoluteSize.y / 2 ) movementVectorUnit = Vector2.new(touchLocation.x - thumbstickInnerCenter.x, touchLocation.y - thumbstickInnerCenter.y).unit outerToInnerVectorCurrent = Vector2.new( thumbstickInnerCenter.x - thumbstickOuterCenter.x, thumbstickInnerCenter.y - thumbstickOuterCenter.y ) outerToInnerVectorCurrentUnit = outerToInnerVectorCurrent.unit movementVector = Vector2.new touchLocation.x - thumbstickInnerCenter.x, touchLocation.y - thumbstickInnerCenter.y -- First, find the angle between the new thumbstick movement vector, -- and the vector between thumbstick inner and thumbstick outer. -- We will use this to pivot thumbstick outer around thumbstick inner, gives a nice joystick feel crossOuterToInnerWithMovement = (outerToInnerVectorCurrentUnit.x * movementVectorUnit.y) - (outerToInnerVectorCurrentUnit.y * movementVectorUnit.x) angle = math.atan2 crossOuterToInnerWithMovement, dotProduct outerToInnerVectorCurrentUnit, movementVectorUnit anglePercent = angle * math.min(movementVector.magnitude / outerToInnerVectorCurrent.magnitude, 1.0) -- If angle is significant, rotate about the inner thumbsticks current center if math.abs(anglePercent) > 0.00001 outerThumbCenter = rotatePointAboutLocation thumbstickOuterCenter, thumbstickInnerCenter, anglePercent thumbstickOuter.Position = transformFromCenterToTopLeft Vector2.new(outerThumbCenter.x, outerThumbCenter.y), thumbstickOuter -- now just translate outer thumbstick to make sure it stays nears inner thumbstick thumbstickOuter.Position = UDim2.new( 0, thumbstickOuter.Position.X.Offset + movementVector.x, 0, thumbstickOuter.Position.Y.Offset + movementVector.y ) thumbstickFrame.Position = transformFromCenterToTopLeft touchLocation, thumbstickFrame -- a bit of error checking to make sure thumbsticks stay close to eachother thumbstickFramePosition = Vector2.new thumbstickFrame.Position.X.Offset, thumbstickFrame.Position.Y.Offset thumbstickOuterPosition = Vector2.new thumbstickOuter.Position.X.Offset, thumbstickOuter.Position.Y.Offset if DistanceBetweenTwoPoints(thumbstickFramePosition, thumbstickOuterPosition) > thumbstickSize / 2 vectorWithLength = (thumbstickOuterPosition - thumbstickFramePosition).unit * thumbstickSize / 2 thumbstickOuter.Position = UDim2.new( 0, thumbstickFramePosition.x + vectorWithLength.x, 0, thumbstickFramePosition.y + vectorWithLength.y ) Vector2.new( thumbstickFrame.Position.X.Offset - thumbstickOuter.Position.X.Offset, thumbstickFrame.Position.Y.Offset - thumbstickOuter.Position.Y.Offset ) movementOutsideDeadZone = (movementVector) -> (math.abs(movementVector.x) > ThumbstickDeadZone) or (math.abs(movementVector.y) > ThumbstickDeadZone) constructThumbstick = (defaultThumbstickPos, updateFunction, stationaryThumbstick) -> thumbstickFrame = New "Frame", "ThumbstickFrame" Active: true Size: UDim2.new 0, thumbstickSize, 0, thumbstickSize Position: defaultThumbstickPos BackgroundTransparency: 1 New "ImageLabel", "InnerThumbstick" Image: touchControlsSheet ImageRectOffset: Vector2.new 220, 0 ImageRectSize: Vector2.new 111, 111 BackgroundTransparency: 1 Size: UDim2.new 0, thumbstickSize / 2, 0, thumbstickSize / 2 Position: UDim2.new( 0, thumbstickFrame.Size.X.Offset / 2 - thumbstickSize / 4, 0, thumbstickFrame.Size.Y.Offset / 2 - thumbstickSize / 4 ) ZIndex: 2 Parent: thumbstickFrame outerThumbstick = New "ImageLabel", "OuterThumbstick" Image: touchControlsSheet ImageRectOffset: Vector2.new 0, 0 ImageRectSize: Vector2.new 220, 220 BackgroundTransparency: 1 Size: UDim2.new 0, thumbstickSize, 0, thumbstickSize Position: defaultThumbstickPos Parent: Game.CoreGui.RobloxGui local thumbstickTouch local userInputServiceTouchMovedCon local userInputSeviceTouchEndedCon startInputTracking = (inputObject) -> return if thumbstickTouch return if inputObject == cameraTouch return if inputObject == currentJumpTouch return if inputObject.UserInputType ~= Enum.UserInputType.Touch thumbstickTouch = inputObject table.insert thumbstickTouches, thumbstickTouch thumbstickFrame.Position = transformFromCenterToTopLeft(thumbstickTouch.Position, thumbstickFrame) outerThumbstick.Position = thumbstickFrame.Position userInputServiceTouchMovedCon = userInputService.TouchMoved\connect (movedInput) -> if movedInput == thumbstickTouch local movementVector if stationaryThumbstick movementVector = stationaryThumbstickTouchMove( thumbstickFrame, outerThumbstick, Vector2.new movedInput.Position.x, movedInput.Position.y ) else movementVector = followThumbstickTouchMove( thumbstickFrame, outerThumbstick, Vector2.new movedInput.Position.x, movedInput.Position.y ) if updateFunction updateFunction movementVector, outerThumbstick.Size.X.Offset / 2 userInputSeviceTouchEndedCon = userInputService.TouchEnded\connect (endedInput) -> if endedInput == thumbstickTouch if updateFunction updateFunction Vector2.new(0, 0), 1 userInputSeviceTouchEndedCon\disconnect! userInputServiceTouchMovedCon\disconnect! thumbstickFrame.Position = defaultThumbstickPos outerThumbstick.Position = defaultThumbstickPos for i, object in pairs thumbstickTouches if object == thumbstickTouch table.remove thumbstickTouches, i break thumbstickTouch = nil userInputService.Changed\connect (prop) -> if prop == "ModalEnabled" thumbstickFrame.Visible = outerThumbstick.Visible = not userInputService.ModalEnabled thumbstickFrame.InputBegan\connect startInputTracking thumbstickFrame setupCharacterMovement = (parentFrame) -> local lastMovementVector, lastMaxMovement moveCharacterFunc = localPlayer.MoveCharacter moveCharacterFunction = (movementVector, maxMovement) -> if localPlayer if movementOutsideDeadZone movementVector lastMovementVector = movementVector lastMaxMovement = maxMovement -- sometimes rounding error will not allow us to go max speed at some -- thumbstick angles, fix this with a bit of fudging near 100% throttle if movementVector.magnitude / maxMovement > ThumbstickMaxPercentGive maxMovement = movementVector.magnitude - 1 moveCharacterFunc localPlayer, movementVector, maxMovement else lastMovementVector = Vector2.new 0, 0 lastMaxMovement = 1 moveCharacterFunc localPlayer, lastMovementVector, lastMaxMovement thumbstickPos = UDim2.new 0, thumbstickSize / 2, 1, -thumbstickSize * 1.75 if isSmallScreenDevice! thumbstickPos = UDim2.new 0, (thumbstickSize / 2) - 10, 1, -thumbstickSize - 20 characterThumbstick = constructThumbstick thumbstickPos, moveCharacterFunction, false characterThumbstick.Name = "CharacterThumbstick" characterThumbstick.Parent = parentFrame refreshCharacterMovement = -> if localPlayer and moveCharacterFunc and lastMovementVector and lastMaxMovement moveCharacterFunc localPlayer, lastMovementVector, lastMaxMovement refreshCharacterMovement setupJumpButton = (parentFrame) -> jumpButton = New "ImageButton", "JumpButton" BackgroundTransparency: 1 Image: touchControlsSheet ImageRectOffset: Vector2.new 176, 222 ImageRectSize: Vector2.new 174, 174 Size: UDim2.new 0, jumpButtonSize, 0, jumpButtonSize Position: UDim2.new 1, if isSmallScreenDevice! -(jumpButtonSize * 2.25), 1, -jumpButtonSize - 20 else -(jumpButtonSize * 2.75), 1, -jumpButtonSize - 120 playerJumpFunc = localPlayer.JumpCharacter doJumpLoop = -> while currentJumpTouch if localPlayer playerJumpFunc localPlayer wait 1 / 60 jumpButton.InputBegan\connect (inputObject) -> return if inputObject.UserInputType ~= Enum.UserInputType.Touch return if currentJumpTouch return if inputObject == cameraTouch for _, touch in pairs oldJumpTouches return if touch == inputObject currentJumpTouch = inputObject jumpButton.ImageRectOffset = Vector2.new 0, 222 jumpButton.ImageRectSize = Vector2.new 174, 174 doJumpLoop! jumpButton.InputEnded\connect (inputObject) -> return if inputObject.UserInputType ~= Enum.UserInputType.Touch jumpButton.ImageRectOffset = Vector2.new 176, 222 jumpButton.ImageRectSize = Vector2.new 174, 174 if inputObject == currentJumpTouch table.insert oldJumpTouches, currentJumpTouch currentJumpTouch = nil userInputService.InputEnded\connect (globalInputObject) -> for i, touch in pairs oldJumpTouches if touch == globalInputObject table.remove oldJumpTouches, i break userInputService.Changed\connect (prop) -> if prop == "ModalEnabled" jumpButton.Visible = not userInputService.ModalEnabled jumpButton.Parent = parentFrame isTouchUsedByJumpButton = (touch) -> if touch == currentJumpTouch return true for _, touchToCompare in pairs oldJumpTouches if touch == touchToCompare return true false isTouchUsedByThumbstick = (touch) -> for _, touchToCompare in pairs thumbstickTouches if touch == touchToCompare return true false setupCameraControl = (parentFrame, refreshCharacterMoveFunc) -> local lastPos hasRotatedCamera = false rotateCameraFunc = userInputService.RotateCamera pinchTime = -1 shouldPinch = false local lastPinchScale zoomCameraFunc = userInputService.ZoomCamera pinchTouches = {} local pinchFrame resetCameraRotateState = -> cameraTouch = nil hasRotatedCamera = false lastPos = nil resetPinchState = -> pinchTouches = {} lastPinchScale = nil shouldPinch = false pinchFrame\Destroy! pinchFrame = nil startPinch = (firstTouch, secondTouch) -> -- track pinching in new frame pinchFrame?\Destroy! -- make sure we didn't track in any mud pinchFrame = New "Frame" Name: "PinchFrame" BackgroundTransparency: 1 Size: UDim2.new 1, 0, 1, 0 Parent: parentFrame pinchFrame.InputChanged\connect (inputObject) -> if not shouldPinch resetPinchState! return resetCameraRotateState! if not lastPinchScale? then -- first pinch move, just set up scale if inputObject == firstTouch lastPinchScale = (inputObject.Position - secondTouch.Position).magnitude firstTouch = inputObject elseif inputObject == secondTouch lastPinchScale = (inputObject.Position - firstTouch.Position).magnitude secondTouch = inputObject else -- we are now actually pinching, do comparison to last pinch size newPinchDistance = 0 if inputObject == firstTouch newPinchDistance = (inputObject.Position - secondTouch.Position).magnitude firstTouch = inputObject elseif inputObject == secondTouch newPinchDistance = (inputObject.Position - firstTouch.Position).magnitude secondTouch = inputObject if newPinchDistance ~= 0 pinchDiff = newPinchDistance - lastPinchScale if pinchDiff ~= 0 zoomCameraFunc userInputService, (pinchDiff * CameraZoomSensitivity) lastPinchScale = newPinchDistance pinchFrame.InputEnded\connect (inputObject) -> -- pinch is over, destroy all if inputObject == firstTouch or inputObject == secondTouch resetPinchState! pinchGestureReceivedTouch = (inputObject) -> if #pinchTouches < 1 table.insert pinchTouches, inputObject pinchTime = tick! shouldPinch = false elseif #pinchTouches == 1 shouldPinch = ((tick! - pinchTime) <= PinchZoomDelay) if shouldPinch table.insert pinchTouches, inputObject startPinch pinchTouches[1], pinchTouches[2] else -- shouldn't ever get here, but just in case pinchTouches = {} parentFrame.InputBegan\connect (inputObject) -> return if inputObject.UserInputType ~= Enum.UserInputType.Touch return if isTouchUsedByJumpButton inputObject usedByThumbstick = isTouchUsedByThumbstick inputObject if not usedByThumbstick pinchGestureReceivedTouch inputObject if not cameraTouch? and not usedByThumbstick cameraTouch = inputObject lastPos = Vector2.new cameraTouch.Position.x, cameraTouch.Position.y -- lastTick = tick! userInputService.InputChanged\connect (inputObject) -> return if inputObject.UserInputType ~= Enum.UserInputType.Touch return if cameraTouch ~= inputObject newPos = Vector2.new cameraTouch.Position.x, cameraTouch.Position.y touchDiff = (lastPos - newPos) * CameraRotateSensitivity -- first time rotating outside deadzone, just setup for next changed event if not hasRotatedCamera and (touchDiff.magnitude > CameraRotateDeadZone) hasRotatedCamera = true lastPos = newPos -- fire everytime after we have rotated out of deadzone if hasRotatedCamera and (lastPos ~= newPos) rotateCameraFunc userInputService, touchDiff refreshCharacterMoveFunc! lastPos = newPos userInputService.InputEnded\connect (inputObject) -> if cameraTouch == inputObject or not cameraTouch? resetCameraRotateState! for i, touch in pairs pinchTouches if touch == inputObject table.remove pinchTouches, i setupTouchControls = -> touchControlFrame = New "Frame", "TouchControlFrame" Size: UDim2.new 1, 0, 1, 0 BackgroundTransparency: 1 Parent: Game.CoreGui.RobloxGui refreshCharacterMoveFunc = setupCharacterMovement touchControlFrame setupJumpButton touchControlFrame setupCameraControl touchControlFrame, refreshCharacterMoveFunc userInputService.ProcessedEvent\connect (inputObject, processed) -> return if not processed -- kill camera pan if the touch is used by some user controls if inputObject == cameraTouch and inputObject.UserInputState == Enum.UserInputState.Begin cameraTouch = nil ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- -- Start of Script -- if true then --userInputService\IsLuaTouchControls! setupTouchControls! -- else -- script\Destroy!