Синхронізація між логічним потоком гри та потоком візуалізації


16

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

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

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

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


1
Я ненавиджу лише спам-посилання, але я вважаю, що це дуже добре читати, і він повинен відповісти на всі ваші запитання: altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


1
Ці посилання дають типовий кінцевий результат, який хотілося б, але не уточнюйте, як це зробити. Ви скопіювали весь графік сцени кожен кадр чи щось інше? Обговорення занадто високі та невиразні.
користувач782220

Я вважав, що посилання є досить явними щодо того, скільки штатів копіюється в кожному випадку. напр. (з 1-го посилання) "Пакет містить всю інформацію, необхідну для малювання кадру, але не містить іншого ігрового стану." або (з 2-го посилання) "Дані все ще потрібно обмінюватися, але тепер замість кожної системи, яка має доступ до загальної локації даних, щоб сказати, отримати дані про положення чи орієнтацію, кожна система має свою копію" (Див. особливо 3.2.2 - Держава Менеджер)
DMGregory

Хто б не писав, що стаття від Intel, схоже, не знає, що нарізка верху - дуже погана ідея. Ніхто не робить щось таке дурне. Раптом вся програма повинна зв’язуватися через спеціалізовані канали, і всюди є замки та / або величезні координовані обміни держав. Не кажучи вже про те, що немає інформації про те, коли надсилаються дані будуть оброблені, тому вкрай важко міркувати про те, що робить код. Набагато простіше скопіювати відповідні дані сцени (незмінні як покажчики з перерахунком, що змінюються - за значенням) в один момент і дозволити підсистемі розбирати її, як того хоче.
змія5

Відповіді:


1

Я працював над тим самим. Додаткове занепокоєння полягає в тому, що OpenGL (і, наскільки мені відомо, OpenAL), і ряд інших апаратних інтерфейсів, фактично є державними машинами, які не добре поєднуються з викликом кількох потоків. Я не думаю, що їх поведінка навіть визначена, і для LWJGL (можливо, також JOGL) вона часто є винятком.

Що я в кінцевому підсумку робив, це створити послідовність потоків, що реалізували певний інтерфейс, і завантажувати їх у стек об'єкта управління. Коли цей об'єкт отримав сигнал про вимикання гри, він пробіжить кожну нитку, викликає реалізований метод ceaseOperations () і чекає, коли вони закриються, перш ніж закрити себе. Універсальні дані, які можуть бути релевантними для відтворення звуку, графіки чи будь-яких інших даних, зберігаються в послідовності об'єктів, які є летючими або загальнодоступними для всіх потоків, але ніколи не зберігаються в пам'яті потоків. Там є незначне покарання за продуктивність, але при правильному використанні це дозволило мені гнучко призначати звук одному потоці, графіку іншому, фізику іншому тощо, не вкладаючи їх у традиційний (і страшний) "ігровий цикл".

Отже, як правило, всі виклики OpenGL проходять через графічний потік, всі OpenAL - через аудіопотоки, всі вхідні через вхідний потік, і все, про що потрібно турбувати організаційний потік управління, - це управління потоками. Стан гри проводиться в класі GameState, на який усі вони можуть поглянути, як потрібно. Якщо я коли-небудь вирішу, що, скажімо, JOAL отримав датування, і я хочу використовувати замість цього нову редакцію JavaSound, я просто реалізую інший потік для Audio.

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


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

1

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

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

Це залежить від платформи, яку ви використовуєте. Наприклад:

  • якщо ви робите це на більшості платформ, пов'язаних з відкритим GL, ( GLUT для C / C ++ , JOLG для Java , Android, пов’язаних з OpenGL ES ), вони зазвичай дають вам метод / функцію, яка періодично викликається потоком візуалізації, і яку ви може інтегруватися у ваш цикл гри (не роблячи ітерацій gameloop залежними від того, коли цей метод викликається). Для GLUT, що використовує C, ви робите щось подібне:

    glutDisplayFunc (myFunctionForGraphicsDrawing);

    glutIdleFunc (myFunctionForUpdatingState);

  • в JavaScript ви можете використовувати Web Workers, оскільки немає багатопотокової передачі (до якої можна дійти програмно) , ви також можете використовувати механізм "requestAnimationFrame", щоб отримувати сповіщення, коли буде заплановано нове графічне відображення, і відповідно оновити стан своєї гри .

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


