ДонНТУ   Портал магистров

Паттерны программирования

Введение

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

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

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

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

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

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

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

Шаблон проектирования "команда"

«Команда» – это материализация вызова метода. Это объектно-ориентированная замена обратного вызова.

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

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

Такая функция обычно вызывается на каждом кадре внутри «игрового цикла». Здесь можно видеть жёсткую привязку пользовательского ввода с действиями в игре. Однако многие игры позволяют пользователям настраивать какие кнопки за что отвечают.

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

Стоит отметить, что проверки на null тут нет. Подразумевается, что к каждой кнопке привязана определённая команда.

В этом и заключается сущность шаблона «команда».

Шаблон проектирования "приспособленец"

Данный шаблон предназначен для сцен с огромным количеством объектов. Например, величественный лес с бесчисленными кедрами, на которых ажурная листва пронизывается отдельными лучиками света. Именно для таких сцен как нельзя лучше подходит скромный шаблон с именем «приспособленец».

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

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

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

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

Остальные же данные будут являться индивидуальными для каждого дерева.

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

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

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

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

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

Шаблон решает эту проблему с помощью разделения данных объекта на два типа; первый тип данных – это неуникальные для каждого экземпляра объекта данные, которые можно иметь в одном экземпляре для всех объектов.

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

Шаблон проектирования "наблюдатель"

«Наблюдатель» – один из самых используемых и широко известных шаблонов.

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

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

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

Как раз здесь и пригодится шаблон «наблюдатель». Он позволяет коду объявлять, что произошло нечто интересное не заботясь о том, кто получит уведомление.

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

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

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

Шаблон проектирования "прототип"

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

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

Данный шаблон одновременно и элегантен, и удивителен.

Шаблон проектирования "синглтон"

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

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

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

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

1. Экземпляр не будет создан, если его никто не захочет использовать. Экономия памяти и циклов процессора - это всегда хорошо. Благодаря тому, что «синглтон» инициализируется при первом вызове, его экземпляр не создастся, если никто в игре к нему не обратится.

2. Инициализация во время выполнения. Очевидной альтернативой «Синглтону» является класс со статическими переменными членами. Однако у статических членов есть одно огромное ограничение: автоматическая инициализация. Компилятор инициализирует статические переменные до вызова main(). Это значит, что они не могут использовать информацию, которая будет известна только после того, как программа запустится и начнет работать (например, когда будет загружен файл настроек). А ещё это значит, что они не могут полагаться друг на друга – компилятор не гарантирует очередности, в которой относительно друг друга статические переменные будут инициализированы.

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

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

Основная проблема заключается в том, что использовать глобальные переменные – это не очень хорошо.

Шаблон проектирования "состояние"

Рассмотрим ниже суть шаблона «состояние».

Есть фиксированный набор состояний, в которых может находиться автомат. Используя примеры из предыдущих паттернов такими состояниями будут: прыжок, приседание, подкат и просто стояние на месте.

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

Последовательность ввода или событий, передаваемых автомату.

Каждое состояние имеет переходное, каждый из которых связан с вводом и указывает на состояние.

Шаблон проектирования "двойная буферизация"

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

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

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

Если говорить конкретнее этот шаблон стоит применять если справедливо одно из следующих утверждений:

1) есть состояние, изменяющееся постепенно;

2) к состоянию есть доступ посередине процесса его изменения;

3) нужно предотвратить код, считывающий состояние от чтения незаконченного изменения;

4) нужно иметь возможность считывать состояние, не дожидаясь, когда оно будет изменено.

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

Таким образом, видеодрайвер никогда не видит буфер, с которым в данный момент работает программа. Последняя часть заключается в вызове swap() после того, как сцена заканчивает отрисовывать кадр. Он меняет местами два буфера, просто обменивая между собой указатели в next_ и current_. В следующий раз, когда видеодрайвер вызовет getBuffer(), он обратится к новому буферу в который только что закончила рисовать программа и выведет последний кадр на экран.

Шаблон проектирования "игровой цикл"

Игровой цикл – это квинтэссенция примера «шаблона в игровом программировании». Он есть практически в каждой игре и двух одинаковых практически нет. При этом не в играх он встречается крайне редко.

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

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

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

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

Основные нюансы использования игрового цикла движка заключаются в том, что не придется его писать. Так, как этот код выполняется на каждом кадре, то любые баги или проблемы с производительностью будут серьёзно влиять на всю игру. Надежный игровой цикл – это одна из причин использовать уже готовый движок.

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

Шаблон проектирования "метод обновления"

Игровой мир содержит коллекцию объектов. Каждый объект реализует «метод обновления», симулирующий один кадр поведения объекта. На каждом кадре игра обновляет каждый объект из коллекции.

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

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

Метод обновления хорошо работает, когда:

1) в игре есть некоторое количество объектов или систем, которые должны работать одновременно;

2) поведение каждого объекта практически не зависит от остальных;

3) объекты нужно обновлять постоянно.

Этот шаблон достаточно прост чтобы скрывать в себе какие-то неприятные сюрпризы. Тем не менее, каждая строка кода имеет последствия.

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

Если обновление А происходит перед В в списке обновления, тогда во время обновления А, оно видит предыдущее состояние В. Но когда обновляется В оно уже видит А в новом состоянии, потому, что А на этом кадре уже обновилось.

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

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

Выводы

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

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

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

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

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

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

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

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

Источники

  • Game programming patterns

    Книга о паттернах в игровой индустрии

  • Рефакторинг гуру

    Портал посвященный рефакторингу, паттернам проектирования

  • Статья на сайте Хабр

    Статья посвященная шаблонам проектирования

  • Статья о шаблонах проектирования на сайте Хабр

    Статья-шпаргалка по шаблонам проектирования

  • Статья о паттернах проектирования на сайте Tproger

    Статья о шаблонах проектирования