namespace Oculus.Platform.Samples.VrBoardGame { using UnityEngine; using Oculus.Platform; using Oculus.Platform.Models; using UnityEngine.UI; using System.Collections.Generic; using System; using UnityEngine.Assertions; // This classes uses the Oculus Matchmaking Service to find opponents of a similar // skill and play a match with them. A skill pool is used with the matchmaking pool // to coordinate the skill matching. Follow the instructions in the Readme to setup // the matchmaking pools. // The Datastore for the Room is used to communicate between the clients. This only // works for relatively simple games with tolerance for latency. For more complex // or realtime requirements, you'll want to use the Oculus.Platform.Net API. public class MatchmakingManager : MonoBehaviour { // GameController to notify about match completions or early endings [SerializeField] private GameController m_gameController = null; // Text for the button that controls matchmaking [SerializeField] private Text m_matchButtonText = null; // Test widget to render matmaking statistics [SerializeField] private Text m_infoText = null; // name of the Quckmatch Pool configured on the Oculus Developer Dashboard // which is expected to have an associated skill pool private const string POOL = "VR_BOARD_GAME_POOL"; // the ID of the room for the current match private ulong m_matchRoom; // opponent User data private User m_remotePlayer; // last time we've received a room update private float m_lastUpdateTime; // how long to wait before polling for updates private const float POLL_FREQUENCY = 30.0f; private enum MatchRoomState { None, Queued, Configuring, MyTurn, RemoteTurn } private MatchRoomState m_state; void Start() { Matchmaking.SetMatchFoundNotificationCallback(MatchFoundCallback); Rooms.SetUpdateNotificationCallback(MatchmakingRoomUpdateCallback); TransitionToState(MatchRoomState.None); } void Update() { switch (m_state) { case MatchRoomState.Configuring: case MatchRoomState.MyTurn: case MatchRoomState.RemoteTurn: // if we're expecting an update form the remote player and we haven't // heard from them in a while, check the datastore just-in-case if (POLL_FREQUENCY < (Time.time - m_lastUpdateTime)) { Debug.Log("Polling Room"); m_lastUpdateTime = Time.time; Rooms.Get(m_matchRoom).OnComplete(MatchmakingRoomUpdateCallback); } break; } } public void MatchButtonPressed() { switch (m_state) { case MatchRoomState.None: TransitionToState(MatchRoomState.Queued); break; default: TransitionToState(MatchRoomState.None); break; } } public void EndMatch(int localScore, int remoteScore) { switch (m_state) { case MatchRoomState.MyTurn: case MatchRoomState.RemoteTurn: var myID = PlatformManager.MyID.ToString(); var remoteID = m_remotePlayer.ID.ToString(); var rankings = new Dictionary(); if (localScore > remoteScore) { rankings[myID] = 1; rankings[remoteID] = 2; } else if (localScore < remoteScore) { rankings[myID] = 2; rankings[remoteID] = 1; } else { rankings[myID] = 1; rankings[remoteID] = 1; } // since there is no secure server to simulate the game and report // verifiable results, each client needs to independently report their // results for the service to compate for inconsistencies Matchmaking.ReportResultsInsecure(m_matchRoom, rankings) .OnComplete(GenericErrorCheckCallback); break; } TransitionToState(MatchRoomState.None); } void OnApplicationQuit() { // be a good matchmaking citizen and leave any queue immediately Matchmaking.Cancel(); if (m_matchRoom != 0) { Rooms.Leave(m_matchRoom); } } private void TransitionToState(MatchRoomState state) { var m_oldState = m_state; m_state = state; switch (m_state) { case MatchRoomState.None: m_matchButtonText.text = "Find Match"; // the player can abort from any of the other states to the None state // so we need to be careful to clean up all state variables m_remotePlayer = null; Matchmaking.Cancel(); if (m_matchRoom != 0) { Rooms.Leave(m_matchRoom); m_matchRoom = 0; } break; case MatchRoomState.Queued: Assert.AreEqual(MatchRoomState.None, m_oldState); m_matchButtonText.text = "Leave Queue"; Matchmaking.Enqueue2(POOL).OnComplete(MatchmakingEnqueueCallback); break; case MatchRoomState.Configuring: Assert.AreEqual(MatchRoomState.Queued, m_oldState); m_matchButtonText.text = "Cancel Match"; break; case MatchRoomState.MyTurn: case MatchRoomState.RemoteTurn: Assert.AreNotEqual(MatchRoomState.None, m_oldState); Assert.AreNotEqual(MatchRoomState.Queued, m_oldState); m_matchButtonText.text = "Cancel Match"; break; } } void MatchmakingEnqueueCallback(Message untyped_msg) { if (untyped_msg.IsError) { Debug.Log(untyped_msg.GetError().Message); TransitionToState(MatchRoomState.None); return; } Message msg = (Message)untyped_msg; MatchmakingEnqueueResult info = msg.Data; m_infoText.text = string.Format( "Avg Wait Time: {0}s\n" + "Max Expected Wait: {1}s\n" + "In Last Hour: {2}\n" + "Recent Percentage: {3}%", info.AverageWait, info.MaxExpectedWait, info.MatchesInLastHourCount, info.RecentMatchPercentage); } void MatchFoundCallback(Message msg) { if (msg.IsError) { Debug.Log(msg.GetError().Message); TransitionToState(MatchRoomState.None); return; } if (m_state != MatchRoomState.Queued) { // ignore callback - user already cancelled return; } // since this example communicates via updates to the datastore, it's vital that // we subscribe to room updates Matchmaking.JoinRoom(msg.Data.ID, true /* subscribe to update notifications */) .OnComplete(MatchmakingJoinRoomCallback); m_matchRoom = msg.Data.ID; } void MatchmakingJoinRoomCallback(Message msg) { if (msg.IsError) { Debug.Log(msg.GetError().Message); TransitionToState(MatchRoomState.None); return; } if (m_state != MatchRoomState.Queued) { // ignore callback - user already cancelled return; } int numUsers = (msg.Data.UsersOptional != null) ? msg.Data.UsersOptional.Count : 0; Debug.Log ("Match room joined: " + m_matchRoom + " count: " + numUsers); TransitionToState(MatchRoomState.Configuring); // only process the room data if the other user has already joined if (msg.Data.UsersOptional != null && msg.Data.UsersOptional.Count == 2) { ProcessRoomData(msg.Data); } } // Room Datastore updates are used to send moves between players. So if the MatchRoomState // is RemoteTurn I'm looking for the other player's move in the Datastore. If the // MatchRoomState is MyTurn I'm waiting for the room ownership to change so that // I have authority to write to the datastore. void MatchmakingRoomUpdateCallback(Message msg) { if (msg.IsError) { Debug.Log(msg.GetError().Message); TransitionToState(MatchRoomState.None); return; } string ownerOculusID = msg.Data.OwnerOptional != null ? msg.Data.OwnerOptional.OculusID : ""; int numUsers = (msg.Data.UsersOptional != null) ? msg.Data.UsersOptional.Count : 0; Debug.LogFormat( "Room Update {0}\n" + " Owner {1}\n" + " User Count {2}\n" + " Datastore Count {3}\n", msg.Data.ID, ownerOculusID, numUsers, msg.Data.DataStore.Count); // check to make sure the room is valid as there are a few odd timing issues (for // example when leaving a room) that can trigger an uninteresting update if (msg.Data.ID != m_matchRoom) { Debug.Log("Unexpected room update from: " + msg.Data.ID); return; } ProcessRoomData(msg.Data); } private void ProcessRoomData(Room room) { m_lastUpdateTime = Time.time; if (m_state == MatchRoomState.Configuring) { // get the User info for the other player if (room.UsersOptional != null) { foreach (var user in room.UsersOptional) { if (PlatformManager.MyID != user.ID) { Debug.Log("Found remote user: " + user.OculusID); m_remotePlayer = user; break; } } } if (m_remotePlayer == null) return; bool i_go_first = DoesLocalUserGoFirst(); TransitionToState(i_go_first ? MatchRoomState.MyTurn : MatchRoomState.RemoteTurn); Matchmaking.StartMatch(m_matchRoom).OnComplete(GenericErrorCheckCallback); m_gameController.StartOnlineMatch(m_remotePlayer.OculusID, i_go_first); } // if it's the remote player's turn, look for their move in the datastore if (m_state == MatchRoomState.RemoteTurn && room.DataStore.ContainsKey(m_remotePlayer.OculusID) && room.DataStore[m_remotePlayer.OculusID] != "") { // process remote move ProcessRemoteMove(room.DataStore[m_remotePlayer.OculusID]); TransitionToState(MatchRoomState.MyTurn); } // If the room ownership transferred to me, we can mark the remote turn complete. // We don't do this when the remote move comes in if we aren't yet the owner because // the local user will not be able to write to the datastore if they aren't the // owner of the room. if (m_state == MatchRoomState.MyTurn && room.OwnerOptional != null && room.OwnerOptional.ID == PlatformManager.MyID) { m_gameController.MarkRemoteTurnComplete(); } if (room.UsersOptional == null || (room.UsersOptional != null && room.UsersOptional.Count != 2)) { Debug.Log("Other user quit the room"); m_gameController.RemoteMatchEnded(); } } private void ProcessRemoteMove(string moveString) { Debug.Log("Processing remote move string: " + moveString); string[] tokens = moveString.Split(':'); GamePiece.Piece piece = (GamePiece.Piece)Enum.Parse(typeof(GamePiece.Piece), tokens[0]); int x = Int32.Parse(tokens[1]); int y = Int32.Parse(tokens[2]); // swap the coordinates since each player assumes they are player 0 x = GameBoard.LENGTH_X-1 - x; y = GameBoard.LENGTH_Y-1 - y; m_gameController.MakeRemoteMove(piece, x, y); } public void SendLocalMove(GamePiece.Piece piece, int boardX, int boardY) { string moveString = string.Format("{0}:{1}:{2}", piece.ToString(), boardX, boardY); Debug.Log("Sending move: " + moveString); var dict = new Dictionary(); dict[PlatformManager.MyOculusID] = moveString; dict[m_remotePlayer.OculusID] = ""; Rooms.UpdateDataStore(m_matchRoom, dict).OnComplete(UpdateDataStoreCallback); TransitionToState(MatchRoomState.RemoteTurn); } private void UpdateDataStoreCallback(Message msg) { if (m_state != MatchRoomState.RemoteTurn) { // ignore calback - user already quit the match return; } // after I've updated the datastore with my move, change ownership so the other // user can perform their move Rooms.UpdateOwner(m_matchRoom, m_remotePlayer.ID); } // deterministic but somewhat random selection for who goes first private bool DoesLocalUserGoFirst() { // if the room ID is even, the lower ID goes first if (m_matchRoom % 2 == 0) { return PlatformManager.MyID < m_remotePlayer.ID; } // otherwise the higher ID goes first { return PlatformManager.MyID > m_remotePlayer.ID; } } private void GenericErrorCheckCallback(Message msg) { if (msg.IsError) { Debug.Log(msg.GetError().Message); TransitionToState(MatchRoomState.None); return; } } } }