How do re-create original built-in mobile control that are seamless?

I want to create a custom make mobile control that look like original built-in mobile control, as the same time is more customizable.

Here is the video demo

Here is the code

local UIS = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")

local joystickBG = script.Parent
local knob = joystickBG:WaitForChild("Knob")

local dragging = false
local dragTouchId = nil
local radius = joystickBG.AbsoluteSize.X / 2
local moveDirection = Vector2.new(0, 0)
local currentMoveDirection = Vector2.new(0, 0)
local DEADZONE = 0.12 -- 12% deadzone, tweak as needed
local RELEASE_DECAY = 0.18 -- seconds to fully stop after release

-- Utility: Get direction and distance from center
local function getDirectionVector(pos)
	local center = joystickBG.AbsolutePosition + joystickBG.AbsoluteSize / 2
	local offset = Vector2.new(pos.X, pos.Y) - Vector2.new(center.X, center.Y)
	local distance = math.min(offset.Magnitude, radius)
	local direction = offset.Magnitude > 0 and offset.Unit or Vector2.new(0, 0)
	return direction, distance
end

local function updateKnob(pos)
	local direction, distance = getDirectionVector(pos)
	-- Clamp knob to circle
	knob.Position = UDim2.new(0.5, direction.X * distance, 0.5, direction.Y * distance)
	-- Deadzone logic
	if distance / radius < DEADZONE then
		moveDirection = Vector2.new(0, 0)
	else
		-- Scale so full radius = 1, deadzone = 0
		local scaled = (distance - radius * DEADZONE) / (radius * (1 - DEADZONE))
		moveDirection = direction * math.clamp(scaled, 0, 1)
	end
end

local function resetKnob()
	-- Smoothly reset the knob to center
	TweenService:Create(knob, TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
		Position = UDim2.new(0.5, 0, 0.5, 0)
	}):Play()
	moveDirection = Vector2.new(0, 0)
end

-- Touch handling: robust, works even if finger leaves joystick frame
UIS.TouchStarted:Connect(function(input, processed)
	if processed then return end
	if not dragging then
		-- Only start drag if touch is inside joystick
		local absPos = Vector2.new(input.Position.X, input.Position.Y)
		local center = joystickBG.AbsolutePosition + joystickBG.AbsoluteSize / 2
		if (absPos - Vector2.new(center.X, center.Y)).Magnitude <= radius then
			dragging = true
			dragTouchId = input.UserInputState == Enum.UserInputState.Begin and input.TouchId or nil
			updateKnob(input.Position)
		end
	end
end)

UIS.TouchMoved:Connect(function(input, processed)
	if dragging and (not dragTouchId or input.TouchId == dragTouchId) then
		updateKnob(input.Position)
	end
end)

UIS.TouchEnded:Connect(function(input, processed)
	if dragging and (not dragTouchId or input.TouchId == dragTouchId) then
		dragging = false
		dragTouchId = nil
		resetKnob()
	end
end)

-- Fallback for mouse input (for testing in studio)
joystickBG.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		dragging = true
		updateKnob(input.Position)
	end
end)
joystickBG.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		dragging = false
		resetKnob()
	end
end)
UIS.InputChanged:Connect(function(input)
	if dragging and input.UserInputType == Enum.UserInputType.MouseMovement then
		updateKnob(input.Position)
	end
end)

-- Move the player using the joystick direction, matching built-in Roblox joystick
RunService.RenderStepped:Connect(function(dt)
	local player = Players.LocalPlayer
	local character = player.Character
	if character then
		local humanoid = character:FindFirstChildOfClass("Humanoid")
		if humanoid then
			-- Smoothly interpolate movement direction when released
			if dragging and (moveDirection.Magnitude > 0) then
				currentMoveDirection = moveDirection
			else
				-- Lerp to zero over RELEASE_DECAY seconds
				currentMoveDirection = currentMoveDirection:Lerp(Vector2.new(0,0), math.clamp(dt / RELEASE_DECAY, 0, 1))
			end

			local camera = workspace.CurrentCamera
			if camera then
				-- Get camera's CFrame
				local camCF = camera.CFrame
				-- Project camera's LookVector and RightVector onto XZ plane and normalize
				local camForward = Vector3.new(camCF.LookVector.X, 0, camCF.LookVector.Z)
				if camForward.Magnitude > 0 then
					camForward = camForward.Unit
				end
				local camRight = Vector3.new(camCF.RightVector.X, 0, camCF.RightVector.Z)
				if camRight.Magnitude > 0 then
					camRight = camRight.Unit
				end
				-- Y axis on joystick is -1 at top, +1 at bottom, so invert Y for correct forward
				local moveVec = camRight * currentMoveDirection.X + camForward * -currentMoveDirection.Y
				if moveVec.Magnitude > 1 then
					moveVec = moveVec.Unit
				end
				humanoid:Move(moveVec, false)
			end
		end
	end
end)

MobileControls.rbxm (8.7 KB)