Авторизация  

   

Подпишитесь на нас  

   

Поиск по сайту  

   

Продолжаем рассматривать этап разработки Проектирование. В первой части мы составили общее представление о нем и рассмотрели важный принцип "Разделяй и властвуй", а также такие вещи, как user story и epic story. Сегодня мы рассмотрим подробнее, что именно мы будем разделять и над чем последовательно властвовать, а также каких еще принципов нужно придерживаться во время проектирования и программирования.

Иерархические уровни проекта

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

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

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

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

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

Наконец, самая нижняя ступень (по крайней мере, в объектно-ориентированных языках подобных C++) - это отдельные классы. Они являются элементарными составляющими проекта. Например, Страуструп в "Специальном издании" в основном говорил о проектировании отдельных классов. На этом уровне определяется как минимум полный интерфейс каждого класса (возможно, не учитывая детали реализации, скрытые от пользователя в private-секции или с помощью методики pimpl).

Проектирование и программирование - итеративный клубок

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

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

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

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

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

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

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

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

Подготовка проекта и кода к изменениям

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

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

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

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

На языке C++ полиморфизм доступен через наследование (лежащее в основе ООП) и использование шаблонов (поддерживающих обобщенное и метапрограммирование).

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

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

   
© Создание игры - взгляд изнутри. The Gamedev. При использовании материалов сайта ссылка на источник обязательна.