Best way to clamp marble velocity?

I’m making a player-controlled marble thing, and I’m having a bit of trouble coming up with a solution to clamp the maximum force applied to the marble. I think I have something that works, however, controlling the marble becomes finnicky when it actually executes, it becomes hard to steer and starts veering off in inaccurate directions.

Not really sure what else I can try here. Thanks.

local function clampForce(forceToAdd) --//Prevents too much velocity from being applied
	local wasClampingNeeded = false
	local totalForce = marble.AssemblyLinearVelocity
	
	--//Check each axis to determine if too much velocity is applied
	local clampedAxes = {totalForce.X, totalForce.Y, totalForce.Z}
	
	for i = 1, 3 do
		local axis = (i == 1 and "X") or (i == 2 and "Y") or "Z"
		local currentVel = math.abs(totalForce[axis])
		
		if currentVel > MAX_VELOCITY then
			local newVel = (totalForce.Unit*MAX_VELOCITY)[axis]
			
			wasClampingNeeded = true 
			clampedAxes[i] = newVel
		end
	end
	
	if wasClampingNeeded then
		marble.AssemblyLinearVelocity = Vector3.new(unpack(clampedAxes))
	end
end

^ clamping code that executes every step

isn’t this easier?

local function clampVector(vector)
	local dist = vector.Magnitude / MAX_VELOCITY
	return vector.Unit * (dist < 1 and dist or 1)
end

and faster. Unless you want a specific clamp for each axis

1 Like

Is there a particular reason why you’re clamping each axis independently? When you do it that way your marble will move faster diagonally than it will on a straight axis because the valid space your velocity can be is the shape of a cube instead of a sphere.

Usually when I do clamps like this I just set the vector to its unit * maxvel.

local MAX_VEL = 8
local function getClampedVel(curVel)
    return curVel.Unit * math.min(MAX_VEL, curVel.Magnitude)
end

I usually remove the Y axis because I don’t usually clamp gravity though. And putting Y in there can mess with your controls if you only have X,Z controls.

local MAX_VEL = 8
local function getClampedXZVel(curVel)
    local yVel = Vector3.new(0,curVel.Y,0)
    curVel*= Vector3.new(1,0.0000001,1) --Can't be zero so that .Unit will never have Vector3.new()
    return curVel.Unit * math.min(MAX_VEL, curVel.Magnitude) + yVel
end

It’s worth noting that when I tested your code in my little test place, my marble controlled just fine which might mean it is an issue elsewhere.

My marble code

I just put this in starterPlayerScripts in a blank baseplate. I have removed your code and just used my version of clamping, but your code worked just fine.

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

local INPUT_VEL_PER_SECOND = 60
local MAX_VELOCITY = 35

local character = game.Players.LocalPlayer.Character
while not character do wait() character = game.Players.LocalPlayer.Character end
character.Parent = game.Lighting

local marble = Instance.new("Part")
marble.Shape = Enum.PartType.Ball
marble.TopSurface = Enum.SurfaceType.Smooth
marble.BottomSurface = Enum.SurfaceType.Smooth
marble.Size = Vector3.new(4,4,4)
marble.Position = Vector3.new(0,5,0)
marble.Parent = game.Workspace


local control = function(dt)
	local inputDirection = Vector3.new(
		(UserInputService:IsKeyDown(Enum.KeyCode.A) and 1 or 0) + (UserInputService:IsKeyDown(Enum.KeyCode.D) and -1 or 0),
		0,
		(UserInputService:IsKeyDown(Enum.KeyCode.S) and -1 or 0) + (UserInputService:IsKeyDown(Enum.KeyCode.W) and 1 or 0)
	)
	
	local additionalForce = inputDirection * INPUT_VEL_PER_SECOND * dt
	
	--marble.AssemblyLinearVelocity+= additionalForce --unclamped
	local projectedVelocity = marble.AssemblyLinearVelocity + additionalForce
	marble.AssemblyLinearVelocity = projectedVelocity.Unit * Vector3.new(1,0,1) * math.min(MAX_VELOCITY, projectedVelocity.Magnitude) + Vector3.new(0, projectedVelocity.Y, 0)
	
	local projectedVelocity = marble.AssemblyLinearVelocity + additionalForce
	local yProjection = projectedVelocity.Y
	projectedVelocity*=Vector3.new(1,-0.000001,1)
	marble.AssemblyLinearVelocity = projectedVelocity.Unit * math.min(MAX_VELOCITY, projectedVelocity.Magnitude) + Vector3.new(0, yProjection, 0)
end

local camera = game.Workspace.CurrentCamera
camera.CameraType = Enum.CameraType.Scriptable

local cam = function()
	camera.CFrame = CFrame.new(marble.Position + Vector3.new(0,10,-25), marble.Position)
end

RunService:BindToRenderStep("control", Enum.RenderPriority.Character.Value, control)
RunService:BindToRenderStep("cam", Enum.RenderPriority.Camera.Value + 1, cam)

Huh. I left my computer for a moment and came back and the marble disappeared because of a Nan error. I’m too lazy to find it right now, but that’s a thing that can happen apparently (despite the fact I thought I fixed it). Weirdly it also removed one of the ramps in my test place despite the fact my code doesn’t affect those.

2 Likes