namespace Oculus.Platform.Samples.VrHoops { using UnityEngine; using UnityEngine.Assertions; using UnityEngine.UI; using System.Collections.Generic; using Oculus.Platform.Models; // This class coordinates playing matches. It mediates being idle // and entering a practice or online game match. public class MatchController : MonoBehaviour { // Text to display when the match will start or finish [SerializeField] private Text m_timerText = null; // the camera is moved between the idle position and the assigned court position [SerializeField] private Camera m_camera = null; // where the camera will be when not in a match [SerializeField] private Transform m_idleCameraTransform = null; // button that toggles between matchmaking and cancel [SerializeField] private Text m_matchmakeButtonText = null; // this should equal the maximum number of players configured on the Oculus Dashboard [SerializeField] private PlayerArea[] m_playerAreas = new PlayerArea[3]; // the time to wait between selecting Practice and starting [SerializeField] private uint PRACTICE_WARMUP_TIME = 5; // seconds to wait to coordinate P2P setup with other match players before starting [SerializeField] private uint MATCH_WARMUP_TIME = 30; // seconds for the match [SerializeField] private uint MATCH_TIME = 20; // how long to remain in position after the match to view results [SerializeField] private uint MATCH_COOLDOWN_TIME = 10; // panel to add most-wins leaderboard entries to [SerializeField] private GameObject m_mostWinsLeaderboard = null; // panel to add high-score leaderboard entries to [SerializeField] private GameObject m_highestScoresLeaderboard = null; // leaderboard entry Text prefab [SerializeField] private GameObject m_leaderboardEntryPrefab = null; // Text prefab to use for achievements fly-text [SerializeField] private GameObject m_flytext = null; // the current state of the match controller private State m_currentState; // transition time for states that automatically transition to the next state, // for example ending the match when the timer expires private float m_nextStateTransitionTime; // the court the local player was assigned to private int m_localSlot; void Start() { PlatformManager.Matchmaking.EnqueueResultCallback = OnMatchFoundCallback; PlatformManager.Matchmaking.MatchPlayerAddedCallback = MatchPlayerAddedCallback; PlatformManager.P2P.StartTimeOfferCallback = StartTimeOfferCallback; PlatformManager.Leaderboards.MostWinsLeaderboardUpdatedCallback = MostWinsLeaderboardCallback; PlatformManager.Leaderboards.HighScoreLeaderboardUpdatedCallback = HighestScoreLeaderboardCallback; TransitionToState(State.NONE); } void Update() { UpdateCheckForNextTimedTransition(); UpdateMatchTimer(); } public float MatchStartTime { get { switch(m_currentState) { case State.WAITING_TO_START_PRACTICE: case State.WAITING_TO_SETUP_MATCH: return m_nextStateTransitionTime; default: return 0; } } private set { m_nextStateTransitionTime = value; } } #region State Management private enum State { UNKNOWN, // no current match, waiting for the local user to select something NONE, // user selected a practice match, waiting for the match timer to start WAITING_TO_START_PRACTICE, // playing a Practice match against AI players PRACTICING, // post practice match, time to view the scores VIEWING_RESULTS_PRACTICE, // selecting Player Online and waiting for the Matchmaking service to find and create a // match and join the assigned match room WAITING_FOR_MATCH, // match room is joined, waiting to coordinate with the other players WAITING_TO_SETUP_MATCH, // playing a competative match against other players PLAYING_MATCH, // match is complete, viewing the match scores VIEWING_MATCH_RESULTS, } void TransitionToState(State newState) { Debug.LogFormat("MatchController State {0} -> {1}", m_currentState, newState); if (m_currentState != newState) { var oldState = m_currentState; m_currentState = newState; // state transition logic switch (newState) { case State.NONE: SetupForIdle(); MoveCameraToIdlePosition(); PlatformManager.TransitionToState(PlatformManager.State.WAITING_TO_PRACTICE_OR_MATCHMAKE); m_matchmakeButtonText.text = "Play Online"; break; case State.WAITING_TO_START_PRACTICE: Assert.AreEqual(oldState, State.NONE); SetupForPractice(); MoveCameraToMatchPosition(); PlatformManager.TransitionToState(PlatformManager.State.MATCH_TRANSITION); m_nextStateTransitionTime = Time.time + PRACTICE_WARMUP_TIME; break; case State.PRACTICING: Assert.AreEqual(oldState, State.WAITING_TO_START_PRACTICE); PlatformManager.TransitionToState(PlatformManager.State.PLAYING_A_LOCAL_MATCH); m_nextStateTransitionTime = Time.time + MATCH_TIME; break; case State.VIEWING_RESULTS_PRACTICE: Assert.AreEqual(oldState, State.PRACTICING); PlatformManager.TransitionToState(PlatformManager.State.MATCH_TRANSITION); m_nextStateTransitionTime = Time.time + MATCH_COOLDOWN_TIME; m_timerText.text = "0:00.00"; break; case State.WAITING_FOR_MATCH: Assert.AreEqual(oldState, State.NONE); PlatformManager.TransitionToState(PlatformManager.State.MATCH_TRANSITION); m_matchmakeButtonText.text = "Cancel"; break; case State.WAITING_TO_SETUP_MATCH: Assert.AreEqual(oldState, State.WAITING_FOR_MATCH); m_nextStateTransitionTime = Time.time + MATCH_WARMUP_TIME; break; case State.PLAYING_MATCH: Assert.AreEqual(oldState, State.WAITING_TO_SETUP_MATCH); PlatformManager.TransitionToState(PlatformManager.State.PLAYING_A_NETWORKED_MATCH); m_nextStateTransitionTime = Time.time + MATCH_TIME; break; case State.VIEWING_MATCH_RESULTS: Assert.AreEqual(oldState, State.PLAYING_MATCH); PlatformManager.TransitionToState(PlatformManager.State.MATCH_TRANSITION); m_nextStateTransitionTime = Time.time + MATCH_COOLDOWN_TIME; m_timerText.text = "0:00.00"; CalculateMatchResults(); break; } } } void UpdateCheckForNextTimedTransition() { if (m_currentState != State.NONE && Time.time >= m_nextStateTransitionTime) { switch (m_currentState) { case State.WAITING_TO_START_PRACTICE: TransitionToState(State.PRACTICING); break; case State.PRACTICING: TransitionToState(State.VIEWING_RESULTS_PRACTICE); break; case State.VIEWING_RESULTS_PRACTICE: TransitionToState(State.NONE); break; case State.WAITING_TO_SETUP_MATCH: TransitionToState(State.PLAYING_MATCH); break; case State.PLAYING_MATCH: TransitionToState(State.VIEWING_MATCH_RESULTS); break; case State.VIEWING_MATCH_RESULTS: PlatformManager.Matchmaking.EndMatch(); TransitionToState(State.NONE); break; } } } void UpdateMatchTimer() { if (Time.time <= m_nextStateTransitionTime) { switch (m_currentState) { case State.WAITING_TO_START_PRACTICE: case State.WAITING_TO_SETUP_MATCH: m_timerText.text = string.Format("{0:0}", Mathf.Ceil(Time.time - MatchStartTime)); break; case State.PRACTICING: case State.PLAYING_MATCH: var delta = m_nextStateTransitionTime - Time.time; m_timerText.text = string.Format("{0:#0}:{1:#00}.{2:00}", Mathf.Floor(delta / 60), Mathf.Floor(delta) % 60, Mathf.Floor(delta * 100) % 100); break; } } } #endregion #region Player Setup/Teardown void SetupForIdle() { for (int i = 0; i < m_playerAreas.Length; i++) { m_playerAreas[i].SetupForPlayer("* AI *"); } } void SetupForPractice() { // randomly select a position for the local player m_localSlot = Random.Range(0,m_playerAreas.Length-1); for (int i=0; i < m_playerAreas.Length; i++) { if (i == m_localSlot) { m_playerAreas[i].SetupForPlayer(PlatformManager.MyOculusID); } else { m_playerAreas[i].SetupForPlayer("* AI *"); } } } Player MatchPlayerAddedCallback(int slot, User user) { Player player = null; if (m_currentState == State.WAITING_TO_SETUP_MATCH && slot < m_playerAreas.Length) { if (user.ID == PlatformManager.MyID) { var localPlayer = m_playerAreas[slot].SetupForPlayer(user.OculusID); MoveCameraToMatchPosition(); player = localPlayer; m_localSlot = slot; } else { var remotePlayer = m_playerAreas[slot].SetupForPlayer(user.OculusID); remotePlayer.User = user; player = remotePlayer; } } return player; } #endregion #region Main Camera Movement void MoveCameraToIdlePosition() { var ejector = m_camera.gameObject.GetComponentInChildren(); if (ejector) { ejector.transform.SetParent(m_camera.transform.parent, false); m_camera.transform.SetParent(m_idleCameraTransform, false); } } void MoveCameraToMatchPosition() { foreach (var playerArea in m_playerAreas) { var player = playerArea.GetComponentInChildren(); if (player) { var ejector = player.GetComponentInChildren(); m_camera.transform.SetParent(player.transform, false); ejector.transform.SetParent(m_camera.transform, false); break; } } DisplayAchievementFlytext(); } #endregion #region Match Initiation public void StartPracticeMatch() { if (m_currentState == State.NONE) { TransitionToState(State.WAITING_TO_START_PRACTICE); } } public void PlayOnlineOrCancel() { Debug.Log ("Play online or Cancel"); if (m_currentState == State.NONE) { PlatformManager.Matchmaking.QueueForMatch(); TransitionToState (State.WAITING_FOR_MATCH); } else if (m_currentState == State.WAITING_FOR_MATCH) { PlatformManager.Matchmaking.LeaveQueue(); TransitionToState (State.NONE); } } // notification from the Matchmaking service if we succeeded in finding an online match void OnMatchFoundCallback(bool success) { if (success) { TransitionToState(State.WAITING_TO_SETUP_MATCH); } else { TransitionToState(State.NONE); } } // handle an offer from a remote player for a new match start time float StartTimeOfferCallback(float remoteTime) { if (m_currentState == State.WAITING_TO_SETUP_MATCH) { // if the remote start time is later use that, as long as it's not horribly wrong if (remoteTime > MatchStartTime && (remoteTime - 60) < MatchStartTime) { Debug.Log("Moving Start time by " + (remoteTime - MatchStartTime)); MatchStartTime = remoteTime; } } return MatchStartTime; } #endregion #region Leaderboards and Achievements void MostWinsLeaderboardCallback(SortedDictionary entries) { foreach (Transform entry in m_mostWinsLeaderboard.transform) { Destroy(entry.gameObject); } foreach (var entry in entries.Values) { GameObject label = Instantiate(m_leaderboardEntryPrefab); label.transform.SetParent(m_mostWinsLeaderboard.transform, false); label.GetComponent().text = string.Format("{0} - {1} - {2}", entry.Rank, entry.User.OculusID, entry.Score); } } void HighestScoreLeaderboardCallback(SortedDictionary entries) { foreach (Transform entry in m_highestScoresLeaderboard.transform) { Destroy(entry.gameObject); } foreach (var entry in entries.Values) { GameObject label = Instantiate(m_leaderboardEntryPrefab); label.transform.SetParent(m_highestScoresLeaderboard.transform, false); label.GetComponent().text = string.Format("{0} - {1} - {2}", entry.Rank, entry.User.OculusID, entry.Score); } } void CalculateMatchResults() { LocalPlayer localPlayer = null; RemotePlayer remotePlayer = null; foreach (var court in m_playerAreas) { if (court.Player is LocalPlayer) { localPlayer = court.Player as LocalPlayer; } else if (court.Player is RemotePlayer && (remotePlayer == null || court.Player.Score > remotePlayer.Score)) { remotePlayer = court.Player as RemotePlayer; } } // ignore the match results if the player got into a session without an opponent if (!localPlayer || !remotePlayer) { return; } bool wonMatch = localPlayer.Score > remotePlayer.Score; PlatformManager.Leaderboards.SubmitMatchScores(wonMatch, localPlayer.Score); if (wonMatch) { PlatformManager.Achievements.RecordWinForLocalUser(); } } void DisplayAchievementFlytext() { if (PlatformManager.Achievements.LikesToWin) { GameObject go = Instantiate(m_flytext); go.GetComponent().text = "Likes to Win!"; go.transform.position = Vector3.up * 40; go.transform.SetParent(m_playerAreas[m_localSlot].NameText.transform, false); } } #endregion } }