UserInputService.TouchTapInWorld but with holding

Hey there!
So I have a button that should be held by mobile users
How I do this is with raycasting and stuff

Anyways my problem is that TouchTapInWorld (more info)
doesn’t work with holding so I kinda just have to stick around with taps,
Can someone help me make it work with holding too?

The reason behind using TouchTapInWorld is because it returns world positions which are accurate touch positions, to demonstrate this:

UIS.TouchTapInWorld:Connect(function(positions) 
	print("tap in world positions: ", positions.X, positions.Y)
end)

UIS.TouchTap:Connect(function(t: {any}, gameProcessedEvent: boolean) 
	print("regular tap positions: ", t[1].X, t[1].Y)
end)

image

Anyways,
Here’s some of the code I currently have to use:

UIS.TouchTapInWorld:Connect(function(position: Vector2, processedByUI: boolean) 
		local x, y = position.X, position.Y
		local result = Raycast(chr, x, y)
		local instance = result and result.Instance
		local descendant :Model = ...

		-- do the rest of the stuff I want
end)

UIS.TouchEnded:Connect(function(touch: InputObject, gameProcessedEvent: boolean) 
        -- touch ended
end)
	

This is how I handle the mobile touching currently
Please if you got any idea on how to make this work with holding too, let me know in this topic.
Have a good day!

1 Like

You might be able to use UserInputService.TouchLongPress to detect this by checking if the third parameter is false (which is gameProcessedEvent, which would be true if a user interacted with a UI object).


Alternatively, you could replace both TouchTapInWorld and TouchLongPress with UserInputService.TouchStarted, which would detect when a player has touched the screen regardless of the duration of the press (however, it does not return a Vector2 value with this. You would need to refer to the X and Y coordinate of inputObject.Position for that).

UserInputService.TouchLongPress
also returns positions which won’t work for my case, they are supposed to be world positions.
I have already tried every single method, If you think this will work please elaborate more or even write me a little code that I can check, thanks for the response!

1 Like

Hey guys, so after some more research it was this simple… :skull:
Had to use GetMouseLocation from User input service

UIS.TouchStarted:Connect(function(touch: InputObject, gameProcessedEvent: boolean) 
	  local position = UIS:GetMouseLocation()
	  local x, y = position.X, position.Y
	  local result = Raycast(chr, x, y)
	  local instance = result and result.Instance
end)

UIS.TouchEnded:Connect(function(touch: InputObject, gameProcessedEvent: boolean) 
      -- hold stop
end)

EDIT :
If you want to dig deeper into this topic, @StrongBigeMan9 posted a really good explanation which covers everything in this topic, you can find good examples that you’ll eventually learn from
You can find his reply here

Huge thank you to @StrongBigeMan9 :heart: !

1 Like

After 2 hours of experimenting (and many more hours to complete this post, mostly for creating the example place file included at the end of this post), I have a much better understanding of each of the events, including why TouchTapInWorld seemed to be so much more accurate than TouchTap and TouchStarted!

And also, I have a solution that makes use of TouchStarted while still being accurate.


Edit (mid-draft)

I saw that you posted while I was still writing this post, so I ran some quick tests and observed that GetMouseLocation returns the same Vector2 value as TouchTapInWorld does, which makes it seem like that would be a viable solution for what you described in your original post.


However, I then checked the documentation for GetMouseLocation:

If the location of the mouse pointer is offscreen or the players device does not have a mouse, the value returned will be undetermined instead of Vector2.

It made sense that it works in Roblox Studio (since there is a mouse connected to the computer), but I expected it to stop working on mobile devices based on the documentation… but after I went to test this in-game while using a phone, I found out that it works there, too, despite the documentation indicating that it shouldn’t work if the device does not have a mouse.

Even though that would be a viable solution right now, I’d be cautious about relying on that long-term for the following reasons:

  • To avoid issues if its current functionality is ever updated to better reflect what is documented about it.

  • The :GetMouseLocation() method was designed around a mouse (given its name), whereas TouchStarted was designed with multiple UserInputTypes in mind, seeing as how it returns the InputObject.

    • (And since you’re already using the TouchStarted event, it would make more sense to refer to the InputObject.Position that’s already provided from the event firing instead of making an additional call to the UserInputService for the mouse location).

