reorganized into a package, added a namespace, added an assemblydefinition, attempt at ci for upm branch creation

handTracking
Anton Franzluebbers 2022-01-06 16:02:50 -05:00
parent 18746b43d8
commit 480b9cd1ca
60 changed files with 5304 additions and 868 deletions

19
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: CI
on:
push:
branches:
- main
jobs:
split-upm:
name: split upm branch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: split upm branch
run: |
git subtree split -P "$PKG_ROOT" -b upm
git push -u origin upm
env:
PKG_ROOT: TestVelGameServer\Packages\VelNetUnity

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# VelNetUnity
Custom networking package for Unity
![VelNet Logo](velnet_logo.png)
---
See [VelNetServer](https://github.com/velaboratory/VelNetServer) for description of the server.
By contrast to the server, the Unity side is pretty complex.
### NetworkManager
Deals with instantiating NetworkPlayers, and dealing with join, left, new master client events. Also manages the list of NetworkObjects. It maintains a list of prefabs that can be instantiated, as well as the prefab to use for players.
### NetworkPlayer
One of these will be instantiated for each player that joins the "room". Most application communication happens by calling methods on this object. Voice comms are also handled within this object. Only one NetworkPlayer "isLocal". The others are remote. The local network player is the one that can modify it's "owned" network objects. It also has a special networkobject representing itself (setup within the prefab, not part of the networkmanager list). This is where most of the user behavior is going to go (e.g. an avatar).
### NetworkObject
Something that can be owned by a network player, who is responsible for updating it. Only the owner network player can/should modify a network object, whether locally or in response to a received message from the local version. NetworkObjects are delineated by an session-unique id, which consists of the creator's userid + "-" + an increasing number designated by the creator. Only local NetworkPlayers should send instantiate message, which contain this id. This class should be extended to sync and send messages. Scenes are basically built from NetworkObjects. Scene's can also start with network objects. Those are known by all, and start with "-1-increasing". That should happen the same on all clients, and is done locally at the start of the networkmanager. An example of a NetworkObject is SyncTransform, which is a simple one that keeps an object's position and orientation synchronized.
### Ownership
Ownership of objects changes regularly. Any local networkplayer can send a message to take ownership of a networkid. That client immediately assumes ownership. The message is sent to the network, and will be ordered with any other ownership messages. So, if two clients try to get ownership at the same time, both will assume ownership, but one will happen after the other. This means that the one who was first will quickly lose ownership.
### Joining
An interesting problem is what to do when new clients join. This is partially left up to the developer. Any objects that are instantiated (or scene objects that are deleted) will be automatically handled.
### Message Groups
Clients can manage network traffic using messaging groups. This is especially useful for audio, where you can maintain a list of people close enough to you to hear audio

View File

@ -1,18 +0,0 @@
See VelNet for description of the server.
By contrast to the server, the Unity side is pretty complex.
NetworkManager - Deals with instantiating NetworkPlayers, and dealing with join, left, new master client events. Also manages the list of NetworkObjects. It maintains a list of prefabs that can be instantiated, as well as the prefab to use for players.
NetworkPlayer - One of these will be instantiated for each player that joins the "room". Most application communication happens by calling methods on this object. Voice comms are also handled within this object. Only one NetworkPlayer "isLocal". The others are remote. The local network player is the one that can modify it's "owned" network objects. It also has a special networkobject representing itself (setup within the prefab, not part of the networkmanager list). This is where most of the user behavior is going to go (e.g. an avatar).
NetworkObject - Something that can be owned by a network player, who is responsible for updating it. Only the owner network player can/should modify a network object, whether locally or in response to a received message from the local version. NetworkObjects are delineated by an session-unique id, which consists of the creator's userid + "-" + an increasing number designated by the creator. Only local NetworkPlayers should send instantiate message, which contain this id. This class should be extended to sync and send messages. Scenes are basically built from NetworkObjects. Scene's can also start with network objects. Those are known by all, and start with "-1-increasing". That should happen the same on all clients, and is done locally at the start of the networkmanager. An example of a NetworkObject is SyncTransform, which is a simple one that keeps an object's position and orientation synchronized.
Ownership of objects changes regularly. Any local networkplayer can send a message to take ownership of a networkid. That client immediately assumes ownership. The message is sent to the network, and will be ordered with any other ownership messages. So, if two clients try to get ownership at the same time, both will assume ownership, but one will happen after the other. This means that the one who was first will quickly lose ownership.
An interesting problem is what to do when new clients join. This is partially left up to the developer. Any objects that are instantiated (or scene objects that are deleted) will be automatically handled.
Clients can manage network traffic using messaging groups. This is especially useful for audio, where you can maintain a list of people close enough to you to hear audio

View File

@ -70,3 +70,5 @@ crashlytics-build.properties
# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*
.idea/

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 867c77c74f84b41acb49da778488a75f
guid: b52a74ceb5dcd9d498e95617f4247a27
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 09c6fdabe05be4c86917b1c585ed4a40
guid: 8b822dd2b9e99144097e61694b8dab65
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 21c2212b420fd48a1b3552af8d219f80
guid: 14a35ccfe945cc54292ca4e8940b53b9
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c506b9118efc2de4ea5f2b2f26a52b9d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,94 @@
using System;
using Dissonance;
using Dissonance.Networking;
using UnityEngine;
namespace VelNetUnity
{
public class VelCommsNetwork : MonoBehaviour, ICommsNetwork
{
public ConnectionStatus Status => manager.connected ? ConnectionStatus.Connected : ConnectionStatus.Disconnected;
public NetworkMode Mode => NetworkMode.Client;
public event Action<NetworkMode> ModeChanged;
public event Action<string, CodecSettings> PlayerJoined;
public event Action<string> PlayerLeft;
public event Action<VoicePacket> VoicePacketReceived;
public event Action<TextMessage> TextPacketReceived;
public event Action<string> PlayerStartedSpeaking;
public event Action<string> PlayerStoppedSpeaking;
public event Action<RoomEvent> PlayerEnteredRoom;
public event Action<RoomEvent> PlayerExitedRoom;
ConnectionStatus _status = ConnectionStatus.Disconnected;
CodecSettings initSettings;
public string dissonanceId;
public DissonanceComms comms;
public NetworkManager manager;
public Action<ArraySegment<byte>> voiceQueued = delegate { }; //listen to this if you want to send voice
public void Initialize(string playerName, Rooms rooms, PlayerChannels playerChannels, RoomChannels roomChannels, CodecSettings codecSettings)
{
dissonanceId = playerName;
initSettings = codecSettings;
comms.ResetMicrophoneCapture();
}
public void voiceReceived(string sender, byte[] data)
{
uint sequenceNumber = BitConverter.ToUInt32(data, 0);
VoicePacket vp = new VoicePacket(sender, ChannelPriority.Default, 1, true, new ArraySegment<byte>(data, 4, data.Length - 4), sequenceNumber);
VoicePacketReceived(vp);
}
public void SendText(string data, ChannelType recipientType, string recipientId)
{
Debug.Log("sending text");
}
public void SendVoice(ArraySegment<byte> data)
{
voiceQueued(data);
}
// Start is called before the first frame update
void Start()
{
_status = ConnectionStatus.Connected;
comms = GetComponent<DissonanceComms>();
}
public void playerJoined(string id)
{
Debug.Log("dissonance player joined");
PlayerJoined(id, initSettings);
RoomEvent re = new RoomEvent();
re.Joined = true;
re.Room = "Global";
re.PlayerName = id;
PlayerEnteredRoom(re);
}
public void playerLeft(string id)
{
RoomEvent re = new RoomEvent();
re.Joined = false;
re.Room = "Global";
re.PlayerName = id;
PlayerExitedRoom(re);
PlayerLeft(id);
}
public void playerStartedSpeaking(string id)
{
PlayerStartedSpeaking(id);
}
public void playerStoppedSpeaking(string id)
{
PlayerStoppedSpeaking(id);
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 2fc687ece57d64c7082cd32261ac438b
guid: 07900994db5c50c4794e338da120f31b
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,76 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace VelNetUnity
{
public class NetworkGUI : MonoBehaviour
{
public NetworkManager networkManager;
public InputField userInput;
public InputField sendInput;
public InputField roomInput;
public Text messages;
public List<string> messageBuffer;
public Dropdown microphones;
Dissonance.DissonanceComms comms;
public void handleSend()
{
if (sendInput.text != "")
{
networkManager.sendTo(NetworkManager.MessageType.OTHERS, sendInput.text);
}
}
public void handleLogin()
{
if (userInput.text != "")
{
networkManager.login(userInput.text, "nopass");
}
}
public void handleJoin()
{
if (roomInput.text != "")
{
networkManager.join(roomInput.text);
}
}
// Start is called before the first frame update
void Start()
{
comms = FindObjectOfType<Dissonance.DissonanceComms>();
microphones.AddOptions(new List<string>(Microphone.devices));
networkManager.messageReceived += (m) =>
{
string s = m.type + ":" + m.sender + ":" + m.text;
messageBuffer.Add(s);
messages.text = "";
if (messageBuffer.Count > 10)
{
messageBuffer.RemoveAt(0);
}
for (int i = 0; i < messageBuffer.Count; i++)
{
messages.text = messages.text + messageBuffer[i] + "\n";
}
};
}
public void handleMicrophoneSelection()
{
comms.MicrophoneName = microphones.options[microphones.value].text;
}
// Update is called once per frame
void Update()
{
}
}
}

View File

@ -0,0 +1,258 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Dissonance;
using System.Text;
namespace VelNetUnity
{
public class PlayerController : NetworkObject, IDissonancePlayer
{
VelCommsNetwork comms;
bool isSpeaking = false;
uint lastAudioId = 0;
public string dissonanceID = "";
//required by dissonance for spatial audio
public string PlayerId => dissonanceID;
public Vector3 Position => transform.position;
public Quaternion Rotation => transform.rotation;
public NetworkPlayerType Type => owner.isLocal ? NetworkPlayerType.Local : NetworkPlayerType.Remote;
public bool IsTracking => true;
public Vector3 targetPosition;
public Quaternion targetRotation;
public List<int> closePlayers = new List<int>();
public byte[] getSyncMessage()
{
float[] data = new float[7];
for (int i = 0; i < 3; i++)
{
data[i] = transform.position[i];
data[i + 3] = transform.rotation[i];
}
data[6] = transform.rotation[3];
byte[] toReturn = new byte[sizeof(float) * data.Length];
Buffer.BlockCopy(data, 0, toReturn, 0, toReturn.Length);
return toReturn;
}
public override void handleMessage(string identifier, byte[] message)
{
switch (identifier)
{
case "s": //sync message
float[] data = new float[7];
Buffer.BlockCopy(message, 0, data, 0, message.Length);
for (int i = 0; i < 3; i++)
{
targetPosition[i] = data[i];
targetRotation[i] = data[i + 3];
}
targetRotation[3] = data[6];
break;
case "a": //audio data
{
if (isSpeaking)
{
comms.voiceReceived(dissonanceID, message);
}
break;
}
case "d": //dissonance id (player joined)
{
if (dissonanceID == "") //I don't have this yet
{
dissonanceID = Encoding.UTF8.GetString(message);
//tell the comms network that this player joined the channel
comms.playerJoined(dissonanceID); //tell dissonance
comms.comms.TrackPlayerPosition(this); //tell dissonance to track the remote player
}
break;
}
case "x": //speaking state
{
if (message[0] == 0)
{
comms.playerStoppedSpeaking(dissonanceID);
isSpeaking = false;
}
else
{
comms.playerStartedSpeaking(dissonanceID);
isSpeaking = true;
}
break;
}
}
}
// Start is called before the first frame update
void Start()
{
//handle dissonance stuff
comms = FindObjectOfType<VelCommsNetwork>();
if (comms != null)
{
if (owner.isLocal)
{
setDissonanceID(comms.dissonanceId);
comms.voiceQueued += sendVoiceData;
//we also need to know when other players join, so we can send the dissonance ID again
owner.manager.onPlayerJoined += (player) =>
{
byte[] b = Encoding.UTF8.GetBytes(dissonanceID);
owner.sendMessage(this, "d", b);
};
owner.manager.setupMessageGroup("close", closePlayers.ToArray());
}
}
if (owner.isLocal)
{
StartCoroutine(syncBehavior());
}
}
void sendVoiceData(ArraySegment<byte> data)
{
//need to send it
if (owner != null && owner.isLocal)
{
byte[] toSend = new byte[data.Count + 4];
byte[] lastAudioIdBytes = BitConverter.GetBytes(lastAudioId++);
Buffer.BlockCopy(lastAudioIdBytes, 0, toSend, 0, 4);
Buffer.BlockCopy(data.Array, data.Offset, toSend, 4, data.Count);
owner.sendGroupMessage(this, "close", "a", toSend, false); //send voice data unreliably
}
}
public void setDissonanceID(string id) //this sort of all initializes dissonance
{
dissonanceID = id;
byte[] b = Encoding.UTF8.GetBytes(dissonanceID);
owner.sendMessage(this, "d", b);
comms.comms.TrackPlayerPosition(this);
}
void voiceInitialized(string id)
{
dissonanceID = id;
}
void OnDestroy()
{
comms.playerLeft(dissonanceID);
}
IEnumerator syncBehavior()
{
while (true)
{
owner.sendMessage(this, "s", getSyncMessage());
yield return new WaitForSeconds(.1f);
}
}
// Update is called once per frame
void Update()
{
if (owner != null && owner.isLocal)
{
PlayerController[] players = FindObjectsOfType<PlayerController>();
bool shouldUpdate = false;
for (int i = 0; i < players.Length; i++)
{
if (players[i] == this)
{
continue;
}
float dist = Vector3.Distance(players[i].transform.position, transform.position);
if (dist < 2 && !closePlayers.Contains(players[i].owner.userid))
{
closePlayers.Add(players[i].owner.userid);
shouldUpdate = true;
}
else if (dist >= 2 && closePlayers.Contains(players[i].owner.userid))
{
closePlayers.Remove(players[i].owner.userid);
shouldUpdate = true;
}
}
if (shouldUpdate)
{
owner.manager.setupMessageGroup("close", closePlayers.ToArray());
}
}
if (owner != null && !owner.isLocal)
{
transform.position = Vector3.Lerp(transform.position, targetPosition, .1f);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, .1f);
}
else if (owner != null && owner.isLocal)
{
//handle dissonance comms
//if we're not speaking, and the comms say we are, send a speaking event, which will be received on other network players and sent to their comms accordingly
if (comms.comms.FindPlayer(dissonanceID).IsSpeaking != isSpeaking) //unfortunately, there does not seem to be an event for this
{
isSpeaking = !isSpeaking;
byte[] toSend = new byte[1];
toSend[0] = isSpeaking ? (byte)1 : (byte)0;
owner.sendMessage(this, "x", toSend);
if (!isSpeaking)
{
lastAudioId = 0;
}
}
Vector3 movement = new Vector3();
movement.x += Input.GetAxis("Horizontal");
movement.y += Input.GetAxis("Vertical");
movement.z = 0;
transform.Translate(movement * Time.deltaTime);
if (Input.GetKeyDown(KeyCode.Space))
{
owner.networkInstantiate("TestNetworkedGameObject");
}
if (Input.GetKeyDown(KeyCode.BackQuote))
{
foreach (KeyValuePair<string, NetworkObject> kvp in owner.manager.objects)
{
owner.takeOwnership(kvp.Key);
}
}
if (Input.GetKeyDown(KeyCode.Backspace))
{
foreach (KeyValuePair<string, NetworkObject> kvp in owner.manager.objects)
{
owner.networkDestroy(kvp.Key);
}
}
}
}
}
}

View File

@ -1,505 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using Dissonance;
using System.Net;
public class NetworkManager : MonoBehaviour
{
public enum MessageType { OTHERS=0,ALL=1,OTHERS_ORDERED=2,ALL_ORDERED=3};
public string host;
public int port;
#region private members
private TcpClient socketConnection;
private Socket udpSocket;
public bool udpConnected = false;
IPEndPoint RemoteEndPoint;
private Thread clientReceiveThread;
private Thread clientReceiveThreadUDP;
public int userid = -1;
public string room;
int messagesReceived = 0;
public GameObject playerPrefab;
public Dictionary<int, NetworkPlayer> players = new Dictionary<int, NetworkPlayer>();
public Action<NetworkPlayer> onJoinedRoom = delegate { };
public Action<NetworkPlayer> onPlayerJoined = delegate { };
public Action<NetworkPlayer> onPlayerLeft = delegate { };
public List<NetworkObject> prefabs = new List<NetworkObject>();
public NetworkObject[] sceneObjects;
public List<string> deletedSceneObjects = new List<string>();
public Dictionary<string, NetworkObject> objects = new Dictionary<string, NetworkObject>(); //maintains a list of all known objects on the server (ones that have ids)
NetworkPlayer masterPlayer = null;
#endregion
// Use this for initialization
public class Message
{
public int type;
public string text;
public int sender;
}
public List<Message> receivedMessages = new List<Message>();
void Start()
{
ConnectToTcpServer();
sceneObjects = GameObject.FindObjectsOfType<NetworkObject>(); //add all local network objects
}
private void addMessage(Message m)
{
lock (receivedMessages)
{
//Debug.Log(messagesReceived++);
receivedMessages.Add(m);
}
}
private void Update()
{
lock(receivedMessages) { //the main thread, which can do Unity stuff
foreach(Message m in receivedMessages)
{
if(m.type == 0) //when you join the server
{
this.userid = m.sender;
Debug.Log("joined server");
//start the udp thread
clientReceiveThreadUDP = new Thread(new ThreadStart(ListenForDataUDP));
clientReceiveThreadUDP.IsBackground = true;
clientReceiveThreadUDP.Start();
}
if (m.type == 2)
{
//if this message is for me, that means I joined a new room...
if (this.userid == m.sender)
{
foreach (KeyValuePair<int, NetworkPlayer> kvp in players)
{
Destroy(kvp.Value.gameObject);
}
players.Clear(); //we clear the list, but will recreate as we get messages from people in our room
if (m.text != "")
{
NetworkPlayer player = GameObject.Instantiate<GameObject>(playerPrefab).GetComponent<NetworkPlayer>();
player.isLocal = true;
player.userid = m.sender;
players.Add(userid, player);
player.room = m.text;
player.manager = this;
onJoinedRoom(player);
}
}
else //not for me, a player is joining or leaving
{
NetworkPlayer me = players[userid];
if (me.room != m.text)
{
//we got a left message, kill it
//change ownership of all objects to master
foreach (KeyValuePair<string, NetworkObject> kvp in objects)
{
if (kvp.Value.owner == players[m.sender]) //the owner is the player that left
{
if (me.isLocal && me == masterPlayer) //I'm the local master player, so can take ownership immediately
{
me.takeOwnership(kvp.Key);
}
else if (players[m.sender] == masterPlayer) //the master player left, so everyone should set the owner null (we should get a new master shortly)
{
kvp.Value.owner = null;
}
}
}
Destroy(players[m.sender].gameObject);
players.Remove(m.sender);
}
else
{
//we got a join mesage, create it
NetworkPlayer player = GameObject.Instantiate<GameObject>(playerPrefab).GetComponent<NetworkPlayer>();
player.isLocal = false;
player.room = m.text;
player.userid = m.sender;
player.manager = this;
players.Add(m.sender, player);
onPlayerJoined(player);
}
}
}
if(m.type == 3) //generic message
{
players[m.sender]?.handleMessage(m);
}
if(m.type == 4) //change master player (this should only happen when the first player joins or if the master player leaves)
{
if (masterPlayer == null)
{
masterPlayer = players[m.sender];
//no master player yet, add the scene objects
for (int i = 0; i < sceneObjects.Length; i++)
{
sceneObjects[i].networkId = -1 + "-" + i;
sceneObjects[i].owner = masterPlayer;
sceneObjects[i].isSceneObject = true; //needed for special handling when deleted
objects.Add(sceneObjects[i].networkId,sceneObjects[i]);
}
}
else
{
masterPlayer = players[m.sender];
}
masterPlayer.setAsMasterPlayer();
//master player should take over any objects that do not have an owner
foreach(KeyValuePair<string,NetworkObject> kvp in objects)
{
if (kvp.Value.owner == null)
{
kvp.Value.owner = masterPlayer;
}
}
}
messageReceived(m);
}
receivedMessages.Clear();
}
}
private void OnApplicationQuit()
{
socketConnection.Close();
}
public Action<string, int> joinedRoom = delegate { };
public Action<Message> messageReceived = delegate { };
public Action<string, int> loggedIn = delegate { };
public Action<string[], int> roomsReceived = delegate { };
public bool connected = false;
/// <summary>
/// Setup socket connection.
/// </summary>
private void ConnectToTcpServer()
{
try
{
clientReceiveThread = new Thread(new ThreadStart(ListenForData));
clientReceiveThread.IsBackground = true;
clientReceiveThread.Start();
}
catch (Exception e)
{
Debug.Log("On client connect exception " + e);
}
}
void handleMessage(string s) //this parses messages from the server, and adds them to a queue to be processed on the main thread
{
Message m = new Message();
string[] sections = s.Split(':');
if (sections.Length > 0)
{
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;
}
}
}
}
/// <summary>
/// Runs in background clientReceiveThread; Listens for incomming data.
/// </summary>
private void ListenForData()
{
connected = true;
try
{
socketConnection = new TcpClient(host, port);
socketConnection.NoDelay = true;
Byte[] bytes = new Byte[1024];
string partialMessage = "";
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)
{
var 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)
{
lock(receivedMessages){
for (int i = 0; i < sections.Length - 1; i++)
{
if (i == 0)
{
handleMessage(partialMessage + sections[0]);
partialMessage = "";
}
else
{
handleMessage(sections[i]);
}
}
}
}
partialMessage = partialMessage + sections[sections.Length - 1];
}
}
}
}
catch (Exception socketException)
{
Debug.Log("Socket exception: " + socketException);
}
connected = false;
}
private void ListenForDataUDP()
{
//I don't yet have a UDP connection
try
{
var addresses = Dns.GetHostAddresses(host);
Debug.Assert(addresses.Length > 0);
RemoteEndPoint = new IPEndPoint(addresses[0], port);
udpSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
udpConnected = false;
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);
if(udpSocket.Available == 0)
{
Thread.Sleep(100);
Debug.Log("Waiting for UDP response");
}
else
{
break;
}
}
udpConnected = true;
while (true) {
int numReceived = udpSocket.Receive(buffer);
string message = Encoding.UTF8.GetString(buffer, 0, numReceived);
string[] sections = message.Split(':');
if(sections[0] == "0")
{
Debug.Log("UDP connected");
}
if(sections[0] == "3")
{
handleMessage(message);
}
}
} catch(Exception socketException)
{
Debug.Log("Socket exception: " + socketException);
}
}
private void SendUdpMessage(string message)
{
if (udpSocket == null || !udpConnected)
{
return;
}
byte[] data = Encoding.UTF8.GetBytes(message);
//Debug.Log("Attempting to send: " + message);
udpSocket.SendTo(data, data.Length, SocketFlags.None, RemoteEndPoint);
}
/// <summary>
/// Send message to server using socket connection.
/// </summary>
private void SendNetworkMessage(string clientMessage)
{
if (socketConnection == null)
{
return;
}
try
{
// Get a stream object for writing.
NetworkStream stream = socketConnection.GetStream();
if (stream.CanWrite)
{
// Convert string message to byte array.
clientMessage = 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);
}
}
catch (SocketException socketException)
{
Debug.Log("Socket exception: " + socketException);
}
}
public void login(string username, string password)
{
SendNetworkMessage("0:" + username + ":" + password);
}
public void join(string roomname)
{
SendNetworkMessage("2:" + roomname);
}
public void leave()
{
SendNetworkMessage("2:-1");
}
public void sendTo(MessageType type, string message, bool reliable=true)
{
if (reliable)
{
SendNetworkMessage("3:" + (int)type + ":" + message);
}
else
{
SendUdpMessage(userid + ":3:" + (int)type + ":" + message);
}
}
public void sendToGroup(string group, string message, bool reliable=true)
{
if (reliable)
{
SendNetworkMessage("4:" + group + ":" + message);
}
else
{
SendUdpMessage(userid + ":4:" + group + ":" + message);
}
}
//changes the designated group that sendto(4) will go to
public void setupMessageGroup(string groupname, int[] userids)
{
SendNetworkMessage("5:" + groupname + ":" + String.Join(":", userids));
}
public void deleteNetworkObject(string networkId)
{
if (objects.ContainsKey(networkId))
{
NetworkObject obj = objects[networkId];
if (obj.isSceneObject)
{
deletedSceneObjects.Add(networkId);
}
GameObject.Destroy(obj.gameObject);
objects.Remove(networkId);
}
}
}

View File

@ -1,15 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// This is a base class for all objects that a player can instantiated/owned
/// </summary>
public abstract class NetworkObject: MonoBehaviour
{
public NetworkPlayer owner;
public string networkId; //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
public string prefabName; //this may be empty if it's not a prefab (scene object)
public bool isSceneObject=false;
public abstract void handleMessage(string identifier, byte[] message);
}

View File

@ -1,233 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Text;
using System;
[RequireComponent(typeof(NetworkObject))]
public class NetworkPlayer : MonoBehaviour
{
public NetworkObject myObject;
public int userid;
public string username;
public string room;
public NetworkManager manager;
public bool isLocal = false;
public int lastObjectId=0; //for instantiation
bool isMaster = false;
void Start()
{
myObject.owner = this;
this.manager = GameObject.FindObjectOfType<NetworkManager>();
manager.onPlayerJoined += handlePlayerJoined;
}
// Update is called once per frame
void Update()
{
}
public void handlePlayerJoined(NetworkPlayer player)
{
//if this is the local player, go through the objects that I own, and send instantiation messages for the ones that have prefab names
if (this.isLocal)
{
foreach(KeyValuePair<string,NetworkObject> kvp in manager.objects)
{
if(kvp.Value.owner == this && kvp.Value.prefabName != "")
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "7," + kvp.Value.networkId + "," + kvp.Value.prefabName);
}
}
if (isMaster)
{
//send a list of scene object ids when someone joins
sendSceneUpdate();
}
}
}
public void handleMessage(NetworkManager.Message m)
{
//these are generally things that come from the "owner" and should be enacted locally, where appropriate
//we need to parse the message
//types of messages
string[] messages = m.text.Split(';'); //messages are split by ;
for(int i = 0; i < messages.Length; i++)
{
//individual message parameters separated by comma
string[] sections = messages[i].Split(',');
switch (sections[0])
{
case "1": //update my object's data
{
string identifier = sections[1];
byte[] message = Convert.FromBase64String(sections[2]);
myObject.handleMessage(identifier, message);
break;
}
case "5": //sync update for an object I may own
{
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)
{
manager.objects[objectKey].handleMessage(identifier, messageBytes);
}
}
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
}
NetworkObject temp = manager.prefabs.Find((prefab) => prefab.name == prefabName);
if (temp != null)
{
NetworkObject instance = GameObject.Instantiate<NetworkObject>(temp);
instance.networkId = networkId;
instance.prefabName = prefabName;
instance.owner = this;
manager.objects.Add(instance.networkId, instance);
}
break;
}
case "8": //I'm trying to destroy a gameobject I own
{
string networkId = sections[1];
manager.deleteNetworkObject(networkId);
break;
}
case "9": //deleted scene objects
{
for (int k = 1; k < sections.Length; k++)
{
manager.deleteNetworkObject(sections[k]);
}
break;
}
}
}
}
public void setAsMasterPlayer()
{
isMaster = true;
//if I'm master, I'm now responsible for updating all scene objects
//FindObjectsOfType<NetworkObject>();
}
public void sendGroupMessage(NetworkObject obj, string group, string identifier, byte[] data, bool reliable=true)
{
if (obj == myObject)
{
manager.sendToGroup(group, "1," + identifier + "," + Convert.ToBase64String(data), reliable);
}
else
{
manager.sendToGroup(group, "5," + obj.networkId + "," + identifier + "," + Convert.ToBase64String(data), reliable);
}
}
public void sendMessage(NetworkObject obj, string identifier, byte[] data, bool reliable=true)
{
if (obj == myObject)
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "1," + identifier +"," + Convert.ToBase64String(data), reliable);
}
else
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "5," + obj.networkId + "," + identifier + "," + Convert.ToBase64String(data), reliable);
}
}
public NetworkObject networkInstantiate(string prefabName)
{
if (!isLocal)
{
return null; //must be the local player to call instantiate
}
string networkId = this.userid + "-" + lastObjectId++;
NetworkObject temp = manager.prefabs.Find((prefab) => prefab.name == prefabName);
if (temp != null)
{
NetworkObject instance = GameObject.Instantiate<NetworkObject>(temp);
instance.networkId = networkId;
instance.prefabName = prefabName;
instance.owner = this;
manager.objects.Add(instance.networkId, instance);
manager.sendTo(NetworkManager.MessageType.OTHERS, "7," + networkId + "," + prefabName); //only sent to others, as I already instantiated this. Nice that it happens immediately.
return instance;
}
return null;
}
public void networkDestroy(string networkId)
{
if (!manager.objects.ContainsKey(networkId) || manager.objects[networkId].owner != this || !isLocal) return; //must be the local owner of the object to destroy it
manager.sendTo(NetworkManager.MessageType.ALL_ORDERED, "8," + networkId); //send to all, which will make me delete as well
}
public void takeOwnership(string networkId)
{
if (!manager.objects.ContainsKey(networkId) || !isLocal) return; //must exist and be the the local player
manager.objects[networkId].owner = this; //immediately successful
manager.sendTo(NetworkManager.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.
}
public void sendSceneUpdate()
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "9," + string.Join(",", manager.deletedSceneObjects));
}
}

View File

@ -1,80 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
/// <summary>
/// A simple class that will sync the position and rotation of a network object
/// </summary>
public class SyncTransform : NetworkObject
{
public Vector3 targetPosition;
public Quaternion targetRotation;
public byte[] getSyncMessage()
{
float[] data = new float[7];
for(int i = 0; i < 3; i++)
{
data[i] = transform.position[i];
data[i + 3] = transform.rotation[i];
}
data[6] = transform.rotation[3];
byte[] toReturn = new byte[sizeof(float) * data.Length];
Buffer.BlockCopy(data, 0, toReturn,0, toReturn.Length);
return toReturn;
}
public override void handleMessage(string identifier, byte[] message)
{
switch (identifier)
{
case "s":
float[] data = new float[7];
Buffer.BlockCopy(message, 0, data, 0, message.Length);
for (int i = 0; i < 3; i++)
{
targetPosition[i] = data[i];
targetRotation[i] = data[i + 3];
}
targetRotation[3] = data[6];
break;
}
}
// Start is called before the first frame update
void Start()
{
StartCoroutine(syncBehavior());
}
IEnumerator syncBehavior()
{
while (true)
{
if (owner != null && owner.isLocal)
{
owner.sendMessage(this, "s", getSyncMessage());
}
yield return new WaitForSeconds(.1f);
}
}
// Update is called once per frame
void Update()
{
if (owner != null && !owner.isLocal)
{
transform.position = Vector3.Lerp(transform.position, targetPosition, .1f);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, .1f);
}
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 453f243ca8f3d8b4db4a1db0d9f9d300
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 63ae5231117a1484198ba0a8f681612b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,522 @@
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using System.Net;
namespace VelNetUnity
{
public class NetworkManager : MonoBehaviour
{
public enum MessageType
{
OTHERS = 0,
ALL = 1,
OTHERS_ORDERED = 2,
ALL_ORDERED = 3
};
public string host;
public int port;
#region private members
private TcpClient socketConnection;
private Socket udpSocket;
public bool udpConnected = false;
IPEndPoint RemoteEndPoint;
private Thread clientReceiveThread;
private Thread clientReceiveThreadUDP;
public int userid = -1;
public string room;
int messagesReceived = 0;
public GameObject playerPrefab;
public Dictionary<int, NetworkPlayer> players = new Dictionary<int, NetworkPlayer>();
public Action<NetworkPlayer> onJoinedRoom = delegate { };
public Action<NetworkPlayer> onPlayerJoined = delegate { };
public Action<NetworkPlayer> onPlayerLeft = delegate { };
public List<NetworkObject> prefabs = new List<NetworkObject>();
public NetworkObject[] sceneObjects;
public List<string> deletedSceneObjects = new List<string>();
public Dictionary<string, NetworkObject> objects = new Dictionary<string, NetworkObject>(); //maintains a list of all known objects on the server (ones that have ids)
NetworkPlayer masterPlayer = null;
#endregion
// Use this for initialization
public class Message
{
public int type;
public string text;
public int sender;
}
public List<Message> receivedMessages = new List<Message>();
void Start()
{
ConnectToTcpServer();
sceneObjects = FindObjectsOfType<NetworkObject>(); //add all local network objects
}
private void addMessage(Message m)
{
lock (receivedMessages)
{
//Debug.Log(messagesReceived++);
receivedMessages.Add(m);
}
}
private void Update()
{
lock (receivedMessages)
{
//the main thread, which can do Unity stuff
foreach (Message m in receivedMessages)
{
if (m.type == 0) //when you join the server
{
userid = m.sender;
Debug.Log("joined server");
//start the udp thread
clientReceiveThreadUDP = new Thread(new ThreadStart(ListenForDataUDP));
clientReceiveThreadUDP.IsBackground = true;
clientReceiveThreadUDP.Start();
}
if (m.type == 2)
{
//if this message is for me, that means I joined a new room...
if (userid == m.sender)
{
foreach (KeyValuePair<int, NetworkPlayer> kvp in players)
{
Destroy(kvp.Value.gameObject);
}
players.Clear(); //we clear the list, but will recreate as we get messages from people in our room
if (m.text != "")
{
NetworkPlayer player = Instantiate<GameObject>(playerPrefab).GetComponent<NetworkPlayer>();
player.isLocal = true;
player.userid = m.sender;
players.Add(userid, player);
player.room = m.text;
player.manager = this;
onJoinedRoom(player);
}
}
else //not for me, a player is joining or leaving
{
NetworkPlayer me = players[userid];
if (me.room != m.text)
{
//we got a left message, kill it
//change ownership of all objects to master
foreach (KeyValuePair<string, NetworkObject> kvp in objects)
{
if (kvp.Value.owner == players[m.sender]) //the owner is the player that left
{
if (me.isLocal && me == masterPlayer) //I'm the local master player, so can take ownership immediately
{
me.takeOwnership(kvp.Key);
}
else if (players[m.sender] == masterPlayer) //the master player left, so everyone should set the owner null (we should get a new master shortly)
{
kvp.Value.owner = null;
}
}
}
Destroy(players[m.sender].gameObject);
players.Remove(m.sender);
}
else
{
//we got a join mesage, create it
NetworkPlayer player = Instantiate<GameObject>(playerPrefab).GetComponent<NetworkPlayer>();
player.isLocal = false;
player.room = m.text;
player.userid = m.sender;
player.manager = this;
players.Add(m.sender, player);
onPlayerJoined(player);
}
}
}
if (m.type == 3) //generic message
{
players[m.sender]?.handleMessage(m);
}
if (m.type == 4) //change master player (this should only happen when the first player joins or if the master player leaves)
{
if (masterPlayer == null)
{
masterPlayer = players[m.sender];
//no master player yet, add the scene objects
for (int i = 0; i < sceneObjects.Length; i++)
{
sceneObjects[i].networkId = -1 + "-" + i;
sceneObjects[i].owner = masterPlayer;
sceneObjects[i].isSceneObject = true; //needed for special handling when deleted
objects.Add(sceneObjects[i].networkId, sceneObjects[i]);
}
}
else
{
masterPlayer = players[m.sender];
}
masterPlayer.setAsMasterPlayer();
//master player should take over any objects that do not have an owner
foreach (KeyValuePair<string, NetworkObject> kvp in objects)
{
if (kvp.Value.owner == null)
{
kvp.Value.owner = masterPlayer;
}
}
}
messageReceived(m);
}
receivedMessages.Clear();
}
}
private void OnApplicationQuit()
{
socketConnection.Close();
}
public Action<string, int> joinedRoom = delegate { };
public Action<Message> messageReceived = delegate { };
public Action<string, int> loggedIn = delegate { };
public Action<string[], int> roomsReceived = delegate { };
public bool connected = false;
/// <summary>
/// Setup socket connection.
/// </summary>
private void ConnectToTcpServer()
{
try
{
clientReceiveThread = new Thread(new ThreadStart(ListenForData));
clientReceiveThread.IsBackground = true;
clientReceiveThread.Start();
}
catch (Exception e)
{
Debug.Log("On client connect exception " + e);
}
}
void handleMessage(string s) //this parses messages from the server, and adds them to a queue to be processed on the main thread
{
Message m = new Message();
string[] sections = s.Split(':');
if (sections.Length > 0)
{
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;
}
}
}
}
/// <summary>
/// Runs in background clientReceiveThread; Listens for incomming data.
/// </summary>
private void ListenForData()
{
connected = true;
try
{
socketConnection = new TcpClient(host, port);
socketConnection.NoDelay = true;
Byte[] bytes = new Byte[1024];
string partialMessage = "";
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)
{
var 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)
{
lock (receivedMessages)
{
for (int i = 0; i < sections.Length - 1; i++)
{
if (i == 0)
{
handleMessage(partialMessage + sections[0]);
partialMessage = "";
}
else
{
handleMessage(sections[i]);
}
}
}
}
partialMessage = partialMessage + sections[sections.Length - 1];
}
}
}
}
catch (Exception socketException)
{
Debug.Log("Socket exception: " + socketException);
}
connected = false;
}
private void ListenForDataUDP()
{
//I don't yet have a UDP connection
try
{
var addresses = Dns.GetHostAddresses(host);
Debug.Assert(addresses.Length > 0);
RemoteEndPoint = new IPEndPoint(addresses[0], port);
udpSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
udpConnected = false;
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);
if (udpSocket.Available == 0)
{
Thread.Sleep(100);
Debug.Log("Waiting for UDP response");
}
else
{
break;
}
}
udpConnected = true;
while (true)
{
int numReceived = udpSocket.Receive(buffer);
string message = Encoding.UTF8.GetString(buffer, 0, numReceived);
string[] sections = message.Split(':');
if (sections[0] == "0")
{
Debug.Log("UDP connected");
}
if (sections[0] == "3")
{
handleMessage(message);
}
}
}
catch (Exception socketException)
{
Debug.Log("Socket exception: " + socketException);
}
}
private void SendUdpMessage(string message)
{
if (udpSocket == null || !udpConnected)
{
return;
}
byte[] data = Encoding.UTF8.GetBytes(message);
//Debug.Log("Attempting to send: " + message);
udpSocket.SendTo(data, data.Length, SocketFlags.None, RemoteEndPoint);
}
/// <summary>
/// Send message to server using socket connection.
/// </summary>
private void SendNetworkMessage(string clientMessage)
{
if (socketConnection == null)
{
return;
}
try
{
// Get a stream object for writing.
NetworkStream stream = socketConnection.GetStream();
if (stream.CanWrite)
{
// Convert string message to byte array.
clientMessage = 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);
}
}
catch (SocketException socketException)
{
Debug.Log("Socket exception: " + socketException);
}
}
public void login(string username, string password)
{
SendNetworkMessage("0:" + username + ":" + password);
}
public void join(string roomname)
{
SendNetworkMessage("2:" + roomname);
}
public void leave()
{
SendNetworkMessage("2:-1");
}
public void sendTo(MessageType type, string message, bool reliable = true)
{
if (reliable)
{
SendNetworkMessage("3:" + (int)type + ":" + message);
}
else
{
SendUdpMessage(userid + ":3:" + (int)type + ":" + message);
}
}
public void sendToGroup(string group, string message, bool reliable = true)
{
if (reliable)
{
SendNetworkMessage("4:" + group + ":" + message);
}
else
{
SendUdpMessage(userid + ":4:" + group + ":" + message);
}
}
//changes the designated group that sendto(4) will go to
public void setupMessageGroup(string groupname, int[] userids)
{
SendNetworkMessage("5:" + groupname + ":" + String.Join(":", userids));
}
public void deleteNetworkObject(string networkId)
{
if (objects.ContainsKey(networkId))
{
NetworkObject obj = objects[networkId];
if (obj.isSceneObject)
{
deletedSceneObjects.Add(networkId);
}
Destroy(obj.gameObject);
objects.Remove(networkId);
}
}
}
}

View File

@ -0,0 +1,16 @@
using UnityEngine;
namespace VelNetUnity
{
/// <summary>
/// This is a base class for all objects that a player can instantiated/owned
/// </summary>
public abstract class NetworkObject : MonoBehaviour
{
public NetworkPlayer owner;
public string networkId; //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
public string prefabName; //this may be empty if it's not a prefab (scene object)
public bool isSceneObject = false;
public abstract void handleMessage(string identifier, byte[] message);
}
}

View File

@ -0,0 +1,219 @@
using System.Collections.Generic;
using UnityEngine;
using System;
namespace VelNetUnity
{
[RequireComponent(typeof(NetworkObject))]
public class NetworkPlayer : MonoBehaviour
{
public NetworkObject myObject;
public int userid;
public string username;
public string room;
public NetworkManager manager;
public bool isLocal = false;
public int lastObjectId = 0; //for instantiation
private bool isMaster = false;
private void Start()
{
myObject.owner = this;
manager = FindObjectOfType<NetworkManager>();
manager.onPlayerJoined += handlePlayerJoined;
}
public void handlePlayerJoined(NetworkPlayer player)
{
//if this is the local player, go through the objects that I own, and send instantiation messages for the ones that have prefab names
if (isLocal)
{
foreach (KeyValuePair<string, NetworkObject> kvp in manager.objects)
{
if (kvp.Value.owner == this && kvp.Value.prefabName != "")
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "7," + kvp.Value.networkId + "," + kvp.Value.prefabName);
}
}
if (isMaster)
{
//send a list of scene object ids when someone joins
sendSceneUpdate();
}
}
}
public void handleMessage(NetworkManager.Message m)
{
//these are generally things that come from the "owner" and should be enacted locally, where appropriate
//we need to parse the message
//types of messages
string[] messages = m.text.Split(';'); //messages are split by ;
for (int i = 0; i < messages.Length; i++)
{
//individual message parameters separated by comma
string[] sections = messages[i].Split(',');
switch (sections[0])
{
case "1": //update my object's data
{
string identifier = sections[1];
byte[] message = Convert.FromBase64String(sections[2]);
myObject.handleMessage(identifier, message);
break;
}
case "5": //sync update for an object I may own
{
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)
{
manager.objects[objectKey].handleMessage(identifier, messageBytes);
}
}
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
}
NetworkObject temp = manager.prefabs.Find((prefab) => prefab.name == prefabName);
if (temp != null)
{
NetworkObject instance = Instantiate<NetworkObject>(temp);
instance.networkId = networkId;
instance.prefabName = prefabName;
instance.owner = this;
manager.objects.Add(instance.networkId, instance);
}
break;
}
case "8": //I'm trying to destroy a gameobject I own
{
string networkId = sections[1];
manager.deleteNetworkObject(networkId);
break;
}
case "9": //deleted scene objects
{
for (int k = 1; k < sections.Length; k++)
{
manager.deleteNetworkObject(sections[k]);
}
break;
}
}
}
}
public void setAsMasterPlayer()
{
isMaster = true;
//if I'm master, I'm now responsible for updating all scene objects
//FindObjectsOfType<NetworkObject>();
}
public void sendGroupMessage(NetworkObject obj, string group, string identifier, byte[] data, bool reliable = true)
{
if (obj == myObject)
{
manager.sendToGroup(group, "1," + identifier + "," + Convert.ToBase64String(data), reliable);
}
else
{
manager.sendToGroup(group, "5," + obj.networkId + "," + identifier + "," + Convert.ToBase64String(data), reliable);
}
}
public void sendMessage(NetworkObject obj, string identifier, byte[] data, bool reliable = true)
{
if (obj == myObject)
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "1," + identifier + "," + Convert.ToBase64String(data), reliable);
}
else
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "5," + obj.networkId + "," + identifier + "," + Convert.ToBase64String(data), reliable);
}
}
public NetworkObject networkInstantiate(string prefabName)
{
if (!isLocal)
{
return null; //must be the local player to call instantiate
}
string networkId = userid + "-" + lastObjectId++;
NetworkObject temp = manager.prefabs.Find((prefab) => prefab.name == prefabName);
if (temp != null)
{
NetworkObject instance = Instantiate<NetworkObject>(temp);
instance.networkId = networkId;
instance.prefabName = prefabName;
instance.owner = this;
manager.objects.Add(instance.networkId, instance);
manager.sendTo(NetworkManager.MessageType.OTHERS, "7," + networkId + "," + prefabName); //only sent to others, as I already instantiated this. Nice that it happens immediately.
return instance;
}
return null;
}
public void networkDestroy(string networkId)
{
if (!manager.objects.ContainsKey(networkId) || manager.objects[networkId].owner != this || !isLocal) return; //must be the local owner of the object to destroy it
manager.sendTo(NetworkManager.MessageType.ALL_ORDERED, "8," + networkId); //send to all, which will make me delete as well
}
public void takeOwnership(string networkId)
{
if (!manager.objects.ContainsKey(networkId) || !isLocal) return; //must exist and be the the local player
manager.objects[networkId].owner = this; //immediately successful
manager.sendTo(NetworkManager.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.
}
public void sendSceneUpdate()
{
manager.sendTo(NetworkManager.MessageType.OTHERS, "9," + string.Join(",", manager.deletedSceneObjects));
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: feb473f59814842848dd4183ddeb6d9e
guid: cd6bc3db2c19417498a737fbb79e8c7d
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,80 @@
using System;
using System.Collections;
using UnityEngine;
namespace VelNetUnity
{
/// <summary>
/// A simple class that will sync the position and rotation of a network object
/// </summary>
public class SyncTransform : NetworkObject
{
public Vector3 targetPosition;
public Quaternion targetRotation;
public byte[] getSyncMessage()
{
float[] data = new float[7];
for (int i = 0; i < 3; i++)
{
data[i] = transform.position[i];
data[i + 3] = transform.rotation[i];
}
data[6] = transform.rotation[3];
byte[] toReturn = new byte[sizeof(float) * data.Length];
Buffer.BlockCopy(data, 0, toReturn, 0, toReturn.Length);
return toReturn;
}
public override void handleMessage(string identifier, byte[] message)
{
switch (identifier)
{
case "s":
float[] data = new float[7];
Buffer.BlockCopy(message, 0, data, 0, message.Length);
for (int i = 0; i < 3; i++)
{
targetPosition[i] = data[i];
targetRotation[i] = data[i + 3];
}
targetRotation[3] = data[6];
break;
}
}
// Start is called before the first frame update
void Start()
{
StartCoroutine(syncBehavior());
}
IEnumerator syncBehavior()
{
while (true)
{
if (owner != null && owner.isLocal)
{
owner.sendMessage(this, "s", getSyncMessage());
}
yield return new WaitForSeconds(.1f);
}
}
// Update is called once per frame
private void Update()
{
if (owner != null && !owner.isLocal)
{
transform.position = Vector3.Lerp(transform.position, targetPosition, .1f);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, .1f);
}
}
}
}

View File

@ -0,0 +1,83 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!244 &-1719467165466355418
AudioMixerEffectController:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_EffectID: 6362d3ceb713f4c3fa42448a3e29217e
m_EffectName: Dissonance Echo Cancellation
m_MixLevel: a240a2d1f057e4cf9bbc59f6cfbec367
m_Parameters: []
m_SendTarget: {fileID: 0}
m_EnableWetMix: 0
m_Bypass: 0
--- !u!241 &24100000
AudioMixerController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: VelAudioMixer
m_OutputGroup: {fileID: 0}
m_MasterGroup: {fileID: 24300002}
m_Snapshots:
- {fileID: 24500006}
m_StartSnapshot: {fileID: 24500006}
m_SuspendThreshold: -80
m_EnableSuspend: 1
m_UpdateMode: 1
m_ExposedParameters: []
m_AudioMixerGroupViews:
- guids:
- b8e40716c8c1442ab9ef14e149b2423c
name: View
m_CurrentViewIndex: 0
m_TargetSnapshot: {fileID: 24500006}
--- !u!243 &24300002
AudioMixerGroupController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Master
m_AudioMixer: {fileID: 24100000}
m_GroupID: b8e40716c8c1442ab9ef14e149b2423c
m_Children: []
m_Volume: 3d9590ea0314643bd94e1616b88508ef
m_Pitch: 31f1ddadf3a5d47caab0e64b81ced2ae
m_Send: 00000000000000000000000000000000
m_Effects:
- {fileID: 24400004}
- {fileID: -1719467165466355418}
m_UserColorIndex: 0
m_Mute: 0
m_Solo: 0
m_BypassEffects: 0
--- !u!244 &24400004
AudioMixerEffectController:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_EffectID: f79719d1f394d4b4783ede937d403602
m_EffectName: Attenuation
m_MixLevel: 8d97d9fd1e8d446eb9244b5dead78e90
m_Parameters: []
m_SendTarget: {fileID: 0}
m_EnableWetMix: 0
m_Bypass: 0
--- !u!245 &24500006
AudioMixerSnapshotController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Snapshot
m_AudioMixer: {fileID: 24100000}
m_SnapshotID: 86ea5b9fedb76448594a5ab2119e34a1
m_FloatValues: {}
m_TransitionOverrides: {}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fa1da19d935e241119cdd522ceae772c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 24100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5d2009d8e264649749c0315d48765749
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a7db5bc792cd471dbd8039664359eee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89e3af759df774692a566a166b4bf69b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,136 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6139051692386484099
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3076416102083120807}
- component: {fileID: 8527011532923434593}
- component: {fileID: 6854617867369839}
- component: {fileID: 5845716565458182149}
- component: {fileID: 5713671764962751473}
- component: {fileID: -4404668399269848200}
m_Layer: 0
m_Name: PlayerPrefab
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3076416102083120807
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6139051692386484099}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &8527011532923434593
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6139051692386484099}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &6854617867369839
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6139051692386484099}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!65 &5845716565458182149
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6139051692386484099}
m_Material: {fileID: 0}
m_IsTrigger: 0
m_Enabled: 1
serializedVersion: 2
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &5713671764962751473
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6139051692386484099}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d8d3b6de660834e3e898725928251405, type: 3}
m_Name:
m_EditorClassIdentifier:
myObject: {fileID: -4404668399269848200}
userid: 0
username:
room:
manager: {fileID: 0}
isLocal: 0
lastObjectId: 0
commsNetwork: {fileID: 0}
dissonanceID:
--- !u!114 &-4404668399269848200
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6139051692386484099}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 89e3af759df774692a566a166b4bf69b, type: 3}
m_Name:
m_EditorClassIdentifier:
owner: {fileID: 5713671764962751473}
networkId:
targetPosition: {x: 0, y: 0, z: 0}
targetRotation: {x: 0, y: 0, z: 0, w: 0}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d4158ab9c4a204cdbba28d3273fc1fb3
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,145 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &8565720275311462453
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8565720275311462455}
- component: {fileID: 8565720275311462452}
m_Layer: 0
m_Name: TestNetworkedGameObject
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8565720275311462455
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8565720275311462453}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children:
- {fileID: 8565720276181857624}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8565720275311462452
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8565720275311462453}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3f1f9b0bbd93a484a987c51f1107ebe5, type: 3}
m_Name:
m_EditorClassIdentifier:
owner: {fileID: 0}
networkId:
targetPosition: {x: 0, y: 0, z: 0}
targetRotation: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &8565720276181857625
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8565720276181857624}
- component: {fileID: 8565720276181857605}
- component: {fileID: 8565720276181857626}
- component: {fileID: 8565720276181857627}
m_Layer: 0
m_Name: Sphere
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8565720276181857624
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8565720276181857625}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 8565720275311462455}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &8565720276181857605
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8565720276181857625}
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &8565720276181857626
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8565720276181857625}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!135 &8565720276181857627
SphereCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8565720276181857625}
m_Material: {fileID: 0}
m_IsTrigger: 0
m_Enabled: 1
serializedVersion: 2
m_Radius: 0.5
m_Center: {x: 0, y: 0, z: 0}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6e4a023f70e01405e8b249a4488fe319
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e4e43899246c941c78acfc59ce2f664a
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
{
"name": "VelNetUnity",
"rootNamespace": "VelNetUnity",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1e55e2c4387020247a1ae212bbcbd381
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
{
"name": "edu.uga.engr.vel.velnetunity",
"displayName": "VelNetUnity",
"version": "1.0.0",
"unity": "2019.1",
"description": "A custom networking library for Unity.",
"keywords": [
"networking",
"self-hosted"
],
"author": {
"name": "Kyle Johnsen",
"email": "kjohnsen@uga.edu",
"url": "https://vel.engr.uga.edu"
},
"samples": [
{
"displayName": "Dissonance Integration",
"description": "Includes support for Dissonance Voice, available separately from the Unity Asset Store.",
"path": "Samples~/DissonanceIntegration"
},
{
"displayName": "Example",
"description": "Example Scene",
"path": "Samples~/Example"
}
],
"dependencies": {
}
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 525b0bfd38079f24f86b406a8da0b3c7
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,10 +1,10 @@
{
"dependencies": {
"com.unity.collab-proxy": "1.7.1",
"com.unity.collab-proxy": "1.15.7",
"com.unity.ide.rider": "2.0.7",
"com.unity.ide.visualstudio": "2.0.11",
"com.unity.ide.vscode": "1.2.3",
"com.unity.test-framework": "1.1.27",
"com.unity.ide.visualstudio": "2.0.12",
"com.unity.ide.vscode": "1.2.4",
"com.unity.test-framework": "1.1.30",
"com.unity.textmeshpro": "3.0.6",
"com.unity.timeline": "1.4.8",
"com.unity.ugui": "1.0.0",

View File

@ -1,11 +1,12 @@
{
"dependencies": {
"com.unity.collab-proxy": {
"version": "1.7.1",
"version": "1.15.7",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "2.0.0"
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.core": "1.0.1"
},
"url": "https://packages.unity.com"
},
@ -26,7 +27,7 @@
"url": "https://packages.unity.com"
},
"com.unity.ide.visualstudio": {
"version": "2.0.11",
"version": "2.0.12",
"depth": 0,
"source": "registry",
"dependencies": {
@ -35,7 +36,7 @@
"url": "https://packages.unity.com"
},
"com.unity.ide.vscode": {
"version": "1.2.3",
"version": "1.2.4",
"depth": 0,
"source": "registry",
"dependencies": {},
@ -48,8 +49,17 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.services.core": {
"version": "1.0.1",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.modules.unitywebrequest": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.test-framework": {
"version": "1.1.27",
"version": "1.1.30",
"depth": 0,
"source": "registry",
"dependencies": {
@ -89,6 +99,12 @@
"com.unity.modules.imgui": "1.0.0"
}
},
"edu.uga.engr.vel.velnetunity": {
"version": "file:VelNetUnity",
"depth": 0,
"source": "embedded",
"dependencies": {}
},
"com.unity.modules.ai": {
"version": "1.0.0",
"depth": 0,

View File

@ -0,0 +1,15 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &1
MonoBehaviour:
m_ObjectHideFlags: 61
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a287be6c49135cd4f9b2b8666c39d999, type: 3}
m_Name:
m_EditorClassIdentifier:
assetDefaultFramerate: 60

BIN
velnet_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB