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.
Mental model
Section titled “Mental model”Requests and slots
Section titled “Requests and slots”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.
Ranking
Section titled “Ranking”When there are more requests than slots, requests rank by:
Priority, higher first (default 0).- 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. - Request order, first come first served, so exact ties are stable.
Ranking runs on these events, with no polling:
addof 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’sDestroying. If the request held a slot, the best waiting request takes it.- A re-
addthat changesPriority, which re-ranks everything throughrefresh(). refresh(), which re-ranks every request by where the camera is now: the topBudgetshow, 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).
Restyling
Section titled “Restyling”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.
Endings you did not ask for
Section titled “Endings you did not ask for”- The adornee is destroyed: its
Destroyingends the request and frees the slot. - Other code destroys the Highlight (a
ClearAllChildrenon 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 youremoveit.TagWatch’sonRemovedis the place to do that.
One manager for the client
Section titled “One manager for the client”--!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 200end
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 slotfunction 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 OutlineControllerRestyle, check and remove
Section titled “Restyle, check and remove”Marks:add(chestModel, { OutlineColor = Color3.new(1, 1, 1) }) -- same adornee: restyled in placeprint(Marks:isShown(chestModel)) -- false while it waits for a slotMarks:remove(chestModel) -- the Highlight is destroyed, and the best waiting request takes the slotAPI reference
Section titled “API reference”| 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.