added RPC support 😲

upm
Anton Franzluebbers 2022-06-17 00:13:33 -04:00
parent eeb539ba49
commit bcd39cf601
7 changed files with 294 additions and 52 deletions

View File

@ -1,4 +1,8 @@
using UnityEngine; using System;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
namespace VelNet namespace VelNet
{ {
@ -13,20 +17,95 @@ namespace VelNet
/// </summary> /// </summary>
protected void SendBytes(byte[] message, bool reliable = true) protected void SendBytes(byte[] message, bool reliable = true)
{ {
networkObject.SendBytes(this, message, reliable); networkObject.SendBytes(this, false, message, reliable);
} }
/// <summary> /// <summary>
/// call this in child classes to send a message to other people /// call this in child classes to send a message to other people
/// </summary> /// </summary>
protected void SendBytesToGroup(string group, byte[] message, bool reliable = true) protected void SendBytesToGroup(string group, byte[] message, bool reliable = true)
{ {
networkObject.SendBytesToGroup(this, group, message, reliable); networkObject.SendBytesToGroup(this, false, group, message, reliable);
} }
/// <summary> /// <summary>
/// This is called by <see cref="NetworkObject"/> when messages are received for this component /// This is called by <see cref="NetworkObject"/> when messages are received for this component
/// </summary> /// </summary>
public abstract void ReceiveBytes(byte[] message); public abstract void ReceiveBytes(byte[] message);
public void ReceiveRPC(byte[] message)
{
using MemoryStream mem = new MemoryStream(message);
using BinaryReader reader = new BinaryReader(mem);
byte methodIndex = reader.ReadByte();
int length = reader.ReadInt32();
byte[] parameterData = reader.ReadBytes(length);
MethodInfo[] mInfos = GetType().GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
Array.Sort(mInfos, (m1, m2) => string.Compare(m1.Name, m2.Name, StringComparison.Ordinal));
try
{
mInfos[methodIndex].Invoke(this, length > 0 ? new object[] { parameterData } : Array.Empty<object>());
}
catch (Exception e)
{
Debug.LogError($"Error processing received RPC {e}");
}
}
protected void SendRPCToGroup(string group, bool runLocally, string methodName, byte[] parameterData = null)
{
if (GenerateRPC(methodName, parameterData, out byte[] bytes)) return;
if (runLocally) ReceiveRPC(bytes);
networkObject.SendBytesToGroup(this, true, group, bytes, true);
}
protected void SendRPC(string methodName, bool runLocally, byte[] parameterData = null)
{
if (GenerateRPC(methodName, parameterData, out byte[] bytes)) return;
if (networkObject.SendBytes(this, true, bytes, true))
{
// only run locally if we can successfully send
if (runLocally) ReceiveRPC(bytes);
}
}
private bool GenerateRPC(string methodName, byte[] parameterData, out byte[] bytes)
{
bytes = null;
using MemoryStream mem = new MemoryStream();
using BinaryWriter writer = new BinaryWriter(mem);
MethodInfo[] mInfos = GetType().GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
Array.Sort(mInfos, (m1, m2) => string.Compare(m1.Name, m2.Name, StringComparison.Ordinal));
int methodIndex = mInfos.ToList().FindIndex(m => m.Name == methodName);
switch (methodIndex)
{
case > 255:
Debug.LogError("Too many methods in this class.");
return true;
case < 0:
Debug.LogError("Can't find a method with that name.");
return true;
}
writer.Write((byte)methodIndex);
if (parameterData != null)
{
writer.Write(parameterData.Length);
writer.Write(parameterData);
}
else
{
writer.Write(0);
}
bytes = mem.ToArray();
return false;
}
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.NetworkInformation;
#if UNITY_EDITOR #if UNITY_EDITOR
using UnityEditor; using UnityEditor;
#endif #endif
@ -19,7 +20,7 @@ namespace VelNet
public bool ownershipLocked; public bool ownershipLocked;
public bool IsMine => owner?.isLocal ?? false; public bool IsMine => owner?.isLocal ?? false;
/// <summary> /// <summary>
/// This is forged from the combination of the creator's id (-1 in the case of a scene object) and an object id, so it's always unique for a room /// This is forged from the combination of the creator's id (-1 in the case of a scene object) and an object id, so it's always unique for a room
/// </summary> /// </summary>
@ -40,51 +41,95 @@ namespace VelNet
public List<NetworkComponent> syncedComponents; public List<NetworkComponent> syncedComponents;
public void SendBytes(NetworkComponent component, byte[] message, bool reliable = true) /// <summary>
/// Player is the new owner
/// </summary>
public Action<VelNetPlayer> OwnershipChanged;
public bool SendBytes(NetworkComponent component, bool isRpc, byte[] message, bool reliable = true)
{ {
if (!IsMine) // only needs to be owner if this isn't an RPC
// RPC calls can be called by non-owner
if (!IsMine && !isRpc)
{ {
Debug.LogError("Can't send message if owner is null or not local", this); Debug.LogError("Can't send message if owner is null or not local", this);
return; return false;
}
if (!VelNetManager.InRoom)
{
Debug.LogError("Can't send message if not in a room", this);
return false;
} }
// send the message and an identifier for which component it belongs to // send the message and an identifier for which component it belongs to
if (!syncedComponents.Contains(component)) if (!syncedComponents.Contains(component))
{ {
Debug.LogError("Can't send message if this component is not registered with the NetworkObject.", this); Debug.LogError("Can't send message if this component is not registered with the NetworkObject.", this);
return; return false;
} }
int index = syncedComponents.IndexOf(component); int componentIndex = syncedComponents.IndexOf(component);
if (index < 0) switch (componentIndex)
{ {
Debug.LogError("WAAAAAAAH. NetworkObject doesn't have a reference to this component.", component); case > 127:
} Debug.LogError("Too many components.", component);
else return false;
{ case < 0:
VelNetPlayer.SendMessage(this, (byte)index, message, reliable); Debug.LogError("WAAAAAAAH. NetworkObject doesn't have a reference to this component.", component);
return false;
} }
byte componentByte = (byte)(componentIndex << 1);
// the leftmost bit determines if this is an rpc or not
// this leaves only 128 possible NetworkComponents per NetworkObject
componentByte |= (byte)(isRpc ? 1 : 0);
return VelNetPlayer.SendMessage(this, componentByte, message, reliable);
} }
public void SendBytesToGroup(NetworkComponent component, string group, byte[] message, bool reliable = true)
public bool SendBytesToGroup(NetworkComponent component, bool isRpc, string group, byte[] message, bool reliable = true)
{ {
if (!IsMine) // only needs to be owner if this isn't an RPC
// RPC calls can be called by non-owner
if (!IsMine && !isRpc)
{ {
Debug.LogError("Can't send message if owner is null or not local", this); Debug.LogError("Can't send message if owner is null or not local", this);
return; return false;
} }
// send the message and an identifier for which component it belongs to // send the message and an identifier for which component it belongs to
int index = syncedComponents.IndexOf(component); int componentIndex = syncedComponents.IndexOf(component);
VelNetPlayer.SendGroupMessage(this, group, (byte)index, message, reliable); switch (componentIndex)
{
case > 127:
Debug.LogError("Too many components.", component);
return false;
case < 0:
Debug.LogError("WAAAAAAAH. NetworkObject doesn't have a reference to this component.", component);
return false;
}
byte componentByte = (byte)(componentIndex << 1);
componentByte |= (byte)(isRpc ? 1 : 0);
return VelNetPlayer.SendGroupMessage(this, group, componentByte, message, reliable);
} }
public void ReceiveBytes(byte componentIdx, byte[] message) public void ReceiveBytes(byte componentIdx, bool isRpc, byte[] message)
{ {
// send the message to the right component // send the message to the right component
try try
{ {
syncedComponents[componentIdx].ReceiveBytes(message); if (isRpc)
{
syncedComponents[componentIdx].ReceiveRPC(message);
}
else
{
syncedComponents[componentIdx].ReceiveBytes(message);
}
} }
catch (Exception e) catch (Exception e)
{ {
@ -123,13 +168,14 @@ namespace VelNet
if (GUILayout.Button("Find Network Components and add backreferences.")) if (GUILayout.Button("Find Network Components and add backreferences."))
{ {
NetworkComponent[] comps = t.GetComponents<NetworkComponent>(); NetworkComponent[] comps = t.GetComponentsInChildren<NetworkComponent>();
t.syncedComponents = comps.ToList(); t.syncedComponents = comps.ToList();
foreach (NetworkComponent c in comps) foreach (NetworkComponent c in comps)
{ {
c.networkObject = t; c.networkObject = t;
PrefabUtility.RecordPrefabInstancePropertyModifications(c); PrefabUtility.RecordPrefabInstancePropertyModifications(c);
} }
PrefabUtility.RecordPrefabInstancePropertyModifications(t); PrefabUtility.RecordPrefabInstancePropertyModifications(t);
} }
@ -139,7 +185,7 @@ namespace VelNet
// find the first unused value // find the first unused value
int[] used = FindObjectsOfType<NetworkObject>().Select(o => o.sceneNetworkId).ToArray(); int[] used = FindObjectsOfType<NetworkObject>().Select(o => o.sceneNetworkId).ToArray();
int available = -1; int available = -1;
for (int i = 1; i <= used.Max()+1; i++) for (int i = 1; i <= used.Max() + 1; i++)
{ {
if (!used.Contains(i)) if (!used.Contains(i))
{ {

View File

@ -1,5 +1,4 @@
using System.Collections; using System.Collections.Generic;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
@ -33,6 +32,24 @@ namespace VelNet
writer.Write(c.a); writer.Write(c.a);
} }
public static void Write(this BinaryWriter writer, List<int> l)
{
writer.Write(l.Count());
foreach (int i in l)
{
writer.Write(i);
}
}
public static void Write(this BinaryWriter writer, List<string> l)
{
writer.Write(l.Count());
foreach (string i in l)
{
writer.Write(i);
}
}
#endregion #endregion
#region Readers #region Readers
@ -62,14 +79,38 @@ namespace VelNet
); );
} }
public static List<int> ReadIntList(this BinaryReader reader)
{
int length = reader.ReadInt32();
List<int> l = new List<int>(length);
for (int i = 0; i < length; i++)
{
l.Add(reader.ReadInt32());
}
return l;
}
public static List<string> ReadStringList(this BinaryReader reader)
{
int length = reader.ReadInt32();
List<string> l = new List<string>(length);
for (int i = 0; i < length; i++)
{
l.Add(reader.ReadString());
}
return l;
}
#endregion #endregion
public static bool BytesSame(byte[] b1, byte[] b2) 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 && b2 == null) return false; // only one null if (b1 != null && b2 == null) return false; // only one null
if (b1 == null) return true; // both null if (b1 == null) return true; // both null
// length doesn't match // length doesn't match
if (b1.Length != b2.Length) if (b1.Length != b2.Length)

View File

@ -139,5 +139,11 @@ namespace VelNet
); );
} }
} }
[VelNetRPC]
private void Test()
{
}
} }
} }

