-
Notifications
You must be signed in to change notification settings - Fork 561
feat: client-authoritative character select #401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
fernando-cortez
wants to merge
7
commits into
develop
from
feature/client-authoritative-character-select
Closed
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9160bb2
wip for locally reactive char select
fernando-cortez f11ce95
fixing locked in scenario, removed unused enum
fernando-cortez f518b4e
Merge branch 'develop' into feature/client-authoritative-character-se…
fernando-cortez 8ea30f4
more detailed remarks/comments, player seats array renamed, valid che…
fernando-cortez a3ccce3
merge develop (taking develop char select scene)
fernando-cortez 7890226
char select scene references fixed
fernando-cortez b1e97d5
removed local variable usage on onlistevent callback
fernando-cortez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Git LFS file not shown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using UnityEngine; | ||
using UnityEngine.UI; | ||
using UnityEngine.SceneManagement; | ||
using TMPro; | ||
using Unity.Netcode; | ||
|
@@ -19,66 +18,69 @@ public class ClientCharSelectState : GameStateBehaviour | |
/// </summary> | ||
public static ClientCharSelectState Instance { get; private set; } | ||
|
||
public override GameState ActiveState { get { return GameState.CharSelect; } } | ||
public override GameState ActiveState => GameState.CharSelect; | ||
|
||
public CharSelectData CharSelectData { get; private set; } | ||
|
||
[SerializeField] | ||
[Tooltip("This is triggered when the player chooses a character")] | ||
private string m_AnimationTriggerOnCharSelect = "BeginRevive"; | ||
string m_AnimationTriggerOnCharSelect = "BeginRevive"; | ||
|
||
[SerializeField] | ||
[Tooltip("This is triggered when the player presses the \"Ready\" button")] | ||
private string m_AnimationTriggerOnCharChosen = "BeginRevive"; | ||
string m_AnimationTriggerOnCharChosen = "BeginRevive"; | ||
|
||
[Header("Lobby Seats")] | ||
[SerializeField] | ||
[Tooltip("Collection of 8 portrait-boxes, one for each potential lobby member")] | ||
private List<UICharSelectPlayerSeat> m_PlayerSeats; | ||
List<UICharSelectPlayerSeat> m_UIPlayerSeats; | ||
|
||
[System.Serializable] | ||
[Serializable] | ||
public class ColorAndIndicator | ||
{ | ||
public Sprite Indicator; | ||
public Color Color; | ||
} | ||
|
||
[Tooltip("Representational information for each player")] | ||
public ColorAndIndicator[] m_IdentifiersForEachPlayerNumber; | ||
|
||
[SerializeField] | ||
[Tooltip("Text element containing player count which updates as players connect")] | ||
private TextMeshProUGUI m_NumPlayersText; | ||
TextMeshProUGUI m_NumPlayersText; | ||
|
||
[SerializeField] | ||
[Tooltip("Text element for the Ready button")] | ||
private TextMeshProUGUI m_ReadyButtonText; | ||
TextMeshProUGUI m_ReadyButtonText; | ||
|
||
[Header("UI Elements for different lobby modes")] | ||
[SerializeField] | ||
[Tooltip("UI elements to turn on when the player hasn't chosen their seat yet. Turned off otherwise!")] | ||
private List<GameObject> m_UIElementsForNoSeatChosen; | ||
List<GameObject> m_UIElementsForNoSeatChosen; | ||
|
||
[SerializeField] | ||
[Tooltip("UI elements to turn on when the player has locked in their seat choice (and is now waiting for other players to do the same). Turned off otherwise!")] | ||
private List<GameObject> m_UIElementsForSeatChosen; | ||
List<GameObject> m_UIElementsForSeatChosen; | ||
|
||
[SerializeField] | ||
[Tooltip("UI elements to turn on when the lobby is closed (and game is about to start). Turned off otherwise!")] | ||
private List<GameObject> m_UIElementsForLobbyEnding; | ||
List<GameObject> m_UIElementsForLobbyEnding; | ||
|
||
[SerializeField] | ||
[Tooltip("UI elements to turn on when there's been a fatal error (and the client cannot proceed). Turned off otherwise!")] | ||
private List<GameObject> m_UIElementsForFatalError; | ||
List<GameObject> m_UIElementsForFatalError; | ||
|
||
[Header("Misc")] | ||
[SerializeField] | ||
[Tooltip("The controller for the class-info box")] | ||
private UICharSelectClassInfoBox m_ClassInfoBox; | ||
UICharSelectClassInfoBox m_ClassInfoBox; | ||
|
||
[SerializeField] | ||
Transform m_CharacterGraphicsParent; | ||
|
||
private int m_LastSeatSelected = -1; | ||
private bool m_HasLocalPlayerLockedIn = false; | ||
int m_LastSeatSelected = -1; | ||
|
||
bool m_HasLocalPlayerLockedIn; | ||
|
||
GameObject m_CurrentCharacterGraphics; | ||
|
||
|
@@ -92,17 +94,17 @@ public class ColorAndIndicator | |
/// an abstraction that makes it easier to configure which UI elements should | ||
/// be enabled/disabled in each stage of the lobby. | ||
/// </summary> | ||
private enum LobbyMode | ||
enum LobbyMode | ||
{ | ||
ChooseSeat, // "Choose your seat!" stage | ||
SeatChosen, // "Waiting for other players!" stage | ||
LobbyEnding, // "Get ready! Game is starting!" stage | ||
FatalError, // "Fatal Error" stage | ||
} | ||
|
||
private Dictionary<LobbyMode, List<GameObject>> m_LobbyUIElementsByMode; | ||
Dictionary<LobbyMode, List<GameObject>> m_LobbyUIElementsByMode; | ||
|
||
private void Awake() | ||
void Awake() | ||
{ | ||
Instance = this; | ||
CharSelectData = GetComponent<CharSelectData>(); | ||
|
@@ -118,9 +120,9 @@ private void Awake() | |
protected override void Start() | ||
{ | ||
base.Start(); | ||
for (int i = 0; i < m_PlayerSeats.Count; ++i) | ||
for (int i = 0; i < m_UIPlayerSeats.Count; ++i) | ||
{ | ||
m_PlayerSeats[i].Initialize(i); | ||
m_UIPlayerSeats[i].Initialize(i); | ||
} | ||
|
||
ConfigureUIForLobbyMode(LobbyMode.ChooseSeat); | ||
|
@@ -158,23 +160,45 @@ public override void OnNetworkSpawn() | |
/// Called when our PlayerNumber (e.g. P1, P2, etc.) has been assigned by the server | ||
/// </summary> | ||
/// <param name="playerNum"></param> | ||
private void OnAssignedPlayerNumber(int playerNum) | ||
void OnAssignedPlayerNumber(int playerNum) | ||
{ | ||
m_ClassInfoBox.OnSetPlayerNumber(playerNum); | ||
} | ||
|
||
private void UpdatePlayerCount() | ||
void UpdatePlayerCount() | ||
{ | ||
int count = CharSelectData.LobbyPlayers.Count; | ||
var pstr = (count > 1) ? "players" : "player"; | ||
m_NumPlayersText.text = "<b>" + count + "</b> " + pstr +" connected"; | ||
var count = CharSelectData.LobbyPlayers.Count; | ||
m_NumPlayersText.text = "<b>" + count + "</b> " + "player(s) connected"; | ||
} | ||
|
||
/// <summary> | ||
/// Called by the server when any of the seats in the lobby have changed. (Including ours!) | ||
/// </summary> | ||
private void OnLobbyPlayerStateChanged(NetworkListEvent<CharSelectData.LobbyPlayerState> changeEvent) | ||
/// <remarks> | ||
/// This method serves as the conflict resolution of all player seats. A seat change request is initially sent | ||
/// by a client via a ServerRPC (see OnPlayerClickedSeat method), the server modifies a NetworkList, and that | ||
/// list change event is replicated to all clients. | ||
/// Since the local player's seat is modified inside this class' OnPlayerChangedSeat method directly when a UI | ||
/// element is selected, a NetworkListEvent that contains any local seat change will be simply ignored. | ||
/// </remarks> | ||
void OnLobbyPlayerStateChanged(NetworkListEvent<CharSelectData.LobbyPlayerState> changeEvent) | ||
{ | ||
// ignore state changes for the local player unless the change event is a locked in event, or when seat | ||
// has been invalidated (both server-authoritative) | ||
if (changeEvent.Value.ClientId == NetworkManager.Singleton.LocalClientId) | ||
{ | ||
var isLockedInEvent = | ||
(changeEvent.Value.SeatState == CharSelectData.SeatState.LockedIn && | ||
changeEvent.PreviousValue.SeatState != CharSelectData.SeatState.LockedIn) || | ||
(changeEvent.Value.SeatState == CharSelectData.SeatState.Active && | ||
changeEvent.PreviousValue.SeatState == CharSelectData.SeatState.LockedIn); | ||
|
||
if (!isLockedInEvent && changeEvent.Value.IsValid()) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
UpdateSeats(); | ||
UpdatePlayerCount(); | ||
|
||
|
@@ -216,7 +240,7 @@ private void OnLobbyPlayerStateChanged(NetworkListEvent<CharSelectData.LobbyPlay | |
/// </summary> | ||
/// <param name="state">Our current seat state</param> | ||
/// <param name="seatIdx">Which seat we're sitting in, or -1 if SeatState is Inactive</param> | ||
private void UpdateCharacterSelection(CharSelectData.SeatState state, int seatIdx = -1) | ||
void UpdateCharacterSelection(CharSelectData.SeatState state, int seatIdx = -1) | ||
{ | ||
bool isNewSeat = m_LastSeatSelected != seatIdx; | ||
|
||
|
@@ -279,16 +303,16 @@ private void UpdateCharacterSelection(CharSelectData.SeatState state, int seatId | |
/// <summary> | ||
/// Internal utility that sets the graphics for the eight lobby-seats (based on their current networked state) | ||
/// </summary> | ||
private void UpdateSeats() | ||
void UpdateSeats() | ||
{ | ||
// Players can hop between seats -- and can even SHARE seats -- while they're choosing a class. | ||
// Once they have chosen their class (by "locking in" their seat), other players in that seat are kicked out. | ||
// But until a seat is locked in, we need to display each seat as being used by the latest player to choose it. | ||
// So we go through all players and figure out who should visually be shown as sitting in that seat. | ||
CharSelectData.LobbyPlayerState[] curSeats = new CharSelectData.LobbyPlayerState[m_PlayerSeats.Count]; | ||
CharSelectData.LobbyPlayerState[] curSeats = new CharSelectData.LobbyPlayerState[m_UIPlayerSeats.Count]; | ||
foreach (CharSelectData.LobbyPlayerState playerState in CharSelectData.LobbyPlayers) | ||
{ | ||
if (playerState.SeatIdx == -1 || playerState.SeatState == CharSelectData.SeatState.Inactive) | ||
if (!playerState.IsValid() || playerState.SeatState == CharSelectData.SeatState.Inactive) | ||
continue; // this player isn't seated at all! | ||
if ( curSeats[playerState.SeatIdx].SeatState == CharSelectData.SeatState.Inactive | ||
|| (curSeats[playerState.SeatIdx].SeatState == CharSelectData.SeatState.Active && curSeats[playerState.SeatIdx].LastChangeTime < playerState.LastChangeTime)) | ||
|
@@ -299,16 +323,16 @@ private void UpdateSeats() | |
} | ||
|
||
// now actually update the seats in the UI | ||
for (int i = 0; i < m_PlayerSeats.Count; ++i) | ||
for (int i = 0; i < m_UIPlayerSeats.Count; ++i) | ||
{ | ||
m_PlayerSeats[i].SetState(curSeats[i].SeatState, curSeats[i].PlayerNum, curSeats[i].PlayerName); | ||
m_UIPlayerSeats[i].SetState(curSeats[i].SeatState, curSeats[i].PlayerNum, curSeats[i].PlayerName); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Called by the server when the lobby closes (because all players are seated and locked in) | ||
/// </summary> | ||
private void OnLobbyClosedChanged(bool wasLobbyClosed, bool isLobbyClosed) | ||
void OnLobbyClosedChanged(bool wasLobbyClosed, bool isLobbyClosed) | ||
{ | ||
if (isLobbyClosed) | ||
{ | ||
|
@@ -321,7 +345,7 @@ private void OnLobbyClosedChanged(bool wasLobbyClosed, bool isLobbyClosed) | |
/// It can also disable/enable the lobby seats and the "Ready" button if they are inappropriate for the | ||
/// given mode. | ||
/// </summary> | ||
private void ConfigureUIForLobbyMode(LobbyMode mode) | ||
void ConfigureUIForLobbyMode(LobbyMode mode) | ||
{ | ||
// first the easy bit: turn off all the inappropriate ui elements, and turn the appropriate ones on! | ||
foreach (var list in m_LobbyUIElementsByMode.Values) | ||
|
@@ -368,7 +392,7 @@ private void ConfigureUIForLobbyMode(LobbyMode mode) | |
} | ||
|
||
// go through all our seats and enable or disable buttons | ||
foreach (var seat in m_PlayerSeats) | ||
foreach (var seat in m_UIPlayerSeats) | ||
{ | ||
// disable interaction if seat is already locked or all seats disabled | ||
seat.SetDisableInteraction(seat.IsLocked() || isSeatsDisabledInThisMode); | ||
|
@@ -379,9 +403,51 @@ private void ConfigureUIForLobbyMode(LobbyMode mode) | |
/// <summary> | ||
/// Called directly by UI elements! | ||
/// </summary> | ||
/// <remarks> | ||
/// For a reactive character select screen, the local player's selection is presented instantaneously | ||
/// before a NetworkListEvent can be received from the server. | ||
/// To accomplish this, the previous seat selected will be cleared or re-populated by a shared player, and | ||
/// the new seat will be populated with the local selection. | ||
/// </remarks> | ||
/// <param name="seatIdx"></param> | ||
public void OnPlayerClickedSeat(int seatIdx) | ||
{ | ||
if (m_LastSeatSelected != -1) | ||
{ | ||
var otherPlayerSharesSeat = false; | ||
|
||
// if any other user shares seat, set them as owner; else deactivate seat | ||
foreach (var lobbyPlayer in CharSelectData.LobbyPlayers) | ||
{ | ||
if (lobbyPlayer.ClientId == NetworkManager.Singleton.LocalClientId) | ||
{ | ||
// ignore self; | ||
continue; | ||
} | ||
|
||
if (lobbyPlayer.SeatIdx == m_LastSeatSelected) | ||
{ | ||
// populate this seat with a shared player's lobby player state | ||
otherPlayerSharesSeat = true; | ||
m_UIPlayerSeats[m_LastSeatSelected].SetState(lobbyPlayer.SeatState, lobbyPlayer.PlayerNum, lobbyPlayer.PlayerName); | ||
} | ||
} | ||
|
||
if (!otherPlayerSharesSeat) | ||
{ | ||
// no other player shared this seat; it is safe to just disable | ||
m_UIPlayerSeats[m_LastSeatSelected].SetState(CharSelectData.SeatState.Inactive, -1, string.Empty); | ||
} | ||
} | ||
|
||
// get local player, and populate the seat that is anticipated to be taken | ||
TryGetLobbyPlayer(NetworkManager.Singleton.LocalClientId, out var localLobbyPlayerState); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be worth to note this is the client drive part where we take the seat without waiting for the server driven value |
||
|
||
// apply seat change directly to local player without waiting for server-driven NetworkList event | ||
m_UIPlayerSeats[seatIdx].SetState(CharSelectData.SeatState.Active, localLobbyPlayerState.PlayerNum, localLobbyPlayerState.PlayerName); | ||
UpdateCharacterSelection(CharSelectData.SeatState.Active, seatIdx); | ||
|
||
// send server rpc containing selection | ||
CharSelectData.ChangeSeatServerRpc(NetworkManager.Singleton.LocalClientId, seatIdx, false); | ||
} | ||
|
||
|
@@ -406,9 +472,24 @@ public void OnPlayerExit() | |
SceneManager.LoadScene("MainMenu"); | ||
} | ||
|
||
bool TryGetLobbyPlayer(ulong clientId, out CharSelectData.LobbyPlayerState lobbyPlayerState) | ||
{ | ||
foreach (var lobbyPlayer in CharSelectData.LobbyPlayers) | ||
{ | ||
if (lobbyPlayer.ClientId == clientId) | ||
{ | ||
lobbyPlayerState = lobbyPlayer; | ||
return true; | ||
} | ||
} | ||
|
||
lobbyPlayerState = default; | ||
return false; | ||
} | ||
|
||
GameObject GetCharacterGraphics(Avatar avatar) | ||
{ | ||
if (!m_SpawnedCharacterGraphics.TryGetValue(avatar.Guid, out GameObject characterGraphics)) | ||
if (!m_SpawnedCharacterGraphics.TryGetValue(avatar.Guid, out var characterGraphics)) | ||
{ | ||
characterGraphics = Instantiate(avatar.GraphicsCharacterSelect, m_CharacterGraphicsParent); | ||
m_SpawnedCharacterGraphics.Add(avatar.Guid, characterGraphics); | ||
|
@@ -418,13 +499,13 @@ GameObject GetCharacterGraphics(Avatar avatar) | |
} | ||
|
||
#if UNITY_EDITOR | ||
private void OnValidate() | ||
void OnValidate() | ||
{ | ||
if (gameObject.scene.rootCount > 1) // Hacky way for checking if this is a scene object or a prefab instance and not a prefab definition. | ||
{ | ||
while (m_PlayerSeats.Count < CharSelectData.k_MaxLobbyPlayers) | ||
while (m_UIPlayerSeats.Count < CharSelectData.k_MaxLobbyPlayers) | ||
{ | ||
m_PlayerSeats.Add(null); | ||
m_UIPlayerSeats.Add(null); | ||
} | ||
} | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"owner"? I thought the lockedIn state was server driven?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a player chooses a new seat, I need to clear the local player's previous seat selection from the UI. But sometimes that seat is shared by another player.
Whether that previous seat now becomes empty, or "held" by another player is determined by the last update that was received by the server on the seats.