Raycasting mobile top-down camera problem

Hello! I wanna create multiplatform top-down camera system but I encountered a problem when I wanted to use the same system o mobile as on PC.

Problem: On PC it works fine but on mobile when I start slowly, it works somehow, but when I accelerate, the whole screen starts shaking. I don’t know why this is not working.

PC:

Mobile:

Maybe Raycasts may not work well on mobile devices.

This is my module script:

Thanks for any help!

local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local GuiService = game:GetService("GuiService")

--local RenderMap = require(ReplicatedStorage.ClientModules.RenderMap)

local ShopCamera = {}
ShopCamera.Connections = {}
ShopCamera.Tweens = {}

ShopCamera.zoomActive = false
ShopCamera.Running = false
ShopCamera.UpdateCamera = true

local camera = workspace.CurrentCamera
local cameraPlayerStartCFrame = nil

local mapShopFolder = workspace:WaitForChild("MapShopFolder")
local backgroundModel = mapShopFolder:WaitForChild("BackgroundModel")
local background = backgroundModel:WaitForChild("Background")


local zoom = 25
local zoomSensitivity = 5
local minZoom, maxZoom = 10, 50

local zoomValue = Instance.new("NumberValue")
zoomValue.Value = zoom

local CAMERA_ROTATION = CFrame.Angles(math.rad(-90), 0, 0)

local mousePressed = false
local mouseAnchorPoint = nil

local cameraPosition = Vector3.new(
	background.Position.X,
	background.Position.Y + zoomValue.Value,
	background.Position.Z
)

local function IsMobile()
	return UserInputService.TouchEnabled
end

local function ClampCameraPosition(pos)
	local cf, size = backgroundModel:GetBoundingBox()

	local halfViewZ = zoomValue.Value * math.tan(math.rad(camera.FieldOfView / 2))

	local aspect = camera.ViewportSize.X / camera.ViewportSize.Y
	local halfViewX = halfViewZ * aspect

	local minX = cf.Position.X - size.X/2 + halfViewX
	local maxX = cf.Position.X + size.X/2 - halfViewX

	local minZ = cf.Position.Z - size.Z/2 + halfViewZ
	local maxZ = cf.Position.Z + size.Z/2 - halfViewZ

	return Vector3.new(
		math.clamp(pos.X, minX, maxX),
		pos.Y,
		math.clamp(pos.Z, minZ, maxZ)
	)
end


function ShopCamera.TweenCameraTo(targetCFrame, duration)

	if not ShopCamera.Running or ShopCamera.Tweens.cameraTween then return end

	local TargetCFrame =
		CFrame.new(background.Position)
		* targetCFrame
		* CFrame.Angles(math.rad(-90), 0, 0)

	ShopCamera.UpdateCamera = false

	local tweenInfo = TweenInfo.new(
		duration,
		Enum.EasingStyle.Sine,
		Enum.EasingDirection.Out
	)

	ShopCamera.Tweens.cameraTween = TweenService:Create(
		camera,
		tweenInfo,
		{CFrame = TargetCFrame}
	)

	ShopCamera.Tweens.cameraTween:Play()

	ShopCamera.Tweens.cameraTween.Completed:Connect(function()


		cameraPosition = ClampCameraPosition(TargetCFrame.Position)

		zoomValue.Value = TargetCFrame.Position.Y - background.Position.Y


		ShopCamera.UpdateCamera = true
		ShopCamera.Tweens.cameraTween = nil
	end)
end

local function TweenZoom(newZoom)
	if ShopCamera.Tweens.zoomTween then
		ShopCamera.Tweens.zoomTween:Cancel()
	end

	ShopCamera.Tweens.zoomTween = TweenService:Create(
		zoomValue,
		TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.Out),
		{Value = newZoom}
	)

	ShopCamera.Tweens.zoomTween:Play()
end

local function RaycastFromMouse(pos)
	
	local ray = camera:ScreenPointToRay(pos.X, pos.Y)

	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Include
	params.FilterDescendantsInstances = {background}
	
	local result = workspace:Raycast(ray.Origin, ray.Direction * 1000, params)
	return result
end


