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

1.7 Добавляем систему стейтов (State system)


Рис. 1 Сейчас Проект нашего движка выглядит так
Рис. 1 Сейчас Проект нашего движка выглядит так

Напомним, что система стейтов нужна для рационального расходования системных ресурсов во время подготовки кадра к прорисовке и выводу на экран. Само понятие стейт (от англ. state - состояние) является сокращением от словосочетания "состояние операции" (state of operation), которое означает текущий процесс данного приложения, отправленный на выполнение. Главное меню любой игры - это стейт, игровой процесс - это тоже стейт. Даже показ окна инвентори (inventory; содержимое карманов или ручной клади главного героя).
При создании движка мы будем применять т.н. программирование, основанное на стейтах (state-based programming; SBP). Если коротко, то SBP ветвит (перенаправляет) выполнение, основываясь на стеке стейтов (stack of states). Каждый стейт представляет собой объект или набор функций. Если в данном стейте необходимы определённые функции, то их можно добавить в стек, а затем при необходимости удалить из него. Мы будем добавлять, удалять и обрабатывать стейты с помощью менеджера стейтов (state manager). При добавлении стейта он добавляется в стек и ждёт своей очереди на обработку. Именно для этого мы разработаем менеджер стейтов.

Сейчас в Проекте Engine нашего движка всего 5 файлов: Engine.h, Engine.cpp, LinkedList.h, ResourceManagement.h и Geometry.h, которые мы создали в предыдущих главах (см. Рис. 1). Чуть ниже есть второй Проект Test. Его пока не трогаем!

Создаём State.h (Проект Engine)

В заголовочном файле State.h внутри класса State представлены объявления функций для работы со стейтами.
ОК, приступаем.

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

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

  • В только что созданном и открытом файле State.h набираем следующий код:

State.h (Проект Engine)
//-----------------------------------------------------------------------------
// Обеспечивает базовый функционал системы стейтов. Приложения
// должны создавать дочерние классы от класса State для добавления
// этого функционала.
//
// Original sourcecode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef STATE_H
#define STATE_H

//-----------------------------------------------------------------------------
// Viewer Setup Structure (структура установок вирт. просмотрщика)
//-----------------------------------------------------------------------------
struct ViewerSetup
{
};

//-----------------------------------------------------------------------------
// State Class
//-----------------------------------------------------------------------------
class State
{
public:
	State( unsigned long id = 0 );

	virtual void Load();
	virtual void Close();

	virtual void RequestViewer( ViewerSetup *viewer );
	virtual void Update( float elapsed );
	virtual void Render();

	unsigned long GetID();

private:
	unsigned long m_id; // ID, назначаемое приложением (должно быть уникальным для переключения стейтов).
};

#endif

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

Viewer Setup Structure (структура установок просмотрщика)

(В русскоязычном переводе это название звучит коряво, так что далее в тексте пользуемся общепринятым англоязычным аналогом "Viewer Setup Structure".) В самом начале State.h видим объявление структуры ViewerSetup. Она будет использоваться системой стейтов, так что рассказать о ней необходимо именно сейчас.
Эта, на данный момент совершенно пустая, структура служит своеобразным виртуальным окном в игровое пространство. Её также можно сравнить с глазком видоискателя виртуальной камеры, в которой видно всё, что попадает в (опять же, виртуальный) объектив. В двух словах, Viewer Setup Structure определяет, что будет показываться на экран в каждом из стейтов, а что нет. Она используется для определения параметров, используемых движком для обозначения того, что именно будет показываться на экране монитора в данном кадре. Например, движку необходимо знать, где именно в игровом мире находится игрок (т.е. его виртуальный персонаж) и в каком направлении он (точнее, его лицо) повёрнут. Только после этого он просчитает, что необходимо отрендерить на экране в каждом кадре. Задача программера как раз и заключается в информировании движка об этих деталях посредством структуры ViewerSetup.
Сейчас структура ViewerSetup нам не нужна, поэтому она пока пуста. Но, подобно структуре EngineSetup, она также будет со временем разрастаться по мере добавления в неё новых записей.
В данный момент наш движок пока не способен что-либо рендерить, так что пока не будем об этом беспокоиться. Но чуть позднее, при создании системы рендеринга, без данной структуры нам просто не обойтись. Вьюер вступает в дело именно при рендеринге, в момент выполнения движка, т.е. запуска функции Run().
Поэтому разместим инстанс структуры ViewerSetup в функции Run() класса Engine из файла Engine.cpp:

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

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

		// Используется для получения настроек вьюера от приложения.
		ViewerSetup viewer;

		// Входим в цикл выборки сообщений.
		MSG msg;
		ZeroMemory( &msg, sizeof( MSG ) );
