Грязный флаг (Dirty Flag)

Задача

Избегать ненужной работы, откладывая ее до тех пор, пока не потребуется результат.

Набор первичных данных изменяется со временем. Набор вторичных данных вычисляется на основе первичных с помощью ресурсоемкого процесса«Грязный» флаг отслеживает рассинхронизацию вторичных данных с первичными. Он устанавливается, когда первичные данные изменяются. Если флаг установлен, когда нам понадобились вторичные данные, они вычисляются и флаг снимается. В противном случае используются уже вычисленные вторичные данные.

Мотивация

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

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

Механизм того, как хранится трансформация выходит за рамки нашего рассмотрения. Короче говоря, это матрица 4х4. Вы можете создать трансформацию, которая будет объединять две других — например перенос и потом поворот объекта с помощью перемножения двух матриц.

Как и почему это работает я оставляю вам на самостоятельное изучение.

Когда рендер отрисовывает объект, он берет модель объекта, применяет трансформацию и затем рендерит его в мире. Если у нас будет просто мешок с объектами, а не граф сцены, то все так просто работать и будет.

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

Представьте себе для примера игру про пиратский корабль в море. Наверху мачты находится воронье гнездо. В этом гнезде сидит пират. На плече у пирата —  попугай. Локальная трансформация корабля определяет его положение в море. Позиция гнезда задается относительно корабля и т.д.

A pirate ship containing a crow's nest with a pirate in it with a parrot on his shoulder.

Образчик программистского искусства!

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

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

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

Локальные и мировые трансформации

Расчет мировой трансформации объекта довольно незамысловат: вам просто нужно обойти цепочку его родительских объектов, начиная с корня и заканчивая самим объектом, и объединить все их трансформации. Другими словами мировая трансформация попугая будет следующей:

The parrot's world position comes from multiplying the local positions for the ship, nest, pirate, and parrot.

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

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

Проще всего вычислять трансформации на лету во время рендеринга. На каждом кадре мы рекурсивно обходим граф сцену начиная с вершины иерархии. Для каждого объекта мы вычисляем мировую трансформацию и отрисовываем его.

Но при этом тратится куча драгоценного процессорного времени! Ведь многие объекты в мире не двигаются на каждом кадре. Подумайте о статической геометрии, образующей уровень. Пересчитывать ее мировые трансформации на каждом кадре иначе как пустой тратой ресурсов и не назовешь.

Кэширование мировых трансформаций

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

А вот когда объект движется, проще всего будет сразу обновить его мировую трансформацию. Только не забудьте об иерархии. Когда объект движется, нам нужно будет пересчитывать рекурсивно трансформацию всех его дочерних объектов.

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

Any time an object moves, the world coordinates are recalculated eagerly and redundantly.

По строкам со звездочкой (★) вы можете заметить, что мы пересчитываем мировую трансформацию попугая четыре раза, хотя результат нам нужен всего один.

Мы изменили всего четыре объекта, но выполнили десять пересчетов мировых трансформаций. Результаты шести бесполезных пересчетов придется выкинуть, потому что наш рендер их использовать не будет. Мы пересчитали мировую трансформацию попугая четыре раза, а рендерим его всего один раз.

Проблема здесь в том что мировая трансфоромация зависит от нескольких локальных трансформаций. Так как мы делаем пересчет всех, как только изменится хотя бы одна из них, у нас получилось, что мы пересчитываем одну и ту же трансформацию столько раз за кадр, сколько локальных трансформаций от которых она зависит изменяется.

Отложенный пересчет

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

Интересно, в какое количество архитектур ПО умышленно добавлено небольшое проскальзывание.

Чтобы этого добиться, мы добавим флаг в каждый объект графа. «Флаг» и «бит» в программировании почти синонимы: они оба значат наименьшую порцию данных, могущую быть всего в двух состояниях. Эти состояния можно называть «правда» и «ложь» или «установлен», «снят», «очищен». Я буду использовать их во взаимозаменяемом смысле.

Когда локальная трансформация будет изменяться, мы его установим. И когда нам понадобится мировая трансформация объекта, мы его будем проверять. Если флаг установлен, мы рассчитываем мировую трансформацию и очищаем флаг. Флаг отвечает на вопрос «Мировая трансформация устарела?» По неустановленной причине традиционно эта «устарелость» называется «грязью». Отсюда: грязный флаг. «Грязный бит» (Dirty bit) — это еще одно название данного шаблона, но я лично привык к менее похотливо звучащему названию.

Редакторы википедии не разделяют мое мнение и используют название грязный бит.

Если мы применим этот шаблон и переместим объекты из предыдущего примера, у нас получится вот что:

By deferring until all moves are done, we only recalculate once.

На что мы можем надеяться в лучшем случае, так это на то, что мировая трансформация каждого затрагиваемого объекта вычисляется только один раз. С помощью всего одного бита, шаблон добился следующего:

  • Он разбивает изменения на множество локальных трансформаций по цепочке наследования, по одному вычислению на объект.
  • Мы избавились от вычислений для объектов, которые не двигались.
  • И еще маленький бонус: если объект был удален перед рендерингом, мировая трансформация для него вычисляться не будет.

Шаблон

Набор первичных данных изменяется со временем. Набор вторичных данных вычисляется на основе первичных с помощью ресурсоемкого процесса«Грязный» флаг отслеживает рассинхронизацию вторичных данных с первичными. Он устанавливается, когда первичные данные изменяются. Если флаг установлен, когда нам понадобились вторичные данные, они вычисляются и флаг снимается. В противном случае используются уже вычисленные вторичные данные.

Когда использовать

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

Грязный флаг применяется к двум типам работ: вычислениям и синхронизации. В обеих случаях процесс перехода от первичных данных к вторичным требует либо времени, либо дополнительных затрат.

В нашем примере с графом сцены, процесс замедляется из-за количества математических вычислений. С другой стороны когда этот шаблон используется для синхронизации, чаще всего вторичные данные находятся где-то еще — либо на диске, либо в сети — и даже простая передача данных из A в B будет затратной.

Есть и еще несколько требований:

  • Первичные данные должны изменяться чаще, чем используются вторичные данные. Этот шаблон избегает обработки вторичных данных, если последующее изменение первичных данных может сделать их неактуальными до того, как они будут использованы. Если вы обнаружите, что вам требуется пересчет вторичных данных после каждого изменения первичных, этот шаблон вам не поможет.
  • Их сложно обновлять постепенно. Скажем наш пиратский корабль может перевозить определенный вес. Поэтому нам нужен общий вес всего, что в него нагружено. Мы можем использовать этот шаблон и грязный флаг для общего веса. Каждый раз, когда мы будем загружать или выгружать добычу, мы будем устанавливать флаг. А когда нам потребуется общий вес, мы будем его вычислять и снимать флаг.Но, возможно, проще будет просто поддерживать текущий общий вес. Когда мы добавляем или удаляем вещь, мы добавляем или вычитаем ее вес из общего веса. Если мы можем «платить на ходу» таким образом и поддерживать вторичные данные в актуальном состоянии, такой вариант может работать лучше чем наш шаблон и пересчитывать вторичные данные целиком не потребуется.

Звучит так, как будто грязный флаг мало где нужен, но иногда он все таки полезен. Если поискать в любой кодовой базе слово «dirty», наверняка отыщется несколько мест, где используется этот шаблон.

По моим наблюдениям, таким образом можно найти и множество комментариев с извинениями за «грязные» хаки.

Имейте в виду

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

Бывает что стоимость вычислений слишком высока, чтобы ее откладывать

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

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

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

Например, текстовый редактор знает о том, что в вашем документе есть «несохраненные данные». Эта маленькая пулька или звездочка в заголовке программы и является наглядной визуализацией грязного флага. Первичные данные — это открытый в памяти документ, а вторичные данные — это файл на диске.

A window titlebar showing the little icon representing unsaved changes.

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

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

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

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

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

Вам нужно быть уверенным, что вы устанавливаете грязный флаг при каждом изменении состояния

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

Фил Карлтон когда-то сказал: «Есть только две сложные вещи в компьютерных науках: актуальность кэша и придумывание имен».

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

Одним из способов этого добиться является инкапсуляция изменений первичных данных за каким-либо интерфейсом. Если все что может изменить состояние будет проходить через специальный API, вы можете устанавливать грязный флаг в этом интерфейсе и быть уверенным что вы ничего не упустите.

Вам нужно хранить предыдущие вторичные данные в памяти

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

Если вы используете шаблон для синхронизации первичного состояния с каким-либо другим местом — это не слишком большая проблема. В этом случае вторичные данные обычно вообще не находятся в памяти.

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

Как и другие методы оптимизации, этот шаблон жертвует памятью ради скорости. В обмен на необходимость хранить ранее рассчитанные данные, вам не нужно будет пересчитывать их, если они не менялись. Такие расходы имеют смысл, когда вычисления медленные, а памятью можно пренебречь. Если у вас больше свободного времени, чем памяти, будет лучше рассчитывать их по мере необходимости.

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

Пример кода

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

class Transform
{
public:
  static Transform origin();

  Transform combine(Transform& other);
};

Единственная нужная нам операция здесь — это combine(), с помощью которой вы можете получить мировую трансформацию, которая объединяет локальную трансформацию и всю цепочку предков. Также, здесь есть метод для получения «начальной» трансформации — обычно единичной матрицы, вообще не содержащей переноса, поворота и масштабирования.

Теперь напишем набросок класса для объекта графа сцены. Это самый минимум того, что нам понадобится до применения шаблона.

class GraphNode
{
public:
  GraphNode(Mesh* mesh) : mesh_(mesh), local_(Transform::origin())
  {}

private:
  Transform local_;
  Mesh* mesh_;

  GraphNode* children_[MAX_CHILDREN];
  int numChildren_;
};

Каждый узел имеет локальную трансформацию, описывающую его позицию относительно родителя. Он содержит сетку, представляющую графическую часть объекта (мы позволим mesh_ принимать значение NULL для обработки невидимых узлов, используемых для группировки своих потомков.) И, наконец, каждый узел имеет коллекцию дочерних узлов, которая может быть и пустой.

Таким образом «граф сцены» — это всего лишь единственный корневой GraphNode, чьи дети (и внуки и т.д.) представляют все объекты в мире:

GraphNode* graph_ = new GraphNode(NULL);
// Добавление дочернего узла к корневому узлу графа...

Для того, чтобы отрендерить граф сцены, всё, что нам нужно, так это обойти всё дерево узлов, начиная с корневого, и вызвать следующую функцию для сетки каждого узла вместе с правильной мировой трансформацией:

void renderMesh(Mesh* mesh, Transform transform);

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

Неоптимизированный обход

Чтобы запачкать руки, давайте напишем простейший обход графа сцены для рендеринга, подсчитывающий мировые координаты на лету. Он не будет оптимальным, но по крайней мере будет простым. Добавим в GraphNode новый метод:

void GraphNode::render(Transform parentWorld)
{
  Transform world = local_.combine(parentWorld);

  if (mesh_) 
    renderMesh(mesh_, world);

  for (int i = 0; i < numChildren_; i++) {
    children_[i]->render(world);
  }
}

Мы передаем сюда с помощью parentWorld трансформацию родителя узла. Таким образом, все, что нам остается для вычисления правильной мировой трансформации данного узла — это объединить ее с локальной трансформацией. Нам не нужно выполнять обход вверх по цепочке родителей для подсчета мировых трансформаций, потому что они уже будут вычислены по мере того, как мы спускаемся вниз по цепочке.

Мы вычисляем мировую трансформацию узла и сохраняем ее в world, а затем рендерим сетку, если она есть. И, наконец, мы рекурсивно обходим дочерние узлы, передавая внутрь мировую трансформацию данного узла. В конце концов, это простой рекурсивный метод.

Для отрисовки всего графа сцены, мы запускаем процесс начиная с корневого узла:

graph_->render(Transform::origin());

Давайте запачкаемся

