1
\$\begingroup\$

I have GameManager class that has Log method:

using UnityEngine;
using UnityEngine.UI;

public class GameManager: MonoBehaviour {
    public static GameManager Instance;
    [SerializeField] private ScrollRect m_GUIConsoleContainer;
    [SerializeField] private UnityEngine.UI.Text m_GUIConsole;
    ...

    void Awake() {
        Instance = this;
    }

    ...

    public void Log(string message) {
        m_GUIConsole.text = m_GUIConsole.text + message + "\n";
    }
}

GameManager class has static instance, so I can invoke Log method in anywhere like this:

GameManager.Instance.Log("Helloworld");

However it works only first time. After invoke Log again, Unity gives me this error message:

get_isActiveAndEnabled can only be called from the main thread. Constructors and field initializers will be executed from the loading thread when loading a scene. Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function. UnityEngine.UI.Text:set_text(String)

I assigned reference of m_GUIConsoleContainer and m_GUIConsole in inspector, but I don't get it what this means and why it happens. Just using Debug.Log() or print() works without any error.

How should I avoid this error message? Using Unity 2018.2.1f1.

p.s. Error coming from here, another class called NetworkManager:

void OnReceivedBytes(IAsyncResult result) {
    try {
        if(m_Socket == null) {
            return;
        }

        int resultLength = m_Socket.EndReceive(result);

        if(resultLength == 0) {
            Shutdown();
            return;
        }

        Log("Received: " + m_RecvBuffer[0]);  <<- Error coming from here
        ...
    }
    ...
}

And this is where it call OnReceivedBytes:

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;

public class NetworkManager: MonoBehaviour {
    private Socket m_Socket;

    void Start() {
        m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        m_Socket.Connect("localhost", 1337);

        if(m_Socket.Connected) {
            Log("Connected.");

            m_Socket.BeginReceive(
                m_RecvBuffer,
                0,
                m_RecvBuffer.Length,
                SocketFlags.None,
                new System.AsyncCallback(OnReceivedBytes),
                null
            );
        }
        else {
            Log("Failed to Connect.");
        }
    }

    ...
}
\$\endgroup\$
  • \$\begingroup\$ Can you show us where you're calling GameManager.Instance.Log from? The error might be at the call site, rather than inside GameManager itself. \$\endgroup\$ – DMGregory Mar 3 at 13:34
  • \$\begingroup\$ @DMGregory Ok, I just added code. Note that I already mentioned in my question, it works when using Debug.Log instead. \$\endgroup\$ – modernator Mar 3 at 13:39
  • \$\begingroup\$ Interesting. Can you show us now where OnReceivedBytes is being called? It looks like something is leading to this being called on a thread other than the main thread, and setting text on UI Text object is supported only on the main thread. \$\endgroup\$ – DMGregory Mar 3 at 13:42
  • \$\begingroup\$ @DMGregory Sure, just added more code. Then seems like it caused from System.AsyncCallback maybe? \$\endgroup\$ – modernator Mar 3 at 13:48
  • 1
    \$\begingroup\$ As a side note, you could register for a callback from Application.logMessageReceivedThreaded, which will do the same thing except you would access it via Debug.Log and it would show all log messages, which may or may not be useful to you \$\endgroup\$ – Ed Marty Mar 3 at 14:39
1
\$\begingroup\$

get_isActiveAndEnabled can only be called from the main thread.

This tells us somehow we've tried to call the logging method from a thread that isn't the main thread. This could lead to nasty race conditions if the UI layout on the main thread is busy building the vertices for our text string, and another thread modifies the string out from under it, so this error catches the cross-thread access before it can lead to subtle and hard to reproduce bugs.

To fix this, we'll need a way for code on other threads to log messages without grabbing hold of the text object directly. For instance, we could have them write to a queue of messages that the GameManager can safely pump into the text field on its next update. To make sure we don't have contention on this queue, we could add our own mutex locking code, or we can use the standard ConcurrentQueue which handles thread-safety internally:

using System.Collections.Concurrent;

...

ConcurrentQueue<string> m_queuedLogs = new ConcurrentQueue<string>();

void ThreadsafeLog(string message) {
    m_queuedLogs.Enqueue(message);
}

void Update() {
    string message;
    while(m_queuedLogs.TryDequeue(out message)) {
        m_GUIConsole.text = m_GUIConsole.text + message + "\n";
    }
}

Now code that's executing on other threads - like in AsyncCallback methods - can log its messages via ThreadsafeLog and they'll be added to the GUI console's text next frame.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.