Urho3D: Игры всерьез

в 13:51, , рубрики: c++, open source, urho3d, разработка игр, метки:

В прошлых статьях (Основы, Редактор: Часть 1 и Редактор: Часть 2) мы создавали небольшие приложения на языке AngelScript. На этот раз я хочу показать, что благодаря продуманной структуре движка писать игры на таком страшном языке, как C++, так же легко, как и на скриптовом языке. И чтобы вам не было слишком скучно читать, я подготовил небольшую игру (клон Flappy Bird), которую можно скачать здесь: github.com/1vanK/FlappyUrho. Кстати, исходный код игры можно читать как самостоятельную статью, потому что он очень подробно прокомментирован.

image

Рассматриваемая версия движка

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

Версия от 9 апреля 2016.

:: Указываем путь к git.exe
set "PATH=c:Program Files (x86)Gitbin"
:: Скачиваем репозиторий
git clone https://github.com/Urho3D/Urho3D.git
:: Переходим в папку со скачанными исходниками
cd Urho3D
:: Возвращаем состояние репозитория к определённой версии (9 апреля 2016)
git reset --hard 4c8bd3efddf442cd31b49ce2c9a2e249a1f1d082
:: Ждём нажатия ENTER для закрытия консоли
pause

Минимальное приложение

#include <Urho3D/Engine/Application.h>

// Чтобы везде не писать Urho3D::ИмяТипа.
using namespace Urho3D;

// Главный класс игры.
class Game : public Application
{
    // Макрос добавляет в класс информацию о текущем и базовом типе.
    URHO3D_OBJECT(Game, Application);

public:
    // Конструктор класса.
    Game(Context* context) : Application(context)
    {
    }
};

// Указываем движку главный класс игры.
URHO3D_DEFINE_APPLICATION_MAIN(Game)

Обращаю ваше внимание на первую особенность: каждый класс, который является производным от класса Urho3D::Object (а в Urho3D таких большинство) должен содержать макрос URHO3D_OBJECT(имяКласса, имяБазовогоКласса).

Для любознательных.

Этот макрос определен в файле Urho3D/Core/Object.h и, среди прочего, сохраняет имя класса в виде строки, которое (имя) в движке активно используется, например в фабрике объектов, о которой чуть позже. Смотрите также urho3d.github.io/documentation/1.5/_object_types.html.

Облегчаем жизнь

Сравните переписанный на языке C++ пример с вращающимся кубом из первой статьи с оригиналом. Как видите, разница минимальна. Однако в глаза бросается обилие заголовочных файлов, которые потребовалось подключить даже в таком маленьком примере. Порой возникает желание просто взять и подключить все заголовочные файлы движка разом. Это не совсем профессионально, но удобно, поэтому можно воспользоваться этим чудо-файлом: github.com/1vanK/Urho3DAll.h.

Компиляция своего проекта

Будем считать, что движок компилировать вы уже умеете (Основы Urho3D). Хочу заметить только, что бывает удобно иметь несколько разных конфигураций движка: полную версию (которая будет использоваться в качестве редактора и тестового полигона) и индивидуальные версии для конкретных игр с минимально необходимым функционалом для компактного размера исполняемого файла. Например, для упомянутой выше игры «Flappy Urho» я отключил скрипты, сеть, навигацию и оставил только физику. Один небольшой совет: при использовании cmake-gui включите галочку «Grouped», чтобы не утонуть в настройках, так как с недавних пор в списке появились еще и параметры SDL.

Чтобы сгенерировать проект для своей игры необходимо в папке с исходниками создать файл CMakeLists.txt, шаблон которого находится здесь: urho3d.github.io/documentation/1.5/_using_library.html. Немного модифицированная версия, которую я обычно использую:

# Название проекта
project (Game)
# Имя результирующего исполняемого файла
set (TARGET_NAME Game)
# Можно не использовать переменные окружения, а указать путь к скомпилированному движку в самом скрипте
set (ENV{URHO3D_HOME} D:/MyGames/Engine/Build)
# Бывает удобно не копировать папку CMake в директорию с исходниками игры, а просто указать путь к ней
set (CMAKE_MODULE_PATH D:/MyGames/Engine/Urho3D/CMake/Modules)

# Остальное менять не нужно
cmake_minimum_required (VERSION 2.8.6)
if (COMMAND cmake_policy)
    cmake_policy (SET CMP0003 NEW)
    if (CMAKE_VERSION VERSION_GREATER 2.8.12 OR CMAKE_VERSION VERSION_EQUAL 2.8.12)
        cmake_policy (SET CMP0022 NEW)
    endif ()
    if (CMAKE_VERSION VERSION_GREATER 3.0.0 OR CMAKE_VERSION VERSION_EQUAL 3.0.0)
        cmake_policy (SET CMP0026 OLD)
        cmake_policy (SET CMP0042 NEW)
    endif ()
endif ()
include (Urho3D-CMake-common)
find_package (Urho3D REQUIRED)
include_directories (${URHO3D_INCLUDE_DIRS})
define_source_files ()
setup_main_executable ()

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

Имхо.

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

Регистрируйте компоненты

Помните, что каждый компонент, который вы создаете, перед использованием необходимо зарегистрировать с помощью Context::RegisterFactory(). Как пример смотрите github.com/1vanK/FlappyUrho/blob/master/GameSrc/EnvironmentLogic.cpp.

Для любознательных.

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

<node id="2">
    ...
    <component type="StaticModel" id="3">
        ....
    </component>
</node>

он будет способен создать и прикрепить к ноде требуемый компонент StaticModel.

Смена сцен и состояния игры

Золотое правило: никогда не уничтожайте сцены и не меняйте состояние игры посередине игрового цикла.

Рассмотрим пример:

class Game : public Application
{
    void HandleUpdae(...)
    {
        Если была нажата клавиша ESC и состояние игры == игровой процесс,
            то состояние игры = главное меню.
    }
}

class UILogic : public LogicComponent
{
    void Update(...)
    {
        Если была нажата клавиша ESC и состояние игры == главное меню,
            то состояние игры = игровой процесс.
    }
}

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

Решением этого будет не менять состояние игры мгновенно, а хранить требуемое состояние в дополнительной переменной и фактическую смену состояния производить в начале следующей итерации игрового цикла до обработки любых событий. В качестве примера смотрите файл github.com/1vanK/FlappyUrho/blob/master/GameSrc/Global.h, в котором объявляются две переменные gameState_ (текущее состояние игры) и neededGameState_ (требуемое состояние игры) и исходник github.com/1vanK/FlappyUrho/blob/master/GameSrc/Game.cpp, в котором реализована смена состояния в обработчике HandleBeginFrame главного класса игры.

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

Умные указатели

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

  • Пока существует хотя бы один строгий указатель Urho3D::SharedPtr, указывающий на какой-то объект, этот объект будет существовать.
  • Слабый указатель Urho3D::WeakPtr не удерживает объект от уничтожения, а значит помогает решать проблему цикличности ссылок. Он похож на обычный указатель, однако позволяет точно знать, был ли объект уничтожен.
  • Urho3D использует интрузивный подсчет ссылок, то есть в отличие от std::shared_ptr счетчик находится в самом объекте. Это позволяет передавать в функции обычные указатели.
  • Создавать умные указатели в глобальной области видимости — плохая идея. При выходе из игры вы можете получите креш при обращении указателя к памяти, уже освобожденной при разрушении контекста.

Кстати.

В игре «Flappy Urho» я вообще не использовал умные указатели, так как все необходимые объекты создаются в начале игры и существуют на протяжении всей работы программы. Поэтому обратитесь к примерам, поставляемым с движком. Смотрите также urho3d.github.io/documentation/HEAD/_conventions.html.

Собственные подсистемы

Для доступа к глобальным переменным и функциям бывает очень удобно создать собственную подсистему. Подсистема — это обычный объект Urho3D, который существует в единственном экземпляре. После регистрации подсистемы с помощью функции Context::RegisterSubsystem() вы можете получить в ней доступ из любого объекта как к любой другой подсистеме с помощью метода GetSubsystem<...>(). Данный подход используется в игре «Flappy Urho» (подсистема Global). Смотрите также urho3d.github.io/documentation/1.5/_subsystems.html.

Атрибуты

Здесь пояснять особо нечего, но я должен был их упомянуть. Атрибуты позволяют автоматизировать сериализацию/десериализацию объектов, которая выполняется при их загрузке и сохранении на диск, а также при сетевой репликации. Подробнее смотрите urho3d.github.io/documentation/1.5/_serialization.html.

Спасибо за внимание!

Автор: 1vanK

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js