Итак, данный код работает правильно — рендерит все сетки в нужных местах — но не слишком эффективно. Он вызывает local_.combine(parentWorld) для каждого узла в графе на каждом кадре. Давайте посмотрим, как с этим сможет справиться шаблон. Для начала нам нужно добавить в GraphNode два поля:

class GraphNode
{
public:
  GraphNode(Mesh* mesh) : mesh_(mesh),
    local_(Transform::origin()),
    dirty_(true)
  {}

  // Другие методы...

private:
  Transform world_;
  bool dirty_;
  // Другие поля...
};

Поле world_ кэширует ранее вычисленную мировую трансформацию, а dirty_ — это естественно грязный флаг. Обратите внимание, что вначале он равен true. Когда мы создаем новый узел, у нас еще нет рассчитанной мировой трансформации и поэтому она еще не синхронизирована с локальной трансформацией.

Единственная причина, почему нам вообще нужен этот шаблон — это способность объектов двигаться, так что добавим соответствующую поддержку:

void GraphNode::setTransform(Transform local)
{
  local_ = local;
  dirty_ = true;
}

Главное здесь то, что здесь снова устанавливается грязный флаг. Мы ничего не забыли? Ах да! Дочерние узлы!

Когда родительский узел двигается, мировые координаты всех дочерних становятся недействительными. Но здесь мы не будем устанавливать их грязный флаг. Мы могли бы это сделать, но такая рекурсия будет слишком медленной. Вместо этого мы поступим умнее, когда доберемся до рендера. Итак:

void GraphNode::render(Transform parentWorld, bool dirty)
{
  dirty |= dirty_;
  if (dirty) {
    world_ = local_.combine(parentWorld);
    dirty_ = false;
  }

  if (mesh_) 
    renderMesh(mesh_, world_);

  for (int i = 0; i < numChildren_; i++) {
    children_[i]->render(world_, dirty);
  }
}

Здесь используется допущение, что если проверка if работает быстрее, чем умножение матриц. Интуитивно вам может таки показаться. Ведь логично, что проверка одного бита быстрее, чем куча операций с вещественными числами.

Однако современные процессоры очень сложные штуки. Они нацелены на конвейерную обработку (pipelining) — очереди из серии последовательных инструкций. Ветвление типа нашего if может привести к ошибке предсказаний перехода (branch misprediction) и заставить процессор потерять циклы на повторное заполнение конвейера.

Глава Локальность данных (Data Locality) рассказывает о том, как вы можете помочь современным процессорам работать быстрее и не попадать в такие неприятные ситуации.

Очень похоже на оригинальную наивную реализацию. Основное отличие заключается в том, что мы проверяем грязность узла перед расчетом мировой трансформации и сохраняем результат в поле, а не в локальной переменной. Когда узел становится чистым, мы полностью пропускаем combine() и используем старое, но корректное значение world_.

Хитрость заключается в параметре dirty. Он будет равен true, если любой узел выше этого узла в цепочке иерархии был грязным. Примерно также, как parentWorld инкрементно обновляет мировую трансформацию во время спуска по иерархии, dirty отслеживает грязность дочерней цепочки.

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

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

Обратите внимание, что этот трюк работает только благодаря тому что render() — это единственная вещь в GraphNode, которой нужно обновлять мировую трансформацию. Если бы эта информация была бы нужна кому-то еще, нам пришлось бы выдумывать что-то другое.

Архитектурные решения

Этот шаблон довольно специфичен, так что настраивать здесь можно всего несколько вещей.

Когда очищается грязный флаг?

  • Когда нам потребуется результат:
    • Если результат нам не понадобится, вычисления вообще не будут выполняться. Для первичных данных, которые изменяются значительно чаще, чем выполняется доступ к вторичным, это даст заметный выигрыш.
    • Если вычисления требуют много времени, это может привести к возникновению заметной паузы. Откладывание работы на потом до тех пор, пока игрок должен будет увидеть результат, может испортить впечатление от игры. Обычно, если работа не занимает слишком много времени — это незаметно, но в противном случае ее нужно выполнять все-таки раньше.
  • В жестко определенных контрольных точках:Иногда, в работе игры возникают моменты, где процесс выполнения отложенных вычислений смотрится органично. Например, мы можем захотеть выполнять сохранение, когда наш пират заходит в порт. Или же точка синхронизации вообще может не быть частью игровой механики. Мы можем захотеть скрыть работу за загрузочным экраном или катсценой.
    • Выполнение работы не влияет на впечатление игрока от игрового процесса. В отличие от предыдущего варианта, у вас обычно есть чем отвлечь игрока, пока выполняется нужная вам обработка.
    • Когда возникает необходимость выполнить работу вы теряете управление. Это своего рода противоположность предыдущего пункта. Вы можете на микро-уровне управлять тем, когда выполнится работа и вы можете быть уверены что игра ее корректно выполнит.А вот в чем вы не можете быть уверены — так это в том, что игрок действительно доберется до контрольной точки или выполнит определенный вами критерий. Если он заблудится или игра поведет себя некорректно, работа отложится на более долгое время чем вы рассчитывали.
  • В фоне:Обычно, при первом изменении, запускается таймер и затем обрабатываются все изменения, произошедшие между следующим тиком таймера и предыдущим состоянием.В терминах человеко-компьютерного взаимодействия специальная задержка между получением программой пользовательских данных и их обработкой называется гистеризисом.
    • Вы можете настраивать частоту выполняемой работы. Настраивая интервал таймера, вы можете задавать нужную вам частоту или нерегулярность.
    • Вы можете выполнять больше лишней работы. Если за время работы таймера первичное состояние изменилось незначительно, вы можете столкнуться с тем, что будете обрабатывать большой объем почти неизменившихся данных.
    • Вам нужна поддержка для выполнения работы асинхронно. Обработка данных «в фоне» подразумевает, что игрок в то же время может продолжать делать, что ему хочется. Это значит, что вам придется воспользоваться потоками или другим методом поддержки конкурентной работы, чтобы игра могла работать с вашими данными прямо во время своей работы.Так как игрок может взаимодействовать с тем же первичным состоянием, которое вы обрабатываете, вам нужно позаботиться о том, чтобы сделать его более безопасным в плане конкурентного изменения.

Насколько детализированным будет ваше применение грязных флагов?

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

  • Если оно сильно детализированное:Предположим, что у вас есть грязный флаг для каждой отдельной доски на каждой палубе.
    • Вы будете обрабатывать только изменившиеся данные. Вы будете отсылать на сервер только ту часть состояния корабля, которая изменилась.
  • Менее детализированное:В качестве альтернативного решения вы можете ассоциировать отдельный грязный флаг с каждой палубой. Изменение чего либо на палубе делает грязной всю палубу.Я хотел вставить сюда какую-нибудь шутку про швабру и мойку, но воздержусь.
    • Вы будете вынуждены обрабатывать неизменившиеся данные. Добавьте на палубу всего одну бочку и придется отправлять на сервер данные о содержимом всей палубы.
    • На хранение грязных флагов понадобится меньше памяти. Добавьте на палубу десять бочек и вам понадобится всего один бит, чтобы отслеживать их всех.
    • На постоянные накладные расходы получится тратить меньше времени./ Когда мы обрабатываем некоторые измененные данные, для самой обработки этих данных чаще всего нужно выполнять какую-либо определенную работу. В нашем примере это метаданные, которые нужны для того, чтобы определить, где на корабле были изменены данные. Чем больше обрабатываемый кусок данных, тем меньше этих метаданных и тем меньше накладные расходы на их обработку.

Смотрите также

  • Этот шаблон очень часто можно встретить не в играх, а в браузерных фреймворках, типа Angular. Они используют грязные флаги для отслеживания того, какие данные были изменены в браузере и должны быть отправлены на сервер.
  • Физические движки следят как за движущимися объектами, так и за покоящимися. Так, как покоящееся тело не двигается до тех пор, пока к нему не будет приложен импульс, их не нужно обрабатывать, пока их кто-нибудь не тронет. Этот флаг «двигается ли» представляет из себя грязный флаг, обозначающий к каким объектам приложена сила, и то, что для них нужно применять физические вычисления.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *