Simulating smoother character movement?

EDIT :warning: EDIT :warning: EDIT :warning:EDIT :warning:

There’s a bug in the code I posted. It’s quite easy to fix, see what it is and how to fix it here:

Thanks to @dthecoolest for catching it :partying_face: Now here’s the rest of the comment:


Can’t tell if you want character to accelerate slower without changing physical properties, or if you want to normally with changed physical properties.

EDIT: nvm, figured it out :stuck_out_tongue: Making proper reply

I would make a custom movement script from scratch that uses the built-in humanoid movement system, but manually managing the exact movement velocity so instead of each direction being only on/off, it gradually reaches a target velocity.

Basic movement script that has the old beahvior (put in a LocalScript called ControlScript in game.StarterPlayer.StarterPlayerScripts)
local RunS = game:GetService("RunService")
local InputS = game:GetService("UserInputService")

local player = game.Players.LocalPlayer
local camera = game.Workspace.CurrentCamera
local character = player.Character or player.CharacterAdded:Wait()

player.CharacterAdded:Connect(function(_character)
	character = _character
end)

local walkKeyBinds = {
	Forward = { Key = Enum.KeyCode.W, Direction = Enum.NormalId.Front },
	Backward = { Key = Enum.KeyCode.S, Direction = Enum.NormalId.Back },
	Left = { Key = Enum.KeyCode.A, Direction = Enum.NormalId.Left },
	Right = { Key = Enum.KeyCode.D, Direction = Enum.NormalId.Right }
}

local function getWalkDirectionCameraSpace()
	local walkDir = Vector3.new()

	for keyBindName, keyBind in pairs(walkKeyBinds) do
		if InputS:IsKeyDown(keyBind.Key) then
			walkDir += Vector3.FromNormalId( keyBind.Direction )
		end
	end

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end
	
	return walkDir
end

local function getWalkDirectionWorldSpace()
	local walkDir = camera.CFrame:VectorToWorldSpace( getWalkDirectionCameraSpace() )
	walkDir *= Vector3.new(1, 0, 1) --Set Y axis to 0

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end

	return walkDir
end

local function updateMovement( dt )
	local humanoid = character:FindFirstChild("Humanoid")
	if humanoid then
		humanoid:Move( getWalkDirectionWorldSpace() )
	end
end	

RunS.RenderStepped:Connect(updateMovement)

The thing that we’ll change to make the character slowly get up to speed is the updateMovement function. I’ll add this to the top of the script:

local targetMoveVelocity = Vector3.new()
local moveVelocity = Vector3.new()
local moveAcceleration = 8

and this is the new updateMovement function:

local function updateMovement( dt )
	local humanoid = character:FindFirstChild("Humanoid")
	if humanoid then
		local moveDir = getWalkDirectionWorldSpace()
		targetMoveVelocity = moveDir
		moveVelocity = lerp( moveVelocity, targetMoveVelocity, math.clamp(dt * moveAcceleration, 0, 1) )
		humanoid:Move( moveVelocity )
	end
end	

Every frame it makes the actual velocity a little closer to the target velocity. It changes by a small fraction every update. This fraction is determined by the frame-frate and the moveAcceleration, which represents the acceleration from a complete stand-still. All of this causes it to asymptotically go towards the target velocity, which means as it gets closer to the target speed, it accelerates slower and slower, so we get a very smoothed-out speed change as it gets up to speed or slows down, but it still accelerates pretty fast and feels pretty responsive when the player suddenly changes direction.

It could definitely be improved to feel a lot more responsive, but this is a super simple example you can build off of.

Does this help? Let me know if you need help getting the scripts to work or have questions :slight_smile:

