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.
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:
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
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 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 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
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
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
Yeah, good question I don’t know how I found out I think that maybe I saw how you can walk slowly with joysticks on mobile and wondered what was up with that.
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.
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)
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)
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)
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