
Key Required
Ball Tracer Trajectory
Lua Script•215 lines (7258 chars)
-- Blox League ball tracer: thin line showing where the ball will go, including bounces.
-- Press + (numpad + or = key) to toggle.
if getgenv().BallTracerCleanup then pcall(getgenv().BallTracerCleanup) end -- remove a previous run
local RunService = game:GetService("RunService")
local UIS = game:GetService("UserInputService")
local cam = workspace.CurrentCamera
local CFG = {
Enabled = true,
Color = Color3.fromRGB(130, 205, 255),
Thickness = 2,
SimStep = 1 / 40, -- seconds per drawn segment
MaxTime = 3, -- how far ahead to predict
MaxBounces = 3,
Gravity = 34, -- measured from the real ball's flights (world gravity is 196, ball is floaty)
FloorBounce = 0.65,
WallBounce = 0.6,
CornerRadius = 10, -- radius of the curved floor/wall/ceiling corners
GoalHalfWidth = 40, -- rough goal opening, estimated from the goal model
GoalHeight = 30,
GoalDepth = 300,
MinSpeed = 6, -- hide the line when the ball is basically still
Window = 0.15, -- seconds of ball history used to estimate velocity (bigger = steadier, slower to react)
GroundY = 5.6, -- ball centre below this = rolling on the floor (no gravity in the speed fit)
HitThreshold = 8, -- studs of surprise movement that count as a hit/bounce and reset the estimate
}
local MAX_PTS = math.ceil(CFG.MaxTime / CFG.SimStep) + 2
-- Drawn with GUI frames (the Drawing library does not render on every setup)
local gui = Instance.new("ScreenGui")
gui.Name = "BallTracer"
gui.IgnoreGuiInset = true
gui.ResetOnSpawn = false
gui.DisplayOrder = 999
gui.Parent = (gethui and gethui()) or game:GetService("CoreGui")
local lines = {}
for i = 1, MAX_PTS do
local f = Instance.new("Frame")
f.AnchorPoint = Vector2.new(0.5, 0.5)
f.BorderSizePixel = 0
f.Visible = false
f.Parent = gui
lines[i] = f
end
local conns = {}
getgenv().BallTracerCleanup = function()
for _, c in ipairs(conns) do c:Disconnect() end
gui:Destroy()
getgenv().BallTracerCleanup = nil
end
local function hideFrom(n)
for i = n, MAX_PTS do lines[i].Visible = false end
end
local function setLine(f, a, b)
local d = b - a
local len = d.Magnitude
f.Position = UDim2.fromOffset((a.X + b.X) / 2, (a.Y + b.Y) / 2)
f.Size = UDim2.fromOffset(len + 1, CFG.Thickness) -- +1 hides hairline gaps between segments
f.Rotation = math.deg(math.atan2(d.Y, d.X))
f.BackgroundColor3 = CFG.Color
f.Visible = true
end
local BOX = { min = Vector3.new(-205.8, 0, -257), max = Vector3.new(205.8, 103.7, 257) }
-- Ball velocity: the ball is anchored (physics velocity reads 0), so estimate it from position history.
-- Least-squares line through the last CFG.Window seconds after removing gravity's curve, which averages
-- out frame-time noise. A sudden unexpected jump (hit / bounce / reset) clears the history so it reacts fast.
local hist = {}
local vel = Vector3.zero
local base = nil -- smoothed ball position at "now"
local function updateState(ball, now)
local p = ball.Position
local grounded = p.Y < CFG.GroundY
local G = Vector3.new(0, grounded and 0 or -CFG.Gravity, 0)
local fresh = false
local last = hist[#hist]
if last then
local dt = now - last.t
if dt <= 0 then return end
local pred = last.p + vel * dt + G * (0.5 * dt * dt)
if (p - pred).Magnitude > CFG.HitThreshold or (p - last.p).Magnitude > 60 then
hist = {}
fresh = true
end
end
hist[#hist + 1] = { t = now, p = p }
while #hist > 2 and now - hist[1].t > CFG.Window do table.remove(hist, 1) end
base = p
local n = #hist
if n < 2 or now - hist[1].t < 0.015 then return end
local sumT, sumP = 0, Vector3.zero
local pcs = table.create(n)
local taus = table.create(n)
for i, h in ipairs(hist) do
local tau = h.t - now
local pc = h.p - G * (0.5 * tau * tau) -- remove gravity so the rest is a straight line
taus[i], pcs[i] = tau, pc
sumT += tau
sumP += pc
end
local tm, pm = sumT / n, sumP / n
local sxx, sxy = 0, Vector3.zero
for i = 1, n do
local dtau = taus[i] - tm
sxx += dtau * dtau
sxy += (pcs[i] - pm) * dtau
end
if sxx < 1e-9 then return end
local newVel = sxy / sxx
vel = (fresh or n <= 3) and newVel or vel:Lerp(newVel, 0.5)
base = pm - newVel * tm -- fitted position at "now" (steadier than the raw one)
end
local function project(p)
local v = cam:WorldToViewportPoint(p)
return Vector2.new(v.X, v.Y), v.Z
end
local pts = table.create(MAX_PTS)
local function step(now)
local bodies = workspace:FindFirstChild("Bodies")
local ball = bodies and bodies:FindFirstChild("Ball")
if not CFG.Enabled or not ball then
hideFrom(1)
hist = {}
return
end
cam = workspace.CurrentCamera
updateState(ball, now)
local radius = math.max(ball.Size.X, ball.Size.Y, ball.Size.Z) / 2
local p, v = base or ball.Position, vel
if v.Magnitude < CFG.MinSpeed then hideFrom(1) return end
local g = Vector3.new(0, -CFG.Gravity, 0)
local R = CFG.CornerRadius
local bounces, count, t = 0, 1, 0
pts[1] = p
while t < CFG.MaxTime and count < MAX_PTS do
local nv = v + g * CFG.SimStep
local np = p + (v + nv) * 0.5 * CFG.SimStep
-- The arena has no collidable/queryable parts, so collide with a rounded box:
-- every edge and corner is a quarter-pipe, so the ball rides up and along walls.
local bmin, bmax = BOX.min, BOX.max
if math.abs(np.X) < CFG.GoalHalfWidth and np.Y < CFG.GoalHeight then
bmin = Vector3.new(bmin.X, bmin.Y, -CFG.GoalDepth)
bmax = Vector3.new(bmax.X, bmax.Y, CFG.GoalDepth)
end
local lo = bmin + Vector3.one * (radius + R)
local hi = bmax - Vector3.one * (radius + R)
local q = Vector3.new(math.clamp(np.X, lo.X, hi.X), math.clamp(np.Y, lo.Y, hi.Y), math.clamp(np.Z, lo.Z, hi.Z))
local d = np - q
local dist = d.Magnitude
if dist > R and dist > 1e-6 then
local n = d / dist
local vn = nv:Dot(n)
if vn > 0 then
np = q + n * R
local e = CFG.WallBounce + (CFG.FloorBounce - CFG.WallBounce) * math.abs(n.Y)
nv = nv - n * vn * (1 + e)
if vn > 15 then bounces += 1 end -- a real impact, not just rolling along the curve
end
end
count += 1
pts[count] = np
p, v = np, nv
t += CFG.SimStep
if bounces > CFG.MaxBounces then break end
if v.Magnitude < CFG.MinSpeed and bounces > 0 then break end
end
-- project each point once, then draw segments between them (clipping at the camera plane)
local scr, depth = table.create(count), table.create(count)
for i = 1, count do scr[i], depth[i] = project(pts[i]) end
local used = 0
for i = 1, count - 1 do
local a2, b2, ad, bd = scr[i], scr[i + 1], depth[i], depth[i + 1]
if ad > 0 or bd > 0 then
if ad <= 0 then
a2 = project(pts[i]:Lerp(pts[i + 1], (0.1 - ad) / (bd - ad)))
elseif bd <= 0 then
b2 = project(pts[i]:Lerp(pts[i + 1], (0.1 - ad) / (bd - ad)))
end
used += 1
setLine(lines[used], a2, b2)
end
end
hideFrom(used + 1)
end
table.insert(conns, RunService.RenderStepped:Connect(function()
step(os.clock())
end))
table.insert(conns, UIS.InputBegan:Connect(function(input, gpe)
if gpe then return end
local k = input.KeyCode
if k == Enum.KeyCode.KeypadPlus or k == Enum.KeyCode.Equals then
CFG.Enabled = not CFG.Enabled
print("[Tracer] " .. (CFG.Enabled and "ON" or "OFF"))
end
end))
getgenv().BallTracerCFG = CFG
print("[Tracer] loaded, press + to toggle")Features & Information
Small little ball trajectory for Blox League.
Community Comments (0)
No comments yet for this script.