92 Likes
How to create momentum for platforming
How does climbing work for X Position?
Help with movement and replication
Player Rotation to mouse position not working
A better increase method?
How to make smooth character movement
My character moves faster in diagonal, how to solve it?
Physicsless chassis
Custom Movement curves
Adding acceleration to players
Help needed with getting character to move relative to camera
Create custom Character Controls
Vector3 values become NAN after period of time
How to separate character movement from camera direction
Capsule won't stay on floor
How Does roblox character movement work again?
Help me achieve this Movement System
Can't turn around my character without stopping their movement
Client input for a remote controlled drone being sticky
How to make player slowly deaccelerate when stop pressing move keys
How would i implement inertia
How do i make an decent player WalkTo() system
Ice-walking effect for player
How to separate camera and player movement?
How to add friction to roblox's character controller
How can I drive my car to Vector3 Position?
Can you slow players without changing their walkspeed?
"Orienting" a vector3 to the camera
Spaceship controls inverting when i move my ship
"Orienting" a vector3 to the camera
Help finding something in the default jeep
How to limit the players speed without using walkspeed?
How do we calculate the speed of steering using delta time?
Help with a character clone what repeat player moves
Making characters move with wasd relative to world and not to camera
Walk speed ramp up
How would I go about making a CS:GO shooter?
How would I go about creating an "Offset angle" for the camera?
Making player skid?
Recreate this type of movement?
Issue with having movement relative to camera
Bug..? - Setting CharacterAutoLoad to false disables movement for non-keyboard
X rotation of camera affects diagonal movement of custom movement script
Help with CFrame/Vector3 math (Ball Controller)

This is really great and does what I and many others have been looking for!

My only question is if it possible to have it so your character can’t change direction mid jumping?

I converted it into a module for anyone:

Code
local SM = {}

local RunS = game:GetService("RunService")
local InputS = game:GetService("UserInputService")

local player = game.Players.LocalPlayer
local camera = game.Workspace.CurrentCamera

local function lerp(a, b, c)
	return a + ((b - a) * c)
end

local targetMoveVelocity = Vector3.new()
local moveVelocity = Vector3.new()
local isBusy = false

local MOVE_ACCELERATION = 9

local walkKeyBinds = {
	Forward = { Key = Enum.KeyCode.W, Direction = Enum.NormalId.Front },
	Backward = { Key = Enum.KeyCode.S, Direction = Enum.NormalId.Back },
	Left = { Key = Enum.KeyCode.A, Direction = Enum.NormalId.Left },
	Right = { Key = Enum.KeyCode.D, Direction = Enum.NormalId.Right }
}

local function getWalkDirectionCameraSpace()
	local walkDir = Vector3.new()

	for keyBindName, keyBind in pairs(walkKeyBinds) do
		if InputS:IsKeyDown(keyBind.Key) then
			walkDir += Vector3.FromNormalId( keyBind.Direction )
		end
	end

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end

	return walkDir
end

local function getWalkDirectionWorldSpace()
	local walkDir = camera.CFrame:VectorToWorldSpace( getWalkDirectionCameraSpace() )
	walkDir *= Vector3.new(1, 0, 1) --Set Y axis to 0

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end

	return walkDir
end


function SM:Update(character,dt)
	local humanoid = character:FindFirstChild("Humanoid")
	if humanoid then
		local moveDir = getWalkDirectionWorldSpace()
		targetMoveVelocity = moveDir
		moveVelocity = lerp( moveVelocity, targetMoveVelocity, math.clamp(dt * MOVE_ACCELERATION, 0, 1) )

		--print(moveVelocity.Magnitude)
		
		if isBusy or moveVelocity.Magnitude <= 0.2 then
			humanoid:Move(Vector3.new())
		else
			humanoid:Move( moveVelocity )
		end
	end
end

InputS.InputBegan:Connect(function(_,busy)
	isBusy = busy
end)

InputS.InputEnded:Connect(function(_,busy)
	isBusy = busy
end)

return SM
11 Likes

Just went through the main player ControlModule, if you add this line of code at like line 351

mainMoveVector = lerp(mainMoveVector, moveVector, math.clamp(dt * MOVE_ACCELERATION, 0, 1) )

It will achieve the exact same effect with less bugs.

Full function edit
--settings:
local MOVE_ACCELERATION = 9
local JUMP_DIRECTION_FREEZE = true -- stop player from changing direction mid jump
local MIN_VALUE = 0.55 -- do not make less than 0.1 or character can fling into void...
---------------------------

----don't touch
local targetMoveVector = Vector3.new()
local mainMoveVector = Vector3.new()
local lX,lY,lZ = 0,0,0
---------------------------

function ControlModule:OnRenderStepped(dt)
	if self.activeController and self.activeController.enabled and self.humanoid then
		-- Give the controller a chance to adjust its state
		self.activeController:OnRenderStepped(dt)

		-- Now retrieve info from the controller
		local moveVector = self.activeController:GetMoveVector()
		local cameraRelative = self.activeController:IsMoveVectorCameraRelative()

		local clickToMoveController = self:GetClickToMoveController()
		if self.activeController ~= clickToMoveController then
			if moveVector.magnitude > 0 then
				-- Clean up any developer started MoveTo path
				clickToMoveController:CleanupPath()
			else
				-- Get move vector for developer started MoveTo
				clickToMoveController:OnRenderStepped(dt)
				moveVector = clickToMoveController:GetMoveVector()
				cameraRelative = clickToMoveController:IsMoveVectorCameraRelative()
			end
		end
		

		-- Are we driving a vehicle ?
		local vehicleConsumedInput = false
		if self.vehicleController then
			moveVector, vehicleConsumedInput = self.vehicleController:Update(moveVector, cameraRelative, self.activeControlModule==Gamepad)
		end

		-- If not, move the player
		-- Verification of vehicleConsumedInput is commented out to preserve legacy behavior,
		-- in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat
		--if not vehicleConsumedInput then
		if cameraRelative then
			moveVector = calculateRawMoveVector(self.humanoid, moveVector)
		end

		local willJump = self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping())

		if not JUMP_DIRECTION_FREEZE or (JUMP_DIRECTION_FREEZE and not willJump) then
			targetMoveVector = moveVector
		end

		mainMoveVector = lerp(mainMoveVector, targetMoveVector, math.clamp(dt * MOVE_ACCELERATION, 0, 1) )
	
		local X,Y,Z = mainMoveVector.X,mainMoveVector.Y,mainMoveVector.Z

		--print(X,Y,Z,"		",lX,lY,lZ)

		----set minimum value (otherwise your character will be sliding)
		if (X<lX and X>0 and X<MIN_VALUE) or (X>lX and X<0 and X>-MIN_VALUE) then
			X = 0
		end
		if Y<lY and Y>0 and Y<MIN_VALUE or (Y>lY and Y<0 and Y>-MIN_VALUE) then
			Y = 0
		end	
		if Z<lZ and Z>0 and  Z<MIN_VALUE or (Z>lZ and Z<0 and Z>-MIN_VALUE) then
			Z = 0
		end
		-------------------
		
		mainMoveVector = Vector3.new(X,Y,Z)
		lX,lY,lZ = X,Y,Z
	
		self.moveFunction(Players.LocalPlayer, mainMoveVector, false)
	--end

		-- And make them jump if needed
		self.humanoid.Jump = willJump
	end
end
26 Likes

Wait so, we are gradually adjusting the magnitude of the move direction between 0-1 in order to control the currentWalkSpeed?

How is this possible and how did you find out, it doesn’t seem to be in the documentation?

3 Likes

Yeah, good question I don’t know how I found out :stuck_out_tongue: I think that maybe I saw how you can walk slowly with joysticks on mobile and wondered what was up with that.

4 Likes

Sorry, just a heads up and advice to other people looking to use this method which works.

If you use the script as is it’s possible to end up with a NAN value if the humanoid stops moving after around 30 seconds as explained in this post:

It’s probably a bug with humanoid I believe the Humanoid:Move() method does not take into account NAN values and make them (0,0,0) instead. Instead what we get now is that the humanoid completely dissapears for a second?

Yeah just be aware of what happens if the current value keeps lerping to (0,0,0) and include a failsafe like what I did for now:

        if moveVelocity.Magnitude<0.01 then
            moveVelocity = moveVelocity*Vector3.new(0,0,0)
        end

this will ensure the move velocity becomes a static (0,0,0) and not eventually NAN if the player stands still.

Edit: Update Roblox has fixed this issue, no need to manually do a small magnitude check anymore.

8 Likes

I’m kinda confused, The controlscript module has been changed and i don’t know what to pick in-order to make my movement smooth

4 Likes

line 406

self.mainMoveVector = lerp(self.mainMoveVector, moveVector, math.clamp(dt * MOVE_ACCELERATION, 0, 1))
self.moveFunction(Players.LocalPlayer, self.mainMoveVector, false)

