Готовый перевод Game Programming Patterns / Паттерны программирования в играх: 0.1. Architecture, Performance, and Games

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

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

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

Заметьте, что я не делал предположений о том, чью сторону вы занимаете в этом споре.

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

Что же такое архитектура программного обеспечения?

Если вы прочитаете книгу от корки до корки, вы не вынесете знаний о линейной алгебре, стоящей за 3D графикой, или о вычислениях, стоящих за игровой физикой.

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

Ух, такой параграф может стать плохой рекламой для этой книги.

Вместо этого, книга - о коде, который связывает все перечисленное.

Меньше о написании самого кода и больше - о его организации.

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

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

Как отличить хорошую архитектуру от плохой?

Я думал над этим вопросом примерно пять лет.

Конечно, как и вы, я интуитивно чувствую хороший проект.

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

Большинство из нас создавали что-то подобное, учась программировать.

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

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

Какая между ними разница?

Что такое хорошая архитектура приложения?

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

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

Миленько звучит, но не совсем действенно.

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

Верно.

Давайте попытаемся разобраться с этим.

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

Кто-то должен модифицировать исходный код.

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

Мера правильности архитектуры - это то, насколько легко она принимает изменения.

Без изменений, архитектура - это бегун, который никогда не покидает стартовую линию.

Где-то здесь кроется загадка Дзен.

"Скомпилируется ли программа, если никто не видел исходников?"

Как же внести изменения?

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

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

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

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

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

Как только в вашей голове оказывается верный контекст, вы немного думаете и выдаете решение.

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

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

Вы стучите своими мясистыми пальцами по клавиатуре некоторое время, пока на экране не появятся нужные разноцветные лампочки, и на этом все, не так ли?

Как бы не так!

Перед тем как вы напишете тесты и направите их на проверку кода, часто приходится проводить некоторую чистку.

Я сказал "тесты"?

О да, тесты.

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

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

Нет ли у вас более полезных дел, нежели вручную тестировать опять и опять?

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

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

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

Короче говоря, блок-схема для процесса программирования выглядит как-то так:

Факт того, что из данного цикла нет выхода, немного тревожит меня сейчас.

Как может помочь декомпозиция?

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

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

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

Вы можете дать определение "декомпозиции"

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

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

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

Для меня это и есть ключевая цель архитектуры приложения:

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

Конечно, в игру вступают и следующие этапы.

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

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

Какой ценой?

Звучит великолепно, не правда ли?

Примените декомпозицию ко всему и сможете писать код свободно, как ветер.

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

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

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

А хорошая архитектура дает огромное преимущество в продуктивности.

Трудно переоценить эффект, который может дать хорошая архитектура.

Но, как и все в этой жизни, ничто не приходит бесплатно.

Хорошая архитектура требует усилий и дисциплины.

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

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

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

Я видел много программ, которые начинались за здравие, а затем медленно и мучительно умирали по мере того, как программисты делали "только одну малюсенькую правку"

снова и снова.

Как и в садоводстве, мало сажать новые растения.

Надо еще полоть и обрезать их.

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

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

Люди реально загораются этими идеями.

Они представляют будущих разработчиков (или просто себя в будущем),

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

Они представляют себе "Единственный игровой движок, управляющий всем".

Но в этом самом месте начинаются сложности.

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

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

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

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

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

Народная молва придумала термин "YAGNI" (расшифровывается как "Вам это не понадобится")

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

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

Везде натыканы интерфейсы и абстракции.

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

Чтобы найти реальный код, который что-то делает, придется потратить вечность, пробираясь сквозь этот частокол.

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

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

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

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

Сладкая песня сирен о расширяемости засасывает бессчетное количество разработчиков, которые годами работают над "движком",

даже не выясняя, для чего он предназначен.

Производительность и скорость

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

это сказывается на производительности игры.

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

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

Здесь мы располагаем целым спектром гибкости.

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

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

Это гораздо более гибко, но предполагает некоторые потери во времени выполнения.

Метапрограммирование шаблонов находится где-то между двумя этими подходами.

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

Тому есть причина.

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

И чтобы любые изменения требовали меньше усилий.

Это означает кодирование меньшего числа допущений в программе.

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

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

Но производительность вся основана на допущениях.

Искусство оптимизации базируется на конкретных ограничениях.

Можем ли мы безопасно предположить, что число врагов никогда не превысит 256?

Отлично, тогда можно уместить ID в один байт.

Будем ли мы здесь вызывать метод одного конкретного типа?

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

Все сущности будут одного и того же класса?

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

Но как бы то ни было, это не означает, что гибкость плоха сама по себе!

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