Either way, regardless of which solution you decide to go with, I’ll still submit the rest of my post for anyone who is interested in learning more about the different events, methods, etc. involved with determining the position in the Workspace that the screen touch corresponds with:


Preface

According to the documentation for UserInputService.TouchTapInWorld, it doesn’t return a position in the world by default, as it returns a Vector2 (which would be the X and Y coordinates on the screen).

To determine the X, Y, and Z coordinates within the Workspace based on where the player pressed the screen, the code sample provided on that same Roblox Creator Documentation page provides an example of how you would find the Vector3 position within the world based on that Vector2 value:

-- Code sample from the Roblox Creator Documentation site
local UserInputService = game:GetService("UserInputService")

local camera = workspace.CurrentCamera
local LENGTH = 500

local function createPart(position, processedByUI)
	-- Do not create a part if the player clicked on a GUI/UI element
	if processedByUI then
		return
	end

	-- Get Vector3 world position from the Vector2 viewport position
	local unitRay = camera:ViewportPointToRay(position.X, position.Y)
	local ray = Ray.new(unitRay.Origin, unitRay.Direction * LENGTH)
	local hitPart, worldPosition = workspace:FindPartOnRay(ray)

	-- Create a new part at the world position if the player clicked on a part
	-- Do not create a new part if player clicks on empty skybox
	if hitPart then
		local part = Instance.new("Part")
		part.Parent = workspace
		part.Anchored = true
		part.Size = Vector3.new(1, 1, 1)
		part.Position = worldPosition
	end
end

UserInputService.TouchTapInWorld:Connect(createPart)

Experiments!

This is where I started to get a bit confused after experimenting with each of the events mentioned at the beginning of this post. To keep this post organized and easy-to-read, I’ve separated each experiment into its own details menu that you can open / close to read:

1. Conflicting documentation

My first bit of confusion was because of conflicting information based on what was mentioned in the documentation for TouchTapInWorld:

This event can be used to determine when a user taps the screen and does not tap a GUI element. If the user taps a GUI element, UserInputService.TouchTap will fire instead of TouchTapInWorld.

That made sense, but it didn’t seem like that was true after I ran a test:

  • I created a TextButton that would print Hi! whenever it was activated.
  • I included print statements in the functions activated by both TouchTapInWorld and TouchTap

However, all of the print statements ran whenever I pressed the TextButton, including the one from TouchTapInWorld which wasn’t supposed to fire if the documentation was correct.

Nonetheless, I continued experimenting…


2. The Vector2s returned by the events didn't make sense...

During my initial tests, I was creating a Frame within a default ScreenGui for each event; its position (X offset and Y offset) would be set to the Vector2 values returned from each event.

I observed that frames which were created with the Vector2 values from the TouchTap and TouchStarted events were positioned correctly (exactly where the screen was pressed) but the frame created for the TouchTapInWorld event was not positioned correctly on the Y axis.


To make a long story short, it turns out that when ScreenGui.IgnoreGuiInset is true (meaning that the ScreenInsets is ignored), the position of the InputObject that’s returned from the TouchStarted event will accurately represent the X and Y coordinates on the screen where the screen press occurred (and then vice versa when IgnoreGuiInset is set to false).


3. The Vector3s returned by the events also didn't make sense?

During my initial tests, I was also creating a Part in the Workspace; its position (X, Y, and Z coordinates) would be set to the Vector3 value returned from calling Camera:ViewportPointToRay() on the Vector2 values that each of the events returned (Note: I used the function from the code sample on that Roblox Creator Documentation page as the basis for creating the part in the Workspace).

Out of the 3 parts that were created for each event, I observed that the Part which was created based on the TouchTapInWorld event was positioned correctly (exactly where the screen was pressed) but the parts created for the TouchTap and TouchStarted events were not positioned correctly.


