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

1.8 Добавляем пользовательский ввод (User Input)




Intro

Наш движок будет поддерживать всего 2 устройства ввода - клавиатуру и мышь.1 Конечно, можно добавить поддержку и других устройств, например геймпада или руля. Но, как правило, в FPS они не используются. В данный момент мы даже не реализовали поддержку нажатия каких-либо клавиш в игре, т.к. это строго код, специфичный для игры. Вместо этого, мы создадим специальный класс-обёртку, который будет заключать в себе основной функционал DirectInput, предлагая его в виде нескольких высокоуровневых интерфейсов. По сути, этот специальный класс (назовём его Input) будет являться своеобразной надстройкой или "обёрткой" для классов и функций DirectInput. Поэтому здесь и далее будем называть его враппер или класс-обёртка (от англ "wrapper" - обёртка). Это означает, что многие внутренние DirectX операции будут скрыты от игрокодера и тот сможет более легко внедрять пользовательский ввод в своё игрвое приложение.
Сейчас в Проекте Engine нашего движка всего 7 файлов: Engine.h, Engine.cpp, LinkedList.h, ResourceManagement.h, Geometry.h, State.h и State.cpp, которые мы создали в предыдущих главах.

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

В заголовочном файле Input.h представлен авторский (т.е. в DirectX SDK его никогда не было) класс Input для обработки пользовательского ввода.
ОК, приступаем.
  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент... (Add->New Item...)
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Input.h".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле Input.h набираем следующий код:
Input.h
//-----------------------------------------------------------------------------
// File: Input.h 
// A wrapper class for handling input via DirectInput. Both the keyboard and
// mouse devices are supported through this class.
// Объявление класса Input, его членов и методов.
// Input - класс-обёртка для поддержки устройств ввода через функции DirectX.
// Он поддерживает клавиатуру и мышь.
//
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef INPUT_H
#define INPUT_H

//-----------------------------------------------------------------------------
// Input Class
//-----------------------------------------------------------------------------
class Input
{
public:
	Input( HWND window );
	virtual ~Input();

	void Update();

	bool GetKeyPress( char key, bool ignorePressStamp = false );

	bool GetButtonPress( char button, bool ignorePressStamp = false );
	long GetPosX();
	long GetPosY();
	long GetDeltaX();
	long GetDeltaY();
	long GetDeltaWheel();

private:
	HWND m_window; // Handle of the parent window.
				// Дескриптор родительского окна
	IDirectInput8 *m_di; // DirectInput object.
	unsigned long m_pressStamp; // Current press stamp (incremented every frame).
				// Текущий штамп (=метка) нажатия (увеличивается на единицу
				// в каждом кадре).

	IDirectInputDevice8 *m_keyboard; // DirectInput keyboard device.
	char m_keyState[256]; // Stores the state of the keyboard keys.
	unsigned long m_keyPressStamp[256]; // Stamps the last frame each key was preseed.
					// Штамп нажатия клавиш в последнем кадре.

	IDirectInputDevice8 *m_mouse; // DirectInput mouse device.
	DIMOUSESTATE m_mouseState; // Stores the state of the mouse buttons.
					// Хранит состояние кнопок мыши.
	unsigned long m_buttonPressStamp[3]; // Stamps the last frame each button was preseed.
				// Хранит состояние каждой кнопки мыши в последнем кадре.
	POINT m_position; // Stores the position of the mouse cursor on the screen.
				// Хранит позицию экранного курсора мыши.
};

#endif

  • Сохрани Решение (Файл->Сохранить все).
Класс Input довольно содержательный, и здесь есть, на что обратить внимание. В то же время, большинство функций предельно просты и по завершению просто возвращают какое-либо значение. Рассмотрим каждую из этих функций, обратившись к их реализации, которую мы разместим в файле исходного кода Input.cpp .

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

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

//-----------------------------------------------------------------------------
// The input class constructor.
//-----------------------------------------------------------------------------
Input::Input( HWND window )
{
	// Store the handle to the parent window.
	// Сохраняем дескриптор родительского окна.
	m_window = window;

	// Create a DirectInput interface.
	DirectInput8Create( GetModuleHandle( NULL ), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&m_di, NULL );

	// Create, prepare, and aquire the keyboard device.
	// Создаём, готовим и захватываем клавиатуру.
	m_di->CreateDevice( GUID_SysKeyboard, &m_keyboard, NULL );
	m_keyboard->SetDataFormat( &c_dfDIKeyboard );
	m_keyboard->SetCooperativeLevel( m_window, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );
	m_keyboard->Acquire();

	// Создаём, готовим и захватываем мышь.
	m_di->CreateDevice( GUID_SysMouse, &m_mouse, NULL );
	m_mouse->SetDataFormat( &c_dfDIMouse );
	m_mouse->SetCooperativeLevel( m_window, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );
	m_mouse->Acquire();

	// Start the press stamp.
	// Ставим первый штамп нажатия.
	m_pressStamp = 0;
}

//-----------------------------------------------------------------------------
// The input class destructor.
//-----------------------------------------------------------------------------
Input::~Input()
{
	SAFE_RELEASE( m_di );
	SAFE_RELEASE( m_keyboard );
	SAFE_RELEASE( m_mouse );
}

