2
votes

We are making a unity game that uses commands received at a tcp socket to handle actions at a certain calibration state of the game. A statemanager handles events raised by a socketmanager when new strings are received. This statemanager then has to fire a method on a gameobject that has been referenced to in a field at the start.

The problem we are facing now is that this object cannot be accessed by the thread handling these events. We get the following error:

ToString can only be called from the main thread. Constructors and field initializers will be executed from the loading thread when loading a scene.

At the line marked with THIS LINE GIVES THE ERROR

How does Unity handle threading with EventHandlers and how do we access this object?

Thanks in advance!

public class StateManager : MonoBehaviour {

private bool initialized;
private GameObject calibrationController;


void Start(){
    InitializeEventHandler ();
}

void OnLevelWasLoaded(int level) {
    if (level == 3) {
        calibrationController = GameObject.FindGameObjectWithTag("CalibrationController");
        Debug.Log ("calibrationController 1: " + calibrationController);
        calibrationController.GetComponent<CalibrationController> ().NewState += NewCalibrationState;
        calibrationController.GetComponent<CalibrationController>().setupCalibration();
    }
}

private void InitializeEventHandler(){
    GetComponent<GameSocket> ().NewCommand += NewCommandReceived;
}

private void NewCommandReceived(object sender, NewCommandEventArgs e){
    HandleCommandReceived (e.Command);
}

private void NewCalibrationState(object sender, NewCalibrationStateEventArgs e)
{
    HandleNewCalibrationState (e.State);
}

private void HandleCommandReceived(string command){
    switch (command) {
    case "startcalibrationcomplete":
        Debug.Log ("startcalibrationcomplete");
        Debug.Log ("calibrationController 2: " + calibrationController); THIS LINE GIVES THE ERROR !!!
        Debug.Log(GameObject.FindGameObjectWithTag("CalibrationController"));
            break;
    default:
            Debug.Log ("state10");
        break;
    }
}

private void HandleNewCalibrationState(string state){
    switch (state) {
    case "startcalibration":
        GetComponent<GameSocket>().MySend("startcalibration");
        // ...
        break;
    case "animationdone":
        GetComponent<GameSocket>().MySend("animationdone");
        break;
    default:
        Debug.Log ("state10");
        break;
    }
}

For future readers: It is impossible to call methods on gameobjects from the event handlers due to Unity's poor threading. I found a workaround by letting the event handlers set properties of a data script on an empty gameobject. The data on this script can be accessed from whichever gameobject i.e. in an update cycle.

1
(1) Unity is not multithreaded and has nothing to do with multithreading. (2) for this reason, every Unity call ("ToString" and every single other Unity function) only works on the main thread. This is a basic fact about Unity (3) in the rare case where you need to start another thread (why?), it is thus essential that you go "back to" the main thread to tell your app about anything. (4) threaded programming is difficult; if you are threading, you will know how to "get back to" the main thread in Unity! Finally (5) you should be using UnityEvent (which is marvellous) - Fattie
I agree with you on everything except the UnityEvent. UnityEvent is very slow. Very very slow. I did a benchmark on it few years ago and also googled my result to verify wha t I got and it's not worth it. Unity knows about this and they will remove it or re-write it in Unity 6 according to one of their programmers. Delegates and events are much more faster. - Programmer
That "very very slow" argument is only valid for update events. If your event is called once in a while, UnityEvent is fine. It is actually safer as well since they are not real event, just a list of delegate iterated on request. The slow part comes from the check for the object being alive or not. - Everts

1 Answers

0
votes

Fact is Unity does not support Thread out of box. They changed their API few years ago to throw exception when they are called from another Thread.

When you raise an event from another thread, that event is called in that thread not in Unity/Main thread.So this means that when the event is called, you still can't use the Unity API's that has thread restriction on them from that event callback.

I did lots of experiments in with Threads in Unity and came to conclusion that below is how Thread may be integrated with Unity API.

To use Thread in Unity, you have to use lock, a boolean global variable that can be accessed from Unity Main Thread and the TCP Thread you created. You also need a way to call Unity API when you receive something from the network. Using lock and boolean variables should accomplish that. Then, you can call Unity API's from the Update function instead of the new created Thread.

Don't create the TCP instance in Main Thread then try to access it from another Thread. Create and access the TCP instance in another Thread. You will run into crashes/freezes or errors if you do this.

The code below should show you how you should raise events with TCP and Thread in Unity. The goal of the code below is raise event from the Main Thread (Update function) instead of the another Thread. You can add as many events as you need to this code.

public class TCPRECEIVER: MonoBehaviour
{

    readonly object locker = new object(); //For Locking variables
    bool continueReading = false;
    bool gotNewMessage = false;

    byte[] receivedBytes; //Stores bytes received from the server (will be accessed from Multiple Threads with lock)


    //Event to notify other functions when something is received
    public delegate void newMessageReceieved(byte[] bytesFromServer);
    public static event newMessageReceieved onNewMessageReceieved;


    public void Start()
    {
        receivedBytes = new byte[40];

    }


    void Update()
    {
        //Lock is expensive so make sure that we are still in reading mode before locking
        if (continueReading)
        {
            //lock variables
            lock (locker)
            {
                //Check if there is a new message
                if (gotNewMessage)
                {
                    gotNewMessage = false; //Set to false so that we don't run this again until we receive from server again

                    //Raise the event here if there are subscribers
                    if (onNewMessageReceieved != null)
                    {
                        onNewMessageReceieved(receivedBytes);
                    }
                }
            }
        }
    }

    //Start Reading from Server
    void startReading()
    {
        continueReading = true;

        //Start new Thread
        new System.Threading.Thread(() =>
        {
            //Create Client outside the loo
            System.Net.Sockets.TcpClient tcpClient = new System.Net.Sockets.TcpClient("192.168.1.1", 8090);
            System.Net.Sockets.NetworkStream tcpStream = tcpClient.GetStream();

            //Read Forever until stopReading is called
            while (continueReading)
            {
                byte[] bytesToRead = new byte[tcpClient.ReceiveBufferSize];
                int bytesRead = tcpStream.Read(bytesToRead, 0, tcpClient.ReceiveBufferSize);

                //Check if we received anything from server
                if (bytesRead > 0)
                {
                    //lock variables
                    lock (locker)
                    {
                        //Copy the recived data to the Global variable "receivedBytes"
                        System.Buffer.BlockCopy(bytesToRead, 0, receivedBytes, 0, bytesRead);

                        //Notify the Update function that we got something
                        gotNewMessage = true;
                    }
                }
                System.Threading.Thread.Sleep(1); //So that we don't lock up
            }
        }).Start();
    }

    //Stop reading
    void stopReading()
    {
        continueReading = false;
    }
}

Then you can subscribe and unsubscribe to the event/message from other classes with:

public void OnEnable()
{
    //Subscribe to the event
    TCPRECEIVER.onNewMessageReceieved += receivedBytesFromServer;
}

public void OnDisable()
{
    //Un-Subscribe to the event
    TCPRECEIVER.onNewMessageReceieved -= receivedBytesFromServer;
}


void receivedBytesFromServer(byte []bytesFromServer)
{
  //Do something with the bytes
}