Ball Tracer Trajectory
Key Required

Ball Tracer Trajectory

Blox Leaguebilo0obilo0o 1.5K views 1.5K executesUpdated Oct 1, 2026
Unrated
00
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.
#Blox League#Keyless#ESP#Tracer

Community Comments (0)

No comments yet for this script.