Як не заморозити головну нитку в Unity?


33

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


1
Ви можете використовувати систему нарізки для запуску інших завдань у фоновому режимі ... але вони можуть нічого не викликати в API Unity, оскільки цей матеріал не є безпечним для потоків. Просто коментуючи, тому що я цього не зробив і не можу впевнено швидко навести вам приклад коду.
Алмо

Використання Listдій для зберігання функцій, які потрібно зателефонувати в основний потік. заблокуйте та скопіюйте Actionсписок у Updateфункції у тимчасовий список, очистіть оригінальний список, а потім виконайте Actionкод у цьому Listголовному потоці. Дивіться UnityThread з мого іншого повідомлення про те, як це зробити. Наприклад, викликати функцію на головній Темі, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
програміст

Відповіді:


48

Оновлення: У 2018 році Unity впроваджує систему робочих завдань C # як спосіб розвантажити роботу та використовувати декілька ядер CPU.

Відповідь нижче передує цій системі. Він все ще працюватиме, але можуть бути кращі варіанти, доступні в сучасному Unity, залежно від ваших потреб. Зокрема, система завдань, як видається, вирішує деякі обмеження щодо того, до чого можуть створюватися потоки, створені вручну, безпечно отримати доступ, описані нижче. Наприклад, розробники експериментують із звітом попереднього перегляду, виконуючи ретрансляції та будуючи сітки паралельно , наприклад.

Я б запропонував користувачам, які мають досвід використання цієї системи завдань, щоб додати власні відповіді, що відображають поточний стан двигуна.


У минулому я використовував нарізки для важких завдань в Unity (зазвичай це обробка зображень та геометрії), і це не відрізняється кардинально, ніж використання ниток в інших додатках C #, з двома застереженнями:

  1. Оскільки Unity використовує дещо старіший підмножину .NET, є деякі новіші функції потоків і бібліотеки, які ми не можемо використовувати поза коробкою, але основи все є.

  2. Як зазначає Альмо у коментарі вище, багато типів Unity не є безпечними для потоків, і вони викинуть винятки, якщо ви спробуєте сконструювати, використати або навіть порівняти їх із головної нитки. Що потрібно пам’ятати:

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

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

    • Випадкові та часові статичні члени не доступні. Створіть примірник System.Random за нитку, якщо вам потрібна випадковість, і System.Diagnostics.Stopwatch, якщо вам потрібна інформація про час.

    • Функції Mathf, вектор, матриця, кватерніон та кольори всі добре працюють в нитках, тому ви можете робити більшість своїх обчислень окремо

    • Створення 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є екземпляр Coroutine , щоб відновити його після 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
Mikael Högström

Гарна відповідь і спасибі. Мені цікаво, що якщо мені потрібно кілька разів зупинити і перезапустити нитку, я спробую перервати і почати, отримує мені помилку
flankechen

@flankechen Запуск і просівання нитки - відносно дорогі операції, тому часто ми вважаємо за краще тримати доступну нитку, але спокійну - використовуючи такі речі, як семафори чи монітори, щоб сигналізувати про те, коли у нас є нова робота. Хочете опублікувати нове запитання, яке детально описує ваш випадок використання, і люди можуть запропонувати ефективні способи його досягнення?
DMGregory

0

Ви можете помістити ваші важкі обчислення в інший потік, але API Unity не є безпечним для потоків, ви повинні виконати їх у головному потоці.

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



0

@DMGregory пояснив це дуже добре.

Можна використовувати як нитки різьблення, так і супроводи. Попереднє для завантаження основної нитки, а пізніше для повернення контролю до основної нитки. Натискання важкого підйому на окрему нитку здається більш розумним. Тому черги на роботу можуть бути такими, за якими ви хочете.

На Unity Wiki є справді хороший приклад сценарію JobQueue .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.