To make another long story short, it turns out that Camera:ViewportPointToRay() is accurate when using the Vector2 for TouchTapInWorld, whereas Camera:ScreenPointToRay() is accurate when using the X and Y positions for TouchTap and TouchStarted.

I probably had more realizations during the experimentation phase that I can’t remember, but those were the main things I had to figure out before it felt like I understood how everything works.


The A solution!

After much confusion all around the board, I came to a realization that there is an alternative method to Camera:ViewportPointToRay() which accounts for GUI Insets… Camera:ScreenPointToRay()!

To sum it all up:

  • ScreenPointToRay() will return accurate Vector3s if:

    • Using the Vector2 returned from TouchTap or TouchStarted.
    • (TouchTapInWorld will not be accurate here)
  • ViewportPointToRay() will return accurate Vector3s if:

    • Using the Vector2 returned from TouchTapInWorld
    • (TouchTap and TouchStarted will not be accurate here)

This means that as long as you use Camera:ScreenPointToRay() for acquiring the in-world position based off of a Vector2 value, you can use TouchStarted as a replacement for TouchTapInWorld so that no matter the duration of the screen press, it will accurately return the corresponding in-world Position based on the coordinates of the initial screen press.

(And if you don’t want it to do anything if the screen press happened to interact with a UI Object, make sure the second parameter of gameProcessedEvent returns false before continuing with the function).


Here’s a comprehensive test that lets you choose different configurations to demonstrate some of the behavior that I described about my experiments as well as the solution:

Example video

(And here’s a place file for ease of testing, which also includes an extra ScreenGui to change the configuration you’re using while in-game so it’s easier to visualize what happens when IgnoreGuiInset is enabled / disabled, as well as whether ViewportPointToRay() or ScreenPointToRay() is being used.

Example Place as of 4.20.2024 (Version 11).rbxl (64.5 KB)

-- Code for a LocalScript within the "StarterPlayerScripts"
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")

local player = Players.LocalPlayer
local PlayerGui = player:WaitForChild("PlayerGui")

local camera = workspace.CurrentCamera

---

local configurations = {
	
	default = {

		["TouchTapInWorld"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchTap"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchStarted"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ViewportPointToRay"
		}
	},
	
	---
	
	uiAccurate = {
		
		["TouchTapInWorld"] = {
			["IgnoreGuiInset"] = true,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchTap"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchStarted"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ViewportPointToRay"
		}
	},
	
	---
	
	uiInaccurate = {
		
		["TouchTapInWorld"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchTap"] = {
			["IgnoreGuiInset"] = true,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchStarted"] = {
			["IgnoreGuiInset"] = true,
			["RayType"] = "ViewportPointToRay"
		}
	},
	
	---
	
	everythingAccurate = {
		["TouchTapInWorld"] = {
			["IgnoreGuiInset"] = true,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchTap"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ScreenPointToRay"
		},
		
		["TouchStarted"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ScreenPointToRay"
		}
		
	},
	
	---
	
	everythingInaccurate = {
		["TouchTapInWorld"] = {
			["IgnoreGuiInset"] = false,
			["RayType"] = "ScreenPointToRay"
		},
		
		["TouchTap"] = {
			["IgnoreGuiInset"] = true,
			["RayType"] = "ViewportPointToRay"
		},
		
		["TouchStarted"] = {
			["IgnoreGuiInset"] = true,
			["RayType"] = "ViewportPointToRay"
		}
		
	},
	
	---
	
	custom = {
		["TouchTapInWorld"] = {
			["IgnoreGuiInset"] = 0, -- true or false
			["RayType"] = "0" -- "ScreenPointToRay" or "ViewportPointToRay"
		},
		
		["TouchTap"] = {
			["IgnoreGuiInset"] = 0, -- true or false
			["RayType"] = "0" -- "ScreenPointToRay" or "ViewportPointToRay"
		},
		
		["TouchStarted"] = {
			["IgnoreGuiInset"] = 0, -- true or false
			["RayType"] = "0" -- "ScreenPointToRay" or "ViewportPointToRay"
		}
		
	}

}

-- IMPORTANT!!!!


local configurationToUse = configurations["default"]

--[[
Change "default" with the config to use
for the duration of a playtest.
(Can be changed in-game via a ScreenGui
if using the example place file)

"default" is what would happen if IgnoreGuiInset
was never modified and camera:ViewportPointToRay()
was used for processing the Vector2 of every event.


"uiAccurate" is what would happen if
IgnoreGuiInset was updated to the value where
the Vector2 returned from each event has
its position accurately represented on the screen.
This does NOT affect the corresponding position
in the Workspace (it retains the default RayType).

"uiInaccurate" is what would happen if
IgnoreGuiInset was updated to the value where
the Vector2 returned from each event does NOT
have its position accurately represented on the screen.
This does NOT affect the corresponding position
in the Workspace (it retains the default RayType).


"everythingAccurate" is what would happen if
IgnoreGuiInset was updated to the value where
the Vector2 returned from each event has
its position accurately represented on the screen,
AND the position in the Workspace accurately
corresponds with where the screen was pressed.

"everythingInaccurate" is what would happen if
IgnoreGuiInset was updated to the value where
the Vector2 returned from each event does NOT
have its position accurately represented on the screen,
AND the position in the Workspace does NOT accurately
correspond with where the screen was pressed.


"custom" is for your own custom configuration to
experiment with a different combination of settings.

--]]


task.spawn(function()
	local ConfigurationScreenGui = PlayerGui:WaitForChild("ConfigurationScreenGui", 5)
	
--[[
If using the example place file, you can use
the pre-existing ScreenGui to swap between
the different configurations in-game
--]]
	if not ConfigurationScreenGui then return end
	local Frame = ConfigurationScreenGui:WaitForChild("Frame")
	
	local currentButton
	local function createButtonFunctionality(item)
		if item:IsA("TextButton") then
			
			item.Activated:Connect(function()
				
				if currentButton then
					currentButton.BackgroundColor3 = Color3.new(1, 1, 1)
				end
				
				currentButton = nil
				currentButton = item
				currentButton.BackgroundColor3 = Color3.new(0, 1, 0)
				
				configurationToUse = configurations[item.Text]
				warn("Now using Configuration: "..item.Text)
				
				local function updateVisualizationScreenGuis()
					
					for _, gui in PlayerGui:GetChildren() do
						
						local eventName = gui:GetAttribute("EventName")
						local currentConfigurationForEvent = configurationToUse[eventName]
						
						if eventName and currentConfigurationForEvent ~= nil then
							
							gui.IgnoreGuiInset = currentConfigurationForEvent.IgnoreGuiInset
						end
					end
				end 
				updateVisualizationScreenGuis()
				
			end)
		end
	end
	
	Frame.ChildAdded:Connect(createButtonFunctionality)
	for _, item in Frame:GetChildren() do
		createButtonFunctionality(item)
	end
end)

---

local visualizationProperties = {
	["TouchTapInWorld"] = {
		["Color"] = Color3.new(1, 0, 0), -- Red
		["PartSize"] = Vector3.new(1, 1, 1),
		["PartTransparency"] = 0.5,
		["UiSize"] = UDim2.new(0, 50, 0, 20),
		["UiTransparency"] = 0.25,
		["UiDisplayOrder"] = -1
	},
	
	["TouchTap"] = {
		["Color"] = Color3.new(0, 1, 0), -- Green
		["PartSize"] = Vector3.new(0.5, 5, 0.5),
		["PartTransparency"] = 0.5,
		["UiSize"] = UDim2.new(0, 30, 0, 20),
		["UiTransparency"] = 0,
		["UiDisplayOrder"] = 0
	},
	
	["TouchStarted"] = {
		["Color"] = Color3.new(0, 0, 1), -- Blue
		["PartSize"] = Vector3.new(3, 0.1, 3),
		["PartTransparency"] = 0.5,
		["UiSize"] = UDim2.new(0, 10, 0, 20),
		["UiTransparency"] = 0.5,
		["UiDisplayOrder"] = 1
	}	
}

---

for eventName, settingsTable in configurationToUse do
	local ScreenGui = Instance.new("ScreenGui")
	ScreenGui.Name = eventName.."UiVisualization"
	ScreenGui.IgnoreGuiInset = settingsTable.IgnoreGuiInset
	ScreenGui.DisplayOrder = visualizationProperties[eventName].UiDisplayOrder
	ScreenGui:SetAttribute("EventName", eventName)
	ScreenGui.Parent = PlayerGui
end




local LENGTH = 500
local function createPart(position, processedByUI, event)
	-- Do not create a part if the player clicked on a GUI/UI element
	if processedByUI then
		return
	end
	
	local selectedEventConfiguration = configurationToUse[event]
	
	-- Get Vector3 world position from the Vector2 position
	local unitRay
	local rayType = selectedEventConfiguration["RayType"]
	if rayType == "ViewportPointToRay" then
		unitRay = camera:ViewportPointToRay(position.X, position.Y)
	elseif rayType == "ScreenPointToRay" then
		unitRay = camera:ScreenPointToRay(position.X, position.Y)
	end
	
	local ray = Ray.new(unitRay.Origin, unitRay.Direction * LENGTH)
	local hitPart, worldPosition = workspace:FindPartOnRay(ray)
	
	-- Create a new part at the world position if the player clicked on a part
	-- Do not create a new part if player clicks on empty skybox
	if hitPart then
		local eventProperties = visualizationProperties[event]
		
		local part = Instance.new("Part")
		part.Color = eventProperties.Color
		part.Transparency = eventProperties.PartTransparency
		part.Size = eventProperties.PartSize
		part.Anchored = true
		part.CanCollide = false
		part.Parent = workspace
		
		part.Position = worldPosition
		print(event.." is using "..rayType.." with IgnoreGuiInset set to "..tostring(selectedEventConfiguration["IgnoreGuiInset"]))
	end
end

local function createUiVisualization(positions, event)
	local eventProperties = visualizationProperties[event]
	
	local newObjectTest = Instance.new("Frame")
	newObjectTest.AnchorPoint = Vector2.new(0.5, 0.5)
	
	---
	
	newObjectTest.BackgroundColor3 = eventProperties.Color
	newObjectTest.Size = eventProperties.UiSize
	newObjectTest.Transparency = eventProperties.UiTransparency
	
	---
	
	newObjectTest.Position = UDim2.new(0, positions.X, 0, positions.Y)
	
	newObjectTest.Parent = PlayerGui:WaitForChild(event.."UiVisualization")
	task.delay(3, function()
		newObjectTest:Destroy()
	end)
end

UserInputService.TouchTap:Connect(function(positions, gameProcessedEvent)
	--print("TouchTap", positions[1].X, positions[1].Y)
	createPart(positions[1], gameProcessedEvent, "TouchTap")
	createUiVisualization(positions[1], "TouchTap")
end)

UserInputService.TouchTapInWorld:Connect(function(positions, gameProcessedEvent)
	--print({"TouchTapInWorld", positions.X, positions.Y})
	createPart(positions, gameProcessedEvent, "TouchTapInWorld")
	createUiVisualization(positions, "TouchTapInWorld")
end)

UserInputService.TouchStarted:Connect(function(inputObject, gameProcessedEvent)
	local positions = inputObject.Position
	--print(positions)
	createPart(positions, gameProcessedEvent, "TouchStarted")
	createUiVisualization(positions, "TouchStarted")
end)

UserInputService.TouchEnded:Connect(function()
-- Note: The delay was added for the sole purpose of making the Output easier to read
	task.delay(0.2, function()
		warn("TouchEnded")
	end)
end)
2 Likes

First of all, I would like to deeply thank you for putting your time and effort into helping me solve this issue. Hopefully other devs can learn from it!

As of the solution I’m currently using, I doubt there’s any better one
I do agree with this as hopefully there’s a better long-term solution to come, but as of this moment, I have to cope with this.

Anyways, again, realy appreciate the effort and the video and the examples and everything :pray:, I made sure to read through all of it

Have a wonderful day!

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.