diff --git a/Editor/EditorUtils.cs b/Editor/EditorUtils.cs new file mode 100644 index 0000000..ab66d21 --- /dev/null +++ b/Editor/EditorUtils.cs @@ -0,0 +1,48 @@ +#if UNITY_EDITOR + +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace VelNet.Editor +{ + public class EditorUtils : MonoBehaviour + { + [MenuItem("VelNet/Check For Duplicate NetworkIds", false, 10)] + private static void CheckDuplicateNetworkIds() + { + NetworkObject[] objs = FindObjectsOfType(); + Dictionary ids = new Dictionary(); + foreach (NetworkObject o in objs) + { + if (!o.isSceneObject) continue; + + if (ids.ContainsKey(o.sceneNetworkId) || o.sceneNetworkId < 100) + { + if (ids.ContainsKey(o.sceneNetworkId)) + { + Debug.Log($"Found duplicated id: {o.name} {ids[o.sceneNetworkId].name}", o); + } + else + { + Debug.Log($"Found duplicated id: {o.name} {o.sceneNetworkId}", o); + } + + o.sceneNetworkId = 100; + while (ids.ContainsKey(o.sceneNetworkId)) + { + o.sceneNetworkId += 1; + } + + PrefabUtility.RecordPrefabInstancePropertyModifications(o); + EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); + } + + ids.Add(o.sceneNetworkId, o); + } + } + } +} +#endif \ No newline at end of file diff --git a/Editor/EditorUtils.cs.meta b/Editor/EditorUtils.cs.meta new file mode 100644 index 0000000..a0118c0 --- /dev/null +++ b/Editor/EditorUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f2f5f489d44f614c96bcf8f493c787d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VelNet.Editor.asmdef b/Editor/VelNet.Editor.asmdef new file mode 100644 index 0000000..044643a --- /dev/null +++ b/Editor/VelNet.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "VelNet.Editor", + "rootNamespace": "VelNet.Editor", + "references": [ + "GUID:1e55e2c4387020247a1ae212bbcbd381" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/VelNet.Editor.asmdef.meta b/Editor/VelNet.Editor.asmdef.meta new file mode 100644 index 0000000..4389fa2 --- /dev/null +++ b/Editor/VelNet.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ae0703a992a8fe347978b1cd2dd2d7a9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkObject.cs b/Runtime/NetworkObject.cs index 5c605ac..0dd136a 100644 --- a/Runtime/NetworkObject.cs +++ b/Runtime/NetworkObject.cs @@ -62,7 +62,7 @@ namespace VelNet } else { - owner.SendMessage(this, index.ToString(), message, reliable); + VelNetPlayer.SendMessage(this, (byte)index, message, reliable); } } @@ -76,15 +76,15 @@ namespace VelNet // send the message and an identifier for which component it belongs to int index = syncedComponents.IndexOf(component); - owner.SendGroupMessage(this, group, index.ToString(), message, reliable); + VelNetPlayer.SendGroupMessage(this, group, (byte)index, message, reliable); } - public void ReceiveBytes(string identifier, byte[] message) + public void ReceiveBytes(byte componentIdx, byte[] message) { // send the message to the right component try { - syncedComponents[int.Parse(identifier)].ReceiveBytes(message); + syncedComponents[componentIdx].ReceiveBytes(message); } catch (Exception e) { @@ -128,6 +128,7 @@ namespace VelNet foreach (NetworkComponent c in comps) { c.networkObject = t; + PrefabUtility.RecordPrefabInstancePropertyModifications(c); } PrefabUtility.RecordPrefabInstancePropertyModifications(t); } diff --git a/Runtime/Util/BinaryWriterExtensions.cs b/Runtime/Util/BinaryWriterExtensions.cs index f43524d..309a239 100644 --- a/Runtime/Util/BinaryWriterExtensions.cs +++ b/Runtime/Util/BinaryWriterExtensions.cs @@ -8,6 +8,8 @@ namespace VelNet { public static class BinaryWriterExtensions { + #region Writers + public static void Write(this BinaryWriter writer, Vector3 v) { writer.Write(v.x); @@ -23,6 +25,18 @@ namespace VelNet writer.Write(q.w); } + public static void Write(this BinaryWriter writer, Color c) + { + writer.Write(c.r); + writer.Write(c.g); + writer.Write(c.b); + writer.Write(c.a); + } + + #endregion + + #region Readers + public static Vector3 ReadVector3(this BinaryReader reader) { return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); @@ -31,19 +45,48 @@ namespace VelNet public static Quaternion ReadQuaternion(this BinaryReader reader) { return new Quaternion( - reader.ReadSingle(), - reader.ReadSingle(), - reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), reader.ReadSingle() ); } + public static Color ReadColor(this BinaryReader reader) + { + return new Color( + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle() + ); + } + + #endregion + + + public static bool BytesSame(byte[] b1, byte[] b2) + { + if (b1 == null && b2 != null) return false; // only one null + if (b1 != null && b2 == null) return false; // only one null + if (b1 == null) return true; // both null + + // length doesn't match + if (b1.Length != b2.Length) + { + return false; + } + + // check if any bytes are different + return !b1.Where((t, i) => t != b2[i]).Any(); + } + /// /// Compresses the list of bools into bytes using a bitmask /// public static byte[] GetBitmasks(this IEnumerable bools) { - List values = bools.ToList(); + List values = bools.ToList(); List bytes = new List(); for (int b = 0; b < Mathf.Ceil(values.Count / 8f); b++) { @@ -61,7 +104,7 @@ namespace VelNet return bytes.ToArray(); } - + public static List GetBitmaskValues(this IEnumerable bytes) { List l = new List(); @@ -72,7 +115,7 @@ namespace VelNet return l; } - + public static List GetBitmaskValues(this byte b) { List l = new List(); @@ -83,11 +126,10 @@ namespace VelNet return l; } - + public static bool GetBitmaskValue(this byte b, int index) { return (b & (1 << index)) != 0; } - } } \ No newline at end of file diff --git a/Runtime/Util/NetworkSerializedObject.cs b/Runtime/Util/NetworkSerializedObject.cs index 0569eed..7836f2b 100644 --- a/Runtime/Util/NetworkSerializedObject.cs +++ b/Runtime/Util/NetworkSerializedObject.cs @@ -1,6 +1,6 @@ -using System.Collections; +using System; +using System.Collections; using UnityEngine; -using UnityEngine.Serialization; namespace VelNet { @@ -9,6 +9,15 @@ namespace VelNet [Tooltip("Send rate of this object. This caps out at the framerate of the game.")] public float serializationRateHz = 30; + /// + /// If the data hasn't changed, only sends updates across the network at 1Hz + /// + public bool hybridOnChangeCompression = true; + + private byte[] lastSentBytes; + private double lastSendTime; + private const double slowSendInterval = 2; + protected virtual void Awake() { StartCoroutine(SendMessageUpdate()); @@ -18,9 +27,31 @@ namespace VelNet { while (true) { - if (IsMine) + try { - SendBytes(SendState()); + if (IsMine && enabled) + { + byte[] newBytes = SendState(); + if (hybridOnChangeCompression) + { + if (Time.timeAsDouble - lastSendTime > slowSendInterval || !BinaryWriterExtensions.BytesSame(lastSentBytes, newBytes)) + { + SendBytes(newBytes); + lastSendTime = Time.timeAsDouble; + } + } + else + { + SendBytes(newBytes); + lastSendTime = Time.timeAsDouble; + } + + lastSentBytes = newBytes; + } + } + catch (Exception e) + { + Debug.LogError(e); } yield return new WaitForSeconds(1f / serializationRateHz); diff --git a/Runtime/Util/NetworkSerializedObjectStream.cs b/Runtime/Util/NetworkSerializedObjectStream.cs index 161791f..5808afd 100644 --- a/Runtime/Util/NetworkSerializedObjectStream.cs +++ b/Runtime/Util/NetworkSerializedObjectStream.cs @@ -2,7 +2,6 @@ using System.Collections; using System.IO; using UnityEngine; -using UnityEngine.Serialization; namespace VelNet { @@ -11,6 +10,16 @@ namespace VelNet [Tooltip("Send rate of this object. This caps out at the framerate of the game.")] public float serializationRateHz = 30; + /// + /// If the data hasn't changed, only sends updates across the network at 1Hz + /// + public bool hybridOnChangeCompression = true; + + private byte[] lastSentBytes; + private double lastSendTime; + private const double slowSendInterval = 2; + + protected virtual void Awake() { StartCoroutine(SendMessageUpdate()); @@ -22,12 +31,28 @@ namespace VelNet { try { - if (IsMine) + if (IsMine && enabled) { using MemoryStream mem = new MemoryStream(); using BinaryWriter writer = new BinaryWriter(mem); SendState(writer); - SendBytes(mem.ToArray()); + + byte[] newBytes = mem.ToArray(); + if (hybridOnChangeCompression) + { + if (Time.timeAsDouble - lastSendTime > slowSendInterval || !BinaryWriterExtensions.BytesSame(lastSentBytes, newBytes)) + { + SendBytes(newBytes); + lastSendTime = Time.timeAsDouble; + } + } + else + { + SendBytes(newBytes); + lastSendTime = Time.timeAsDouble; + } + + lastSentBytes = newBytes; } } catch (Exception e) @@ -44,7 +69,7 @@ namespace VelNet { using MemoryStream mem = new MemoryStream(message); using BinaryReader reader = new BinaryReader(mem); - + ReceiveState(reader); } diff --git a/Runtime/VelNetManager.cs b/Runtime/VelNetManager.cs index 8a2e447..0b5174b 100644 --- a/Runtime/VelNetManager.cs +++ b/Runtime/VelNetManager.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; @@ -8,20 +7,36 @@ using System.Threading; using UnityEngine; using System.Net; using UnityEngine.SceneManagement; +using System.IO; namespace VelNet { [AddComponentMenu("VelNet/VelNet Manager")] public class VelNetManager : MonoBehaviour { - public enum MessageType + public enum MessageSendType { - OTHERS = 0, - ALL = 1, - OTHERS_ORDERED = 2, - ALL_ORDERED = 3 + MESSAGE_OTHERS_ORDERED = 7, + MESSAGE_ALL_ORDERED = 8, + MESSAGE_LOGIN = 0, + MESSAGE_GETROOMS = 1, + MESSAGE_JOINROOM = 2, + MESSAGE_OTHERS = 3, + MESSAGE_ALL = 4, + MESSAGE_GROUP = 5, + MESSAGE_SETGROUP = 6 }; + public enum MessageType : byte + { + ObjectSync, + TakeOwnership, + Instantiate, + Destroy, + DeleteSceneObjects, + Custom + } + public string host; public int port; @@ -34,10 +49,11 @@ namespace VelNet private Thread clientReceiveThread; private Thread clientReceiveThreadUDP; public int userid = -1; - private int messagesReceived = 0; public readonly Dictionary players = new Dictionary(); + #region Callbacks + /// /// We just joined a room /// string - the room name @@ -61,9 +77,13 @@ namespace VelNet public static Action OnPlayerLeft; public static Action OnConnectedToServer; - public static Action MessageReceived; public static Action OnLoggedIn; - public static Action RoomsReceived; + public static Action RoomsReceived; + + public static Action MessageReceived; + public static Action CustomMessageReceived; + + #endregion public bool connected; @@ -85,7 +105,7 @@ namespace VelNet public static VelNetPlayer LocalPlayer => instance != null ? instance.players.Where(p => p.Value.isLocal).Select(p => p.Value).FirstOrDefault() : null; public static bool InRoom => LocalPlayer != null && LocalPlayer.room != "-1" && LocalPlayer.room != ""; public static string Room => LocalPlayer?.room; - + /// /// The player count in this room. /// -1 if not in a room. @@ -100,13 +120,59 @@ namespace VelNet public static bool IsConnected => instance != null && instance.connected && instance.udpConnected; + //this is for sending udp packets + private static readonly byte[] toSend = new byte[1024]; // Use this for initialization - public class Message + public abstract class Message + { + } + + public class ListedRoom + { + public string name; + public int numUsers; + + public override string ToString() + { + return "Room Name: " + name + "\tUsers: " + numUsers; + } + } + + public class LoginMessage : Message + { + public int userId; + } + + public class RoomsMessage : Message + { + public List rooms; + + public override string ToString() + { + return string.Join("\n", rooms); + } + } + + public class JoinMessage : Message + { + public int userId; + public string room; + } + + public class DataMessage : Message + { + public int senderId; + public byte[] data; + } + + public class ChangeMasterMessage : Message + { + public int masterId; + } + + public class ConnectedMessage : Message { - public int type; - public string text; - public int sender; } public readonly List receivedMessages = new List(); @@ -123,24 +189,13 @@ namespace VelNet SceneManager.sceneLoaded += (scene, mode) => { // add all local network objects - sceneObjects = FindObjectsOfType().Where(o=>o.isSceneObject).ToArray(); + sceneObjects = FindObjectsOfType().Where(o => o.isSceneObject).ToArray(); }; } - private IEnumerator Start() + private void Start() { ConnectToTcpServer(); - yield return null; - - try - { - OnConnectedToServer?.Invoke(); - } - // prevent errors in subscribers from breaking our code - catch (Exception e) - { - Debug.LogError(e); - } } @@ -151,6 +206,15 @@ namespace VelNet //Debug.Log(messagesReceived++); receivedMessages.Add(m); } + + try + { + MessageReceived?.Invoke(m); + } + catch (Exception e) + { + Debug.LogError(e); + } } private void Update() @@ -160,12 +224,26 @@ namespace VelNet //the main thread, which can do Unity stuff foreach (Message m in receivedMessages) { - switch (m.type) + switch (m) { - // when you join the server - case 0: - userid = m.sender; - Debug.Log("Joined server"); + case ConnectedMessage msg: + { + try + { + OnConnectedToServer?.Invoke(); + } + // prevent errors in subscribers from breaking our code + catch (Exception e) + { + Debug.LogError(e); + } + + break; + } + case LoginMessage lm: + { + userid = lm.userId; + Debug.Log("Joined server " + userid); try { @@ -179,33 +257,80 @@ namespace VelNet //start the udp thread clientReceiveThreadUDP = new Thread(ListenForDataUDP); - clientReceiveThreadUDP.IsBackground = true; clientReceiveThreadUDP.Start(); + break; - // if this message is for me, that means I joined a new room... - case 2 when userid == m.sender: + } + case RoomsMessage rm: { - string oldRoom = LocalPlayer?.room; + Debug.Log("Got Rooms Message:\n" + rm); - // we clear the list, but will recreate as we get messages from people in our room - players.Clear(); - masterPlayer = null; - - if (m.text != "") + try { - VelNetPlayer player = new VelNetPlayer - { - isLocal = true, - userid = m.sender, - room = m.text - }; + RoomsReceived?.Invoke(rm); + } + // prevent errors in subscribers from breaking our code + catch (Exception e) + { + Debug.LogError(e); + } - players.Add(userid, player); - if (m.text != "") + break; + } + case JoinMessage jm: + { + if (userid == jm.userId) //this is us + { + string oldRoom = LocalPlayer?.room; + + // we clear the list, but will recreate as we get messages from people in our room + players.Clear(); + masterPlayer = null; + + if (jm.room != "") { + VelNetPlayer player = new VelNetPlayer + { + isLocal = true, + userid = jm.userId, + room = jm.room + }; + + players.Add(userid, player); + try { - OnJoinedRoom?.Invoke(m.text); + OnJoinedRoom?.Invoke(jm.room); + } + // prevent errors in subscribers from breaking our code + catch (Exception e) + { + Debug.LogError(e); + } + } + // we just left a room + else + { + // delete all networkobjects that aren't sceneobjects or are null now + objects + .Where(kvp => kvp.Value == null || !kvp.Value.isSceneObject) + .Select(o => o.Key) + .ToList().ForEach(NetworkDestroy); + + // then remove references to the ones that are left + objects.Clear(); + + // empty all the groups + foreach (string group in instance.groups.Keys) + { + SetupMessageGroup(group, new List()); + } + + instance.groups.Clear(); + + try + { + OnLeftRoom?.Invoke(oldRoom); } // prevent errors in subscribers from breaking our code catch (Exception e) @@ -214,126 +339,95 @@ namespace VelNet } } } - // we just left a room else { - // delete all networkobjects that aren't sceneobjects or are null now - objects - .Where(kvp => kvp.Value == null || !kvp.Value.isSceneObject) - .Select(o => o.Key) - .ToList().ForEach(NetworkDestroy); - - // then remove references to the ones that are left - objects.Clear(); + VelNetPlayer me = players[userid]; - // empty all the groups - foreach (string group in instance.groups.Keys) + if (me.room != jm.room) { - SetupMessageGroup(group, new List()); - } - - instance.groups.Clear(); - - try - { - OnLeftRoom?.Invoke(oldRoom); - } - // prevent errors in subscribers from breaking our code - catch (Exception e) - { - Debug.LogError(e); - } - } - - break; - } - // not for me, a player is joining or leaving - case 2: - { - VelNetPlayer me = players[userid]; - - if (me.room != m.text) - { - // we got a left message, kill it - // change ownership of all objects to master - List deleteObjects = new List(); - foreach ((string key, NetworkObject value) in objects) - { - if (value.owner == players[m.sender]) // the owner is the player that left + // we got a left message, kill it + // change ownership of all objects to master + List deleteObjects = new List(); + foreach (KeyValuePair kvp in objects) { - // if this object has locked ownership, delete it - if (value.ownershipLocked) + if (kvp.Value.owner == players[jm.userId]) // the owner is the player that left { - deleteObjects.Add(value.networkId); - } - // I'm the local master player, so can take ownership immediately - else if (me.isLocal && me == masterPlayer) - { - TakeOwnership(key); - } - // the master player left, so everyone should set the owner null (we should get a new master shortly) - else if (players[m.sender] == masterPlayer) - { - value.owner = null; + // if this object has locked ownership, delete it + if (kvp.Value.ownershipLocked) + { + deleteObjects.Add(kvp.Value.networkId); + } + // I'm the local master player, so can take ownership immediately + else if (me.isLocal && me == masterPlayer) + { + TakeOwnership(kvp.Key); + } + // the master player left, so everyone should set the owner null (we should get a new master shortly) + else if (players[jm.userId] == masterPlayer) + { + kvp.Value.owner = null; + } } } - } - // TODO this may check for ownership in the future. We don't need ownership here - deleteObjects.ForEach(NetworkDestroy); + // TODO this may check for ownership in the future. We don't need ownership here + deleteObjects.ForEach(NetworkDestroy); - VelNetPlayer removedPlayer = players[m.sender]; - players.Remove(m.sender); - try - { - OnPlayerLeft?.Invoke(removedPlayer); + VelNetPlayer leftPlayer = players[jm.userId]; + players.Remove(jm.userId); + + try + { + OnPlayerLeft?.Invoke(leftPlayer); + } + // prevent errors in subscribers from breaking our code + catch (Exception e) + { + Debug.LogError(e); + } } - catch (Exception e) + else { - Debug.LogError(e); - } - } - else - { - // we got a join message, create it - VelNetPlayer player = new VelNetPlayer - { - isLocal = false, - room = m.text, - userid = m.sender - }; - players.Add(m.sender, player); - try - { - OnPlayerJoined?.Invoke(player); - } - // prevent errors in subscribers from breaking our code - catch (Exception e) - { - Debug.LogError(e); + // we got a join message, create it + VelNetPlayer player = new VelNetPlayer + { + isLocal = false, + room = jm.room, + userid = jm.userId + }; + players.Add(jm.userId, player); + try + { + OnPlayerJoined?.Invoke(player); + } + // prevent errors in subscribers from breaking our code + catch (Exception e) + { + Debug.LogError(e); + } } } break; } - // generic message - case 3: - if (players.ContainsKey(m.sender)) + case DataMessage dm: + { + if (players.ContainsKey(dm.senderId)) { - players[m.sender]?.HandleMessage(m); + players[dm.senderId]?.HandleMessage(dm); } else { - Debug.LogError("Received message from player that doesn't exist: " + m.text); + Debug.LogError("Received message from player that doesn't exist "); } break; - // change master player (this should only happen when the first player joins or if the master player leaves) - case 4: + } + case ChangeMasterMessage cm: { if (masterPlayer == null) { - masterPlayer = players[m.sender]; + masterPlayer = players[cm.masterId]; // no master player yet, add the scene objects @@ -346,12 +440,20 @@ namespace VelNet sceneObjects[i].networkId = -1 + "-" + sceneObjects[i].sceneNetworkId; sceneObjects[i].owner = masterPlayer; sceneObjects[i].isSceneObject = true; // needed for special handling when deleted - objects.Add(sceneObjects[i].networkId, sceneObjects[i]); + + if (objects.ContainsKey(sceneObjects[i].networkId)) + { + Debug.LogError($"Duplicate NetworkID: {sceneObjects[i].networkId} {sceneObjects[i].name} {objects[sceneObjects[i].networkId]}"); + } + else + { + objects.Add(sceneObjects[i].networkId, sceneObjects[i]); + } } } else { - masterPlayer = players[m.sender]; + masterPlayer = players[cm.masterId]; } masterPlayer.SetAsMasterPlayer(); @@ -366,7 +468,7 @@ namespace VelNet } } - MessageReceived?.Invoke(m); + //MessageReceived?.Invoke(m); } receivedMessages.Clear(); @@ -376,6 +478,8 @@ namespace VelNet private void OnApplicationQuit() { socketConnection?.Close(); + clientReceiveThreadUDP?.Abort(); + clientReceiveThread?.Abort(); } /// @@ -386,7 +490,6 @@ namespace VelNet try { clientReceiveThread = new Thread(ListenForData); - clientReceiveThread.IsBackground = true; clientReceiveThread.Start(); } catch (Exception e) @@ -395,77 +498,30 @@ namespace VelNet } } - private void HandleMessage(string s) // this parses messages from the server, and adds them to a queue to be processed on the main thread - { - // Debug.Log("Received: " + s); - Message m = new Message(); - string[] sections = s.Split(':'); - if (sections.Length <= 0) return; - - int type = int.Parse(sections[0]); - - switch (type) - { - case 0: // logged in message - { - if (sections.Length > 1) - { - m.type = type; - m.sender = int.Parse(sections[1]); - m.text = ""; - AddMessage(m); - } - - break; - } - case 1: // room info message - { - break; - } - case 2: // joined room message - { - if (sections.Length > 2) - { - m.type = 2; - int user_id = int.Parse(sections[1]); - m.sender = user_id; - string new_room = sections[2]; - m.text = new_room; - - AddMessage(m); - } - - break; - } - case 3: // text message - { - if (sections.Length > 2) - { - m.type = 3; - m.sender = int.Parse(sections[1]); - m.text = sections[2]; - AddMessage(m); - } - - break; - } - case 4: // change master client - { - if (sections.Length > 1) - { - m.type = 4; - m.sender = int.Parse(sections[1]); - AddMessage(m); - } - - break; - } - } - } /// - /// Runs in background clientReceiveThread; Listens for incomming data. - /// + /// Runs in background clientReceiveThread; Listens for incoming data. + /// + private static byte[] ReadExact(Stream stream, int N) + { + byte[] toReturn = new byte[N]; + + int numRead = 0; + int numLeft = N; + while (numLeft > 0) + { + numRead += stream.Read(toReturn, numRead, numLeft); + numLeft = N - numRead; + } + + return toReturn; + } + + private static int GetIntFromBytes(byte[] bytes) + { + return BitConverter.ToInt32(BitConverter.IsLittleEndian ? bytes.Reverse().ToArray() : bytes, 0); + } + private void ListenForData() { connected = true; @@ -473,41 +529,85 @@ namespace VelNet { socketConnection = new TcpClient(host, port); socketConnection.NoDelay = true; - byte[] bytes = new byte[1024]; - string partialMessage = ""; + // Get a stream object for reading + NetworkStream stream = socketConnection.GetStream(); + using BinaryReader reader = new BinaryReader(stream); + //now we are connected, so add a message to the queue + AddMessage(new ConnectedMessage()); while (true) { - // Get a stream object for reading - using NetworkStream stream = socketConnection.GetStream(); - int length; - // Read incomming stream into byte arrary. - while ((length = stream.Read(bytes, 0, bytes.Length)) != 0) + + //read a byte + MessageSendType type = (MessageSendType)stream.ReadByte(); + + switch (type) { - byte[] incommingData = new byte[length]; - Array.Copy(bytes, 0, incommingData, 0, length); - // Convert byte array to string message. - string serverMessage = Encoding.ASCII.GetString(incommingData); - string[] sections = serverMessage.Split('\n'); - if (sections.Length > 1) + //login + case MessageSendType.MESSAGE_LOGIN: { - lock (receivedMessages) + LoginMessage m = new LoginMessage(); + m.userId = GetIntFromBytes(ReadExact(stream, 4)); //not really the sender... + AddMessage(m); + break; + } + //rooms + case MessageSendType.MESSAGE_GETROOMS: + { + RoomsMessage m = new RoomsMessage(); + m.rooms = new List(); + int N = GetIntFromBytes(ReadExact(stream, 4)); //the size of the payload + byte[] utf8data = ReadExact(stream, N); + string roomMessage = Encoding.UTF8.GetString(utf8data); + + + string[] sections = roomMessage.Split(','); + foreach (string s in sections) { - for (int i = 0; i < sections.Length - 1; i++) + string[] pieces = s.Split(':'); + if (pieces.Length == 2) { - if (i == 0) - { - HandleMessage(partialMessage + sections[0]); - partialMessage = ""; - } - else - { - HandleMessage(sections[i]); - } + ListedRoom lr = new ListedRoom(); + lr.name = pieces[0]; + lr.numUsers = int.Parse(pieces[1]); + m.rooms.Add(lr); } } - } - partialMessage = partialMessage + sections[sections.Length - 1]; + AddMessage(m); + break; + } + //joined + case MessageSendType.MESSAGE_JOINROOM: + { + JoinMessage m = new JoinMessage(); + m.userId = GetIntFromBytes(ReadExact(stream, 4)); + int N = stream.ReadByte(); + byte[] utf8data = ReadExact(stream, N); //the room name, encoded as utf-8 + m.room = Encoding.UTF8.GetString(utf8data); + AddMessage(m); + break; + } + //data + case MessageSendType.MESSAGE_OTHERS: + // case MessageSendType.MESSAGE_OTHERS_ORDERED: + // case MessageSendType.MESSAGE_ALL: + // case MessageSendType.MESSAGE_ALL_ORDERED: + { + DataMessage m = new DataMessage(); + m.senderId = GetIntFromBytes(ReadExact(stream, 4)); + int N = GetIntFromBytes(ReadExact(stream, 4)); //the size of the payload + m.data = ReadExact(stream, N); //the message + AddMessage(m); + break; + } + //new master + case MessageSendType.MESSAGE_ALL: + { + ChangeMasterMessage m = new ChangeMasterMessage(); + m.masterId = GetIntFromBytes(ReadExact(stream, 4)); //sender is the new master + AddMessage(m); + break; + } } } } @@ -539,9 +639,9 @@ namespace VelNet byte[] buffer = new byte[1024]; while (true) { - string welcome = userid + ":0:Hello"; - byte[] data = Encoding.ASCII.GetBytes(welcome); - udpSocket.SendTo(data, data.Length, SocketFlags.None, RemoteEndPoint); + buffer[0] = 0; + Array.Copy(get_be_bytes(userid), 0, buffer, 1, 4); + udpSocket.SendTo(buffer, 5, SocketFlags.None, RemoteEndPoint); if (udpSocket.Available == 0) { @@ -558,18 +658,24 @@ namespace VelNet while (true) { int numReceived = udpSocket.Receive(buffer); - - string message = Encoding.UTF8.GetString(buffer, 0, numReceived); - - string[] sections = message.Split(':'); - if (sections[0] == "0") + switch (buffer[0]) { - Debug.Log("UDP connected"); - } - - if (sections[0] == "3") - { - HandleMessage(message); + case 0: + Debug.Log("UDP connected"); + break; + case 3: + { + DataMessage m = new DataMessage(); + //we should get the sender address + byte[] senderBytes = new byte[4]; + Array.Copy(buffer, 1, senderBytes, 0, 4); + m.senderId = GetIntFromBytes(senderBytes); + byte[] messageBytes = new byte[numReceived - 5]; + Array.Copy(buffer, 5, messageBytes, 0, messageBytes.Length); + m.data = messageBytes; + AddMessage(m); + break; + } } } } @@ -579,22 +685,20 @@ namespace VelNet } } - private static void SendUdpMessage(string message) + private static void SendUdpMessage(byte[] message, int N) { if (instance.udpSocket == null || !instance.udpConnected) { return; } - byte[] data = Encoding.UTF8.GetBytes(message); - //Debug.Log("Attempting to send: " + message); - instance.udpSocket.SendTo(data, data.Length, SocketFlags.None, instance.RemoteEndPoint); + instance.udpSocket.SendTo(message, N, SocketFlags.None, instance.RemoteEndPoint); } /// - /// Send message to server using socket connection. + /// Send message to server using socket connection. /// - private static void SendNetworkMessage(string clientMessage) + private static void SendTcpMessage(byte[] message) //we can assume that this message is already formatted, so we just send it { // Debug.Log("Sent: " + clientMessage); if (instance.socketConnection == null) @@ -608,11 +712,7 @@ namespace VelNet NetworkStream stream = instance.socketConnection.GetStream(); if (stream.CanWrite) { - // Convert string message to byte array. - clientMessage += "\n"; // append a new line to delineate the message - byte[] clientMessageAsByteArray = Encoding.ASCII.GetBytes(clientMessage); - // Write byte array to socketConnection stream. - stream.Write(clientMessageAsByteArray, 0, clientMessageAsByteArray.Length); + stream.Write(message, 0, message.Length); } } catch (SocketException socketException) @@ -621,12 +721,33 @@ namespace VelNet } } + public static byte[] get_be_bytes(int n) + { + return BitConverter.GetBytes(n).Reverse().ToArray(); + } + /// /// Connects to the server with a username /// public static void Login(string username, string password) { - SendNetworkMessage("0:" + username + ":" + password); + MemoryStream stream = new MemoryStream(); + BinaryWriter writer = new BinaryWriter(stream); + + byte[] uB = Encoding.UTF8.GetBytes(username); + byte[] pB = Encoding.UTF8.GetBytes(password); + writer.Write((byte)MessageSendType.MESSAGE_LOGIN); + writer.Write((byte)uB.Length); + writer.Write(uB); + writer.Write((byte)pB.Length); + writer.Write(pB); + + SendTcpMessage(stream.ToArray()); + } + + public static void GetRooms() + { + SendTcpMessage(new byte[] { 1 }); //very simple message } /// @@ -635,52 +756,125 @@ namespace VelNet /// The name of the room to join public static void Join(string roomname) { - SendNetworkMessage("2:" + roomname); + MemoryStream stream = new MemoryStream(); + BinaryWriter writer = new BinaryWriter(stream); + + byte[] R = Encoding.UTF8.GetBytes(roomname); + writer.Write((byte)MessageSendType.MESSAGE_JOINROOM); + writer.Write((byte)R.Length); + writer.Write(R); + SendTcpMessage(stream.ToArray()); } + /// /// Leaves a room if we're in one /// public static void Leave() { - if (InRoom) SendNetworkMessage("2:-1"); - } - - public static void SendTo(MessageType type, string message, bool reliable = true) - { - if (reliable) + if (InRoom) { - SendNetworkMessage("3:" + (int)type + ":" + message); - } - else - { - SendUdpMessage(instance.userid + ":3:" + (int)type + ":" + message); + Join(""); //super secret way to leave } } - public static void SendToGroup(string group, string message, bool reliable = true) + public static void SendCustomMessage(byte[] message, bool include_self = false, bool reliable = true, bool ordered = false) { + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)MessageType.Custom); + writer.Write(message.Length); + writer.Write(message); + SendToRoom(mem.ToArray(), include_self, reliable, ordered); + } + + public static void SendCustomMessageToGroup(string group, byte[] message,bool reliable = true) + { + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)MessageType.Custom); + writer.Write(message.Length); + writer.Write(message); + SendToGroup(group, mem.ToArray(), reliable); + } + + internal static void SendToRoom(byte[] message, bool include_self = false, bool reliable = true, bool ordered = false) + { + byte sendType = (byte)MessageSendType.MESSAGE_OTHERS; + if (include_self && ordered) sendType = (byte)MessageSendType.MESSAGE_ALL_ORDERED; + if (include_self && !ordered) sendType = (byte)MessageSendType.MESSAGE_ALL; + if (!include_self && ordered) sendType = (byte)MessageSendType.MESSAGE_OTHERS_ORDERED; + + if (reliable) { - SendNetworkMessage("4:" + group + ":" + message); + MemoryStream mem = new MemoryStream(); + BinaryWriter writer = new BinaryWriter(mem); + writer.Write(sendType); + writer.Write(get_be_bytes(message.Length)); + writer.Write(message); + SendTcpMessage(mem.ToArray()); } else { - SendUdpMessage(instance.userid + ":4:" + group + ":" + message); + //udp message needs the type + toSend[0] = sendType; //we don't + Array.Copy(get_be_bytes(instance.userid), 0, toSend, 1, 4); + Array.Copy(message, 0, toSend, 5, message.Length); + SendUdpMessage(toSend, message.Length + 5); //shouldn't be over 1024... + } + } + + + internal static void SendToGroup(string group, byte[] message, bool reliable = true) + { + byte[] utf8bytes = Encoding.UTF8.GetBytes(group); + if (reliable) + { + MemoryStream stream = new MemoryStream(); + BinaryWriter writer = new BinaryWriter(stream); + writer.Write((byte)MessageSendType.MESSAGE_GROUP); + writer.Write(get_be_bytes(message.Length)); + writer.Write(message); + writer.Write((byte)utf8bytes.Length); + writer.Write(utf8bytes); + SendTcpMessage(stream.ToArray()); + } + else + { + toSend[0] = (byte)MessageSendType.MESSAGE_GROUP; + Array.Copy(get_be_bytes(instance.userid), 0, toSend, 1, 4); + //also need to send the group + toSend[5] = (byte)utf8bytes.Length; + Array.Copy(utf8bytes, 0, toSend, 6, utf8bytes.Length); + Array.Copy(message, 0, toSend, 6 + utf8bytes.Length, message.Length); + SendUdpMessage(toSend, 6 + utf8bytes.Length + message.Length); } } /// /// changes the designated group that sendto(4) will go to /// - public static void SetupMessageGroup(string groupName, List userIds) + public static void SetupMessageGroup(string groupname, List client_ids) { - if (userIds.Count > 0) + if (client_ids.Count > 0) { - instance.groups[groupName] = userIds.ToList(); + instance.groups[groupname] = client_ids.ToList(); } - SendNetworkMessage($"5:{groupName}:{string.Join(":", userIds)}"); + MemoryStream stream = new MemoryStream(); + BinaryWriter writer = new BinaryWriter(stream); + byte[] R = Encoding.UTF8.GetBytes(groupname); + writer.Write((byte)6); + writer.Write((byte)R.Length); + writer.Write(R); + writer.Write(get_be_bytes(client_ids.Count * 4)); + for (int i = 0; i < client_ids.Count; i++) + { + writer.Write(get_be_bytes(client_ids[i])); + } + + SendTcpMessage(stream.ToArray()); } @@ -700,14 +894,21 @@ namespace VelNet Debug.LogError("Can't instantiate object. Obj with that network ID was already instantiated.", instance.objects[networkId]); return null; } + NetworkObject newObject = Instantiate(prefab); newObject.networkId = networkId; newObject.prefabName = prefabName; newObject.owner = localPlayer; instance.objects.Add(newObject.networkId, newObject); + // only sent to others, as I already instantiated this. Nice that it happens immediately. - SendTo(MessageType.OTHERS, "7," + newObject.networkId + "," + prefabName); + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)MessageType.Instantiate); + writer.Write(newObject.networkId); + writer.Write(prefabName); + SendToRoom(mem.ToArray(), include_self:false, reliable:true); return newObject; } @@ -737,6 +938,7 @@ namespace VelNet instance.objects.Remove(networkId); return; } + if (obj.isSceneObject) { instance.deletedSceneObjects.Add(networkId); @@ -745,7 +947,7 @@ namespace VelNet Destroy(obj.gameObject); instance.objects.Remove(networkId); } - + /// /// Takes local ownership of an object by id. /// @@ -759,7 +961,7 @@ namespace VelNet Debug.LogError("Can't take ownership. No local player."); return false; } - + // obj must exist if (!instance.objects.ContainsKey(networkId)) { @@ -773,13 +975,18 @@ namespace VelNet Debug.LogError("Can't take ownership. Ownership for this object is locked."); return false; } - + // immediately successful instance.objects[networkId].owner = LocalPlayer; - // must be ordered, so that ownership transfers are not confused. Also sent to all players, so that multiple simultaneous requests will result in the same outcome. - SendTo(MessageType.ALL_ORDERED, "6," + networkId); - + // must be ordered, so that ownership transfers are not confused. + // Also sent to all players, so that multiple simultaneous requests will result in the same outcome. + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)MessageType.TakeOwnership); + writer.Write(networkId); + SendToRoom(mem.ToArray(), false, true); + return true; } } diff --git a/Runtime/VelNetManager.cs.meta b/Runtime/VelNetManager.cs.meta index 56f31aa..014ff23 100644 --- a/Runtime/VelNetManager.cs.meta +++ b/Runtime/VelNetManager.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 03a4d4e1a7fd74c7ab2eccca4ce168db +guid: 233344de094f11341bdb834d564708dc MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/VelNetPlayer.cs b/Runtime/VelNetPlayer.cs index 1152432..1b8a3c0 100644 --- a/Runtime/VelNetPlayer.cs +++ b/Runtime/VelNetPlayer.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; using System; +using System.IO; +using System.Text; +using UnityEngine; namespace VelNet { @@ -39,7 +42,12 @@ namespace VelNet { if (kvp.Value.owner == this && kvp.Value.prefabName != "") { - VelNetManager.SendTo(VelNetManager.MessageType.OTHERS, "7," + kvp.Value.networkId + "," + kvp.Value.prefabName); + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)VelNetManager.MessageType.Instantiate); + writer.Write(kvp.Value.networkId); + writer.Write(kvp.Value.prefabName); + VelNetManager.SendToRoom(mem.ToArray(), false, true); } } @@ -53,77 +61,87 @@ namespace VelNet /// /// These are generally things that come from the "owner" and should be enacted locally, where appropriate + /// + /// Message encoding: + /// byte: message type + /// byte[]: message + /// + /// The length of the byte[] for message is fixed according to the message type /// - public void HandleMessage(VelNetManager.Message m) + public void HandleMessage(VelNetManager.DataMessage m) { - //we need to parse the message + using MemoryStream mem = new MemoryStream(m.data); + using BinaryReader reader = new BinaryReader(mem); + + //individual message parameters separated by comma + VelNetManager.MessageType messageType = (VelNetManager.MessageType)reader.ReadByte(); - //types of messages - string[] messages = m.text.Split(';'); //messages are split by ; - foreach (string s in messages) + switch (messageType) { - //individual message parameters separated by comma - string[] sections = s.Split(','); - - switch (sections[0]) + case VelNetManager.MessageType.ObjectSync: // sync update for an object I may own { - case "5": // sync update for an object I may own + string objectKey = reader.ReadString(); + byte componentIdx = reader.ReadByte(); + int messageLength = reader.ReadInt32(); + byte[] syncMessage = reader.ReadBytes(messageLength); + if (manager.objects.ContainsKey(objectKey)) { - string objectKey = sections[1]; - string identifier = sections[2]; - string syncMessage = sections[3]; - byte[] messageBytes = Convert.FromBase64String(syncMessage); - if (manager.objects.ContainsKey(objectKey)) + if (manager.objects[objectKey].owner == this) { - if (manager.objects[objectKey].owner == this) - { - manager.objects[objectKey].ReceiveBytes(identifier, messageBytes); - } + manager.objects[objectKey].ReceiveBytes(componentIdx, syncMessage); } - - break; } - case "6": // I'm trying to take ownership of an object - { - string networkId = sections[1]; - if (manager.objects.ContainsKey(networkId)) - { - manager.objects[networkId].owner = this; - } - - break; - } - case "7": // I'm trying to instantiate an object - { - string networkId = sections[1]; - string prefabName = sections[2]; - if (manager.objects.ContainsKey(networkId)) - { - break; //we already have this one, ignore - } - - VelNetManager.SomebodyInstantiatedNetworkObject(networkId, prefabName, this); - - break; - } - case "8": // I'm trying to destroy a gameobject I own - { - string networkId = sections[1]; - - VelNetManager.NetworkDestroy(networkId); - break; - } - case "9": //deleted scene objects - { - for (int k = 1; k < sections.Length; k++) - { - VelNetManager.NetworkDestroy(sections[k]); - } - - break; - } + break; } + case VelNetManager.MessageType.TakeOwnership: // I'm trying to take ownership of an object + { + string networkId = reader.ReadString(); + + if (manager.objects.ContainsKey(networkId)) + { + manager.objects[networkId].owner = this; + } + + break; + } + case VelNetManager.MessageType.Instantiate: // I'm trying to instantiate an object + { + string networkId = reader.ReadString(); + string prefabName = reader.ReadString(); + if (manager.objects.ContainsKey(networkId)) + { + break; //we already have this one, ignore + } + + VelNetManager.SomebodyInstantiatedNetworkObject(networkId, prefabName, this); + + break; + } + case VelNetManager.MessageType.Destroy: // I'm trying to destroy a gameobject I own + { + string networkId = reader.ReadString(); + + VelNetManager.NetworkDestroy(networkId); + break; + } + case VelNetManager.MessageType.DeleteSceneObjects: //deleted scene objects + { + int len = reader.ReadInt32(); + for (int k = 1; k < len; k++) + { + VelNetManager.NetworkDestroy(reader.ReadString()); + } + break; + } + case VelNetManager.MessageType.Custom: // custom packets + { + int len = reader.ReadInt32(); + VelNetManager.CustomMessageReceived?.Invoke(reader.ReadBytes(len)); + break; + } + default: + throw new ArgumentOutOfRangeException(); } } @@ -134,19 +152,42 @@ namespace VelNet //FindObjectsOfType(); } - public void SendGroupMessage(NetworkObject obj, string group, string identifier, byte[] data, bool reliable = true) + public static void SendGroupMessage(NetworkObject obj, string group, byte componentIdx, byte[] data, bool reliable = true) { - VelNetManager.SendToGroup(group, "5," + obj.networkId + "," + identifier + "," + Convert.ToBase64String(data), reliable); + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)VelNetManager.MessageType.ObjectSync); + writer.Write(obj.networkId); + writer.Write(componentIdx); + writer.Write(data.Length); + writer.Write(data); + VelNetManager.SendToGroup(group, mem.ToArray(), reliable); } - public void SendMessage(NetworkObject obj, string identifier, byte[] data, bool reliable = true) + public static void SendMessage(NetworkObject obj, byte componentIdx, byte[] data, bool reliable = true) { - VelNetManager.SendTo(VelNetManager.MessageType.OTHERS, "5," + obj.networkId + "," + identifier + "," + Convert.ToBase64String(data), reliable); + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)VelNetManager.MessageType.ObjectSync); + writer.Write(obj.networkId); + writer.Write(componentIdx); + writer.Write(data.Length); + writer.Write(data); + VelNetManager.SendToRoom(mem.ToArray(), false, reliable); } public void SendSceneUpdate() { - VelNetManager.SendTo(VelNetManager.MessageType.OTHERS, "9," + string.Join(",", manager.deletedSceneObjects)); + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)VelNetManager.MessageType.DeleteSceneObjects); + writer.Write(manager.deletedSceneObjects.Count); + foreach (string o in manager.deletedSceneObjects) + { + writer.Write(o); + } + + VelNetManager.SendToRoom(mem.ToArray()); } [Obsolete("Use VelNetManager.NetworkDestroy() instead.")] @@ -156,7 +197,12 @@ namespace VelNet if (!manager.objects.ContainsKey(networkId) || manager.objects[networkId].owner != this || !isLocal) return; // send to all, which will make me delete as well - VelNetManager.SendTo(VelNetManager.MessageType.ALL_ORDERED, "8," + networkId); + + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)VelNetManager.MessageType.Destroy); + writer.Write(networkId); + VelNetManager.SendToRoom(mem.ToArray(), true, true); } /// True if successful, False if failed to transfer ownership @@ -172,8 +218,13 @@ namespace VelNet // immediately successful manager.objects[networkId].owner = this; - // must be ordered, so that ownership transfers are not confused. Also sent to all players, so that multiple simultaneous requests will result in the same outcome. - VelNetManager.SendTo(VelNetManager.MessageType.ALL_ORDERED, "6," + networkId); + // must be ordered, so that ownership transfers are not confused. + // Also sent to all players, so that multiple simultaneous requests will result in the same outcome. + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + writer.Write((byte)VelNetManager.MessageType.TakeOwnership); + writer.Write(networkId); + VelNetManager.SendToRoom(mem.ToArray(), true, true, ordered: true); return true; } diff --git a/package.json b/package.json index 956001f..c278bc1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "edu.uga.engr.vel.velnet", "displayName": "VelNet", - "version": "1.0.7", + "version": "1.0.8", "unity": "2019.1", "description": "A custom networking library for Unity.", "keywords": [