|
|
- 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
- }
- }
|