line 431

self.humanoid = char:FindFirstChildOfClass("Humanoid")
self.mainMoveVector = Vector3.new()

change MOVE_ACCELERATION to whichever value you want
sorry if this doesn’t help

4 Likes

It keeps saying "Attempt to call a nil value with the lerping, can anyone help?

1 Like

you need to define a lerp function on your own
function lerp(a, b, t) return a + (b - a) * t end

5 Likes

Thanks, I already figured that out :smiley:

2 Likes

Could you break this down a bit? Im a major beginner

2 Likes

Lerping, otherwise known as linear interpolation, is a mathematical operation that lets you determine a certain position within two points.

In the case of this function A is the origin point, B is the end point, and T is the ratio as to where you want your point to be.

2 Likes

sorry for the late post, but where exactly are you suposed to define the lerp function (im kinda new to this so maybe a little explanation would be appreciated)

1 Like

I combined the information from this whole post and made this script:

local targetMoveVelocity = Vector3.new()
local moveVelocity = Vector3.new()
local moveAcceleration = 8

local RunS = game:GetService("RunService")
local InputS = game:GetService("UserInputService")

local player = game.Players.LocalPlayer
local camera = game.Workspace.CurrentCamera
local character = player.Character or player.CharacterAdded:Wait()

player.CharacterAdded:Connect(function(_character)
	character = _character
end)

local walkKeyBinds = {
	Forward = { Key = Enum.KeyCode.W, Direction = Enum.NormalId.Front },
	Backward = { Key = Enum.KeyCode.S, Direction = Enum.NormalId.Back },
	Left = { Key = Enum.KeyCode.A, Direction = Enum.NormalId.Left },
	Right = { Key = Enum.KeyCode.D, Direction = Enum.NormalId.Right }
}

function lerp(a, b, t) return a + (b - a) * t end

local function getWalkDirectionCameraSpace()
	local walkDir = Vector3.new()

	for keyBindName, keyBind in pairs(walkKeyBinds) do
		if InputS:IsKeyDown(keyBind.Key) then
			walkDir += Vector3.FromNormalId( keyBind.Direction )
		end
	end

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end

	return walkDir
end

local function getWalkDirectionWorldSpace()
	local walkDir = camera.CFrame:VectorToWorldSpace( getWalkDirectionCameraSpace() )
	walkDir *= Vector3.new(1, 0, 1) --Set Y axis to 0

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end

	return walkDir
end

local function updateMovement( dt )
	local humanoid = character:FindFirstChild("Humanoid")
	if humanoid then
		local moveDir = getWalkDirectionWorldSpace()
		targetMoveVelocity = moveDir
		moveVelocity = lerp( moveVelocity, targetMoveVelocity, math.clamp(dt * moveAcceleration, 0, 1) )
		humanoid:Move( moveVelocity )
	end
end	

RunS.RenderStepped:Connect(updateMovement)
12 Likes

is there a reason why we can’t jump anymore ?

1 Like

heres an improved script which works on mobile AND pc

local character = plr.Character or plr.CharacterAdded:Wait()
local camera = game.Workspace.CurrentCamera
local RunS = game:GetService("RunService")

local targetMoveVelocity = Vector3.new()
local moveVelocity = Vector3.new()
local moveAcceleration = 6

local function lerp(a, b, t) return a + (b - a) * t end

local function getWalkDirectionCameraSpace()
	local walkDir = require(plr:WaitForChild("PlayerScripts").PlayerModule:WaitForChild("ControlModule")):GetMoveVector()
		
	if walkDir.Magnitude > 0 then
		walkDir = walkDir.Unit
	end

	return walkDir
end

local function getWalkDirectionWorldSpace()
	local walkDir = camera.CFrame:VectorToWorldSpace(getWalkDirectionCameraSpace())
	walkDir *= Vector3.new(1, 0, 1) --Set Y axis to 0

	if walkDir.Magnitude > 0 then
		walkDir = walkDir.Unit
	end

	return walkDir
end

