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?

5 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:

https://streamable.com/mcp3g4

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

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:

6 Likes

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
1 Like

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
4 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?

https://developer.roblox.com/en-us/api-reference/function/Humanoid/Move

1 Like

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.

2 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.

2 Likes