1
0
forked from cgvr/DeltaVR

Initial Commit

This commit is contained in:
Toomas Tamm
2020-11-28 16:54:41 +02:00
parent 97292ee26e
commit ea967135f2
4217 changed files with 2945663 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections;
// An AI Player just shoots a ball forward with some random delay.
public class AIPlayer : Player {
void FixedUpdate ()
{
if (HasBall)
{
// add a little randomness to the shoot rate so the AI's don't look synchronized
if (Random.Range(0f, 1f) < 0.03f)
{
ShootBall();
}
}
else
{
CheckSpawnBall();
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 71702635af79217469ed41ba39db8d5a
timeCreated: 1475264776
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections;
using Oculus.Platform;
using Oculus.Platform.Models;
public class AchievementsManager
{
// API NAME defined on the dashboard for the achievement
private const string LIKES_TO_WIN = "LIKES_TO_WIN";
// true if the local user hit the achievement Count setup on the dashboard
private bool m_likesToWinUnlocked;
public bool LikesToWin
{
get { return m_likesToWinUnlocked; }
}
public void CheckForAchievmentUpdates()
{
Achievements.GetProgressByName(new string[]{ LIKES_TO_WIN }).OnComplete(
(Message<AchievementProgressList> msg) =>
{
foreach (var achievement in msg.Data)
{
if (achievement.Name == LIKES_TO_WIN)
{
m_likesToWinUnlocked = achievement.IsUnlocked;
}
}
}
);
}
public void RecordWinForLocalUser()
{
Achievements.AddCount(LIKES_TO_WIN, 1);
CheckForAchievmentUpdates();
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 66d441ddf01234331b50e929139f4780
timeCreated: 1477071923
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections;
public class BallEjector : MonoBehaviour {
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b4164fa75939f1e46a3e36dfbdc7f821
timeCreated: 1474514990
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
// Helper class to attach to the MainCamera so it can be moved with the mouse while debugging
// in 2D mode on a PC.
public class Camera2DController : MonoBehaviour
{
void Update ()
{
if (Input.GetButton("Fire2"))
{
var v = Input.GetAxis("Mouse Y");
var h = Input.GetAxis("Mouse X");
transform.rotation *= Quaternion.AngleAxis(h, Vector3.up);
transform.rotation *= Quaternion.AngleAxis(-v, Vector3.right);
Vector3 eulers = transform.eulerAngles;
eulers.z = 0;
transform.eulerAngles = eulers;
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7ebf96caaf397684b86c4ff4d566798f
timeCreated: 1474514266
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using UnityEngine.UI;
// Uses two triggers to detect that a basket is made by traveling from top to bottom
// through the hoop.
public class DetectBasket : MonoBehaviour
{
private enum BasketPhase { NONE, TOP, BOTH, BOTTOM }
private BasketPhase m_phase = BasketPhase.NONE;
private Player m_owningPlayer;
public Player Player
{
set { m_owningPlayer = value; }
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject.name == "Basket Top" && m_phase == BasketPhase.NONE)
{
m_phase = BasketPhase.TOP;
}
else if (other.gameObject.name == "Basket Bottom" && m_phase == BasketPhase.TOP)
{
m_phase = BasketPhase.BOTH;
}
else
{
m_phase = BasketPhase.NONE;
}
}
void OnTriggerExit(Collider other)
{
if (other.gameObject.name == "Basket Top" && m_phase == BasketPhase.BOTH)
{
m_phase = BasketPhase.BOTTOM;
}
else if (other.gameObject.name == "Basket Bottom" && m_phase == BasketPhase.BOTTOM)
{
m_phase = BasketPhase.NONE;
switch (PlatformManager.CurrentState)
{
case PlatformManager.State.PLAYING_A_LOCAL_MATCH:
case PlatformManager.State.PLAYING_A_NETWORKED_MATCH:
if (m_owningPlayer)
{
m_owningPlayer.Score += 2;
}
break;
}
}
else
{
m_phase = BasketPhase.NONE;
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a63d0cd5dd6d39a4abd35114563fe347
timeCreated: 1475105001
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
// helper script to render fading flytext above an object
public class FlyText : MonoBehaviour
{
// destory the gameobject after this many seconds
private const float LIFESPAN = 3.0f;
// how far to move upwards per frame
private readonly Vector3 m_movePerFrame = 0.5f * Vector3.up;
// actual destruction time
private float m_eol;
void Start()
{
m_eol = Time.time + LIFESPAN;
GetComponent<Text>().CrossFadeColor(Color.black, LIFESPAN * 1.7f, false, true);
}
void Update()
{
if (Time.time < m_eol)
{
transform.localPosition += m_movePerFrame;
}
else
{
Destroy(gameObject);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: dc96f5380d7d743d9ae91f11379eb85b
timeCreated: 1477078886
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,84 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
// This script moves to goal around in a random direction to add a bit more difficulty
// to the game.
public class GoalMover : MonoBehaviour {
// how far to from the center before changing direction
[SerializeField] private float MAX_OFFSET = 2.0f;
// how fast the backboard will move
[SerializeField] private float m_speed = 0.005f;
// maximum interpolation distance allow to correct per update
private const float MOVE_TOLERANCE = 0.1f;
// the position the goal should be in - only differs if network updates come in
private Vector3 m_expectedPosition;
// the current move vector * m_speed;
private Vector3 m_moveDirection;
// the direction to move when we run into the boundary
private Vector3 m_nextMoveDirection;
public Vector3 ExpectedPosition
{
get { return m_expectedPosition; }
set { m_expectedPosition = value; }
}
public Vector3 MoveDirection
{
get { return m_moveDirection; }
set { m_moveDirection = value; }
}
public Vector3 NextMoveDirection
{
get { return m_nextMoveDirection; }
set { m_nextMoveDirection = value; }
}
void Start ()
{
ExpectedPosition = transform.localPosition;
m_moveDirection.x = Random.Range(-1.0f, 1.0f);
m_moveDirection.y = Random.Range(-1.0f, 1.0f);
m_moveDirection = Vector3.ClampMagnitude(m_moveDirection, m_speed);
m_nextMoveDirection.x = -Mathf.Sign(m_moveDirection.x) * Random.Range(0f, 1.0f);
m_nextMoveDirection.y = -Mathf.Sign(m_moveDirection.y) * Random.Range(0f, 1.0f);
m_nextMoveDirection = Vector3.ClampMagnitude(m_nextMoveDirection, m_speed);
}
void FixedUpdate ()
{
// move a bit along our random direction
transform.localPosition += MoveDirection;
ExpectedPosition += MoveDirection;
// make a slight correction to the position if we're not where we should be
Vector3 correction = ExpectedPosition - transform.localPosition;
correction = Vector3.ClampMagnitude(correction, MOVE_TOLERANCE);
transform.localPosition += correction;
// if we've gone too far from the center point, correct and change direction
if (transform.localPosition.sqrMagnitude > (MAX_OFFSET*MAX_OFFSET))
{
transform.localPosition = Vector3.ClampMagnitude(transform.localPosition, MAX_OFFSET);
ExpectedPosition = transform.localPosition;
MoveDirection = NextMoveDirection;
// select a the next randomish direction to move in
m_nextMoveDirection.x = -Mathf.Sign(m_moveDirection.x) * Random.Range(0f, 1.0f);
m_nextMoveDirection.y = -Mathf.Sign(m_moveDirection.y) * Random.Range(0f, 1.0f);
m_nextMoveDirection = Vector3.ClampMagnitude(m_nextMoveDirection, m_speed);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: fbea2c3cb080a064f84d3bd86c2f3a53
timeCreated: 1475173562
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,199 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections.Generic;
using Oculus.Platform;
using Oculus.Platform.Models;
// Coordinates updating leaderboard scores and polling for leaderboard updates.
public class LeaderboardManager
{
// API NAME for the leaderboard where we store how many matches the user has won
private const string MOST_MATCHES_WON = "MOST_MATCHES_WON";
// API NAME for the leaderboard where we store the user's match score
private const string HIGHEST_MATCH_SCORE = "HIGHEST_MATCH_SCORE";
// the top number of entries to query
private const int TOP_N_COUNT = 5;
// how often to poll the service for leaderboard updates
private const float LEADERBOARD_POLL_FREQ = 30.0f;
// the next time to check for leaderboard updates
private float m_nextCheckTime;
// cache to hold most-wins leaderboard entries as they come in
private volatile SortedDictionary<int, LeaderboardEntry> m_mostWins;
// whether we've found the local user's entry yet
private bool m_foundLocalUserMostWinsEntry;
// number of times the local user has won
private long m_numWins;
// callback to deliver the most-wins leaderboard entries
private OnMostWinsLeaderboardUpdated m_mostWinsCallback;
// cache to hold high-score leaderboard entries as they come in
private volatile SortedDictionary<int, LeaderboardEntry> m_highScores;
// whether we've found the local user's entry yet
private bool m_foundLocalUserHighScore;
// callback to deliver the high-scores leaderboard entries
private OnHighScoreLeaderboardUpdated m_highScoreCallback;
public void CheckForUpdates()
{
if (Time.time >= m_nextCheckTime &&
PlatformManager.CurrentState == PlatformManager.State.WAITING_TO_PRACTICE_OR_MATCHMAKE)
{
m_nextCheckTime = Time.time + LEADERBOARD_POLL_FREQ;
QueryMostWinsLeaderboard();
QueryHighScoreLeaderboard();
}
}
#region Most Wins Leaderboard
public delegate void OnMostWinsLeaderboardUpdated(SortedDictionary<int, LeaderboardEntry> entries);
public OnMostWinsLeaderboardUpdated MostWinsLeaderboardUpdatedCallback
{
set { m_mostWinsCallback = value; }
}
void QueryMostWinsLeaderboard()
{
// if a query is already in progress, don't start a new one.
if (m_mostWins != null)
return;
m_mostWins = new SortedDictionary<int, LeaderboardEntry>();
m_foundLocalUserMostWinsEntry = false;
Leaderboards.GetEntries(MOST_MATCHES_WON, TOP_N_COUNT, LeaderboardFilterType.None,
LeaderboardStartAt.Top).OnComplete(MostWinsGetEntriesCallback);
}
void MostWinsGetEntriesCallback(Message<LeaderboardEntryList> msg)
{
if (!msg.IsError)
{
foreach (LeaderboardEntry entry in msg.Data)
{
m_mostWins[entry.Rank] = entry;
if (entry.User.ID == PlatformManager.MyID)
{
m_foundLocalUserMostWinsEntry = true;
m_numWins = entry.Score;
}
}
// results might be paged for large requests
if (msg.Data.HasNextPage)
{
Leaderboards.GetNextEntries(msg.Data).OnComplete(MostWinsGetEntriesCallback);
return;
}
// if local user not in the top, get their position specifically
if (!m_foundLocalUserMostWinsEntry)
{
Leaderboards.GetEntries(MOST_MATCHES_WON, 1, LeaderboardFilterType.None,
LeaderboardStartAt.CenteredOnViewer).OnComplete(MostWinsGetEntriesCallback);
return;
}
}
// else an error is returned if the local player isn't ranked - we can ignore that
if (m_mostWinsCallback != null)
{
m_mostWinsCallback(m_mostWins);
}
m_mostWins = null;
}
#endregion
#region Highest Score Board
public delegate void OnHighScoreLeaderboardUpdated(SortedDictionary<int, LeaderboardEntry> entries);
public OnHighScoreLeaderboardUpdated HighScoreLeaderboardUpdatedCallback
{
set { m_highScoreCallback = value; }
}
void QueryHighScoreLeaderboard()
{
// if a query is already in progress, don't start a new one.
if (m_highScores != null)
return;
m_highScores = new SortedDictionary<int, LeaderboardEntry>();
m_foundLocalUserHighScore = false;
Leaderboards.GetEntries(HIGHEST_MATCH_SCORE, TOP_N_COUNT, LeaderboardFilterType.None,
LeaderboardStartAt.Top).OnComplete(HighestScoreGetEntriesCallback);
}
void HighestScoreGetEntriesCallback(Message<LeaderboardEntryList> msg)
{
if (!msg.IsError)
{
foreach (LeaderboardEntry entry in msg.Data)
{
m_highScores[entry.Rank] = entry;
if (entry.User.ID == PlatformManager.MyID)
{
m_foundLocalUserHighScore = true;
}
}
// results might be paged for large requests
if (msg.Data.HasNextPage)
{
Leaderboards.GetNextEntries(msg.Data).OnComplete(HighestScoreGetEntriesCallback);;
return;
}
// if local user not in the top, get their position specifically
if (!m_foundLocalUserHighScore)
{
Leaderboards.GetEntries(HIGHEST_MATCH_SCORE, 1, LeaderboardFilterType.None,
LeaderboardStartAt.CenteredOnViewer).OnComplete(HighestScoreGetEntriesCallback);
return;
}
}
// else an error is returned if the local player isn't ranked - we can ignore that
if (m_highScoreCallback != null)
{
m_highScoreCallback(m_highScores);
}
m_highScores = null;
}
#endregion
// submit the local player's match score to the leaderboard service
public void SubmitMatchScores(bool wonMatch, uint score)
{
if (wonMatch)
{
m_numWins += 1;
Leaderboards.WriteEntry(MOST_MATCHES_WON, m_numWins);
}
if (score > 0)
{
Leaderboards.WriteEntry(HIGHEST_MATCH_SCORE, score);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b5525a4fa9d4898438f479b1c25ff8b4
timeCreated: 1476809789
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections;
// This class listens for Input events to shoot a ball, and also notifies the P2PManager when
// ball or scores needs to be synchronized to remote players.
public class LocalPlayer : Player {
public override uint Score
{
set
{
base.Score = value;
if (PlatformManager.CurrentState == PlatformManager.State.PLAYING_A_NETWORKED_MATCH)
{
PlatformManager.P2P.SendScoreUpdate(base.Score);
}
}
}
void Update ()
{
GameObject newball = null;
// if the player is holding a ball
if (HasBall)
{
// check to see if the User is hitting the shoot button
if (Input.GetButton("Fire1") || Input.GetKey(KeyCode.Space))
{
newball = ShootBall();
}
}
// spawn a new held ball if we can
else
{
newball = CheckSpawnBall();
}
if (newball && PlatformManager.CurrentState == PlatformManager.State.PLAYING_A_NETWORKED_MATCH)
{
PlatformManager.P2P.AddNetworkBall(newball);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: ac564d79b10fc01448f93b8dcc74d3d0
timeCreated: 1475264810
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,472 @@
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
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 5e31d7fbc2f31b7499684c9e87fc8454
timeCreated: 1475257003
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,145 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections.Generic;
using Oculus.Platform;
using Oculus.Platform.Models;
// This class coordinates with the Oculus Platform Matchmaking Service to
// establish a Quickmatch session with one or two other players.
public class MatchmakingManager
{
// the name we setup on the Developer Dashboard for the quickmatch pool
private const string NORMAL_POOL = "NORMAL_QUICKMATCH";
// the ID of the Room the matchmaking service sent to join
private ulong m_matchRoom;
// the list of players that join the match room.
// it may not be all the match players since some might disconnect
// before joining the room, but then again they might disconnect
// midway through a match as well.
private readonly Dictionary<ulong, User> m_remotePlayers;
public MatchmakingManager()
{
m_remotePlayers = new Dictionary<ulong, User>();
Matchmaking.SetMatchFoundNotificationCallback(MatchFoundCallback);
Rooms.SetUpdateNotificationCallback(MatchmakingRoomUpdateCallback);
}
public delegate void OnEnqueueResult(bool successful);
public delegate Player OnMatchPlayerAdded(int slot, User user);
private OnEnqueueResult m_enqueueCallback;
private OnMatchPlayerAdded m_playerCallback;
public OnEnqueueResult EnqueueResultCallback
{
private get { return m_enqueueCallback; }
set { m_enqueueCallback = value; }
}
public OnMatchPlayerAdded MatchPlayerAddedCallback
{
private get { return m_playerCallback; }
set { m_playerCallback = value; }
}
public void QueueForMatch()
{
Matchmaking.Enqueue (NORMAL_POOL).OnComplete(MatchmakingEnqueueCallback);
}
void MatchmakingEnqueueCallback(Message msg)
{
if (msg.IsError)
{
Debug.Log(msg.GetError().Message);
EnqueueResultCallback(false);
return;
}
}
void MatchFoundCallback(Message<Room> msg)
{
m_matchRoom = msg.Data.ID;
Matchmaking.JoinRoom(msg.Data.ID, true).OnComplete(MatchmakingJoinRoomCallback);
}
void MatchmakingJoinRoomCallback(Message<Room> msg)
{
if (msg.IsError)
{
Debug.Log (msg.GetError().Message);
EnqueueResultCallback(false);
return;
}
Debug.Log ("Match found and room joined " + m_matchRoom);
EnqueueResultCallback(true);
// this sample doesn't try to coordinate that all the players see consistent
// positioning to assigned courts, but that would be a great next feature to add
int slot = 0;
if (msg.Data.UsersOptional != null)
{
foreach (var user in msg.Data.UsersOptional)
{
var player = MatchPlayerAddedCallback(slot++, user);
if (PlatformManager.MyID != user.ID)
{
m_remotePlayers[user.ID] = user;
PlatformManager.P2P.AddRemotePlayer (player as RemotePlayer);
}
}
}
}
void MatchmakingRoomUpdateCallback(Message<Room> msg)
{
if (msg.IsError)
{
PlatformManager.TerminateWithError(msg);
return;
}
// 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)
{
if (msg.Data.UsersOptional != null)
{
foreach (User user in msg.Data.UsersOptional)
{
if (PlatformManager.MyID != user.ID && !m_remotePlayers.ContainsKey(user.ID))
{
m_remotePlayers[user.ID] = user;
var player = MatchPlayerAddedCallback(m_remotePlayers.Count, user);
PlatformManager.P2P.AddRemotePlayer(player as RemotePlayer);
}
}
}
}
}
public void EndMatch()
{
if (m_matchRoom != 0)
{
Rooms.Leave (m_matchRoom);
m_remotePlayers.Clear ();
PlatformManager.P2P.DisconnectAll ();
m_matchRoom = 0;
}
}
public void LeaveQueue()
{
Matchmaking.Cancel();
EndMatch();
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: fa69df7dcb6814fab9439789f1e23e2e
timeCreated: 1475629968
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,579 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections.Generic;
using Oculus.Platform;
using Oculus.Platform.Models;
using System;
using UnityEngine.Assertions;
// This helper class coordinates establishing Peer-to-Peer connections between the
// players in the match. It tries to sychronize time between the devices and
// handles position update messages for the backboard and moving balls.
public class P2PManager
{
#region Member variables
// helper class to hold data we need for remote players
private class RemotePlayerData
{
// the last received Net connection state
public PeerConnectionState state;
// the Unity Monobehaviour
public RemotePlayer player;
// offset from my local time to the time of the remote host
public float remoteTimeOffset;
// the last ball update remote time, used to disgard out of order packets
public float lastReceivedBallsTime;
// remote Instance ID -> local MonoBahaviours for balls we're receiving updates on
public readonly Dictionary<int, P2PNetworkBall> activeBalls = new Dictionary<int, P2PNetworkBall>();
}
// authorized users to connect to and associated data
private readonly Dictionary<ulong, RemotePlayerData> m_remotePlayers = new Dictionary<ulong, RemotePlayerData>();
// when to send the next update to remotes on the state on my local balls
private float m_timeForNextBallUpdate;
private const byte TIME_SYNC_MESSAGE = 1;
private const uint TIME_SYNC_MESSAGE_SIZE = 1+4;
private const int TIME_SYNC_MESSAGE_COUNT = 7;
private const byte START_TIME_MESSAGE = 2;
private const uint START_TIME_MESSAGE_SIZE = 1+4;
private const byte BACKBOARD_UPDATE_MESSAGE = 3;
private const uint BACKBOARD_UPDATE_MESSAGE_SIZE = 1+4+12+12+12;
private const byte LOCAL_BALLS_UPDATE_MESSAGE = 4;
private const uint LOCAL_BALLS_UPDATE_MESSATE_SIZE_MAX = 1+4+(2*Player.MAX_BALLS*(1+4+12+12));
private const float LOCAL_BALLS_UPDATE_DELAY = 0.1f;
private const byte SCORE_UPDATE_MESSAGE = 5;
private const uint SCORE_UPDATE_MESSAGE_SIZE = 1 + 4;
// cache of local balls that we are sending updates for
private readonly Dictionary<int, P2PNetworkBall> m_localBalls = new Dictionary<int, P2PNetworkBall>();
// reusable buffer to read network data into
private readonly byte[] readBuffer = new byte[LOCAL_BALLS_UPDATE_MESSATE_SIZE_MAX];
// temporary time-sync cache of the calculated time offsets
private readonly Dictionary<ulong, List<float>> m_remoteSyncTimeCache = new Dictionary<ulong, List<float>>();
// temporary time-sync cache of the last sent message
private readonly Dictionary<ulong, float> m_remoteSentTimeCache = new Dictionary<ulong, float>();
// the delegate to handle start-time coordination
private StartTimeOffer m_startTimeOfferCallback;
#endregion
public P2PManager()
{
Net.SetPeerConnectRequestCallback(PeerConnectRequestCallback);
Net.SetConnectionStateChangedCallback(ConnectionStateChangedCallback);
}
public void UpdateNetwork()
{
if (m_remotePlayers.Count == 0)
return;
// check for new messages
Packet packet;
while ((packet = Net.ReadPacket()) != null)
{
if (!m_remotePlayers.ContainsKey(packet.SenderID))
continue;
packet.ReadBytes(readBuffer);
switch (readBuffer[0])
{
case TIME_SYNC_MESSAGE:
Assert.AreEqual(TIME_SYNC_MESSAGE_SIZE, packet.Size);
ReadTimeSyncMessage(packet.SenderID, readBuffer);
break;
case START_TIME_MESSAGE:
Assert.AreEqual(START_TIME_MESSAGE_SIZE, packet.Size);
ReceiveMatchStartTimeOffer(packet.SenderID, readBuffer);
break;
case BACKBOARD_UPDATE_MESSAGE:
Assert.AreEqual(BACKBOARD_UPDATE_MESSAGE_SIZE, packet.Size);
ReceiveBackboardUpdate(packet.SenderID, readBuffer);
break;
case LOCAL_BALLS_UPDATE_MESSAGE:
ReceiveBallTransforms(packet.SenderID, readBuffer, packet.Size);
break;
case SCORE_UPDATE_MESSAGE:
Assert.AreEqual(SCORE_UPDATE_MESSAGE_SIZE, packet.Size);
ReceiveScoredUpdate(packet.SenderID, readBuffer);
break;
}
}
if (Time.time >= m_timeForNextBallUpdate && m_localBalls.Count > 0)
{
SendLocalBallTransforms();
}
}
#region Connection Management
// adds a remote player to establish a connection to, or accept a connection from
public void AddRemotePlayer(RemotePlayer player)
{
if (!m_remotePlayers.ContainsKey (player.ID))
{
m_remotePlayers[player.ID] = new RemotePlayerData();
m_remotePlayers[player.ID].state = PeerConnectionState.Unknown;
m_remotePlayers [player.ID].player = player;
// ID comparison is used to decide who Connects and who Accepts
if (PlatformManager.MyID < player.ID)
{
Debug.Log ("P2P Try Connect to: " + player.ID);
Net.Connect (player.ID);
}
}
}
public void DisconnectAll()
{
foreach (var id in m_remotePlayers.Keys)
{
Net.Close(id);
}
m_remotePlayers.Clear();
}
void PeerConnectRequestCallback(Message<NetworkingPeer> msg)
{
if (m_remotePlayers.ContainsKey(msg.Data.ID))
{
Debug.LogFormat("P2P Accepting Connection request from {0}", msg.Data.ID);
Net.Accept(msg.Data.ID);
}
else
{
Debug.LogFormat("P2P Ignoring unauthorized Connection request from {0}", msg.Data.ID);
}
}
void ConnectionStateChangedCallback(Message<NetworkingPeer> msg)
{
Debug.LogFormat("P2P {0} Connection state changed to {1}", msg.Data.ID, msg.Data.State);
if (m_remotePlayers.ContainsKey(msg.Data.ID))
{
m_remotePlayers[msg.Data.ID].state = msg.Data.State;
switch (msg.Data.State)
{
case PeerConnectionState.Connected:
if (PlatformManager.MyID < msg.Data.ID)
{
SendTimeSyncMessage(msg.Data.ID);
}
break;
case PeerConnectionState.Timeout:
if (PlatformManager.MyID < msg.Data.ID)
{
Net.Connect(msg.Data.ID);
}
break;
case PeerConnectionState.Closed:
m_remotePlayers.Remove(msg.Data.ID);
break;
}
}
}
#endregion
#region Time Synchronizaiton
// This section implements some basic time synchronization between the players.
// The algorithm is:
// -Send a time-sync message and receive a time-sync message response
// -Estimate time offset
// -Repeat several times
// -Average values discarding any statistical anomalies
// Normally delays would be added in case there is intermittent network congestion
// however the match times are so short we don't do that here. Also, if one client
// pauses their game and Unity stops their simulation, all bets are off for time
// synchronization. Depending on the goals of your app, you could either reinitiate
// time synchronization, or just disconnect that player.
void SendTimeSyncMessage(ulong remoteID)
{
if (!m_remoteSyncTimeCache.ContainsKey(remoteID))
{
m_remoteSyncTimeCache[remoteID] = new List<float>();
}
float time = Time.realtimeSinceStartup;
m_remoteSentTimeCache[remoteID] = time;
byte[] buf = new byte[TIME_SYNC_MESSAGE_SIZE];
buf[0] = TIME_SYNC_MESSAGE;
int offset = 1;
PackFloat(time, buf, ref offset);
Net.SendPacket(remoteID, buf, SendPolicy.Reliable);
}
void ReadTimeSyncMessage(ulong remoteID, byte[] msg)
{
if (!m_remoteSentTimeCache.ContainsKey(remoteID))
{
SendTimeSyncMessage(remoteID);
return;
}
int offset = 1;
float remoteTime = UnpackFloat(msg, ref offset);
float now = Time.realtimeSinceStartup;
float latency = (now - m_remoteSentTimeCache[remoteID]) / 2;
float remoteTimeOffset = now - (remoteTime + latency);
m_remoteSyncTimeCache[remoteID].Add(remoteTimeOffset);
if (m_remoteSyncTimeCache[remoteID].Count < TIME_SYNC_MESSAGE_COUNT)
{
SendTimeSyncMessage(remoteID);
}
else
{
if (PlatformManager.MyID < remoteID)
{
// this client started the sync, need to send one last message to
// the remote so they can finish their sync calculation
SendTimeSyncMessage(remoteID);
}
// sort the times and remember the median
m_remoteSyncTimeCache[remoteID].Sort();
float median = m_remoteSyncTimeCache[remoteID][TIME_SYNC_MESSAGE_COUNT/2];
// calucate the mean and standard deviation
double mean = 0;
foreach (var time in m_remoteSyncTimeCache[remoteID])
{
mean += time;
}
mean /= TIME_SYNC_MESSAGE_COUNT;
double std_dev = 0;
foreach (var time in m_remoteSyncTimeCache[remoteID])
{
std_dev += (mean-time)*(mean-time);
}
std_dev = Math.Sqrt(std_dev)/TIME_SYNC_MESSAGE_COUNT;
// time delta is the mean of the values less than 1 standard deviation from the median
mean = 0;
int meanCount = 0;
foreach (var time in m_remoteSyncTimeCache[remoteID])
{
if (Math.Abs(time-median) < std_dev)
{
mean += time;
meanCount++;
}
}
mean /= meanCount;
Debug.LogFormat("Time offset to {0} is {1}", remoteID, mean);
m_remoteSyncTimeCache.Remove(remoteID);
m_remoteSentTimeCache.Remove(remoteID);
m_remotePlayers[remoteID].remoteTimeOffset = (float)mean;
// now that times are synchronized, lets try to coordinate the
// start time for the match
OfferMatchStartTime();
}
}
float ShiftRemoteTime(ulong remoteID, float remoteTime)
{
if (m_remotePlayers.ContainsKey(remoteID))
{
return remoteTime + m_remotePlayers[remoteID].remoteTimeOffset;
}
else
{
return remoteTime;
}
}
#endregion
#region Match Start Coordination
// Since all the clients will calculate a slightly different start time, this
// message tries to coordinate the match start time to be the lastest of all
// the clients in the match.
// Delegate to coordiate match start times - the return value is our start time
// and the argument is the remote start time, or 0 if that hasn't been given yet.
public delegate float StartTimeOffer(float remoteTime);
public StartTimeOffer StartTimeOfferCallback
{
private get { return m_startTimeOfferCallback; }
set { m_startTimeOfferCallback = value; }
}
void OfferMatchStartTime()
{
byte[] buf = new byte[START_TIME_MESSAGE_SIZE];
buf[0] = START_TIME_MESSAGE;
int offset = 1;
PackFloat(StartTimeOfferCallback(0), buf, ref offset);
foreach (var remoteID in m_remotePlayers.Keys)
{
if (m_remotePlayers [remoteID].state == PeerConnectionState.Connected)
{
Net.SendPacket (remoteID, buf, SendPolicy.Reliable);
}
}
}
void ReceiveMatchStartTimeOffer(ulong remoteID, byte[] msg)
{
int offset = 1;
float remoteTime = UnpackTime(remoteID, msg, ref offset);
StartTimeOfferCallback(remoteTime);
}
#endregion
#region Backboard Transforms
public void SendBackboardUpdate(float time, Vector3 pos, Vector3 moveDir, Vector3 nextMoveDir)
{
byte[] buf = new byte[BACKBOARD_UPDATE_MESSAGE_SIZE];
buf[0] = BACKBOARD_UPDATE_MESSAGE;
int offset = 1;
PackFloat(time, buf, ref offset);
PackVector3(pos, buf, ref offset);
PackVector3(moveDir, buf, ref offset);
PackVector3(nextMoveDir, buf, ref offset);
foreach (KeyValuePair<ulong,RemotePlayerData> player in m_remotePlayers)
{
if (player.Value.state == PeerConnectionState.Connected)
{
Net.SendPacket(player.Key, buf, SendPolicy.Reliable);
}
}
}
void ReceiveBackboardUpdate(ulong remoteID, byte[] msg)
{
int offset = 1;
float remoteTime = UnpackTime(remoteID, msg, ref offset);
Vector3 pos = UnpackVector3(msg, ref offset);
Vector3 moveDir = UnpackVector3(msg, ref offset);
Vector3 nextMoveDir = UnpackVector3(msg, ref offset);
var goal = m_remotePlayers [remoteID].player.Goal;
goal.RemoteBackboardUpdate(remoteTime, pos, moveDir, nextMoveDir);
}
#endregion
#region Ball Tansforms
public void AddNetworkBall(GameObject ball)
{
m_localBalls[ball.GetInstanceID()] = ball.AddComponent<P2PNetworkBall>();
}
public void RemoveNetworkBall(GameObject ball)
{
m_localBalls.Remove(ball.GetInstanceID());
}
void SendLocalBallTransforms()
{
m_timeForNextBallUpdate = Time.time + LOCAL_BALLS_UPDATE_DELAY;
int msgSize = 1 + 4 + (m_localBalls.Count * (1 + 4 + 12 + 12));
byte[] sendBuffer = new byte[msgSize];
sendBuffer[0] = LOCAL_BALLS_UPDATE_MESSAGE;
int offset = 1;
PackFloat(Time.realtimeSinceStartup, sendBuffer, ref offset);
foreach (var ball in m_localBalls.Values)
{
PackBool(ball.IsHeld(), sendBuffer, ref offset);
PackInt32(ball.gameObject.GetInstanceID(), sendBuffer, ref offset);
PackVector3(ball.transform.localPosition, sendBuffer, ref offset);
PackVector3(ball.velocity, sendBuffer, ref offset);
}
foreach (KeyValuePair<ulong, RemotePlayerData> player in m_remotePlayers)
{
if (player.Value.state == PeerConnectionState.Connected)
{
Net.SendPacket(player.Key, sendBuffer, SendPolicy.Unreliable);
}
}
}
void ReceiveBallTransforms(ulong remoteID, byte[] msg, ulong msgLength)
{
int offset = 1;
float remoteTime = UnpackTime(remoteID, msg, ref offset);
// because we're using unreliable networking the packets could come out of order
// and the best thing to do is just ignore old packets because the data isn't
// very useful anyway
if (remoteTime < m_remotePlayers[remoteID].lastReceivedBallsTime)
return;
m_remotePlayers[remoteID].lastReceivedBallsTime = remoteTime;
// loop over all ball updates in the message
while (offset != (int)msgLength)
{
bool isHeld = UnpackBool(msg, ref offset);
int instanceID = UnpackInt32(msg, ref offset);
Vector3 pos = UnpackVector3(msg, ref offset);
Vector3 vel = UnpackVector3(msg, ref offset);
if (!m_remotePlayers[remoteID].activeBalls.ContainsKey(instanceID))
{
var newball = m_remotePlayers[remoteID].player.CreateBall().AddComponent<P2PNetworkBall>();
newball.transform.SetParent(m_remotePlayers[remoteID].player.transform.parent);
m_remotePlayers[remoteID].activeBalls[instanceID] = newball;
}
var ball = m_remotePlayers[remoteID].activeBalls[instanceID];
if (ball)
{
ball.ProcessRemoteUpdate(remoteTime, isHeld, pos, vel);
}
}
}
#endregion
#region Score Updates
public void SendScoreUpdate(uint score)
{
byte[] buf = new byte[SCORE_UPDATE_MESSAGE_SIZE];
buf[0] = SCORE_UPDATE_MESSAGE;
int offset = 1;
PackUint32(score, buf, ref offset);
foreach (KeyValuePair<ulong, RemotePlayerData> player in m_remotePlayers)
{
if (player.Value.state == PeerConnectionState.Connected)
{
Net.SendPacket(player.Key, buf, SendPolicy.Reliable);
}
}
}
void ReceiveScoredUpdate(ulong remoteID, byte[] msg)
{
int offset = 1;
uint score = UnpackUint32(msg, ref offset);
m_remotePlayers[remoteID].player.ReceiveRemoteScore(score);
}
#endregion
#region Serialization
// This region contains basic data serialization logic. This sample doesn't warrant
// much optimization, but the opportunites are ripe those interested in the topic.
void PackVector3(Vector3 vec, byte[] buf, ref int offset)
{
PackFloat(vec.x, buf, ref offset);
PackFloat(vec.y, buf, ref offset);
PackFloat(vec.z, buf, ref offset);
}
Vector3 UnpackVector3(byte[] buf, ref int offset)
{
Vector3 vec;
vec.x = UnpackFloat(buf, ref offset);
vec.y = UnpackFloat(buf, ref offset);
vec.z = UnpackFloat(buf, ref offset);
return vec;
}
void PackQuaternion(Quaternion quat, byte[] buf, ref int offset)
{
PackFloat(quat.x, buf, ref offset);
PackFloat(quat.y, buf, ref offset);
PackFloat(quat.z, buf, ref offset);
PackFloat(quat.w, buf, ref offset);
}
void PackFloat(float value, byte[] buf, ref int offset)
{
Buffer.BlockCopy(BitConverter.GetBytes(value), 0, buf, offset, 4);
offset = offset + 4;
}
float UnpackFloat(byte[] buf, ref int offset)
{
float value = BitConverter.ToSingle(buf, offset);
offset += 4;
return value;
}
float UnpackTime(ulong remoteID, byte[] buf, ref int offset)
{
return ShiftRemoteTime(remoteID, UnpackFloat(buf, ref offset));
}
void PackInt32(int value, byte[] buf, ref int offset)
{
Buffer.BlockCopy(BitConverter.GetBytes(value), 0, buf, offset, 4);
offset = offset + 4;
}
int UnpackInt32(byte[] buf, ref int offset)
{
int value = BitConverter.ToInt32(buf, offset);
offset += 4;
return value;
}
void PackUint32(uint value, byte[] buf, ref int offset)
{
Buffer.BlockCopy(BitConverter.GetBytes(value), 0, buf, offset, 4);
offset = offset + 4;
}
uint UnpackUint32(byte[] buf, ref int offset)
{
uint value = BitConverter.ToUInt32(buf, offset);
offset += 4;
return value;
}
void PackBool(bool value, byte[] buf, ref int offset)
{
buf[offset++] = (byte)(value ? 1 : 0);
}
bool UnpackBool(byte[] buf, ref int offset)
{
return buf[offset++] != 0;;
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f418f541d32d24e7aad85c24970462d1
timeCreated: 1475634295
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,87 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections;
// This component handles network coordination for moving balls.
// Synchronizing moving objects that are under the influence of physics
// and other forces is somewhat of an art and this example only scratches
// the surface. Ultimately how you synchronize will depend on the requirements
// of your application and its tolerance for users seeing slightly different
// versions of the simulation.
public class P2PNetworkBall : MonoBehaviour
{
// the last time this ball locally collided with something
private float lastCollisionTime;
// cached reference to the GameObject's Rigidbody component
private Rigidbody rigidBody;
void Awake()
{
rigidBody = gameObject.GetComponent<Rigidbody>();
}
public Vector3 velocity
{
get { return rigidBody.velocity; }
}
public bool IsHeld()
{
return !rigidBody.useGravity;
}
public void ProcessRemoteUpdate(float remoteTime, bool isHeld, Vector3 pos, Vector3 vel)
{
if (isHeld)
{
transform.localPosition = pos;
}
// if we've collided since the update was sent, our state is going to be more accurate so
// it's better to ignore the update
else if (lastCollisionTime < remoteTime)
{
// To correct the position this sample directly moves the ball.
// Another approach would be to gradually lerp the ball there during
// FixedUpdate. However, that approach aggravates any errors that
// come from estimatePosition and estimateVelocity so the lerp
// should be done over few timesteps.
float deltaT = Time.realtimeSinceStartup - remoteTime;
transform.localPosition = estimatePosition(pos, vel, deltaT);
rigidBody.velocity = estimateVelocity(vel, deltaT);
// if the ball is transitioning from held to ballistic, we need to
// update the RigidBody parameters
if (IsHeld())
{
rigidBody.useGravity = true;
rigidBody.detectCollisions = true;
}
}
}
// Estimates the new position assuming simple ballistic motion.
private Vector3 estimatePosition(Vector3 startPosition, Vector3 startVelocty, float time)
{
return startPosition + startVelocty * time + 0.5f * Physics.gravity * time * time;
}
// Estimates the new velocity assuming ballistic motion and drag.
private Vector3 estimateVelocity(Vector3 startVelocity, float time)
{
return startVelocity + Physics.gravity * time * Mathf.Clamp01 (1 - rigidBody.drag * time);
}
void OnCollisionEnter(Collision collision)
{
lastCollisionTime = Time.realtimeSinceStartup;
}
void OnDestroy()
{
PlatformManager.P2P.RemoveNetworkBall(gameObject);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b4360c472d4f2ab498faa9c614ac7c80
timeCreated: 1476304390
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,65 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using System.Collections;
// This component handles network coordination for the moving backboard.
// Although there is randomness in the next direction, the movement is
// otherwise completely predictable, much like a moving platform or door,
// thus we only need to send occasional updates. If the position of the
// backboard is not correct, the GoalMover will gradually nudge it in the
// correct direction until the local and remote motion is synchronized.
public class P2PNetworkGoal : MonoBehaviour
{
// cached reference to the associated GoalMover component
private GoalMover m_goal;
// the last move direction we sent to remote clients
private Vector3 m_lastSentMoveDirection;
private bool m_sendUpdates;
public bool SendUpdates
{
set { m_sendUpdates = value; }
}
void Awake()
{
m_goal = gameObject.GetComponent<GoalMover>();
}
void FixedUpdate ()
{
// since the backboard's movement is deterministic, we don't need to send position
// updates constantly, just when the move direction changes
if (m_sendUpdates && m_goal.MoveDirection != m_lastSentMoveDirection)
{
SendBackboardUpdate();
}
}
public void SendBackboardUpdate()
{
m_lastSentMoveDirection = m_goal.MoveDirection;
float time = Time.realtimeSinceStartup;
PlatformManager.P2P.SendBackboardUpdate(
time, transform.localPosition,
m_goal.MoveDirection, m_goal.NextMoveDirection);
}
// message from the remote player with new transforms
public void RemoteBackboardUpdate(float remoteTime, Vector3 pos, Vector3 moveDir, Vector3 nextMoveDir)
{
// interpolate the position forward since the backboard would have moved over
// the time it took to send the message
float time = Time.realtimeSinceStartup;
float numMissedSteps = (time - remoteTime) / Time.fixedDeltaTime;
m_goal.ExpectedPosition = pos + (Mathf.Round(numMissedSteps) * moveDir);
m_goal.MoveDirection = moveDir;
m_goal.NextMoveDirection = nextMoveDir;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1aef3541237f243e4856d8913668f9b8
timeCreated: 1476575180
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,195 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using Oculus.Platform;
using Oculus.Platform.Models;
public class PlatformManager : MonoBehaviour
{
private static PlatformManager s_instance;
private MatchmakingManager m_matchmaking;
private P2PManager m_p2p;
private LeaderboardManager m_leaderboards;
private AchievementsManager m_achievements;
private State m_currentState;
// my Application-scoped Oculus ID
private ulong m_myID;
// my Oculus user name
private string m_myOculusID;
void Update()
{
m_p2p.UpdateNetwork();
m_leaderboards.CheckForUpdates();
}
#region Initialization and Shutdown
void Awake()
{
// make sure only one instance of this manager ever exists
if (s_instance != null)
{
Destroy(gameObject);
return;
}
s_instance = this;
DontDestroyOnLoad(gameObject);
Core.Initialize();
m_matchmaking = new MatchmakingManager();
m_p2p = new P2PManager();
m_leaderboards = new LeaderboardManager();
m_achievements = new AchievementsManager();
}
void Start()
{
// First thing we should do is perform an entitlement check to make sure
// we successfully connected to the Oculus Platform Service.
Entitlements.IsUserEntitledToApplication().OnComplete(IsEntitledCallback);
}
void IsEntitledCallback(Message msg)
{
if (msg.IsError)
{
TerminateWithError(msg);
return;
}
// Next get the identity of the user that launched the Application.
Users.GetLoggedInUser().OnComplete(GetLoggedInUserCallback);
}
void GetLoggedInUserCallback(Message<User> msg)
{
if (msg.IsError)
{
TerminateWithError(msg);
return;
}
m_myID = msg.Data.ID;
m_myOculusID = msg.Data.OculusID;
TransitionToState(State.WAITING_TO_PRACTICE_OR_MATCHMAKE);
Achievements.CheckForAchievmentUpdates();
}
// In this example, for most errors, we terminate the Application. A full App would do
// something more graceful.
public static void TerminateWithError(Message msg)
{
Debug.Log("Error: " + msg.GetError().Message);
UnityEngine.Application.Quit();
}
public void QuitButtonPressed()
{
UnityEngine.Application.Quit();
}
void OnApplicationQuit()
{
// be a good matchmaking citizen and leave any queue immediately
Matchmaking.LeaveQueue();
}
#endregion
#region Properties
public static MatchmakingManager Matchmaking
{
get { return s_instance.m_matchmaking; }
}
public static P2PManager P2P
{
get { return s_instance.m_p2p; }
}
public static LeaderboardManager Leaderboards
{
get { return s_instance.m_leaderboards; }
}
public static AchievementsManager Achievements
{
get { return s_instance.m_achievements; }
}
public static State CurrentState
{
get { return s_instance.m_currentState; }
}
public static ulong MyID
{
get
{
if (s_instance != null)
{
return s_instance.m_myID;
}
else
{
return 0;
}
}
}
public static string MyOculusID
{
get
{
if (s_instance != null && s_instance.m_myOculusID != null)
{
return s_instance.m_myOculusID;
}
else
{
return string.Empty;
}
}
}
#endregion
#region State Management
public enum State
{
// loading platform library, checking application entitlement,
// getting the local user info
INITIALIZING,
// waiting on the user to join a matchmaking queue or play a practice game
WAITING_TO_PRACTICE_OR_MATCHMAKE,
// waiting for the match to start or viewing results
MATCH_TRANSITION,
// actively playing a practice match
PLAYING_A_LOCAL_MATCH,
// actively playing an online match
PLAYING_A_NETWORKED_MATCH,
};
public static void TransitionToState(State newState)
{
if (s_instance && s_instance.m_currentState != newState)
{
s_instance.m_currentState = newState;
}
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 65808a6a27b5f4917a2b9a171e240753
timeCreated: 1475341418
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,133 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
// The base Player component manages the balls that are in play. Besides spawning new balls,
// old balls are destroyed when too many are around or the Player object itself is destroyed.
public abstract class Player : MonoBehaviour {
// maximum number of balls allowed at a time
public const uint MAX_BALLS = 6;
// the initial force to impart when shooting a ball
private const float INITIAL_FORCE = 870f;
// delay time before a new ball will spawn.
private const float RESPAWN_SECONDS = 2.0f;
// current score for the player
private uint m_score;
// cached reference to the Text component to render the score
private Text m_scoreUI;
// prefab for the GameObject representing a ball
private GameObject m_ballPrefab;
// gameobject for the position and orientation of where the ball will be shot
private BallEjector m_ballEjector;
// queue of active balls for the player to make sure too many arent in play
private Queue<GameObject> m_balls = new Queue<GameObject>();
// reference to a ball that hasn't been shot yet and is tied to the camera
private GameObject m_heldBall;
// when to spawn a new ball
private float m_nextSpawnTime;
#region Properties
public virtual uint Score
{
get { return m_score; }
set
{
m_score = value;
if (m_scoreUI)
{
m_scoreUI.text = m_score.ToString();
}
}
}
public GameObject BallPrefab
{
set { m_ballPrefab = value; }
}
protected bool HasBall
{
get { return m_heldBall != null; }
}
#endregion
void Start()
{
m_ballEjector = transform.GetComponentInChildren<BallEjector>();
m_scoreUI = transform.parent.GetComponentInChildren<Text>();
m_scoreUI.text = "0";
}
public GameObject CreateBall()
{
if (m_balls.Count >= MAX_BALLS)
{
Destroy(m_balls.Dequeue());
}
var ball = Instantiate(m_ballPrefab);
m_balls.Enqueue(ball);
ball.transform.position = m_ballEjector.transform.position;
ball.transform.SetParent(m_ballEjector.transform, true);
ball.GetComponent<Rigidbody>().useGravity = false;
ball.GetComponent<Rigidbody>().detectCollisions = false;
ball.GetComponent<DetectBasket>().Player = this;
return ball;
}
protected GameObject CheckSpawnBall()
{
switch (PlatformManager.CurrentState)
{
case PlatformManager.State.WAITING_TO_PRACTICE_OR_MATCHMAKE:
case PlatformManager.State.PLAYING_A_LOCAL_MATCH:
case PlatformManager.State.PLAYING_A_NETWORKED_MATCH:
if (Time.time >= m_nextSpawnTime && !HasBall)
{
m_heldBall = CreateBall();
return m_heldBall;
}
break;
}
return null;
}
protected GameObject ShootBall()
{
GameObject ball = m_heldBall;
m_heldBall = null;
ball.GetComponent<Rigidbody>().useGravity = true;
ball.GetComponent<Rigidbody>().detectCollisions = true;
ball.GetComponent<Rigidbody>().AddForce(m_ballEjector.transform.forward * INITIAL_FORCE, ForceMode.Acceleration);
ball.transform.SetParent(transform.parent, true);
m_nextSpawnTime = Time.time + RESPAWN_SECONDS;
return ball;
}
void OnDestroy()
{
foreach (var ball in m_balls)
{
Destroy(ball);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9f31217f54e40894f84c58432a8f7e65
timeCreated: 1475264829
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using UnityEngine.UI;
using Oculus.Platform.Models;
public class PlayerArea : MonoBehaviour
{
// the prefab for the ball that players will shoot
[SerializeField] private GameObject m_ballPrefab = null;
// cached gameobject that where the player camera will move to
private GameObject m_playerHead;
// cached Text component where we'll render the player's name
private Text m_nameText;
// cached component used to align the backboard movement between devices
private P2PNetworkGoal m_p2pGoal;
public Player Player
{
get { return m_playerHead.GetComponent<Player>(); }
}
public Text NameText
{
get { return m_nameText; }
}
void Awake()
{
m_playerHead = gameObject.transform.Find("Player Head").gameObject;
m_nameText = gameObject.GetComponentsInChildren<Text>()[1];
m_p2pGoal = gameObject.GetComponentInChildren<P2PNetworkGoal> ();
}
public T SetupForPlayer<T>(string name) where T : Player
{
var oldplayer = m_playerHead.GetComponent<Player>();
if (oldplayer) Destroy(oldplayer);
var player = m_playerHead.AddComponent<T>();
player.BallPrefab = m_ballPrefab;
m_nameText.text = name;
if (player is RemotePlayer)
{
(player as RemotePlayer).Goal = m_p2pGoal;
m_p2pGoal.SendUpdates = false;
}
else if (player is LocalPlayer)
{
m_p2pGoal.SendUpdates = true;
}
else
{
m_p2pGoal.SendUpdates = false;
}
return player;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4bf1a3ead6c1aa341873bdfda4366001
timeCreated: 1475265143
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
namespace Oculus.Platform.Samples.VrHoops
{
using Oculus.Platform.Models;
public class RemotePlayer : Player
{
private User m_user;
private P2PNetworkGoal m_goal;
public User User
{
set { m_user = value; }
}
public ulong ID
{
get { return m_user.ID; }
}
public P2PNetworkGoal Goal
{
get { return m_goal; }
set { m_goal = value; }
}
public override uint Score
{
set
{
// For now we ignore the score determined from locally scoring backets.
// To get an indication of how close the physics simulations were between devices,
// or whether the remote player was cheating, an estimate of the score could be
// kept and compared against what the remote player was sending us.
}
}
public void ReceiveRemoteScore(uint score)
{
base.Score = score;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 125c22697d9e63447888218eccbc7005
timeCreated: 1475264820
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
namespace Oculus.Platform.Samples.VrHoops
{
using UnityEngine;
using UnityEngine.UI;
// Helper class to attach to the main camera that raycasts where the
// user is looking to select/deselect Buttons.
public class VREyeRaycaster : MonoBehaviour
{
[SerializeField] private UnityEngine.EventSystems.EventSystem m_eventSystem = null;
private Button m_currentButton;
void Update ()
{
RaycastHit hit;
Button button = null;
// do a forward raycast to see if we hit a Button
if (Physics.Raycast(transform.position, transform.forward, out hit, 50f))
{
button = hit.collider.GetComponent<Button>();
}
if (button != null)
{
if (m_currentButton != button)
{
m_currentButton = button;
m_currentButton.Select();
}
}
else if (m_currentButton != null)
{
m_currentButton = null;
if (m_eventSystem != null)
{
m_eventSystem.SetSelectedGameObject(null);
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b02d3b7f6a849724695a1255745c4fa7
timeCreated: 1475533243
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: