UI & Online · Updated 2552.10.14.17.16
Front-End, Settings, Party & Sessions — Design Doc
How a player goes from launching the game to shooting a friend they invited, and every menu in between. This doc owns the screen flow, the menu inventory, the player-settings model
How a player goes from launching the game to shooting a friend they invited, and every menu in between. This doc owns the screen flow, the menu inventory, the player-settings model, and the EOS-based party/invite/session architecture.
It sits on top of Docs/UISystem.md, which owns the CommonUI plumbing (layer model, base widget classes, controller-input pipeline, footguns). Read that for "how a button focuses on gamepad"; read this for "what screens exist and how multiplayer is wired."
#0. Decisions Locked (2026-06-07)
| Decision | Choice | Notes |
|---|---|---|
| Social / identity / sessions | Epic Online Services (EOS) | Free, store-agnostic friends + invites + lobbies + P2P relay. The only thing that gives real internet "invite a friend." |
| Reach | Internet-primary, LAN also | EOS P2P relay for internet (no port-forwarding); LAN beacon / direct-IP path for same-network. |
| Match hosting (now) | Player-hosted listen server | Party leader hosts. Zero infra/cost. Over EOS P2P it works across the internet with no port forwarding. |
| Match hosting (later) | AWS GameLift, optional | Dedicated servers swap in behind the ISLSessionService abstraction (§7). Party/menu code never changes. |
| Settings storage | Hybrid | Video → UGameUserSettings; rebinds → UEnhancedInputUserSettings; name + gameplay prefs → small USaveGame profile. |
#The two-layer mental model (the thing not to forget)
SOCIAL / PARTY layer → "Who agreed to play together?" → EOS (Lobbies, Friends, Invites)
MATCH-HOSTING layer → "Where does the match actually run?" → Listen server now / GameLift later
EOS is required no matter what. GameLift only ever replaces the hosting layer. We build EOS first and keep hosting behind ISLSessionService so the upgrade is a contained change.
#1. Screen Flow (boot → match)
[Engine/Studio splash]
│
▼
[EOS Login] ── silent if already authed; first run may need account-portal / device-id ──┐
│ (shows "Signing in…"; on fail → offline/LAN-only mode with a retry) │
▼ │
[First-run: Set Display Name] ── only if no profile yet; else skipped ───────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ MAIN MENU (Play · Settings · Profile · Credits · Quit) │
└──────────────────────────────────────────────────────────┘
│ Play
▼
┌──────────────────────────────────────────────────────────┐
│ PARTY / LOBBY (you are leader of a party-of-one) │
│ • Member list + ready states │
│ • Invite Friends ▸ Friends list ▸ send invite │
│ • Leader: pick Map + Mode │
│ • Leader: START MATCH │
└──────────────────────────────────────────────────────────┘
│ Start (leader) ▲ incoming invite (anywhere)
▼ │ toast → Accept → join party
[ISLSessionService.HostMatch] ──────────────────┘
• Internet → create EOS Session, listen server on leader (SocketSubsystemEOS)
• LAN → create LAN session / direct host
│
▼
[ServerTravel to gameplay map] ── party members ClientTravel to host ──
│
▼
┌──────────────────────────────────────────────────────────┐
│ IN MATCH (existing HUD; Pause menu on Start/Esc) │
│ Pause: Resume · Settings · Leave Match │
└──────────────────────────────────────────────────────────┘
│ Leave Match
▼
back to PARTY / LOBBY
Invite-only is enforced structurally: the only way into a party is an invite the recipient accepts. The EOS Lobby is created with JoinViaPresence/invite-only permissions, no public listing, no quick-join. "Only players who agree to play together can play together" falls out of the lobby permission model — there is no code path that puts an un-invited stranger in your match.
#2. Menu Inventory
Each screen maps onto a base class from Docs/UISystem.md §4.3. Stacks (MenuStack, ModalStack) already exist on USLPrimaryGameLayout.
| Screen | Base class | Notes |
|---|---|---|
WBP_SL_LoginScreen | USLScreenWidget | "Signing in…" + retry / offline fallback. Shown only during boot. |
WBP_SL_FirstRunName | USLScreenWidget | First-launch display-name capture. Text entry + confirm. |
WBP_SL_MainMenu | USLScreenWidget | Root. Buttons → push other screens onto MenuStack. |
WBP_SL_PartyLobby | USLScreenWidget | The heart of invite-only. Member list, ready, map/mode, invite, start. |
WBP_SL_FriendsList | USLScreenWidget | EOS friends + presence; "Invite" per row. Uses UCommonListView. |
WBP_SL_Settings | USLTabbedScreenWidget | Tabs: Gameplay · Controls · Video · Audio · Account. |
WBP_SL_PauseMenu | USLScreenWidget | In-match. Resume · Settings · Leave Match. (Already on the build-order list.) |
WBP_SL_Modal_Confirm | USLModalWidget | Quit / Leave / Disband / Discard-settings confirms. |
WBP_SL_KeybindCapture | USLModalWidget | "Press any key…" overlay during rebind. |
WBP_SL_InviteToast | USLGameOverlayWidget | Incoming-invite notification (Accept/Decline). HUD-tier, non-back-handler. |
Settings tab content widgets (children of the tabbed screen's switcher): WBP_SL_Settings_Gameplay, _Controls, _Video, _Audio, _Account.
Settings rows (from UISystem.md §4.3): USLSettingsRow_Toggle, _Slider, _Rotator (UCommonRotator — best for gamepad enum cycling), _KeyBind.
Correction 2026-08-29 — Settings splits into Profiles and Options. The single tabbed
WBP_SL_Settings above (Gameplay · Controls · Video · Audio · Account) mixes per-player and
per-machine settings in one screen, which stops working the moment split-screen exists: video and
scalability are machine-wide (UGameUserSettings), so one player cannot own them. The main menu now
carries two siblings instead:
| > | Screen | Scope | Content |
|---|---|---|---|
| > | --- | --- | --- |
| > | WBP_SL_Profiles (new) | per machine → picks a per-player profile | Pick / create / rename / delete a profile; opens that profile's settings |
| > | WBP_SL_Settings (existing) | per player | Gameplay, Controls — everything in USLPlayerProfileSaveGame. Reached through Profiles. |
| > | WBP_SL_Options (new) | per machine | Video, Scalability, Audio |
The rule that settles future placement: a setting lives where it persists (§3.3). If placement is
ambiguous, the persistence choice is wrong. Full design, including the registry, claiming and naming
rules: Docs/PlayerProfiles.md §8.
#3. Player Settings (the part the user wants early)
Priority: Gameplay/Controls ships first — it carries the Y-invert + sensitivity the user called out. It's also low-risk and independent of EOS, so it delivers value before the hard party work.
#3.1 Settings catalog
Gameplay / Controls
- Look Sensitivity X / Look Sensitivity Y (sliders)
- ADS / Zoom Sensitivity Multiplier (slider)
- Invert Look Y (toggle) — the headline request
- Invert Look X (toggle, less common but cheap)
- Controller: Aim Assist (toggle), Vibration (toggle), Stick Deadzones (sliders), Trigger as ADS hold-vs-toggle
- ADS / Crouch: Hold vs Toggle (rotator)
- Field of View (slider)
Controls (rebinding)
- Per-action KBM + gamepad rebind rows via
UEnhancedInputUserSettings(UE5.3+)
- Reset-to-default (per-row + all)
Video
- Resolution, Window Mode (rotator), VSync, Frame Rate Limit
- Quality preset + scalability groups (View Distance, Shadows, AA, Post, Textures, Effects, Foliage, Shading)
- Brightness / Gamma
Audio
- Master / Music / SFX / Voice / UI (sliders, drive Sound Class volumes)
Account / Profile
- Display Name (editable), EOS account status, Sign Out
#3.2 How Y-invert + sensitivity actually apply
Store on a small profile object the look handler reads every frame:
USLPlayerProfileSaveGame (USaveGame)
float LookSensitivityX = 1.0
float LookSensitivityY = 1.0
float ADSSensitivityMul = 0.6
bool bInvertLookY = false
bool bInvertLookX = false
FString DisplayName
// (audio levels can live here too, or in GameUserSettings — see §3.3)
In ASLPlayerController (or wherever Look is bound), the look input becomes:
Yaw = Raw.X * LookSensitivityX * (bInvertLookX ? -1 : 1) * ADSMul(ifAiming)
Pitch = Raw.Y * LookSensitivityY * (bInvertLookY ? -1 : 1) * ADSMul(ifAiming)
Profile loads on login, applies immediately, and re-applies on every change from the settings screen (broadcast a OnPlayerProfileChanged delegate the controller listens to). No restart, live preview.
Alternative considered: Enhanced Input scalar/negate modifiers driven by the profile. Cleaner in
theory but more moving parts; the direct-multiply path is the "shooting faster" choice. Revisit if
we want per-mapping-context sensitivity curves later.
#3.3 Persistence split
- Video / scalability →
UGameUserSettings(or aUSLGameUserSettingssubclass). Engine
auto-persists to GameUserSettings.ini. Use the built-in ApplySettings / RunHardwareBenchmark.
- Rebinds →
UEnhancedInputUserSettings. Engine persists; gives us mappable configs + save/load
for free.
- Name + gameplay prefs (+ optionally audio) →
USLPlayerProfileSaveGamein one slot.
#4. EOS Architecture (the new dependency)
#4.1 Plugin & subsystem choice
Two routes exist in UE 5.7:
- OSSv1 —
OnlineSubsystemEOS(+OnlineSubsystemEOSPlus): mature, the most tutorials, plays
well with community Advanced Sessions. Recommended for fastest path.
- OSSv2 — "Online Services" (
OnlineServicesEOS): Epic's forward-looking API. Cleaner, but
thinner docs and more churn.
Either way we wrap it in our own USLOnlineSubsystem (game-instance subsystem) so the rest of the game never calls EOS types directly — keeps the OSSv1↔OSSv2 choice swappable and isolates SDK churn. Sub-decision still open — see §9.
#4.2 Setup (one-time)
- Epic Dev Portal: create a Product, Sandbox, Deployment; get Client ID / Client Secret / Encryption
key.
- Enable
OnlineSubsystemEOS+OnlineSubsystemEOSPlusplugins; enableSocketSubsystemEOS.
Config/DefaultEngine.ini:[/Script/OnlineSubsystemEOS.EOSSettings]artifact block + set
DefaultPlatformService=EOS, and [/Script/Engine.Engine] !NetDriverDefinitions / NetDriver to the EOS NetDriver + SocketSubsystemEOS so listen-server traffic rides EOS P2P/relay.
- EOS Dev Auth Tool for logging test accounts into PIE (so two editor instances = two players).
#4.3 Login
- On boot,
USLOnlineSubsystem::Login()→ EOS Auth (Account Portal / Persistent / Device ID for a
no-account guest). Resolve a stable ProductUserId (PUID) + display name.
- Success → continue to Main Menu. Failure → offline mode: LAN/direct only, social features greyed
out, retry button. (Don't hard-block the whole game on EOS being up.)
#4.4 Party = EOS Lobby; Match = EOS Session
- Lobby is the social/party primitive: persistent membership, invites, presence, member
- Create on "Play" (party-of-one) or on accepting an invite.
- Invite:
SendInvite(FriendPUID)→ friend getsOnLobbyInviteReceived→WBP_SL_InviteToast→ - Leader-only controls (map/mode/start) gated on
IsLobbyOwner.
attributes (ready state, chosen character later). Created invite-only (no public search).
Accept → JoinLobby.
- Session is the match registration the host creates at Start. Carries the connection info party
- Pattern: keep the Lobby alive as the party "home"; create a Session for the match; on Leave
members travel to.
Match everyone falls back to the Lobby. (Simpler MVP alternative: use the Lobby itself as the match container and skip a separate Session — fine for first playable.)
#4.5 Getting everyone into the match
Leader presses Start → ISLSessionService::HostMatch(MapName, Mode):
- Host creates the Session (or marks the Lobby in-game) and
ServerTravels to the gameplay map as a
listen server bound to SocketSubsystemEOS.
- Host broadcasts the connect string (the host's PUID-based EOS URL, not an IP) to lobby members
via a lobby attribute / RPC.
- Members
ClientTravelto that EOS URL → join the listen server over P2P/relay. **No port
forwarding** because EOS relays when direct NAT punch fails.
#4.6 LAN path
Same ISLSessionService interface, different implementation: LAN session beacon (or direct "Join by IP / code"). The Party screen offers a "LAN" toggle for the leader; everything downstream (travel, HUD, pause) is identical.
#5. Front-End Map & Layout Ownership (architectural change needed)
Today USLPrimaryGameLayout is created in ASLPlayerHUD::BeginPlay, which is tied to a possessed gameplay pawn. The main menu has no pawn, so this must move up.
Plan: a dedicated front-end map (/Game/SystemLink/Maps/L_FrontEnd) with a minimal front-end GameMode + PlayerController, and the USLPrimaryGameLayout promoted to live at a level that exists with or without a gameplay pawn:
- Preferred: a
UGameInstanceSubsystem(e.g.USLUIManagerSubsystem) owns the root layout and the
push/pop API; both the front-end controller and the in-game ASLPlayerHUD route through it. (See UISystem.md footgun 6.23 — bind OnInputMethodChangedNative in OnLocalPlayerAdded, not Initialize.)
- Lighter interim: a front-end-only HUD/controller that creates the layout, mirroring
ASLPlayerHUD.
This is a prerequisite for any main-menu work and should be sequenced before §6 Phase 1's screens.
#6. Build Order (phased)
Dependency reality: the menu plumbing in Docs/UISystem.md §7 (UI input actions, CommonUI data assets, USLCommonActivatableWidget enhancements, USLButtonBase, pop/clear API) must land before any screen here. Then:
Phase A — Menu foundation (UISystem.md §7 steps 1–5) — prerequisite, already specced there.
Phase B — Front-end shell
- B1.
USLUIManagerSubsystem+ layout-ownership move (§5).
- B2. Front-end map + GameMode/PC.
- B3.
WBP_SL_MainMenu(Play/Settings/Quit wired; Play stubs for now).
- B4. Pause menu (UISystem.md §7.6) — reuses the same screen base in-match.
Phase C — Settings (deliver the Y-invert/sensitivity ask)
- C1.
USLPlayerProfileSaveGame+ load/apply on boot +OnPlayerProfileChanged.
- C2. Sensitivity / Invert-Y wired into the look handler (§3.2). Testable in isolation, no EOS.
- C3.
WBP_SL_Settingstabbed screen + Gameplay/Controls tab + rows.
- C4. Video tab via
UGameUserSettings; Audio tab via Sound Classes.
- C5. Controls/rebind tab via
UEnhancedInputUserSettings.
Phase D — EOS foundation
- D1. Plugins + Dev Portal config + Dev Auth Tool (§4.2).
- D2.
USLOnlineSubsystemwrapper +Login()+ login screen + offline fallback (§4.3).
Phase E — Party & invites (invite-only core)
- E1.
WBP_SL_FriendsList(EOS friends + presence).
- E2. EOS Lobby create/join + invite send/receive +
WBP_SL_InviteToast(§4.4).
- E3.
WBP_SL_PartyLobby(members, ready, leader map/mode, Start disabled until ready).
Phase F — Sessions / getting into a match
- F1.
ISLSessionServiceinterface +USLSessionService_ListenEOS(internet, §4.5).
- F2. Start → host travel → members ClientTravel → everyone in the same match.
- F3. Leave Match → back to lobby.
Phase G — LAN (§4.6) — USLSessionService_LAN behind the same interface.
Phase H — GameLift (optional, later) — USLSessionService_GameLift swaps in for hosting. Needs a dedicated server build target + GameLift Server SDK + a thin backend (Lambda/API Gateway) so the client never holds AWS creds. Party/menu UI unchanged. Use GameLift Anywhere for dev/testing.
Fastest-to-shooting-with-a-friend MVP = A → B(min) → D → E2(min) → F2. Settings (C) can run in
parallel and is what the user wants first, so in practice: A → C → B → D → E → F.
#7. The Hosting Abstraction
// Pseudocode — the seam that lets GameLift replace listen-server later.
class ISLSessionService
{
virtual void HostMatch(FName Map, FName Mode, FOnMatchReady) = 0; // leader calls
virtual void JoinMatch(const FSLConnectInfo&, FOnJoined) = 0; // members call
virtual void LeaveMatch() = 0;
};
// Implementations, chosen at runtime by transport (Internet/LAN) + future config:
USLSessionService_ListenEOS // now: leader hosts over EOS P2P/relay
USLSessionService_LAN // now: LAN beacon / direct IP
USLSessionService_GameLift // later: request a GameLift placement, return its IP:port
Menus and the party system only ever talk to ISLSessionService. Swapping hosting = registering a different implementation; no screen or party code changes.
#8. Open Sub-Decisions
- OSSv1 (
OnlineSubsystemEOS) vs OSSv2 (OnlineServicesEOS) — recommend OSSv1 for docs/speed;
wrapped either way. Confirm before Phase D.
- Separate Session vs Lobby-as-match — MVP can use the Lobby as the match container and add a
real Session later. Decide at Phase F.
- Display name source — EOS account display name vs our own editable profile name. Plan:
default to EOS name, allow override in profile.
- Pause in multiplayer — listen server: pausing stops time for everyone, so the in-match pause
should be a UI overlay that does not call SetGamePaused once we're networked (single-player testing can pause). Matches UISystem.md footgun 6.12.
- Character/loadout select — does it live in the Party screen (pre-match) or in-match? Out of
scope here; flag for a later pass.
#9. Split-Screen (local multiplayer) — TO IMPLEMENT
Decision 2026-07-26: split-screen is a supported target. Nothing implements it yet. This section is the work item; Docs/PlayerProfiles.md covers the profile half in detail.
#Already ready
- Player profiles are per local player.
USLPlayerProfileSubsystemis aULocalPlayerSubsystem, so each
local player owns their own profile, save slot, and OnPlayerProfileChanged. Widgets resolve their own player's profile via GetForWidget. (Before 2026-07-26 this was a game-instance subsystem — P2's invert would have flipped P1's.)
- Menu focus is per local player.
USLMenuFocusSubsystemis scoped the same way, so "exactly one control
lit" is per player, not global.
- GAS is per player already — the ASC lives on
ASLPlayerState.
#What it will require
Design notes, not a queue — the cards are on Docs/KanbanBoard/cards.json.
- Local player creation + viewport split —
UGameplayStatics::CreatePlayer, and the split layout
(UGameViewportClient split types). Decided 2026-09-04: a "press Ⓐ to join" prompt in the main menu's identity zone, and the viewport splits when the match loads, never in the front end — the menu holds SetForceDisableSplitscreen(true) and releases it on travel. A quadrant player-select screen was considered and rejected (it needs a screen between Host and the match, which MainMenuScreen.md §7 rules out). → PlayerProfiles.md §8.15
- Input device → local player mapping. Enhanced Input is per-player; a joining pad must bind to the new
ULocalPlayer. Watch for the KBM player: CD_SL_Xbox is the only registered controller data, and the command bar hides itself on KBM (UISystem.md §"Gamepad-only visibility") — with mixed devices that's now a per-viewport decision.
- Per-player UI layers. Only for GAMEPLAY — the front end stays undivided (item 1), so there is never
more than one menu stack. Verify USLPrimaryGameLayout / ASLPlayerHUD are created per local player and that CommonUI's action router routes Back/Accept to the right viewport. This is the most likely source of surprises — CommonUI is per-player by design, but our HUD build path was written single-player. Done 2026-10-14: a HUD per player via AddToPlayerScreen, and Back routes per player.
- Pause semantics.
bPauseGameWhileActivefreezes the whole game; with two local players one opening a
menu must not freeze the other. Same reasoning as §8.4 for networked play — the pause becomes a UI overlay. Superseded 2026-10-14: a menu takes the WHOLE window (Halo-style) — USLGameViewportClient zero-sizes the other players' views, ASLPlayerHUD collapses their UI, and an input gate swallows their presses until the menu closes. The others cannot see or act, so freezing them costs nothing; only one player can have a menu open at a time.
Profile picking for the guest.Solved 2026-09-02. A joining player is given a generated
profile immediately, so they can play at once and change it later — PlayerProfiles.md §8.14. Guest profiles were dropped with Default.
- HUD/reticle scaling for half-height (or quarter) viewports, and per-player FOV —
FieldOfViewis a
profile value, so it already differs per player once profiles do. Checked 2026-10-14: the HUD reads fine at half height as-is; no scaling needed.
- Test gate: two pads, one player after the other (a menu holds the whole window, item 4): P1 changes a
Settings value and closes; P2 opens Settings, sees P1's change did not reach them, changes a different value and closes; both reopen — each sees only their own.
#10. References
Docs/UISystem.md— CommonUI plumbing, base classes, controller-input pipeline, ~28 footguns.
- EOS: Epic Dev Portal + EOS SDK docs; UE
OnlineSubsystemEOSplugin.
- GameLift (later): AWS GameLift docs; GameLift Anywhere for dev; GameLift Server SDK for UE.
- Reference UI project:
C:\3D-DEV\HaloProject\SystemLink— portable activatable/button/modal/tab
base classes (port, don't copy — UE version gap).