Cross-platform controls - Easy method

I am tired of creating listeners for the User Input service over and over again, it makes a huge mess! Here is the best way I have figured out to handle all user inputs from PC, Mobile, and Console.

The goal of this method is to prevent the need for duplicate code that listens for specific user inputs, and centralize all the input listeners into one reliable script.

Step 1:
Make a list of all the controls for your game.

Example:
Attack
(Button A on Console)
(X Key on PC)
(Touchscreen button on Mobile)

Block
(Button B on Console)
(C Key on PC)
(Touchscreen button on Mobile)

Step 2:
Create a LocalScript under StarterCharacterScripts. Create a Bindable Event under the script for each control action you have listed.
image

Step 3:
Listen for events in the PlayerController script.
Example -

-- 📜 This script is the entry point for any/all controller inputs.
-- The events that are children of this script will be fired and other scripts can listen for controller inputs.
local PlayerController = script
-- 📜


-- ⚡ Events (client)
local evt_Attack = script:WaitForChild("Attack")
local evt_Block = script:WaitForChild("Block")
-- ⚡


-- ⚙️ Services
local UserInputService = game:GetService("UserInputService")
-- ⚙️

-- This is a GUI that contains buttons to be used for mobile controls
local MobileActionGui = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("MobileActionGui")
-- Enable this GUI if we are on Mobile
MobileActionGui.Enabled = UserInputService.TouchEnabled

-- 📱 Mobile Controls
local MobileAttackButton = MobileActionGui:WaitForChild("AttackButton")
local MobileBlockButton = MobileActionGui:WaitForChild("BlockButton")

-- When the button is activated, call the attack/block event
MobileAttackButton.Activated:Connect(function()
	evt_Attack:Fire()
end)
MobileBlockButton.Activated:Connect(function()
	evt_Block:Fire()
end)
-- 📱
	

-- ⌨️ Keyboard Controls
local function onKeyboardInputBegan(input)
	if input.KeyCode == Enum.KeyCode.X then
		evt_Attack:Fire()
	elseif input.KeyCode == Enum.KeyCode.C then
		evt_Block:Fire()
	end
end
UserInputService.InputBegan:Connect(onKeyboardInputBegan)
-- ⌨️


-- 🎮 Console Controls
local function onControllerInputBegan(input)
	
	if input.KeyCode == Enum.KeyCode.ButtonR2 then
		evt_Attack:Fire()
	elseif input.KeyCode == Enum.KeyCode.ButtonL2 then
		evt_Block:Fire()
	end
	
end
UserInputService.InputBegan:Connect(onControllerInputBegan)
-- 🎮 

Step 4:
In your other (client side) scripts where you normally listen for controller inputs, instead listen for the Bindable Events being fired.
Example usage -

-- Event listeners
local character = Players.LocalPlayer.Character or Players.LocalPlayer.CharacterAdded:Wait()
local PlayerController = character:WaitForChild("PlayerController")
local evt_Attack = PlayerController:WaitForChild("Attack")
local evt_Block = PlayerController:WaitForChild("Block")

evt_Attack.Event:Connect(function()
	print("Attack")
end)

evt_Block.Event:Connect(function()
	print("Block")
end)
5 Likes

TL;DR: If you are writing different code for every platform you are doing something wrong. How the code works should be almost identical. So create functions/events that you will easily able to just bind to different types of inputs.

Realistically, you can also just design two separate halves to the player character.

  1. A Player “Agent”/“Actor” that manages input device listening, such as keyboard, mouse or controller.
  2. A universal character controller that can take any set of Vector2/Vector3 values for direction input, as well as events that can be called for actions (like Jump or Sprint).

This will also give you a universal character controller that custom AI “Agents” can interact with. This system is how we’ve been making multi-platform controls for Teensy Tiny Tankery, and it’s well practiced throughout the Games Industry.

1 Like