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<AIPlayer>("* 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<LocalPlayer>(PlatformManager.MyOculusID);
|
|
}
|
|
else
|
|
{
|
|
m_playerAreas[i].SetupForPlayer<AIPlayer>("* 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<LocalPlayer>(user.OculusID);
|
|
MoveCameraToMatchPosition();
|
|
player = localPlayer;
|
|
m_localSlot = slot;
|
|
}
|
|
else
|
|
{
|
|
var remotePlayer = m_playerAreas[slot].SetupForPlayer<RemotePlayer>(user.OculusID);
|
|
remotePlayer.User = user;
|
|
player = remotePlayer;
|
|
}
|
|
}
|
|
|
|
return player;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Main Camera Movement
|
|
|
|
void MoveCameraToIdlePosition()
|
|
{
|
|
var ejector = m_camera.gameObject.GetComponentInChildren<BallEjector>();
|
|
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<LocalPlayer>();
|
|
if (player)
|
|
{
|
|
var ejector = player.GetComponentInChildren<BallEjector>();
|
|
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<int, LeaderboardEntry> 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>().text =
|
|
string.Format("{0} - {1} - {2}", entry.Rank, entry.User.OculusID, entry.Score);
|
|
}
|
|
}
|
|
|
|
void HighestScoreLeaderboardCallback(SortedDictionary<int, LeaderboardEntry> 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>().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>().text = "Likes to Win!";
|
|
go.transform.position = Vector3.up * 40;
|
|
go.transform.SetParent(m_playerAreas[m_localSlot].NameText.transform, false);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|