Чи зберігання всіх ігрових об’єктів в одному списку є прийнятним дизайном?


24

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

Мені було цікаво, чи це правильний шлях для речей, чи швидший більш ефективний шлях?


4
Зауважую, ви сказали "список масивів", припускаючи, що це .Net, замість цього вам слід оновити до List <T>. Це майже ідентично, за винятком того, що список <T> є сильним типом, і тому вам не доведеться вводити команду щоразу, коли ви отримуєте доступ до елемента. Ось документ: msdn.microsoft.com/en-us/library/6sh2ey19.aspx
Джон Макдональд

Відповіді:


26

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

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

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

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


16

Як казали інші, якщо це досить швидко, то це досить швидко. Якщо вам цікаво, чи є кращий спосіб, питання, яке слід задати собі, - це

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

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

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

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


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

8

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

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

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

Для початку я зазначу, що оновлення ваших ігрових об’єктів та їх надання може мати різні вимоги в деяких іграх, і тому потрібно обробляти їх окремо. Деякі можливі проблеми з оновленням та відображенням ігрових об’єктів:

Оновлення ігрових об’єктів

Ось уривок з Game Engine Architecture, який я рекомендую прочитати:

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

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

введіть тут опис зображення

Відображення ігрових об’єктів

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

введіть тут опис зображення

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


4

Відповідь «так, це добре». Коли я працював над великими моделями заліза, де продуктивність не підлягала переговорам, я був здивований, побачивши все в одному великому глобальному масиві (BIG та FAT). Пізніше я працював над системою ОО і отримав порівняння. Хоча це було дуже некрасиво, версія "один масив, щоб керувати ними всі", в однопотоковому простому старому C, зібраному в налагодженні, була в кілька разів швидшою, ніж "симпатична" багатопотокова система OO, складена -O2 в C ++. Я думаю, що це мало пов'язане з відмінною локалізацією кешу (і в просторі, і в часі) у версії масиву з великим жиром.

Якщо ви хочете швидкодіяти, один великий жировий глобальний масив C буде верхньою межею (з досвіду).


2

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

Мені завжди потрібен був один головний список, незалежно від того, скільки я мав інших списків. Зазвичай я роблю з них якусь карту, таблицю з ключами або словник, щоб я міг випадково отримувати доступ до окремих елементів. Але простий список набагато простіший; якщо ви не випадково отримуєте доступ до ігрових об’єктів, вам не потрібна карта. І випадковий послідовний пошук через кілька тисяч записів, щоб знайти потрібний, не зайнять багато часу. Тому я б сказав, що б ви не робили, плануєте дотримуватися головного списку. Подумайте про використання карти, якщо вона стає великою. (Хоча якщо ви можете використовувати індекси замість ключів, ви можете дотримуватися масивів і мати щось набагато краще, ніж Map. Я завжди стикався з великими прогалинами між номерами індексу, тому мені довелося його відмовити.)

Слово про масиви: вони неймовірно швидкі. Але коли вони набирають близько 100 000 елементів, вони починають надавати колекціонерам сміття. Якщо ви можете виділити простір один раз і не торкатися його згодом, добре. Але якщо ви постійно розширюєте масив, ви постійно виділяєте та звільняєте великі шматки пам’яті і прагнете, щоб ваша гра зависла секунди за один раз і навіть виходила з ладу з помилкою пам’яті.

Так швидше і ефективніше? Звичайно. Карта для випадкового доступу. Для дійсно великих списків, пов'язаний список буде бити масив, якщо і коли вам не потрібен випадковий доступ. Кілька карт / списків для організації об'єктів різними способами, як вам потрібно. Ви можете багатопотоково оновити оновлення.

Але це все багато роботи. Цей єдиний масив швидкий і простий. Ви можете додавати інші списки та речі лише за потреби, і вони, ймовірно, не знадобляться. Просто пам’ятайте про те, що може піти не так, якщо ваша гра стане великою. Можливо, ви хочете мати тестову версію з 10 разів більшою кількістю об'єктів, ніж у реальній версії, щоб дати вам уявлення про те, коли вас чекають проблеми. (Тільки робити це , якщо гра є отримувати більшу.) (І не намагайтеся багатопоточність , не даючи їй багато думав. Все інше легко почати з , і ви можете зробити , як багато або як мало , як вам подобається і відступайте в будь-який час. Завдяки багаторядковій оплаті ви заплатите велику ціну, щоб потрапити, плата за перебування там висока, і важко повернутися.)

Іншими словами, просто продовжуйте робити те, що ви робите. Найбільше обмеження - це, мабуть, ваш власний час, і ви максимально використовуєте це.


Я дійсно не думаю, що пов'язаний список буде перевершувати масив будь-якого розміру, якщо ви часто не додаєте або видаляєте речі з середини.
Зан Лінкс

