Загрузка...
 
Печать
Кодим 3D FPS DX9

1.2 Создаём фреймворк движка




Первая, по-настоящему практическая глава. Здесь мы впервые оставим сухую теорию и займёмся настоящим игрокодингом! В Главе 1.1, где проектировался наш будущий движок, мы неоднократно отмечали, что ему необходимо иметь так называемую "единую точку контакта".1
Понятие "единая точка контакта" определяется как возможность игрокодера включить движок в игру через одно выражение #include, одну библиотеку LIB и один главный (main) класс, экземпляр которого должен быть инициирован. Более того, пользователь должен иметь доступ к движку через одну глобальную переменную (global variable).
К счастью, сделать это очень просто. Для этого достаточно:
  • связать все исходные и заголовочные файлы в проекте Engine, дав ссылку на них (через выражение #include) в одном общем заголовочном файле, назвав его, например, Engine.h;
  • создать всего один единственный класс Engine, который используется для доступа ко всему функционалу движка;
  • создать внешний глобальный указатель (external global pointer). Назовём его g_engine. Он будет доступен везде: как в пределах проекта Engine, так и при создании игрового приложения. Мы разрешим движку самостоятельно устанавливать этот указатель всякий раз, когда будет объявляться экземпляр класса Engine, освобождая игрокодера от выполнения этой задачи.
Первый шаг включает в себя создание Проекта движка, его первоначальная настрока и подключение заголовочных файлов MS DirectX SDK.
ОК, приступаем.

Устанавливаем DirectX SDK 9 и прописываем пути к нему в MSVC++2010

  • Скачай и установи DirectX SDK 9
Его нетрудно найти в Гугле или Яндексе.
В последних версиях много "лишнего": DirectX 10, 11 и 12 версий ушёл далеко вперёд. Раз мы прогаем под DirectX 9, скачай релиз DXSDK 2010 года. Например отсюда: [https://softfamous.com/directx-9-sdk/download]. Другие релизы найдёшь в разделе Софт нашего сайта.
  • Обязательно настрой в MS Visual Studio 2010 пути к DirectX SDK.
(Проект->Свойства->Свойства конфигурации->Каталоги VC++...) А вообще, весь процесс подробно расписан здесь: MS Visual Cpp 2010 Express. Установка и указание путей к DirectX SDK.
Закрыть
noteПримечание

Путь установки по умолчанию (DX SDK 2010) следующий: C:\Program Files (x86)\Microsoft DirectX SDK (June 2010). При указании пути к каталогу библиотек (lib) в нашем случае выбирай подкаталог x86, где собраны библиотеки для создания 32-битных приложений.

  • Сохрани Решение (Файл->Сохранить все).
Проект создан. Так как это "Пустой проект", он не содержит в себе никаких файлов. В левой части главного окна MS Visual C++ 2010 расположен "Обозреватель решений". (Если его нет, в главном меню выбираем: Вид->Другие окна->Обозреватель решений. Или комбинация горячих клавиш Ctrl+Alt+L.) В Обозревателе решений видна древовидная структура Проектов, входящих в данное Решение. Чуть ниже названия Проекта видим специально заготовленные папки (в MSVC++2010 они называются "фильтры") для файлов Проекта.

Создаём Проект движка

  • Стартуй MS Visual C++ 2010.
Видим Окно приветствия (т.е. в IDE ни один Проект пока не открыт).
  • Создай новый Проект Win32.
(В Главном меню: Файл->Создать->Проект...). В окне "Создать Проект":
Отмечаем щелчком пункт "Проект Win32"
  • В поле Name введи имя Проекта GameProject01
Строки "Расположение" и "Имя Решения" заполняются автоматически (при необходимости изменяем).
Image
  • Жмём ОК.
Появится окно мастера приложений (Application Wizzard).
  • Жмём Далее.
Напомним, что на протяжении всей первой части данного курса мы программируем игровой движок, который физически будет содержаться в одном файле статической библиотеки с расширением .lib. Поэтому...
  • На странице мастера "Параметры приложения" отмечаем пункт "Статическая библиотека" и убираем галку у пункта "Предварительно скомпилированный заголовок":
Image
  • Жмём Готово.
При создании наш Проект автоматически размещается внутри Решения с тем же именем. В результате в левой колонке обозревателя решений видим Решение GameProject01, которое содержит Проект GameProject01.
Нам это не подходит, так как согласно замысла, внутри Решения GameProject01 будут содержаться два Проекта: Engine (движок; статическая библиотека с расширением .lib) и Game (игра на базе этого движка; файл приложения с расширением .exe). Напомним, что в первой части курса мы разрабатываем движок.
Поэтому...
  • Смени имя вновь созданного Проекта с GameProject01 на Engine (правой кнопкой мыши по названию
Проекта -> из контекстного меню выбрать "Переименовать").

Добавляем Engine.h (Проект Engine)

Напомним, что именно Engine.h будет включать в себя ссылки на остальные заголовки движка и, таким образом, будет использоваться в качестве единой точки контакта.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" (Header Files).
  • Во всплывающем меню Добавить->Создать элемент...
Image
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи Engine.h.
  • Жмём "Добавить".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле Engine.h набираем следующий код:
Engine.h
//-----------------------------------------------------------------------------
//	File: Engine.h
//	Desc: Главный заголовочный файл движка. Этот файл соединяет движок воедино
//	и является единственным заголовочным файлом, который следует добавлять в проекты,
//	создаваемые на базе данного движка.
//	Представляет собой воплощение концепции единой точки контакта.
//
//	Original Source code:
//	Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------

#ifndef ENGINE_H
#define ENGINE H

//-----------------------------------
// Указывает на использование 8 версии DirectInput (Функционала 8 версии будет предостаточно)
//-----------------------------------
#define DIRECTINPUT_VERSION 0x0800

//--------------------
// System Includes
//--------------------
#include <stdio.h>
#include <tchar.h>
#include <windowsx.h>

//-----------------
// DirectX Includes
//-----------------
#include <d3dx9.h>
#include <dinput.h>

//-----------------------------
// Engine Includes
//-----------------------------


//----------------------------------------------------------------------------
// Macros
// Макросы безопасного удаления
//----------------------------------------------------------------------------
#define SAFE_DELETE( p )       { if( p ) { delete ( p );     ( p ) = NULL; } }
#define SAFE_DELETE_ARRAY( p ) { if( p ) { delete[] ( p );   ( p ) = NULL; } }
#define SAFE_RELEASE( p )      { if( p ) { ( p )->Release(); ( p ) = NULL; } }

//-----------------------------------------------------------------------------
// Engine Setup Structure
//-----------------------------------------------------------------------------
struct EngineSetup
{
	HINSTANCE instance; // Application instance handle.
					// Дескриптор инстанса приложения
	char *name; // Name of the application.

	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		instance = NULL;
		name = "Application";
	}
};

//-----------------------------------------------------------------------------
// Engine Class
//-----------------------------------------------------------------------------
class Engine
{
public:
	Engine( EngineSetup *setup = NULL );
	virtual ~Engine();

	void Run();

	HWND GetWindow();
	void SetDeactiveFlag( bool deactive );

private:
	bool m_loaded; // Indicates if the engine is loading.
					// Флаг показывает, загружен ли движок
	HWND m_window; // Main window handle.
					// Дескриптор главного окна приложения
	bool m_deactive; // Indicates if the application is active or not.
					// Флаг активности приложения

	EngineSetup *m_setup; // Copy of the engine setup structure.
					// Копия структуры EngineSetup
};

//-----------------------------------------------------------------------------
// Externals
// Глобальные объявления
//-----------------------------------------------------------------------------
extern Engine *g_engine;

#endif

  • Сохрани Решение (Файл -> Сохранить все).
Обрати внимание на комментарии. Профессиональные программеры всегда их оставляют в разных участках кода. Напоминаем, что при компиляции Release-версии проекта все комментарии автоматически отбрасываются. При создании Проекта/Решения по умолчанию стоит Debug-версия.
Закрыть
noteПримечание

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


Исследуем код Engine.h

Первые 3 выражения #include в нашем файле Engine.h связывают проект с различным функционалом ОС MS Windows.
Фрагмент Engine.h
...
//--------------------
// System Includes
//--------------------
#include <stdio.h>
#include <tchar.h>
#include <windowsx.h>
...

В данный момент весь этот функционал в полном объёме нам не требуется, но мы включим его в наш заголовочный файл сейчас, чтобы потом уже к этому не возвращаться.
ЗАГОЛОВОЧНЫЙ ФАЙЛ ОПИСАНИЕ
stdio.h Даёт доступ к стандартным функциям файлового ввода/вывода.
tchar.h Открывает доступ к типу данных text string (текстовая строка).
windowsx.h Даёт доступ к расширенному набору Windows-функций.

Второй набор выражений #include добавляет функционал различных компонентов DirectX, которые мы будем использовать. Он напрямую зависит от версии DirectX, под которую создаётся движок. Это связано с тем, что на протяжении более чем 20-летней истории этой библиотеки Microsoft постоянно меняла состав её компонентов. С выходом каждой новой версии одни компоненты удаляются (как устаревшие) или объёдиняются, другие появляются, третьи просто меняют своё название. Этот процесс продолжается и сегодня и уследить за этим не всегда просто. Подробнее об этом здесь: Эволюция DirectX. Наш движок создаётся под DirectX 9.0c (вышел в свет в конце 2004 г.). И поэтому далее мы будем рассматривать компоненты, входящие именно в эту версию DirectX.
Фрагмент Engine.h
...
//-----------------
// DirectX Includes
//-----------------
#include <d3dx9.h>
#include <dinput.h>
...

По ходу курса мы будем добавлять сюда другие операторы #include, содержащие ссылки на заголовочные файлы различных компонентов DirectX. Они будут добавляться по мере необходимости. Вот полный список заголовочных файлов DirectX SDK, которые нам понадобятся:
ЗАГОЛОВОЧНЫЙ ФАЙЛ ОПИСАНИЕ
d3dx9.h Задействует библиотеку D3DX.
dinput.h Даёт возможность использовать интерфейсы DirectInput, для поддержки устройств ввода (в нашем случае это клавиатура и мышь).
dplay8.h Даёт возможность использовать интерфейсы DirectPlay, для разработки сетевых режимов игры.
dmusici.h Даёт возможность использовать интерфейсы DirectMusic, для проигрывания любых звуков в игре.

Существуют также множество других заголовочных файлов (и соответствующих им библиотек и компонентов DirectX). Полную информацию о них ищем в документации к DirectX SDK.
Чуть выше заголовков DirectX расположен оператор #define, который указывает движку использовать 8 версию компонента DirectInput (в DirectX 9.0 используется именно эта версия DirectInput). Данный оператор уже есть в dinput.h и в нашем случае его можно было бы не указывать. Но в этом случае при компиляции будет выдаваться предупреждение "DIRECTINPUT_VERSION is undefined". (Некритично, но лишние предупреждения в логе компилирования нам ни к чему.)
Следующий шаг - привязать различные (пока не созданные) компоненты движка к заголовочному файлу Engine.h:
Фрагмент Engine.h
...
//-----------------------------
// Engine Includes
//-----------------------------
...

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

Забегая вперёд, скажем, что в будущем в самом начале каждого, вновь создаваемого, исходного файла (.cpp) мы укажем ссылку на заголовок Engine.h . В нём уже собраны вместе ссылки на все остальные заголовочные файлы, участвующие в компиляции. от так легко и изящно мы избежим нагромождения операторов #include в наших исходных файлах, сделая код более структурированным и читабельным. Это наилучшим образом соответствует концепции "единой точки контакта", сформулированной в начале этой главы.

Рассмотрим оставшуюся часть содержимого Engine.h .

Макросы управления памятью

Прошли те дни, когда объём памяти компьютера был крайне ограничен. В принципе и тогда создавались отличные работоспособные приложения. А всё потому, что талантливые программисты отлично владели приёмами, предотвращающими т.н. "утечки памяти" (от англ. "memory leaks"). Несмотря на то, что современные компьютеры обладают объёмом памяти, которого должно "хватить всем", важно помнить, что он (объём памяти) всё-таки ограничен и рано или поздно может возникнуть ошибка переполнения памяти. Переполнение памяти в основном происходит из-за неэффективного кода и нехватки в нём специальных служебных процедур по её освобождению (т.н. "housekeeping routines"). Если твой софт испытывает нехватку памяти, от него вполне можно ожидать нестабильной работы и даже "падение" ОС, что само по себе уже ни для кого не будет смешно. Профессионально созданное приложение потребляет минимум системных ресурсов (и памяти) и обладает хорошим быстродействием даже на "бюджетных" ПК. Игровые приложения не исключение. К счастью, современные ОС оснащены эффективными средствами, предотвращающими утечки неиспользуемой памяти, особенно когда твой софт неожиданно аварийно завершает работу. Несмотря на это, ты никогда не должен полагаться на ОС. Вместо этого у тебя должен быть свой собственный метод управления памятью и её своевременного освобождения. Для этих целей ты можешь применять уже готовые системы управления памятью от сторонних разработчиков, которые будут выделять, отслеживать и освобождать всю неиспользуемую память. Такие системы часто хорошо защищены от ошибок неопытных программистов и могут с высокой долей вероятности гарантировать отсутствие утечек памяти из уязвимых мест в твоём коде. В данном курсе мы не будем использовать такие системы (для простоты изложения). В любом случае мы настоятельно рекомендуем изучить эту тему более детально (в Интернете есть вся информация) и позднее разработать свой собственный метод управления памятью, который ты сможешь использовать в своих будущих проектах.
Напомним, что при создании нашего движка мы будем использовать очень простой метод управления памятью. Для начала мы будем использовать стандартный оператор new для создания всего, начиная с экземпляра класса и заканчивая хранилищами данных (например массивы и связные списки). По ходу этого курса ты будешь встречать оператор new практически повсеместно.
Для того, чтобы освободить занимаемую память, мы будем использовать три простых макроса из Engine.h:
Фрагмент Engine.h
...
//----------------------------------------------------------------------------
// Macros
// Макросы безопасного удаления
//----------------------------------------------------------------------------
#define SAFE_DELETE( p )       { if( p ) { delete ( p );     ( p ) = NULL; } }
#define SAFE_DELETE_ARRAY( p ) { if( p ) { delete[] ( p );   ( p ) = NULL; } }
#define SAFE_RELEASE( p )      { if( p ) { ( p )->Release(); ( p ) = NULL; } }
...

Каждый из них принимает в качестве параметра указатель на определённый адрес в памяти и используется для освобождения определённых типов памяти:
МАКРОС ОПИСАНИЕ
SAFE_DELETE(p) Освобождает любые участки памяти, созданные с использованием оператора new, за исключением массивов.
SAFE_DELETE_ARRAY(p) Освобождает память, занимаемую массивами. Безопасно удаляет массивы.
SAFE_RELEASE(p) Специальный макрос, применяющийся для освобождения памяти, которая была выделена путём создания объекта DirectX COM Технология COM (Component Object Model), который использует интерфейс IUnknown. Так как весь DirectX построен на базе модели COM, в примерах DirectX SDK этот макрос применяется повсеместно.

Всякий раз, когда ты создаёшь один из объектов DirectX, ты неявно выделяешь под него необходимый объём памяти. Единственный способ освободить эту память - вызвать функцию Release, экспонированную интерфейсом IUnknown. Именно этим и занимается наш макрос SAFE_RELEASE. Тогда возникает вопрос: зачем вообще нужны эти макросы, в то время как в DirectX уже имеется такая замечательная функция Release, а для освобождения памяти от объектов, созданными оператором new, существует обратный ему опеатор delete? Ответ - для безопасности. Макрос ограждает нас от ситуаций, когда мы пытаемся уничтожить объект, память для которого никогда ранее не выделялась. Например, если ты попытаешься применить функцию Release к указателю NULL, то получишь в ответ ошибку доступа к памяти (access violation error). Макрос SAFE_RELEASE предотвращает появление этой ошибки.
Закрыть
noteСовет

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


Объявление структуры EngineSetup

Продолжаем исследовать Engine.h. В нём, сразу после всех директив #include, мы видим объявления структуры EngineSetup и класса Engine. Оба они делят весь движок на две большие логические части:
EngineSetup (Исходя из названия.) Настраивает и подготавливает движок к работе.
Engine Запускает движок.

Начнём с рассмотрения EngineSetup.
Как мы ранее упоминали, наш движок должен иметь метод, позволяющий программисту предварительно инициализировать движок, чтобы настроить его определённым образом. Как раз для этого мы объявляем структуру EngineSetup, которая представляет собой обычную структуру struct, типичную для многих C/C++ программ. Позднее мы создадим инстанс (экземпляр) этой структуры и заполним его необходимыми данными. Это похоже на заполнение обычной анкеты, которую ты заполняешь и передаёшь движку. Движок, в свою очередь, берёт данные из этой "анкеты" и настраивает себя согласно данным, указанным в ней. Здесь важно помнить, что у программиста должна быть возможность указать в этой, так называемой, "анкете" как можно больше (или как можно меньше) данных. Другими словами, движок должен иметь набор параметров по умолчанию для каждого пункта в ней (например для тех случаев, когда программер не указал данные в соответствующих "графах").
Объявление EngineSetup в Engine.h выглядит так:
Фрагмент Engine.h
...
//-----------------------------------------------------------------------------
// Engine Setup Structure
//-----------------------------------------------------------------------------
struct EngineSetup
{
	HINSTANCE instance; // Application instance handle.
					// Дескриптор инстанса приложения
	char *name; // Name of the application.

	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		instance = NULL;
		name = "Application";
	}
};
...

Сейчас наша структура EngineSetup в общем-то никак не заполнена. Это нормально. Ведь наш движок пока также чист как белый лист бумаги. По мере разрастания движка мы будем добавлять различные данные в струткуру EngineSetup, что в итоге даст конечному программеру (пользователю движка) возможность высталять различные опции, чтобы таким образом настраивать поведение движка перед началом работы. В струткуре EngineSetup в настоящий момент ты можешь видеть конструктор EngineSetup(), в котором установлены значения по умолчанию для каждого из параметров. Сейчас быстро пробежимся по двум параметрам которые здесь есть.
ПАРАМЕТР ОПИСАНИЕ
instance Дескриптор (handle) приложения. Именно он посылается ОС Windows во время выполнения функции WinMain. Позднее мы создадим тестовое приложение, чтобы проверить работу нашего движка. Там этот дескриптор будет обязательно фигурировать. При создании игры, всё, что тебе необходимо будет сделать, это передать этот дескриптор движку, чтобы он знал, с каким приложением (игрой) он работает в данный момент.
name Название приложения (игры). Этот текст будет появляться на тайтлбаре (верхняя полоса окна) оконного приложения (если таковое будет). Обычно переопределяется при создании Проекта игры.


Объявление класса Engine


Image
Рис 1. Класс Engine в качестве единой точки контакта


Рассмотрим главную "рабочую лошадку" нашего движка - класс Engine. Он служит своеобразной "точкой входа". В нём как раз и будет находиться единая точка контакта. Чтобы использовать текущий движок, игрокодеру необходимо лишь создать новый экземпляр класса Engine. И всё! На Рис. 1 схематично показано использование класса Engine в качестве единой точки контакта. Объявление класса Engine в хэдере Engine.h выглядит так:
Фрагмент Engine.h
...
//-----------------------------------------------------------------------------
// Engine Class
//-----------------------------------------------------------------------------
class Engine
{
public:
	Engine( EngineSetup *setup = NULL );
	virtual ~Engine();

	void Run();

	HWND GetWindow();
	void SetDeactiveFlag( bool deactive );

private:
	bool m_loaded; // Indicates if the engine is loading.
					// Флаг показывает, загружен ли движок
	HWND m_window; // Main window handle.
					// Дескриптор главного окна приложения
	bool m_deactive; // Indicates if the application is active or not.
					// Флаг активности приложения

	EngineSetup *m_setup; // Copy of the engine setup structure.
					// Копия структуры EngineSetup
};
...

Как ты мог заметить, в данный момент класс Engine совсем небольшой, так как он лишь намечает каркас нашего движка. По мере изучения последующих глав данного курса, в код класса Engine будут добавляться новые функции и он будет постоянно расти в объёме.
Сейчас в нём содержится объявление 5 функций:
ЧЛЕН КЛАССА ENGINE ОПИСАНИЕ
m_loaded Переменная типа BOOL. Используется для определения, загружен движок (и готов к работе) или нет.
m_window Содержит дескриптор главного окна приложения, которое использует движок. Возвращается при вызове функции GetWindow(). Это тот же самый дескриптор, который который передаётся функцией WindowProc всякий раз, когда приложение получает сообщение от Windows.
m_deactive Переменная типа BOOL, которая переключается всякий раз, когда окно приложения обретает или теряет фокус (активируется/деактивируется).
*m_setup Указатель на экземпляр структуры EngineSetup, используемой при создании движка.


Добавляем Engine.cpp (Проект Engine)

Если помнишь, для написания хорошо структурированного кода мы размещаем объявления всех классов и функций в заголовочных файлах (.h), а их реализацию - в файлах исходного кода (.cpp), в которых также прописываем ссылку на "сопровождающий" его хэдер (=заголовочный файл .h).
Создадим файл исходного кода Engine.cpp, в котором будут содержаться реализации функций, объявленных в заголовочном файле Engine.h.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
Image
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи Engine.cpp. Жмём "Добавить".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле Engine.cpp набираем следующий код:
Engine.cpp
//------------------------------------------------------------------------
// File: Engine.cpp
// Реализация классов и функций, объявленных в Engine.h.
//	Refer to the Engine.h interface for more details. //
//	Оригинальный исходный код:
//	Programming a Multiplayer First Person Shooter in DirectX
//	Copyright (c) 2004 Vaughan Young
//	Перевод комментариев и адаптация под MS Visual C++ 2010 - Igrocoder.ru
//------------------------------------------------------------------------

#include "Engine.h"

//-----------------------------------------------------------------------------
// Globals
//-----------------------------------------------------------------------------
Engine *g_engine = NULL;

//-----------------------------------------------------------------------------
// Handles Windows messages.
// Обработчик сообщений Windows
//-----------------------------------------------------------------------------
LRESULT CALLBACK WindowProc( HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam )
{
	switch( msg )
	{
		case WM_ACTIVATEAPP:
			g_engine->SetDeactiveFlag( !wparam );
			return 0;

		case WM_DESTROY:
			PostQuitMessage( 0 );
			return 0;

		default:
			return DefWindowProc( wnd, msg, wparam, lparam );
	}
}

//-----------------------------------------------------------------------------
// The engine class constructor.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Indicate that the engine is not yet loaded.
	// Показываем, что движок пока не загружен.
	m_loaded = false;

	// If no setup structure was passed in, then create a default one.
	// Otehrwise, make a copy of the passed in structure.
	// Если структура EngineSetup не передана, то создаём одну.
	// Если есть, то делаем её копию.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Store a pointer to the engine in a global variable for easy access.
	// Сохраняем указатель на экземпляр движка в глобальную переменную.
	// Для более простого доступа к ней из любой части движка
	g_engine = this;

	// Prepare and register the window class.
	// Заполняем переменные члены оконного класса.
	WNDCLASSEX wcex;
	wcex.cbSize        = sizeof( WNDCLASSEX );
	wcex.style         = CS_CLASSDC;
	wcex.lpfnWndProc   = WindowProc;
	wcex.cbClsExtra    = 0;
	wcex.cbWndExtra    = 0;
	wcex.hInstance     = m_setup->instance;
	wcex.hIcon         = LoadIcon( NULL, IDI_APPLICATION );
	wcex.hCursor       = LoadCursor( NULL, IDC_ARROW );
	wcex.hbrBackground = NULL;
	wcex.lpszMenuName  = NULL;
	wcex.lpszClassName = "WindowClass";
	wcex.hIconSm       = LoadIcon( NULL, IDI_APPLICATION );
	RegisterClassEx( &wcex );

	// Initialise the COM using multithreaded concurrency.
	// Инициализируем COM в мультизадачном режиме.
	CoInitializeEx( NULL, COINIT_MULTITHREADED );

	// Create the window and retrieve a handle to it.
	// Note: Later the window will be created using a windowed/fullscreen flag.
	// Создаём окно.
	// Позднее окно будет создаваться с учётом флага оконного/полноэкранного режимов.
	m_window = CreateWindow( "WindowClass", m_setup->name, WS_OVERLAPPED, 0, 0, 800, 600, NULL, NULL, m_setup->instance, NULL );

	// Seed the random number generator with the current time.
	// Стартуем генератор сулчайных чисел на основе текущего времени.
	srand( timeGetTime() );

	// The engine is fully loaded and ready to go.
	// Движок полностью загружен и готов к работе.
	m_loaded = true;
}

//-----------------------------------------------------------------------------
// The engine class destructor.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Ensure the engine is loaded.
	// Проверяем, загружен ли движок.
	if( m_loaded == true )
	{
		// Everything will be destroyed here (such as the DirectX components).
		// Здесь всё уничтожаем.
	}

	// Uninitialise the COM.
	CoUninitialize();

	// Unregister the window class.
	UnregisterClass( "WindowClass", m_setup->instance );

	// Destroy the engine setup structure.
	SAFE_DELETE( m_setup );
}

//-----------------------------------------------------------------------------
// Enters the engine into the main processing loop.
// Главный цикл обработки сообщений движка.
//-----------------------------------------------------------------------------
void Engine::Run()
{
	// Ensure the engine is loaded.
	// Проверяем, загружен ли движок.
	if( m_loaded == true )
	{
		// Show the window.
		// Показываем окно.
		ShowWindow( m_window, SW_NORMAL );

		// Enter the message loop.
		// Входим в цикл обработки сообщений.
		MSG msg;
		ZeroMemory( &msg, sizeof( MSG ) );
		while( msg.message != WM_QUIT )
		{
			if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
			{
				TranslateMessage( &msg );
				DispatchMessage( &msg );
			}
			else if( !m_deactive )
			{
				// Calculate the elapsed time.
				// Подсчитываем затраченное время.
				unsigned long currentTime = timeGetTime();
				static unsigned long lastTime = currentTime;
				float elapsed = ( currentTime - lastTime ) / 1000.0f;
				lastTime = currentTime;
			}
		}
	}

	// Destroy the engine.
	// Уничтожаем движок
	SAFE_DELETE( g_engine );
}

//-----------------------------------------------------------------------------
// Returns the window handle.
// Возвращает дескриптор окна.
//-----------------------------------------------------------------------------
HWND Engine::GetWindow()
{
	return m_window;
}

//-----------------------------------------------------------------------------
// Sets the deactive flag.
// Устанавливает флаг неактивности.
//-----------------------------------------------------------------------------
void Engine::SetDeactiveFlag( bool deactive )
{
	m_deactive = deactive;
}

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

Исследуем код Engine.cpp

Следуя ранее утверждённой концепции "единой точки контакта", подключаем заголовок Engine.h, который уже содержит ссылки на все остальные заголовочные файлы:
Фрагмент Engine.cpp
...
#include "Engine.h"
...

Далее видим внешний глобальный указатель (external global pointer). Назовём его g_engine:
Фрагмент Engine.cpp
...
//-----------------------------------------------------------------------------
// Globals
//-----------------------------------------------------------------------------
Engine *g_engine = NULL;
...

Он будет доступен везде: как в пределах проекта Engine, так и при создании игрового приложения. Мы разрешим движку самостоятельно устанавливать этот указатель всякий раз, когда будет объявляться экземпляр класса Engine, освобождая игрокодера от выполнения этой задачи. Пока движок не создан, его значение равно NULL (ноль).

Функция обработки сообщений ОС Windows

Любое приложение, которое запускается на твоём компьютере обязано отвечать на запросы операционной системы. Другими словами, приложение не может просто "повесить" компьютер, игнорируя запросы от ОС. По этой причине любое оконное приложение Windows обязательно должно содержать т.н. функцию обратного вызова "оконной процедуры" ([https://ru.wikipedia.org/wiki/Callback_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)|call-back function]).
Функцией обратного вызова называется функция приложения, которая никогда не вызывается напрямую другими функциями или процедурами этого приложения (хотя ничто не запрещает это делать), а вызывается непосредственно операционной системой Windows. Данный вид функций должен знать каждый программист, т.к. он является основополагающим в Windows-программировании.
В нашем случае назовём её WindowProc. Вот её прототип:
Прототип функции WindowProc
LRESULT CALLBACK WindowProc(HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam);

Тот факт, что мы определили функцию WindowProc в нашем приложении совсем не означает, что мы будем её вызывать. Более того, мы вообще никогда не будем её вызывать. Вместо этого, данная функция (обратного вызова) автоматически вызывается всякий раз, когда наше приложение получает сообщение от ОС MS Windows. Причём даже в том случае, когда такое сообщение сгенерировано нашим приложением. А когда вызывается, она начинает обрабатывать все сообщения, которые MS Windows посылает нашему приложению. Чтобы помочь тебе, MS Windows передаёт через вызов функции WindowProc четыре параметра:
ПАРАМЕТР ОПИСАНИЕ
HWND hwnd Дескриптор окна, которое получило сообщение. Как правило, это окно твоего приложения.
UINT msg Идентификатор входящего сообщения.
WPARAM wparam Параметр входящего сообщения. Используется (или нет) в зависимости от сообщения.
LPARAM lparam Параметр входящего сообщения. Используется (или нет) в зависимости от сообщения.

Лучший способ обработать вызов WindowProc - это проверить параметр msg на предмет наличия в нём поступившего сообщения. А затем, используя данные параметров wparam и/или lparam, обработать сообщение.
Существует множество различных сообщений, обработку которых должен поддерживать наш движок. К счастью, ОС MS Windows предоставляет т.н. "обработчик по умолчанию", который может обработать любое сообщение Windows, используя значения по умолчанию. Это означает, что мы можем выбрать часть сообщений, которые (так, как нам надо) будет обрабатывать наш движок, а все остальные "скормить" обработчику по умолчанию. Наиболее часто используемые сообщения Windows приведены в Таблице 1:
Таблица 1. Наиболее часто используемые сообщения Windows
СООБЩЕНИЕ ОПИСАНИЕ
WM_ACTIVATEAPP Сообщение посылается всякий раз при смене фокуса (активации или деактивации) окна приложения.
WM_COMMAND Сообщение, которое всякий раз посылается при выборе пользователем пунктов меню (и нажатии на них) и активировании других элементов пользовательского интерфейса.
WM_CREATE Сообщение посылается всякий раз, когда приложение создаёт новое окно.
WM_DESTROY Сообщение посылается всякий раз, когда приложение уничтожает окно.
WM_PAINT Сообщение посылается всякий раз, когда приложение уничтожает окно.
WM_SIZE Сообщение посылается всякий раз, когда изменяются размеры окна приложения.

Для нужд нашего движка нам нужны всего 2 сообщения из этого списка: WM_ACTIVATEAPP и WM_DESTROY. Мы будем обрабатывать их индивидуально, "навешивая" нужный нам функционал. В Engine.cpp мы видим законченую реализацию нашей функции обратного вызова WindowProc, с поддержкой обоих этих сообщений и установленным обработчиком по умолчанию для всех остальных:
Фрагмент Engine.cpp
...
//-----------------------------------------------------------------------------
// Handles Windows messages.
// Обработчик сообщений Windows
//-----------------------------------------------------------------------------
LRESULT CALLBACK WindowProc( HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam )
{
	switch( msg )
	{
		case WM_ACTIVATEAPP:
			g_engine->SetDeactiveFlag( !wparam );
			return 0;

		case WM_DESTROY:
			PostQuitMessage( 0 );
			return 0;

		default:
			return DefWindowProc( wnd, msg, wparam, lparam );
	}
}
...

Здесь мы используем ключевые слова switch...case, где для каждого из интересующих нас сообщений мы указываем нужные команды.
Когда движок получает сообщение WM_ACTIVATEAPP, мы переключаем флаг, используемый движком, для того, чтобы указывать, активно наше приложение или нет.
Закрыть
noteПереключение (flipping)

Переключением (flipping) в программировании называют однократное изменение (как правило булева) значения параметра на противоположеное. То есть, если флаг был установлен в TRUE ("1"), он устанавливается в FALSE ("0"). И наоборот.

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

Создаём экземпляр (инстанс) класса Engine

Двигаемся дальше по исходному коду файла Engine.cpp. Сразу после функции обработки сообщений, видим создание экземпляра класса Engine, путём вызова конструктора:
Фрагмент Engine.cpp
...
//-----------------------------------------------------------------------------
// The engine class constructor.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Indicate that the engine is not yet loaded.
	// Показываем, что движок пока не загружен.
	m_loaded = false;

	// If no setup structure was passed in, then create a default one.
	// Otehrwise, make a copy of the passed in structure.
	// Если структура EngineSetup не передана, то создаём одну.
	// Если есть, то делаем её копию.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Store a pointer to the engine in a global variable for easy access.
	// Сохраняем указатель на экземпляр движка в глобальную переменную.
	// Для более простого доступа к ней из любой части движка
	g_engine = this;

	// Prepare and register the window class.
	// Заполняем переменные члены оконного класса.
	WNDCLASSEX wcex;
	wcex.cbSize        = sizeof( WNDCLASSEX );
	wcex.style         = CS_CLASSDC;
	wcex.lpfnWndProc   = WindowProc;
	wcex.cbClsExtra    = 0;
	wcex.cbWndExtra    = 0;
	wcex.hInstance     = m_setup->instance;
	wcex.hIcon         = LoadIcon( NULL, IDI_APPLICATION );
	wcex.hCursor       = LoadCursor( NULL, IDC_ARROW );
	wcex.hbrBackground = NULL;
	wcex.lpszMenuName  = NULL;
	wcex.lpszClassName = "WindowClass";
	wcex.hIconSm       = LoadIcon( NULL, IDI_APPLICATION );
	RegisterClassEx( &wcex );

	// Initialise the COM using multithreaded concurrency.
	// Инициализируем COM в мультизадачном режиме.
	CoInitializeEx( NULL, COINIT_MULTITHREADED );

	// Create the window and retrieve a handle to it.
	// Note: Later the window will be created using a windowed/fullscreen flag.
	// Создаём окно.
	// Позднее окно будет создаваться с учётом флага оконного/полноэкранного режимов.
	m_window = CreateWindow( "WindowClass", m_setup->name, WS_OVERLAPPED, 0, 0, 800, 600, NULL, NULL, m_setup->instance, NULL );

	// Seed the random number generator with the current time.
	// Стартуем генератор сулчайных чисел на основе текущего времени.
	srand( timeGetTime() );

	// The engine is fully loaded and ready to go.
	// Движок полностью загружен и готов к работе.
	m_loaded = true;
}
...

Здесь сначала создаём экземпляр класса Engine, где в качестве параметра передаём имя структуры EngineSetup (где содержатся настройки движка):
Фрагмент Engine.cpp
...
Engine::Engine( EngineSetup *setup )
...

Далее распишем всё его содержимое.
Флаг m_loaded (по сути, это переменная типа BOOL, объявленная в Engine.h) установлен в FALSE, чтобы показать, что наш движок пока ещё не загружен.
Затем создаётся экземпляр структуры EngineSetup. В этом случае все её параметры установлены по умолчанию. В случае, когда в конструктор движка передаётся имя заранее подготовленной и настроенной структуры EngineSetup, её содержимое (параметры движка) копируются в этот новый экземпляр, заменяя параметры по умолчанию.
Выражение g_engine = this; используется для того, чтобы сделать наш движок глобальной переменной. Это нужно, чтобы движок был одинаково доступен для всех его компонентов.
Следующий шаг - заполнение и регистрация структуры оконного класса WNDCLASSEX, которая полностью описывает внешний вид и поведение окна приложения. (Да, наша будущая игра - это тоже оконное приложение!):
Фрагмент Engine.cpp
...
	// Prepare and register the window class.
	// Заполняем переменные члены оконного класса.
	WNDCLASSEX wcex;
	wcex.cbSize        = sizeof( WNDCLASSEX );
	wcex.style         = CS_CLASSDC;
	wcex.lpfnWndProc   = WindowProc;
	wcex.cbClsExtra    = 0;
	wcex.cbWndExtra    = 0;
	wcex.hInstance     = m_setup->instance;
	wcex.hIcon         = LoadIcon( NULL, IDI_APPLICATION );
	wcex.hCursor       = LoadCursor( NULL, IDC_ARROW );
	wcex.hbrBackground = NULL;
	wcex.lpszMenuName  = NULL;
	wcex.lpszClassName = "WindowClass";
	wcex.hIconSm       = LoadIcon( NULL, IDI_APPLICATION );
	RegisterClassEx( &wcex );
...

Все параметры расписывать не будем. Остановимся только на самых важных для нашего движка.
wcex - произвольное название. Но на протяжении всего курса именно так мы будем называть наш оконный класс.
Ты наверняка заметил, что в третьем параметре передаётся имя оконной процедуры WindowProc, которую мы обсуждали ранее. Это позволяет "подключить" к вновь создаваемому окну нашу уже готовую процедуру выборки сообщений, чтобы оно могло получать сообщения Windows и обрабатывать их (реагировать на них).
Мы также указали в параметре hInstance наш инстанс из структуры EngineSetup. Таким образом, окно будет "знать", к какому приложению оно принадлежит.
Ну, и ещё один параметр, заслуживающий внимания - это lpszClassName, который мы устанавливаем в WindowClass. Используется для ссылки на т.н. "базовый" оконный класс.
Сразу после заполнения структуры оконного класса, регистрируем её функцией RegisterClassEx. Теперь мы можем создавать окна на базе оконного класса wcex.
Фрагмент Engine.cpp
...
	// Initialise the COM using multithreaded concurrency.
	// Инициализируем COM в мультизадачном режиме.
	CoInitializeEx( NULL, COINIT_MULTITHREADED );
...

Мы используем функцию CoInitializeEx для инициализации библиотеки COM с использованием многопоточного взаимодействия. Позднее это очень пригодится. Ведь библиотека DirectX создана на базе COM и потому сильно привязана к этой технологии.
А теперь мы, всё-таки, создаём окно с помощью функции CreateWindowEx. Её прототип выглядит так:
Прототип функции CreateWindowEx
HWND CreateWindowEx
	(DWORD	dwExStyle,	// Расширенный (extended) стиль создаваемого окна.
	LPCTSTR	lpClassName,	// Имя зарегистрированного оконного класса, на создаётся окно
	LPCTSTR	lpWindowName,	// Имя окна. Отображаетмя на тайтлбаре окна. DWORD dwStyle, // Стиль окна
	int	x,	// Позиция окна по горизонтали
	int	y,	// Позиция окна по вертикали
	int	nWidth,	// Ширина окна
	int	nHeight,	// Высота окна
	HWND	hWndParent,	// Дескриптор родительского окна (если есть)
	HMENU	hMenu,	// Дескриптор меню окна (если есть)
	HINSTANCE	hInstance,	// Дескриптор экземпляра (инстанса) окна
	LPVOID	lpParam	// Дополнительные параметры окна
	);

Соотнеси все эти параметры с нашей функцией CreateWindowEx:
Фрагмент Engine.cpp
...
	// Create the window and retrieve a handle to it.
	// Note: Later the window will be created using a windowed/fullscreen flag.
	// Создаём окно.
	// Позднее окно будет создаваться с учётом флага оконного/полноэкранного режимов.
	m_window = CreateWindow( "WindowClass", m_setup->name, WS_OVERLAPPED, 0, 0,
		800, 600, NULL, NULL, m_setup->instance, NULL );
...

Функция возвращает дескриптор созданного окна, который мы сохраняем в m_window. Из параметров ясно, что создаваемое окно будет иметь размеры 800 на 600 точек, и стиль WS_OVERLAPPED (окно, перекрывающее все остальные окна).
Далее создаём генератор случайных чисел на основе текущего времени:
Фрагмент Engine.cpp
...
	// Seed the random number generator with the current time.
	// Стартуем генератор сулчайных чисел на основе текущего времени.
	srand( timeGetTime() );
...

Функция srand является одной из базовых в C++ и поддерживается практически всеми компиляторами этого языка программирования. Пригодится позднее.
По завершении всех вышеперечисленных операций выставляем флаг m_loaded в TRUE:
Фрагмент Engine.cpp
...
	// The engine is fully loaded and ready to go.
	// Движок полностью загружен и готов к работе.
	m_loaded = true;
...

Закрыть
noteСовет

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


Реализация функции Run(). Игровой цикл (Game Loop)

Как только наш движок был создан и полностью загружен, необходимо вызвать функцию Run(), которая вводит движок в непрекращающийся процессинговый цикл ( главный цикл игры^, game loop). Вот её реализация в Engine.cpp:
Фрагмент Engine.cpp
...
//-----------------------------------------------------------------------------
// Enters the engine into the main processing loop.
// Главный цикл обработки сообщений движка.
//-----------------------------------------------------------------------------
void Engine::Run()
{
	// Ensure the engine is loaded.
	// Проверяем, загружен ли движок.
	if( m_loaded == true )
	{
		// Show the window.
		// Показываем окно.
		ShowWindow( m_window, SW_NORMAL );

		// Enter the message loop.
		// Входим в цикл обработки сообщений.
		MSG msg;
		ZeroMemory( &msg, sizeof( MSG ) );
		while( msg.message != WM_QUIT )
		{
			if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
			{
				TranslateMessage( &msg );
				DispatchMessage( &msg );
			}
			else if( !m_deactive )
			{
				// Calculate the elapsed time.
				// Подсчитываем затраченное время.
				unsigned long currentTime = timeGetTime();
				static unsigned long lastTime = currentTime;
				float elapsed = ( currentTime - lastTime ) / 1000.0f;
				lastTime = currentTime;
			}
		}
	}

	// Destroy the engine.
	// Уничтожаем движок
	SAFE_DELETE( g_engine );
}
...

В первую очередь функция Run() проверяет (с импользованием оператора условного перехода if), загружен ли движок. Если да, то функция показывает окно (ShowWindow( m_window, SW_NORMAL );). Иначе вообще нет смысла показывать окно.

Обработка сообщений

Далее она подготавливает структуру MSG, которая используется для хранения подробной информации о сообщениях, которые будут посылаться в окно приложения.
Затем мы входим в цикл while, который прерывается при поступлении сообщения WM_QUIT. Это, собственно, и есть главный игровой цикл (game loop[j), где мы обрабатываем:
  • сообщения нашего окна
  • кадры нашего приложения.
Мы используем функцию PeekMessage, которая всё время проверяет, ожидает ли сообщение обработки окном нашего приложения. Если да, то обрабатываем сообщение двумя (встроенным в Windows API) функциями TranslateMessage и DispatchMessage, которые будут диспетчерезировать (= доставлять) сообщение в функцию обработки сообщений WindowProc окна нашего приложения.
В остальных случаях, когда нет сообщений, ожидающих обработки, мы проверяем, активно ли в данный момент наше приложение или нет. Если активно, то это тот самый случай, когда мы можем обработать ОДИН кадр нашего игрового приложения. Собственно, для чего всё и затевалось.

Подсчёт времени рендеринга одного кадра


Image
Рис.2 Игровой цикл


На данный момент наш движок занимается лишь тем, что подсчитывает затраченное (elapsed) время при переходе от одного кадра к другому, которое сохраняет в переменную elapsed (тип float). Другими словами, переменная elapsed будет хранить значение времени (на практике это доли секунд), которое прошло с того момента, когда был обработан последний кадр. Позже ты обнаружишь, что когда движок работает, он обрабатывает кадры так быстро, что это значение будет всегда меньше единицы.
Например, если движок обрабатывает 40 кадров в секунду (40 fps), ты обнаружишь, что в переменной elapsed будет храниться значение около 0,025 (т.е. 25 милисекунд; см. Рис 2).
По теме времени в игрокодинге на нашем сайте есть статья Время в игрокодинге.

Уничтожение экземпляра движка при завершении работы приложения

Последний шаг, который проделывает функция Run() - это вызов макроса SAFE_DELETE(g_engine), который безопасно уничтожает движок и очищает более неиспользуемую память. Этот макрос выполняется всего один раз, сразу после выхода из цикла while, что, в свою очередь, происходит лишь в случае, когда в движок поступает команда выхода из приложения и его закрытия. Только в этом случае есть смысл вызывать этот макрос..
При уничтожении движка вызывается деструктор класса Engine. Вот его реализация в Engine.cpp:
Фрагмент Engine.cpp
...
//-----------------------------------------------------------------------------
// The engine class destructor.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Ensure the engine is loaded.
	// Проверяем, загружен ли движок.
	if( m_loaded == true )
	{
		// Everything will be destroyed here (such as the DirectX components).
		// Здесь всё уничтожаем.
	}

	// Uninitialise the COM.
	CoUninitialize();

	// Unregister the window class.
	UnregisterClass( "WindowClass", m_setup->instance );

	// Destroy the engine setup structure.
	SAFE_DELETE( m_setup );
}
...

Деструктор в начале проверяет, а был ли до этого движок успешно загружен. Если да, то продолжаем уничтожать все компоненты, которые относятся к нашему движку. В настоящий момент у нас нет каких-либо компонентов, которые необходимо уничтожить. Но в следующих главах они обязательно появятся (например, объекты, связанные с выводом звука и поддержкой сетевых режимов игры). Сперва закрываем доступ к библиотеке COM, путём вызова функции CoUninitialize(). Затем удаляем ранее зарегистрированный оконный класс методом UnregisterClass().
И наконец, уничтожаем нашу структуру EngineSetup, отвечающую за тонкую настройку движка и подготовку его к работе.
Закрыть
noteПока движок пуст...

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


Делаем движок глобальным (global)

Один из наиболее важных аспектов нашего движка - это юзабилити (удобство использования). Вообще, вся суть того, что кто-либо использует готовый движок, заключается в том, что сделать это намного быстрее и проще, чем создавать "с нуля" свой собственный. Один из ключевых аспектов юзабилити - это доступность (accessibility), что в нашем случае означает простой и быстрый доступ к функционалу движка. Для того, чтобы достичь нашей цели (сделать движок юзабельным), мы сделаем наш движок настолько доступным, насколько это вообще возможно. Следуя ранее намеченому проекту, мы легко этого добьёмся.
При проектировании движка мы решили, что он должен иметь единую точку контакта. Затем мы предприняли ряд шагов, чтобы достичь этого. Например, создали класс Engine, который будет полностью инкапсулировать (содержать) в себе весь функционал движка. Теперь проблема заключается в том, что каждый компонент движка и игр, которые мы планируем создавать на его базе, должен иметь доступ ко всем компонентам класса Engine. Один из способов достичь этого - передать указатель на экземпляр класса Engine всем компонентам, которым он понадобится. Но у этого решения есть свои недостатки:
  1. Мы заранее знаем, что вобщем-то всем компонентам нужен доступ ко всему функционалу движка, либо к его части.
  2. Легко запутаться в том, какие функции движка использует тот или иной компонент. Это вызовет затруднения при внесении изменений в исходный код в будущем, что безусловно приведёт к снижению "ремонтопригодности" (пригодности к обслуживанию) движка.
  3. Исходный код становится неорганизованным и запутанным, что может привести к проблемам (например к появлению т.н. "нулевых указателей" (NULL POINTERS)).
Но есть способ лучше: сделать указатель на экземпляр класса Engine глобальным ( global Не лучшая идея?
Многие программеры считают использование глобальных указателей и переменных плохой идеей. Они правы, т.к.:
  • Легко потерять след переменной и область её видимости.
  • Они могут быть непреднамеренно разрушены или изменены из-за логических ошибок.
  • При чрезмерном использовании это нарушает фундаментальные принципы объектно-ориентированного программирования.
В то же время, все эти "оплошности" происходят, как правило, из-за невнимательности программера (т.е. из-за человеческого фактора). Если мы решили (а мы решили)) использовать глобальные указатели, то должны это делать с осторожностью, помня о "подводных камнях" этого метода. Если ты используешь глобальные указатели и переменные лишь время от времени и делаешь это правильно, то проблем не будет.
Помня об этом, мы будем использовать глобальную переменную в качестве указателя на экземпляр класса Engine. Благодаря этому, каждый компонент нашего движка и будущих игр бует иметь доступ к этому указателю и уже через него - ко всему функционалу движка. Ты, должно быть, заметил следующую строку кода в начале файла Engine.cpp:
Фрагмент Engine.cpp
...
//-----------------------------------------------------------------------------
// Globals
//-----------------------------------------------------------------------------
Engine *g_engine = NULL;
...

Всего одна эта строка кода определяет наш глобальный указатель на экземпляр класса Engine. Эта переменная создаётся сразу же перед выполнением и ей присваивается значение NULL, что означает, что движок пока ещё не создан. Сейчас у нас есть этот пустой указатель, но для того, чтобы он стал глобальным, необходимо добавить ещё одну строку кода, на этот раз уже в заголовочный файл Engine.h (проверь её наличие):
Фрагмент Engine.h
...
//-----------------------------------------------------------------------------
// Externals
// Глобальные объявления
//-----------------------------------------------------------------------------
extern Engine *g_engine;
...

Используя ключевое слово extern, мы определяем нашу переменную (переменный указатель) как external (от англ. "внешний"), то есть делаем её глобальной. С этого момента наш переменный указатель становится глобальным и доступен для любого файла исходного кода или заголовка, которые содержат выражение include Engine.h .
Осталось выполнить всего 2 задачи:
  • присвоить значение глобальной переменной;
  • уничтожить её (в нужный момент).
Мы можем оставить выполнение этих задач на откуп игрокодерам, которые будут применять наш движок. Но это не соответствует нашим представлениям о юзабилити. Мы должны сделать так, чтобы движок выполнял эти задачи самостоятельно. К счастью, сделать это даже проще, чем создать глобальную переменную. Для установки указателя мы добавляем всего 1 строку в начало конструктора класса Engine в Engine.cpp (проверь её наличие):
Фрагмент Engine.cpp
...
	// Store a pointer to the engine in a global variable for easy access.
	// Сохраняем указатель на экземпляр движка в глобальную переменную.
	// Для более простого доступа к ней из любой части движка
	g_engine = this;
...

Ключевое слово this само по себе является указателем на экземпляр класса, в котором оно размещено. Таким образом, в нашем случае this является указателем на экземляр класса Engine, который создаётся при вызове его (экземпляра класса) конструктора. Присвоив наш глобальный указатель указателю this, мы говорим ему указывать на адрес в памяти, где хранится вновь созданный экземпляр класса Engine.
Финальный шаг - освободить память, на которую указывает этот указатель, когда мы закончим использовать экземпляр класса Engine. Мы проделываем это, добавив всего 1 строку в конце реализации функции Run() в файле Engine.cpp (проверь её наличие):
Фрагмент Engine.cpp
...
	// Destroy the engine.
	// Уничтожаем движок
	SAFE_DELETE( g_engine );
...

Здесь мы применяем первый из трёх макросов, определённых в Engine.h, для безопасного удаления единичных объектов и освобождения памяти, более неиспользуемой ими.
Вот мы и сделали наш движок глобальным. По началу весь процесс может показаться несколько сложным, но использование данного глобального указателя - очень важная часть нашего движка, которую не стоит упускать из виду. Если ты так и не понял, что и для чего, перечитай эту главу заново + обрати внимание на подробные комментарии в исходном коде.

Источники


1. Young V. Programming a Multiplayer FPS in DirectX 9.0. - Charles River Media, 2005


ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.3 Добавляем поддержку связных списков (LinkedList.h)

Последние изменения страницы Суббота 09 / Июль, 2022 03:14:59 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++