If you've ever had the pleasure of implementing a TCP client that would work in both the Unity editor (for development/debugging) and UWP on HoloLens (for production) you will know how painful the whole process is. Below you can find some info on how I tackled this problem. My code might not work for you as it's tailored for one specific purpose, nonetheless it might serve as a good starting point.
A bit of context on this (click here to jump straight to the code): I've been doing a lot of HoloLens development as a part of my internship with UCL Surgical Robot Vision lab. One of the pipelines I was developing involved processing IR tracking data from OptiTrack V120:Trio and sending rigid body positions/rotations to HoloLens, which is where the TCP client got involved. I'll post an article about the whole system some time in the future.
Disclaimer: This might not be the best solution, but it works. Before I came up with this I was googling for a while and could not find any useful info, so hopefully this article will at least point you in the right direction. If you want to suggest an improvement, use the comment section below.
Things to look out for
Unfortunately there isn't just a single thing that makes the whole process a pain, there is a whole lot of them. I summarise the obstacles I encountered and how I overcame them in the list below.
- Unity C# and UWP C# are different. There might be better ways to put it but this sentence summarises it pretty well. If you implement a TCP client the Unity way, UWP/HoloLens will complain that namespace
System.Net.Sockets
does not exist. If you implement a TCP client the UWP way, Unity editor with complain aboutWindows.Networking.Sockets
or something similar. Additionally, Unity uses some "stone age" C# version that doesn't supportasync
/await
keywords which makes it even more of a hassle.
- Solution: Wrap platform specific code in
#if UNITY_EDITOR
and#endif
or its equivalent. As you probably figured out from the name, this would tell the compiler to completely ignore the code between the two if that particular variable is not set. I was only deploying to UWP and Unity, so in my case undefinedUNITY_EDITOR
meant I'm on UWP. If you deploy to other platforms too, you might want to add other conditions too. - Alternative: You could also compile platform specific DLLs and place them in the plugins folder in your assets.
- Note on
async
/await
: These shouldn't cause you too much trouble as long as you don't write your cross-platform (i.e. code that is meant to work in both Unity and UWP without#if
's) usingawait
. If you look at theConnect()
in my code, you will see that I simply callConnectUWP()
as a normal function also it's technicallyasync
on UWP and yet the code compiles without any issues. It's important you understand thatasync
functions get called just like any other functions, but the program doesn't wait for the function to return before proceeding.
- UWP doesn't support threads (at the time of writing). If normally you'd just use the
Thread
class, in UWP you have to use theSystem.Threading.Tasks.Task
class to achieve the same functionality. To clarify, when implementing a TCP client in Unity you absolutely most definitely have to use a separate thread/task or your code will hang the whole app.Task
isn't that bad but I found it to be very poorly documented, and most importantly its interface is incompatible withThread
so you will have to rewrite your code.
- Solution: Extract the code for creating and stopping threads into a separate function which will handle platform-specific thread creation. In my code, you can see thread/task creation in
RestartExchange()
, while deleting a thread can be seen inStopExchange()
. Both of these use#if ...
and#endif
mentioned above.
- Most
GameObject
manipulations in Unity can only be performed by the main thread. I didn't have the time to find out why exactly that is but there is probably some resource-management reason, so we'll just have to deal with it. As your TCP client runs in a separate thread, whenever you try to make a change on some object Unity will call you out on not being the main thread. This happens in both the Unity editor and UWP, and I suspect everywhere else too.
- Solution: Instead of modifying the objects directly, use some shared variables to notify the main thread to make changes. In my code, you can see that the TCP client thread updates
lastPacket
string, which is then picked up by the main thread through theUpdate()
method. This way, TCP client thread receives the information but it's the main thread who actually applies the changes to game objects.
The code
Here's the code I used in my project. At the moment I provide it as-is, so it will not work out of the box. I might trim it to a more general solution in the future.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using UnityEngine;
#if !UNITY_EDITOR
using System.Threading.Tasks;
#endif
public class USTrackingTcpClient : MonoBehaviour
{
public USTrackingManager TrackingManager;
public USStatusTextManager StatusTextManager;
#if !UNITY_EDITOR
private bool _useUWP = true;
private Windows.Networking.Sockets.StreamSocket socket;
private Task exchangeTask;
#endif
#if UNITY_EDITOR
private bool _useUWP = false;
System.Net.Sockets.TcpClient client;
System.Net.Sockets.NetworkStream stream;
private Thread exchangeThread;
#endif
private Byte[] bytes = new Byte[256];
private StreamWriter writer;
private StreamReader reader;
public void Connect(string host, string port)
{
if (_useUWP)
{
ConnectUWP(host, port);
}
else
{
ConnectUnity(host, port);
}
}
#if UNITY_EDITOR
private void ConnectUWP(string host, string port)
#else
private async void ConnectUWP(string host, string port)
#endif
{
#if UNITY_EDITOR
errorStatus = "UWP TCP client used in Unity!";
#else
try
{
if (exchangeTask != null) StopExchange();
socket = new Windows.Networking.Sockets.StreamSocket();
Windows.Networking.HostName serverHost = new Windows.Networking.HostName(host);
await socket.ConnectAsync(serverHost, port);
Stream streamOut = socket.OutputStream.AsStreamForWrite();
writer = new StreamWriter(streamOut) { AutoFlush = true };
Stream streamIn = socket.InputStream.AsStreamForRead();
reader = new StreamReader(streamIn);
RestartExchange();
successStatus = "Connected!";
}
catch (Exception e)
{
errorStatus = e.ToString();
}
#endif
}
private void ConnectUnity(string host, string port)
{
#if !UNITY_EDITOR
errorStatus = "Unity TCP client used in UWP!";
#else
try
{
if (exchangeThread != null) StopExchange();
client = new System.Net.Sockets.TcpClient(host, Int32.Parse(port));
stream = client.GetStream();
reader = new StreamReader(stream);
writer = new StreamWriter(stream) { AutoFlush = true };
RestartExchange();
successStatus = "Connected!";
}
catch (Exception e)
{
errorStatus = e.ToString();
}
#endif
}
private bool exchanging = false;
private bool exchangeStopRequested = false;
private string lastPacket = null;
private string errorStatus = null;
private string warningStatus = null;
private string successStatus = null;
private string unknownStatus = null;
public void RestartExchange()
{
#if UNITY_EDITOR
if (exchangeThread != null) StopExchange();
exchangeStopRequested = false;
exchangeThread = new System.Threading.Thread(ExchangePackets);
exchangeThread.Start();
#else
if (exchangeTask != null) StopExchange();
exchangeStopRequested = false;
exchangeTask = Task.Run(() => ExchangePackets());
#endif
}
public void Update()
{
if(lastPacket != null)
{
ReportDataToTrackingManager(lastPacket);
}
if(errorStatus != null)
{
StatusTextManager.SetError(errorStatus);
errorStatus = null;
}
if (warningStatus != null)
{
StatusTextManager.SetWarning(warningStatus);
warningStatus = null;
}
if (successStatus != null)
{
StatusTextManager.SetSuccess(successStatus);
successStatus = null;
}
if (unknownStatus != null)
{
StatusTextManager.SetUnknown(unknownStatus);
unknownStatus = null;
}
}
public void ExchangePackets()
{
while (!exchangeStopRequested)
{
if (writer == null || reader == null) continue;
exchanging = true;
writer.Write("X\n");
Debug.Log("Sent data!");
string received = null;
#if UNITY_EDITOR
byte[] bytes = new byte[client.SendBufferSize];
int recv = 0;
while (true)
{
recv = stream.Read(bytes, 0, client.SendBufferSize);
received += Encoding.UTF8.GetString(bytes, 0, recv);
if (received.EndsWith("\n")) break;
}
#else
received = reader.ReadLine();
#endif
lastPacket = received;
Debug.Log("Read data: " + received);
exchanging = false;
}
}
private void ReportDataToTrackingManager(string data)
{
if (data == null)
{
Debug.Log("Received a frame but data was null");
return;
}
var parts = data.Split(';');
foreach(var part in parts)
{
ReportStringToTrackingManager(part);
}
}
private void ReportStringToTrackingManager(string rigidBodyString)
{
var parts = rigidBodyString.Split(':');
var positionData = parts[1].Split(',');
var rotationData = parts[2].Split(',');
int id = Int32.Parse(parts[0]);
float x = float.Parse(positionData[0]);
float y = float.Parse(positionData[1]);
float z = float.Parse(positionData[2]);
float qx = float.Parse(rotationData[0]);
float qy = float.Parse(rotationData[1]);
float qz = float.Parse(rotationData[2]);
float qw = float.Parse(rotationData[3]);
Vector3 position = new Vector3(x, y, z);
Quaternion rotation = new Quaternion(qx, qy, qz, qw);
TrackingManager.UpdateRigidBodyData(id, position, rotation);
}
public void StopExchange()
{
exchangeStopRequested = true;
#if UNITY_EDITOR
if (exchangeThread != null)
{
exchangeThread.Abort();
stream.Close();
client.Close();
writer.Close();
reader.Close();
stream = null;
exchangeThread = null;
}
#else
if (exchangeTask != null) {
exchangeTask.Wait();
socket.Dispose();
writer.Dispose();
reader.Dispose();
socket = null;
exchangeTask = null;
}
#endif
writer = null;
reader = null;
}
public void OnDestroy()
{
StopExchange();
}
}