there are ways vector force is the right option if you want simplicity, but In the SBB game you said I would bet they use angular velocity so differing from it I’m not 100% sure it will be “smooth”. Like blocks might work but you could just make the block collision off and weld it to a ball but its corners will go into the ground.
Here are 2 options
Note:
It’s ok not to understand 100% of the whole code Like I worked on a fps game before and using cframes I found it very complicated I low key just used AI for it so as long as you get the gist of the code then you should be fine and guessing you have not finished school like me you will understand it eventually. also I’m going to spill the beans I am using AI for this response I do copy and paste but I will try to understand it to the full potential
Option A (no angular math): push the ball with VectorForce
Let friction make it roll. You just apply a force in the move direction and clamp speed.
-- LocalScript (StarterPlayerScripts)
local Players = game:GetService("Players")
local UIS = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local cam = workspace.CurrentCamera
local ball = workspace:WaitForChild("Ball")
-- grippy, not bouncy (so it rolls instead of sliding)
pcall(function()
ball.CustomPhysicalProperties = PhysicalProperties.new(1, 0.8, 0, 0, 1)
end)
local att = ball:FindFirstChildOfClass("Attachment") or Instance.new("Attachment", ball)
local vf = ball:FindFirstChildOfClass("VectorForce") or Instance.new("VectorForce", ball)
vf.Attachment0 = att
vf.RelativeTo = Enum.ActuatorRelativeTo.World
local move = Vector3.zero
local function setKey(k, down)
local d = down and 1 or -1
if k == Enum.KeyCode.W then move += Vector3.new(0,0,-1)*d end
if k == Enum.KeyCode.S then move += Vector3.new(0,0, 1)*d end
if k == Enum.KeyCode.A then move += Vector3.new(-1,0,0)*d end
if k == Enum.KeyCode.D then move += Vector3.new( 1,0,0)*d end
end
UIS.InputBegan:Connect(function(i,g) if not g then setKey(i.KeyCode, true) end end)
UIS.InputEnded:Connect(function(i,g) setKey(i.KeyCode, false) end)
local ACCEL = 80 -- how hard you push (studs/s^2)
local MAX_SPEED = 60
RunService.RenderStepped:Connect(function()
local dir = Vector3.new(move.X,0,move.Z)
if dir.Magnitude > 0 then
dir = cam.CFrame:VectorToWorldSpace(dir)
dir = Vector3.new(dir.X,0,dir.Z).Unit
-- F = m * a, use AssemblyMass so it feels consistent
vf.Force = dir * (ball.AssemblyMass * ACCEL)
else
vf.Force = Vector3.zero
end
-- simple speed cap
local v = ball.AssemblyLinearVelocity
if v.Magnitude > MAX_SPEED then
ball.AssemblyLinearVelocity = v.Unit * MAX_SPEED
end
end)
Pros: super simple; no cross product; works great if friction is high.
Cons: on very slippery surfaces it may slide before it “catches”.
Option B (keep AngularVelocity, hide the math)
Make a tiny helper so you never look at the formula again.
-- one helper
local function rollTowards(av, dir, targetSpeed, radius)
if dir.Magnitude < 1e-3 then
av.AngularVelocity = Vector3.zero
return
end
dir = dir.Unit
-- internally still uses cross, but you don't deal with it elsewhere
local axis = Vector3.yAxis:Cross(dir) -- which way to spin
local spin = targetSpeed / math.max(0.01, radius) -- how fast to spin (ω = v/r)
av.AngularVelocity = axis * spin
end
Use it like:
local radius = ball.Size.X * 0.5
local targetSpeed = 40
-- each frame:
local dir = Vector3.new(move.X,0,move.Z)
if dir.Magnitude > 0 then
dir = cam.CFrame:VectorToWorldSpace(dir)
dir = Vector3.new(dir.X,0,dir.Z)
rollTowards(av, dir, targetSpeed, radius)
else
av.AngularVelocity = Vector3.zero
end
Pick whichever style you like: VectorForce keeps it math-free; the helper keeps AngularVelocity but hides the details.