Custom Walk Forces Cause Excessive Deceleration

Hey everyone! I’m modifying Egomoose’s GravityController, and everything is working correctly—except for a minor nitpick. The walkForce is applied in this section:

local dVelocity = tVelocity - hVelocity + fVelocity
	local walkForceM = math.min(10000, WALKF * self.CharacterMass * dVelocity.Magnitude)
	local walkForce = walkForceM > 0 and dVelocity.Unit*walkForceM or ZERO

It handles acceleration and deceleration forces well, but I would much rather preserve momentum while in the air such as when the player gets knocked back by an attack. Currently, the player grinds to a halt because the walkForce counteracts it.

I’m looking for a way to allow external forces to affect the player without being overridden by the deceleration logic. Does anyone know how this could be achieved? Perhaps using LinearVelocity or VectorForce?

Any help would be greatly appreciated this is a significant aspect of my game.

Entire Script:

local ZERO = Vector3.new(0, 0, 0)
local UNIT_X = Vector3.new(1, 0, 0)
local UNIT_Y = Vector3.new(0, 1, 0)
local UNIT_Z = Vector3.new(0, 0, 1)
local VEC_XY = Vector3.new(1, 0, 1)

local IDENTITYCF = CFrame.new()

local JUMPMODIFIER = 1.2
local TRANSITION = 0.15
local WALKF = 200 / 3

local UIS = game:GetService("UserInputService")
local RUNSERVICE = game:GetService("RunService")

local InitObjects = require(script:WaitForChild("InitObjects"))
local CameraModifier = require(script:WaitForChild("CameraModifier"))
local AnimationHandler = require(script:WaitForChild("AnimationHandler"))
local StateTracker = require(script:WaitForChild("StateTracker"))

-- Class

local GravityController = {}
GravityController.__index = GravityController

-- Private Functions

local function getRotationBetween(u, v, axis)
	local dot, uxv = u:Dot(v), u:Cross(v)
	if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
	return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end

local function lookAt(pos, forward, up)
	local r = forward:Cross(up)
	local u = r:Cross(forward)
	return CFrame.fromMatrix(pos, r.Unit, u.Unit)
end

local function getMass(array)
	local mass = 0
	for _, part in next, array do
		if (part:IsA("BasePart")) then
			mass = mass + part:GetMass()
		end
	end
	return mass
end

local function getPointVelocity(part, point)
	local pcf = part.CFrame
	local lp = pcf:PointToObjectSpace(point)
	local angularVelocity = pcf:VectorToObjectSpace(part.RotVelocity)
	local cross = angularVelocity:Cross(lp)
	return part.Velocity + pcf:VectorToWorldSpace(cross)
end

-- Public Constructor

function GravityController.new(player)
	local self = setmetatable({}, GravityController)

	-- Camera
	local loaded = player.PlayerScripts:WaitForChild("PlayerScriptsLoader"):WaitForChild("Loaded")
	if (not loaded.Value) then
		loaded.Changed:Wait()
	end
	
	local playerModule = require(player.PlayerScripts:WaitForChild("PlayerModule"))
	self.Controls = playerModule:GetControls()
	self.Camera = playerModule:GetCameras()
	
	self.CameraModifier = CameraModifier.new(player)
	
	-- Player and character
	self.Player = player
	self.Character = player.Character
	self.Humanoid = player.Character:WaitForChild("Humanoid")
	self.HRP = player.Character:WaitForChild("HumanoidRootPart")
	
	-- Animation
	self.AnimationHandler = AnimationHandler.new(self.Humanoid, self.Character:WaitForChild("Animate"))
	self.AnimationHandler:EnableDefault(false)
	
	local soundState = player.PlayerScripts:WaitForChild("RbxCharacterSounds"):WaitForChild("SetState")
	
	self.StateTracker = StateTracker.new(self.Humanoid, soundState)
	self.StateTracker.Changed:Connect(function(name, speed)
		self.AnimationHandler:Run(name, speed)
	end)
	
	-- Collider and forces
	local collider, gyro, vForce, floor, grounded = InitObjects(self)
	
	floor.Touched:Connect(function() end)
	
	self.Collider = collider
	self.VForce = vForce
	self.Gyro = gyro
	self.Floor = floor
	self.Grounded = grounded
	
	-- Gravity properties
	self.GravityUp = UNIT_Y
	self.FloorVelocity = ZERO
	self.Ignores = {self.Character}
	
	function self.Camera.GetUpVector(this, oldUpVector)
		return self.GravityUp
	end
	
	-- Events etc
	self.Humanoid.PlatformStand = true
	
	self.CharacterMass = getMass(self.Character:GetDescendants())
	self.Character.AncestryChanged:Connect(function() self.CharacterMass = getMass(self.Character:GetDescendants()) end)
	
	self.JumpCon = UIS.JumpRequest:Connect(function() self:OnJumpRequest() end)
	self.DeathCon = self.Humanoid.Died:Connect(function() self:Destroy() end)
	self.SeatCon = self.Humanoid.Seated:Connect(function(active) if (active) then self:Destroy() end end)
	RUNSERVICE:BindToRenderStep("GravityStep", Enum.RenderPriority.Input.Value + 1, function(dt) self:OnGravityStep(dt) end)
	
	return self
end

-- Public Methods

function GravityController:Destroy()
	self.JumpCon:Disconnect()
	self.DeathCon:Disconnect()
	self.SeatCon:Disconnect()
	
	RUNSERVICE:UnbindFromRenderStep("GravityStep")
	
	self.CameraModifier:Destroy()
	self.Collider:Destroy()
	self.VForce:Destroy()
	self.Gyro:Destroy()
	self.StateTracker:Destroy()
	
	self.Humanoid.PlatformStand = false
	self.AnimationHandler:EnableDefault(true)
	
	self.GravityUp = UNIT_Y
end

function GravityController:GetGravityUp(oldGravity)
	return oldGravity
end

function GravityController:IsGrounded()
	local parts = self.Floor:GetTouchingParts()
	for _, part in next, parts do
		if (not part:IsDescendantOf(self.Character)) then
			self.Grounded.Value = true
			return true
		end
	end
	self.Grounded.Value = false
	return false
end

function GravityController:OnJumpRequest()
	if (not self.StateTracker.Jumped and self:IsGrounded()) then
		local hrpVel = self.HRP.Velocity
		self.HRP.Velocity = hrpVel + self.GravityUp*self.Humanoid.JumpPower*JUMPMODIFIER
		self.StateTracker.Jumped = true
	end
end

function GravityController:GetFloorVelocity()
	local ray = Ray.new(self.Collider.Position, -1.1*self.GravityUp)
	local hit, pos, normal = workspace:FindPartOnRayWithIgnoreList(ray, self.Ignores)
	
	local velocity = ZERO
	if (hit and hit:isA("BasePart")) then
		-- assumes the center of mass of the part is part.CFrame.p
		velocity = getPointVelocity(hit, self.HRP.Position)
	end
	
	return velocity
end

function GravityController:GetMoveVector()
	return self.Controls:GetMoveVector()
end

function GetTagActive(character, name)
	local trigger = character:FindFirstChild(name) 
	local isTrue = trigger and trigger.Value and true or false
	return isTrue
end

function GravityController:OnGravityStep(dt)
	-- update gravity up vector
	local oldGravity = self.GravityUp
	local newGravity = self:GetGravityUp(oldGravity)
	
	local normalTorque = Vector3.new(100000, 100000, 100000)

	local isRagdolled = GetTagActive(self.Character, "RagdollTrigger")
	local isFlying = GetTagActive(self.Character, "Flying")
	
	local rotation = getRotationBetween(oldGravity, newGravity, workspace.CurrentCamera.CFrame.RightVector)
	rotation = IDENTITYCF:Lerp(rotation, TRANSITION)
	
	self.GravityUp = rotation * oldGravity
	
	-- get world move vector
	local camCF = workspace.CurrentCamera.CFrame
	local fDot = camCF.LookVector:Dot(newGravity)
	local cForward = math.abs(fDot) > 0.5 and -math.sign(fDot)*camCF.UpVector or camCF.LookVector
	
	local left = cForward:Cross(-newGravity).Unit
	local forward = -left:Cross(newGravity).Unit
	
	local move = self:GetMoveVector()
	local worldMove = forward*move.z - left*move.x
	worldMove = worldMove:Dot(worldMove) > 1 and worldMove.Unit or worldMove
	
	local isInputMoving = worldMove:Dot(worldMove) > 0
	
	-- get the desired character cframe
	local hrpCFLook = self.HRP.CFrame.LookVector
	local charF = hrpCFLook:Dot(forward)*forward + hrpCFLook:Dot(left)*left
	local charR = charF:Cross(newGravity).Unit
	local newCharCF = CFrame.fromMatrix(ZERO, charR, newGravity, -charF)
	
	local newCharRotation = IDENTITYCF
	if (isInputMoving) then
		newCharRotation = IDENTITYCF:Lerp(getRotationBetween(charF, worldMove, newGravity), 0.7)	
	end
	
	-- calculate forces
	local g = workspace.Gravity
	local gForce = g * self.CharacterMass * (UNIT_Y - newGravity)
	
	local cVelocity = self.HRP.Velocity
	local tVelocity = self.Humanoid.WalkSpeed * worldMove
	local gVelocity = cVelocity:Dot(newGravity)*newGravity
	local hVelocity = cVelocity - gVelocity
	local fVelocity = self:GetFloorVelocity()
	
	if (hVelocity:Dot(hVelocity) < 1) then
		hVelocity = ZERO
	end
	
	local dVelocity = tVelocity - hVelocity + fVelocity
	local walkForceM = math.min(10000, WALKF * self.CharacterMass * dVelocity.Magnitude)
	local walkForce = walkForceM > 0 and dVelocity.Unit*walkForceM or ZERO
	
	-- mouse lock
	local charRotation = newCharRotation * newCharCF
	if (self.CameraModifier.IsCamLocked) then
		local lv = workspace.CurrentCamera.CFrame.LookVector
		local hlv = lv - charRotation.UpVector:Dot(lv)*charRotation.UpVector
		charRotation = lookAt(ZERO, hlv, charRotation.UpVector)
	end

	-- get state
	self.StateTracker:OnStep(self.GravityUp, self:IsGrounded(), isInputMoving)

	-- update values
	self.Gyro.Enabled = not isRagdolled and not isFlying
	self.VForce.Enabled = not isRagdolled and not isFlying
	
	self.VForce.Force = gForce + walkForce
	if not isRagdolled then
		self.Gyro.CFrame = charRotation
	end
end


return GravityController