This commit is contained in:
2023-11-28 11:41:03 +05:30
commit da3b6cf083
1281 changed files with 97466 additions and 0 deletions

View File

@@ -0,0 +1,678 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.MultipleMatch
{
public class CanvasController : MonoBehaviour
{
/// <summary>
/// Match Controllers listen for this to terminate their match and clean up
/// </summary>
public event Action<NetworkConnectionToClient> OnPlayerDisconnected;
/// <summary>
/// Cross-reference of client that created the corresponding match in openMatches below
/// </summary>
internal static readonly Dictionary<NetworkConnectionToClient, Guid> playerMatches = new Dictionary<NetworkConnectionToClient, Guid>();
/// <summary>
/// Open matches that are available for joining
/// </summary>
internal static readonly Dictionary<Guid, MatchInfo> openMatches = new Dictionary<Guid, MatchInfo>();
/// <summary>
/// Network Connections of all players in a match
/// </summary>
internal static readonly Dictionary<Guid, HashSet<NetworkConnectionToClient>> matchConnections = new Dictionary<Guid, HashSet<NetworkConnectionToClient>>();
/// <summary>
/// Player informations by Network Connection
/// </summary>
internal static readonly Dictionary<NetworkConnection, PlayerInfo> playerInfos = new Dictionary<NetworkConnection, PlayerInfo>();
/// <summary>
/// Network Connections that have neither started nor joined a match yet
/// </summary>
internal static readonly List<NetworkConnectionToClient> waitingConnections = new List<NetworkConnectionToClient>();
/// <summary>
/// GUID of a match the local player has created
/// </summary>
internal Guid localPlayerMatch = Guid.Empty;
/// <summary>
/// GUID of a match the local player has joined
/// </summary>
internal Guid localJoinedMatch = Guid.Empty;
/// <summary>
/// GUID of a match the local player has selected in the Toggle Group match list
/// </summary>
internal Guid selectedMatch = Guid.Empty;
// Used in UI for "Player #"
int playerIndex = 1;
[Header("GUI References")]
public GameObject matchList;
public GameObject matchPrefab;
public GameObject matchControllerPrefab;
public Button createButton;
public Button joinButton;
public GameObject lobbyView;
public GameObject roomView;
public RoomGUI roomGUI;
public ToggleGroup toggleGroup;
// RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void ResetStatics()
{
playerMatches.Clear();
openMatches.Clear();
matchConnections.Clear();
playerInfos.Clear();
waitingConnections.Clear();
}
#region UI Functions
// Called from several places to ensure a clean reset
// - MatchNetworkManager.Awake
// - OnStartServer
// - OnStartClient
// - OnClientDisconnect
// - ResetCanvas
internal void InitializeData()
{
playerMatches.Clear();
openMatches.Clear();
matchConnections.Clear();
waitingConnections.Clear();
localPlayerMatch = Guid.Empty;
localJoinedMatch = Guid.Empty;
}
// Called from OnStopServer and OnStopClient when shutting down
void ResetCanvas()
{
InitializeData();
lobbyView.SetActive(false);
roomView.SetActive(false);
gameObject.SetActive(false);
}
#endregion
#region Button Calls
/// <summary>
/// Called from <see cref="MatchGUI.OnToggleClicked"/>
/// </summary>
/// <param name="matchId"></param>
public void SelectMatch(Guid matchId)
{
if (!NetworkClient.active) return;
if (matchId == Guid.Empty)
{
selectedMatch = Guid.Empty;
joinButton.interactable = false;
}
else
{
if (!openMatches.Keys.Contains(matchId))
{
joinButton.interactable = false;
return;
}
selectedMatch = matchId;
MatchInfo infos = openMatches[matchId];
joinButton.interactable = infos.players < infos.maxPlayers;
}
}
/// <summary>
/// Assigned in inspector to Create button
/// </summary>
public void RequestCreateMatch()
{
if (!NetworkClient.active) return;
NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Create });
}
/// <summary>
/// Assigned in inspector to Join button
/// </summary>
public void RequestJoinMatch()
{
if (!NetworkClient.active || selectedMatch == Guid.Empty) return;
NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Join, matchId = selectedMatch });
}
/// <summary>
/// Assigned in inspector to Leave button
/// </summary>
public void RequestLeaveMatch()
{
if (!NetworkClient.active || localJoinedMatch == Guid.Empty) return;
NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Leave, matchId = localJoinedMatch });
}
/// <summary>
/// Assigned in inspector to Cancel button
/// </summary>
public void RequestCancelMatch()
{
if (!NetworkClient.active || localPlayerMatch == Guid.Empty) return;
NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Cancel });
}
/// <summary>
/// Assigned in inspector to Ready button
/// </summary>
public void RequestReadyChange()
{
if (!NetworkClient.active || (localPlayerMatch == Guid.Empty && localJoinedMatch == Guid.Empty)) return;
Guid matchId = localPlayerMatch == Guid.Empty ? localJoinedMatch : localPlayerMatch;
NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Ready, matchId = matchId });
}
/// <summary>
/// Assigned in inspector to Start button
/// </summary>
public void RequestStartMatch()
{
if (!NetworkClient.active || localPlayerMatch == Guid.Empty) return;
NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Start });
}
/// <summary>
/// Called from <see cref="MatchController.RpcExitGame"/>
/// </summary>
public void OnMatchEnded()
{
if (!NetworkClient.active) return;
localPlayerMatch = Guid.Empty;
localJoinedMatch = Guid.Empty;
ShowLobbyView();
}
/// <summary>
/// Sends updated match list to all waiting connections or just one if specified
/// </summary>
/// <param name="conn"></param>
internal void SendMatchList(NetworkConnectionToClient conn = null)
{
if (!NetworkServer.active) return;
if (conn != null)
{
conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.List, matchInfos = openMatches.Values.ToArray() });
}
else
{
foreach (NetworkConnectionToClient waiter in waitingConnections)
{
waiter.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.List, matchInfos = openMatches.Values.ToArray() });
}
}
}
#endregion
#region Server & Client Callbacks
// Methods in this section are called from MatchNetworkManager's corresponding methods
internal void OnStartServer()
{
if (!NetworkServer.active) return;
InitializeData();
NetworkServer.RegisterHandler<ServerMatchMessage>(OnServerMatchMessage);
}
internal void OnServerReady(NetworkConnectionToClient conn)
{
if (!NetworkServer.active) return;
waitingConnections.Add(conn);
playerInfos.Add(conn, new PlayerInfo { playerIndex = this.playerIndex, ready = false });
playerIndex++;
SendMatchList();
}
internal void OnServerDisconnect(NetworkConnectionToClient conn)
{
if (!NetworkServer.active) return;
// Invoke OnPlayerDisconnected on all instances of MatchController
OnPlayerDisconnected?.Invoke(conn);
Guid matchId;
if (playerMatches.TryGetValue(conn, out matchId))
{
playerMatches.Remove(conn);
openMatches.Remove(matchId);
foreach (NetworkConnectionToClient playerConn in matchConnections[matchId])
{
PlayerInfo _playerInfo = playerInfos[playerConn];
_playerInfo.ready = false;
_playerInfo.matchId = Guid.Empty;
playerInfos[playerConn] = _playerInfo;
playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Departed });
}
}
foreach (KeyValuePair<Guid, HashSet<NetworkConnectionToClient>> kvp in matchConnections)
{
kvp.Value.Remove(conn);
}
PlayerInfo playerInfo = playerInfos[conn];
if (playerInfo.matchId != Guid.Empty)
{
MatchInfo matchInfo;
if (openMatches.TryGetValue(playerInfo.matchId, out matchInfo))
{
matchInfo.players--;
openMatches[playerInfo.matchId] = matchInfo;
}
HashSet<NetworkConnectionToClient> connections;
if (matchConnections.TryGetValue(playerInfo.matchId, out connections))
{
PlayerInfo[] infos = connections.Select(playerConn => playerInfos[playerConn]).ToArray();
foreach (NetworkConnectionToClient playerConn in matchConnections[playerInfo.matchId])
{
if (playerConn != conn)
{
playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos });
}
}
}
}
SendMatchList();
}
internal void OnStopServer()
{
ResetCanvas();
}
internal void OnClientConnect()
{
playerInfos.Add(NetworkClient.connection, new PlayerInfo { playerIndex = this.playerIndex, ready = false });
}
internal void OnStartClient()
{
if (!NetworkClient.active) return;
InitializeData();
ShowLobbyView();
createButton.gameObject.SetActive(true);
joinButton.gameObject.SetActive(true);
NetworkClient.RegisterHandler<ClientMatchMessage>(OnClientMatchMessage);
}
internal void OnClientDisconnect()
{
if (!NetworkClient.active) return;
InitializeData();
}
internal void OnStopClient()
{
ResetCanvas();
}
#endregion
#region Server Match Message Handlers
void OnServerMatchMessage(NetworkConnectionToClient conn, ServerMatchMessage msg)
{
if (!NetworkServer.active) return;
switch (msg.serverMatchOperation)
{
case ServerMatchOperation.None:
{
Debug.LogWarning("Missing ServerMatchOperation");
break;
}
case ServerMatchOperation.Create:
{
OnServerCreateMatch(conn);
break;
}
case ServerMatchOperation.Cancel:
{
OnServerCancelMatch(conn);
break;
}
case ServerMatchOperation.Start:
{
OnServerStartMatch(conn);
break;
}
case ServerMatchOperation.Join:
{
OnServerJoinMatch(conn, msg.matchId);
break;
}
case ServerMatchOperation.Leave:
{
OnServerLeaveMatch(conn, msg.matchId);
break;
}
case ServerMatchOperation.Ready:
{
OnServerPlayerReady(conn, msg.matchId);
break;
}
}
}
void OnServerPlayerReady(NetworkConnectionToClient conn, Guid matchId)
{
if (!NetworkServer.active) return;
PlayerInfo playerInfo = playerInfos[conn];
playerInfo.ready = !playerInfo.ready;
playerInfos[conn] = playerInfo;
HashSet<NetworkConnectionToClient> connections = matchConnections[matchId];
PlayerInfo[] infos = connections.Select(playerConn => playerInfos[playerConn]).ToArray();
foreach (NetworkConnectionToClient playerConn in matchConnections[matchId])
{
playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos });
}
}
void OnServerLeaveMatch(NetworkConnectionToClient conn, Guid matchId)
{
if (!NetworkServer.active) return;
MatchInfo matchInfo = openMatches[matchId];
matchInfo.players--;
openMatches[matchId] = matchInfo;
PlayerInfo playerInfo = playerInfos[conn];
playerInfo.ready = false;
playerInfo.matchId = Guid.Empty;
playerInfos[conn] = playerInfo;
foreach (KeyValuePair<Guid, HashSet<NetworkConnectionToClient>> kvp in matchConnections)
{
kvp.Value.Remove(conn);
}
HashSet<NetworkConnectionToClient> connections = matchConnections[matchId];
PlayerInfo[] infos = connections.Select(playerConn => playerInfos[playerConn]).ToArray();
foreach (NetworkConnectionToClient playerConn in matchConnections[matchId])
{
playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos });
}
SendMatchList();
conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Departed });
}
void OnServerCreateMatch(NetworkConnectionToClient conn)
{
if (!NetworkServer.active || playerMatches.ContainsKey(conn)) return;
Guid newMatchId = Guid.NewGuid();
matchConnections.Add(newMatchId, new HashSet<NetworkConnectionToClient>());
matchConnections[newMatchId].Add(conn);
playerMatches.Add(conn, newMatchId);
openMatches.Add(newMatchId, new MatchInfo { matchId = newMatchId, maxPlayers = 2, players = 1 });
PlayerInfo playerInfo = playerInfos[conn];
playerInfo.ready = false;
playerInfo.matchId = newMatchId;
playerInfos[conn] = playerInfo;
PlayerInfo[] infos = matchConnections[newMatchId].Select(playerConn => playerInfos[playerConn]).ToArray();
conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Created, matchId = newMatchId, playerInfos = infos });
SendMatchList();
}
void OnServerCancelMatch(NetworkConnectionToClient conn)
{
if (!NetworkServer.active || !playerMatches.ContainsKey(conn)) return;
conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Cancelled });
Guid matchId;
if (playerMatches.TryGetValue(conn, out matchId))
{
playerMatches.Remove(conn);
openMatches.Remove(matchId);
foreach (NetworkConnectionToClient playerConn in matchConnections[matchId])
{
PlayerInfo playerInfo = playerInfos[playerConn];
playerInfo.ready = false;
playerInfo.matchId = Guid.Empty;
playerInfos[playerConn] = playerInfo;
playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Departed });
}
SendMatchList();
}
}
void OnServerStartMatch(NetworkConnectionToClient conn)
{
if (!NetworkServer.active || !playerMatches.ContainsKey(conn)) return;
Guid matchId;
if (playerMatches.TryGetValue(conn, out matchId))
{
GameObject matchControllerObject = Instantiate(matchControllerPrefab);
matchControllerObject.GetComponent<NetworkMatch>().matchId = matchId;
NetworkServer.Spawn(matchControllerObject);
MatchController matchController = matchControllerObject.GetComponent<MatchController>();
foreach (NetworkConnectionToClient playerConn in matchConnections[matchId])
{
playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Started });
GameObject player = Instantiate(NetworkManager.singleton.playerPrefab);
player.GetComponent<NetworkMatch>().matchId = matchId;
NetworkServer.AddPlayerForConnection(playerConn, player);
if (matchController.player1 == null)
{
matchController.player1 = playerConn.identity;
}
else
{
matchController.player2 = playerConn.identity;
}
/* Reset ready state for after the match. */
PlayerInfo playerInfo = playerInfos[playerConn];
playerInfo.ready = false;
playerInfos[playerConn] = playerInfo;
}
matchController.startingPlayer = matchController.player1;
matchController.currentPlayer = matchController.player1;
playerMatches.Remove(conn);
openMatches.Remove(matchId);
matchConnections.Remove(matchId);
SendMatchList();
OnPlayerDisconnected += matchController.OnPlayerDisconnected;
}
}
void OnServerJoinMatch(NetworkConnectionToClient conn, Guid matchId)
{
if (!NetworkServer.active || !matchConnections.ContainsKey(matchId) || !openMatches.ContainsKey(matchId)) return;
MatchInfo matchInfo = openMatches[matchId];
matchInfo.players++;
openMatches[matchId] = matchInfo;
matchConnections[matchId].Add(conn);
PlayerInfo playerInfo = playerInfos[conn];
playerInfo.ready = false;
playerInfo.matchId = matchId;
playerInfos[conn] = playerInfo;
PlayerInfo[] infos = matchConnections[matchId].Select(playerConn => playerInfos[playerConn]).ToArray();
SendMatchList();
conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Joined, matchId = matchId, playerInfos = infos });
foreach (NetworkConnectionToClient playerConn in matchConnections[matchId])
{
playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos });
}
}
#endregion
#region Client Match Message Handler
void OnClientMatchMessage(ClientMatchMessage msg)
{
if (!NetworkClient.active) return;
switch (msg.clientMatchOperation)
{
case ClientMatchOperation.None:
{
Debug.LogWarning("Missing ClientMatchOperation");
break;
}
case ClientMatchOperation.List:
{
openMatches.Clear();
foreach (MatchInfo matchInfo in msg.matchInfos)
{
openMatches.Add(matchInfo.matchId, matchInfo);
}
RefreshMatchList();
break;
}
case ClientMatchOperation.Created:
{
localPlayerMatch = msg.matchId;
ShowRoomView();
roomGUI.RefreshRoomPlayers(msg.playerInfos);
roomGUI.SetOwner(true);
break;
}
case ClientMatchOperation.Cancelled:
{
localPlayerMatch = Guid.Empty;
ShowLobbyView();
break;
}
case ClientMatchOperation.Joined:
{
localJoinedMatch = msg.matchId;
ShowRoomView();
roomGUI.RefreshRoomPlayers(msg.playerInfos);
roomGUI.SetOwner(false);
break;
}
case ClientMatchOperation.Departed:
{
localJoinedMatch = Guid.Empty;
ShowLobbyView();
break;
}
case ClientMatchOperation.UpdateRoom:
{
roomGUI.RefreshRoomPlayers(msg.playerInfos);
break;
}
case ClientMatchOperation.Started:
{
lobbyView.SetActive(false);
roomView.SetActive(false);
break;
}
}
}
void ShowLobbyView()
{
lobbyView.SetActive(true);
roomView.SetActive(false);
foreach (Transform child in matchList.transform)
{
if (child.gameObject.GetComponent<MatchGUI>().GetMatchId() == selectedMatch)
{
Toggle toggle = child.gameObject.GetComponent<Toggle>();
toggle.isOn = true;
//toggle.onValueChanged.Invoke(true);
}
}
}
void ShowRoomView()
{
lobbyView.SetActive(false);
roomView.SetActive(true);
}
void RefreshMatchList()
{
foreach (Transform child in matchList.transform)
{
Destroy(child.gameObject);
}
joinButton.interactable = false;
foreach (MatchInfo matchInfo in openMatches.Values)
{
GameObject newMatch = Instantiate(matchPrefab, Vector3.zero, Quaternion.identity);
newMatch.transform.SetParent(matchList.transform, false);
newMatch.GetComponent<MatchGUI>().SetMatchInfo(matchInfo);
newMatch.GetComponent<Toggle>().group = toggleGroup;
if (matchInfo.matchId == selectedMatch)
{
newMatch.GetComponent<Toggle>().isOn = true;
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c12d7cb6cdb799041927819f22a2c931
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.MultipleMatch
{
public class CellGUI : MonoBehaviour
{
public MatchController matchController;
public CellValue cellValue;
[Header("GUI References")]
public Image image;
public Button button;
[Header("Diagnostics - Do Not Modify")]
public NetworkIdentity playerIdentity;
public void Awake()
{
matchController.MatchCells.Add(cellValue, this);
}
public void MakePlay()
{
if (matchController.currentPlayer.isLocalPlayer)
matchController.CmdMakePlay(cellValue);
}
public void SetPlayer(NetworkIdentity playerIdentity)
{
if (playerIdentity != null)
{
this.playerIdentity = playerIdentity;
image.color = this.playerIdentity.isLocalPlayer ? Color.blue : Color.red;
button.interactable = false;
}
else
{
this.playerIdentity = null;
image.color = Color.white;
button.interactable = true;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9cda2a394a443474689a3c6d6044f7b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,313 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.MultipleMatch
{
[RequireComponent(typeof(NetworkMatch))]
public class MatchController : NetworkBehaviour
{
internal readonly SyncDictionary<NetworkIdentity, MatchPlayerData> matchPlayerData = new SyncDictionary<NetworkIdentity, MatchPlayerData>();
internal readonly Dictionary<CellValue, CellGUI> MatchCells = new Dictionary<CellValue, CellGUI>();
CellValue boardScore = CellValue.None;
bool playAgain = false;
[Header("GUI References")]
public CanvasGroup canvasGroup;
public Text gameText;
public Button exitButton;
public Button playAgainButton;
public Text winCountLocal;
public Text winCountOpponent;
[Header("Diagnostics - Do Not Modify")]
public CanvasController canvasController;
public NetworkIdentity player1;
public NetworkIdentity player2;
public NetworkIdentity startingPlayer;
[SyncVar(hook = nameof(UpdateGameUI))]
public NetworkIdentity currentPlayer;
void Awake()
{
canvasController = FindObjectOfType<CanvasController>();
}
public override void OnStartServer()
{
StartCoroutine(AddPlayersToMatchController());
}
// For the SyncDictionary to properly fire the update callback, we must
// wait a frame before adding the players to the already spawned MatchController
IEnumerator AddPlayersToMatchController()
{
yield return null;
matchPlayerData.Add(player1, new MatchPlayerData { playerIndex = CanvasController.playerInfos[player1.connectionToClient].playerIndex });
matchPlayerData.Add(player2, new MatchPlayerData { playerIndex = CanvasController.playerInfos[player2.connectionToClient].playerIndex });
}
public override void OnStartClient()
{
matchPlayerData.Callback += UpdateWins;
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
exitButton.gameObject.SetActive(false);
playAgainButton.gameObject.SetActive(false);
}
public void UpdateGameUI(NetworkIdentity _, NetworkIdentity newPlayerTurn)
{
if (!newPlayerTurn) return;
if (newPlayerTurn.gameObject.GetComponent<NetworkIdentity>().isLocalPlayer)
{
gameText.text = "Your Turn";
gameText.color = Color.blue;
}
else
{
gameText.text = "Their Turn";
gameText.color = Color.red;
}
}
public void UpdateWins(SyncDictionary<NetworkIdentity, MatchPlayerData>.Operation op, NetworkIdentity key, MatchPlayerData matchPlayerData)
{
if (key.gameObject.GetComponent<NetworkIdentity>().isLocalPlayer)
{
winCountLocal.text = $"Player {matchPlayerData.playerIndex}\n{matchPlayerData.wins}";
}
else
{
winCountOpponent.text = $"Player {matchPlayerData.playerIndex}\n{matchPlayerData.wins}";
}
}
[Command(requiresAuthority = false)]
public void CmdMakePlay(CellValue cellValue, NetworkConnectionToClient sender = null)
{
// If wrong player or cell already taken, ignore
if (sender.identity != currentPlayer || MatchCells[cellValue].playerIdentity != null)
return;
MatchCells[cellValue].playerIdentity = currentPlayer;
RpcUpdateCell(cellValue, currentPlayer);
MatchPlayerData mpd = matchPlayerData[currentPlayer];
mpd.currentScore = mpd.currentScore | cellValue;
matchPlayerData[currentPlayer] = mpd;
boardScore = boardScore | cellValue;
if (CheckWinner(mpd.currentScore))
{
mpd.wins += 1;
matchPlayerData[currentPlayer] = mpd;
RpcShowWinner(currentPlayer);
currentPlayer = null;
}
else if (boardScore == CellValue.Full)
{
RpcShowWinner(null);
currentPlayer = null;
}
else
{
// Set currentPlayer SyncVar so clients know whose turn it is
currentPlayer = currentPlayer == player1 ? player2 : player1;
}
}
bool CheckWinner(CellValue currentScore)
{
if ((currentScore & CellValue.TopRow) == CellValue.TopRow)
return true;
if ((currentScore & CellValue.MidRow) == CellValue.MidRow)
return true;
if ((currentScore & CellValue.BotRow) == CellValue.BotRow)
return true;
if ((currentScore & CellValue.LeftCol) == CellValue.LeftCol)
return true;
if ((currentScore & CellValue.MidCol) == CellValue.MidCol)
return true;
if ((currentScore & CellValue.RightCol) == CellValue.RightCol)
return true;
if ((currentScore & CellValue.Diag1) == CellValue.Diag1)
return true;
if ((currentScore & CellValue.Diag2) == CellValue.Diag2)
return true;
return false;
}
[ClientRpc]
public void RpcUpdateCell(CellValue cellValue, NetworkIdentity player)
{
MatchCells[cellValue].SetPlayer(player);
}
[ClientRpc]
public void RpcShowWinner(NetworkIdentity winner)
{
foreach (CellGUI cellGUI in MatchCells.Values)
cellGUI.GetComponent<Button>().interactable = false;
if (winner == null)
{
gameText.text = "Draw!";
gameText.color = Color.yellow;
}
else if (winner.gameObject.GetComponent<NetworkIdentity>().isLocalPlayer)
{
gameText.text = "Winner!";
gameText.color = Color.blue;
}
else
{
gameText.text = "Loser!";
gameText.color = Color.red;
}
exitButton.gameObject.SetActive(true);
playAgainButton.gameObject.SetActive(true);
}
// Assigned in inspector to ReplayButton::OnClick
[Client]
public void RequestPlayAgain()
{
playAgainButton.gameObject.SetActive(false);
CmdPlayAgain();
}
[Command(requiresAuthority = false)]
public void CmdPlayAgain(NetworkConnectionToClient sender = null)
{
if (!playAgain)
{
playAgain = true;
}
else
{
playAgain = false;
RestartGame();
}
}
[Server]
public void RestartGame()
{
foreach (CellGUI cellGUI in MatchCells.Values)
cellGUI.SetPlayer(null);
boardScore = CellValue.None;
NetworkIdentity[] keys = new NetworkIdentity[matchPlayerData.Keys.Count];
matchPlayerData.Keys.CopyTo(keys, 0);
foreach (NetworkIdentity identity in keys)
{
MatchPlayerData mpd = matchPlayerData[identity];
mpd.currentScore = CellValue.None;
matchPlayerData[identity] = mpd;
}
RpcRestartGame();
startingPlayer = startingPlayer == player1 ? player2 : player1;
currentPlayer = startingPlayer;
}
[ClientRpc]
public void RpcRestartGame()
{
foreach (CellGUI cellGUI in MatchCells.Values)
cellGUI.SetPlayer(null);
exitButton.gameObject.SetActive(false);
playAgainButton.gameObject.SetActive(false);
}
// Assigned in inspector to BackButton::OnClick
[Client]
public void RequestExitGame()
{
exitButton.gameObject.SetActive(false);
playAgainButton.gameObject.SetActive(false);
CmdRequestExitGame();
}
[Command(requiresAuthority = false)]
public void CmdRequestExitGame(NetworkConnectionToClient sender = null)
{
StartCoroutine(ServerEndMatch(sender, false));
}
public void OnPlayerDisconnected(NetworkConnectionToClient conn)
{
// Check that the disconnecting client is a player in this match
if (player1 == conn.identity || player2 == conn.identity)
{
StartCoroutine(ServerEndMatch(conn, true));
}
}
public IEnumerator ServerEndMatch(NetworkConnectionToClient conn, bool disconnected)
{
canvasController.OnPlayerDisconnected -= OnPlayerDisconnected;
RpcExitGame();
// Skip a frame so the message goes out ahead of object destruction
yield return null;
// Mirror will clean up the disconnecting client so we only need to clean up the other remaining client.
// If both players are just returning to the Lobby, we need to remove both connection Players
if (!disconnected)
{
NetworkServer.RemovePlayerForConnection(player1.connectionToClient, true);
CanvasController.waitingConnections.Add(player1.connectionToClient);
NetworkServer.RemovePlayerForConnection(player2.connectionToClient, true);
CanvasController.waitingConnections.Add(player2.connectionToClient);
}
else if (conn == player1.connectionToClient)
{
// player1 has disconnected - send player2 back to Lobby
NetworkServer.RemovePlayerForConnection(player2.connectionToClient, true);
CanvasController.waitingConnections.Add(player2.connectionToClient);
}
else if (conn == player2.connectionToClient)
{
// player2 has disconnected - send player1 back to Lobby
NetworkServer.RemovePlayerForConnection(player1.connectionToClient, true);
CanvasController.waitingConnections.Add(player1.connectionToClient);
}
// Skip a frame to allow the Removal(s) to complete
yield return null;
// Send latest match list
canvasController.SendMatchList();
NetworkServer.Destroy(gameObject);
}
[ClientRpc]
public void RpcExitGame()
{
canvasController.OnMatchEnded();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ccb1dd1fc7cc624e9bff1d0d7a5c741
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.MultipleMatch
{
public class MatchGUI : MonoBehaviour
{
Guid matchId;
[Header("GUI Elements")]
public Image image;
public Toggle toggleButton;
public Text matchName;
public Text playerCount;
[Header("Diagnostics - Do Not Modify")]
public CanvasController canvasController;
public void Awake()
{
canvasController = FindObjectOfType<CanvasController>();
toggleButton.onValueChanged.AddListener(delegate { OnToggleClicked(); });
}
public void OnToggleClicked()
{
canvasController.SelectMatch(toggleButton.isOn ? matchId : Guid.Empty);
image.color = toggleButton.isOn ? new Color(0f, 1f, 0f, 0.5f) : new Color(1f, 1f, 1f, 0.2f);
}
public Guid GetMatchId()
{
return matchId;
}
public void SetMatchInfo(MatchInfo infos)
{
matchId = infos.matchId;
matchName.text = $"Match {infos.matchId.ToString().Substring(0, 8)}";
playerCount.text = $"{infos.players} / {infos.maxPlayers}";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e875a237ce12c3145b20f17222f10b68
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,118 @@
using System;
namespace Mirror.Examples.MultipleMatch
{
/// <summary>
/// Match message to be sent to the server
/// </summary>
public struct ServerMatchMessage : NetworkMessage
{
public ServerMatchOperation serverMatchOperation;
public Guid matchId;
}
/// <summary>
/// Match message to be sent to the client
/// </summary>
public struct ClientMatchMessage : NetworkMessage
{
public ClientMatchOperation clientMatchOperation;
public Guid matchId;
public MatchInfo[] matchInfos;
public PlayerInfo[] playerInfos;
}
/// <summary>
/// Information about a match
/// </summary>
[Serializable]
public struct MatchInfo
{
public Guid matchId;
public byte players;
public byte maxPlayers;
}
/// <summary>
/// Information about a player
/// </summary>
[Serializable]
public struct PlayerInfo
{
public int playerIndex;
public bool ready;
public Guid matchId;
}
[Serializable]
public struct MatchPlayerData
{
public int playerIndex;
public int wins;
public CellValue currentScore;
}
/// <summary>
/// Match operation to execute on the server
/// </summary>
public enum ServerMatchOperation : byte
{
None,
Create,
Cancel,
Start,
Join,
Leave,
Ready
}
/// <summary>
/// Match operation to execute on the client
/// </summary>
public enum ClientMatchOperation : byte
{
None,
List,
Created,
Cancelled,
Joined,
Departed,
UpdateRoom,
Started
}
// A1 | B1 | C1
// ---+----+---
// A2 | B2 | C2
// ---+----+---
// A3 | B3 | C3
[Flags]
public enum CellValue : ushort
{
None,
A1 = 1 << 0,
B1 = 1 << 1,
C1 = 1 << 2,
A2 = 1 << 3,
B2 = 1 << 4,
C2 = 1 << 5,
A3 = 1 << 6,
B3 = 1 << 7,
C3 = 1 << 8,
// winning combinations
TopRow = A1 + B1 + C1,
MidRow = A2 + B2 + C2,
BotRow = A3 + B3 + C3,
LeftCol = A1 + A2 + A3,
MidCol = B1 + B2 + B3,
RightCol = C1 + C2 + C3,
Diag1 = A1 + B2 + C3,
Diag2 = A3 + B2 + C1,
// board is full (winner / draw)
Full = TopRow + MidRow + BotRow
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a98b377d8481e52449b996e45a015b8c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
using UnityEngine;
/*
Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager
API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html
*/
namespace Mirror.Examples.MultipleMatch
{
[AddComponentMenu("")]
public class MatchNetworkManager : NetworkManager
{
[Header("Match GUI")]
public GameObject canvas;
public CanvasController canvasController;
#region Unity Callbacks
/// <summary>
/// Runs on both Server and Client
/// Networking is NOT initialized when this fires
/// </summary>
public override void Awake()
{
base.Awake();
canvasController.InitializeData();
}
#endregion
#region Server System Callbacks
/// <summary>
/// Called on the server when a client is ready.
/// <para>The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerReady(NetworkConnectionToClient conn)
{
base.OnServerReady(conn);
canvasController.OnServerReady(conn);
}
/// <summary>
/// Called on the server when a client disconnects.
/// <para>This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerDisconnect(NetworkConnectionToClient conn)
{
canvasController.OnServerDisconnect(conn);
base.OnServerDisconnect(conn);
}
#endregion
#region Client System Callbacks
/// <summary>
/// Called on the client when connected to a server.
/// <para>The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects.</para>
/// </summary>
public override void OnClientConnect()
{
base.OnClientConnect();
canvasController.OnClientConnect();
}
/// <summary>
/// Called on clients when disconnected from a server.
/// <para>This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects.</para>
/// </summary>
public override void OnClientDisconnect()
{
canvasController.OnClientDisconnect();
base.OnClientDisconnect();
}
#endregion
#region Start & Stop Callbacks
/// <summary>
/// This is invoked when a server is started - including when a host is started.
/// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartServer()
{
if (mode == NetworkManagerMode.ServerOnly)
canvas.SetActive(true);
canvasController.OnStartServer();
}
/// <summary>
/// This is invoked when the client is started.
/// </summary>
public override void OnStartClient()
{
canvas.SetActive(true);
canvasController.OnStartClient();
}
/// <summary>
/// This is called when a server is stopped - including when a host is stopped.
/// </summary>
public override void OnStopServer()
{
canvasController.OnStopServer();
canvas.SetActive(false);
}
/// <summary>
/// This is called when a client is stopped.
/// </summary>
public override void OnStopClient()
{
canvasController.OnStopClient();
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b15c78bb8ab8da42a94aa0bc3081814
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.MultipleMatch
{
public class PlayerGUI : MonoBehaviour
{
public Text playerName;
public void SetPlayerInfo(PlayerInfo info)
{
playerName.text = $"Player {info.playerIndex}";
playerName.color = info.ready ? Color.green : Color.red;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f61e9bc3715eb904b91222a5526f63d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.MultipleMatch
{
public class RoomGUI : MonoBehaviour
{
public GameObject playerList;
public GameObject playerPrefab;
public GameObject cancelButton;
public GameObject leaveButton;
public Button startButton;
public bool owner;
public void RefreshRoomPlayers(PlayerInfo[] playerInfos)
{
// Debug.Log($"RefreshRoomPlayers: {playerInfos.Length} playerInfos");
foreach (Transform child in playerList.transform)
{
Destroy(child.gameObject);
}
startButton.interactable = false;
bool everyoneReady = true;
foreach (PlayerInfo playerInfo in playerInfos)
{
GameObject newPlayer = Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
newPlayer.transform.SetParent(playerList.transform, false);
newPlayer.GetComponent<PlayerGUI>().SetPlayerInfo(playerInfo);
if (!playerInfo.ready)
{
everyoneReady = false;
}
}
startButton.interactable = everyoneReady && owner && (playerInfos.Length > 1);
}
public void SetOwner(bool owner)
{
this.owner = owner;
cancelButton.SetActive(owner);
leaveButton.SetActive(!owner);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc0f113afba9a074b9a3e4fb56f16abb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: