Реализация TCP асинхронного подключения на игровом движке Unity 3D

Всем привет. Не могу правильно реализовать систему подключения. Задача следующая: Нужно создать асинхронное подключение, чтобы срабатывал метод только в том случае, когда был ответ от сервера NetworkStream.ReadAsync. этот метод приостанавливает выполнения вторичного потока и ожидает приход от сервера пакета(ов). Проблема в том, что Unity не многопоточная. И когда я открываю вторичный поток на чтение, мне даёт ошибку как раз о том, что я пытаюсь использовать API Unity не в главном его потоке. Думал реализовать собственный сигнализированный метод, с помощью: ManualResetEvent. Но понятия не имею, как проверить пришли ли данные или нет. Через IEnumerator не подходит. При задержке в 1 секунду. Происходят артефакты сети. К примеру пакет пришел, и он должен сразу же выполнится. А IEnumerator будет ещё ждать секунду. Как лучше поступить? Как с помощью Task.Factory.StartNew синхронизировать поток с главным потоком Unity? Для не кастыльного общения с API Unity


Ответы (1 шт):

Автор решения: aepot

Рабочий клиент-сервер я представляю примерно так.

Но у вас не одна проблема, для начала разберитесь с сетью. TCP - протокол с гарантированной доставной, но нет гарантии, что принимаемый пакет прилетит за раз. То есть пакет может быть фрагментирован. Решается это только передачей и проверкой длины пакета (реализовано в примере по ссылке выше).

Далее, чтобы передавать и принимать данные из рабочего потока, вам нужна реализация Producre/Consumer, то есть потокобезопасная коллекция, исполняющая роль буфера. В .NET Framework 4.x я знаю только одну такую - BlockingCollection, возможно в самой Unity есть и другие средства, я с движком не работал, знаю только сам .NET. И не используйте Task.Factory.StartNew, используйте Task.Run.

Есть еще например для приема данных неблокирующая ConcurrentQueue, в рабочем потоке вы например можете класть в нее принятые пакеты, а в каком-нибудь FixedUpdate() проверять, есть ли там что-нибудь, и если есть - забирать.

Но можно и без async/await обойтись, как говорится, старыми дедовским способами.

Отправка

using System.Collections.Concurrent;

class SendExample : MonoBehavior
{
    // byte[] просто для примера, здесь может быть любой тип.
    private readonly BlockingCollection<byte[]> _sendQueue = new BlockingCollection<byte[]>();

    void Start()
    {
        Task.Run(SendLoop);
    }

    private void SendLoop()
    {
        foreach (byte[] data in _sendQueue.GetConsumingEnumerable())
        {
            SendToServer(data); // вызов метода вашей сетевой части из рабочего потока
        }
    }

    private void StopSend() // этот метод надо вызвать перед остановкой работы скрипта
    {
        _sendQueue.Complete();
    }

    private void Send(byte[] data)
    {
        _sendQueue.Add(data);
    }
}

Получение

class ReceiveExample : MonoBehavior
{
    private readonly ConcurrentQueue<byte[]> _receiveQueue = new ConcurrentQueue<byte[]>();

    void FixedUpdate()
    {
        if (_receiveQueue.TryDequeue(out byte[] data))
        {
            // получены новые данные
        }
    }

    private void Receive(byte[] data) // этот метод надо вызвать из рабочего потока, получающего данные из сети
    {
        _receiveQueue.Enqueue(data);
    }
}

Это по сути самая простая Poducer/Consumer развязка между потоками. К примеру по ссылке в начале этого ответа ее должно быть не сложно прикрутить. И цель этой развязки - не допустить тормозорв игры, пока работает сеть.

→ Ссылка