//-----------------------------------------------------------------------------
// Updates the state of both the keyboard and mouse device.
// Обновляем состояние клавиатуры и мыши.
//-----------------------------------------------------------------------------
void Input::Update()
{
	static HRESULT result;

	// Poll the keyboard until it succeeds or returns an unknown error.
	// Опрашиваем клавиатуру, пока на ней что-нибудь не нажмут,
	// либо пока функция не вернёт сообщение об ошибке.
	while( true )
	{
		m_keyboard->Poll();
		if( SUCCEEDED( result = m_keyboard->GetDeviceState( 256, (LPVOID)&m_keyState ) ) )
			break;
		if( result != DIERR_INPUTLOST && result != DIERR_NOTACQUIRED )
			return;

		// Reacquire the device if the focus was lost.
		// Перезахватываем устройство ввода в случае
		// утери окном фокуса.
		if( FAILED( m_keyboard->Acquire() ) )
			return;
	}

	// Poll the mouse until it succeeds or returns an unknown error.
	// Опрашиваем мышь, пока её не сдвинут с места, что-нибудь на ней не нажмут,
	// либо пока функция не вернёт сообщение об ошибке.
	while( true )
	{
		m_mouse->Poll();
		if( SUCCEEDED( result = m_mouse->GetDeviceState( sizeof( DIMOUSESTATE ), &m_mouseState ) ) )
			break;
		if( result != DIERR_INPUTLOST && result != DIERR_NOTACQUIRED )
			return;

		// Reacquire the device if the focus was lost.
		// Перезахватываем устройство ввода в случае
		// утери окном фокуса.
		if( FAILED( m_mouse->Acquire() ) )
			return;
	}

	// Get the relative position of the mouse.
	// Получаем относительные координаты положения (курсора) мыши.
	GetCursorPos( &m_position );
	ScreenToClient( m_window, &m_position );

	// Increment the press stamp.
	// Инкрементируем штамп нажатия.
	m_pressStamp++;
}

//-----------------------------------------------------------------------------
// Returns true if the given key is pressed.
// Note: Consistent presses will return false when using the press stamp.
// Возвращаем TRUE в случае нажатия клавиши.
// Примечание: В случае применения штампа нажатия, длительные нажатия вернут FALSE.
//-----------------------------------------------------------------------------
bool Input::GetKeyPress( char key, bool ignorePressStamp )
{
	if( ( m_keyState[key] & 0x80 ) == false )
		return false;

	bool pressed = true;

	if( ignorePressStamp == false )
		if( m_keyPressStamp[key] == m_pressStamp - 1 || m_keyPressStamp[key] == m_pressStamp )
			pressed = false;

	m_keyPressStamp[key] = m_pressStamp;

	return pressed;
}

//-----------------------------------------------------------------------------
// Returns true if the given button is pressed.
// Note: Consistent presses will return false when using the press stamp.
// Возвращаем TRUE в случае нажатия кнопки мыши.
// Примечание: В случае применения штампа нажатия, длительные нажатия вернут FALSE.
//-----------------------------------------------------------------------------
bool Input::GetButtonPress( char button, bool ignorePressStamp )
{
	if( ( m_mouseState.rgbButtons[button] & 0x80 ) == false )
		return false;

	bool pressed = true;

	if( ignorePressStamp == false )
		if( m_buttonPressStamp[button] == m_pressStamp - 1 || m_buttonPressStamp[button] == m_pressStamp )
			pressed = false;

	m_buttonPressStamp[button] = m_pressStamp;

	return pressed;
}

//-----------------------------------------------------------------------------
// Returns the x position of the mouse.
// Возвращает положение курсора мыши по оси X
//-----------------------------------------------------------------------------
long Input::GetPosX()
{
	return m_position.x;
}

//-----------------------------------------------------------------------------
// Returns the y position of the mouse.
// Возвращает положение курсора мыши по оси Y
//-----------------------------------------------------------------------------
long Input::GetPosY()
{
	return m_position.y;
}

//-----------------------------------------------------------------------------
// Returns the change in the mouse's x coordinate.
// Возвращает изменение X-координаты курсора мыши.
//-----------------------------------------------------------------------------
long Input::GetDeltaX()
{
	return m_mouseState.lX;
}

//-----------------------------------------------------------------------------
// Returns the change in the mouse's y coordinate.
// Возвращает изменение Y-координаты курсора мыши.
//-----------------------------------------------------------------------------
long Input::GetDeltaY()
{
	return m_mouseState.lY;
}

//-----------------------------------------------------------------------------
// Returns the change in the mouse's scroll wheel.
// Возвращает изменение положения колеса прокрутки мыши.
//-----------------------------------------------------------------------------
long Input::GetDeltaWheel()
{
	return m_mouseState.lZ;
}

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

Исследуем Input.cpp


Конструктор и деструктор класса Input

Конструктор класса Input принимает в качестве параметра дескриптор родительского окна. Это должно быть главное окно игрового приложения. Наш движок создаёт окно, а его дескриптор может быть запрошен с помощью функции GetWindow, расположенной в классе Engine. Ты можешь использовать дескриптор, возвращаемый этой функцией, для его указания в конструкторе класса Input. Конструктор затем сохраняет копию этого дескриптора в своей внутренней переменной (т.к. позднее она понадобится для использования в других функциях). Что мы и делаем в конструкторе класса Input:
Фрагмент Input.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The input class constructor.
//-----------------------------------------------------------------------------
Input::Input( HWND window )
{
	// Store the handle to the parent window.
	// Сохраняем дескриптор родительского окна.
	m_window = window;
...

Сразу после этого приступаем к самой главной задаче - созданию интерфейса DirectInput:
Фрагмент Input.cpp (Проект Engine)
...
	// Create a DirectInput interface.
	DirectInput8Create( GetModuleHandle( NULL ), DIRECTINPUT_VERSION,
		IID_IDirectInput8, (void**)&m_di, NULL );
...

Видно, что используется 8 версия DirectInput. Вот прототип функции DirectInput8Create:
Прототип функции DirectInput8Create
HRESULT WINAPI DirectInput8Create (
	HINSTANCE hInst,	// Дескриптор экземпляра (инстанса) модуля приложения
	DWORD dwVersion,	// Версия DirectInput, используемая в приложении
	REFIID riidltf,	// Уникальный идентификатор выбранного интерфейса
	LPVOID *ppvOut,	// Внутренний указатель интерфейса (для обращения к нему)
	LPUNKNOWN punkOuter	// Используется для объединения. Оставь его в NULL.
)

В нашей функции DirectInput8Create, после завершения работы функции, указатель m_di хранит адрес нашего нового объекта DirectInput.

Как только мы создали объект DirectInput, можно сразу приступать к подготовке устройств ввода (клавиатура и мышь). Процесс подготовки этих устройств, на удивление, очень схож и состоит из нескольких операций:
  • Создаём device (устройство);
  • Устанавливаем формат данных (data format), которое оно будет использовать;
  • Устанавливаем уровень кооперации (взаимодействия) устройства с другими приложениями;
  • Захватываем (aquire) устройство для получения ввода с него.
Рассмотрим на примере подготовки клавиатуры.
Фрагмент Input.cpp (Проект Engine)
...
	// Create, prepare, and aquire the keyboard device.
	// Создаём, готовим и захватываем клавиатуру.
	m_di->CreateDevice( GUID_SysKeyboard, &m_keyboard, NULL );
	m_keyboard->SetDataFormat( &c_dfDIKeyboard );
	m_keyboard->SetCooperativeLevel( m_window, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );
	m_keyboard->Acquire();
...

Первым делом вызываем функцию CreateDevice, которая представлена нашим, только что созданным, интерфейсом DirectInput. Вот её прототип:
Прототип функции CreateDevice
HRESULT CreateDevice (
	REFGUID rguid,	// GUID создаваемого устройства
	LPDIRECTINPUTDEVICE *lplpDirectInputDevice,	// Внутренний указатель для обращения к устройству
	LPUNKNOWN punkOuter	// Используется для объединения. Оставь его в NULL.
	)

Единственное, о чём здесь надо позаботиться, это о передаче верного глобального уникального идентификатора (Globally Unique Identifier, GUID) для создаваенмого устройства. Также необходимо указать внутренний указатель на адрес данного устройства в памяти. Мы используем GUID_SysKeyboard для указания на устройство ввода клавиатуру, которая будет возвращать указатель m_keyboard, при обращении к нему. То есть, сразу после того, как устройство было создано, мы можем обращаться к нему через его внутренний указатель (m_keyboard). Что мы и делаем.
Вслед за этим, устанавливаем формат данных при помощи вызова функции SetDataFormat. Формат данных - это то, что описывает, как именно данные будут считываться с данного устройства и обрабатываться. Существует несколько предопределённых форматов данных, которые мы можем использовать (их описание можно найти в документации к DirectInput8). Для нашей клавиатуры мы используем формат данных c_dfDIKeyboard.
Далее устанавливаем уровень кооперации, вызывая функцию SetCooperativeLevel. Функция устанавливает то, как данное устройство будет разделять ввод с другими приложениями. Ты можешь настроить работу с устройством, когда приложение активно (foreground) или неактивно (background), а также установить эксклюзивный (exclusive) или неэксклюзивный (nonexclusive) режим (см. Таблицу 1).

Таблица 1. Возможные значения 2-го параметра функции SetCooperativeLevel
ЗНАЧЕНИЕ ОПИСАНИЕ
DISCL_NONEXCLUSIVE Приложение разделяет доступ к данному устройству ввода с другими приложениями.
DISCL_EXCLUSIVE Приложение имеет эксклюзивный доступ к данному устройству ввода.
DISCL_FOREGROUND Ввод с данного устройства возможен, только когда приложение активно (на нём установлен фокус).
DISCL_BACKGROUND Ввод с данного устройства возможен при любом состоянии активности/неактивности приложения.
DISCL_NOWINKEY Если приложение активно, то блокируется клавиша WINDOWS. Тем самым исключается возможность непреднамеренного прерывания приложения при случайном нажатии этой клавиши.

Данные флаги можно комбинировать. Но флаги DISCL_EXCLUSIVE и DISCL_NONEXCLUSIVE взаимоисключают друг друга, впрочем как и DISCL_FOREGROUND и DISCL_BACKGROUND. Мы будем работать с нашей клавиатурой при активизации окна (DISCL_FOREGROUND) в неэксклюзивном режиме (DISCL_NONEXCLUSIVE). То есть, другие приложения тоже смогут получить доступ к устройству, одновременно с нашей игрой.
Завершающий шаг подготовки клавиатуры - это захват устройства функцией Aquire для получения введённых с него данных. Без захвата устройства получение с него данных невозможно. Устройство также может быть освобождено (unaquired) вручную путём вызова функции Unaquire. Хотя, чаще всего это происходит неявно, например при утере фокуса окном приложения или когда другое приложение пытается получить к устройству эксклюзивный доступ.
Чуть ниже те же самые шаги повторяются для другого устройства ввода - компьютерной мыши. Вызываемые функции отличаются от вышеприведённых лишь некоторыми параметрами вызываемых функций. Взгляни на следующие 4 строки кода из Input.cpp и ты увидишь отличия:
Фрагмент Input.cpp (Проект Engine)
...
	// Создаём, готовим и захватываем мышь.
	m_di->CreateDevice( GUID_SysMouse, &m_mouse, NULL );
	m_mouse->SetDataFormat( &c_dfDIMouse );
	m_mouse->SetCooperativeLevel( m_window, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );
	m_mouse->Acquire();
...

После того, как устройство ввода создано и подготовлено к работе, указатель на только что созданное устройство сохраняется в переменной m_mouse.
Конструктор класса Input завершается всего 1 строкой кода, где мы очищаем (устанавливаем в 0) отметку нажатия (press stamp) для дальнейшего использования:
Фрагмент Input.cpp (Проект Engine)
...
	// Start the press stamp.
	// Ставим первый штамп нажатия.
	m_pressStamp = 0;
}
...

