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<string, int>();
|
|
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<MatchmakingEnqueueResult> msg = (Message<MatchmakingEnqueueResult>)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<Room> 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<Room> 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<Room> 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<string, string>();
|
|
dict[PlatformManager.MyOculusID] = moveString;
|
|
dict[m_remotePlayer.OculusID] = "";
|
|
|
|
Rooms.UpdateDataStore(m_matchRoom, dict).OnComplete(UpdateDataStoreCallback);
|
|
TransitionToState(MatchRoomState.RemoteTurn);
|
|
}
|
|
|
|
private void UpdateDataStoreCallback(Message<Room> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|