local function updateMovement(dt)
	local humanoid = character:FindFirstChild("Humanoid")
	if humanoid then
		local moveDir = getWalkDirectionWorldSpace()
		targetMoveVelocity = moveDir
		moveVelocity = lerp(moveVelocity, targetMoveVelocity, math.clamp(dt * moveAcceleration, 0, 1))
		humanoid:Move(moveVelocity)
	end
end	

RunS.RenderStepped:Connect(updateMovement)
7 Likes

For anyone that has the following issue:
When looking up or down with the camera, the movement vector direction changes slightly. Aka: left / right input have a greater ‘weight’, resulting in a non diagonal movement vector, when pressed with the forward or backward input.
This happens because the magnitude of the camera LookVector changes when the camera is rotated up or down.

This could be a desired effect, not in my case however.
Here is the updated GetInput (GetWalkDirection) function, which gets rid of this issue / feature:

local function GetWalkDirection()

	local walkDir = Vector3.new()
	local camDir = (camera.CFrame.LookVector * Vector3.new(1, 0, 1)).Unit
	local angle = math.atan2(camDir.Z, camDir.X)
	
	if(isGameProcess) then
		walkDir = Vector3.zero
	else
		for keyBindName, keyBind in pairs(walkKeyBinds) do
			for _, k in pairs(keyBind.Key) do
				if InputService:IsKeyDown(k) then
					walkDir += Vector3.FromNormalId(keyBind.Direction)
				end
			end
		end
	end

	walkDir = rotateVectorAround(walkDir, ((3*math.pi)/2) - angle, Vector3.FromAxis(Enum.Axis.Y))

	if walkDir.Magnitude > 0 then
		walkDir = walkDir.Unit
	end

	return walkDir
end
4 Likes

I used one of the scripts and I keep getting this error on line 55
“moveVelocity = math.lerp( moveVelocity, targetMoveVelocity, math.clamp(dt * moveAcceleration, 0, 1) )”

 14:30:17.548  Players.Varsyte.PlayerScripts.MovementControl:55: attempt to call a nil value  -  Client - MovementControl:55

This is the code:

local RunS = game:GetService("RunService")
local InputS = game:GetService("UserInputService")

local player = game.Players.LocalPlayer
local camera = game.Workspace.CurrentCamera
local character = player.Character or player.CharacterAdded:Wait()

local targetMoveVelocity = Vector3.new()
local moveVelocity = Vector3.new()
local moveAcceleration = 8

player.CharacterAdded:Connect(function(_character)
	character = _character
end)

local walkKeyBinds = {
	Forward = { Key = Enum.KeyCode.W, Direction = Enum.NormalId.Front },
	Backward = { Key = Enum.KeyCode.S, Direction = Enum.NormalId.Back },
	Left = { Key = Enum.KeyCode.A, Direction = Enum.NormalId.Left },
	Right = { Key = Enum.KeyCode.D, Direction = Enum.NormalId.Right }
}

local function getWalkDirectionCameraSpace()
	local walkDir = Vector3.new()

	for keyBindName, keyBind in pairs(walkKeyBinds) do
		if InputS:IsKeyDown(keyBind.Key) then
			walkDir += Vector3.FromNormalId( keyBind.Direction )
		end
	end

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end

	return walkDir
end

local function getWalkDirectionWorldSpace()
	local walkDir = camera.CFrame:VectorToWorldSpace( getWalkDirectionCameraSpace() )
	walkDir *= Vector3.new(1, 0, 1) --Set Y axis to 0

	if walkDir.Magnitude > 0 then --(0, 0, 0).Unit = NaN, do not want
		walkDir = walkDir.Unit --Normalize, because we (probably) changed an Axis so it's no longer a unit vector
	end

	return walkDir
end

local function updateMovement( dt )
	local humanoid = character:FindFirstChild("Humanoid")
	if humanoid then
		local moveDir = getWalkDirectionWorldSpace()
		targetMoveVelocity = moveDir
		moveVelocity = math.lerp( moveVelocity, targetMoveVelocity, math.clamp(dt * moveAcceleration, 0, 1) )
		humanoid:Move( moveVelocity )
	end
end	

RunS.RenderStepped:Connect(updateMovement)
2 Likes

Hi,
it looks like the culprit here is your attempt to call “math.lerp”. I was unable to find this function in the documentation: math | Documentation - Roblox Creator Hub