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

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




Intro

По теме стейтов читай статью на Игрокодере: Стейты игры и процессы (Game States and Processes).
Напомним, что система стейтов нужна для рационального расходования системных ресурсов во время подготовки кадра к прорисовке и выводу на экран.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, которые мы создали в предыдущих главах. Чуть ниже есть второй Проект Test. Его пока не трогаем!

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

В заголовочном файле State.h внутри класса State будут представлены объявления функций для работы со стейтами.
ОК, приступаем.
  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент... (Add->New Item...)
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "State.h".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле State.h набираем следующий код:
State.h
//-----------------------------------------------------------------------------
// File: State.h
// Base state functionality. Applications must derive new states from this
// class to add to the engine.
//
// 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; // Application defined ID (must be unique for state switching).
};

#endif

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

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


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

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

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

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

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

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

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

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

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

В файл исходного кода State.cpp разместим реализации (вернее, своеобразные "заготовки") функций для работы со стейтами, объявленных в State.h. ОК, приступаем.:
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "State.cpp".
  • Жмём "Добавить".
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле Main.cpp набираем следующий код:
State.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// File: State.cpp
// State.h implementation. Реализация функций, объявленных в State.h
// Refer to the State.h interface for more details.
//
// 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;
}

//-----------------------------------------------------------------------------
// Allows the state to preform any pre-processing construction.
// Функции предобработки стейта.
//-----------------------------------------------------------------------------
void State::Load()
{

}

//-----------------------------------------------------------------------------
// Allows the state to preform any post-processing destruction.
// Функции постобработки стейта.
//-----------------------------------------------------------------------------
void State::Close()
{

}

//-----------------------------------------------------------------------------
// Returns the view setup details for the given frame.
// Возвращает объект-вьюер (виртуальную "камеру") в данном кадре.
//-----------------------------------------------------------------------------
void State::RequestViewer( ViewerSetup *viewer )
{

}

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

}

//-----------------------------------------------------------------------------
// Renders the state.
// Рендерит стейт.
//-----------------------------------------------------------------------------
void State::Render()
{

}

//-----------------------------------------------------------------------------
// Returns the state's ID.
// Возвращает 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 вызывается ПОСЛЕ выполнения движком её собственной функции Render. Часто это не играет никакой роли. Но, если ты попытаешься делать специальные эффекты (например, alpha blending), учти, что они будут налагаться на уже отрендеренное движком изображением.


Интегрируем систему стейтов в движок

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

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

  • Добавь инструкцию #include "State.h" в файл Engine.h, чуть ниже строки #include "Geometry.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; // Application instance handle.
					// Дескриптор инстанса приложения
	char *name; // Name of the application.
	void (*StateSetup)(); // Функция подготовки стейта.
...

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

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

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

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

Устанавливаем добавленный член StateSetup в NULL. Для этого:
  • Добавь строку StateSetup = NULL в конструктор структуры EngineSetup в файле Engine.h:
Фрагмент Engine.h (Проект Engine)
...
	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		instance = NULL;
		name = "Application";
		StateSetup = NULL;
	}
...

  • Добавь три переменных члена в класс Engine файла Engine.h, в секцию private, чуть ниже строки EngineSetup *m_setup:
Фрагмент Engine.h (Проект Engine)
...
private:
	bool m_loaded; // Indicates if the engine is loading.
					// Флаг показывает, загружен ли движок
	HWND m_window; // Main window handle.
					// Дескриптор главного окна приложения
	bool m_deactive; // Indicates if the application is active or not.
					// Флаг активности приложения

	EngineSetup *m_setup; // Copy of the engine setup structure.
					// Копия структуры EngineSetup
	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();
...

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

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

  • Добавь реализацию этих функций в самый конец файла Engine.cpp:
Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Добавляет стейт в движок.
//-----------------------------------------------------------------------------
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, сразу после вызова функции создания окна (CreateWindow):

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

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

	m_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)
...
	// Seed the random number generator with the current time.
	// Стартуем генератор сулчайных чисел на основе текущего времени.
	srand( timeGetTime() );
	
	if( m_setup->StateSetup != NULL )
	{
		m_setup->StateSetup();
	}
...


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

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

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

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


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

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

ViewerSetup viewer;

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

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


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

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

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

				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)
...
else if( !m_deactive )
			{
				// Calculate the elapsed time.
				// Подсчитываем затраченное время.
				unsigned long currentTime = timeGetTime();
				static unsigned long lastTime = currentTime;
				float elapsed = ( currentTime - lastTime ) / 1000.0f;
				lastTime = currentTime;

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

				// Обновить текущиё стейт (если таковой имеется),
				// учитывая возможную смену стейтов.
				m_stateChanged = false;

				if( m_currentState != NULL )
				{
					m_currentState->Update( elapsed );
				}

				if( m_stateChanged == true )
					continue;
			}
		}
	}
...

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

Перекомпилируем Engine.lib

Итак, система стейтов полностью интегрирована в движок. Конечно, мы ещё не раз вернёмся к файлам State.h и State.cpp для добавления в них новых пунктов. Особенно это касается структуры ViewerSetup.
Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:
  • В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).
По окончании компиляции в панели "Вывод" (в нижней части главного окна IDE) будет представлен отчёт (лог) об успешной (либо неуспешной) компиляции. Полученная в результате компиляции двоичная библиотека 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).

Источники


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


ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.8 Добавляем пользовательский ввод

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

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

No records to display

Search Wiki Page

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

Категории

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