The mental model behind RCE: combat states, loadouts, layers, and how they flow through the rendering pipeline.
At the heart of RCE is a reactive state machine with four combat categories. Each category holds its own independent set of shapes that define the reticle's appearance for that state. When the active state changes, RCE handles the transition animation automatically.
| Category | Enum Value | Typical Use |
|---|---|---|
| HUD | LoadoutState.HUD |
Always-on elements. Shapes in this category render continuously regardless of the active combat state. Use for static crosshairs, compass widgets, or persistent UI anchors. |
| Situational Awareness | LoadoutState.SituationalAwareness |
The idle/scanning state. Typically a wider, lower-profile reticle for general navigation. Active when no weapon actions are occurring. |
| Hip Fire | LoadoutState.HipFire |
Firing from the hip. Often a tighter, more dynamic reticle with spread indicators or animated elements that react to fire rate. |
| Aim Down Sight | LoadoutState.AimDownSight |
Precision aiming. Usually the most detailed reticle — stadia marks, fine crosshairs, ranging indicators. |
HUD layers render on top of every state. The other three categories (SA, HF, ADS) are mutually exclusive — only one is active at a time, and switching between them triggers a category transition.
In addition to the four standard categories, RCE provides a Global category for transient effects like hit markers, damage flashes, and kill confirmations. Global layers render on a dedicated renderer instance, completely isolated from the main reticle pipeline. This means global animations are never interrupted by category transitions. See the GlobalFX API for runtime control.
A loadout is the top-level container for all reticle configuration. It holds:
CategoryData per state (HUD, SA, HF, ADS, Global), each containing a list of layersLoadouts are serialized as versioned JSON (currently schema v8) and stored on disk. The ILoadoutStorage interface handles persistence, and the LoadoutSerializer manages migration between schema versions automatically.
Because loadouts are plain JSON files, they're inherently modding-friendly. Players or community tools can create and share loadout files. RCE's version migration ensures older loadouts always load correctly in newer engine versions.
Each category contains an ordered list of layers. A layer represents a single SDF shape (circle, polygon, lineburst, etc.) with its own set of properties:
| Property Group | What It Controls |
|---|---|
| Shape Type | The SDF primitive — Circle, Ring, Polygon, LineBurst, Chevron, Stadia, ChamferBox, or Custom |
| Transform | Position (X, Y), rotation, scale (X, Y) |
| Color | Fill color with alpha, plus separate outline color |
| Glow | Intensity and size of the outer glow emanating from the shape boundary |
| Outline | True outline: a separate colored border rendered outside the shape fill |
| Feather | Edge softness. 0 = hard pixel-perfect edges, higher = softer anti-aliasing |
| Hollow | Donut/ring mode: thickness of the hollow interior |
| Gap | Gap mode (Radial, Grid, Lines, CenterCutout) with inversion and SDF geometry options |
| Shape Params | Type-specific parameters (polygon sides, lineburst count, stadia marks, etc.) |
| Animations | Per-layer property animations — see Animations |
Layers are rendered back-to-front in list order. Each layer occupies one ShapeData slot in the GPU structured buffer.
Layers can be combined into boolean groups using union or subtraction operations. A group consists of a base layer (the primary shape) and one or more operand layers that modify it.
Because these operations happen at the SDF level, glow and outline effects follow the true combined boundary of the group, not the individual shape edges. This means glow pools naturally in concavities and outlines wrap around the merged silhouette — a significant visual quality advantage over simple alpha masking.
In the editor, operand layers are shown indented under their base layer. Collapsing a group hides its operands. Deleting a base layer removes the entire group. The Link Operands toggle makes transform changes on the base layer propagate to all operands as a parent-child relationship.
Understanding the data flow helps when debugging or extending RCE. Here's how a frame gets from your game state to pixels on screen:
SetActiveState() calls and emits the current category's shapes as an array of ShapeData structs.ReticleSDF fragment shader evaluates every shape's SDF, applies glow, outline, feathering, gaps, and boolean operations, then composites the result.For Global category layers, a parallel pipeline exists: LoadoutManager.EmitGlobalShapes() → GlobalFXBridge (visibility filtering) → ShapePropertyAnimator → a dedicated second ReticleRenderer instance. This keeps global effects fully isolated from combat state rendering.
Every shape in RCE is defined mathematically as a signed distance field. For any point on screen, the SDF function returns the distance from that point to the nearest shape boundary: negative values are inside, positive values are outside, and zero is exactly on the edge.
This representation has several advantages over sprite-based approaches:
fwidth() function provides screen-space adaptive feathering that produces uniform edge quality regardless of shape size.min(a, b), subtraction is max(a, -b). These operations compose cleanly because they operate on the distance field itself.For custom freeform shapes, RCE computes an exact Bézier SDF using a compute shader that evaluates the distance to cubic Bézier curves analytically — no approximation or rasterization. The result is stored in a texture array and sampled in the same fragment shader pass as all other shapes.