Штампы - это, обычно, просто числовое значение (как правило, типа LONG), которое инкрементально увеличивается с каждым кадром. Таким образом, в первом кадре переменная m_pressStamp будет равна нулю (0), во втором - 1, в третьем - 2 и так далее. Такие штампы обычно применяются для отслеживания наступившего события, либо для определения, в каком именно кадре последний раз произошло данное событие. В нашем классе Input мы будем применять этот штамп для отслеживания, когда кнопки на клавиатуре и мыши были нажаты последний раз.

Наш класс Input также имеет деструктор, который использует ранее подготовленные макросы SAFE_RELEASE для уничтожения нашего объекта DirectInput и обоих устройств ввода, что необходимо проделать при уничтожении класса Input:
Фрагмент Input.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The input class destructor.
//-----------------------------------------------------------------------------
Input::~Input()
{
	SAFE_RELEASE( m_di );
	SAFE_RELEASE( m_keyboard );
	SAFE_RELEASE( m_mouse );
}
...


Обновление пользовательского ввода. Функция Update

Только что мы рассмотрели, как класс Input создаётся и уничтожается. Теперь мы изучим, как он поддерживает информацию о пользовательском вводе в актуальном состоянии. В двух словах, в каждом кадре класс Input должен иметь возможность опрашивать (to query) устройства для проверки наличия попыток пользователя ввести какие-либо данные. Эта цель достигается с помощью функции Update, экспонированной нашим классом Input. Эта функция вызывается в каждом кадре всего 1 раз. Рассмотрим её реализацию в фале Input.cpp:
Фрагмент Input.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Updates the state of both the keyboard and mouse device.
// Обновляем состояние клавиатуры и мыши.
//-----------------------------------------------------------------------------
void Input::Update()
{
	static HRESULT result;

	// Poll the keyboard until it succeeds or returns an unknown error.
	// Опрашиваем клавиатуру, пока на ней что-нибудь не нажмут,
	// либо пока функция не вернёт сообщение об ошибке.
	while( true )
	{
		m_keyboard->Poll();
		if( SUCCEEDED( result = m_keyboard->GetDeviceState( 256, (LPVOID)&m_keyState ) ) )
			break;
		if( result != DIERR_INPUTLOST && result != DIERR_NOTACQUIRED )
			return;

		// Reacquire the device if the focus was lost.
		// Перезахватываем устройство ввода в случае
		// утери окном фокуса.
		if( FAILED( m_keyboard->Acquire() ) )
			return;
	}

	// Poll the mouse until it succeeds or returns an unknown error.
	// Опрашиваем мышь, пока её не сдвинут с места, что-нибудь на ней не нажмут,
	// либо пока функция не вернёт сообщение об ошибке.
	while( true )
	{
		m_mouse->Poll();
		if( SUCCEEDED( result = m_mouse->GetDeviceState( sizeof( DIMOUSESTATE ), &m_mouseState ) ) )
			break;
		if( result != DIERR_INPUTLOST && result != DIERR_NOTACQUIRED )
			return;

		// Reacquire the device if the focus was lost.
		// Перезахватываем устройство ввода в случае
		// утери окном фокуса.
		if( FAILED( m_mouse->Acquire() ) )
			return;
	}

	// Get the relative position of the mouse.
	// Получаем относительные координаты положения (курсора) мыши.
	GetCursorPos( &m_position );
	ScreenToClient( m_window, &m_position );

	// Increment the press stamp.
	// Инкрементируем штамп нажатия.
	m_pressStamp++;
}
...