@ZanLynx: І навіть тоді. Я думаю, що нові оптимізації виконання багато допомагають масивам - не те, що їм це потрібно. Але якщо виділити та звільнити великі шматки пам’яті неодноразово та швидко, як це трапляється з великими масивами, ви можете зв’язати смітник у вузли. (Загальний об'єм пам'яті також є фактором тут. Проблема полягає у функції розміру блоків, частоти вільного та розподілення, а також наявної пам'яті.) Збій виникає раптово, коли програма зависає або виходить з ладу. Зазвичай є багато вільної пам’яті; GC просто не може його використовувати. Пов'язані списки уникають цієї проблеми.
РальфЧапін

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

Так, але навіть у масиві ви не просто зберігаєте вказівники на об’єкти? (Якщо тільки це недовговічний масив для системи частинок чи щось таке.) Якщо так, то в кеш-програмах все одно буде багато.
Тодд Леман

1

Я використовував дещо подібний метод для простих ігор. Зберігання всього в одному масиві дуже корисно, коли виконуються такі умови:

  1. У вас є невелика кількість або відомий максимум ігрових об'єктів
  2. Ви віддаєте перевагу простоті щодо продуктивності (тобто: оптимізованіші структури даних, такі як дерева, не варті проблем)

У будь-якому випадку я реалізував це рішення як масив фіксованого розміру, який містить a gameObject_ptr структуру. Ця структура містила вказівник на сам ігровий об’єкт і булеве значення, що представляє, чи є об'єкт "живим".

Метою булевого типу є прискорити операції створення та видалення. Замість того, щоб руйнувати ігрові об’єкти, коли їх видаляють із симуляції, я просто встановив би «правий» прапор false.

Під час виконання обчислень на об'єктах код проходить цикл через масив, виконуючи обчислення на об'єктах, позначених як "живі", і надаються лише об'єкти, позначені "живими".

Інша проста оптимізація - це організація масиву за типом об'єкта. Усі кулі, наприклад, могли жити в останній російській клітинах масиву. Потім, зберігаючи int зі значенням індексу або вказівника на елемент масиву, на певні типи можна посилатися із зниженою вартістю.

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

Це моє перше повідомлення! Я сподіваюся, що він відповість на ваше запитання!


перший пост! так!
Тілі

0

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


2
Правда, що стосується більшості ігор XNA, але організація ваших об'єктів може насправді зробити ітерацію через них швидше: об’єкти одного типу швидше потрапляють у кеш інструкцій та даних вашого процесора. Пропуски кешу дорогі, особливо на консолях. Наприклад, на Xbox 360, помилка кешу L2 обійдеться вам у 600 циклів. Це залишає багато результатів на столі. Це одне місце, де керований код має перевагу перед власним кодом: об’єкти, виділені близько в часі, також будуть близькими до пам'яті, а ситуація покращується зі збиранням сміття (ущільнення).
Хронічний ігровий програміст

0

Ви кажете один масив та об'єкти.

Тож (без коду для перегляду) я здогадуюсь, що всі вони пов'язані з 'uberGameObject'.

Тож, можливо, дві проблеми.

1) У вас може бути об'єкт "бог" - чи є тонни речей, не дуже сегментовані тим, що він робить чи є.

2) Ви повторюєте цей список і кастинг набираєте? або розпаковувати кожен. Це велика ефективність покарання.

Подумайте про розбиття різних типів (кулі, недоброзичливці тощо) у власних сильно набраних списках.

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.

Не потрібно виконувати кастинг типів, якщо є якийсь загальний базовий клас або інтерфейс, який має всі методи, які будуть викликані в циклі оновлення (наприклад, update()).
bummzack

0

Я можу запропонувати певний компроміс між загальним єдиним списком та окремими списками для кожного типу об’єктів.

Зберігайте посилання на один об’єкт у кількох списках. Ці списки визначаються базовою поведінкою. Наприклад, MarineSoldierоб'єкт буде посилатися на PhysicalObjList, GraphicalObjList, UserControlObjList, HumanObjList. Отже, коли ви обробляєте фізику, ви повторюєте PhysicalObjList, коли процес пошкоджується від отрути - HumanObjListякщо солдат помирає - виймаєте її, HumanObjListале тримайте в ній PhysicalObjList. Щоб цей механізм працював, вам потрібно організувати автоматичне вставлення / видалення об'єкта при його створенні / видаленні.

Переваги підходу:

  • типова організація об'єктів (ні AllObjectsListз кастингом на оновлення)
  • списки засновані на логіці, а не на об'єктних класах
  • не виникає проблем з об'єктами з комбінованими здібностями ( MegaAirHumanUnderwaterObjectбуде вставлено у відповідні списки замість створення нового списку для його типу)

PS: Що стосується самостійного оновлення, ви зіткнетеся з проблемою, коли взаємодіють кілька об'єктів, і кожен з них залежить від інших. Щоб вирішити це, нам потрібно впливати на об'єкт зовні, а не всередині самооновлення.

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