Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.2 Создаём фреймворк движка
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

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


Первая, по-настоящему практическая глава. Здесь мы впервые оставим сухую теорию и займёмся настоящим игрокодингом!
В Главе 1.1, где проектировался наш будущий движок, мы неоднократно отмечали, что ему необходимо иметь так называемую "единую точку контакта".
Понятие "единая точка контакта" определяется как возможность игрокодера включить движок в игру через одно выражение #include, одну библиотеку LIB и один главный (main) класс, экземпляр которого должен быть инициирован. Более того, пользователь должен иметь доступ к движку через одну глобальную переменную(external link) (global variable).

К счастью, сделать это очень просто. Для этого достаточно:

  • связать все исходные и заголовочные файлы в проекте Engine, дав ссылку на них (через выражение #include) в одном общем заголовочном файле, назвав его, например, Engine.h;
  • создать всего один единственный класс Engine, который используется для доступа ко всему функционалу движка;
  • создать внешний глобальный указатель (external global pointer). Назовём его g_engine. Он будет доступен везде: как в пределах проекта Engine, так и при создании игрового приложения. Мы разрешим движку самостоятельно устанавливать этот указатель всякий раз, когда будет объявляться экземпляр класса Engine, освобождая игрокодера от выполнения этой задачи.


ОК, приступаем.

Создаём пустой Проект и настраиваем его


Первый шаг включает в себя создание Проекта движка, его первоначальная настрока и подключение заголовочных файлов MS DirectX SDK.

  • Запускай MS Visual Studio 2010.

Видим Окно приветствия (т.е. в IDE ни один Проект пока не открыт).

  • Создай новый пустой Проект Win32. (В Главном меню: Файл->Создать->Проект...).

В окне "Создать Проект":

  • Отмечаем щелчком пункт "Проект Win32"

Строки "Расположение" и "Имя Решения" заполняются автоматически (при необходимости изменяем).
Под Проект GameProject01 будет автоматически создано Решение с тем же именем.
Image

  • Жмём ОК.

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

  • На странице "Параметры приложения" отмечаем пункт "Статическая библиотека" и убираем галку у пункта "Предварительно скомпилированный заголовок":

Image

  • Жмём "Готово".

При создании наш Проект автоматически размещается внутри Решения с тем же именем. В результате в левой колонке обозревателя решений видим Решение GameProject01, которое содержит Проект GameProject01.
Это неправильно, так как согласно замысла, внутри Решения GameProject01 будут содержаться два Проекта: Engine (движок; статическая библиотека с расширением .lib) и Game (игра на базе этого движка; файл приложения с расширением .exe). Напомним, что в первой части курса мы разрабатываем движок.

  • Поэтому смени имя вновь созданного Проекта с GameProject01 на Engine (правой кнопкой мыши по названию Проекта -> из контекстного меню выбрать "Переименовать").
Переименовываем Проект GameProject01 в Engine
Переименовываем Проект GameProject01 в Engine

  • Скачай и установи последнюю (полную) версию DirectX SDK (его нетрудно найти в гугле или Яндексе).
  • Обязательно настрой в MS Visual Studio 2010 пути к DirectX SDK. (Проект->Свойства->Свойства конфигурации->Каталоги VC++... А вообще, весь процесс подробно расписан здесь: Настройка MS Visual C plus plus 2010 и DirectX SDK.)
  • Сохрани Решение (Файл->Сохранить все).


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

Закрыть
noteПримечание

В MS Visual C++ 2010 в настройках по умолчанию стоит набор (кодировка) символов UNICODE. В MS Visual C++ 6.0 - напротив, по умолчанию стоит кодировка ANSI (многобайтовая). Данная настройка сильно влияет на типы используемых переменных, что приводит к заметным различиям в исходном коде.
Несмотря на то, что во всех случаях рекомендуется использовать кодировку UNICODE, поддерживаемую во всех современных ОС семейства MS Windows (начиная с Win 2000/XP), большинство книг по программированию игр на классическом C++ придерживаются именно многобайтовой кодировки. Чтобы сильно не переделывать исходные коды под UNICODE, все наши игровые Проекты мы настроим под многобайтовую кодировку. Для этого...

  • В Обозревателе решений щёлкаем по названию только что созданного Проекта Engine.
  • Во всплывающем меню выбираем "Свойства"
  • В появившемся окне установки свойств Проекта жмём "Свойства конфигурации", в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все)

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

Создаём Engine.h

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

  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы".
  • Во всплывающем меню Добавить->Создать элемент...

Image

  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Engine.h".
  • Жмём "Добавить".

Image
Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле Engine.h набираем следующий код:
Engine.h
//-----------------------------------------------------------------------------
// Главный заголовочный файл движка. Этот файл соединяет движок воедино,
// и является единственным заголовочным файлом, который следует добавлять в проекты,
// создаваемые на базе данного движка.
//
//	Original Source code:
//  Programming a Multiplayer First Person Shooter in DirectX
//  Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef ENGINE_H
#define ENGINE_H

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

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

//-----------------------------------------------------------------------------
// DirectX Includes
//-----------------------------------------------------------------------------
#include <d3dx9.h>
#include <dinput.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; } }

//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"

//-----------------------------------------------------------------------------
// Engine Setup Structure
//-----------------------------------------------------------------------------
struct EngineSetup
{
	HINSTANCE instance; // Дескриптор экземпляра приложения.
	char *name; // Название приложения.

	//-------------------------------------------------------------------------
	// 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; // Флаг показывает, загружен ли движок или нет.
	HWND m_window; // Дескриптор главного окна.
	bool m_deactive; // Флаг показывает, активно приложение или нет.

	EngineSetup *m_setup; // Копия (инстанс) структуры EngineSetup.
};

//-----------------------------------------------------------------------------
// Externals
//-----------------------------------------------------------------------------
extern Engine *g_engine;

#endif

  • Сохрани созданное Решение и входяшие в него Проекты (Файл -> Сохранить все).


Обрати внимание на комментарии. Профессиональные программеры всегда их оставляют в разных участках кода. Не будем и мы нарушать традицию. Напоминаем, что при компиляции Release-версии проекта все комменарии автоматически отбрасываются (и потому никак не сказываются на объёме конечного файла приложения или DLL-библиотеки).

Закрыть
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
...
//-----------------------------------
// Определяет (утверждает) версию DirectInput Version to 8.0 (Функционала 8 версии будет предостаточно)
//-----------------------------------
#define DIRECTINPUT_VERSION 0x0800

//-----------------------------------------------------------------------------
// 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
//-----------------------------------------------------------------------------
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
...

Заголовки LinkedList.h, ResourceManagement.h и Geometry.h мы самостоятельно создадим чуть позднее. Все эти файлы будут являться компонентами нашего движка. Мы создадим их буквально "с нуля" и обязательно разберёмся, что в них содержится и для чего они нужны. Вот их краткое описание:

Заголовочный файл Описание
LinkedList.h Подключает реализацию механизма обработки данных Linked List(external link). В программировании он давно известен и применяется повсеместно. Подробнее см. Главу 1.3 .
ResourceManagement.h Включает поддержку менеджеров ресурсов (в нашем случае это будут изображения, 3D-сцены, звуки и т.д.). Напрямую основан на механизме LinkedList. Подробнее см. Главу 1.4 .
Geometry.h Содержит объявление и реализацию функций и классов для работы с простейшими геометрическими фигурами (примитивами). Активно использует интерфейсы DirectX. Подробнее см. Главу 1.5 .

Обрати внимание, что заголовок LinkedList.h расположен выше заголовка ResourceManagement.h. Это важно, так как наш будущий менеджер ресурсов будет основан именно технологии Linked list (англ. - "свзязный список"), код которой содержится в LinkedList.h и должен быть скомпилирован раньше чем код из ResourceManagement.h. Компилятор "просматривает" листинг строка за строкой сверху вниз. И если в данном случае поменять местами LinkedList.h и ResourceManagement.h, возникнет ошибка, так как в ResourceManagement.h содержится реализация связны списков (Linked list), о котором компилятор ничего (на тот момент) не знает.

Закрыть
noteОбрати внимание:

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


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

SomeFile.cpp
#include "Engine.h"

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

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

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

Прошли те дни, когда объём памяти компьютера был крайне ограничен. В принципе и тогда создавались отличные работоспособные приложения. А всё потому, что талантливые программисты отлично владели приёмами, предотвращающими т.н. "утечки памяти"(external link) (от англ. "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; // Дескриптор экземпляра приложения.
	char *name; // Название приложения.

	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		instance = NULL;
		name = "Application";	// Название игрового приложения
	}
};
...

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

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

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

Рис 1. Класс Engine в качестве единой точки контакта
Рис 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; // Флаг показывает, загружен ли движок или нет.
	HWND m_window; // Дескриптор главного окна.
	bool m_deactive; // Флаг показывает, активно приложение или нет.

	EngineSetup *m_setup; // Копия (инстанс) структуры EngineSetup.
};
...


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

Сейчас в нём содержится объявление 5 функций:

Функция класса Engine Описание
Engine( EngineSetup *setup = NULL ) Это, конечно же, конструктор класса Engine. В качестве параметра передаётся указатель на структуру EngineSetup, предварительно заполненную всевозможными опциями, отвечающими за всю последующую работу движка.
~Engine( ) Деструктор класса.
Run( ) Запускает движок после предварительной инициализации.
GetWindow( ) Возвращает дескриптор текущего окна приложения движка.
SetDeactiveFlag( bool deactive ) Служебная функция, которая устанавливает/отключает флаг неактивности. Позднее очень пригодится.


Далее видим, что в классе Engine объявлены 4 переменных члена:

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

Создаём Engine.cpp

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

  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
Добавляем исходный файл
Добавляем исходный файл
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "Engine.cpp".
  • Жмём "Добавить".

Image
Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле Engine.cpp набираем следующий код:
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"

//-----------------------------------------------------------------------------
// Глобальные переменные и константы
//-----------------------------------------------------------------------------
Engine *g_engine = NULL;

//-----------------------------------------------------------------------------
// Обработка сообщений 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 );
	}
}

//-----------------------------------------------------------------------------
// Конструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Указываем, что движок ещё не загружен.
	m_loaded = false;

	// Если никакой структуры EngineSetup не передаётся, обязательно создадим её.
	// Иначе, копируем имя передаваемой структуры.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Сохраняем указатель движка в глобальной переменной для более простого и быстрого доступа.
	g_engine = this;

	// Подготавливаем оконный класс и регистрируем его.
	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 );

	// Инициализируем COM используя многопоточное взаимодействие.
	CoInitializeEx( NULL, COINIT_MULTITHREADED );

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

	// Создаём генератор случайных чисел на основе текущего времени.
	srand( timeGetTime() );

	// Движок полностью загружен и готов к работе.
	m_loaded = true;
}

//-----------------------------------------------------------------------------
// Деструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Проверяем, что движок загружен.
	if( m_loaded == true )
	{
		// Всё, что здесь, будет уничтожаться (например, более неиспользуемые DirectX компоненты).
	}

	// Деинициализация COM.
	CoUninitialize();

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

	// Уничтожение структуры engine setup.
	SAFE_DELETE( m_setup );
}

//-----------------------------------------------------------------------------
// Вводит движок в главный цикл выборки сообщений.
//-----------------------------------------------------------------------------
void Engine::Run()
{
	// Убеждаемся, что движок загружен.
	if( m_loaded == true )
	{
		// Показываем окно.
		ShowWindow( m_window, SW_NORMAL );

		// Входим в цикл выборки сообщений.
		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 )
			{
				// Подсчитываем затраченное время.
				unsigned long currentTime = timeGetTime();
				static unsigned long lastTime = currentTime;
				float elapsed = ( currentTime - lastTime ) / 1000.0f;
				lastTime = currentTime;
			}
		}
	}

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

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

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

  • Сохрани Решение (Файл -> Сохранить все).


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

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


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

Фрагмент файла Engine.cpp
...
#include "Engine.h"
...


Далее видим внешний глобальный указатель (external global pointer). Назовём его g_engine:

Фрагмент файла Engine.cpp
...
//-----------------------------------------------------------------------------
// Глобальные переменные и константы
//-----------------------------------------------------------------------------
Engine *g_engine = NULL;
...

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

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

Любое приложение, которое запускается на твоём компьютере обязано отвечать на запросы операционной системы. Другими словами, приложение не может просто "повесить" компьютер, игнорируя запросы от ОС. По этой причине любое оконное приложение Windows обязательно должно содержать т.н. функцию обратного вызова(external link) "оконной процедуры" (window procedure call-back function).

Закрыть
noteФункция обратного вызова

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


В нашем случае назовём её WindowProc. Вот её прототип:

Прототип функции WindowProc
LRESULT CALLBACK WindowProc(HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam)


Тот факт, что мы определили функцию WindowProc в нашем приложении совсем не означает, что мы будем её вызывать. Более того, мы вообще никогда не будем её вызывать. Вместо этого, данная функция (обратного вызова) автоматически вызывается всякий раз, когда наше приложение получает сообщение от ОС MS Windows. Причём даже в том случае, когда такое сообщение сгенерировано нашим приложением. А когда вызывается, она начинает обрабатывать все сообщения, которые MS Windows посылает нашему приложению. Чтобы помочь тебе, MS Windows передаёт через вызов функции WindowProc четыре параметра:

Параметр функции 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
...
//-----------------------------------------------------------------------------
// Обработка сообщений 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
...
//-----------------------------------------------------------------------------
// Конструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Указываем, что движок ещё не загружен.
	m_loaded = false;

	// Если никакой структуры EngineSetup не передаётся, обязательно создадим её.
	// Иначе, копируем имя передаваемой структуры.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Сохраняем указатель движка в глобальной переменной для более простого и быстрого доступа.
	g_engine = this;

	// Подготавливаем оконный класс и регистрируем его.
	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 );

	// Инициализируем COM используя многопоточное взаимодействие.
	CoInitializeEx( NULL, COINIT_MULTITHREADED );

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

	// Создаём генератор случайных чисел на основе текущего времени.
	srand( timeGetTime() );

	// Движок полностью загружен и готов к работе.
	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
...
	// Подготавливаем оконный класс и регистрируем его.
	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
...
		// Инициализируем 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: Позднее окно будет создаваться с флагом windowed/fullscreen (оконный/полноэкранный режимы).
	m_window = CreateWindowEx( WS_EX_TOPMOST, L"WindowClass", m_setup->name, WS_OVERLAPPED, 0, 0, 800, 600, NULL, NULL, m_setup->instance, NULL );
...

Функция возвращает дескриптор созданного окна, который мы сохраняем в m_window. Из параметров ясно, что создаваемое окно будет иметь размеры 800 на 600 точек, и стиль WS_OVERLAPPED (окно, перекрывающее все остальные окна).

Далее создаём генератор случайных чисел на основе текущего времени:

Фрагмент файла Engine.cpp
...
	// Создаём генератор случайных чисел на основе текущего времени.
	srand( timeGetTime() );
...

Функция srand является одной из базовых в C++ и поддерживается практически всеми компиляторами этого языка программирования. Пригодится позднее.

По завершении всех вышеперечисленных операци выставляем флаг m_loaded в TRUE:

Фрагмент файла Engine.cpp
...
	// Движок полностью загружен и готов к работе.
	m_loaded = true;
...

Закрыть

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

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

Как только наш движок был создан и полностью загружен, необходимо вызвать функцию Run(), которая вводит движок в непрекращающийся процессинговый цикл ( главный цикл игры(external link), game loop). Вот её реализация в Engine.cpp:

Фрагмент файла Engine.cpp
...
//-----------------------------------------------------------------------------
// Вводит движок в главный цикл выборки сообщений.
//-----------------------------------------------------------------------------
void Engine::Run()
{
	// Убеждаемся, что движок загружен.
	if( m_loaded == true )
	{
		// Показываем окно.
		ShowWindow( m_window, SW_NORMAL );

		// Входим в цикл выборки сообщений.
		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 )
			{
				// Подсчитываем затраченное время.
				unsigned long currentTime = timeGetTime();
				static unsigned long lastTime = currentTime;
				float elapsed = ( currentTime - lastTime ) / 1000.0f;
				lastTime = currentTime;
			}
		}
	}

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

В первую очередь функция Run() проверяет (с импользованием оператора условного перехода if), загружен ли движок. Если да, то функция показывает окно (ShowWindow( m_window, SW_NORMAL );). Иначе вообще нет смысла показывать окно.
Далее она подготавливает структуру MSG, которая используется для хранения подробной информации о сообщениях, которые будут посылаться в окно приложения.
Затем мы входим в цикл while, который прерывается при поступлении сообщения WM_QUIT. Это, собственно, и есть главный игровой цикл ( game loop(external link)), где мы обрабатываем:

  • сообщения нашего окна
  • кадры нашего приложения.

Мы используем функцию PeekMessage, которая всё время проверяет, ожидает ли сообщение обработки окном нашего приложения. Если да, то обрабатываем сообщение двумя (встроенным в Windows API) функциями TranslateMessage и DispatchMessage, которые будут диспетчерезировать (= доставлять) сообщение в функцию обработки сообщений WindowProc окна нашего приложения.

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

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

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

При уничтожении движка вызывается деструктор класса Engine. Вот его реализация в Engine.cpp:

Фрагмент файла Engine.cpp
...
//-----------------------------------------------------------------------------
// Деструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Проверяем, что движок загружен.
	if( m_loaded == true )
	{
		// Всё, что здесь, будет уничтожаться (например, более неиспользуемые DirectX компоненты).
	}

	// Деинициализация COM.
	CoUninitialize();

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

	// Уничтожение структуры engine setup.
	SAFE_DELETE( m_setup );
}
...

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

Закрыть
noteПока движок пуст...

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

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

Один из наиболее важных аспектов нашего движка - это юзабилити (удобство использования). Вообще, вся суть того, что кто-либо использует готовый движок, заключается в том, что сделать это намного быстрее и проще, чем создавать "с нуля" свой собственный. Один из ключевых аспектов юзабилити - это доступность (accessibility), что в нашем случае означает простой и быстрый доступ к функционалу движка. Для того, чтобы достичь нашей цели (сделать движок юзабельным), мы сделаем наш движок настолько доступным, насколько это вообще возможно. Следуя ранее намеченому проекту, мы легко этого добьёмся.
При проектировании движка мы решили, что он должен иметь единую точку контакта. Затем мы предприняли ряд шагов, чтобы достичь этого. Например, создали класс Engine, который будет полностью инкапсулировать (содержать) в себе весь функционал движка. Теперь проблема заключается в том, что каждый компонент движка и игр, которые мы планируем создавать на его базе, должен иметь доступ ко всем компонентам класса Engine. Один из способов достичь этого - передать указатель на экземпляр класса Engine всем компонентам, которым он понадобится. Но у этого решения есть свои недостатки:

  1. Мы заранее знаем, что вобщем-то всем компонентам нужен доступ ко всему функционалу движка, либо к его части.
  2. Легко запутаться в том, какие функции движка использует тот или иной компонент. Это вызовет затруднения при внесении изменений в исходный код в будущем, что безусловно приведёт к снижению "ремонтопригодности" (пригодности к обслуживанию) движка.
  3. Исходный код становится неорганизованным и запутанным, что может привести к проблемам (например к появлению т.н. "нулевых указателей" (NULL POINTERS)).


Но есть способ лучше: сделать указатель на экземпляр класса Engine глобальным ( global(external link)).

Закрыть
noteНе лучшая идея?

Многие программеры считают использование глобальных указателей и переменных плохой идеей. Они правы, т.к.:
- Легко потерять след переменной и область её видимости.
- Они могут быть непреднамеренно разрушены или изменены из-за логических ошибок.
- При чрезмерном использовании это нарушает фундаментальные принципы объектно-ориентированного программирования.
В то же время, все эти "оплошности" происходят, как правило, из-за невнимательности программера (т.е. из-за человеческого фактора). Если мы решили (а мы решили)) использовать глобальные указатели, то должны это делать с осторожностью, помня о "подводных камнях" этого метода. Если ты используешь глобальные указатели и переменные лишь время от времени и делаешь это правильно, то проблем не будет.


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

Фрагмент файла Engine.cpp
...
//-----------------------------------------------------------------------------
// Глобальные переменные и константы
//-----------------------------------------------------------------------------
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
...
//-----------------------------------------------------------------------------
// Конструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Указываем, что движок ещё не загружен.
	m_loaded = false;

	// Если никакой структуры EngineSetup не передаётся, обязательно создадим её.
	// Иначе, копируем имя передаваемой структуры.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Сохраняем указатель движка в глобальной переменной для более простого и быстрого доступа.
	g_engine = this;
...

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

Финальный шаг - освободить память, на которую указывает этот указатель, когда мы закончим использовать экземпляр класса Engine. Мы проделываем это, добавив всего 1 строку в конце реализации функции Run() в файле Engine.cpp (проверь её наличие):

Фрагмент файла Engine.cpp
...
	// Уничтожаем движок.
	SAFE_DELETE( g_engine );
...

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

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


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.2 Создаём фреймворк движка

Contributors to this page: slymentat .
Последнее изменение страницы Вторник 29 / Ноябрь, 2016 14:03:59 MSK автор slymentat.

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

No records to display

Хостинг