UI & Online · Updated Jun 16, 2026
Front-End, Settings, Party & Sessions — Design Doc
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, 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.
#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. 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).