forked from cgvr/DeltaVR
Initial Commit
This commit is contained in:
25
Assets/Oculus/Platform/Samples/VrHoops/Scripts/AIPlayer.cs
Normal file
25
Assets/Oculus/Platform/Samples/VrHoops/Scripts/AIPlayer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71702635af79217469ed41ba39db8d5a
|
||||
timeCreated: 1475264776
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66d441ddf01234331b50e929139f4780
|
||||
timeCreated: 1477071923
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Oculus.Platform.Samples.VrHoops
|
||||
{
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
public class BallEjector : MonoBehaviour {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4164fa75939f1e46a3e36dfbdc7f821
|
||||
timeCreated: 1474514990
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ebf96caaf397684b86c4ff4d566798f
|
||||
timeCreated: 1474514266
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a63d0cd5dd6d39a4abd35114563fe347
|
||||
timeCreated: 1475105001
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
37
Assets/Oculus/Platform/Samples/VrHoops/Scripts/FlyText.cs
Normal file
37
Assets/Oculus/Platform/Samples/VrHoops/Scripts/FlyText.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc96f5380d7d743d9ae91f11379eb85b
|
||||
timeCreated: 1477078886
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
84
Assets/Oculus/Platform/Samples/VrHoops/Scripts/GoalMover.cs
Normal file
84
Assets/Oculus/Platform/Samples/VrHoops/Scripts/GoalMover.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbea2c3cb080a064f84d3bd86c2f3a53
|
||||
timeCreated: 1475173562
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5525a4fa9d4898438f479b1c25ff8b4
|
||||
timeCreated: 1476809789
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac564d79b10fc01448f93b8dcc74d3d0
|
||||
timeCreated: 1475264810
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e31d7fbc2f31b7499684c9e87fc8454
|
||||
timeCreated: 1475257003
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa69df7dcb6814fab9439789f1e23e2e
|
||||
timeCreated: 1475629968
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
579
Assets/Oculus/Platform/Samples/VrHoops/Scripts/P2PManager.cs
Normal file
579
Assets/Oculus/Platform/Samples/VrHoops/Scripts/P2PManager.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f418f541d32d24e7aad85c24970462d1
|
||||
timeCreated: 1475634295
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4360c472d4f2ab498faa9c614ac7c80
|
||||
timeCreated: 1476304390
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1aef3541237f243e4856d8913668f9b8
|
||||
timeCreated: 1476575180
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65808a6a27b5f4917a2b9a171e240753
|
||||
timeCreated: 1475341418
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
133
Assets/Oculus/Platform/Samples/VrHoops/Scripts/Player.cs
Normal file
133
Assets/Oculus/Platform/Samples/VrHoops/Scripts/Player.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f31217f54e40894f84c58432a8f7e65
|
||||
timeCreated: 1475264829
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
64
Assets/Oculus/Platform/Samples/VrHoops/Scripts/PlayerArea.cs
Normal file
64
Assets/Oculus/Platform/Samples/VrHoops/Scripts/PlayerArea.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bf1a3ead6c1aa341873bdfda4366001
|
||||
timeCreated: 1475265143
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 125c22697d9e63447888218eccbc7005
|
||||
timeCreated: 1475264820
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b02d3b7f6a849724695a1255745c4fa7
|
||||
timeCreated: 1475533243
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user