Авторизация  

   

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

   

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

   

С момента прошлой публикации прошло около двух с половиной лет – развитие проекта сильно затормозилось, но не прекратилось! За это время (хотя в основном – за последние полгода) была проделана довольно большая работа, о результатах которой я расскажу в новом цикле статей.

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

Почему же так долго

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

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

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

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

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

"Быть может, такой менеджер состояний будет немного слишком сложным," - думал я тогда. Это оказалось не так. Такой менеджер состояний чертовски сложен!

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

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

Так и не сумев придумать работающую реализацию этой концепции, я забросил проект примерно на год. За этот год я уже успел забыть, в чем были проблемы, и наконец подумал: "А не попробовать ли мне еще раз?" Я снова взялся реализовать все с нуля, наступил на те же грабли, помучился пару месяцев, и понял, что надо перекраивать все начисто!

Принцип KISS (Keep it simple, stupid!) сработал, и "менее академическая", "менее абстрактная", но, туды в качель, более простая и понятная концепция организации состояний (о которой ниже) наконец сработала!

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

Наконец, несколько слов о том, во что вылился "прототип менеджера состояний".

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

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

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

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

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

Концепция поменялась

А пока рассмотрим, какие существенные изменения претерпел проект за все это время.

Менеджер состояний

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

  • Состояния ничего не знают друг о друге. Поразмыслив, я решил, что ничего плохого в знании о наличии других состояний нет. Тем более, что в чистом виде это незнание  все равно не было бы реализовано – состояния должны были посылать управляющие сигналы конечному автомату, которые по сути напрямую бы транслировались в некоторый идентификатор следующего состояния.
  • Логикой перехода между состояниями управляет менеджер состояний. Теперь текущее состояние само говорит, каким будет следующее состояние. Менеджер просто хранит у себя все состояния и переключается с одного на другое.
  • Менеджер работает по принципу конечного автомата. Вновь не то. Менеджер просто переключает на то состояние, которое ему сказали. Да, собственно говоря, вряд ли реализованный конечный автомат работал бы по какому-то сценарию, кроме: пришло сообщение "перейти в состояние Х" - состояние сменилось на Х.
  • Состояние первично, игровой цикл вторичен. Вместо мудреной реализации этой идеи мы теперь имеем нормальный игровой цикл, в котором просто вызывается функция update соответствующего состояния. А на случай, если состояние должно всего лишь один раз выполнить какую-то работу, а не повторять итеративно одни и те же действия, есть функция-член класса состояния start, которая вызывается в момент переключения на это состояние. Именно в этой функции и происходит вся работа, а update остается просто пустой – пусть вызывается сколько угодно раз, не жалко.
  • Менеджер состояний содержит стек, чтобы можно было на время перейти в другое состояние, и затем вернуться назад. Вообще-то я пока не дошел до реализации сценария, в котором это может быть нужно. Но простая логика подсказывает, что я спокойно обойдусь и без этого – см. следующий пункт.
  • При переключении в другое состояние экземпляр старого уничтожается, и создается экземпляр нового. Пожалуй, постоянно уничтожать-создавать состояния – это слишком брутально. Поэтому я просто храню их все (в ассоциативном массиве, по ключу-перечислению), а активное состояние просто отмечено (умным) указателем. Заодно это избавляет от необходимости использовать стек состояний, чтобы придержать в памяти временно неактивное состояние.

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

std:function вместо самописных функторов

В предыдущей статье я также много распинался по поводу того, что нужно реализовать обобщенные функторы по образу и подобию тех, о которых писал Александреску. Но дело в том, что писал он о них во времена первого стандарта C++, а начиная с C++ 11, как мне верно подсказали, появилась такая вещь как std::function.

В результате всякая необходимость в написании собственных велосипедов отпала. Как и любой компонент стандартной библиотеки, std::function более гибок, эффективен и стабилен, чем кустарный аналог. Не говоря уж о том, что как, скажем, реализовать функцию std::bind – я до сих пор слабо себе представляю.

Асинхронность на основе задач (Task) вместо потоков

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

Задачи (Task), они же действия (Action) как раз и представляют такую концепцию – вы просто говорите, что нужно сделать (вызовом функции doAsync, которой передаете либо Action, либо просто std::function), и это действие асинхронно выполняется.

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

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

Continuous Integration + github

Если уж проект находится под контролем версий (с использованием Git, как я писал ранее), то почему бы не выложить его в публичный репозиторий? Так я и сделал – создал репозиторий на github и синхронизировал его со своим локальным репозиторием.

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

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

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

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

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

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

Для юнит-тестов, как я ранее писал, используется Google Testing Framework (Google Test). Удачно то, что формат файла, в который выводятся результаты прогона тестов, взят у такого для JUnit, который является родным для Jenkins, поэтому подружить их было не так сложно. Где-то в сети была статья на английском, которую я использовал при настройке сервера CI, но ссылка на нее у меня не сохранилась. Заработало – и ладно.

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

Шагов сборки – два. Первый собственно собирает проект (помним, что я использую библиотеку Qt):

qmake
make

Второй запускает тесты и публикует результаты в файл:

cd Test
./Test --gtest_output="xml:./test-result.xml"

Послесборочный шаг один – опубликовать данные из этого отчета (путь к файлу, соответственно – Test/test-result.xml).

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

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

Что касается ошибок компиляции, то мой рабочий компилятор уже дважды проглатывал некорректную ссылку на функцию-член (MyClass::func вместо правильного &MuClass::func), а компилятор на сборочном сервере ее отлавливал. Мелочь, а приятно.

Что дальше

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

Кроме того, я планирую затронуть работу с git и github, а также какие-нибудь общие темы работы над проектом.

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