В прошлых статьях (Основы, Редактор: Часть 1 и Редактор: Часть 2) мы создавали небольшие приложения на языке AngelScript. На этот раз я хочу показать, что благодаря продуманной структуре движка писать игры на таком страшном языке, как C++, так же легко, как и на скриптовом языке. И чтобы вам не было слишком скучно читать, я подготовил небольшую игру (клон Flappy Bird), которую можно скачать здесь: github.com/1vanK/FlappyUrho. Кстати, исходный код игры можно читать как самостоятельную статью, потому что он очень подробно прокомментирован.
Рассматриваемая версия движка
Немного подумав, я решил уточнять в своих статьях, какая именно версия движка рассматривается, так как движок активно развивается и иногда происходят изменения, ломающие обратную совместимость.
:: Указываем путь к 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(имяКласса, имяБазовогоКласса).
Облегчаем жизнь
Сравните переписанный на языке 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 обычным способом, только обратите внимание, что при генерации проекта игры нужно выставить те же настройки, которые вы использовали при конфигурировании самого движка.
Регистрируйте компоненты
Помните, что каждый компонент, который вы создаете, перед использованием необходимо зарегистрировать с помощью Context::RegisterFactory(). Как пример смотрите github.com/1vanK/FlappyUrho/blob/master/GameSrc/EnvironmentLogic.cpp.
<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 счетчик находится в самом объекте. Это позволяет передавать в функции обычные указатели.
- Создавать умные указатели в глобальной области видимости — плохая идея. При выходе из игры вы можете получите креш при обращении указателя к памяти, уже освобожденной при разрушении контекста.
Собственные подсистемы
Для доступа к глобальным переменным и функциям бывает очень удобно создать собственную подсистему. Подсистема — это обычный объект Urho3D, который существует в единственном экземпляре. После регистрации подсистемы с помощью функции Context::RegisterSubsystem() вы можете получить в ней доступ из любого объекта как к любой другой подсистеме с помощью метода GetSubsystem<...>(). Данный подход используется в игре «Flappy Urho» (подсистема Global). Смотрите также urho3d.github.io/documentation/1.5/_subsystems.html.
Атрибуты
Здесь пояснять особо нечего, но я должен был их упомянуть. Атрибуты позволяют автоматизировать сериализацию/десериализацию объектов, которая выполняется при их загрузке и сохранении на диск, а также при сетевой репликации. Подробнее смотрите urho3d.github.io/documentation/1.5/_serialization.html.
Спасибо за внимание!
Автор: 1vanK