After many, many, many HOURS of experimenting in Roblox Studio and looking through the Roblox Creator Documentation, I’ve come up with a solution that works for multiple input types, including:
-
Keyboard
-
Touch-screen (mobile, laptops with touch-screen enabled, etc.)
-
Controller
-
With the possibility to be compatible with more input types. Please let me know if I’ve missed any other ones that I should add. For example, I am not familiar with the KeyCode
that’s used to make the Character jump when using VR.
I’ll explain how everything works, along with brief details about some of the solutions I tried that didn’t end up working, but first, here’s the completed code (that would ideally be placed in a LocalScript
in the StarterPlayerScripts
container):
Complete LocalScript code
-- (LocalScript Code) Place this into the StarterPlayerScripts container!
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
---
local jumpKeybinds = {
[1] = Enum.KeyCode.Space, -- Keyboard
[2] = Enum.KeyCode.ButtonA -- Controller
--[[
Not necessary to define anything for touch-screen devices!
It works for touch-screen devices by checking the UserInputType later on.
--]]
}
---
local connection
local isJumping
local allowedToJump
local function OnCharacterLoad(Character)
allowedToJump = true
isJumping = nil
local Humanoid = Character:WaitForChild("Humanoid")
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
connection = Humanoid.StateChanged:Connect(function(oldState, newState)
if newState == Enum.HumanoidStateType.Jumping then
if isJumping ~= true then
isJumping = true
allowedToJump = false
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
end
elseif newState == Enum.HumanoidStateType.Landed then
isJumping = false
local lastInputType = UserInputService:GetLastInputType()
if lastInputType == Enum.UserInputType.Touch then
allowedToJump = true
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
end
end
end)
end
if player.Character then
OnCharacterLoad(player.Character)
end
player.CharacterAdded:Connect(OnCharacterLoad)
player.CharacterRemoving:Connect(function()
connection:Disconnect()
connection = nil
end)
UserInputService.InputEnded:Connect(function(inputObject, gameProcessedEvent)
if allowedToJump == false and not gameProcessedEvent then
if table.find(jumpKeybinds, inputObject.KeyCode) then
allowedToJump = true
local Character = player.Character or player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
if Character and Humanoid then
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
end
end
end
end)
Explaining how the LocalScript works
I tried a variety of approaches before managing to figure out a viable solution, such as:
-
Listening for UserInputService.JumpRequest
to fire, then using Humanoid:SetStateEnabled()
to enable or disable the ability to jump depending on the Humanoid.FloorMaterial
. However, on its own, this was not able to detect whether or not the player had continuously held down the jump button since the previous request. As a result, it seemed that other events of the UserInputService
may be necessary to make this work.
-
Using a combination of the previous strategy, I tried to use the InputBegan
and InputEnded
events of both the UserInputService
and ImageButton
in order to detect if a player was holding down one of the jump keys on keyboard / controller, or holding down the on-screen button for a touch-screen. This worked decently well with keyboard (and presumably controller), however, I kept encountering issues with the on-screen jump button. This might have worked as a viable solution, but I think that the strategies I ended up using in the completed code above turned out to be much more intuitive and reliable than this would have been.
With that context out of the way, now I’ll go through each section of the script!
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
First, some of the initially important services / objects are defined right away, for ease of access. The UserInputService
will be used at the end of the script to check when the player lets go of the jump key for keyboard / controller.
---
local jumpKeybinds = {
[1] = Enum.KeyCode.Space, -- Keyboard
[2] = Enum.KeyCode.ButtonA -- Controller
}
--[[ NOTE: The next few lines of code were originally included in the
final product, but before I posted this, I realized that it is not necessary.
I've kept it here to provide more context for what I thought was necessary.
--]]
local jumpButton -- Touch-screens (mobile, laptops with touchscreen enabled, etc.)
task.spawn(function()
local touchGui = PlayerGui:WaitForChild("TouchGui", 5)
if touchGui then
local touchControlFrame = touchGui:WaitForChild("TouchControlFrame")
jumpButton = touchControlFrame:WaitForChild("JumpButton")
end
end)
---
This section of code is dedicated to defining how each input type jumps. The jumpKeybinds
table will be referenced in the function activated by the InputEnded
event at the end of the script. The table includes the default keybinds for those input types, but if you’ve changed any to a custom keybind, then you can update the table to match the new one(s).
Click here for an explanation for the "jumpButton" variable, the few lines of code right below it, and a section of one of the upcoming functions that was also revised. This was removed from the completed version of the LocalScript since it wasn't necessary.
The “jumpButton” variable is where a reference to the on-screen jump button for touch-screen devices is supposed to be stored (and, side note, I learned how to reference the on-screen jump button based on the Cross-Platform Design article on the Roblox Creator Documentation site). Originally, I had used this to detect when a player pressed or let go of the button, however, it’s primarily there now (right before revising this) to make sure that everything would continue working if a laptop user, for example, turns off touch-screen (since the “jumpButton” would disappear, so the code would adjust accordingly).
I created a separate thread with task.spawn()
so that the rest of the code would continue to run while it waits for the “JumpButton” to appear. Otherwise, anyone who doesn’t have touch-screen enabled for their device would have to wait for the duration of the timeout period (which is 5 seconds in this case) before the rest of the code in the LocalScript
would run.
And it was at that moment after I finished writing that last paragraph that I realized this didn’t account for the probably extremely rare instances where a player would turn on the touch-screen capabilities mid-game… so I edited the script to accommodate that… and then I realized that I didn’t need to check for the button to exist at all!!! It turns out that I just needed to make sure that the last input type used was Enum.UserInputType.Touch
because the on-screen jump button would only appear if that input type had been processed…
Anyway, before I removed it entirely, here’s what the section of one of the upcoming functions looked like (in specific, the conditional statement that checks if jumpButton ~= nil
:
connection = Humanoid.StateChanged:Connect(function(oldState, newState)
if newState == Enum.HumanoidStateType.Jumping then
if isJumping ~= true then
isJumping = true
allowedToJump = false
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
end
elseif newState == Enum.HumanoidStateType.Landed then
isJumping = false
local lastInputType = UserInputService:GetLastInputType()
if jumpButton ~= nil and lastInputType == Enum.UserInputType.Touch then
allowedToJump = true
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
end
end
end)
local connection
local isJumping
local allowedToJump
These variables will be used in different functions within the script for the following purposes:
-
The “connection” is where the event connection for the Humanoid.StateChanged
event will be stored. This will be disconnected every time the player’s Character needs to respawn and replaced with a brand new one (to be able to reference the Character’s brand new Humanoid
). This ensures that the functionality of the script (requiring players to manually press the jump button again) persists after respawning. Otherwise, players could just respawn once and then they would be able to hold down the jump key, which would make this entire script pointless!
-
The “isJumping” variable will be equal to true
or false
, depending on whether or not the HumanoidStateType
of the Character’s Humanoid
changes to Jumping
or Landed
. This makes it easier to keep track of when a player is in the air.
-
The “allowedToJump” variable will be equal to true
or false
, depending on whether or not the player has already initiated a jump. This variable is SUPER IMPORTANT for keyboard / controller input types because it’s one of the things that helps make sure the player’s Character is not allowed to jump until they let go of the jump key and press it again.
local function OnCharacterLoad(Character)
allowedToJump = true
isJumping = nil
local Humanoid = Character:WaitForChild("Humanoid")
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
connection = Humanoid.StateChanged:Connect(function(oldState, newState)
if newState == Enum.HumanoidStateType.Jumping then
if isJumping ~= true then
isJumping = true
allowedToJump = false
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
end
elseif newState == Enum.HumanoidStateType.Landed then
isJumping = false
local lastInputType = UserInputService:GetLastInputType()
if lastInputType == Enum.UserInputType.Touch then
allowedToJump = true
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
end
end
end)
end
if player.Character then
OnCharacterLoad(player.Character)
end
player.CharacterAdded:Connect(OnCharacterLoad)
player.CharacterRemoving:Connect(function()
connection:Disconnect()
connection = nil
end)
This next section contains the majority of the functionality. Because it can be quite overwhelming, I’ll also go through this a couple of lines of code at a time.
if player.Character then
OnCharacterLoad(player.Character)
end
player.CharacterAdded:Connect(OnCharacterLoad)
player.CharacterRemoving:Connect(function()
connection:Disconnect()
connection = nil
end)
In order to activate the OnCharacterLoad
function in the first place, we check if the player’s Character
already exists. If it does, we call the function and send the player’s Character through to it. Otherwise, we use the CharacterAdded
event of the Player
object to listen for every time the Character respawns, and then activate the function when that happens.
The CharacterRemoving
event after it is used to disconnect the connection that was created from the Humanoid.StateChanged
event for the Character’s previous Humanoid object. I’ll explain more about that event once we get to it in the function.
local function OnCharacterLoad(Character)
allowedToJump = true
isJumping = nil
local Humanoid = Character:WaitForChild("Humanoid")
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
The function begins by updating the allowedToJump
variable and the isJumping
variable. This essentially “resets” it to its default values, making sure that the new Character model will be able to jump. That’s especially important in the case that the previous Character model was mid-air when it needed to respawn, because that means that those variables were never set back to the values that would indicate that the player is standing on the ground / an object.
Afterwards, we look for the new Character’s Humanoid
object and immediately set the “Jumping” HumanoidStateType
to true
, which allows the player to jump. For reference, when it’s set to false
, players using a keyboard or controller can press the jump keybinds, but nothing will happen. For touch-screen devices, the on-screen jump button doesn’t appear when it’s set to false
, which is why it’s really important to make sure it is set to true
from the moment that they’re supposed to be allowed to jump again.
connection = Humanoid.StateChanged:Connect(function(oldState, newState)
This stores the event connection created by Humanoid.StateChanged
so that it can be disconnected at a later point when the player’s Character needs to respawn. newState
refers to the current HumanoidStateType
, and oldState
is the previous one.
Ultimately, I settled on using Humanoid.StateChanged
because it seems to be pretty consistent and it is also able to detect when a player jumped and then when they landed. This makes the process of enabling / disabling the ability to jump so much more seamless because you don’t need to guess the timings of how long the player is in the air for and when they land.
if newState == Enum.HumanoidStateType.Jumping then
if isJumping ~= true then
isJumping = true
allowedToJump = false
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
end
-- The function continues...
This first conditional statement checks if the Character is currently Jumping. If so, we’ll make sure the isJumping
variable isn’t already set to true
(because otherwise, we wouldn’t need to do anything here). Then, we update that to true
and then make the allowedToJump
variable equal to false
as a way of letting the rest of the script know that the player is already jumping.
Lastly, we set the “Jumping” HumanoidStateType
to false
, which makes the on-screen jump button disappear for touch-screen users, and also prevents any other buttons / keybinds / etc. from allowing the Character to jump.
elseif newState == Enum.HumanoidStateType.Landed then
isJumping = false
local lastInputType = UserInputService:GetLastInputType()
if lastInputType == Enum.UserInputType.Touch then
allowedToJump = true
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
end
end
The second conditional statement checks if the Character has just landed on the ground / on an object that they can stand on. If so, we’ll update the isJumping
variable to false
since we know the player is no longer in the air.
Then, we’ll retrieve the most recent UserInputType
that was used (such as Keyboard, Gamepad / controller, Touch, etc.) using UserInputService:GetLastInputType()
. We’re checking if the last input type was Enum.UserInputType.Touch
in order to determine if the jump button should be enabled again. As mentioned earlier, when the “Jumping” HumanoidStateType
is false, the jump button does not appear on-screen for touch-screen users. As a result, we can immediately turn it back on to make sure that they can jump again.
Based on my testing using the Device Emulation feature in Roblox Studio, players shouldn’t be able to hold down the jump button for consecutive jumps (although they might be able to get close to doing so if they’re rocking their finger back and forth to immediately press the button when it appears again).
And to clarify why we’re not using this same idea of checking for the most recent input type for keyboard / controller / etc., that’s because this would not consider whether or not the player continued holding down the physical button on their device since the last time they jumped. It’s different for touch-screens because the digital button becomes completely invisible when the script disables the “Jumping” state for the Humanoid.
UserInputService.InputEnded:Connect(function(inputObject, gameProcessedEvent)
if allowedToJump == false and not gameProcessedEvent then
if table.find(jumpKeybinds, inputObject.KeyCode) then
allowedToJump = true
local Character = player.Character or player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
if Character and Humanoid then
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
end
end
end
end)
At the very end of the script, we have a function that is run whenever the UserInputService.InputEnded
event fires. This means that whenever a player releases a key on a keyboard, a button on a controller, etc., the function will run.
UserInputService.InputEnded:Connect(function(inputObject, gameProcessedEvent)
if allowedToJump == false and not gameProcessedEvent then
The inputObject
refers to the specific input that the user made. In this case, we’ll be referencing the InputObject.KeyCode
property to figure out which key / button they pressed in order to compare it with the KeyCodes defined in the jumpKeybinds
table at the start of the script.
gameProcessedEvent
is a true
or false
value that basically says whether or not the input had anything to do with User Interface objects, such as TextBoxes
or ImageButtons
. We’re making sure it’s equal to false
because if the player was typing in the chat, for example, they would probably be pressing the space bar a lot to add spaces between their words (and the space bar happens to be the default jump key for keyboards!) In cases like that, we don’t need to do anything because the player is not trying to make their Character jump.
Along with that, we also check if the allowedToJump
variable is equal to false
, which is manually updated within the OnCharacterLoad
function to false
after the player jumps. When it’s false, we know that the player is either in the air, or, they still have to let go of the jump key before they are allowed to jump again.
if table.find(jumpKeybinds, inputObject.KeyCode) then
allowedToJump = true
local Character = player.Character or player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
if Character and Humanoid then
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
end
end
Afterwards, using table.find()
, we’ll look through the jumpKeybinds
table from the top of the script for the specific InputObject.KeyCode
that the player released in order to fire the InputEnded
event.
If it matches any of the KeyCodes, then we know that the player released the jump key / button. From there, we begin the process of allowing their Character to jump again. Well, first, we update the allowedToJump
variable back to true so the rest of the script knows that the player is allowed to make their Character jump again. After that, we reference their current Character
model and Humanoid
, make sure both exist, and then use Humanoid:SetStateEnabled
to allow the player’s Character to jump again.
And that’s everything! I did not realize how many hours it would take to create this and then to write out the entire explanation (might have taken more time to write the explanation, if I’m being honest, haha). It was a lot of fun to be able to create this, especially since it didn’t seem like an answer to this specific question for multiple input types had been thoroughly addressed anywhere on the Roblox Developer Forum yet.
If you have any questions even after all of this, feel free to let me know! Hope that this will be very useful for you and everyone else who may be looking for a solution to this question