Как не заморозить основной поток в Unity?

33

У меня есть алгоритм генерации уровня, который является тяжелым в вычислительном отношении. Таким образом, его вызов всегда приводит к зависанию экрана игры. Как я могу поместить функцию во второй поток, пока игра все еще продолжает отображать экран загрузки, чтобы показать, что игра не заморожена?

DarkDestry
источник
1
Вы можете использовать систему потоков для запуска других заданий в фоновом режиме ... но они могут не вызывать ничего в Unity API, так как этот материал не является потокобезопасным. Просто комментирую, потому что я этого не сделал и не могу с уверенностью дать вам пример кода быстро.
Almo
Использование Listдействий для хранения функций, которые вы хотите вызвать в главном потоке. заблокировать и скопировать Actionсписок в Updateфункции во временный список, очистить исходный список и выполнить Actionкод в Listосновном потоке. Смотрите UnityThread из моего другого поста о том, как это сделать. Например, чтобы вызвать функцию в главном потоке, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Программист

Ответы:

48

Обновление: в 2018 году Unity внедряет систему заданий C # как способ разгрузить работу и использовать несколько ядер ЦП.

Ответ ниже предшествует этой системе. Это все еще будет работать, но в современном Unity могут быть и другие варианты, в зависимости от ваших потребностей. В частности, система заданий, по-видимому, устраняет некоторые ограничения на то, что потоки, созданные вручную, могут безопасно получить доступ, описанные ниже. Например, разработчики экспериментируют с отчетом о предварительном просмотре, выполняя raycast и создавая сетки параллельно .

Я бы пригласил пользователей с опытом работы с этой системой заданий добавить свои собственные ответы, отражающие текущее состояние движка.


В прошлом я использовал многопоточность для тяжеловесных задач в Unity (обычно это обработка изображений и геометрии), и она не сильно отличается от использования потоков в других приложениях C # с двумя оговорками:

  1. Поскольку в Unity используется несколько более старое подмножество .NET, есть некоторые новые функции и библиотеки потоков, которые мы не можем использовать "из коробки", но здесь есть основы.

  2. Как отмечает Almo в комментарии выше, многие типы Unity не являются потокобезопасными и будут генерировать исключения, если вы попытаетесь сконструировать, использовать или даже сравнить их из основного потока. Что нужно иметь в виду:

    • Один из распространенных случаев - проверка на наличие нулевой ссылки GameObject или Monobehaviour, прежде чем пытаться получить доступ к его членам. myUnityObject == nullвызывает перегруженный оператор для всего, что происходит от UnityEngine.Object, но System.Object.ReferenceEquals()работает в некоторой степени вокруг этого - просто помните, что редактируемый GameObject Destroy () сравнивается как равный null, используя перегрузку, но еще не ReferenceEqual для null.

    • Чтение параметров из типов Unity обычно безопасно в другом потоке (в том смысле, что оно не вызовет немедленного исключения, если вы будете тщательно проверять наличие нулевых значений, как указано выше), но обратите внимание на предупреждение Филиппа о том, что основной поток может изменять состояние пока ты читаешь это. Вы должны будете быть дисциплинированными в отношении того, кому разрешено изменять что и когда, чтобы избежать чтения какого-либо противоречивого состояния, которое может привести к ошибкам, которые могут быть чертовски трудно отследить, поскольку они зависят от временных интервалов между нитями, которые мы можем Воспроизведение по желанию.

    • Случайные и статические члены Time недоступны. Создайте экземпляр System.Random для каждого потока, если вам нужна случайность, и System.Diagnostics.Stopwatch, если вам нужна информация о времени.

    • Все функции Mathf, Vector, Matrix, Quaternion и Color хорошо работают между потоками, поэтому вы можете выполнять большинство вычислений отдельно.

    • Создание GameObjects, присоединение Monobehaviours или создание / обновление текстур, сеток, материалов и т. Д. - все это должно происходить в главном потоке. В прошлом, когда мне нужно было работать с ними, я настраивал очередь производителя-потребителя, где мой рабочий поток подготавливает необработанные данные (например, большой массив векторов / цветов для применения к сетке или текстуре), и обновление или сопрограмма в главном потоке опрашивает данные и применяет их.

С этими заметками, вот шаблон, который я часто использую для многопоточной работы. Я не гарантирую, что это стиль наилучшей практики, но он выполняет свою работу. (Комментарии или изменения для улучшения приветствуются - я знаю, что многопоточность - очень глубокая тема, из которой я знаю только основы)

using UnityEngine;
using System.Threading; 

public class MyThreadedBehaviour : MonoBehaviour
{

    bool _threadRunning;
    Thread _thread;

    void Start()
    {
        // Begin our heavy work on a new thread.
        _thread = new Thread(ThreadedWork);
        _thread.Start();
    }


    void ThreadedWork()
    {
        _threadRunning = true;
        bool workDone = false;

        // This pattern lets us interrupt the work at a safe point if neeeded.
        while(_threadRunning && !workDone)
        {
            // Do Work...
        }
        _threadRunning = false;
    }

