Momentum Controls 2 - Now with gamepad and touch support!

Works just like the original, whilst also being more convenient to adjust during gameplay now that the rate is an attribute as shown in the new demo video

Also like its predecessor, it’s important that the script is left with the name “ControlScript” and is inside of StarterPlayerScripts so that it can work without the default control script interfering!

I also made an attribute for gamepad deadzone for games that want to be able to adjust it from a settings menu

It now uses type-checking too!

You can find it here :slight_smile: (The original is still available in the marketplace if you prefer it)

February 16th 2024 Update

Source code
--!strict
local ContextActionService = game:GetService("ContextActionService"):: ContextActionService
local GuiService = game:GetService("GuiService"):: GuiService
local Players = game:GetService("Players"):: Players
local RunService = game:GetService("RunService"):: RunService
local UserInputService = game:GetService("UserInputService"):: UserInputService

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

local touchGui = script:WaitForChild("MC2_TouchGui"):: ScreenGui
local baseFrame = touchGui:WaitForChild("Base"):: Frame
local touchThumbstick = baseFrame:WaitForChild("Thumbstick"):: ImageLabel
local jumpButton = touchGui:WaitForChild("JumpButton"):: ImageButton

local inputTypeFilter = Instance.new("StringValue")

local humanoid

local rate -- In seconds. Large values (for example, using 2) result in a stronger and more noticeable effect
-- The rate is now an attribute of this script, which makes it easier to modify it during gameplay

local controllerDeadzone -- Is also an attribute so that you can give players the option to change it if you want to

local up1, down1, left1, right1 = 0, 0, 0, 0
local up2, down2, left2, right2 = 0, 0, 0, 0
local jump = false

local touchConnection: RBXScriptConnection?
local jumpButtonConnection: RBXScriptConnection?

local abs = math.abs
local clamp = math.clamp
local max = math.max
local min = math.min
local newVector3 = Vector3.new

local function onCharacterAdded(character: Model)
	humanoid = character:WaitForChild("Humanoid"):: Humanoid
end

local function onRateChanged()
	local newRate = script:GetAttribute("Rate"):: number

	if newRate == 0 then
		script:SetAttribute("Rate", 0.0001) -- Otherwise a divide by zero will happen
	else
		rate = newRate
	end
end

local function onDeadzoneChanged()
	controllerDeadzone = clamp(script:GetAttribute("Controller_Deadzone"), 0, 0.99)
end

local function mc2_Up(_, inputState: Enum.UserInputState)
	up1 = if inputState == Enum.UserInputState.Begin then 1 else -1
end

local function mc2_Down(_, inputState: Enum.UserInputState)
	down1 = if inputState == Enum.UserInputState.Begin then 1 else -1
end

local function mc2_Left(_, inputState: Enum.UserInputState)
	left1 = if inputState == Enum.UserInputState.Begin then 1 else -1
end

local function mc2_Right(_, inputState: Enum.UserInputState)
	right1 = if inputState == Enum.UserInputState.Begin then 1 else -1
end

local function deadzone(x: number): number
	if abs(x) < controllerDeadzone then return 0 end
	if x < 0 then return (x + controllerDeadzone) / (1 - controllerDeadzone) end
	return (x - controllerDeadzone) / (1 - controllerDeadzone)
end

local function mc2_Thumbstick1(_, _, inputObject: InputObject)
	up1 = deadzone(-inputObject.Position.Y)
	left1 = deadzone(inputObject.Position.X)
end

local function mc2_Jump(_, inputState: Enum.UserInputState)
	jump = (inputState == Enum.UserInputState.Begin)
end

local function mc2_Keyboard(deltaTime: number)
	up2 = clamp(up2 + (deltaTime / rate * up1), 0, 1)
	down2 = clamp(down2 + (deltaTime / rate * down1), 0, 1)
	left2 = clamp(left2 + (deltaTime / rate * left1), 0, 1)
	right2 = clamp(right2 + (deltaTime / rate * right1), 0, 0.99)

	humanoid.Jump = jump

	humanoid:Move(newVector3(right2 - left2, 0, down2 - up2), true)
end

local function mc2_Gamepad(deltaTime: number)
	if up1 == 0 then
		if up2 < 0 then
			up2 = min(up2 + (deltaTime / rate), 0)
		elseif up2 > 0 then
			up2 = max(up2 - (deltaTime / rate), 0)
		end
	else
		up2 = clamp(up2 + (deltaTime / rate * up1), -1, 1)
	end

	if left1 == 0 then
		if left2 < 0 then
			left2 = min(left2 + (deltaTime / rate), 0)
		elseif left2 > 0 then
			left2 = max(left2 - (deltaTime / rate), 0)
		end
	else
		left2 = clamp(left2 + (deltaTime / rate * left1), -1, 1)
	end

	humanoid.Jump = jump

	humanoid:Move(newVector3(left2, 0, up2), true)
end

