/** * Copyright 2019 The Knights Of Unity, created by Piotr Stoch and Paweł Stolarczyk * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using Nakama; using Nakama.TinyJson; using System.Linq; using UnityEngine.SceneManagement; using System.Threading.Tasks; using DemoGame.Scripts.Utils; using DemoGame.Scripts.Session; using DemoGame.Scripts.Gameplay.NetworkCommunication.MatchStates; namespace DemoGame.Scripts.Gameplay.NetworkCommunication { /// /// Role of this manager is sending match information to other players and receiving match messages from them through Nakama Server. /// public class MatchCommunicationManager : Singleton { /// /// Number of players required to start match. /// [SerializeField] private int _playerCount = 2; //This region contains events for all type of match messages that could be send in the game. //Events are fired after getting message sent by other players from Nakama server #region PUBLIC EVENTS //GAME public event Action OnGameStarted; public event Action OnGameEnded; //UNITS public event Action OnUnitSpawned; public event Action OnUnitMoved; public event Action OnUnitAttacked; //SPELLS public event Action OnSpellActivated; //CARDS public event Action OnCardRequested; public event Action OnCardPlayed; public event Action OnCardCancelled; public event Action OnStartingHandReceived; #endregion #region PROPORTIES /// /// Id of current game host /// public string HostId { private set; get; } /// /// Returns true if local player is host /// public bool IsHost { get { return HostId == NakamaSessionManager.Instance.Session.UserId; } } /// /// Id of opponent of local player in current game /// public string OpponentId { get; private set; } /// /// List of IUserPresence of all players /// public List Players { get; private set; } /// /// Returns true if Players presences count is equal to required players number /// public bool AllPlayersJoined { get { return Players.Count == _playerCount; } } /// /// Returns true if game is already started /// public bool GameStarted { get; private set; } /// /// Id of current match /// public string MatchId { get; private set; } /// /// Current socket which connects client to Nakama server. Through this socket are sent match messages. /// private ISocket _socket { get { return NakamaSessionManager.Instance.Socket; } } #endregion #region PRIVATE FIELDS private bool _allPlayersAdded; /// /// Indicates if player already joined match /// private bool _matchJoined; /// /// Indicates if player is already leaving match /// private bool _isLeaving; /// /// Queue used for enquequing incoming match messages when match is locally not started yet to make sure that they will be runned /// in properly order after game start /// private Queue _incommingMessages = new Queue(); #endregion #region MONO private void Start() { OnGameEnded += GameEnded; } protected override void OnDestroy() { _incommingMessages = new Queue(); OnGameEnded -= GameEnded; } #endregion #region PUBLIC METHODS /// /// Joins given match found by matchmaker /// /// public async void JoinMatchAsync(IMatchmakerMatched matched) { //Choosing host in deterministic way, with no need to exchange data between players ChooseHost(matched); //Filling list of match participants Players = new List(); try { // Listen to incomming match messages and user connection changes _socket.OnMatchPresence += OnMatchPresence; _socket.OnMatchState += ReceiveMatchStateMessage; // Join the match IMatch match = await _socket.JoinMatchAsync(matched); // Set current match id // It will be used to leave the match later MatchId = match.Id; Debug.Log("Joined match with id: " + match.Id + "; presences count: " + match.Presences.Count()); // Add all players already connected to the match // If both players uses the same account, exit the game bool noDuplicateUsers = AddConnectedPlayers(match); if (noDuplicateUsers == true) { // Match joined successfully // Setting gameplay _matchJoined = true; StartGame(); } else { LeaveGame(); } } catch (Exception e) { Debug.LogError("Couldn't join match: " + e.Message); } } /// /// Starts procedure of leaving match by local player /// public void LeaveGame() { if (_isLeaving == true) { Debug.Log("Already leaving"); return; } _isLeaving = true; _socket.OnMatchPresence -= OnMatchPresence; _socket.OnMatchState -= ReceiveMatchStateMessage; //Starts coroutine which is loading main menu and also disconnects player from match StartCoroutine(LoadMenuCoroutine()); } /// /// This method sends match state message to other players through Nakama server. /// /// /// /// public void SendMatchStateMessage(MatchMessageType opCode, T message) where T : MatchMessage { try { //Packing MatchMessage object to json string json = MatchMessage.ToJson(message); //Sending match state json along with opCode needed for unpacking message to server. //Then server sends it to other players _socket.SendMatchState(MatchId, (long)opCode, json); } catch (Exception e) { Debug.LogError("Error while sending match state: " + e.Message); } } /// /// This method is used by host to invoke locally event connected with match message which is sent to other players. /// Should be always runned on host client after sending any message, otherwise some of the game logic would not be runned on host game instance. /// Don't use this method when client is not a host! /// /// /// /// public void SendMatchStateMessageSelf(MatchMessageType opCode, T message) where T : MatchMessage { //Choosing which event should be invoked basing on opCode and firing event switch (opCode) { //GAME case MatchMessageType.MatchEnded: OnGameEnded?.Invoke(message as MatchMessageGameEnded); break; //UNITS case MatchMessageType.UnitSpawned: OnUnitSpawned?.Invoke(message as MatchMessageUnitSpawned); break; case MatchMessageType.UnitMoved: OnUnitMoved?.Invoke(message as MatchMessageUnitMoved); break; case MatchMessageType.UnitAttacked: OnUnitAttacked?.Invoke(message as MatchMessageUnitAttacked); break; //SPELLS case MatchMessageType.SpellActivated: OnSpellActivated?.Invoke(message as MatchMessageSpellActivated); break; //CARDS case MatchMessageType.CardPlayRequest: OnCardRequested?.Invoke(message as MatchMessageCardPlayRequest); break; case MatchMessageType.CardPlayed: OnCardPlayed?.Invoke(message as MatchMessageCardPlayed); break; case MatchMessageType.CardCanceled: OnCardCancelled?.Invoke(message as MatchMessageCardCanceled); break; case MatchMessageType.StartingHand: OnStartingHandReceived?.Invoke(message as MatchMessageStartingHand); break; default: break; } } /// /// Reads match messages sent by other players, and fires locally events basing on opCode. /// /// /// public void ReceiveMatchStateHandle(long opCode, string messageJson) { if (GameStarted == false) { _incommingMessages.Enqueue(new IncommingMessageState(opCode, messageJson)); return; } //Choosing which event should be invoked basing on opCode, then parsing json to MatchMessage class and firing event switch ((MatchMessageType)opCode) { //GAME case MatchMessageType.MatchEnded: MatchMessageGameEnded matchMessageGameEnded = MatchMessageGameEnded.Parse(messageJson); OnGameEnded?.Invoke(matchMessageGameEnded); break; //UNITS case MatchMessageType.UnitSpawned: MatchMessageUnitSpawned matchMessageUnitSpawned = MatchMessageUnitSpawned.Parse(messageJson); OnUnitSpawned?.Invoke(matchMessageUnitSpawned); break; case MatchMessageType.UnitMoved: MatchMessageUnitMoved matchMessageUnitMoved = MatchMessageUnitMoved.Parse(messageJson); OnUnitMoved?.Invoke(matchMessageUnitMoved); break; case MatchMessageType.UnitAttacked: MatchMessageUnitAttacked matchMessageUnitAttacked = MatchMessageUnitAttacked.Parse(messageJson); OnUnitAttacked?.Invoke(matchMessageUnitAttacked); break; //SPELLS case MatchMessageType.SpellActivated: MatchMessageSpellActivated matchMessageSpellActivated = MatchMessageSpellActivated.Parse(messageJson); OnSpellActivated?.Invoke(matchMessageSpellActivated); break; //CARDS case MatchMessageType.CardPlayRequest: if (IsHost == true) { MatchMessageCardPlayRequest matchMessageCardPlayRequest = MatchMessageCardPlayRequest.Parse(messageJson); OnCardRequested?.Invoke(matchMessageCardPlayRequest); } break; case MatchMessageType.CardPlayed: MatchMessageCardPlayed matchMessageCardPlayed = MatchMessageCardPlayed.Parse(messageJson); OnCardPlayed?.Invoke(matchMessageCardPlayed); break; case MatchMessageType.CardCanceled: MatchMessageCardCanceled matchMessageCardCancelled = MatchMessageCardCanceled.Parse(messageJson); OnCardCancelled?.Invoke(matchMessageCardCancelled); break; case MatchMessageType.StartingHand: MatchMessageStartingHand matchMessageStartingHand = MatchMessageStartingHand.Parse(messageJson); OnStartingHandReceived?.Invoke(matchMessageStartingHand); break; } } /// /// Retrive match reward from Nakama server. /// public async Task GetMatchRewardAsync(string matchId) { Client client = NakamaSessionManager.Instance.Client; ISession session = NakamaSessionManager.Instance.Session; // Maximum number of attempts to receive match result // Sometimes host tries to receive match message before it is fully stored on the server int maxRetries = 10; // The message containing last match id we send to server in order to receive required match info Dictionary payload = new Dictionary { { "match_id", matchId } }; string payloadJson = JsonWriter.ToJson(payload); while (maxRetries-- > 0) { try { // Calling an rpc method which returns our reward IApiRpc response = await client.RpcAsync(session, "last_match_reward", payloadJson); Dictionary changeset = response.Payload.FromJson>(); return changeset["gold"]; } catch (Exception) { Debug.Log("Couldn't retrieve match reward, retrying"); await Task.Delay(500); } } Debug.LogError("Couldn't retrieve match reward; network error"); return -1; } #endregion #region PRIVATE METHODS /// /// Method fired when any user leaves or joins the match /// /// /// private void OnMatchPresence(object sender, IMatchPresenceEvent e) { foreach (IUserPresence user in e.Joins) { if (Players.FindIndex(x => x.UserId == user.UserId) == -1) { Debug.Log("User " + user.Username + " joined match"); Players.Add(user); if (user.UserId != NakamaSessionManager.Instance.Session.UserId) { OpponentId = user.UserId; } if (AllPlayersJoined == true) { _allPlayersAdded = true; StartGame(); } } } if (e.Leaves.Count() > 0) { Debug.LogWarning("User left the game. Exiting"); UnityMainThreadDispatcher.Instance().Enqueue(LeaveGame); } } /// /// Adds all users from given match to list. /// If any user is already on the list, this means there are two devices /// playing on the same account, which is not allowed. /// /// True if there are no duplicate user id. private bool AddConnectedPlayers(IMatch match) { foreach (IUserPresence user in match.Presences) { // Check if user is already in the game if (Players.FindIndex(x => x.UserId == user.UserId) == -1) { Debug.Log("User " + user.Username + " joined match"); // Add to player list Players.Add(user); // Set opponent id for better access if (user.UserId != NakamaSessionManager.Instance.Session.UserId) { OpponentId = user.UserId; } // If the number of players is equal to _playerCount, no more players will come // Set _allPlayersAdded to true if (AllPlayersJoined == true) { _allPlayersAdded = true; } } else { // User is already present in the game // Two devices use the same account, this is not allowed Debug.LogError("Two devices uses the same account, this is not allowed"); return false; } } return true; } private void StartGame() { if (GameStarted == true) { return; } if (_allPlayersAdded == false || _matchJoined == false) { return; } GameStarted = true; UnityMainThreadDispatcher.Instance().Enqueue(() => { Debug.Log("Starting game"); OnGameStarted?.Invoke(); while (_incommingMessages.Count > 0) { IncommingMessageState incommingMessage = _incommingMessages.Dequeue(); ReceiveMatchStateHandle(incommingMessage.opCode, incommingMessage.message); } }); } private void GameEnded(MatchMessageGameEnded obj) { _socket.OnMatchPresence -= OnMatchPresence; } /// /// Loads main menu scene and then leaves match /// /// private IEnumerator LoadMenuCoroutine() { AsyncOperation operation = SceneManager.LoadSceneAsync("MainScene", LoadSceneMode.Additive); while (operation.isDone == false) { yield return null; } LeaveMatch(); } /// /// Disconnects local player from match /// private async void LeaveMatch() { try { Debug.Log("Leaving match with id: " + MatchId); //Sending request to Nakama server for leaving match await NakamaSessionManager.Instance.Socket.LeaveMatchAsync(MatchId); } catch (Exception e) { Debug.Log("Couldn't leave game: " + e); Debug.Log("Reconnecting..."); await NakamaSessionManager.Instance.ConnectAsync(); } UnityMainThreadDispatcher.Instance().Enqueue(() => { SceneManager.UnloadSceneAsync("BattleScene"); }); } /// /// Chooses host in deterministic way /// private void ChooseHost(IMatchmakerMatched matched) { // Add the session id of all users connected to the match List userSessionIds = new List(); foreach (IMatchmakerUser user in matched.Users) { userSessionIds.Add(user.Presence.SessionId); } // Perform a lexicographical sort on list of user session ids userSessionIds.Sort(); // First user from the sorted list will be the host of current match string hostSessionId = userSessionIds.First(); // Get the user id from session id IMatchmakerUser hostUser = matched.Users.First(x => x.Presence.SessionId == hostSessionId); HostId = hostUser.Presence.UserId; } /// /// Receives and dispatches match state message to be handled in ReceiveMatchStateMesage in main thread /// /// /// private void ReceiveMatchStateMessage(object sender, IMatchState matchState) { UnityMainThreadDispatcher.Instance().Enqueue( delegate () { ReceiveMatchStateMessage(matchState); } ); } /// /// Decodes match state message json from byte form of matchState.State and then sends it to ReceiveMatchStateHandle /// for further reading and handling /// /// private void ReceiveMatchStateMessage(IMatchState matchState) { string messageJson = System.Text.Encoding.UTF8.GetString(matchState.State); if (string.IsNullOrEmpty(messageJson)) { return; } ReceiveMatchStateHandle(matchState.OpCode, messageJson); } #endregion } }