Frage

I am trying to add LAN server discovery to my Unity game. I am using UDP to broadcast requests. If anyone on the network is a server, they will respond with some server data. I have this part figured out, but I need to use some Unity functions(which include other custom monobehaviour scripts) to generate the packet data.

Unity will not allow access to its APIs from any thread other than the main one.

I am using the UdpClient.BeginReceive, UdpClient.EndReceive, AsyncCallback flow. So the receive AsyncCallback function(AsyncRequestReceiveData in the example script below) is run in another thread which doesn't allow me to use any Unity functions.

I used this answer by flamy as a basis for my script.

I have tried making a delegate and event to fire in AsyncRequestReceiveData when the script hears a request, but it seems to fire in the same separate thread.

An event fired in the main thread by the separate thread would work great, but I am not sure on how this could be done.


Although the script inherits from MonoBehaviour (base object in Unity 3D) it should run just fine standalone.

The output of the script should look like:

Sendering Request: requestingServers

Request Received: requestingServers

Here is the barebones script:

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

public class TestUDP : MonoBehaviour {


    public delegate void RequestReceivedEventHandler(string message);
    public event RequestReceivedEventHandler OnRequestReceived;

    // Use this to trigger the event
    protected virtual void ThisRequestReceived(string message)
    {
        RequestReceivedEventHandler handler = OnRequestReceived;
        if(handler != null)
        {
            handler(message);
        }
    }


    public int requestPort = 55795;

    UdpClient udpRequestSender;
    UdpClient udpRequestReceiver;


    // Use this for initialization
    void Start () {

        // Set up the sender for requests
        this.udpRequestSender = new UdpClient();
        IPEndPoint requestGroupEP = new IPEndPoint(IPAddress.Broadcast, this.requestPort);
        this.udpRequestSender.Connect(requestGroupEP);


        // Set up the receiver for the requests
        // Listen for anyone looking for us
        this.udpRequestReceiver = new UdpClient(this.requestPort);
        this.udpRequestReceiver.BeginReceive(new AsyncCallback(AsyncRequestReceiveData), null);

        // Listen for the request
        this.OnRequestReceived += (message) => {
            Debug.Log("Request Received: " + message);
            // Do some more stuff when we get a request
            // Use `Network.maxConnections` for example
        };

        // Send out the request
        this.SendRequest();
    }

    void Update () {

    }


    void SendRequest()
    {
        try
        {
            string message = "requestingServers";

            if (message != "") 
            {
                Debug.Log("Sendering Request: " + message);
                this.udpRequestSender.Send(System.Text.Encoding.ASCII.GetBytes(message), message.Length);
            }
        }
        catch (ObjectDisposedException e)
        {
            Debug.LogWarning("Trying to send data on already disposed UdpCleint: " + e);
            return;
        }
    }


    void AsyncRequestReceiveData(IAsyncResult result)
    {
        IPEndPoint receiveIPGroup = new IPEndPoint(IPAddress.Any, this.requestPort);
        byte[] received;
        if (this.udpRequestReceiver != null) {
            received = this.udpRequestReceiver.EndReceive(result, ref receiveIPGroup);
        } else {
            return;
        }
        this.udpRequestReceiver.BeginReceive (new AsyncCallback(AsyncRequestReceiveData), null);
        string receivedString = System.Text.Encoding.ASCII.GetString(received);

        // Fire the event
        this.ThisRequestReceived(receivedString);

    }
}

I have seen this question (Cleanest Way to Invoke Cross-Thread Events) but I can't seem to figure out how to it works and how to incorporate in.

War es hilfreich?

Lösung

The solution works by keeping track of a list of tasks(a queue) that need to be executed in the main thread, and then run through that queue in the Update() via HandleTasks(); executing and removing each one from the queue. You can add to the task queue by calling QueueOnMainThread() and passing an anonymous function.

I got some inspiration from this answer by Pragmateek

You can have a separate script to manage main thread tasks or just incorporate it into your already existing script where you need to run something in the main thread.

Here is the full source, fixed solution to my original snippet in the question.

Below is just the necessary bits to get the queuing up and running:

// We use this to keep tasks needed to run in the main thread
private static readonly Queue<Action> tasks = new Queue<Action>();


void Update () {
    this.HandleTasks();
}

void HandleTasks() {
    while (tasks.Count > 0)
    {
        Action task = null;

        lock (tasks)
        {
            if (tasks.Count > 0)
            {
                task = tasks.Dequeue();
            }
        }

        task();
    }
}

public void QueueOnMainThread(Action task)
{
    lock (tasks)
    {
        tasks.Enqueue(task);
    }
}

To Queue something up on the main thread just pass it a lambda expression:

this.QueueOnMainThread(() => {
    // We are now in the main thread...
    Debug.Log("maxConnections: " + Network.maxConnections);
});

If you are using WinForms, this article: Avoiding InvokeRequired has some great coding patterns.

Andere Tipps

Does this code sample work for you? X can be called from any thread.

//place in class where main thread object is
void X(int sampleVariable)
{
    if (this.InvokeRequired)
    {
        this.Invoke((MethodInvoker) (() => X(sampleVariable)));
        return;
    }

    // write main thread code here
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top