Files
ue-toolchain/plugins/ue-design-skills/skills/ue-game-settings-architecture/SKILL.md
T
ue-toolchain dab3f35079 feat(skills): ship ue-design-skills bundle, licensing and delivery gate
Phase 0 of the handoff plan, as a marketplace rather than a flat skills/
directory. Content moved out of the LyraResearch archive and depersonalised:
addresses stay in the archive, recipes ship.

- plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the
  six required fields; catalog.json as the harness-neutral source of truth and
  .claude-plugin/ as one adapter over it.
- _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a
  declared file cannot silently miss the line rules.
- ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split
  licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata).
- LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as
  the material the licence decision grew from.

Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their
fixtures with a clean baseline and 2 root files reaching the line rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:48:55 +07:00

12 KiB

name, description
name description
ue-game-settings-architecture Design or review game settings and performance architecture in Unreal Engine: setting model trees separated from widgets, reflection-backed property paths, declarative edit conditions, apply and cancel as a transaction, machine-local versus player-shared persistence, scalability and console variables and device profiles, hardware benchmark wiring, and sampled performance statistics. Use when adding video, audio, input or accessibility settings, cloud saves, quality presets, frame-rate modes, device tuning or performance overlays.

UE game settings architecture

The invariant:

A setting is a model with persistence, validation and application semantics. A widget is one view of that model.

Measured patterns from a reference product: patterns. Detection recipes: failure modes.

Settings look like the least architectural part of a game and are one of the most: they span a UI framework, a reflection layer, two persistence stores, the console-variable system, device profiles and the engine's scalability tables. Every one of those boundaries fails silently.

Related skills: ue-data-driven-architecture, ue-ui-architecture, ue-input-architecture, ue-modular-gameplay.

Method, not architecture — how to check any claim in this bundle before repeating it: ue-evidence-discipline.


1. Separate model, registry and presentation

registry (per local player)
└─ collections and pages
   └─ setting models
      ├─ data source: getter and setter
      ├─ display text and description
      ├─ edit conditions
      ├─ options and validation
      └─ apply / store-initial / restore lifecycle

visual data asset
└─ setting model class → entry widget class

The screen renders a tree of models through a class-to-widget map; it does not hand-build a widget per option. The payoff: one model renders in several screens, search and grouping operate on models, edit conditions are testable without any UI, and settings code depends on no concrete widget class.

Resolve the widget by walking the model's superclass chain, so mapping one base class covers every derived setting and a specific override remains possible.


2. Reflection-backed data sources, and their one cost

A setting stores a path to a getter and setter rooted at the local player, and resolves it through reflection:

local player → GetLocalSettings() → GetShadowQuality / SetShadowQuality

This removes a class per setting. It also removes the compiler from the relationship.

The cost, stated precisely

A macro that stringifies a function name checks that the name exists. It does not check the return type, the parameter count, or constness. Those failures surface as a runtime assertion during registry initialization — and assertions are compiled out of shipping, so in a shipping build a broken path is a setting that silently does nothing.

Three consequences worth designing around:

  1. validate every path during registry initialization and fail loudly with the setting's developer name, not with a reflection error;
  2. run that validation in an automated test, not only when a developer opens the screen;
  3. prefer explicit binding where conversion is complex, where several values change atomically, or where the setter has side effects worth reading.

Recipe: GS-01.


3. Edit conditions are declarative dependencies

Availability depends on platform traits, primary-player status, other settings, capabilities, privileges, input device and build configuration. Express it in the model, never in widget visibility.

Return more than a boolean:

editable
disabled (reason shown to the player)
hidden (reason recorded for the developer)
option disabled (one entry removed from a list)
killed (hidden, not resettable, not reported)

A condition also declares which settings it depends on, so that changing one setting re-evaluates the others.

The two-reason split is the part worth copying. A disabled setting shows the player why; a hidden one records why for whoever later asks where it went. Most hand-built settings screens have neither.


4. Persistence: machine-local versus player-shared

Machine-local Player-shared
Holds resolution, quality, frame rate, device profile, benchmark result accessibility, sensitivities, subtitles, language, bindings
Backed by engine user settings, local ini per-player save game, cloud-syncable
Scope one per machine one per player

The ownership test

  1. Should this follow the user to another machine?
  2. Can two local players need different values at once?
  3. Does it change global renderer or audio device state?
  4. Can a secondary local player safely apply it?

The leak to watch for

A per-player accessor that returns a process-global singleton is the standard way this abstraction breaks. It compiles, it reads naturally at every call site, and it silently makes split-screen impossible for every value behind it. The symptom in a codebase that has it: a proliferation of "only the primary player may edit this" conditions, which is the architecture apologising for itself.

Recipe: GS-03. If you find it, the fix is not to remove the conditions.


5. Apply and cancel is a transaction

  1. store initial values on open;
  2. edits update the model, and optionally preview;
  3. apply commits and invokes application;
  4. cancel restores initial values and reapplies the prior runtime state;
  5. reset-to-defaults is another staged mutation, not an immediate save.