При беглом осмотре ты увидишь, что основу функции Update составляют 2 основных цикла while. Первый из них применяется для проверки ввода с клавиатуры, а второй - с мыши. Здесь также заметно, что оба цикла во многом схожи, за исключением указателей на соответствующие устройства и форматов данных. Комментарии в исходном коде сразу прояснят, что здесь и к чему.
Когда функция Update входит в первый цикл while, она начинает опрашивать клавиатуру. Опрос (polling) означает, что ты даёшь команду устройству обновить его вводимые данные (input data), что достигается путём вызова функции Poll, которая экспонирована интерфейсом устройства ввода (input device interface). Вызов этой функции должен осуществляться хотя бы 1 раз в каждом кадре, для того, чтобы поддерживать информацию о вводе с данного устройства в актуальном состоянии. Как только устройство (клавиатура) было опрошено, вызываем функцию GetDeviceState для получения его состояния. Здесь в качестве второго параметра передаётся адрес массива m_keyState, состоящий из 256 элементов типа BYTE. GetDeviceState позволяет получить введённые с устройства ввода данные и сохранить их для дальнейшего использования. После её вызова в массив помещаются сведения о текущем состоянии клавиатуры. Причём каждая клавиша будет представлена одним элементом массива.
Закрыть
noteОбрати внимание

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

По окончании работы GetDeviceState наш массив будет содержать значения типа char для каждой из нажатых на клавиатуре клавиш. Таким образом, массив m_keyState будет наглядно отображать, какие из клавиш были нажаты в данном кадре.
В цикле if мы применяем в строенный в DirectX макрос SUCCEEDED (от англ. "успешно") для проверки успешного выполнения функции GetDeviceState, стоящей в условии. В случае успеха, цикл while прерывается инструкцией break, т.к. в этом случае у нас есть введённые пользователем данные для данного кадра. В случае неудачного завершения функции, начинаем выяснять причины:
Фрагмент Input.cpp (Проект Engine)
...
		if( result != DIERR_INPUTLOST && result != DIERR_NOTACQUIRED )
			return;

		// Reacquire the device if the focus was lost.
		// Перезахватываем устройство ввода в случае
		// утери окном фокуса.
		if( FAILED( m_keyboard->Acquire() ) )
			return;
...

Нас интересуют 2 наиболее вероятные причины этого:
  • Устройство утеряно. В этом случае функция возвращает DIERR_INPUTLOST.
  • Устройство не было захвачено (aquired). В этом случае функция возвращает DIERR_NOTACQUIRED. В обоих случаях единственным решением является перезахват устройства ввода функцией Aquire, вызываемой чуть ниже и обрамлённой условным оператором if.
После захвата клавиатуры, сразу приступаем к проверке ввода с компьютерной мыши. Здесь процесс во многом аналогичен проверке ввода с клавиатуры. Основное отличие - в способе сохранения введённых данных при вызове функции GetDeviceState. Вместо массива значений типа char, мы используем специальную структуру DIMOUSESTATE, которая позволяет хранить информацию о перемещении мыши, прокрутке колеса (скроллинг), и нажатии на ней до четырёх кнопок.
Закрыть
noteЛюбопытно

Для поддержки мыши с более чем 4-мя кнопками применяют структуру DIMOUSESTATE2, которая поддерживает мыши, оснащённые не более чем 8 кнопками.

Последний шаг включает в себя вычисление позиции экранного курсора мыши и инкрементирование (увеличение на 1) штампа нажатия (press stamp):
Фрагмент Input.cpp (Проект Engine)
...
	// Get the relative position of the mouse.
	// Получаем относительные координаты положения (курсора) мыши.
	GetCursorPos( &m_position );
	ScreenToClient( m_window, &m_position );

	// Increment the press stamp.
	// Инкрементируем штамп нажатия.
	m_pressStamp++;
...

Функция GetCursorPos запрашивает координаты курсора мыши на экране и сохраняет их в структуре m_position (тип POINT). Функция ScreenToClient затем конвертирует эти данные в координаты, относительные окна приложения. После такой обработки координата (0, 0) соответствует левому верхнему углу окна приложения, а не экрана.
Структура типа POINT имеет компоненты x и y, которые являются координатами точки (в нашем случае - курсора) по соответствующим осям, условно проведённым из левого верхнего угла экрана (см. Рис. 2).

Получение пользовательского ввода

Как только класс Input был обновлён (updated), ты можешь приступать к получению вводимых данных с помощью специальных функций:
  • Функция для проверки нажатия клавиш на клавиатуре.
  • Функция для проверки нажатия кнопок мыши.
  • Функции для получения позиции мыши, вычисления её перемещения, а также детекции изменения положения колеса прокрутки.
Функция GetKeyPress возвращает TRUE в случае, когда регистрируется нажатие (одной!) предустановленной клавиши на клавиатуре.
Фрагмент Input.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Returns true if the given key is pressed.
// Note: Consistent presses will return false when using the press stamp.
// Возвращаем TRUE в случае нажатия клавиши.
// Примечание: В случае применения штампа нажатия, длительные нажатия вернут FALSE.
//-----------------------------------------------------------------------------
bool Input::GetKeyPress( char key, bool ignorePressStamp )
{
	if( ( m_keyState[key] & 0x80 ) == false )
		return false;

	bool pressed = true;

	if( ignorePressStamp == false )
		if( m_keyPressStamp[key] == m_pressStamp - 1 || m_keyPressStamp[key] == m_pressStamp )
			pressed = false;

	m_keyPressStamp[key] = m_pressStamp;

	return pressed;
}
...

В DirectInput для каждой клавиши предопределены специальные макросы (предустановленные скан-коды клавиш типа char, соответствующие каждой клавише клавиатуры), используя которые в качестве индексов можно определить, была ли нажата клавиша в момент вызова GetDeviceState. Например, DIK_ESCAPE - индекс клавиши Esc, а DIK_SPACE - это индекс клавиши пробела. Список всех индексов можно найти в файле DINPUT.H. Вот некоторые из них:
DIK_W DIK_UP
DIK_A DIK_LEFT
DIK_S DIK_DOWN
DIK_D DIK_RIGHT
DIK_SPACE DIK_LCONTROL

Функция GetKeyPress также принимает второй параметр ignorePressStamp, который по умолчанию установлен в FALSE. Если помнишь, в нашем классе Input есть переменная PressStamp, значение которой увеличивается в каждом кадре. Она применяется для отслеживания момента времени (или кадра), когда каждая из клавиш была нажата в прошлый раз. Основной причиной использования штампа нажатия является необходимость предотвращения сразу нескольких регистраций нажатия одних и тех же клавиш в данном кадре, в тех случаях когда это нежелательно. Представь, что ты хочешь проверить, нажал ли игрок пробел, для переключения таким образом специального флага (например, переключатель в игре, имеющий 2 положения: "вкл." и "выкл."). Если игрок нажмёт клавишу "пробел" и продолжит удерживать её в нажатом положении, то это нажатие будет регистрировать в каждом кадре и флаг будет постоянно переключаться из одного положения в другое с вполне внушительной скоростью. Для избежания подобных ситуаций как раз и служит штамп нажатия. Когда нажимается клавиша и ты проверяешь это в первый раз, её PressStamp также устанавливается в нужное значение и параметр IgnorePressStamp возвращает TRUE. Затем, если ты проверяешь эту клавишу снова (в этом кадре или спустя несколько кадров), IgnorePressStamp возвращает FALSE, таким образом давая знать, что данная клавиша уже была нажата и проверена ранее (т.е. более позднее нажатие в этом случае считается ложным и потому обрабатываться не будет). Несмотря на то, что использовать штамп нажатия совсем не трудно, эта метода полностью соответствует нашим целям.

Для получения нажатых кнопок мыши мы применяем очень похожую функцию GetButtonPress. Единственная разница заключается в том, что мы проверяем нажатие всего четырёх кнопок. И здесь нам необходимо передать номер проверяемой кнопки, который может принимать значения: 0 (для первой кнопки), 1 (для второй), 2 (для третьей), 3 (для четвёртой).
Фрагмент Input.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Returns true if the given button is pressed.
// Note: Consistent presses will return false when using the press stamp.
// Возвращаем TRUE в случае нажатия кнопки мыши.
// Примечание: В случае применения штампа нажатия, длительные нажатия вернут FALSE.
//-----------------------------------------------------------------------------
bool Input::GetButtonPress( char button, bool ignorePressStamp )
{
	if( ( m_mouseState.rgbButtons[button] & 0x80 ) == false )
		return false;

	bool pressed = true;

	if( ignorePressStamp == false )
		if( m_buttonPressStamp[button] == m_pressStamp - 1 || m_buttonPressStamp[button] == m_pressStamp )
			pressed = false;

	m_buttonPressStamp[button] = m_pressStamp;

	return pressed;
}
...

Затем мы применяем компонент rgbButtons структуры MOUSESTATE для проверки нажатия. Здесь параметр IgnorePressStamp работает точно так же.

Следующие 2 функции GetPosX и GetPosY позволяют получить соответствующие координаты курсора мыши по осям x и y (уже в пространстве окна приложения):
Фрагмент Input.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Returns the x position of the mouse.
// Возвращает положение курсора мыши по оси X
//-----------------------------------------------------------------------------
long Input::GetPosX()
{
	return m_position.x;
}

//-----------------------------------------------------------------------------
// Returns the y position of the mouse.
// Возвращает положение курсора мыши по оси Y
//-----------------------------------------------------------------------------
long Input::GetPosY()
{
	return m_position.y;
}

//-----------------------------------------------------------------------------
// Returns the change in the mouse's x coordinate.
// Возвращает изменение X-координаты курсора мыши.
//-----------------------------------------------------------------------------
long Input::GetDeltaX()
{
	return m_mouseState.lX;
}

//-----------------------------------------------------------------------------
// Returns the change in the mouse's y coordinate.
// Возвращает изменение Y-координаты курсора мыши.
//-----------------------------------------------------------------------------
long Input::GetDeltaY()
{
	return m_mouseState.lY;
}

//-----------------------------------------------------------------------------
// Returns the change in the mouse's scroll wheel.
// Возвращает изменение положения колеса прокрутки мыши.
//-----------------------------------------------------------------------------
long Input::GetDeltaWheel()
{
	return m_mouseState.lZ;
}
...

Функции GetDeltaX и GetDeltaY позволяют получить перемещение (смещение) курсора мыши по осям x и y, по сравнению с предыдущим кадром.
Последняя функция - GetDeltaWheel - даёт нам изменение положения колеса прокрутки мыши, по сравнению с предыдущим кадром.

Интегрируем систему пользовательского ввода в движок

Принцип тот же, что и при интеграции системы стейтов.

Изменения в Input.cpp

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

Изменения в Engine.h

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

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

  • Добавь строку Input *m_input; в файл Engine.h, в самом конце секции private объявления класса Engine:
Фрагмент Engine.h (Проект Engine)
...
	LinkedList< State > *m_states; // Связный список (Linked list) стейтов.
	State *m_currentState; // Указатель на текущий стейт.
	bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре.
	Input *m_input;
...

Таким образом мы создаём переменную Input, в которой будет храниться указатель на экземпляр класса Input.
  • Добавь строку Input *GetInput(); в файл Engine.h, в самом конце секции public объявления класса Engine.
Создание функции GetInput позволяет получить доступ к закрытой (private) переменной *m_input (что позднее мы опишем в её реализации). После внесения этих изменений объявление класса Engine будет выглядеть так:
Фрагмент 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();
	Input *GetInput();

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; // Флаг показывает, изменён ли стейт в текущем кадре.
	Input *m_input;
};
...


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

  • Добавь реализацию функци GetInput в самый конец файла Engine.cpp:
Фрагмент Engine.cpp (Проект Engine)
...
//---------------------------------------
// Возвращает указатель на объект input.
//---------------------------------------
Input *Engine::GetInput()
{
	return m_input;
}
...


В конструкторе класса Engine необходимо создать экземпляр класса Input и присвоить ему созданный ранее указатель *m_input.
  • Добавь строку m_input = new Input( m_window ); в файл Engine.cpp, чуть ниже строк, создающих связный список стейтов:
Фрагмент 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;

	// Создаём экземпляр класса Input.
	m_input = new Input( m_window );

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

	if( m_setup->StateSetup != NULL )
	{
		m_setup->StateSetup();
	}
...


В деструкторе класса Engine нам также необходимо уничтожить объект Input, применив макрос SAFE_DELETE.
  • Добавь строку SAFE_DELETE( m_input ); в файл Engine.cpp, в деструкторе класса Engine, сразу после удаления системы стейтов:
Фрагмент 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 );

			// Уничтожаем объект Input.
			SAFE_DELETE( m_input );
		}
	}
...


  • Добавь следующие строки...

m_input->Update();
if( m_input->GetKeyPress( DIK_F12 ) )
PostQuitMessage( 0 );

...в файл Engine.cpp, в функции Run, сразу после подсчёта затраченного времени, и, соответственно, ДО выполнения обновления текущего стейта:
Фрагмент Engine.cpp (Проект Engine)
...
			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;
				
				// Обновляем объект input, читаем ввод с клавиатуры и мыши.
				m_input->Update();
				// Проверяем нажатие кнопки F12.
				// Если да, то принудительно завершаем работу приложения.
				if( m_input->GetKeyPress( DIK_F12 ) )
				{
					PostQuitMessage( 0 );
				}
...

  • Сохрани Решение (Файл->Сохранить все).
Мы вызываем функцию Update класса Input для обновления данных о вводе с клавиатуры и мыши в каждом кадре. Затем проверяем, не нажал ли пользователь клавишу F12 на клавиатуре. Если да, то командуем движку немедленно закрываться, путём вызова функции PostQuitMessage. Эта функция посылает сообщение WM_QUIT окну нашего приложения, что также немедленно прерывает выполнение всего цикла while.

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

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

Модифицируем тестовое приложение (Проект Test)


В Главе 1.6 для проверки работоспособности движка в нашем Решении GameProjectOl мы создали второй Проект Test, после компиляции которого получили исполняемое приложение (файл .EXE), показывающее окно. Применив полученные в текущей Главе знания на практике, модифицируем его исходный код, оснастив приложение новым "функционалом". Если по каким-то причинам Проект Test отсутствует в Решении GameProjectOl, создай его, следуя инструкциям Главы 1.6$ . ОК, приступаем.
  • Открой для редактирования файл исходного кода Main.cpp (Проект Test) и замени содержащийся в нём код на следующий:
Main.cpp (Проект Test)
//-----------------------------------------------------------------------------
// System Includes
//-----------------------------------------------------------------------------
#include <windows.h>

//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "..\GameProject01\Engine.h"

//-----------------------------------------------------------------------------
// Test State Class
//-----------------------------------------------------------------------------
class TestState : public State
{
	//-------------------------------------------------------------------------
	// Update function for the TestState.
	//-------------------------------------------------------------------------
	virtual void Update( float elapsed )
	{
		// Check if the user wants to exit.
		if( g_engine->GetInput()->GetKeyPress( DIK_Q ) )
			PostQuitMessage( 0 );
	};
};

//-----------------------------------------------------------------------------
// Application specific state setup.
//-----------------------------------------------------------------------------
void StateSetup()
{
	g_engine->AddState( new TestState, true );
}

//-----------------------------------------------------------------------------
// Entry point for the application.
//-----------------------------------------------------------------------------
int WINAPI WinMain( HINSTANCE instance, HINSTANCE prev, LPSTR cmdLine, int cmdShow )
{
	// Create the engine setup structure.
	EngineSetup setup;
	setup.instance = instance;
	setup.name = "Engine Control Test";
	setup.StateSetup = StateSetup;

	// Create the engine (using the setup structure), then run it.
	new Engine( &setup );
	g_engine->Run();

	return true;
}

И вновь в нашем Проекте Test всего 1 файл Main.cpp, который содержит весь исходный код тестового приложения. Перед нами готовые системы стейтов и пользовательского ввода в действии.

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

В самом начале листинга мы объявляем новый класс TestState, который сам по себе является новым стейтом, который мы создали на базе класса State ("ответвили" от него). Затем мы переопределили (override) функцию Update и внесли в неё проверку нажатия пользователем клавиши <q> на клавиатуре. При её нажатии происходит закрытие окна и выход из приложения.
Чуть ниже видим вызов функции StateSetup, которая затем передаётся движку путём её указания в структуре EngineSetup. Это позволяет нам добавить новый стейт в систему ещё на стадии создания движка. Всё максимально логично и требует минимум времени для реализации!

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

Подготовка Проекта Test к перекомпиляции

Напомним, что для успешной компиляции библиотека Engine должна быть добавлена в Проект Test (см. Главу 1.6). Помимо этого, MS Visual C++ 2010 при компиляции нашего обновлённого Проекта Test активно юзает внешние библиотеки winmm.lib, dxguid.lib и dinput8.lib и требует их принудительного указания в настройках Проекта. И если winmm.lib мы уже добавляли ранее, то 2 другие библиотеки добавим сейчас. Для этого:
  • В Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта Test.
  • Во всплывающем контекстном меню выбираем пункт "Свойства" (Project->Properties).
  • В появившемся окне: Свойства конфигурации -> Компоновщик -> Ввод (Configuration Properties->Linker->Input). Во всплывающем списке напротив пункта "Дополнительные зависимости" (Additional Dependencies) выбираем "Изменить..." (Edit).
  • В появившемся окне "Дополнительные зависимости" в поле ввода должны быть указаны (обязательно в столбик и без запятых!) в общей сложности 5 библиотек:
d3d9.lib
d3dx9.lib
dxguid.lib
dinput8.lib
winmm.lib
  • Жмём ОК, ОК.
  • Сохрани изменения в Решении (Файл->Сохранить все).
Библиотеки Direct3D мы пока не используем, но оставим их в списке уже сейчас.

Перекомпилируем Проект Test

  • В Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта Test.
  • Во всплывающем контекстном меню выбираем: Перестроить.

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

При настройках по умолчанию таким образом перекомпилируется только Проект Test. Для компиляции всего Решения:
  • Выбери в Главном меню "Отладка->Построить решение".
  • Или нажми F7.
  • Или в Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта "Test". Во всплывающем контекстном меню выбери пункт "Зависимости проектов..." и в появившемся окне отметь галкой Проект Engine. Жми ОК.
Теперь каждая компиляция Проекта Test "потянет" за собой обязательную перекомпиляцию Проекта Engine. Как результат, в Проекте Test всегда будет самая свежая и актуальная версия библиотеки Engine.lib.

Компиляция завершилась успешно. Итоговый исполняемый двоичный файл (Test.exe) расположен на жёстком диске ПК в той же директории, что и библиотека движка.
В нашем случае (Win7 x64) это: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\Debug. В разных ОС путь к файлу может отличаться от представленного.
  • Найди и запусти получившееся приложение.
На экране появится окно с белым фоном размером 800х600 точек. Примечательно, что приложение теперь поддерживает аж 2 клавиши, дающие команду на закрытие окна и досрочное завершение работы (<Q> и <F12>).
  • Проверь их работу.
Обработчик одной из них (F12) размещён в коде, специфичном для движка (в функции Run, файла Engine.cpp; на случай, если игрокодер "прозевает" кнопку выхода из игры). А второй - в коде приложения Test, где при выполнении стейта TestState обрабатывается нажатие клавиши <q>:
Фрагмент Main.cpp (Проект Test)
...
//-----------------------------------------------------------------------------
// Test State Class
//-----------------------------------------------------------------------------
class TestState : public State
{
	//-------------------------------------------------------------------------
	// Update function for the TestState.
	//-------------------------------------------------------------------------
	virtual void Update( float elapsed )
	{
		// Check if the user wants to exit.
		if( g_engine->GetInput()->GetKeyPress( DIK_Q ) )
			PostQuitMessage( 0 );
	};
};
...


Итоги Глав 1.7 и 1.8

В ходе изучения двух последних глав мы рассмотрели 2 интересные темы: процессинг движка и пользовательский ввод. Мы разработали систему стейтов, которая позволяет нам контролировать, что именно должен процессить наш движок в каждой из ситуаций. Мы также разработали систему пользовательского ввода, которая поддерживает клавиатуру и мышь. Мы также выполнили кучу всевозможных операций по интегрированию этих двух систем в наш движок, который постепенно становится больше и функциональней, но, в то же время, по-прежнему отлично структурирован и легко читаем.
Мы создали тестовое приложение, которое пока не производит особого впечатления, т.к. не выводит на экран забавную 3D-графику и не проигрывает звуки. Но изучение этих тем ещё впереди, так что наберись терпения.
По сути, вся "чёрная" работа по разработке основных систем движка уже позади. Дальше начинается самое интересное -добавление в движок всевозможных "фишек".
В следующей главе мы оснастим наш движок поддержкой скриптов (в Counter Strike и других играх они больше известны как файлы конфигурации или "конфиги"). Как видишь, нам без этого тоже не обойтись.

Источники


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


ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.9 Добавляем поддержку скриптов (scripting)

Последние изменения страницы Понедельник 11 / Июль, 2022 11:35:22 MSK

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

No records to display

Search Wiki Page

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

Категории

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