Hold down key for constant movement

Hello developers,

I am currently working on a custom movement system for a project, and I have ran into a small issue with my code. Originally, I wanted to have the feature where you can hold down a key and your character would “just move” until released. The problem is, if you just spam said key you can go extremely fast and find a few weird edge cases. Here’s a video of the issue:

Here is my code:

local function HandleAction(actionName: string, inputState: Enum.UserInputState, inputObject: InputObject)
	if ActionToVector3[actionName] then
		if inputState == Enum.UserInputState.Begin then
			CurrentKeyCode = inputObject.KeyCode

			PlayerCharacter.Position += ActionToVector3[actionName]
			MovementNetwork:Fire(actionName)
			
			task.wait(0.5)
			
			if UserInputService:IsKeyDown(inputObject.KeyCode) then
				repeat	
					PlayerCharacter.Position += ActionToVector3[actionName]
					MovementNetwork:Fire(actionName)
					task.wait(0.2)
				until not UserInputService:IsKeyDown(inputObject.KeyCode)
			end
		elseif (inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel) and inputObject.KeyCode == CurrentKeyCode then
			return
		end
	end
end

Have any ideas on how I could combat this issue?

So, I know this is the case for Unreal Engine, but Movement Components need to run EVERY TICK to CONSUME the input. In your case, you got an excellent script for detecting player input, but you need to have a tick function that detects ALL inputs and consumes them by adjusting the player’s position/orientation as long as the input is still Begin but never End/Cancelled

I do this for my floating camera script:

--This Table is used to reference all possible consumable inputs from a player
--IE, move forwards, backwards, right left, etc
local InputTable = 
	{
		Forward={Modifier=Vector3.new(0,0,-1),Active=false},
		Backwards={Modifier=Vector3.new(0,0,1),Active=false},
		Left={Modifier=Vector3.new(-1,0,0),Active=false},
		Right={Modifier=Vector3.new(1,0,0),Active=false},
		Up={Modifier=Vector3.new(0,1,0),Active=false},
		Down={Modifier=Vector3.new(0,-1,0),Active=false}
	}
--This function is called every tick to consume the input. It is not responsible for
--resetting the Active values to false because doing so will cause ZERO movement 
--All this function does is scan the input table for any Active inputs and add its 
--modifier to the current nextMove. Then it returns nextMove
function GetNextMovement(deltaTime) 
	local nextMove = Vector3.new()
	for name,element in pairs(InputTable) do
		if element.Active then
			nextMove+=element.Modifier
		end
	end
	return CFrame.new(nextMove*(speed*deltaTime))
end
--This function is called whenever the respective key is pressed
local Move=function(actionName,inputState)
	if inputState==Enum.UserInputState.Begin then
		InputTable[actionName].Active=true
	elseif inputState == Enum.UserInputState.End then
		InputTable[actionName].Active=false
	end
end
--This function is called from the spawn() function. Its a constant tick function that modifies the player's camera based on what GetNextMovement returns
function onSelected()
	lastUpdate=tick()
	local Root = PhysicalCamera
	Camera.CameraSubject=Root
	character:WaitForChild("HumanoidRootPart").Anchored=true
	while true do 
		local delta = tick()-lastUpdate
		local look = (Camera.Focus.p-Camera.CoordinateFrame.p).unit
		local move = GetNextMovement(delta)
		local pos = Root.Position
		pos=ScrubPosition(pos)
		Root.CFrame = CFrame.new(pos,pos+look)*move
		lastUpdate=tick()
		wait()
	end
end
spawn(onSelected)

This code example does not include the action binding at the bottom, and shoudnt since that would be a whole different solution to a different problem.

I hope this code example is a good solution or reference for you!

Laying in bed, this looks complicated. I feel like it’s kind of hard to see where things are being updated as the code is rather messy, though it looks like it would work. The main thing I don’t understand is all the math operations with c frames that you’re doing.

Thank you for actually helping me though, usually whenever I ask for help I never get an answer.

I only respond to posts with no replies. The math here IS complicated, but i can simplify all the operations.

  1. The script binds the user actions with ContextActionServerice:BindAction() and it passes the function Move
  2. The script spawns a thread of the onSelected() function. Its purpose is to repeat EVERY tick and set the camera’s position. Delta is how much time has passed (if too much or too little time has passed, we need to modify the player’s position accordingly).
  3. Move calls GetNextMovement. (it will call this function EVERY tick)
  4. GetNextMovement receives call and compares all the possible inputs within the input table. Ie, Left, Rigth, Forward, Backwards. etc. If the value is True, then it modifies a fresh nextMove vector. Then it creates a basic CFrame with nextMove vector MULTIPLIED BY THE DELTA!!! Very important because it will accommodate the lag.
  5. Now, whenever Move function is called, it will simply set the respective input within the InputTable to Active=true so GetNextMovement will know that the player wishes to move. (the Move function will also fire if the input has cease and will set the Active to false)

Heres a simple mockup:

local IsPlayerMovingFowards =false
local lastUpdate=tick()
local Player = game:GetService("Players").LocalPlayer
local Character = Player.Character

--This function is called whenever the player pressed W!
local Move=function(ActionName,inputState)
    if inputState==Enum.UserInputState.Begin then
        print("The player wishes to move forward!!!!")
		IsPlayerMovingFowards = true
	elseif inputState == Enum.UserInputState.End then
		IsPlayerMovingFowards = false
	end
end

function GetNextPosition(Delta)
  --Make fresh Vector3
  local nextMove = Vector3.new()
  if IsPlayerMovingFowards then
     --if the player is holding down W key, then add 0,0,-1 to the fresh vector
     --(in my case, this makes my camera move forard)
     nextMove= nextMove + Vector3.new(0,0,-1)
  end
  return nextMove
end

game:GetService("ContextActionService"):BindAction("MoveFoward",Move,false,Enum.KeyCode.W)
--Every tick do this
while true do
    --The difference between the last tick and the current tick is the delta. We
    --shall use this to figure out how much time has passed since the last tick. Useful for laggy players!
    local Delta = tick()-lastUpdate
    --Every tick, try to move to humanoid forward with MoveTo. 
    --Get its current position and add the forward offset to it. If the player isnt holding down W, then 
    --the offset will just be ZERO, aka no movement
    Character.Humanoid:MoveTo(  Character.HumanoidRootPart.Position+GetNextPosition() )
    --update old tick to the newest tick and repeat!
    lastUpdate = tick()
end
1 Like

Alright, I’ll try this tomorrow and follow up with my result.
One question though: would I want to wait a frame in the while loop? I feel like it could error. By error, it’s along the lines of the “max runtime”

Ah. You want to wait a single tick in a while loop ALWAYS because of infinity. You computer is fast, almost too fast. The fastest way to crash roblox studio is to implement the infinite loop and cause it to infinitely run a loop with no breaks in between. So thats why we use Wait(). Wait() makes the thread wait a single tick in between and allows your computer to take a break,

In Unreal Engine, the

while true do 
  wait()
end

is referred to as the Tick() function

1 Like

I’m guessing you refer to frame by a tick? Heart if yes.

1 Like