Skip to content

Colour and how an entity looks

ContrastPolicy and the APCA measurement behind it, EntityDisplay and EntityPalette, and the one hex parser everything shares.

All documentation — Reference
On this page

An authored colour is user data. Nothing here changes one unless you ask by name — and when you do ask, you are told exactly what happened to it.

ContrastPolicy

Whether an authored colour may be lightened or darkened for readability, and against what background.

Pure: hex in, hex out, no engine types. A result is worth holding rather than recomputing per frame.

Member
static ContrastPolicy OffThe default. The author’s colour, always untouched and never measured.
static ContrastPolicy ReadableOn(string backgroundHex, double minLc = DefaultMinLc)Enforce readability on a background
const double DefaultMinLc60
bool IsOff · string? BackgroundHex · double MinLc
ContrastResult Apply(string? foregroundHex)
static double? Lc(string? textHex, string? backgroundHex)The signed Lc of one colour on another
static bool IsSafeHex(string? hex)
static bool TryParseHex(string? hex, out byte r, out byte g, out byte b, out byte a)

Readability is measured with APCA, not WCAG 2 relative luminance — which under-weights how dark a saturated colour reads.

backgroundHex takes #RGB, #RGBA, #RRGGBB or #RRGGBBAA. Alpha is ignored in the measurement — APCA models opaque colours, so a host drawing a translucent panel must pass the colour the player actually sees.

minLc is a magnitude, never a signed value.

Apply never throws for any foreground value — null, empty, arbitrary text. Every refusal comes back as an outcome.

ContrastResult

Member
Outcome
HexThe colour to draw, or null
LcThe signed APCA Lc against the policy’s background
HasColorThere is a colour to draw. Says nothing about whether it is readable.
IsReadableIt clears the threshold

For Off and Unchanged, Hex is the same string instance you passed in — casing and short form included. Nothing is rewritten in place.

Lc is zero when nothing was measured, which is every outcome that has no background. Read Outcome first.

ContrastOutcome

Member
NoColorNothing was authored. Absent, which is not the same as black.
InvalidHexNot a colour this can read, so it is refused rather than coerced into a default that would look authored
OffThe policy is Off. Untouched and unmeasured.
UnchangedIt already cleared the threshold, and comes back exactly as authored
AdjustedIt was lightened or darkened until it cleared
UnreachableIt cannot clear the threshold on this background, not even at pure white or pure black. Hex is the author’s colour, unchanged; nothing is substituted.

Unreachable is the one worth surfacing to whoever authored the colour — it is a choice the project has to make, not one the runtime can make for it.

The hex allow-list

IsSafeHex is the one allow-list, and TmpMarkup uses it too, because an authored colour can be any string that reached the project’s data.

TryParseHex expands #RGB and #RGBA by doubling each digit and reads #RRGGBB and #RRGGBBAA as written. Alpha defaults to 255.

EntityDisplay

How one entity is drawn: a name, a colour, a portrait — read off the entity the runtime has already resolved.

Member
static EntityDisplay Of(ResolvedActor actor, ContrastPolicy? contrast = null)From what you are already holding, with no lookup
static EntityDisplay Of(ResolvedEntity entity, ContrastPolicy? contrast = null)
Entity · Id · CodeThe code is stable across a rename, and is what codegen names
NameOverlaid, language-resolved, current
Color · HasColor · ColorHex · ColorOutcome
Color ColorOr(Color fallback)
MediaRef? Portrait
bool IsActionThe entity is an input action: its definition draws a mention of it as a keycap, and Code is what your game maps to its own action
MentionLook ToMentionLook()

Name never falls back to the code, so an entity with no name draws as empty. Both readings — the $name intrinsic first, then the property — are field-shaped, carrying the translated value and the Localized flag.

Color is meaningless when HasColor is false, where it is white: the identity tint, not a value to draw as though it were authored. Prefer ColorOr, which names your default at the call site.

ColorHex is what TmpMarkup wants — a formatted line takes a string and never a Color.

Portrait is the payload’s own media reference, not a Sprite. Pass it to MediaAddress.From and load it through MediaLibrary. It reads the $portrait intrinsic first, else the entity’s picture id as a reference with no URL, which the media layer can still resolve.

ToMentionLook() is how an @mention of an entity draws with the same name and colour as its name plate. For an input action it also marks the look — IsAction, with the entity’s code as ActionCode — and never carries a glyph: the player’s current binding is your game’s, and your mention hook adds it. See an input action’s glyph.

EntityPalette

Where an EntityDisplay comes from when the host does not already hold the entity: one store, one policy, one answer.

Member
EntityPalette(INodeViewContext view, ContrastPolicy? contrast = null)
ContrastPolicy ContrastThe one policy every display from this palette was built with
EntityDisplay Of(ResolvedActor actor) · Of(ResolvedEntity entity)No lookup
EntityDisplay? ByCode(string? code)By the author’s code — what your own script and the generated code know
EntityDisplay? ById(string? id)By id — what a payload reference and an @-mention carry
MentionLook? ResolveMention(string entityId)Hand it straight to TmpMarkup.Format. A mention of an input action comes back marked as one, with no glyph — wrap it to add one

Everything here reads the runtime’s resolved entities, so a renamed or translated character can never be current in one place and stale in another.

Two entities sharing a code resolve first-wins, with one duplicate-entity-code diagnostic at load.

The null rule

It has two halves, and it is consistent across this whole layer:

  • a verb that takes something you are already holding throws on null;
  • a verb that looks one up — ById, ByCode — answers null.

Nothing returns a blank display, which would be indistinguishable from an entity the author left unfilled. ResolveMention answering null for an unknown id is the same rule, and it lets TmpMarkup fall through its own chain rather than drawing nothing.

UnityColors

The hex-to-Color edge, and nothing else.

Member
bool TryParse(string? hex, out Color color)Color.white on a refusal — the identity tint, not a value to draw
Color Or(string? hex, Color fallback)
string ToHex(Color color, bool withAlpha = false)Uppercase

ToHex uses the same formatter the contrast policy does, so a colour adjusted by the policy and one written here are the same string.

The channels are sRGB values written straight into Color, as Unity’s own utility does. In a linear-colour-space project Unity converts at upload — do not gamma-correct here.

Written for
Unity developers · Advanced integrators
Last updated