local function unbindAllActions()
	if touchConnection then
		touchConnection:Disconnect()
		touchConnection = nil

		touchGui.Parent = script
	end

	if jumpButtonConnection then
		jumpButtonConnection:Disconnect()
		jumpButtonConnection = nil
	end

	ContextActionService:UnbindAction("MC2_Up")
	ContextActionService:UnbindAction("MC2_Down")
	ContextActionService:UnbindAction("MC2_Left")
	ContextActionService:UnbindAction("MC2_Right")
	ContextActionService:UnbindAction("MC2_Thumbstick1")
	ContextActionService:UnbindAction("MC2_Jump")

	RunService:UnbindFromRenderStep("MC2_Controls")
end

local function bindKeyboardActions()
	ContextActionService:BindAction("MC2_Up", mc2_Up, false, Enum.KeyCode.W, Enum.KeyCode.Up)
	ContextActionService:BindAction("MC2_Down", mc2_Down, false, Enum.KeyCode.S, Enum.KeyCode.Down)
	ContextActionService:BindAction("MC2_Left", mc2_Left, false, Enum.KeyCode.A)
	ContextActionService:BindAction("MC2_Right", mc2_Right, false, Enum.KeyCode.D)
	ContextActionService:BindAction("MC2_Jump", mc2_Jump, false, Enum.KeyCode.Space)

	RunService:BindToRenderStep("MC2_Controls", Enum.RenderPriority.Input.Value, mc2_Keyboard)
end

local function bindGamepadActions()
	ContextActionService:BindAction("MC2_Thumbstick1", mc2_Thumbstick1, false, Enum.KeyCode.Thumbstick1)
	ContextActionService:BindAction("MC2_Jump", mc2_Jump, false, Enum.KeyCode.ButtonA)

	RunService:BindToRenderStep("MC2_Controls", Enum.RenderPriority.Input.Value, mc2_Gamepad)
end

local function bindTouchActions()
	touchGui.Parent = playerGui

	local switch = false

	local fromScaleUDim2 = UDim2.fromScale

	local function clampedilerp(x: number, min: number, max: number): number
		if x < min then return 0 end
		if x > max then return 1 end
		return (x - min) / (max - min)
	end

	local function lerp(x: number, min: number, max: number): number
		return (max - min) * x + min
	end

	jumpButtonConnection = jumpButton:GetPropertyChangedSignal("GuiState"):Connect(function()
		if jumpButton.GuiState == Enum.GuiState.Press then
			jump = true
		else
			jump = false
		end
	end)

	touchConnection = touchThumbstick.InputBegan:Connect(function(input)
		if switch then return end

		if input.UserInputType == Enum.UserInputType.Touch then
			switch = true

			UserInputService.TouchEnded:Once(function()
				RunService:UnbindFromRenderStep("MC2_Controls")

				RunService:BindToRenderStep("MC2_Controls", Enum.RenderPriority.Input.Value, function(deltaTime: number)
					if up2 < 0 then
						up2 = min(up2 + (deltaTime / rate), 0)
					elseif up2 > 0 then
						up2 = max(up2 - (deltaTime / rate), 0)
					end

					if left2 < 0 then
						left2 = min(left2 + (deltaTime / rate), 0)
					elseif left2 > 0 then
						left2 = max(left2 - (deltaTime / rate), 0)
					end

					humanoid.Jump = jump

					humanoid:Move(newVector3(left2, 0, up2), true)
				end)

				touchThumbstick.Position = fromScaleUDim2(0.5, 0.5)

				switch = false
			end)

			RunService:UnbindFromRenderStep("MC2_Controls")

			RunService:BindToRenderStep("MC2_Controls", Enum.RenderPriority.Input.Value, function(deltaTime: number)
				local mouseLocation = UserInputService:GetMouseLocation()

				left1 = clampedilerp(
					mouseLocation.X,
					baseFrame.AbsolutePosition.X,
					baseFrame.AbsolutePosition.X + baseFrame.AbsoluteSize.X)
				up1 = clampedilerp(
					mouseLocation.Y - GuiService:GetGuiInset().Y,
					baseFrame.AbsolutePosition.Y,
					baseFrame.AbsolutePosition.Y + baseFrame.AbsoluteSize.Y)

				touchThumbstick.Position = fromScaleUDim2(left1, up1)

				up1, left1 = lerp(up1, -1, 1), lerp(left1, -1, 1)

				if up1 < up2 then
					up2 = max(up2 - (deltaTime / rate), up1)
				elseif up1 > up2 then
					up2 = min(up2 + (deltaTime / rate), up1)
				end

				if left1 < left2 then
					left2 = max(left2 - (deltaTime / rate), left1)
				elseif left1 > left2 then
					left2 = min(left2 + (deltaTime / rate), left1)
				end

				humanoid.Jump = jump

				humanoid:Move(newVector3(left2, 0, up2), true)
			end)
		end
	end)
end

local function onInputTypeFilterChanged(newInputType: string)
	unbindAllActions()

	if newInputType == "Keyboard" then
		bindKeyboardActions()
	elseif newInputType == "Gamepad" then
		bindGamepadActions()
	elseif newInputType == "Touch" then
		bindTouchActions()
	end
end

