Reference · Updated 2552.10.22.15.22
Teams — spec (SL-41)
Red and Blue, picked in the lobby and carried into the match. This is the first slice of MultiplayerPlan.md §5: team identity and the lobby, nothing that needs match rules. Scoring
Red and Blue, picked in the lobby and carried into the match. This is the first slice of MultiplayerPlan.md §5: team identity and the lobby, nothing that needs match rules. Scoring (SL-37), medals (SL-38) and the lead (SL-39) build on it.
Lobby: everyone is Red or Blue ─ a player switches ─▶ every lobby screen shows the move
└─ host: Start ─▶ hard ServerTravel ─▶ each player arrives on the same team ─▶ a teammate is "friendly"
#1. Decisions
#1.1 Travel stays hard; the team rides in with the login, like the name
A hard ServerTravel destroys every PlayerState, so a team set in the lobby is gone on arrival. Two ways to keep it:
- Seamless travel — PlayerStates survive through
CopyProperties. Needs a transition map, changes every
arrival path the front end relies on (LobbyAndMapSelect.md §1, Footguns → Input), and is a project in itself.
- Login options — every arrival already carries the player's name: the engine adds
ULocalPlayer::GetGameLoginOptions() next to Name= for host players (LocalPlayer.cpp:293), client players (PendingNetGame.cpp:335) and client split-screen joins (LocalPlayer.cpp:382). SL-36 uses exactly this for names.
Decided: login options. USLLocalPlayer remembers its player's team and sends ?Team=Red. The server reads it in InitNewPlayer. No change to how travel works; seamless travel stays a separate decision for the loading-screen work (SL-35).
Each machine is trusted about its own team. Fine among friends on a LAN; the server still decides when the option is missing or invalid.
#1.2 No AGameMode switch yet
MultiplayerPlan.md §5.1(a) wants AGameMode's match states before scoring. Teams need none of it — a team is a property of a player, not of the match. The switch moves to the scoring card (SL-37), where "don't score before the match starts" is the first rule that needs a match state.
Update 2026-10-14: SL-37 (scoring) shipped without it too — a match starts live and never ends yet, so no kill can land "before the match starts". The match phase now belongs to SL-44, which ends the match at the game type's kill goal.
#1.3 Friendly fire: on, full damage
Classic Halo, and the vision lists betrayals as a source of stories (MultiplayerPlan.md §5.4). Full damage is today's behaviour, so SL-41 adds no damage gate. The Off / On / Heal choice arrives with game types (MultiplayerPlan.md §6.3).
#1.4 Players pick freely
Halo custom games let anyone stand on either side; uneven teams are the host's business among friends. The server only balances a player who arrives without a team: they join the side with fewer players, Red on a tie.
#2. Build
| Where | Change |
|---|---|
Public/Types/ESLTeam.h (new) | enum class ESLTeam : uint8 { None, Red, Blue }. None = no team (FFA later). |
SLTeams (Public/Game/SLTeams.h, new) | The world-free rules: teammates, opposite, choose on arrival, the login option's value. Plus AssignOnArrival, which every game mode's InitNewPlayer calls. |
ASLPlayerState | Replicated Team. SetTeam is authority-only. OnRep_Team redraws the lobby's player list and has the player's own machine remember the team. |
ASLPlayerController | Server_RequestTeamSwitch() — flips Red ↔ Blue, lobby only. ReceivedPlayer remembers the team too: a host's own player gets its team at login, before it has a local player to remember it on. |
USLLocalPlayer | Remembers the last team this player was given and returns it from GetGameLoginOptions(). Cleared when the player reaches the main menu (standalone), so the next lobby starts fresh. |
ASLLobbyGameMode and ASLGameModeBase | InitNewPlayer: parse Team; if missing or None, balance (§1.4). |
| Lobby screen | Player rows tinted by team (USLLobbyPlayerRow::OnTeamSet, with the team's UI colour). Ⓧ / E = Switch Team for every local player (§2.1), shown in the command bar and as a glyph on this machine's rows. |
USLWeaponsComponent::ClassifyHit | A same-team character returns ESLAimTarget::Friendly, already reserved for this. The reticle only reddens on Hostile, so it stays plain over a teammate. Melee lunge only locks onto Hostile, so it no longer lunges at teammates. |
USLTeamDataAsset (new, DA_SL_Teams) | Every team's display name, UI colour (lobby rows, later the HUD) and armour colour in one asset. Project Settings → System Link → System Link Teams points at the active one (USLTeamSettings); a game type may point at its own later. |
|---|---|
ASLCharacterBase | ArmorColorParameter (Class Defaults, default ArmorColor) names the armour material's colour parameter — it belongs to that character's material, so it lives on the character. ApplyArmorColor paints TPMesh and FPMesh from OnPlayerStateChanged, and again on ASLPlayerState::OnTeamChanged and OnArmorColorChanged. The colour comes from SLPlayerColors::GetArmorColor: the team's on a team, the player's profile colour on None (PlayerProfiles.md §8.12, SL-48). Runs on every machine. A body keeps its colour after death. |
Armour and UI colours are separate on purpose: a colour that reads well lit on armour is rarely the value that reads well flat on a menu row.
Not built: IGenericTeamAgentInterface (MultiplayerPlan.md §5.2). It lives in AIModule, which this plugin does not use; add it when bots arrive.
Out of SL-41: team-tagged spawns (MultiplayerPlan.md §5.5), nameplates, team score. Each is visible on its own and gets its own card when it is next.
#2.1 Ⓧ switches the team of whoever pressed it
The front end is one screen owned by player one, so CommonUI routes only player one's pad to it. A guest's Ⓧ never reaches the lobby's bound actions. Two paths, one per kind of player:
- Player one: the lobby binds
IA_SL_UI_Edit(Ⓧ, E) as a CommonUI action labelled Switch Team. That
is also what puts the prompt in the command bar.
- Split-screen guests:
USLLocalJoinSubsystemalready catches every pad's Ⓧ below CommonUI, by
controller id. It is how player two opens their own profile with Ⓧ on the main menu (OnProfileEditRequested). The lobby subscribes to it, finds the local player for that controller id, and asks for that player's switch.
- The catcher also sees player one's Ⓧ and E, so the lobby ignores its own owning player's controller id
there. Otherwise player one would switch twice — straight back.
The row glyph. Each lobby row on this machine shows the Ⓧ glyph, so a player knows the button is theirs. A row is this machine's when PlayerState->GetPlayerController() is non-null and IsLocalController(): a client only holds its own players' controllers, while the host holds everyone's, so the local check is what filters there. Each such row is owned by that player (CreateWidget with their controller), because UCommonActionWidget takes its icon from its owning player's input device — owned by player one, a guest's row would show player one's keyboard key. The glyph also needs a key to show, and keys come only from live mapping contexts, so every local player gets IMC_SL_UI in the front end (ASLPlayerController::ReceivedPlayer), not just the one whose menu owns input.
Both paths end in the pressing player's own Server_RequestTeamSwitch. A host's local players run it directly; a client's go to the server.
⚠ OnProfileEditRequested now means "a joined player pressed Ⓧ", and each screen gives it its own meaning. The header comment says so; the name stays, because the main menu Blueprint binds it.
⚠ OnRep_Team on another machine's player must not assume a valid ASC (Footguns → GAS tag callbacks).
#3. Testing SL-41
- Automation: balance (3 arrivals → 2/1), attitude (same team friendly, other hostile,
Nonehostile to
all), and the login option round-trip (Team=Blue in → Blue out; garbage in → balanced).
- Solo: Host → you are Red → Switch Team → Blue on the lobby row → Start → back to the menu and host
again → you are Red again (teams start fresh).
- Split-screen (Lobby map, one window, a second pad joined): player two's Ⓧ switches player two only;
player one's Ⓧ switches player one only, once. Player one's pad still drives the lobby afterwards. Both rows show the glyph; player one on keyboard shows E while player two's row still shows the pad's Ⓧ.
- Glyph over the network: in the two-player run below, each window shows the glyph on its own row only.
- Two players (PIE, Play As Listen Server, 2 players, starting on the Lobby map): the client arrives
Blue. Either player switches and both screens show it. Put both on Red → Start → each player's reticle stays plain over the other, not red, and both wear red armour in first and third person. One kills the other (friendly fire on) → after respawn both are still Red.
#4. Editor setup for SL-41
The C++ is built. Five steps:
DA_SL_Teams— Content Browser → Miscellaneous → Data Asset → SLTeamDataAsset, saved in
/Game/SystemLink/Data/. It starts on red and blue; tweak the colours here at any time.
- Project Settings → System Link → System Link Teams → Team Data =
DA_SL_Teams.
WBP_SL_LobbyPlayerRow— override On Team Set and tint the row with Team Color (e.g. the
background bar's Brush Color, or Color and Opacity).
WBP_SL_Lobby— Class Defaults → Switch Team Input Action =IA_SL_UI_Edit.
WBP_SL_LobbyPlayerRow— add a Common Action Widget named exactlySwitchTeamGlyphwhere the
glyph should sit. Leave its Input Action empty: the lobby fills it from step 4. It is optional, so a row without it just shows no glyph.
Compile and save the Blueprints before pressing Play.