Skip to content

Highlights

One shared client manager for Highlight instances that keeps them under the engine's 255 limit, ranks requests by priority and camera distance, and destroys outlines instead of disabling them.

Realm
Client
Require
ReplicatedStorage.ChloeKernel.Highlights
Since
v0.9.0
Source
Highlights.luau

Highlights owns every outline on a client. A client renders at most 255 Highlight instances and silently ignores the rest, so a game that outlines pickups, teammates, interactables and a boss runs out of slots without any error. One Highlights manager, shared by every system that outlines things, keeps the count under a budget, gives the slots to the requests that matter most, and hands a freed slot to the best request waiting for one. Pair it with TagWatch to outline tagged instances only while they are in the world, and with the Scheduler when slots should follow the camera.

Why not one Highlight per part, toggled with Enabled

Section titled “Why not one Highlight per part, toggled with Enabled”

The common pattern is to put a Highlight in every pickup and flip Enabled when it should show. It fails in two ways:

  • A disabled Highlight still takes one of the 255 slots. Three hundred pickups with disabled Highlights mean the outline you enable on the boss never renders, and nothing tells you why.
  • A Highlight per part multiplies the count. One Highlight on a Model outlines every part under it; one per part burns a slot for each limb.

So in this module a Highlight exists only while its request holds a slot. It is created on demand and destroyed, never disabled, when the request ends or loses its slot. Requests are keyed by adornee: pass the Model, not each of its parts.

add(target, style) records a request for target and returns whether it shows now. A request that shows gets a new Highlight with the request’s style, Adornee = target, parented under the target so it leaves the world with it, and Archivable = false so a clone of the adornee never carries an unmanaged copy.

Budget (default 200) caps how many Highlights the manager keeps alive. The default leaves 55 slots of headroom for Highlights created outside the manager. It must be 0-255; Highlights.new asserts that.

When there are more requests than slots, requests rank by:

  1. Priority, higher first (default 0).
  2. Distance from the camera to the target’s pivot, nearer first. A target that is not a PVInstance, or no camera, counts as infinitely far.
  3. Request order, first come first served, so exact ties are stable.

Ranking runs on these events, with no polling:

  • add of a new target. If a slot is free, it shows. Otherwise it takes the slot of the worst shown request, if it outranks it; the displaced Highlight is destroyed and that request waits.
  • remove, or the target’s Destroying. If the request held a slot, the best waiting request takes it.
  • A re-add that changes Priority, which re-ranks everything through refresh().
  • refresh(), which re-ranks every request by where the camera is now: the top Budget show, the rest wait.

Distance is read only when ranking runs. As the player walks, slots do not move to nearer targets until something triggers a re-rank. Call refresh() from a Scheduler job that runs only while more is requested than shown (see the example below).

Adding a target that already has a request updates it in place: each style key you pass overwrites the stored one and is written to the live Highlight. Keys you leave out keep their current value; there is no way to reset one key to the engine default short of remove and add.

  • The adornee is destroyed: its Destroying ends the request and frees the slot.
  • Other code destroys the Highlight (a ClearAllChildren on the adornee): the request ends with it rather than fighting that code by recreating it.
  • The adornee streams out without Destroy: the request stays until you remove it. TagWatch’s onRemoved is the place to do that.
src/Client/Game/Controllers/OutlineController.luau
--!strict
-- OutlineController: the client's one Highlights manager, loot outlines and the boss.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Highlights = require(ReplicatedStorage.ChloeKernel.Highlights)
local TagWatch = require(ReplicatedStorage.ChloeKernel.TagWatch)
local OutlineController = {
Name = "OutlineController",
}
local LootStyle = { OutlineColor = Color3.fromRGB(255, 220, 90), FillTransparency = 1 }
function OutlineController.init(self: any, kernel: any)
self.Kernel = kernel
self.Marks = Highlights.new() -- Budget 200
end
function OutlineController.start(self: any)
-- Outlines only for loot in the world; stored templates stay bare
self.StopLoot = TagWatch.watch("Loot", function(model)
self.Marks:add(model, LootStyle)
self:_followCamera()
end, function(model)
self.Marks:remove(model)
end)
end
-- Re-rank by camera distance once a second, only while something is waiting for a slot
function OutlineController._followCamera(self: any)
local Shown, Requested = self.Marks:count()
if self.RankJob or Requested <= Shown then
return
end
self.RankJob = self.Kernel.Scheduler:every(1, function()
self.Marks:refresh()
local NowShown, NowRequested = self.Marks:count()
if NowRequested <= NowShown then
self.RankJob:cancel()
self.RankJob = nil
end
end)
end
-- Other systems call this through getService("OutlineController")
function OutlineController.markBoss(self: any, boss: Model)
self.Marks:add(boss, {
FillColor = Color3.new(1, 0, 0),
DepthMode = Enum.HighlightDepthMode.Occluded,
Priority = 10, -- outranks every pickup, whatever the distance
})
end
function OutlineController.stop(self: any)
if self.StopLoot then
self.StopLoot()
end
if self.RankJob then
self.RankJob:cancel()
end
self.Marks:destroy()
end
return OutlineController
Marks:add(chestModel, { OutlineColor = Color3.new(1, 1, 1) }) -- same adornee: restyled in place
print(Marks:isShown(chestModel)) -- false while it waits for a slot
Marks:remove(chestModel) -- the Highlight is destroyed, and the best waiting request takes the slot
Member Description
Highlights.new(options?) -> Highlights options = { Budget?, Camera? }. Budget is the number of Highlights alive at once, 0-255, default 200. Camera overrides the distance origin (anything with a CFrame); default workspace.CurrentCamera, read at ranking time.
marks:add(target, style?) -> boolean Requests an outline on a Model or BasePart. A second call for the same target restyles it and, on a new Priority, re-ranks. Returns whether it shows now. Errors after destroy().
marks:remove(target) Ends the request and destroys its Highlight. The best waiting request takes the freed slot. Does nothing for an unknown target.
marks:isShown(target) -> boolean Whether the target holds a slot now.
marks:count() -> (number, number) (shown, requested).
marks:refresh() Re-ranks every request by the current camera position: the top Budget show, the rest wait.
marks:clear() Ends every request and destroys every Highlight.
marks:destroy() clear(), then refuses further add calls.
Member Description
FillColor: Color3? Written to the Highlight when set.
FillTransparency: number? Written to the Highlight when set.
OutlineColor: Color3? Written to the Highlight when set.
OutlineTransparency: number? Written to the Highlight when set.
DepthMode: Enum.HighlightDepthMode? AlwaysOnTop or Occluded. Written when set.
Priority: number? Higher keeps its slot first. Default 0.

Keys left out keep the engine’s Highlight defaults on a new outline.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.