Никто, даже Уилл Райт (гейм-дизайнер и один из основателей компании по разработке игр "Maxis", ныне являющейся частью компании "Electronic Arts"), не может сразу на бумаге представить сбалансированный игровой проект.

Процесс создания игры требует итераций и экспериментов.

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

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

Крошечный дисбаланс может разрушить удовольствие от игры.

Здесь нет простого ответа.

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

Напротив, оптимизация кода делает его менее гибким.

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

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

Хорошее в плохом коде

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

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

способом, но в наспех написанном коде тоже есть ценность.

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

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

Нужно относиться к вашему коду так, как хороший турист относится к месту своего отдыха:

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

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

Но, как я упоминал ранее, проектирование игры требует множества экспериментов и исследований.

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

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

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

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

Однако здесь надо сделать очень важную оговорку.

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

Я видел плохих менеджеров, снова и снова играющих в одну и ту же игру:

Босс:

"Эй, у нас есть идея, которую надо бы проверить.

Только прототип, поэтому не думай, что надо сразу правильно реализовать ее.

Как быстро ты сможешь накидать код?"

Разработчик:

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

Босс:

"Отлично!"

Проходит несколько дней...

Босс:

"Слушай, прототип отличный.

Сможешь просто потратить несколько часов, почистить его немного, и, скажем, он пойдет в живой проект?"

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

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

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

Таким образом, вам придется переписать этот код перед тем, как он окажется в реальной игре.

Достигнем баланса

В нашей игре участвует несколько сил:

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

Мы хотим от кода высокой производительности во время выполнения.

Мы хотим быстро реализовать функции, нужные сегодня.

Интересно, что все эти цели, на самом деле, вращаются вокруг понятия скорости:

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

Эти цели по крайней мере частично противоречат друг другу.

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

Наиболее быстрая для написания реализация редко оказывается самой быстрой при выполнении.

Оптимизация занимает у разработчика значительное время.

К тому же, обычно после ее завершения кодовая база костенеет:

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

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

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

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

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

Особенно для новичков, которые просто хотят сделать игру, пугающе слышать: "Здесь нет верного ответа, но есть неверные на любой вкус".

Но, как мне кажется, это и увлекательно!

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

В конце концов, если бы существовал простой ответ, каждый бы его нашел.

Поле деятельности, которым вы можете овладеть за неделю, в конечном счете наскучит.

Вы никогда не услышите о чьей-то выдающейся карьере в области рытья рвов.

Нет, ну может быть, и услышите; я не занимался исследованием данной конкретной аналогии.

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

Кто я, чтобы судить?

На самом деле, мне кажется, что в этом есть много общего с самими играми.

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

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

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

Простота

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

Сейчас в своем коде я очень стараюсь реализовать самое чистое, самое очевидное решение задачи.

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

Я стремлюсь выбирать правильные структуры данных и алгоритмы (примерно в таком порядке)

и затем отталкиваться от них.

Я нахожу, что если поддерживать вещи простыми, то в общем счете будет меньше кода.

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

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

(Хотя, конечно, это не всегда так.

В любое малое количество кода вы можете затолкать кучу циклов и рекурсий.)

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

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

Блез Паскаль завершил свое письмо известной фразой: "Я бы написал более короткое письмо, но у меня не было времени на это".

Автором другой похожей цитаты является Антуан де Сент-Экзюпери:

"Совершенство достигнуто не когда больше нечего добавить, но когда больше нечего отнять".

Ближе к сути отмечу, что каждый раз, как я редактирую главу в данной книге, она становится короче.

Некоторые главы стали меньше на 20% к тому времени, как они были готовы.

Нам редко преподносят элегантную задачу.

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

Нам нужен X, чтобы сделать Y при условии Z, но W, когда A, и так далее.

Другими словами, длинный список различных примеров поведения.

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

Часто новички в программировании делают вот что:

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

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

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

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

Нахождение его немного похоже на сопоставление с шаблоном или решение головоломки.

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

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

Смиримся с этим, наконец

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

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

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

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

Поверьте мне, два месяца до выпуска игры - не то время, когда вы захотите начать волноваться по поводу маленькой надоедливой - "игра работает только на 1 кадре в секунду" -

проблемы.

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

В итоге, вам же придется существовать с этим.

Если собираетесь удалить код, не тратьте время на прихорашивание его.

Рок-звезды крушат номера отелей потому, что знают - они выезжают на следующий день.

Но главное, если хотите сделать что-то увлекательное, то делайте это весело!

Переведено на Нотабеноиде
http://notabenoid.org/book/51192/215207

Переводчики: IngaJolly, Vladislav86, BendLHuck, Qeruim

GromHoll, marapper, deveel

Настройки

Готово:

100.00% КП = 1.2