diff --git a/Runtime/NetworkObject.cs b/Runtime/NetworkObject.cs index 15c4de1..aa39b78 100644 --- a/Runtime/NetworkObject.cs +++ b/Runtime/NetworkObject.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; using System.Linq; +#if UNITY_EDITOR +using UnityEditor; +#endif using UnityEngine; namespace VelNet @@ -9,10 +12,11 @@ namespace VelNet /// public class NetworkObject : MonoBehaviour { - [Header("NetworkObject properties")] - public VelNetPlayer owner; + [Header("NetworkObject properties")] public VelNetPlayer owner; + [Tooltip("Whether this object's ownership is transferrable. Should be true for player objects.")] public bool ownershipLocked; + public bool IsMine => owner != null && owner.isLocal; /// @@ -61,4 +65,44 @@ namespace VelNet syncedComponents[int.Parse(identifier)].ReceiveBytes(message); } } + +#if UNITY_EDITOR + /// + /// Sets up the interface for the CopyTransform script. + /// + [CustomEditor(typeof(NetworkObject))] + public class NetworkObjectEditor : Editor + { + public override void OnInspectorGUI() + { + NetworkObject t = target as NetworkObject; + + EditorGUILayout.Space(); + + if (t == null) return; + + EditorGUILayout.HelpBox("Network Object. One per prefab pls.\nAssign components to the list to be synced.", MessageType.Info); + + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.Toggle("IsMine", t.IsMine); + EditorGUILayout.TextField("Owner ID", t.owner?.userid.ToString() ?? "No owner"); + EditorGUI.EndDisabledGroup(); + EditorGUILayout.Space(); + + if (GUILayout.Button("Find Network Components and add backreferences.")) + { + NetworkComponent[] comps = t.GetComponents(); + t.syncedComponents = comps.ToList(); + foreach (NetworkComponent c in comps) + { + c.networkObject = t; + } + } + + EditorGUILayout.Space(); + + DrawDefaultInspector(); + } + } +#endif } \ No newline at end of file diff --git a/Runtime/Util/NetworkSerializedObject.cs b/Runtime/Util/NetworkSerializedObject.cs index ef1aa14..13037d1 100644 --- a/Runtime/Util/NetworkSerializedObject.cs +++ b/Runtime/Util/NetworkSerializedObject.cs @@ -6,10 +6,10 @@ namespace VelNet { public abstract class NetworkSerializedObject : NetworkComponent { - [FormerlySerializedAs("updateRateHz")] [Tooltip("Send rate of this object")] + [Tooltip("Send rate of this object. This caps out at the framerate of the game.")] public float serializationRateHz = 30; - private void Start() + private void Awake() { StartCoroutine(SendMessageUpdate()); } @@ -23,11 +23,11 @@ namespace VelNet SendBytes(SendState()); } - yield return new WaitForSeconds(1 / serializationRateHz); + yield return new WaitForSeconds(1f / serializationRateHz); } // ReSharper disable once IteratorNeverReturns } - + public override void ReceiveBytes(byte[] message) { ReceiveState(message); diff --git a/Runtime/Util/SyncTransform.cs b/Runtime/Util/SyncTransform.cs index 231a0eb..162a9c9 100644 --- a/Runtime/Util/SyncTransform.cs +++ b/Runtime/Util/SyncTransform.cs @@ -10,21 +10,47 @@ namespace VelNet [AddComponentMenu("VelNet/VelNet Sync Transform")] public class SyncTransform : NetworkSerializedObject { - public Vector3 targetPosition; - public Quaternion targetRotation; - public float smoothness = .1f; + public bool useLocalTransform; + private Vector3 targetPosition; + private Quaternion targetRotation; + private float distanceAtReceiveTime; + private float angleAtReceiveTime; + + private void Start() + { + if (useLocalTransform) + { + targetPosition = transform.localPosition; + targetRotation = transform.localRotation; + } + else + { + targetPosition = transform.position; + targetRotation = transform.rotation; + } + } + + /// + /// This gets called at serializationRateHz when the object is locally owned + /// + /// The state of this object to send across the network protected override byte[] SendState() { using MemoryStream mem = new MemoryStream(); using BinaryWriter writer = new BinaryWriter(mem); - writer.Write(transform.position); - writer.Write(transform.rotation); + writer.Write(transform.localPosition); + writer.Write(transform.localRotation); return mem.ToArray(); } + /// + /// This gets called whenever a message about the state of this object is received. + /// Usually at serializationRateHz. + /// + /// The network state of this object protected override void ReceiveState(byte[] message) { using MemoryStream mem = new MemoryStream(message); @@ -32,13 +58,50 @@ namespace VelNet targetPosition = reader.ReadVector3(); targetRotation = reader.ReadQuaternion(); + + // record the distance from the target for interpolation + if (useLocalTransform) + { + distanceAtReceiveTime = Vector3.Distance(targetPosition, transform.localPosition); + angleAtReceiveTime = Quaternion.Angle(targetRotation, transform.localRotation); + } + else + { + distanceAtReceiveTime = Vector3.Distance(targetPosition, transform.position); + angleAtReceiveTime = Quaternion.Angle(targetRotation, transform.rotation); + } } private void Update() { if (IsMine) return; - transform.position = Vector3.Lerp(transform.position, targetPosition, 1 / smoothness / serializationRateHz); - transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, 1 / smoothness / serializationRateHz); + + if (useLocalTransform) + { + transform.localPosition = Vector3.MoveTowards( + transform.localPosition, + targetPosition, + Time.deltaTime * distanceAtReceiveTime * serializationRateHz + ); + transform.localRotation = Quaternion.RotateTowards( + transform.localRotation, + targetRotation, + Time.deltaTime * angleAtReceiveTime * serializationRateHz + ); + } + else + { + transform.position = Vector3.MoveTowards( + transform.position, + targetPosition, + Time.deltaTime * distanceAtReceiveTime * serializationRateHz + ); + transform.rotation = Quaternion.RotateTowards( + transform.rotation, + targetRotation, + Time.deltaTime * angleAtReceiveTime * serializationRateHz + ); + } } } } \ No newline at end of file diff --git a/Runtime/VelNetManager.cs b/Runtime/VelNetManager.cs index c8b2a41..7fe36d9 100644 --- a/Runtime/VelNetManager.cs +++ b/Runtime/VelNetManager.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using UnityEngine; using System.Net; +using UnityEngine.SceneManagement; namespace VelNet { @@ -104,12 +105,17 @@ namespace VelNet } instance = this; + + SceneManager.sceneLoaded += (scene, mode) => + { + // add all local network objects + sceneObjects = FindObjectsOfType().Where(o=>o.isSceneObject).ToArray(); + }; } private IEnumerator Start() { ConnectToTcpServer(); - sceneObjects = FindObjectsOfType(); //add all local network objects yield return null; try @@ -169,6 +175,7 @@ namespace VelNet // we clear the list, but will recreate as we get messages from people in our room players.Clear(); + masterPlayer = null; if (m.text != "") { @@ -200,7 +207,7 @@ namespace VelNet objects .Where(kvp => kvp.Value == null || !kvp.Value.isSceneObject) .Select(o => o.Key) - .ToList().ForEach(DeleteNetworkObject); + .ToList().ForEach(NetworkDestroy); // empty all the groups foreach (string group in instance.groups.Keys) @@ -246,7 +253,7 @@ namespace VelNet // I'm the local master player, so can take ownership immediately else if (me.isLocal && me == masterPlayer) { - me.TakeOwnership(kvp.Key); + TakeOwnership(kvp.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) @@ -257,7 +264,7 @@ namespace VelNet } // TODO this may check for ownership in the future. We don't need ownership here - deleteObjects.ForEach(DeleteNetworkObject); + deleteObjects.ForEach(NetworkDestroy); players.Remove(m.sender); } @@ -683,18 +690,43 @@ namespace VelNet instance.objects.Add(newObject.networkId, newObject); } - public void DeleteNetworkObject(string networkId) + public static void NetworkDestroy(string networkId) { - if (!objects.ContainsKey(networkId)) return; - NetworkObject obj = objects[networkId]; + if (!instance.objects.ContainsKey(networkId)) return; + NetworkObject obj = instance.objects[networkId]; if (obj == null) return; if (obj.isSceneObject) { - deletedSceneObjects.Add(networkId); + instance.deletedSceneObjects.Add(networkId); } Destroy(obj.gameObject); - objects.Remove(networkId); + instance.objects.Remove(networkId); + } + + /// + /// Takes local ownership of an object by id. + /// + /// Network ID of the object to transfer + /// True if successfully transferred, False if transfer message not sent + public static bool TakeOwnership(string networkId) + { + // local player must exist + if (LocalPlayer == null) return false; + + // obj must exist + if (!instance.objects.ContainsKey(networkId)) return false; + + // if the ownership is locked, fail + if (instance.objects[networkId].ownershipLocked) 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); + + return true; } } } \ No newline at end of file diff --git a/Runtime/VelNetPlayer.cs b/Runtime/VelNetPlayer.cs index 0fb7216..fae5623 100644 --- a/Runtime/VelNetPlayer.cs +++ b/Runtime/VelNetPlayer.cs @@ -113,14 +113,14 @@ namespace VelNet { string networkId = sections[1]; - manager.DeleteNetworkObject(networkId); + VelNetManager.NetworkDestroy(networkId); break; } case "9": //deleted scene objects { for (int k = 1; k < sections.Length; k++) { - manager.DeleteNetworkObject(sections[k]); + VelNetManager.NetworkDestroy(sections[k]); } break; @@ -146,9 +146,12 @@ namespace VelNet VelNetManager.SendTo(VelNetManager.MessageType.OTHERS, "5," + obj.networkId + "," + identifier + "," + Convert.ToBase64String(data), reliable); } - /// - /// TODO could move this to a static method in VelNetManager - /// + public void SendSceneUpdate() + { + VelNetManager.SendTo(VelNetManager.MessageType.OTHERS, "9," + string.Join(",", manager.deletedSceneObjects)); + } + + [Obsolete("Use VelNetManager.NetworkDestroy() instead.")] public void NetworkDestroy(string networkId) { // must be the local owner of the object to destroy it @@ -158,10 +161,8 @@ namespace VelNet VelNetManager.SendTo(VelNetManager.MessageType.ALL_ORDERED, "8," + networkId); } - /// - /// TODO could move this to a static method in VelNetManager - /// /// True if successful, False if failed to transfer ownership + [Obsolete("Use VelNetManager.TakeOwnership() instead.")] public bool TakeOwnership(string networkId) { // must exist and be the the local player @@ -169,19 +170,14 @@ namespace VelNet // if the ownership is locked, fail if (manager.objects[networkId].ownershipLocked) return false; - + // 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); - - return true; - } - public void SendSceneUpdate() - { - VelNetManager.SendTo(VelNetManager.MessageType.OTHERS, "9," + string.Join(",", manager.deletedSceneObjects)); + return true; } } } \ No newline at end of file