local function onLastInputTypeChanged(lastInputType: Enum.UserInputType)
	if lastInputType == Enum.UserInputType.Keyboard or string.find(lastInputType.Name, "Mouse") then
		inputTypeFilter.Value = "Keyboard"
	elseif string.find(lastInputType.Name, "Gamepad") then
		inputTypeFilter.Value = "Gamepad"
	elseif lastInputType == Enum.UserInputType.Touch then
		inputTypeFilter.Value = "Touch"
	end
end

onCharacterAdded(player.Character or player.CharacterAdded:Wait())

onRateChanged()
onDeadzoneChanged()

if UserInputService.KeyboardEnabled then
	bindKeyboardActions()
elseif UserInputService.GamepadEnabled then
	bindGamepadActions()
elseif UserInputService.TouchEnabled then
	bindTouchActions()
end

player.CharacterAdded:Connect(onCharacterAdded)
script:GetAttributeChangedSignal("Rate"):Connect(onRateChanged)
script:GetAttributeChangedSignal("Controller_Deadzone"):Connect(onDeadzoneChanged)
inputTypeFilter.Changed:Connect(onInputTypeFilterChanged)
UserInputService.LastInputTypeChanged:Connect(onLastInputTypeChanged)
10 Likes

How do I configure how smooth it is? Or how much time the momentum has?

As shown in the demo video, the LocalScript has an attribute named Rate which adjusts the time it takes for the character to accelerate and decelerate

Its unit is in seconds, so if for example the rate is set to 2 (assuming the Humanoid’s WalkSpeed is set to the default of 16, although do note that my script will work no matter the value of the WalkSpeed property):

  • 0-16 studs per second will take 2 seconds
  • 16-0 studs per second will also take 2 seconds
  • 0-16-0 studs per second will take 4 seconds

Do note that due to floating-point error the acceleration and deceleration time are approximate, so for example instead of taking 4 seconds exactly in the 0-16-0 case it might instead take 3.99999999998 seconds which is a small enough difference that it won’t be noticeable during gameplay

You can either adjust the rate manually yourself while in Studio, or adjust the rate using a separate script to simulate for example a slippery environment

1 Like

Could I ask what controllerDeadzone is?

I dont seem to notice any typa changes when I change it during gameplay.

Also theres a bit of a problem when using custom modules that change the camera system
For example I have a smoothShiftlock camera system, but for some reason it does not work while Momentum Controls 2 is in use. In order to use Momentum Controls 2 with my custom camera I have to press and hold right mouse while moving the camera to be able to use it. Basically regular camera movement but with shiftlock enabled which is not what i want.

With


I tried moving my mouse like normal between 0:10 seconds - 0:14 seconds.
When camera was moving at 0:14 seconds - 0:16 seconds what was me pressing + hold + moving right mouse button

With out


This is without my smoothshiftlock system in a regular baseplate

The attribute is to adjust a gamepad’s deadzone since physical joysticks don’t always return to exactly 0, 0 when you let go of them especially after they wear out from regular use. The default character controller also corrects for gamepad deadzone, but the value it uses can only be changed by forking it and editing it manually while in Studio which is why I decided to make it an attribute to give developers that use MC2 the option to add a settings menu which allows players to customize their deadzone value individually

Since custom camera scripts can vary significantly, I’ve only verified that MC2 works correctly while paired with the default one although I’m unsure as to why it isn’t working when using your custom shiftlock since there isn’t any code in MC2 that changes the behavior of the camera. Maybe setting the second argument of the humanoid:Move methods to false instead of true would fix the issue in your case, but this is only a theory unfortunately

February 16th 2024 Update

Bug Fix:

  • Fixed a possible divide by 0 if the Controller_Deadzone attribute is set to exactly 1

Yes this worked. Thank you!
@JohhnyLegoKing Sorry I lied. I’m still havinng the same problem

Character Limit

Another possible solution would be to temporarily set the Humanoid’s AutoRotate property to false whenever a player activates shift lock. You might need to do this on the sever-side as well

It still doesnt work. I could link you the shift lock system in case you want to test out this problem

1 Like

I was able to recreate the issue, and after some heavy testing I managed to find a solution :grin:

Here’s what you need to do:

  1. Change the name of MC2 into anything other than ControlScript, like for example MomentumControls2
  2. Add the following code in-between line 9 and 11:
local playerModule = player:WaitForChild("PlayerScripts"):WaitForChild("PlayerModule")
task.delay(1, playerModule.Destroy, playerModule)

(For clarification, that section of MC2’s code will look like this afterwards:)

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

local playerModule = player:WaitForChild("PlayerScripts"):WaitForChild("PlayerModule")
task.delay(1, playerModule.Destroy, playerModule)

local touchGui = script:WaitForChild("MC2_TouchGui"):: ScreenGui
local baseFrame = touchGui:WaitForChild("Base"):: Frame
local touchThumbstick = baseFrame:WaitForChild("Thumbstick"):: ImageLabel
local jumpButton = touchGui:WaitForChild("JumpButton"):: ImageButton

Yes!!! it works. Thank you so much :heart:

1 Like