View File

@ -11,6 +11,12 @@ using System.IO;
namespace VelNet namespace VelNet
{ {
/// <summary>Used to flag methods as remote-callable.</summary>
public class VelNetRPC : Attribute
{
}
[AddComponentMenu("VelNet/VelNet Manager")] [AddComponentMenu("VelNet/VelNet Manager")]
public class VelNetManager : MonoBehaviour public class VelNetManager : MonoBehaviour
{ {
@ -170,7 +176,7 @@ namespace VelNet
public class RoomDataMessage : Message public class RoomDataMessage : Message
{ {
public string room; public string room;
public readonly List<Tuple<int, string>> members = new List<Tuple<int, string>>(); public readonly List<(int, string)> members = new List<(int, string)>();
} }
public class JoinMessage : Message public class JoinMessage : Message
@ -354,7 +360,6 @@ namespace VelNet
try try
{ {
Debug.Log(jm.room);
OnJoinedRoom?.Invoke(jm.room); OnJoinedRoom?.Invoke(jm.room);
} }
// prevent errors in subscribers from breaking our code // prevent errors in subscribers from breaking our code
@ -495,6 +500,13 @@ namespace VelNet
sceneObjects[i].networkId = -1 + "-" + sceneObjects[i].sceneNetworkId; sceneObjects[i].networkId = -1 + "-" + sceneObjects[i].sceneNetworkId;
sceneObjects[i].owner = masterPlayer; sceneObjects[i].owner = masterPlayer;
sceneObjects[i].isSceneObject = true; // needed for special handling when deleted sceneObjects[i].isSceneObject = true; // needed for special handling when deleted
try {
sceneObjects[i].OwnershipChanged?.Invoke(masterPlayer);
}
catch (Exception e)
{
Debug.LogError("Error in event handling.\n" + e);
}
if (objects.ContainsKey(sceneObjects[i].networkId)) if (objects.ContainsKey(sceneObjects[i].networkId))
{ {
@ -681,7 +693,8 @@ namespace VelNet
while (socketConnection.Connected) while (socketConnection.Connected)
{ {
//read a byte //read a byte
MessageReceivedType type = (MessageReceivedType)stream.ReadByte(); int b = stream.ReadByte();
MessageReceivedType type = (MessageReceivedType)b;
switch (type) switch (type)
{ {
@ -735,8 +748,7 @@ namespace VelNet
int s = stream.ReadByte(); //size of string int s = stream.ReadByte(); //size of string
utf8data = ReadExact(stream, s); //the username utf8data = ReadExact(stream, s); //the username
string username = Encoding.UTF8.GetString(utf8data); string username = Encoding.UTF8.GetString(utf8data);
rdm.members.Add(new Tuple<int, string>(client_id, username)); rdm.members.Add((client_id, username));
Debug.Log(username);
} }
AddMessage(rdm); AddMessage(rdm);
@ -1042,7 +1054,7 @@ namespace VelNet
SendToGroup(group, mem.ToArray(), reliable); SendToGroup(group, mem.ToArray(), reliable);
} }
internal static void SendToRoom(byte[] message, bool include_self = false, bool reliable = true, bool ordered = false) internal static bool SendToRoom(byte[] message, bool include_self = false, bool reliable = true, bool ordered = false)
{ {
byte sendType = (byte)MessageSendType.MESSAGE_OTHERS; 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_ORDERED;
@ -1057,7 +1069,7 @@ namespace VelNet
writer.Write(sendType); writer.Write(sendType);
writer.Write(get_be_bytes(message.Length)); writer.Write(get_be_bytes(message.Length));
writer.Write(message); writer.Write(message);
SendTcpMessage(mem.ToArray()); return SendTcpMessage(mem.ToArray());
} }
else else
{ {
@ -1066,11 +1078,12 @@ namespace VelNet
Array.Copy(get_be_bytes(instance.userid), 0, toSend, 1, 4); Array.Copy(get_be_bytes(instance.userid), 0, toSend, 1, 4);
Array.Copy(message, 0, toSend, 5, message.Length); Array.Copy(message, 0, toSend, 5, message.Length);
SendUdpMessage(toSend, message.Length + 5); //shouldn't be over 1024... SendUdpMessage(toSend, message.Length + 5); //shouldn't be over 1024...
return true;
} }
} }
internal static void SendToGroup(string group, byte[] message, bool reliable = true) internal static bool SendToGroup(string group, byte[] message, bool reliable = true)
{ {
byte[] utf8bytes = Encoding.UTF8.GetBytes(group); byte[] utf8bytes = Encoding.UTF8.GetBytes(group);
if (reliable) if (reliable)
@ -1082,7 +1095,7 @@ namespace VelNet
writer.Write(message); writer.Write(message);
writer.Write((byte)utf8bytes.Length); writer.Write((byte)utf8bytes.Length);
writer.Write(utf8bytes); writer.Write(utf8bytes);
SendTcpMessage(stream.ToArray()); return SendTcpMessage(stream.ToArray());
} }
else else
{ {
@ -1093,6 +1106,7 @@ namespace VelNet
Array.Copy(utf8bytes, 0, toSend, 6, utf8bytes.Length); Array.Copy(utf8bytes, 0, toSend, 6, utf8bytes.Length);
Array.Copy(message, 0, toSend, 6 + utf8bytes.Length, message.Length); Array.Copy(message, 0, toSend, 6 + utf8bytes.Length, message.Length);
SendUdpMessage(toSend, 6 + utf8bytes.Length + message.Length); SendUdpMessage(toSend, 6 + utf8bytes.Length + message.Length);
return true;
} }
} }
@ -1148,6 +1162,15 @@ namespace VelNet
newObject.networkId = networkId; newObject.networkId = networkId;
newObject.prefabName = prefabName; newObject.prefabName = prefabName;
newObject.owner = localPlayer; newObject.owner = localPlayer;
try
{
newObject.OwnershipChanged?.Invoke(localPlayer);
}
catch (Exception e)
{
Debug.LogError("Error in event handling.\n" + e);
}
instance.objects.Add(newObject.networkId, newObject); instance.objects.Add(newObject.networkId, newObject);
@ -1170,6 +1193,14 @@ namespace VelNet
newObject.networkId = networkId; newObject.networkId = networkId;
newObject.prefabName = prefabName; newObject.prefabName = prefabName;
newObject.owner = owner; newObject.owner = owner;
try
{
newObject.OwnershipChanged?.Invoke(owner);
}
catch (Exception e)
{
Debug.LogError("Error in event handling.\n" + e);
}
instance.objects.Add(newObject.networkId, newObject); instance.objects.Add(newObject.networkId, newObject);
} }
@ -1230,6 +1261,12 @@ namespace VelNet
/// <returns>True if successfully transferred, False if transfer message not sent</returns> /// <returns>True if successfully transferred, False if transfer message not sent</returns>
public static bool TakeOwnership(string networkId) public static bool TakeOwnership(string networkId)
{ {
if (!InRoom)
{
Debug.LogError("Can't take ownership. Not in a room.");
return false;
}
// local player must exist // local player must exist
if (LocalPlayer == null) if (LocalPlayer == null)
{ {
@ -1253,6 +1290,14 @@ namespace VelNet
// immediately successful // immediately successful
instance.objects[networkId].owner = LocalPlayer; instance.objects[networkId].owner = LocalPlayer;
try
{
instance.objects[networkId].OwnershipChanged?.Invoke(LocalPlayer);
}
catch (Exception e)
{
Debug.LogError("Error in event handling.\n" + e);
}
// must be ordered, so that ownership transfers are not confused. // 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. // Also sent to all players, so that multiple simultaneous requests will result in the same outcome.

View File

@ -24,7 +24,7 @@ namespace VelNet
internal int lastObjectId; internal int lastObjectId;
private bool isMaster; public bool IsMaster { get; private set; }
public VelNetPlayer() public VelNetPlayer()
@ -51,7 +51,7 @@ namespace VelNet
} }
} }
if (isMaster) if (IsMaster)
{ {
//send a list of scene object ids when someone joins //send a list of scene object ids when someone joins
SendSceneUpdate(); SendSceneUpdate();
@ -72,13 +72,15 @@ namespace VelNet
{ {
using MemoryStream mem = new MemoryStream(m.data); using MemoryStream mem = new MemoryStream(m.data);
using BinaryReader reader = new BinaryReader(mem); using BinaryReader reader = new BinaryReader(mem);
//individual message parameters separated by comma //individual message parameters separated by comma
VelNetManager.MessageType messageType = (VelNetManager.MessageType)reader.ReadByte(); VelNetManager.MessageType messageType = (VelNetManager.MessageType)reader.ReadByte();
switch (messageType) switch (messageType)
{ {
case VelNetManager.MessageType.ObjectSync: // sync update for an object I may own // sync update for an object "I" may own
// "I" being the person sending
case VelNetManager.MessageType.ObjectSync:
{ {
string objectKey = reader.ReadString(); string objectKey = reader.ReadString();
byte componentIdx = reader.ReadByte(); byte componentIdx = reader.ReadByte();
@ -86,9 +88,13 @@ namespace VelNet
byte[] syncMessage = reader.ReadBytes(messageLength); byte[] syncMessage = reader.ReadBytes(messageLength);
if (manager.objects.ContainsKey(objectKey)) if (manager.objects.ContainsKey(objectKey))
{ {
if (manager.objects[objectKey].owner == this) bool isRpc = (componentIdx & 1) == 1;
componentIdx = (byte)(componentIdx >> 1);
// rpcs can be sent by non-owners
if (isRpc || manager.objects[objectKey].owner == this)
{ {
manager.objects[objectKey].ReceiveBytes(componentIdx, syncMessage); manager.objects[objectKey].ReceiveBytes(componentIdx, isRpc, syncMessage);
} }
} }
@ -101,6 +107,14 @@ namespace VelNet
if (manager.objects.ContainsKey(networkId)) if (manager.objects.ContainsKey(networkId))
{ {
manager.objects[networkId].owner = this; manager.objects[networkId].owner = this;
try
{
manager.objects[networkId].OwnershipChanged?.Invoke(this);
}
catch (Exception e)
{
Debug.LogError("Error in event handling.\n" + e);
}
} }
break; break;
@ -130,9 +144,12 @@ namespace VelNet
{ {
VelNetManager.SomebodyDestroyedNetworkObject(reader.ReadString()); VelNetManager.SomebodyDestroyedNetworkObject(reader.ReadString());
} }
break; break;
} }
case VelNetManager.MessageType.Custom: // custom packets // Custom packets. These are global data that can be sent from anywhere.
// Any script can subscribe to the callback to receive the message data.
case VelNetManager.MessageType.Custom:
{ {
int len = reader.ReadInt32(); int len = reader.ReadInt32();
try try
@ -153,12 +170,12 @@ namespace VelNet
public void SetAsMasterPlayer() public void SetAsMasterPlayer()
{ {
isMaster = true; IsMaster = true;
//if I'm master, I'm now responsible for updating all scene objects //if I'm master, I'm now responsible for updating all scene objects
//FindObjectsOfType<NetworkObject>(); //FindObjectsOfType<NetworkObject>();
} }
public static void SendGroupMessage(NetworkObject obj, string group, byte componentIdx, byte[] data, bool reliable = true) public static bool SendGroupMessage(NetworkObject obj, string group, byte componentIdx, byte[] data, bool reliable = true)
{ {
using MemoryStream mem = new MemoryStream(); using MemoryStream mem = new MemoryStream();
using BinaryWriter writer = new BinaryWriter(mem); using BinaryWriter writer = new BinaryWriter(mem);
@ -167,10 +184,10 @@ namespace VelNet
writer.Write(componentIdx); writer.Write(componentIdx);
writer.Write(data.Length); writer.Write(data.Length);
writer.Write(data); writer.Write(data);
VelNetManager.SendToGroup(group, mem.ToArray(), reliable); return VelNetManager.SendToGroup(group, mem.ToArray(), reliable);
} }
public static void SendMessage(NetworkObject obj, byte componentIdx, byte[] data, bool reliable = true) public static bool SendMessage(NetworkObject obj, byte componentIdx, byte[] data, bool reliable = true)
{ {
using MemoryStream mem = new MemoryStream(); using MemoryStream mem = new MemoryStream();
using BinaryWriter writer = new BinaryWriter(mem); using BinaryWriter writer = new BinaryWriter(mem);
@ -179,7 +196,7 @@ namespace VelNet
writer.Write(componentIdx); writer.Write(componentIdx);
writer.Write(data.Length); writer.Write(data.Length);
writer.Write(data); writer.Write(data);
VelNetManager.SendToRoom(mem.ToArray(), false, reliable); return VelNetManager.SendToRoom(mem.ToArray(), false, reliable);
} }
public void SendSceneUpdate() public void SendSceneUpdate()
@ -223,6 +240,14 @@ namespace VelNet
// immediately successful // immediately successful
manager.objects[networkId].owner = this; manager.objects[networkId].owner = this;
try
{
manager.objects[networkId].OwnershipChanged?.Invoke(this);
}
catch (Exception e)
{
Debug.LogError("Error in event handling.\n" + e);
}
// must be ordered, so that ownership transfers are not confused. // 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. // Also sent to all players, so that multiple simultaneous requests will result in the same outcome.

View File

@ -1,7 +1,7 @@
{ {
"name": "edu.uga.engr.vel.velnet", "name": "edu.uga.engr.vel.velnet",
"displayName": "VelNet", "displayName": "VelNet",
"version": "1.0.12", "version": "1.0.13",
"unity": "2019.1", "unity": "2019.1",
"description": "A custom networking library for Unity.", "description": "A custom networking library for Unity.",
"keywords": [ "keywords": [