Drawing & Visuals
Two systems for rendering visuals: the executor Drawing API for fast 2D screen overlays, and Roblox Instances (Highlights, BillboardGuis) for 3D-attached visuals. Each has its place.
Executor Drawing API
The Drawing API draws directly to the screen — lines, circles, text, boxes. It's immediate, fast, and works even when Roblox's UI is hidden.
-- Line
local line = Drawing.new("Line")
line.Visible = true
line.From = Vector2.new(0, 0)
line.To = Vector2.new(500, 500)
line.Color = Color3.fromRGB(255, 0, 0)
line.Thickness = 2
line.Transparency = 1 -- 1 = fully opaque in Drawing API
-- Circle
local circle = Drawing.new("Circle")
circle.Visible = true
circle.Position = Vector2.new(400, 300)
circle.Radius = 50
circle.Color = Color3.fromRGB(0, 255, 0)
circle.Thickness = 2
circle.Filled = false -- outline only
circle.NumSides = 64 -- smoothness
-- Square (bounding box)
local box = Drawing.new("Square")
box.Visible = true
box.Position = Vector2.new(100, 100) -- top-left corner
box.Size = Vector2.new(50, 80)
box.Color = Color3.fromRGB(255, 255, 0)
box.Thickness = 1
box.Filled = false
-- Text
local label = Drawing.new("Text")
label.Visible = true
label.Position = Vector2.new(200, 150)
label.Text = "Enemy"
label.Size = 16 -- font size
label.Color = Color3.fromRGB(255, 255, 255)
label.Outline = true
label.OutlineColor = Color3.fromRGB(0, 0, 0)
label.Center = true -- center horizontally at Position
Remove a drawing object when done:
line:Remove()
circle:Remove()
World to Screen Conversion
To draw on screen at a 3D object's position, convert the world position:
local function worldToScreen(worldPos)
local cam = workspace.CurrentCamera
local screenPos, onScreen = cam:WorldToViewportPoint(worldPos)
return Vector2.new(screenPos.X, screenPos.Y), onScreen, screenPos.Z
end
-- Use it:
local enemyPos = enemyHRP.Position + Vector3.new(0, 3, 0) -- slightly above
local screenPos, visible, depth = worldToScreen(enemyPos)
if visible and depth > 0 then
-- Draw a dot at the enemy's screen position
espDot.Position = screenPos
espDot.Visible = true
else
espDot.Visible = false
end
Box ESP with Drawing API
A classic box ESP draws a rectangle around an entity on screen. To get the 2D bounding box from a 3D character:
local function getBoundingBox(character)
local hrp = character:FindFirstChild("HumanoidRootPart")
if not hrp then return nil end
local cam = workspace.CurrentCamera
local pos = hrp.Position
-- Approximate character height: center is hrp, top is +3, bottom is -3
local topScreen, topVis = cam:WorldToViewportPoint(pos + Vector3.new(0, 3.5, 0))
local botScreen, botVis = cam:WorldToViewportPoint(pos + Vector3.new(0, -3.5, 0))
if not topVis or topScreen.Z <= 0 then return nil end
local height = math.abs(topScreen.Y - botScreen.Y)
local width = height * 0.65 -- approximate aspect ratio
return {
x = topScreen.X - width/2,
y = topScreen.Y,
width = width,
height = height,
}
end
-- Draw the box
local box = Drawing.new("Square")
box.Thickness = 1
box.Filled = false
game:GetService("RunService").Heartbeat:Connect(function()
local char = workspace:FindFirstChild("EnemyName")
local bb = char and getBoundingBox(char)
if bb then
box.Position = Vector2.new(bb.x, bb.y)
box.Size = Vector2.new(bb.width, bb.height)
box.Color = Color3.fromRGB(255, 0, 0)
box.Visible = true
else
box.Visible = false
end
end)
Highlight Instances (3D)
Highlights render a colored silhouette over a 3D model. They work for both visible and occluded objects:
local highlight = Instance.new("Highlight")
highlight.FillColor = Color3.fromRGB(255, 0, 0) -- body fill color
highlight.FillTransparency = 0.5 -- 0 = solid, 1 = invisible
highlight.OutlineColor = Color3.fromRGB(255, 255, 255) -- outline color
highlight.OutlineTransparency = 0 -- 0 = solid outline
highlight.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop -- show through walls
highlight.Adornee = enemyCharacter -- what to highlight
highlight.Parent = game:GetService("CoreGui") -- must be in CoreGui to persist
The Adornee is the Model or Part to highlight. Set it to a character model to highlight the full body.
Always parent Highlights to CoreGui, not workspace. If parented to workspace or a character, Roblox's replication system destroys it when the server replicates changes.
Cleaning up:
highlight:Destroy()
BillboardGui Name Labels
BillboardGui attaches a 2D UI panel to a 3D position in the world, always facing the camera:
local function createLabel(adornee, text)
local billboard = Instance.new("BillboardGui")
billboard.Name = "ESPLabel"
billboard.Size = UDim2.new(0, 120, 0, 30)
billboard.StudsOffset = Vector3.new(0, 3, 0) -- offset above the adornee
billboard.AlwaysOnTop = true -- show through walls
billboard.Adornee = adornee -- attach to this part
billboard.Parent = game:GetService("CoreGui")
local label = Instance.new("TextLabel")
label.Size = UDim2.fromScale(1, 1)
label.BackgroundTransparency = 1
label.Text = text
label.TextColor3 = Color3.fromRGB(255, 255, 255)
label.TextScaled = true
label.Font = Enum.Font.GothamBold
label.Parent = billboard
return billboard
end
-- Usage: create a label above an enemy's head
local enemy = workspace.Enemies:FindFirstChild("Boss")
if enemy then
local hrp = enemy:FindFirstChild("HumanoidRootPart")
local label = createLabel(hrp, "Boss — 5000 HP")
end
Health Bar
Add a health bar to the BillboardGui:
local function createHealthBar(adornee, maxHp)
local billboard = Instance.new("BillboardGui")
billboard.Size = UDim2.new(0, 80, 0, 8)
billboard.StudsOffset = Vector3.new(0, 2.5, 0)
billboard.AlwaysOnTop = true
billboard.Adornee = adornee
billboard.Parent = game:GetService("CoreGui")
-- Background (red = missing HP)
local bg = Instance.new("Frame")
bg.Size = UDim2.fromScale(1, 1)
bg.BackgroundColor3 = Color3.fromRGB(150, 0, 0)
bg.BorderSizePixel = 0
bg.Parent = billboard
-- Fill (green = current HP)
local fill = Instance.new("Frame")
fill.Size = UDim2.fromScale(1, 1) -- start full
fill.BackgroundColor3 = Color3.fromRGB(0, 200, 0)
fill.BorderSizePixel = 0
fill.Parent = bg
-- Update function
local function updateHP(current)
local ratio = math.clamp(current / maxHp, 0, 1)
fill.Size = UDim2.fromScale(ratio, 1)
-- Color shift: green → yellow → red
if ratio > 0.6 then
fill.BackgroundColor3 = Color3.fromRGB(0, 200, 0)
elseif ratio > 0.3 then
fill.BackgroundColor3 = Color3.fromRGB(220, 180, 0)
else
fill.BackgroundColor3 = Color3.fromRGB(220, 50, 0)
end
end
return billboard, updateHP
end
Cleanup Pattern
Always track your visual objects so you can clean them up:
local visuals = {} -- store all Drawing objects and Instances
-- Add visuals
local h = Instance.new("Highlight")
h.Parent = CoreGui
table.insert(visuals, h)
local d = Drawing.new("Text")
table.insert(visuals, d)
-- Cleanup function
local function cleanupAll()
for _, v in visuals do
if typeof(v) == "Instance" then
v:Destroy()
else
v:Remove() -- Drawing API objects use :Remove()
end
end
visuals = {}
end