Game Programming Patterns / Паттерны программирования в играх: 2.1. Double Buffer

← →
Английский оригинал Перевод на русский

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

Цель

kabanov 30.04.14 в 20:07

0

Выполнить серию последовательных операций мгновенно или одновременно.

kSky7000 07.05.14 в 15:09

0

Мотивация

kabanov 30.04.14 в 20:07

0

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

kSky7000 07.05.14 в 15:13

0

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

Deo 27.08.14 в 08:10

0

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

kSky7000 07.05.14 в 15:18

0

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

Deo 27.08.14 в 08:13

0

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

kSky7000 07.05.14 в 15:29

0

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

kSky7000 07.05.14 в 15:31

1

Как работает компьютерная графика (кратко)

kabanov 30.04.14 в 20:09

0

Кратко о том, как работает компьютерная графика

kSky7000 07.05.14 в 15:37

0

Видео-дисплей (например, компьютерный монитор) рисует 1 пиксель за раз. Это происходит для всей строки пикселей слева на право, а затем начинается обработка следующей строки снизу. Когда достигается нижний правый угол, он проверяет изображение в обратном порядке до верхнего левого угла и начинает все сначала. Это делается так часто (около 6 раз в секунду), что ваши глаза не замечают этого. Для нас, это единственное неподвижное поле цветных пикселей - изображение.

kabanov 30.04.14 в 20:18

0

Видео-дисплей (например, компьютерный монитор) рисует 1 пиксель за раз. Это происходит для всей строки пикселей слева направо, а затем начинается обработка следующей строки. При достижении нижнего правого угла, начинается движение в обратном порядке до верхнего левого угла и все начинается сначала. Это делается так часто (около 60 раз в секунду), что ваши глаза не замечают этого. Мы видим лишь неподвижное поле из цветных пикселей – изображение.

kSky7000 07.05.14 в 15:40

0

Такой экран как монитор компьютера рисует за раз один пиксель. В каждой строке пикселей он двигается слева направо, затем переходит вниз на следующую. При достижении правого нижнего угла, он возвращается в левый верхний и начинает все заново. Он так быстро это делает(примерно 60 раз в секунду), что наши глаза не успевают разглядеть развертку изображения. Мы видим неподвижное поле цветных пикселей – картинку.

Deo 27.08.14 в 08:34

0
+1

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

kSky7000 07.05.14 в 15:47

0

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

kSky7000 07.05.14 в 15:56

0

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

kSky7000 07.05.14 в 16:06

0

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

RSlafka 03.06.14 в 08:11

0

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

RSlafka 03.06.14 в 11:39

0

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

RSlafka 03.06.14 в 11:57

0

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

RSlafka 03.06.14 в 12:23

0

Мы начинаем закрашивать пиксели, а следом за нами видеодрайвер начинает читать буфер кадра (Рис. 1). Видеодрайвер догоняет отрисовщика и проскальзывает мимо к пикселям, которые мы еще не закрасили (Рис. 2). Мы закончили отрисовку (Рис. 3), но драйвер не прочитал несколько последних пикселей.

RSlafka 03.06.14 в 12:28

0

В результате (Рис. 4) пользователь видит только половину картины. И "разрыв башки", как вы видите, я употребил не случайно.

RSlafka 03.06.14 в 12:31

0

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

RSlafka 03.06.14 в 13:07

0

Акт 1, Сцена 1

RSlafka 03.06.14 в 13:10

0

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

RSlafka 03.06.14 в 13:31

0

И, немного увеличив площадь, мы выступим с гениальным решением: мы установим две сцены так, чтобы зрители видели обе. Каждая будет иметь свое освещение. Обозначим их как сцена А и Б. Первая часть представления показывается на сцене А. Тем временем сцена Б погружена в темноту, и рабочие устанавливают на ней декорации. Как только первая часть спектакля заканчивается, мы гасим свет над сценой А и включаем над сценой Б. Зрители переводят внимание на нее, и вторая часть представления немедленно начинается.

RSlafka 04.06.14 в 08:08

0

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

RSlafka 04.06.14 в 08:20

0

Вернёмся к графике

RSlafka 04.06.14 в 12:45

0

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

RSlafka 04.06.14 в 12:54

0

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

RSlafka 04.06.14 в 13:07

0

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

RSlafka 09.06.14 в 07:32

0

Ну а первый буфер уже доступен для использования. Мы начинаем отрисовывать следующий кадр на нем. Вуаля!

RSlafka 09.06.14 в 07:34

0

Паттерн

RSlafka 09.06.14 в 07:34

0

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

RSlafka 09.06.14 в 08:14

0

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

RSlafka 09.06.14 в 08:24

0

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

RSlafka 09.06.14 в 11:06

0

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

RSlafka 09.06.14 в 11:21

0

- у нас имеется некая структура, изменяемая последовательно;

RSlafka 09.06.14 в 11:22

0

- к этой структуре может осуществляться доступ в процессе изменения;

RSlafka 09.06.14 в 11:24

0

- мы не хотим, чтобы код, обращающийся к структуре, наткнулся на "ждите, операция не завершена";

RSlafka 09.06.14 в 11:28

0

- мы хотим читать данные из структуры и не хотим при этом ждать, пока структура заполняется данными.

RSlafka 09.06.14 в 11:30

0

На заметку

RSlafka 09.06.14 в 11:32

1

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

RSlafka 09.06.14 в 11:43

0

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

Deo 27.08.14 в 08:44

0
+1

Операция Swap требует времени

RSlafka 09.06.14 в 11:43

0

Эта операция необходима для двойного буфера, как только структура была изменена. Эта операция должна быть атомарной - никакой код не может получить доступ к структуре, пока swap() не закончен. Как правило, swap() выполняется также быстро, как и присвоение указателя. Но если она занимает больше времени, чем изменение полностью всей структуры, то мы врядли чем-то себе помогли.

RSlafka 09.06.14 в 11:58

0

Нам нужно два буфера

RSlafka 09.06.14 в 12:24

1

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

RSlafka 09.06.14 в 12:31

1

Пример кода

RSlafka 09.06.14 в 12:31

1

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

RSlafka 09.06.14 в 12:39

1
+1

class Framebuffer

RSlafka 09.06.14 в 12:39

0

{

RSlafka 09.06.14 в 12:39

0

public:

RSlafka 09.06.14 в 12:39

0

Framebuffer() { clear(); }

RSlafka 09.06.14 в 12:39

0
Минутку...

Следующая страница →