...

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

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

Первое, что бросается в глаза в объявлении класса State - это обилие виртуальных функций:

Фрагмент State.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// State Class
//-----------------------------------------------------------------------------
class State
{
public:
	State( unsigned long id = 0 );

	virtual void Load();
	virtual void Close();

	virtual void RequestViewer( ViewerSetup *viewer );
	virtual void Update( float elapsed );
	virtual void Render();

	unsigned long GetID();

private:
	unsigned long m_id; // ID, назначаемое приложением (должно быть уникальным для переключения стейтов).
};
...

Для большей наглядности создадим State.cpp.

Создаём State.cpp (Проект Engine)

В заголовочном файле State.cpp содержатся реализации (вернее, своеобразные "заготовки") функций для работы со стейтами, объявленных в State.h.
ОК, приступаем.

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

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

  • В только что созданном и открытом файле State.cpp набираем следующий код:
State.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация функций, объявленных в State.h.
// Refer to the State.h interface for more details.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// The state class constructor.
//-----------------------------------------------------------------------------
State::State( unsigned long id )
{
	m_id = id;
}

//-----------------------------------------------------------------------------
// Позволяет стейту выполнить какие-либо предварительные (pre-processing) операции.
//-----------------------------------------------------------------------------
void State::Load()
{

}

//-----------------------------------------------------------------------------
// Позволяет стейту выполнить какие-либо post-processing операции (в момент уничтожения стейта).
//-----------------------------------------------------------------------------
void State::Close()
{

}

//-----------------------------------------------------------------------------
// Возвращает параметры настройки вида (viewer) для данного кадра.
//-----------------------------------------------------------------------------
void State::RequestViewer( ViewerSetup *viewer )
{

}

//-----------------------------------------------------------------------------
// Обновляет стейт.
//-----------------------------------------------------------------------------
void State::Update( float elapsed )
{

}

//-----------------------------------------------------------------------------
// Рендерит Стейт.
//-----------------------------------------------------------------------------
void State::Render()
{

}

//-----------------------------------------------------------------------------
// Возвращает ID стейта.
//-----------------------------------------------------------------------------
unsigned long State::GetID()
{
	return m_id;
}

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


Глядя на эти пустые реализации функций, понимаешь, что в данный момент класс State (объявленный в State.h), в общем-то, не делает ничего. На данном этапе это нормально, т.к. позднее эти функции будут многократно расширены и дополнены. Это нечто вроде псевдо-шаблонного класса, от которого ты также можешь создавать инстансы. Причём он изначально создан для того, чтобы быть игрокодер переопределял его при написании своих собственных классов State, создав также свою собственную реализацию каждой из представленных виртуальных функций. Мы снабдили базовый класс State всеми этими пустыми виртуальными функциями для того, чтобы система стейтов нашего движка имела чётко структурированный интерфейс, с которым можно немедленно начинать работать. В конечно счёте у игрокодера есть неизменный базовый класс State, к которому можно обращаться вновь и вновь, и который сохранит систему стейтов в целостном виде.
Быстро пробежимся по каждой из функций в State.cpp.
В начале листинга видим конструктор, который используется для выполнения единожды инициализируемых членов системы стейтов (например, их установка в NULL). Конструктор также используется для установки идентификационного номера стейта. Каждый стейт, который ты создаёшь, должен иметь уникальный идентификационный номер, который ты передаёшь стейту именно через конструктор. Затем он сохраняется в m_id и может быть запрошен с помощью функции GetID. В качестве идентификатора стейта может выступать любое число, которое нетрудно запомнить, т.к. он используется для ссылки на стейт. Всякий раз, когда ты захочешь переключиться на новый стейт, движок запросит у тебя его идентификационный номер. Ты также можешь назначать идентификационный номер стейта через ключевое слово #define. Это позволит тебе обращаться к стейту, используя вместо абстрактного номера более "говорящее" имя (псевдоним).
Ты должно быть заметил отсутствие деструктора. Причина этого в том, что всё для стейта должно загружаться и закрываться через соответствующие функции Load и Close. Более того, тебе даже не придётся самостоятельно вызывать эти функции, т.к. они вызываются движком. Всё, что тебе необходимо сделать, это написать код загрузки и закрытия стейта (при необходимости) в своём дочернем классе. Тебе также никогда не придётся вызывать какие-либо виртуальные функции в твоём дочернем классе. Все они также вызываются движком. Всё, что тебе нужно сделать, это написать реализации функций, которые тебе интересно обрабатывать. Например, функция RequestViewer() вызывается движком, поэтому ты можешь заполнить структуру вьюера (ViewerSetup), которая передаётся при этом движком. Именно таким способом движок получит информацию, которая ему нужна для рендеринга текущего кадра. Если ты создаёшь стейт, который не использует никакие возможности рендеринга, тогда тебе даже не нужно писать реализацию функции RequestViewer() в своём дочернем классе. Вместо этого, ты можешь просто позволить движку использовать пустую функцию из базового класса State (в State.h).
Функции Update и Render также будут вызываться движком для того, чтобы обновить данный стейт и выполнить для него необходимый рендеринг. Когда вызывается функция Update, движок передаёт время, затраченное на обновление предыдущего кадра. Это значение сохраняется в переменной с плавающей точкой elapsed и затем ты можешь его использовать для собственных нужд (например, для тайминга или интерполяции во времени). Мы спроектируем наш движок таким образом, чтобы многие функции рендеринга он выполнял автоматически. В то же время, у нас есть функция Render(), которая позволит тебе создать свой собственный (специализированный) рендеринг, не поддерживаемый движком.

Закрыть
noteВажно помнить!

Функция Render() класса State вызывается ПОСЛЕ выполнения движком его собственного рендеринга. Часто это не играет никакой роли. Но, если ты попытаешься делать специальные эффекты (например, alpha blending), не забудь, что они будут налагаться на уже отрендеренное движком изображением.

Интеграция системы стейтов в движок

Итак, система стейтов, в принципе, завершена, за исключением структуры вьюера (viewer structure), которой мы займёмся чуть позднее. А сейчас интегрируем в движок то, что есть (файлы State.h и State.cpp).

  • Добавь инструкцию #include "Engine.h" в самом начале файла State.cpp. (Проверь её наличие.)

Изменения в Engine.h (Проект Engine)

  • Добавь инструкцию #include "State.h" в файл Engine.h:
Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "State.h"
...


Это была самая простая часть всей процедуры.

  • Добавь новый член (*StateSetup)() в структуру EngineSetup файла Engine.h, чуть ниже переменной *name:
Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Setup Structure
//-----------------------------------------------------------------------------
struct EngineSetup
{
	HINSTANCE instance; // Дескриптор экземпляра приложения.
	char *name; // Название приложения.
	void (*StateSetup)(); // Функция подготовки стейта.

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


Вообще, StateSetup - это функция обратного вызова (callback function), подобная той, что мы использовали в шаблонном класса ResourceManager. Для её применения достаточно просто объявить её в коде, специфичном для приложения, с типом void (не возвращающей значение). Затем просто присваиваешь указатель StateSetup своей новой функции. Во время создания экземпляра движка, он обязательно вызовет эту функцию. Таким образом с помощью вызова StateSetup игрокодер может создать и настроить все стейты, которые ему понадобятся при создании игрового приложения. Это позволяет интегрировать функцию StateSetup на стадии загрузки движка.

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

Интегрировать StateSetup на стадии загрузки движка вовсе необязательно, но очень желательно. Это сделает код движка более стабильным. Установка стейтов именно таким способом поможет избежать попыток создавать стейты в тот момент, когда движок ещё не был загружен.

Устанавливаем добавленный член StateSetup в NULL. Для этого:

  • Добавь строку StateSetup = NULL в конструктор структуры EngineSetup в файле Engine.h:
Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Setup Structure
//-----------------------------------------------------------------------------
struct EngineSetup
{
	HINSTANCE instance; // Дескриптор экземпляра приложения.
	char *name; // Название приложения.
	void (*StateSetup)(); // Функция подготовки стейта.

	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		instance = NULL;
		name = "Application";
		StateSetup = NULL;  // Добавленная строка
	}
};
...

  • Добавь 3 переменных члена в класс Engine файла Engine.h, в секцию private, чуть ниже EngineSetup *m_setup:

LinkedList< State > *m_states;
State *m_currentState;
bool m_stateChanged;

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// 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.

	LinkedList< State > *m_states; // Связный список (Linked list) стейтов.
	State *m_currentState; // Указатель на текущий стейт.
	bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре.
};
...

Как можем видеть, m_states - это связный список (LinkedList), в котором хранятся все наши стейты. Всякий раз, когда мы добавляем новый стейт в движок о сохраняется в этом связном списке. Именно поэтому нам необходим уникальный идентификационный номер для каждого стейта.
m_currentState - это всего лишь указатель на стейт, который в данный момент обрабатывается движком (= активен).
m_stateChanged - это внутренний флаг, используемый движком для индикации того, изменялся ли стейт в данном кадре.

  • Добавь 4 новых функции в класс Engine файла Engine.h, в секцию public, чуть ниже строки void SetDeactiveFlag( bool deactive ):

void AddState( State *state, bool change = true );
void RemoveState ( State *state );
void ChangeState( unsigned long id );
State *GetCurrentState();

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Class
//-----------------------------------------------------------------------------
class Engine
{
public:
	Engine( EngineSetup *setup = NULL );
	virtual ~Engine();

	void Run();

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

	void AddState( State *state, bool change = true );
	void RemoveState( State *state );
	void ChangeState( unsigned long id );
	State *GetCurrentState();

private:
	bool m_loaded; // Флаг показывает, загружен ли движок или нет.
	HWND m_window; // Дескриптор главного окна.
	bool m_deactive; // Флаг показывает, активно приложение или нет.

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

	LinkedList< State > *m_states; // Связный список (Linked list) стейтов.
	State *m_currentState; // Указатель на текущий стейт.
	bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре.
};
...

Изменения в Engine.cpp (Проект Engine)

  • Добавь реализацию этих функций в самый конец файла Engine.cpp:
Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает дескриптор текущего окна.
//-----------------------------------------------------------------------------
HWND Engine::GetWindow()
{
	return m_window;
}

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

//-----------------------------------------------------------------------------
// Добавляет стейт в движок.
//-----------------------------------------------------------------------------
void Engine::AddState( State *state, bool change )
{
	m_states->Add( state );

	if( change == false )
		return;

	if( m_currentState != NULL )
		m_currentState->Close();

	m_currentState = m_states->GetLast();
	m_currentState->Load();
}

//-----------------------------------------------------------------------------
// Удаляет стейт из движка.
//-----------------------------------------------------------------------------
void Engine::RemoveState( State *state )
{
	m_states->Remove( &state );
}

//-----------------------------------------------------------------------------
// Сменить текущий стейт на стейт с указанным ID.
//-----------------------------------------------------------------------------
void Engine::ChangeState( unsigned long id )
{
	// Итерировать через список стейтов и найти новый стейт, на который надо сменить.
	m_states->Iterate( true );
	while( m_states->Iterate() != NULL )
	{
		if( m_states->GetCurrent()->GetID() == id )
		{
			// Закрываем предыдущий стейт.
			if( m_currentState != NULL )
				m_currentState->Close();

			// Устанавливаем новый стейт в качестве текущего и загружаем его.
			m_currentState = m_states->GetCurrent();
			m_currentState->Load();

			// Указываем флаг, что стейт был изменён в данном кадре.
			m_stateChanged = true;

			break;
		}
	}
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущий стейт.
//-----------------------------------------------------------------------------
State *Engine::GetCurrentState()
{
	return m_currentState;
}


Самые интересные функции здесь - это AddState и ChangeState.
Функция RemoveState предельно проста и просто удаляет стейт (указатель на который передаётся в качестве параметра) из связного списка стейтов (подобно тому, как это происходит в LinkedList.h).
Функция GetCurrentState просто возвращает стейт, который обрабатывается в данный момент (т.е. тот, которому присвоен указатель m_currentState).
Функция AddState добавляет новый стейт в движок. Его указатель передаётся в качестве параметра функции. Внутри реализации AddState можем видеть флаг change, который используется для индикации, требуется ли немедленно переключиться на данный стейт сразу после его создания или нет. В случае, если флаг change == TRUE, добавляем стейт, задействовав механизм добавления элемента в связный список, и затем тут же загружаем его.
Функция ChangeState используется для "ручного" переключения движка на обработку нового стейта. Напомним, что одновременно движок может обрабатывать всего 1 стейт. При вызове функции ChangeState, в качестве параметра необходимо передать ID стейта, на который нужно переключиться. Во время выполнения функция начнёт итерацию через связный список стейтов и будет продолжать её до тех пор, пока не найдёт желаемый стейт. Механизм итерации, опять же, не возникает из неоткуда, а прописан всё в том же LinkedList.h. После этого, текущий стейт закрывается и переходит к загрузке нового стейта, по завершении чего флаг m_stateChanged устанавливается в TRUE, давая знать движку о том, что в данном кадре стейт изменился (т.е. был сменён на другой). Этот флаг затем считывается функцией Run, предохраняя движок от случайной попытки обработать стейт, который уже не находится под его контролем (т.е. неактивен).

  • Добавь следующие строки в конструктор класса Engine в файле Engine.cpp, сразу после создания окна (CreateWindowEx):

m_states = new LinkedList< State >;
m_currentState = NULL;

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

  • Добавь следующие строки в конструктор класса Engine в файле Engine.cpp, рядом с его окончанием, сразу после строки srand( timeGetTime() ):

if( m_setup->StateSetup != NULL )
m_setup->StateSetup();

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Конструктор класса 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 );

	// Создаём окно и возвращаем его дескриптор.
// 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 );

	// Создаём связный список стейтов.
	m_states = new LinkedList< State >;		// Добавленная строка
	m_currentState = NULL;					// Добавленная строка

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

	// Позволяет приложению произвести настройку всех необходимых стейтов.
	if( m_setup->StateSetup != NULL )			// Добавленная строка
		m_setup->StateSetup();					// Добавленная строка

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

Здесь мы проверяем, будет ли задействована настройка стейтов при создании движка. Если да, то вызываем функцию StateSetup, которая позволит игрокодеру создать и настроить все стейты, которые будут использоваться в игровом приложении.

  • Добавь следующие строки в деструктор класса Engine в файле Engine.cpp, внутри условного оператора if( m_loaded == true ):

if( m_currentState != NULL )
m_currentState->Close();
SAFE_DELETE( m_states );

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

		// Уничтожаем связные списки со стейтами.
		if( m_currentState != NULL )
			m_currentState->Close();
		SAFE_DELETE( m_states );
	}

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

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

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

Наконец-то мы задействовали этот ранее пустовавший условный оператор if, который изначально создан с прицелом на уничтожение более неиспользуемых компонентов движка, загруженных конструктором. В первую очередь проверяем, пуст ли флаг m_currentState. Если нет, то даём команду на его самостоятельное закрытие (внутренняя функция Close). Затем применяем макрос SAFE_DELETE для уничтожения связного списка стейтов.

  • Добавь следующую строку в функции Run в файле Engine.cpp, сразу после функции ShowWindow:

ViewerSetup viewer;

  • Добавь следующие строки в функции Run в файле Engine.cpp, сразу после функций подсчёта затраченного времени:

if( m_currentState != NULL )
m_currentState->RequestViewer( &viewer );

Функция Run - это последнее место, где нам необходимо внести изменения для того, чтобы "вдохнуть жизнь" в нашу систему стейтов. Сперва проверяем, существует ли в данный момент какой-либо стейт, обрабатываемый движком. Если да, то вызываем функцию RequestViewer, чтобы с помощью неё игрокодер заполнил детали структуры ViewerSetup, необходимые для рендеринга текущего кадра. Так как пока мы не выполняем никакого рендеринга, структура ViewerStructure в настоящий момент пуста и не требует заполнения. Но эти строки мы размещаем уже сейчас, чтобы к тому времени, как мы начнём размещать какие-либо записи в ViewerSetup, всё остальное у нас было готово к активации вьюера.

  • Добавь следующие строки в функции Run в файле Engine.cpp, чуть ниже тех, что были добавлены только что:

m_stateChanged = false;
if( m_currentState != NULL )
m_currentState->Update( elapsed );
if( m_stateChanged == true )
continue;

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

		// Используется для получения деталей настроек вьюера от приложения.
		ViewerSetup viewer;

		// Входим в цикл выборки сообщений.
		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;

				// Запрос вьюера текущего стейта (если таковой имеется).
				if( m_currentState != NULL )
					m_currentState->RequestViewer( &viewer );

				// Обновить текущиё стейт (если таковой имеется),
				// учитывая возможную смену стейтов.
				m_stateChanged = false;
				if( m_currentState != NULL )
					m_currentState->Update( elapsed );
				if( m_stateChanged == true )
					continue;
			}
		}
	}

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

Здесь мы устанавливаем флаг m_stateChanged в FALSE (запрещаем смену стейта в момент рендеринга) и вызываем функцию Update на текущем стейте (если он есть). Ключевое слово continue останавливает любое дальнейшее выполнение циклов do, for или while и переходит к самому началу следующей итерации.

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

Тестовая перекомпиляция Engine.lib

Итак, система стейтов полностью интегрирована в движок. Конечно, мы ещё не раз вернёмся к файлам State.h и State.cpp для добавления в них новых пунктов. Особенно это касается структуры ViewerSetup.
Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:

  • В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).

Image
По окончании компиляции (обычно, при создании небольших проектов, она занимает менее 1 секунды) в панели "Вывод" (в нижней части главного окна IDE) будет представлен отчёт (лог) об успешной (либо неуспешной) компиляции.
В нашем случае компиляция прошла успешно:
Image
Полученная в результате компиляции двоичная библиотека Engine.lib перезаписывается по тому же пути (там же её будет искать тестовое приложение из Проекта Test).

Пример использования системы стейтов

Теперь, когда у нас есть полнофункциональная система стейтов, проведём небольшой инструктаж по её использованию. Это совсем не сложно. Всё, что необходимо сделать - это создать новый дочерний класс, ветвящийся от класса State, и переопределить (override) любую из виртуальных функций, в которой есть необходимость. Допустим, в нашем приложении есть файл исходного кода MyStates.cpp (Создавать ничего не нужно! Просто прочитай и разберись в коде.):

MyStates.cpp
...
#define TEST_STATE 1

class TestState : public State // На базе класса State создаём TestState
{
	public:
	TestState(unsigned long id);	// Конструктор

	virtual void Load();
	virtual void Close();
	virtual void RequestViewer(ViewerSetup *viewer);
	virtual void Update(float elapsed);
	virtual void Render();
};
...


Затем создаём вспомогательную функцию для создания нового стейта:

MyStates.cpp
...
void StateSetup()
{
	g_engine->AddState(new TestState(TEST_STATE), true);
}
...


Если у тебя более одного стейта и ты хочешь переключиться на один из них, вызывай функцию ChangeState. Обычно она ставится перед функцией Update текущего стейта. Вместе с ней тебе надо передать уникальный идентификационный номер стейта, на который ты хочешь сменить (в нашем случае мы определили его с помощью инструкции #define TEST_STATE 1). Вот пример:

...
g_engine->ChangeState(TEST_STATE);
...


===
Вот и всё. Теперь у тебя достаточно знаний для применения системы стейтов на практике. В следующей Главе мы изучим вторую форму контроля движка - пользовательский ввод (user input).


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

Contributors to this page: slymentat .
Последнее изменение страницы Среда 30 / Ноябрь, 2016 13:36:29 MSK автор slymentat.

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

No records to display

Хостинг