    void OnDisable()
    {
        // If the thread is still running, we should shut it down,
        // otherwise it can prevent the game from exiting correctly.
        if(_threadRunning)
        {
            // This forces the while loop in the ThreadedWork function to abort.
            _threadRunning = false;

            // This waits until the thread exits,
            // ensuring any cleanup we do after this is safe. 
            _thread.Join();
        }

        // Thread is guaranteed no longer running. Do other cleanup tasks.
    }
}

Если вам не нужно строго распределять работу между потоками для ускорения, и вы просто ищете способ сделать его неблокирующим, чтобы остальная часть игры продолжала работать, более легким решением в Unity является Coroutines . Это функции, которые могут выполнять некоторую работу, а затем возвращать управление движку для продолжения работы и возобновления работы в более позднее время.

using UnityEngine;
using System.Collections;

public class MyYieldingBehaviour : MonoBehaviour
{ 

    void Start()
    {
        // Begin our heavy work in a coroutine.
        StartCoroutine(YieldingWork());
    }    

    IEnumerator YieldingWork()
    {
        bool workDone = false;

        while(!workDone)
        {
            // Let the engine run for a frame.
            yield return null;

            // Do Work...
        }
    }
}

Для этого не нужны особые соображения по очистке, поскольку движок (насколько я могу судить) избавляет вас от сопрограмм от разрушенных объектов.

Все локальное состояние метода сохраняется при его выходе и возобновлении, поэтому для многих целей он работает так, как будто он работает непрерывно в другом потоке (но у вас есть все удобства работы в основном потоке). Вам просто нужно убедиться, что каждая его итерация достаточно коротка, чтобы не замедлять основной поток.

Убедившись, что важные операции не разделены выходом, вы можете получить согласованность однопоточного поведения - зная, что никакой другой скрипт или система в основном потоке не может изменить данные, над которыми вы работаете.

Линия возврата урожая дает вам несколько вариантов. Вы можете...

  • yield return null возобновить после обновления следующего кадра ()
  • yield return new WaitForFixedUpdate() возобновить после следующего FixedUpdate ()
  • yield return new WaitForSeconds(delay) возобновить игру по истечении определенного количества игрового времени
  • yield return new WaitForEndOfFrame() возобновить после того, как GUI закончит рендеринг
  • yield return myRequestгде myRequestэто WWW экземпляр, чтобы возобновить когда запрошенные данные после загрузки из Интернета или диска.
  • yield return otherCoroutineгде otherCoroutineэто экземпляр сопрограммная , чтобы возобновить после otherCoroutineЗавершает. Это часто используется в форме, yield return StartCoroutine(OtherCoroutineMethod())чтобы связать выполнение с новой сопрограммой, которая сама может дать результат, когда захочет.

    • Экспериментально, пропуская вторую StartCoroutineи простую запись, можно yield return OtherCoroutineMethod()достичь одной и той же цели, если вы хотите связать выполнение в одном контексте.

      Обтекание внутри StartCoroutineможет по-прежнему быть полезным, если вы хотите запустить вложенную сопрограмму вместе со вторым объектом, напримерyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... в зависимости от того, когда вы хотите, чтобы сопрограмма сделала следующий ход.

Или yield break;остановить сопрограмму до того, как она достигнет конца, как вы могли бы использовать return;для раннего выхода из обычного метода.

Д.М.Григорий
источник
Есть ли способ использовать сопрограммы для получения только после того, как итерация занимает более 20 мс?
DarkDestry
@DarkDestry Вы можете использовать экземпляр секундомера, чтобы определить время, которое вы тратите во внутреннем цикле, и перейти к внешнему циклу, где вы уступаете, перед сбросом секундомера и возобновлением внутреннего цикла.
DMGregory
1
Хороший ответ! Я просто хочу добавить, что еще одна причина для использования сопрограмм заключается в том, что если вам когда-либо понадобится собрать webgl, он будет работать так, как если бы вы использовали потоки. Сидя с этой головной болью в огромном проекте прямо сейчас: P
Микаэль Хогстрем
Хороший ответ и спасибо. Просто интересно, что, если мне нужно будет несколько раз остановить и перезапустить поток, я пытаюсь прервать и запустить, и
получаю
@flankechen Запуск и завершение потока - относительно дорогие операции, поэтому часто мы предпочитаем держать поток доступным, но бездействующим - используя такие вещи, как семафоры или мониторы, чтобы сигнализировать, когда у нас есть новая работа для этого. Хотите опубликовать новый вопрос, подробно описывающий ваш вариант использования, и люди могут предложить эффективные способы его достижения?
DMGregory
0

Вы можете поместить свои тяжелые вычисления в другой поток, но API Unity не является потокобезопасным, их нужно выполнять в основном потоке.

Хорошо, вы можете попробовать этот пакет в Asset Store, который поможет вам легче использовать потоки. http://u3d.as/wQg Вы можете просто использовать только одну строку кода для запуска потока и безопасного выполнения Unity API.

Ци Хаоянь
источник
0

@DMGregory объяснил это очень хорошо.

Можно использовать как потоки, так и сопрограммы. Сначала разгрузить основной поток, а затем вернуть управление главному потоку. Толкание тяжестей, чтобы отделить нить, кажется более разумным. Поэтому очереди заданий могут соответствовать вашим ожиданиям.

В Unity Wiki есть действительно хороший пример скрипта JobQueue .

IndieForger
источник