Авторизация  

   

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

   

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

   

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

Игра и сопутствующие приложения

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

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

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

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

А именно, с его помощью можно будет редактировать terrain локаций, статичные объекты в ней, добавлять используемые объекты, NPC и противников. «Оживлять» геймплей можно будет добавлением скриптовых сцен (для реализации которых также понадобятся объекты-триггеры), назначением различным объектам поведения с помощью скриптов, назначением NPC диалогов.
Скриптовые сцены и поведение, помимо прочего, предполагают проигрывание определенных звуков, демонстрацию всплывающих сообщений (такие как «Вы видите следы на земле», или реплики NPC, не предполагающие диалога: «Эх, хороша погодка!»), создание новых объектов, перемещение и изменение старых.

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

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

Редактор текстур. В данном случае под текстурой понимается анимированное изображение, задаваемое собственно изображением (включающим несколько кадров или фреймов), размерами фрейма, количеством кадров, цикличностью (да/нет), задержкой между кадрами. Понятно, что желательно хранить всю эту информацию в одном месте (файле). Назначение редактора как раз и заключается в упаковывании этих данных в общий файл (а при необходимости – и последующее их изменение). Для редактирования собственно изображений должна быть возможность также извлекать картинку в обычном формате.

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

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

Конечно, должна быть возможность работать с такими архивами как в игре, так и в игровом редакторе (см. ниже).

Основы архитектуры игрового приложения

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

Два ключевых понятия здесь – игровое состояние и игровой цикл.

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

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

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

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

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

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

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

Игровые движки

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

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

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

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

  • Текстуры ландшафта (земля, песок, дороги и т.п., неизменяемая основа всей карты)
  • Элементы ландшафта (объекты, которые по сути мало чем отличаются от самой земли; основное их назначение – располагаясь поверх текстур ландшафта, вносить в них разнообразие: это могут быть следы на земле, различные мелкие предметы типа листьев, шишек, травы и т.п.)
  • Подвижные и неподвижные объекты, участвующие в игровом взаимодействии (персонажи, монстры, непроходимые декорации, используемые предметы и многие другие)
  • Эффекты, являющиеся частью декораций игрового мира и допускающие отображение поверх всего вышеперечисленного (например, некоторые световые эффекты, погодные и другие)
  • Over-HUD (визуальные эффекты и всплывающие сообщения, относящиеся к отображению игровой механики, а не к игровому миру: полоски здоровья и прогресса, рамки выделения, цифры нанесенного урона и полученного опыта и другое)
  • HUD (различные элементы пользовательского интерфейса, окна, меню)

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

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

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

Несколько менее очевидный движок – это менеджер ресурсов. Его задача – обеспечить средства вывода (графического и звукового) всеми необходимыми для этого объектами – текстурами, звуками, шрифтами и т.п. Грубо говоря, при загрузке очередной локации (или, например, главного меню) кто-то сверху заявляет: «Ничего не знаю, но сейчас этим двум ребятам понадобится рисовать и проигрывать. Вот тебе список, обеспечь!» – и менеджер ресурсов обеспечивает.

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

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

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

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

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

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

Общая картина

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

Работа игры в общем проходит по следующему сценарию:

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

Здесь пользователь выбирает, желает ли он начать новую игру, загрузить старую, посмотреть список авторов, перейти в настройки, выйти из игры или сделать что-либо еще. «Авторы» и «настройки» можно не выделять в отдельные состояния – здесь мы просто представляем другие меню или окошко с бегущим текстом (опционально – подокна, в которых демонстрируются видеоролики).

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

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

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

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

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

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

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

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