How to "PROPERLY" make mobile buttons

hihi

So alot of games ive played with mobile support had a poor setups with one of them being fire buttons, some games ive played don’t have ways to let users fire and look around which in some cases could hurt the gameplay even more, Today im going to solve how it should be done.

Scenario

let’s say you had a plan to make mobile support for your game as you speak to many, you feel like implementing that couldn’t go wrong with ContextActionService or a lovely way of using InputBegan with UserInputService, but once you made the game compatible with mobile, you realize that controls are very awful and any game like Arsenal, Energy Assault or Weaponry apparently solved this issue.

so all the listed issue, you would see this

  • You cannot look around while holding a button
  • Disabling Active solved but your script still runs no matter out of bound or released when tap pos is not in the button radius
  • InputBegan triggers without any proper checks

In most FPS Mobile game, given with buttons you’ll atleast find another joystick GUI made for fire n’ spray around the map but as roblox doesnt have that option to use (unless you’re a mastermind with Classic Thumbstick left on module, we dont wanna spend hours getting it right) we still have to rely on using the common input events for the function to trigger.

Brown line represent as input from finger (NOT THE TEXT)
Green as the button
Arrow as the direction you swipe to


as you swipe around while Active is false, your function still runs but the moment you release your finger out of there, you realize that it still runs no matter when you tap anywhere else until you press the button again

However if your tap pos was in the button of where you started pressing, it will trigger the same event and as you release it in there, the InputEnded triggers after it finally found the zone, This applies to ContextActionService aswell

and most notably, you cannot swipe around while button is holding which in some game, i find this a very nuisance as you want to spray around too but got to see this being an issue


Getting to solve this issue

There are 2 ways you can solve this issue, mine involves with binding Render step and update the GUI position to where your touch pos is, and other being the most common used technique in some Roblox FPS Games, one being Energy Assault

Hitbox < NOT MY IMPLEMENTATION

Using only 2 components, FireButton and a Frame

When in most FPS Games on roblox, if you don’t want an extra steps on making detection, developers will create one GUI elements that act as a zone to let the events or function run until you release your finger inside a gesture zone, This is a cheaper way as it does not require such things like RunService at all and rather the fastest method.

To do this, you have to create a zone on the right side, it should be half the screen unless you have someone that accidentally swipes to the left, Optionally you can add buttons on whatever you can and make sure to disable Active after buttons created

It should look like this (i had an extra buttons for something later)

Now we need to get into a scripting, we won’t use ContextActionService to do this and instead use it’s Inherited events from UserInputService which is already embedded into all GUI elements.

image

note that even if there’s InputBegan and InputEnded into the buttons, you still need to put some input type and state checking otherwise it’ll trigger when swipe pos touched other UI and the main one.


First top line will be the variables where you set it up to reuse instead of repeatedly indexing from other.

image

The firebutton input began/input ended as said earlier are Inherited events from the service, Inside contains an if statement checking if input type is a touch and state is beginning or not, if conditions are met, the function will run. oops looks like i put the unused arg on the parameters, don’t put GPE in

now your functions can literally be anything as long as it involves a loop but if you have a function that uses RenderStep, you can just use BindToRenderStep easily, That way you don’t have to unbind by making a blank var ref and assign it later to disconnect unless you love Heartbeat or Stepped and you don’t trust RenderStepped anymore.

local runSV = game:GetService("RunService")
local GUI = script.Parent

local hitbox = GUI.hitbox
local FireButton = GUI.FireButton
local disc = GUI.cool
local disc2 = GUI.woooo

local function test(dt)
	print("yahoo", dt)
end

local function input_run(inputOBJ, GPE) --you can just create function from events, it's up to you but doing on there would be easier
	if inputOBJ.UserInputType == Enum.UserInputType.Touch and inputOBJ.UserInputState == Enum.UserInputState.Begin then
		runSV:BindToRenderStep("test", Enum.RenderPriority.Camera.Value + 1, test)
	end
end

after InputBegan, we also had InputEnded, same checking as the first and this time with the UserInputState being End, we put them in 2 UI elements, one for the gesture zone and the main button

local runSV = game:GetService("RunService")
local GUI = script.Parent

local hitbox = GUI.hitbox
local FireButton = GUI.FireButton
local disc = GUI.cool
local disc2 = GUI.woooo

local function test(dt)
	print("yahoo: ", dt)
end

local function input_run(inputOBJ) --you can just create function from events, it's up to you but doing on there would be easier
	if inputOBJ.UserInputType == Enum.UserInputType.Touch and inputOBJ.UserInputState == Enum.UserInputState.Begin then
		runSV:BindToRenderStep("test", Enum.RenderPriority.Camera.Value + 1, test)
	end
end

local function input_end(inputOBJ) --you can just create function from events, it's up to you but doing on there would be easier
	if inputOBJ.UserInputType == Enum.UserInputType.Touch and inputOBJ.UserInputState == Enum.UserInputState.End then
		runSV:UnbindFromRenderStep("test")
	end
end

FireButton.InputBegan:Connect(input_run)
FireButton.InputEnded:Connect(input_end)
hitbox.InputEnded:Connect(input_end)

The reason we put them in 2 instead of Gesture zone only is because sometimes we still could end the swipe pos at the fire button and that could still fire, having to make it 2 would solve that problem at all!

lastly, i need to hide all buttons incase unexpected events trigger, by inserting them in a table called “hide_src” and insert all the UI we need to hide if FireButton is holding

And there you have it, a simple button now functions properly, a happy ending for mobile mates who cannot afford PC also to those seeing RunService on top, it’s to bind the test function and to demonstrate the function running.
https://www.youtube.com/watch?v=9Xk7S2YLgSE

local runSV = game:GetService("RunService")
local GUI = script.Parent

local hitbox = GUI.hitbox
local FireButton = GUI.FireButton
local disc = GUI.cool
local disc2 = GUI.woooo

local hide_src = { --directly insert them or use table.insert later
	button1 = disc,
	button2 = disc2
}

local function test(dt)
	print("yahoo: ", dt)
end

local function input_run(inputOBJ) --you can just create function from events, it's up to you but doing on there would be easier
	if inputOBJ.UserInputType == Enum.UserInputType.Touch and inputOBJ.UserInputState == Enum.UserInputState.Begin then
		runSV:BindToRenderStep("test", Enum.RenderPriority.Camera.Value + 1, test)
		for _, ui in pairs(hide_src) do
			ui.Visible = false
		end
	end
end

local function input_end(inputOBJ)
	if inputOBJ.UserInputType == Enum.UserInputType.Touch and inputOBJ.UserInputState == Enum.UserInputState.End then
		runSV:UnbindFromRenderStep("test")
		for _, ui in pairs(hide_src) do
			ui.Visible = true
		end
	end
end

FireButton.InputBegan:Connect(input_run)
FireButton.InputEnded:Connect(input_end)
hitbox.InputEnded:Connect(input_end)
Dynamic (NOT)

This method applies the same as the Hitbox method above except that buttons move, when you use this technique, it’s only usable when in that case you need it to follow a touch pos, this involves a RenderStep instead of moved events as they don’t create a memory leak

if you’re skipping from the Hitbox method here, i suggest going back to that and come here later as this is all just about modifying the code.

When i first implement this method, it involves using a mouse as the input and some touch started for events, having to separate the input by making a ref for the first input which was a convenient method to do but problem comes with that being the else function triggering unexpectedly and the button rapidly swapping it’s location during swipe, resulting in the event to run continuously even if i tapped the button to stop.

But after realizing input Object has this weird thing that shouldn’t belong and instead supposed to be Vector2, i had to use only Vector3 which somehow works and solved the issue with inputs stuttering

To do this, i have to make another BindToRenderStep function where it triggers, currently it’s not possible to make another function and connect it to the events as the last so this instead has to go inside the input began function since we have InputObject as the parameter, but we need the origin aka the original position to place after input has ended so that it returns back to where it was

image

and there you have it.,.,.,.,.,.,.,.,., a freaky ways to prevent inputbegan freezing but you may notice something here, why in the jolly GAWD is it not aligned in the center?? well the buttons here were calculated before and it’s size dependent, the easy fix is just subtracting or add to the Y or either X to adjust the pos, and if you want, resize the buttons a bit bigger, on input began, that way you can prevent the function freezing if there’s a lag spike :scream:

again a full heaps of code who just want to read instead of followin zzzzzzzzzzz

local runSV = game:GetService("RunService")
local GUI = script.Parent

local hitbox = GUI.hitbox
local FireButton = GUI.FireButton
local disc = GUI.cool
local disc2 = GUI.woooo

local origin_pos = FireButton.Position

local hide_src = { --directly insert them or use table.insert later
	button1 = disc,
	button2 = disc2
}

local function test(dt)
	print("yahoo: ", dt)
end



local function input_run(inputOBJ: InputObject) --type checking helps :3
	if inputOBJ.UserInputType == Enum.UserInputType.Touch and inputOBJ.UserInputState == Enum.UserInputState.Begin then
		runSV:BindToRenderStep("test", Enum.RenderPriority.Camera.Value + 1, test)
		runSV:BindToRenderStep("move_buttonz", Enum.RenderPriority.Camera.Value + 2, function(dt)
			FireButton.Position = UDim2.new(0, inputOBJ.Position.X, 0, inputOBJ.Position.Y + 50)
		end)
		for _, ui in pairs(hide_src) do
			ui.Visible = false
		end
	end
end

local function input_end(inputOBJ: InputObject)
	if inputOBJ.UserInputType == Enum.UserInputType.Touch and inputOBJ.UserInputState == Enum.UserInputState.End then
		runSV:UnbindFromRenderStep("test")
		runSV:UnbindFromRenderStep("move_buttonz")
		FireButton.Position = origin_pos
		for _, ui in pairs(hide_src) do
			ui.Visible = true
		end
	end
end

FireButton.InputBegan:Connect(input_run)
FireButton.InputEnded:Connect(input_end)
hitbox.InputEnded:Connect(input_end)

https://youtu.be/SVa2BhV9kfg

with these 2 methods, you can use this to make fire button function, something that involves using a camera to look at or related to holding and aiming at something else.


Now that’s how you can make a mobile buttons, if you have any questions or something i need to point out, lemme know what you’re specifying

10 Likes

It occurred to me that you could have used ContextActionService for control-based inputs with buttons and not UI inputs

Between button inputs and UI, they are two different things.

2 Likes

I would’ve used that but it still baffles me that function still runs even if released the touch position outside the button (until the tap is in the button) and cannot move the camera while holding, given that PC had a better controls in some FPS Games, that’s why i tend to avoid making buttons with ContextActionService

2 Likes

Ah, so it seems those buttons are not very reliable and lacks good development controls for them. That makes sense then I suppose.

1 Like

if you prefer having a tool to make it then I’ve made a module set here! though it’s still not considered as a release since it’s still in a testing phase.