Как они это сделали: миллионы плиток в Террарии

Я работаю над игровым движком, похожим на Terraria , в основном как вызов, и, хотя я многое выяснил, Кажется, я не могу обернуть голову тем, как они обрабатывают миллионы интерактивных /сборных плиток, которые игра имеет в свое время. Создавая около 500 000 плиток, то есть 1/20 из того, что возможно в Terraria , в моем движке скорость кадров снижается с 60 до примерно 20, даже если я все еще только рисую плитки в поле зрения. Имейте в виду, я ничего не делаю с плитами, сохраняя их только в памяти.

Обновить : добавлен код, чтобы показать, как я это делаю.

Это часть класса, который обрабатывает фрагменты и рисует их. Я предполагаю, что виновником является часть «foreach», которая выполняет итерацию всего, даже пустые индексы.

...
    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, который также может быть связан с обновлением, так как каждый Tile использует четыре вызова метода SpriteBatch.Draw. Это часть моей автонастройки, что означает рисование каждого угла в зависимости от соседних плит. texture_ * - Rectangles, устанавливаются один раз при создании уровня, а не каждое обновление.

...
    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);

            }
        }
    }
...
45 голосов | спросил William Mariager 5 PM00000090000003031 2011, 21:33:30

8 ответов


56

Вы просматриваете все 500 000 фрагментов при рендеринге? Если это так, это, вероятно, вызовет часть ваших проблем. Если вы просматриваете полмиллиона фрагментов при рендеринге и полмиллиона фрагментов при выполнении «обновлений» на них, то вы зацикливаете хотя бы миллион фрагментов каждого кадра.

Очевидно, есть способы обойти это. Вы можете выполнять свои тики обновления, а также рендеринг, тем самым сохраняя половину времени, затрачиваемого на все эти фрагменты. Но это связывает ваш код рендеринга и код обновления вместе с одной функцией и обычно представляет собой BAD IDEA .

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

Наконец, и, возможно, лучший вариант (в большинстве крупных мировых игр) - разделить ландшафт на регионы. Разделите мир на куски, скажем, на 512x512 плитки, и загрузите /разгрузите регионы, когда игрок приблизится или удаляется от региона. Это также избавляет вас от необходимости прокручивать далеко идущие фрагменты, чтобы выполнить какой-либо тик «обновление».

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

ответил thedaian 5 PM000000100000002731 2011, 22:03:27
4

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

Вы должны только перебирать то, что действительно необходимо для обработки. Поэтому подумайте о том, что вам действительно нужно для плитки. Чтобы рисовать 'm, вам нужна текстура, но вы не хотите перебирать фактическое изображение, так как они обрабатываются большими. Вы могли бы просто сделать int [,] или даже unsigned byte [,] (если вы не ожидаете более 255 текстур плитки). Все, что вам нужно сделать, это перебрать эти небольшие массивы и использовать оператор switch или if для рисования текстуры.

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

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

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

Если вы сохраняете указанную выше структуру, она занимает всего 3 байта на один фрагмент. Таким образом, для сохранения и использования памяти это идеальное решение. Для скорости обработки не имеет большого значения, если вы используете int или byte, или даже long int, если у вас 64-битная система.

ответил Madmenyo 27 J0000006Europe/Moscow 2013, 20:49:31
2

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

RLE: Итак, вы начинаете с координаты (x, y), а затем подсчитываете, сколько из одной плитки существует бок о бок (длина) вдоль одной из осей. Пример: (1,1,10,5) означало бы, что начиная с координаты 1,1 10 плиток бок о бок плитки типа 5.

Массивный массив (битмап): каждый элемент массива удерживается в типе плитки, который находится в этой области.

EDIT: Я просто наткнулся на этот замечательный вопрос: Случайная функция семени для генерации карты?

Генератор шума Perlin выглядит как хороший ответ.

ответил Sam Axe 5 PM00000090000002631 2011, 21:55:26
1

Вероятно, вы должны разделить tilemap, как уже было предложено. Например, Структура Quadtree , чтобы избавиться от любой потенциальной обработки (например, даже просто зацикливания) ненужные (не осевые) плитки. Таким образом, вы обрабатываете только то, что может потребоваться для обработки, и увеличение размера набора данных (карта плитки) не вызывает практического снижения производительности. Конечно, предполагая, что дерево хорошо сбалансировано.

Я не хочу звучать скучно или что-то еще, повторяя «старый», но при оптимизации всегда помню, чтобы использовать оптимизацию, поддерживаемую вашим toolchain /компилятором, вы должны поэкспериментировать с ними немного. И да, преждевременная оптимизация - это корень всего зла. Доверяйте своему компилятору, он знает лучше вас в большинстве случаев, но всегда всегда измеряет дважды и никогда не полагается на guesstimates. Речь идет не о быстрой реализации самого быстрого алгоритма, если вы не знаете, где находится узкое место. Вот почему вы должны использовать профилировщик для поиска самых медленных (горячих) путей кода и сосредоточиться на устранении (или оптимизации) их. Низкоуровневые знания целевой архитектуры часто необходимы для выдачи всего, что может предложить оборудование, поэтому изучите эти кэши процессора и узнайте, что такое предиктор отрасли. Посмотрите, что ваш профилировщик говорит вам о хитах /промах кеша /ветки. И, как показывает какая-то форма структуры данных дерева, лучше иметь интеллектуальные структуры данных и немые алгоритмы, чем наоборот. Прежде всего, когда дело доходит до производительности. :)

ответил zxcdw 6 AM00000010000003331 2011, 01:31:33
1

Разве это не слишком много призывов к призыву? Если вы поместите все свои текстуры текстур карты в одно изображение - атлас плитки, при рендеринге текстуры не будет. И если вы загрузите все свои плитки в одну сетку, она должна быть нарисована в одном обратном вызове.

О динамическом подключении ... Возможно, квадровое дерево не такая уж плохая идея. Предполагая, что плитки помещаются в листовые и нелистовые узлы, это всего лишь пакетные сетки из своих дочерних элементов, корень должен содержать все плитки, собранные в одну сетку. Для удаления одной плитки требуется обновление узлов (перестройка сетки) до корня. Но на каждом уровне дерева есть только 1/4-й из сетки, которые не были бы такими, что должно быть так много, 4 * tree_height mesh присоединяется?

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

Просто мои мысли, никакого кода, может быть, его глупости.

ответил arrival 5 MarpmMon, 05 Mar 2012 16:41:50 +04002012-03-05T16:41:50+04:0004 2012, 16:41:50
0

@прибытие - это правильно. Проблема заключается в коде рисования. Вы создаете массив команд 4 * 3000 + draw quad (24000+ draw polygon ) для каждого кадра. Затем эти команды обрабатываются и передаются на GPU. Это довольно плохо.

Есть несколько решений.

  • Отметьте большие блоки (например, размер экрана) фрагментов в статическую текстуру и нарисуйте их за один вызов.
  • Используйте шейдеры для рисования фрагментов без отправки геометрии на графический процессор каждого кадра (т. е. храните карту плитки на графическом процессоре).
ответил Ark-kun 11 MarpmMon, 11 Mar 2013 22:30:00 +04002013-03-11T22:30:00+04:0010 2013, 22:30:00
0

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

С точки зрения обработки таких вещей, как растения, растущие в районах, находящихся далеко от текущего экрана игроков, вы можете иметь, например, таймеры. Эти таймеры будут итерации через файлы, содержащие информацию о растениях, их позиции и т. Д. Вам просто нужно прочитать /обновить /сохранить файлы в таймерах. Когда игрок снова достигнет этих частей мира, двигатель будет читать файлы в обычном режиме и представить на экране новые данные о заводе.

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

ответил Jason Coombes 13 MarpmWed, 13 Mar 2013 17:22:04 +04002013-03-13T17:22:04+04:0005 2013, 17:22:04
-2

Я подумывал о том, как обращаться с этими большими количествами блоков, и единственное, что приходит мне в голову - это шаблон дизайна Flyweight Design. Если вы не знаете, я настоятельно рекомендую прочитать об этом: может помочь много, когда дело доходит до экономии памяти и обработки:

ответил Tiago Frossard 21 MaramWed, 21 Mar 2012 00:01:17 +04002012-03-21T00:01:17+04:0012 2012, 00:01:17

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132