Character gets flung when I jump

i’m working on a character controller and ran into a bug where the character occasionally gets flung when jumping. i’ve identified the root cause, but i don’t fully understand why it happens. it seems like the capsule somehow gets flipped, rotated, or turned around when i modify its upward velocity. it doesn’t happen every single time, but often enough to make normal movement frustrating.

this issue only started showing up after i added jumping, and i haven’t been able to figure out a fix, which kind of stalled the whole project. for context, this is my first attempt at making a character controller from scratch.

here’s footage of this stinking bug

and this is the main controller itself

local runService = game:GetService("RunService")
local userInputs = game:GetService("UserInputService")
local players = game:GetService("Players")
local soundJar = game:GetService("SoundService")

local client = players.LocalPlayer
local camCurrent = workspace.CurrentCamera
local character = client.Character or client.CharacterAdded:Wait()
local rootPart = character:WaitForChild("HumanoidRootPart")
local head = character:WaitForChild("Head")
local sounds = script:WaitForChild("sounds")

camCurrent.CameraSubject = head
camCurrent.CameraType = Enum.CameraType.Custom

-- alignment/rotation force
local bodyGyro = Instance.new("BodyGyro")
bodyGyro.MaxTorque = Vector3.one * (10 * 4^9)
bodyGyro.P = (10 * 7^10)
bodyGyro.D = 250000
bodyGyro.Parent = rootPart

local lastPos = rootPart.Position
local walkSpeed = 4
local groundFriction = 0.85
local airFriction = 0.99 -- keep more momentum in air
local maxVel = 17
local isGrounded = false
local stepTime = 0

local acceptedMaterials = { -- all of the materials that we want to walk on
	Enum.Material.Asphalt,
	Enum.Material.Basalt,
	Enum.Material.Brick,
	Enum.Material.Cobblestone,
	Enum.Material.Concrete,
	Enum.Material.CorrodedMetal,
	Enum.Material.CrackedLava,
	Enum.Material.Fabric,
	Enum.Material.Glass,
	Enum.Material.Glacier,
	Enum.Material.Grass,
	Enum.Material.Ground,
	Enum.Material.Ice,
	Enum.Material.LeafyGrass,
	Enum.Material.Limestone,
	Enum.Material.Marble,
	Enum.Material.Metal,
	Enum.Material.Mud,
	Enum.Material.Neon,
	Enum.Material.Pavement,
	Enum.Material.Pebble,
	Enum.Material.Plastic,
	Enum.Material.Rock,
	Enum.Material.Salt,
	Enum.Material.Sand,
	Enum.Material.Sandstone,
	Enum.Material.Slate,
	Enum.Material.SmoothPlastic,
	Enum.Material.Snow,
	Enum.Material.Wood,
	Enum.Material.WoodPlanks
}

function isFirstPerson() -- firstperson utility
	return (camCurrent.CFrame.Position - head.Position).Magnitude < 0.75
end

function checkGrounded()
	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {character}
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude

	local origin = rootPart.Position
	local direction = Vector3.new(0, -rootPart.Size.Y/2, 0)
	local radius = 0.8
	
	local result = workspace:Spherecast(origin, radius, direction, raycastParams)
	isGrounded = result ~= nil
end

function playSound(sound, parent)
	if not sound then return end

	local realSound = sound:Clone()
	realSound.Parent = parent
	realSound.PlaybackSpeed = math.random(90, 110) / 100
	realSound:Play()

	task.delay(realSound.TimeLength + 0.1, function()
		realSound:Destroy()
	end)
end

-- movement functions
function move()

	-- get forward and right vectors relative to our camera
	local cameraForward = camCurrent.CFrame.LookVector * Vector3.new(1, 0, 1)
	local cameraRight = camCurrent.CFrame.RightVector * Vector3.new(1, 0, 1)
	cameraForward = cameraForward.Unit
	cameraRight = cameraRight.Unit

	-- calculate movement vector
	local movement = Vector3.zero
	if userInputs:IsKeyDown(Enum.KeyCode.W) then movement += cameraForward end
	if userInputs:IsKeyDown(Enum.KeyCode.A) then movement -= cameraRight end
	if userInputs:IsKeyDown(Enum.KeyCode.S) then movement -= cameraForward end
	if userInputs:IsKeyDown(Enum.KeyCode.D) then movement += cameraRight end

	if movement.Magnitude > 1 then
		movement = movement.Unit
	end

	movement *= walkSpeed

	-- apply friction before modifying velocity
	local friction = isGrounded and groundFriction or airFriction
	local velocity = rootPart.Velocity

	velocity = Vector3.new(
		velocity.x * friction,
		velocity.y,  -- Preserve vertical movement
		velocity.z * friction
	)

	-- apply movement force with reduced acceleration in air
	if isGrounded then
		velocity += movement
	else
		velocity += movement * 0.2
	end

	-- clamp final velocity, ensuring diagonal movement is handled separately
	local horizontalSpeed = Vector3.new(velocity.x, 0, velocity.z).Magnitude
	if horizontalSpeed > maxVel then
		local horizontalDirection = Vector3.new(velocity.x, 0, velocity.z).Unit
		velocity = Vector3.new(horizontalDirection.x * maxVel, velocity.y, horizontalDirection.z * maxVel)
	end

	rootPart.Velocity = velocity
end

function rotate()
	local currentPos = rootPart.Position
	local movementDirection = (currentPos - lastPos)

	if movementDirection.Magnitude > 0.1 and not isFirstPerson() then
		local normalizedDirection = movementDirection.Unit
		bodyGyro.CFrame = CFrame.lookAt(
			rootPart.Position, 
			rootPart.Position + Vector3.new(normalizedDirection.x, 0, normalizedDirection.z)
		)

	elseif isFirstPerson() then
		local lookPosition = rootPart.Position + camCurrent.CFrame.LookVector
		bodyGyro.CFrame = CFrame.new(
			rootPart.Position, 
			Vector3.new(lookPosition.X, rootPart.Position.Y, lookPosition.Z)
		)
	end

	lastPos = currentPos
end

function jump(input, chatting) -- self-explanatory, hopefully
	if chatting or input.KeyCode ~= Enum.KeyCode.Space or not isGrounded then return end

	local jumpSound = sounds:WaitForChild("jump")
	playSound(jumpSound, soundJar)

	rootPart.Velocity += Vector3.new(0, 75, 0)
end

function footstep()
	local stepInterval = 0
	local minInterval = 0.25
	local maxInterval = 0.99
	local speed = rootPart.Velocity.Magnitude * 0.86
	
	stepInterval = maxInterval - (speed / maxVel) * (maxInterval - minInterval)
	stepInterval = math.clamp(stepInterval, minInterval, maxInterval)
	-- print(stepInterval)
	
	stepTime += 1/60
	if stepTime < stepInterval or rootPart.Velocity.Magnitude <= 1.5 or not isGrounded then return end

	stepTime = 0
	
	-- perform downwards raycast, if it hits something then play a nifty sound
	local rayParams = RaycastParams.new()
	rayParams.FilterType = Enum.RaycastFilterType.Exclude
	rayParams.FilterDescendantsInstances = {character}

	local origin = rootPart.Position
	local direction = Vector3.new(0, -500, 0)
	local result = workspace:Raycast(origin, direction, rayParams)
	-- print(isGrounded)
	
	if not result then return end
	
	if table.find(acceptedMaterials, result.Material) then
		local materialName = string.lower(result.Material.Name)
		local footstepSound = sounds:WaitForChild(materialName)
		if footstepSound then
			playSound(footstepSound, soundJar)
		else
			playSound(sounds:WaitForChild("plastic"), soundJar) -- default sound
		end
	else
		playSound(sounds:WaitForChild("plastic"), soundJar)
	end
end

-- runtime
runService.PreSimulation:Connect(function()
	checkGrounded()
	move()
	footstep()
end)
runService.RenderStepped:Connect(rotate)
userInputs.InputBegan:Connect(jump)

It seems hard to pinpoint exactly what the root cause of this is, but you probably could add a check when a jump happens and change the velocity or position. I would also try printing when a collision happens and the velocity every time it jumps to try to find the root cause. It could be because of a lot of compounded collisions that trick the system or something else.

You should use LinearVelocity and the AssemblyLinearVelocity property instead of directly accessing Velocity, especially since you are already using a BodyGyro. I can only guess that the instant velocity increase causes your person to fall over, and that gyro with a max torque of 250000 sends you to outer space trying to upright itself