0

У Java є ключове слово "синхронізоване", яке блокує змінні, які ви передаєте йому, щоб зробити їх безпечними. У C ++ ви можете домогтися того ж, використовуючи Mutex. Наприклад:

Java:

synchronized(a){
    //code using a
}

C ++:

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

Блокування змінних гарантує, що вони не змінюються під час запуску коду, що слідує за ним, тому змінні не змінюються вашим потоком оновлення під час їх надання (насправді вони дійсно змінюються, але з точки зору вашої нитки візуалізації вони не ' т). Але вам слід бути обережним із синхронізованим ключовим словом на Java, оскільки він лише гарантує, що вказівник на змінну / Об'єкт не змінюється. Атрибути все ще можуть змінюватися без зміни вказівника. Щоб розглянути це, ви можете скопіювати об'єкт самостійно або зателефонувати синхронізовано на всі атрибути об'єкта, які ви не хочете змінювати.


1
Mutexes - це не обов'язково відповідь тут, тому що ОП не тільки потребує роз'єднання ігрової логіки та візуалізації, але вони також хочуть уникнути будь-якого затримки здатності одного потоку рухатися вперед під час його обробки незалежно від того, де інший потік зараз може бути в його обробці. петля.
Нарос

0

Що я, як правило, бачив для обробки логіки / передавання потоку зв'язку, це потрійний буфер ваших даних. Таким чином, нитка візуалізації має відра 0, з якого вона читається. Логічний потік використовує відро 1 як джерело входу для наступного кадру і записує дані кадру у відро 2.

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

Але не обов'язково є причина, щоб розділити візуалізацію та логіку у відповідних потоках. Насправді ви можете зберегти послідовність ігрового циклу та від'єднати частоту кадрів візуалізації від логічного кроку, використовуючи інтерполяцію. Скористатися перевагами багатоядерних процесорів, що використовують такий тип налаштування, ви б мали пул потоків, який працює над групами завдань. Ці завдання можуть бути просто такими, як, наприклад, повторення списку об'єктів від 0 до 100, ви повторюєте список у 5 відрах з 20 по 5 потоків, що ефективно збільшує вашу ефективність, але не ускладнює основний цикл.


0

Це стара публікація, але вона все ще спливає, тому хотів додати тут свої 2 центи.

Дані першого списку, які слід зберігати в інтерфейсі / потоці відображення та логічному потоці. У потік інтерфейсу користувача можна включити 3D-сітку, текстури, інформацію про світло та копію даних про положення / обертання / напрямки.

У логічному потоці гри вам може знадобитися розмір ігрового об'єкта в 3d, обмежуючі примітиви (сфера, куб), спрощені 3d-сітчасті дані (наприклад, для детальних зіткнень), усі атрибути, що впливають на рух / поведінку, як швидкість об'єкта, відношення повороту тощо, а також дані про положення / обертання / напрямок.

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

Як ви це зробите, залежить від того, якою мовою ви працюєте. У Scala можна використовувати програмну пам'ять транзакцій, у Java / C ++ якусь фіксацію / синхронізацію. Мені подобаються незмінні дані, тому я прагну повертати новий незмінний об'єкт за кожне оновлення. Це небагато пам’яті, але з сучасними комп’ютерами це не так вже й багато. Але якщо ви хочете заблокувати спільні структури даних, ви можете це зробити. Перевірте клас обмінника на Java, використання двох або більше буферів може прискорити роботу.

Перш ніж потрапити в обмін даними між потоками, розробіть, скільки даних насправді потрібно передати. Якщо у вас є розділ octree, що розділяє ваш 3d-простір, і ви можете бачити 5 ігрових об'єктів із 10 об'єктів, навіть якщо вашій логіці потрібно оновити всі 10, щоб перемалювати лише ті 5, які ви бачите. Для більш детального ознайомлення ознайомтеся з цим блогом: http://gameprogrammingpatterns.com/game-loop.html Мова не йде про синхронізацію, але вона показує, як логіка гри відокремлена від відображення та які проблеми потрібно подолати (FPS). Сподіваюся, це допомагає,

Позначити

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