Simulating smoother character movement?

When a character changes direction when movement, by default it is almost instant unless they are moving at a very fast velocity. I want to recreate this, however, the only way I’ve achieved what I want is by changing the physical properties of the character parts, which would break aspects of my game.

Default:

With changed physical properties:

Is it possible to recreate this in another way?

36 Likes

Bumping thread, yeah I’m also curious on a method to control humanoid movement since I’m trying to make a mech like system where I ride a humanoid mech and control it like a tank below:

External Media

Thing is I would also need the humanoid to accelerate slower and I’m wondering of the best ways to do it, changing the walkspeed value? Some sort of custom Vector force movement system? But then I would need the humanoid to be able to climb slopes as well how would that be taken into account?

Edit:

Hmm seems like others are having issues as well, seems like a difficult topic, I’ll research tomorrow

2 Likes

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:

85 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
Custom Movement curves
Physicsless chassis
My character moves faster in diagonal, how to solve it?
Adding acceleration to players
Vector3 values become NAN after period of time
Help needed with getting character to move relative to camera
Capsule won't stay on floor
Create custom Character Controls
How would I go about creating an "Offset angle" for the camera?
How can I drive my car to Vector3 Position?
Making player skid?
Recreate this type of movement?
Walk speed ramp up
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)
Making characters move with wasd relative to world and not to camera
How Does roblox character movement work again?
Help me achieve this Movement System
Help with a character clone what repeat player moves
How do we calculate the speed of steering using delta time?
Can't turn around my character without stopping their movement
How to limit the players speed without using walkspeed?
How to add friction to roblox's character controller
Client input for a remote controlled drone being sticky
Help finding something in the default jeep
"Orienting" a vector3 to the camera
How to separate character movement from camera direction
Spaceship controls inverting when i move my ship
"Orienting" a vector3 to the camera
Can you slow players without changing their walkspeed?
How would I go about making a CS:GO shooter?

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)
9 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)
6 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