Newer
Older
CGTrack / Assets / Oculus / Platform / Samples / VrHoops / Scripts / P2PManager.cs
@Pascal Syma Pascal Syma on 25 Jul 2021 16 KB Initial Commit
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
	}
}