Classify application timing deliberately

Setting Timing
volume, UI preview immediate, restored on cancel
scalability channel preview or apply, per policy
resolution and window mode apply with a confirmation countdown
key binding staged profile plus collision check
restart-required save now, mark pending

Applying a heavyweight global change from inside a change notification is a decision, not a default. If it is done because staging was not written yet, mark it as such — and treat a comment saying "for now" on a global apply path as a finding rather than a note. Recipe: GS-05.


6. From setting to console variable

The preferred path keeps a typed object in the middle:

model → typed settings setter → staged quality levels
→ apply → engine scalability API → expanded console variables
→ clamped by device profile

A typed object gives range validation, persistence, transactional restore, platform clamping, change notification and a test seam. A widget writing a console variable directly gives none of them.

When a setting genuinely maps to a custom variable: resolve and cache safely, set a defined priority source, clamp, restore on cancel, and document shipping availability. Never write to a read-only variable and assume it took.


7. Device profiles are constraints, not presets

platform default → base profile → mode suffix → user request
→ platform clamp → effective value

Keep requested and effective separate, and be able to explain to the player why a request was clamped.

Profile selection commonly composes a name from parts — base, mode, user choice — and falls back through progressively shorter combinations until one exists. That is a good pattern with one specific hazard: a composition part that is declared, read, and never assigned. The fallback then silently always takes the shorter branch, and the feature the part represents does not exist while appearing to. Recipe: GS-07.


8. Wire the benchmark, do not merely implement it

can run? → run benchmark → map indices through thresholds
→ set quality and resolution scale → save and apply

A function named "should this run at startup" with no caller means startup autodetection does not exist, however complete the rest of the pipeline is.

Decide explicitly: automatic on first launch or manual only; how hardware changes are detected; whether user overrides survive; what happens on failure. Then add a call-site test, not only a unit test of the threshold mapping — the mapping was never the part that was missing. Recipe: GS-08.


9. Performance statistics: collect once, present many

engine frame data → one consumer → latest values + sampled ring buffers → widgets

Rules that keep this honest:

  • one collector, never one per widget;
  • a fixed-capacity ring buffer with a documented sample cadence — a buffer size without a cadence describes no window at all;
  • units written down: milliseconds, frames per second, bytes per second, percent;
  • an unavailable statistic returns unavailable, never zero;
  • availability in shipping is a deliberate setting.

A frame-rate counter is not evidence about rendering cost. Use it to notice, not to conclude.


10. Reapply after composition changes

Global scalability and frame pacing may need reapplying after a game mode or feature set finishes loading, because features can change device profile, console variables and frontend policy.

Do it once, at one named lifecycle point, after the composition is fully known — not from each feature. The same entry point should serve the other trigger that changes the same state, such as a hotfixed device profile. Two causes, one reaction, one idempotent function.


11. Guard engine-struct assumptions

Code that enumerates every member of an engine struct — copying, clamping or maxing quality levels field by field — should carry a compile-time size guard with a message telling the upgrader what to review.

A failing size assertion after an engine upgrade is a feature. It is the only thing standing between a newly added quality channel and being silently ignored by five functions. Update the guard only after reviewing every one of them, and know how many there are before you start. Recipe: GS-09.


12. Review checklist

  • Setting is a model; the widget is a view.
  • Stable developer name is separate from localized display text.
  • Every property path is validated at initialization and in a test.
  • Machine-local versus player-shared ownership is explicit per value.
  • No per-player accessor returns a global singleton.
  • Apply and cancel restore runtime state, not only stored values.
  • Expensive apply operations have deliberate, documented timing.
  • Requested and effective values are distinguishable.
  • Direct console-variable writes are justified and restorable.
  • The benchmark has a real caller and a stated policy.
  • Statistics are collected once, sampled at a documented cadence.
  • Unavailable is distinct from zero.
  • Reapplication after composition change is centralized and idempotent.
  • Engine-struct assumptions carry compile-time guards.

13. When to simplify

A prototype can bind widgets straight to engine user settings. Adopt a model registry when at least one holds: platforms hide or clamp settings differently; apply and cancel are required; several local users need separate preferences; search, categories or dynamic conditions matter; gamepad navigation needs reusable rows; settings span ini, save games, console variables and subsystems.

Do not adopt a five-thousand-line framework for three toggles. Adopt it when the settings screen has become an application subsystem rather than a panel.


Provenance

The findings come from a source audit of Epic's Lyra Starter Game on Unreal Engine 5.6 and the settings plugin it ships with — roughly 5700 lines of framework plus 2100 of project-specific settings backends, read rather than run. The framework itself contains no reference to the game and is the most portable component examined in the whole research project; the findings below are about its wiring, not its design.

Source addresses stay in the research archive. Each entry carries a stable identifier (GS-01, GS-03, …) resolving back to the audited location there.

Evidence boundary

One project, one engine version. Concrete widget assets are binary and were not read, so claims here concern the model layer and its bindings. Values quoted from configuration describe that project on that day.