Як вони це зробили: мільйони плиток у Terraria


45

Я працював над ігровим двигуном, подібним до Terraria , здебільшого як виклик, і, хоча я зрозумів більшість із цього, я не можу насправді загорнути голову, як вони обробляють мільйони взаємодіючих / збираючих плиток гра має за один раз. Створюючи близько 500 000 плиток, тобто 1/20 того, що можливо в Terraria , в моєму двигуні частота кадрів падає з 60 до 20, навіть тому я все ще лише вигляд плитки. Зауважте, я нічого не роблю з плитками, лише зберігаю їх у пам’яті.

Оновлення : код додано, щоб показати, як я роблю речі.

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

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        foreach (Tile tile in this.Tiles)
        {
            if (tile != null)
            {
                if (tile.Position.X < -this.Offset.X + 32)
                    continue;
                if (tile.Position.X > -this.Offset.X + 1024 - 48)
                    continue;
                if (tile.Position.Y < -this.Offset.Y + 32)
                    continue;
                if (tile.Position.Y > -this.Offset.Y + 768 - 48)
                    continue;
                tile.Draw(spriteBatch, gameTime);
            }
        }
    }
...

Також тут є метод Tile.Draw, який також може робити з оновленням, оскільки кожна плитка використовує чотири виклики методу SpriteBatch.Draw. Це частина моєї системи автоматичного накладання, що означає малювати кожен кут залежно від сусідніх плиток. text_ * - це прямокутники, встановлюються один раз на рівні створення, а не кожного оновлення.

...
    public virtual void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        if (this.type == TileType.TileSet)
        {
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position, texture_tl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 0), texture_tr, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(0, 8), texture_bl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 8), texture_br, this.BlendColor);
        }
    }
...

Будь-яка критика чи пропозиції до мого коду вітаються.

Оновлення : рішення додано.

Ось остаточний метод Level.Draw. Метод Level.TileAt просто перевіряє введені значення, щоб уникнути виключень OutOfRange.

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        Int32 startx = (Int32)Math.Floor((-this.Offset.X - 32) / 16);
        Int32 endx = (Int32)Math.Ceiling((-this.Offset.X + 1024 + 32) / 16);
        Int32 starty = (Int32)Math.Floor((-this.Offset.Y - 32) / 16);
        Int32 endy = (Int32)Math.Ceiling((-this.Offset.Y + 768 + 32) / 16);

        for (Int32 x = startx; x < endx; x += 1)
        {
            for (Int32 y = starty; y < endy; y += 1)
            {
                Tile tile = this.TileAt(x, y);
                if (tile != null)
                    tile.Draw(spriteBatch, gameTime);

            }
        }
    }
...

2
Ви абсолютно впевнені, що ви рендеруєте лише те, що на думку камери, це означає код для визначення правильності відображення?
Купер

5
Частота падає від 60 до 20 кадрів в секунду, просто через виділену пам'ять? Це дуже малоймовірно, має бути щось не так. Яка платформа? Чи система замінює віртуальну пам'ять на диск?
Maik Semder

6
@Drackir У цьому випадку я б сказав, що це неправильно, якщо навіть є клас плиток, масив відповідних цілих чисел по довжині повинен робити, а коли є півмільйона об'єктів, OO накладні витрати - це не жарт. Я гадаю, що можна робити з предметами, але який би був сенс? Цілий масив мертвий, простий в обробці.
aaaaaaaaaaaa

3
Ой! Ітерація всіх плиток та виклик Намалюйте чотири рази на кожній з них. Тут напевно можливі деякі поліпшення ....
thedaian

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

Відповіді:


56

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

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

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

Нарешті, і, мабуть, найкращий варіант (більшість великих ігор світу роблять це) - розділити свою місцевість на регіони. Розділіть світ на шматки, скажімо, 512x512 плиток, і завантажте / вивантажте регіони, коли гравець наближається до регіону чи віддаляється від нього. Це також позбавляє вас від необхідності перебирати далеко за плитками, щоб виконувати будь-який галочку "оновлення".

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


5
Частина регіону здається цікавою, якщо я нагадаю, що це правильно, так це робить Minecraft, однак це не працює з тим, як розвивається місцевість у Terraria, навіть якщо ти ніде не знаходиться поруч, як розкидання трави, вирощування кактусів тощо. Редагувати : Якщо, звичайно, вони застосовують минулий час з моменту останнього активності регіону, а потім виконують усі зміни, які відбулися б у цей період. Це насправді може спрацювати.
Вільям Маріагер

2
Minecraft, безумовно, використовує регіони (і пізніші ігри Ultima, і я впевнений, що багато з Bethesda ігор). Я менш впевнений у тому, як Terraria обробляє місцевість, але вони, ймовірно, або застосовують минулий час до "нового" регіону, або зберігають всю карту в пам'яті. Останній вимагатиме високого використання оперативної пам’яті, але більшість сучасних комп’ютерів можуть це впоратися. Можуть бути й інші варіанти, які також не були згадані.
thedaian

2
@MindWorX: Здійснювати всі оновлення під час перезавантаження не завжди вдасться - припустимо, у вас є ціла купа безплідної ділянки, а потім трави. Ти йдеш на віки, а потім йдеш до трави. Коли блок з травою завантажується, він наздоганяє і поширюється в цьому блоці, але він не поширюватиметься на тісні блоки, які вже завантажені. Однак, такі речі просуваються набагато повільніше, ніж гра - перевіряйте лише невелику підмножину плиток у будь-якому одному циклі оновлень, і ви можете зробити віддалений місцевість живим, не завантажуючи систему.
Лорен Печтел

1
Як альтернатива (або доповнення) до «наздоганяючого», коли регіон наближається до програвача, ви можете замість цього зробити окрему ітерацію імітації над кожним віддаленим регіоном та оновлювати їх повільніше. Ви можете або зарезервувати час для цього кожного кадру, або мати його на окремому потоці. Так, наприклад, ви можете оновити область, в якій програвач перебуває, плюс 6 сусідніх регіонів у кожному кадрі, а також оновити 1 або 2 додаткові регіони.
Льюїс Уейкфорд

1
Для всіх, хто цікавиться, Terraria зберігає всі свої дані про плитку у двомірному масиві (максимальний розмір - 8400x2400). А потім використовує просту математику, щоб визначити, який розділ плитки відобразити.
FrenchyNZ

4

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

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

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

struct TileUpdate
{
public byte health;
public byte type;
public byte damage;
}

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

Якщо ви зберігаєте вищевказану структуру, вона займає лише 3 байти на плитку. Тому для економії та пам’яті це ідеально. Для швидкості обробки це не має значення, якщо ви використовуєте int або byte, або навіть long int, якщо у вас 64-бітова система.


1
Так, пізніші ітерації мого двигуна перетворилися на використання цього. Переважно для зменшення споживання пам'яті та збільшення швидкості. Типи ByRef повільні порівняно з масивом, наповненим структурами ByVal. Тим не менш, добре, що ви додали це до відповіді.
Вільям Маріагер

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

3
Вам не потрібно ні healthабо damage. Ви можете зберегти невеликий буфер останніх вибраних місць плитки та пошкодження на кожному. Якщо ви вибрали нову плитку і буфер заповнений, тоді виконайте найстаріше місце з неї перед додаванням нової. Це обмежує кількість плиток, які ви можете виконувати за один раз, але все одно є суттєвим обмеження для цього (приблизно #players * pick_tile_size). Ви можете зберігати цей список на гравця, якщо це полегшить. Розмір має значення для швидкості; менший розмір означає більше плиток у кожній кеш-пам'яті процесора.
Шон Міддлічч

@SeanMiddleditch Ви праві, але це було не в чому. Сенс полягав у тому, щоб зупинитися і подумати, що вам насправді потрібно, щоб намалювати плитку на екрані і зробити свої розрахунки. Оскільки в ОП використовувався цілий клас, я зробив простий приклад, простий проходить довгий шлях у навчальному прогресі. Тепер розблокуйте мою тему щодо щільності пікселів, будь ласка, ви, очевидно, програміст спробуйте подивитися на це питання оком художника CG. Оскільки цей сайт також призначений для CG для ігор afaik.
Мадменйо

2

Ви можете використовувати різні методи кодування.

ПРАВИЛА: Отже, ви починаєте з координати (x, y) і потім підраховуєте, скільки однакових плиток існує поруч (довжина) вздовж однієї з осей. Приклад: (1,1,10,5) означатиме, що починаючи з координати 1,1 є 10 плиток поруч із плиткою типу 5.

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

EDIT: Я щойно натрапив на це чудове запитання: Випадкова функція насіння для генерації карт?

Генератор шуму Perlin виглядає як хороша відповідь.


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

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

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

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

1

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

Я не хочу звучати нудно або нічого, повторюючи "старе", але при оптимізації завжди пам’ятайте, щоб використовувати оптимізації, підтримувані вашою ланцюжком інструментів / компілятором, вам слід трохи експериментувати з ними. І так, передчасна оптимізація - корінь усього зла. Довіряйте своєму компілятору, він знає краще за вас у більшості випадків, але завжди, завждивимірюйте двічі і ніколи не покладайтеся на здогадки. Справа не в тому, щоб швидко реалізувати найшвидший алгоритм, доки ви не знаєте, де саме вузьке місце. Ось чому вам слід скористатися профілером, щоб знайти найповільніші (гарячі) шляхи коду і зосередитись на їх усуненні (або оптимізації). Знання цільової архітектури низького рівня часто важливі для витіснення всього обладнання, яке може запропонувати, тому вивчіть ці кешові процесори та дізнайтеся, що таке прогноз гілки. Подивіться, що ваш профайлер розповідає про кеші / гілки гілок / пропусків. І як показує використання якоїсь форми структури даних дерева, краще мати інтелектуальні структури даних та німі алгоритми, ніж навпаки. Дані на перше місце, коли мова йде про продуктивність. :)


+1 за "передчасна оптимізація - корінь усього зла" Я винен у цьому :(
thedaian

16
-1 квадратик? Трохи переборщити? Коли в 2D грі всі плитки однакового розміру (якими вони є для Terraria), надзвичайно легко зрозуміти, який діапазон плиток на екрані - це лише верхній лівий верхній край (на екрані) до плитка внизу праворуч.
BlueRaja - Danny Pflughoeft

1

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

Про динамічне рекламування ... Можливо, квадрове дерево не така погана ідея. Якщо припустити, що плитки вкладаються в листові, а нелистові вузли - це просто забиті сітки з її дітей, корінь повинен містити всі плитки, зв'язані в одну сітку. Для видалення однієї плитки потрібні оновлення вузлів (ретрансляція сітки) до кореня. Але на кожному рівні дерева є лише одна четверта сітка, яка відбивається, що не повинно бути стільки, 4 * дерево_висота сітка приєднується?

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

Просто мої думки, ніякого коду, можливо його дурниці.


0

@arrival має рацію. Проблема полягає в малюванні коду. Ви будуєте масив з 4 * 3000 + малювати квадратичні команди (24000+ команд намалювати полігони ) на кадр. Потім ці команди обробляються і передаються в GPU. Це досить погано.

Є деякі рішення.

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

0

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

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

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


-2

Я думав над тим, як обробити стільки мільйонів блоків, і єдине, що мені приходить в голову, - це модель Flyweight Design. Якщо ви не знаєте, я настійно пропоную прочитати про це: може допомогти дуже багато, коли справа стосується збереження пам'яті та обробки: http://en.wikipedia.org/wiki/Flyweight_pattern


3
-1 Ця відповідь є занадто загальною, щоб бути корисною, і надане вами посилання не стосується безпосередньо цієї проблеми. Шаблон Flyweight - один з багатьох, багато способів ефективного управління великими наборами даних; Ви могли б розібратися, як би ви застосували цей шаблон у конкретному контексті зберігання плиток у грі, схожій на Terraria?
постгудизм
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.