local function Start()
	--RenderMap.Render()

	local touchesPositions = {}
	local lastTouchPosition = nil
	local zoomLastDistance = nil


	cameraPlayerStartCFrame = camera.CFrame
	camera.CameraType = Enum.CameraType.Scriptable


	camera.CFrame = CFrame.new(cameraPosition) * CAMERA_ROTATION

	if IsMobile() then

		GuiService.TouchControlsEnabled = false
		
		ShopCamera.Connections.TouchStartedConnection = UserInputService.TouchStarted:Connect(function(touch)
			touchesPositions[touch] = touch.Position
			local result = RaycastFromMouse(touch.Position)
			if result then
				mouseAnchorPoint = result.Position
			end
		end)

		ShopCamera.Connections.TouchMovedConnection = UserInputService.TouchMoved:Connect(function(touch)
			if not ShopCamera.UpdateCamera then return end

			touchesPositions[touch] = touch.Position

			local touchesCount = 0
			for _ in pairs(touchesPositions) do touchesCount += 1 end
			
			if touchesCount == 1 and not ShopCamera.zoomActive then
				
				local result = RaycastFromMouse(touch.Position)
				if result and mouseAnchorPoint then
					local currentPoint = result.Position			

					local delta = mouseAnchorPoint - currentPoint
					cameraPosition = ClampCameraPosition(Vector3.new(
						cameraPosition.X + delta.X,
						background.Position.Y + zoomValue.Value,
						cameraPosition.Z + delta.Z
						))
				end
			end

			if touchesCount == 2 then
				ShopCamera.zoomActive = true

				local touches = {}
				for _, pos in pairs(touchesPositions) do
					table.insert(touches, pos)
				end

				local zoomCurrentDistance = (touches[1] - touches[2]).Magnitude

				if zoomLastDistance then 
					local delta = zoomCurrentDistance - zoomLastDistance

					local newZoom = zoomValue.Value - delta * zoomSensitivity
					newZoom = math.clamp(newZoom, minZoom, maxZoom)
					TweenZoom(newZoom)
				end

				zoomLastDistance = zoomCurrentDistance
			end
		end)


		ShopCamera.Connections.TouchEndedConnection = UserInputService.TouchEnded:Connect(function(touch)
			touchesPositions[touch] = nil

			local touchesCount = 0
			for _ in pairs(touchesPositions) do touchesCount += 1 end

			if touchesCount < 2 then
				zoomLastDistance = nil
			end

			if touchesCount == 0 then
				mouseAnchorPoint = nil
				ShopCamera.zoomActive = false
			end
			
			touchesCount = 0
			
		end)


	else

		-- PC --

		ShopCamera.Connections.InputBeganConnection =
			UserInputService.InputBegan:Connect(function(input, gp)
				if gp or not ShopCamera.UpdateCamera then return end
				if input.UserInputType == Enum.UserInputType.MouseButton1 then

					mousePressed = true
					local mousePos = UserInputService:GetMouseLocation()
					local result = RaycastFromMouse(mousePos)
					if result then
						mouseAnchorPoint = result.Position
					end
				end
			end)


		ShopCamera.Connections.InputEndedConnection =
			UserInputService.InputEnded:Connect(function(input, gp)
				if gp or not ShopCamera.UpdateCamera then return end
				if input.UserInputType == Enum.UserInputType.MouseButton1 then
					mousePressed = false
					mouseAnchorPoint = nil
				end
			end)


		ShopCamera.Connections.InputChangedConnection =
			UserInputService.InputChanged:Connect(function(input, gp)
				if gp or not ShopCamera.UpdateCamera then return end
				if input.UserInputType == Enum.UserInputType.MouseWheel then

					local newZoom = zoomValue.Value + (-input.Position.Z * zoomSensitivity)
					newZoom = math.clamp(newZoom, minZoom, maxZoom)
					TweenZoom(newZoom)
				end
			end)

	end
	
	ShopCamera.Connections.RenderSteppedConnection =
		RunService.RenderStepped:Connect(function(dt)
		if not ShopCamera.UpdateCamera then return end


		if mousePressed and mouseAnchorPoint then
			

			local mousePos = UserInputService:GetMouseLocation()	
			
			local result = RaycastFromMouse(mousePos)
			if result then
				local currentPoint = result.Position
				local delta = mouseAnchorPoint - currentPoint


				cameraPosition = ClampCameraPosition(Vector3.new(
					cameraPosition.X + delta.X,
					background.Position.Y + zoomValue.Value,
					cameraPosition.Z + delta.Z
					))


			end
		end


		local clamped = ClampCameraPosition(cameraPosition)

		camera.CFrame = CFrame.new(
			clamped.X,
			background.Position.Y + zoomValue.Value,
			clamped.Z
		) * CAMERA_ROTATION


	end)
end

local function Stop()
	for _, c in pairs(ShopCamera.Connections) do
		c:Disconnect()
	end

	for _, t in pairs(ShopCamera.Tweens) do
		t:Cancel()
		t = nil
	end

	--RenderMap.Close()

	ShopCamera.Connections = {}
	mousePressed = false
	mouseAnchorPoint = nil

	camera.CFrame = cameraPlayerStartCFrame
	camera.CameraType = Enum.CameraType.Custom
	GuiService.TouchControlsEnabled = true
end

function ShopCamera.Toggle()
	ShopCamera.Running = not ShopCamera.Running
	if ShopCamera.Running then Start() else Stop() end
	return ShopCamera.Running
end

return ShopCamera

Have you tried the game on a real mobile device?

2 Likes

Yes I tested it on my phone same result.

1 Like

It seems that raycasts are to blame, but what can replace them?

mouse.UnitRay instead of using the camera?

Okey I repair it idk how but work. Is UserinputService.inputBegin better then touchStart?