Example Game
Play the example game here This article features abridged samples of the actual code in the example game, but you can view the full code here.
Behind The Scenes
The concept for this game was to implement three simple mechanics with Axis:
- Toggleable crouch button for mobile
- A hold-to-jump mechanic for all platforms
- Custom camera (to show how to use mouse input,
hold()
, and throw console support in there)
THe above order is also the order in which the features will be explained in to roughly go from least to most complex.
Toggleable Crouch
Alright so here is the actual keymap for the crouching feature. We include all the possible keyboard keys that people might use, as well as the right thumbstick for console. Mobile will be implemented separately.
--inputMap.luau
crouch = input {
Enum.KeyCode.C,
Enum.KeyCode.LeftControl,
Enum.KeyCode.RightControl,
Enum.KeyCode.ButtonR3, -- pressing on the right thumbstick will crouch!
}
Now we're going to write our crouch system in a way that it will work for all devices without needing to handle any input logic specific to any device:
--crouch.luau
local crouching = false
local function crouch(dt)
-- other if statements
if not inputMap.crouch:pressed() then
return
end
crouching = not crouching
-- humanoid effects
end
All we have to do is wait for the pressed()
method to signal that the input was just toggled. You could also use released()
to toggle on release but it might feel delayed from a player's perspective.
--touchControls.luau
local crouching = false
crouchButton.MouseButton1Down:Connect(function()
inputMap.crouch:move(1)
crouching = not crouching
if not crouching then
-- UI effects
return
end
-- UI effects
end)
Since we know that we just want the crouch to be toggleable, we will implement it with move()
to fire the pressed()
method, but you might not always want to make this assumption. If you wanted to make it hold-to-crouch, or add a setting for whether it is toggleable, you would want to implement the button using hold()
as seen in the next section.
Hold-to-Jump
Since we just saw the basic implementation of the crouch button, let's jump into the implementation of the hold-to-jump button. Here we are using a different method of handling input on the touch button to allow for mobile users to drag and move the camera around while holding the button.
local releaseJump, touch = nil, nil
jumpButton.InputBegan:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 and
input.UserInputType ~= Enum.UserInputType.Touch then
return
end
if input.UserInputState ~= Enum.UserInputState.Begin then
return
end
touch = input
releaseJump = inputMap.jump:hold(1) -- imitates pressing an input axis like the `Space` key
-- UI effects
end)
UserInputService.InputEnded:Connect(function(input)
if not releaseJump or input ~= touch or input.UserInputState ~= Enum.UserInputState.End then
return
end
if input.UserInputType ~= Enum.UserInputType.MouseButton1 and
input.UserInputType ~= Enum.UserInputType.Touch then
return
end
releaseJump() -- releases the hold, imitating letting go of the `Space` key
releaseJump = nil
-- UI effects
end)
This method of input only successfully allows for dragging when the button's Active
property is set to false and Interactable
is set to true. If Active
is on, touching the button will be considered a 'processed' input and ignored by most input systems. If Interactable
is off, events like InputBegan
will not be fired. Anyway, for reference, here is what the actual input axis looks like for other devices:
jump = Axis.input {
Enum.KeyCode.Space,
Enum.KeyCode.ButtonA
}
Now for the system to handle the jumping feature. This will be a bit more complicated than the crouch button, since the goal is to let the user hold the jump button for longer to jump higher. We will start by creating a variable called jumpHoldTime
that will keep track of how long the player has held the jump input axis for.
local jumpHoldTime = 0
Then in our jump system the first thing we want to do is check if the player should even be able to jump.
-- if the player is swimming or falling or climbing, reset jump power
if humanoid:GetState() ~= Enum.HumanoidStateType.Running then
jumpHoldTime = 0
return -- end system early
end
Now since we know that the player is in a valid state to jump (they are standing on the ground), we can check if the jump button was released.
if inputMap.jump:released() then -- when the jump axis changed and is not held any more
humanoid.JumpPower = getJumpPower() -- our custom equation using jumpHoldTime
humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
jumpHoldTime = 0
return -- end early again
end
Then we will finish the system off with the following code for incrementing our timer jumpHoldTime
(and setting the jump power to 0 to stop the player from jumping).
humanoid.JumpPower = 0
jumpHoldTime = inputMap.jump:pressing() and (jumpHoldTime or 0) + deltaTime
Camera Movement
Note: This camera system uses the Nevermore Spring to smooth out the camera movement.
In your game, there is a good chance that your systems that handle camera movement or unique camera modes might not want to all handle input manually, this is where Axis can help by using only two input axes.
local inputMap = {
-- ...
drag = input {
Enum.UserInputType.MouseMovement,
Enum.KeyCode.Thumbstick2,
[Enum.KeyCode.Left] = vector.create(-2, 0),
[Enum.KeyCode.Right] = vector.create(2, 0),
},
dragHold = input {
Enum.UserInputType.MouseButton2
},
}
Above you'll see we have the main drag
input and a secondary dragHold
input. The drag
input will be used for general camera movement, while the dragHold
input will be used to hold the mouse in place and allow the mouse to be "unlocked" the rest of the time.
Camera Zoom
Here is where the weighting of input axes comes in handy. Using another unnamed input library, we were handling sensitivities for the zoom something like this:
--camera.luau
local ZOOM_SPEED = {
Touch = 5,
Gamepad = 2,
Desktop = 10,
}
local function clampZoom(zoom: number): number
return math.clamp(offsetVector.Position.Z - zoom
* ZOOM_SPEED[axis.device(UserInputService:GetLastInputType())],
MIN_ZOOM, MAX_ZOOM
)
end
We can start to reduce this code by putting gamepad and desktop sensitivities in the same input axis:
--inputMap.luau
zoom = input {
[Enum.KeyCode.DPadDown] = -2,
[Enum.KeyCode.DPadUp] = 2,
[Enum.KeyCode.I] = 2,
[Enum.KeyCode.O] = -2,
[Enum.UserInputType.MouseWheel] = 10,
}
Now we can use a constant in the mobile input system to add that extra sensitivity:
--mobileInput.luau
local PINCH_MULTI = 50
UserInputService.TouchPinch:Connect(function(_, scale, _, _, _)
-- other logic
inputMap.zoom:move((scale - previousScale) * PINCH_MULTI)
-- other logic
end)
Then we can just completely forget about handling different devices in the camera system!
local function clampZoom(zoom: number): number
return math.clamp(offsetVector.Position.Z - zoom, MIN_ZOOM, MAX_ZOOM)
end