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

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


Наш движок будет поддерживать всего 2 устройства ввода - клавиатуру и мышь. Можно, конечно, добавить поддержку и других устройств, например геймпада или руля. Но, как правило, в FPS они не используются. В данный момент мы даже не реализовали поддержку нажатия каких-либо клавиш в игре, т.к. это строго код, специфичный для игры. Вместо этого, мы создадим специальный класс-обёртку, который будет заключать в себе основной функционал DirectInput, предлагая его в виде нескольких высокоуровневых интерфейсов. По сути, этот специальный класс (назовём его Input) будет являться своеобразной надстройкой или "обёрткой" для классов и функций DirectInput. Поэтому здесь и далее будем называть его враппер или класс-обёртка (от англ "wrapper" - обёртка). Это означает, что многие внутренние DirectX операции будут скрыты от игрокодера и тот сможет более легко внедрять пользовательский ввод в своё игрвое приложение.

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

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

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

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

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

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

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

Input.h (Проект Engine)
//-----------------------------------------------------------------------------
// Класс-обёртка для поддержки пользовательского ввода через DirectInput.
// Поддерживаются клавиатура и мышь.
//
// Original sourcecode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef INPUT_H
#define INPUT_H

//-----------------------------------------------------------------------------
// Класс Input
//-----------------------------------------------------------------------------
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; // Дескриптор родительского окна.
	IDirectInput8 *m_di; // объект DirectInput.
	unsigned long m_pressStamp; // Текущая отметка нажатия (увеличивается с каждым кадром).

	IDirectInputDevice8 *m_keyboard; // Устройство DirectInput клавиатура.
	char m_keyState[256]; // Сохраняет состояние клавиш.
	unsigned long m_keyPressStamp[256]; // Отметка о нажатии каждой из клавиш в предыдущем кадре.

	IDirectInputDevice8 *m_mouse; // Устройство DirectInput мышь.
	DIMOUSESTATE m_mouseState; // Хранит состояние кнопок мыши.
	unsigned long m_buttonPressStamp[3]; // Отметка о нажатии каждой из кнопок мыши в предыдущем кадре.
	POINT m_position; // Хранит текущую позицию курсора мыши на экране.
};

#endif

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


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

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

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

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

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

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

Input.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация функций пользовательского ввода, объявленных в Input.h.
// Обратись к интерфейсам Input.h за более подробной информацией.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

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

	// Создаём интерфейс DirectInput.
	DirectInput8Create( GetModuleHandle( NULL ), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&m_di, NULL );

	// Создаём, подготавливаем, и захватываем (aquire) устройство клавиатуру.
	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();

	// Создаём, подготавливаем, и захватываем (aquire) устройство мышь.
	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();

	// Стартуем отметку о нажатии (press stamp).
	m_pressStamp = 0;
}

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

//-----------------------------------------------------------------------------
// Обновляет (Updates) состояние обоих устройств: клавиатуры и мыши.
//-----------------------------------------------------------------------------
void Input::Update()
{
	static HRESULT result;

	// Опрашиваем (Poll) клавиатуру до тех пор, пока операция не завершится успехом или вернёт неизвестную ошибку.
	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) устройство, если фокус был утерян.
		if( FAILED( m_keyboard->Acquire() ) )
			return;
	}

	// Опрашиваем (Poll) мышь до тех пор, пока операция не завершится успехом или вернёт неизвестную ошибку.
	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) устройство, если фокус был утерян.
		if( FAILED( m_mouse->Acquire() ) )
			return;
	}

	// Запрашиваем относительную позицию мыши.
	GetCursorPos( &m_position );
	ScreenToClient( m_window, &m_position );

	// Инкрементируем (увеличиваем на 1) отметку о нажатии (press stamp).
	m_pressStamp++;
}

//-----------------------------------------------------------------------------
// Возвращает true (истина) если была нажата данная клавиша.
// Примечание: Продолжительные нажатия вернут false при использовании press stamp.
//-----------------------------------------------------------------------------
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;
}

//-----------------------------------------------------------------------------
// Возвращает true (истина) если была нажата данная клавиша.
// Примечание: Продолжительные нажатия вернут false при использовании press stamp.
//-----------------------------------------------------------------------------
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;
}

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

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

//-----------------------------------------------------------------------------
// Возвращает изменение координаты мыши по оси x (смещение по оси x).
//-----------------------------------------------------------------------------
long Input::GetDeltaX()
{
	return m_mouseState.lX;
}

//-----------------------------------------------------------------------------
// Возвращает изменение координаты мыши по оси y (смещение по оси y).
//-----------------------------------------------------------------------------
long Input::GetDeltaY()
{
	return m_mouseState.lY;
}

//-----------------------------------------------------------------------------
// Возвращает изменение положения (смещение) колеса прокрутки мыши.
//-----------------------------------------------------------------------------
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 )
{
	// Сохраняем дескриптор родительского окна во внутренней переменной класса Input.
	m_window = window;
...

Сразу после этого приступаем к самой главной задаче - созданию интерфейса DirectInput:

Input.cpp (Проект Engine)
...
	// Создаём интерфейс DirectInput.
	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)
...
	// Создаём, подготавливаем, и захватываем (aquire) устройство клавиатуру.
	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)
...
	// Создаём, подготавливаем, и захватываем (aquire) устройство мышь.
	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)
...
	// Стартуем отметку о нажатии (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) состояние обоих устройств: клавиатуры и мыши.
//-----------------------------------------------------------------------------
void Input::Update()
{
	static HRESULT result;

	// Опрашиваем (Poll) клавиатуру до тех пор, пока операция не завершится успехом или вернёт неизвестную ошибку.
	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) устройство, если фокус был утерян.
		if( FAILED( m_keyboard->Acquire() ) )
			return;
	}

	// Опрашиваем (Poll) мышь до тех пор, пока операция не завершится успехом или вернёт неизвестную ошибку.
	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) устройство, если фокус был утерян.
		if( FAILED( m_mouse->Acquire() ) )
			return;
	}

	// Запрашиваем относительную позицию мыши.
	GetCursorPos( &m_position );
	ScreenToClient( m_window, &m_position );

	// Инкрементируем (увеличиваем на 1) отметку о нажатии (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) устройство, если фокус был утерян.
		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)
...

	// Запрашиваем относительную позицию мыши.
	GetCursorPos( &m_position );
	ScreenToClient( m_window, &m_position );

	// Инкрементируем (увеличиваем на 1) отметку о нажатии (press stamp).
	m_pressStamp++;
}
...
Рис. 2 Координатная система курсора мыши
Рис. 2 Координатная система курсора мыши

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

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

Как только класс Input был обновлён (updated), ты можешь приступать к получению вводимых данных с помощью специальных функций:

  • Функция для проверки нажатия клавиш на клавиатуре.
  • Функция для проверки нажатия кнопок мыши.
  • Функции для получения позиции мыши, вычисления её перемещения, а также детекции изменения положения колеса прокрутки.


Функция GetKeyPress возвращает TRUE в случае, когда регистрируется нажатие (одной!) предустановленной клавиши на клавиатуре.

Input.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает true (истина) если была нажата данная клавиша.
// Примечание: Продолжительные нажатия вернут false при использовании press stamp.
//-----------------------------------------------------------------------------
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)
...
//-----------------------------------------------------------------------------
// Возвращает true (истина) если была нажата данная клавиша.
// Примечание: Продолжительные нажатия вернут false при использовании press stamp.
//-----------------------------------------------------------------------------
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)
...
//-----------------------------------------------------------------------------
// Возвращает положение мыши по оси x.
//-----------------------------------------------------------------------------
long Input::GetPosX()
{
	return m_position.x;
}

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

//-----------------------------------------------------------------------------
// Возвращает изменение координаты мыши по оси x (смещение по оси x).
//-----------------------------------------------------------------------------
long Input::GetDeltaX()
{
	return m_mouseState.lX;
}

//-----------------------------------------------------------------------------
// Возвращает изменение координаты мыши по оси y (смещение по оси y).
//-----------------------------------------------------------------------------
long Input::GetDeltaY()
{
	return m_mouseState.lY;
}

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

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

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

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

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

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

  • Добавь инструкцию #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.

Таким образом мы создаём переменную Input, в которой будет храниться указатель на экземпляр класса Input.

  • Добавь строку Input *GetInput(); в файл Engine.h, в самом конце секции public объявления класса Engine.

Создание функции GetInput позволяет получить доступ к переменной *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; // Флаг показывает, загружен ли движок или нет.
	HWND m_window; // Дескриптор главного окна.
	bool m_deactive; // Флаг показывает, активно приложение или нет.

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

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

	Input *m_input; // Input object.
};
...

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

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

//-----------------------------------------------------------------------------
// Возвращает указатель на объект input.
//-----------------------------------------------------------------------------
Input *Engine::GetInput()
{
	return m_input;
}


В конструкторе класса Engine необходимо создать экземпляр класса Input и присвоить ему созданный ранее указатель *m_input:

  • Добавь строку m_input = new Input( m_window ); в файл Engine.cpp, чуть ниже строк, создающих связный список стейтов:
Фрагмент Engine.cpp (Проект Engine)
...
	// Создаём окно и возвращаем его дескриптор.
// Note: Позднее окно будет создаваться с флагом windowed/fullscreen (оконный/полноэкранный режимы).
	m_window = CreateWindowEx( WS_EX_TOPMOST, "WindowClass", (LPCWSTR)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 );

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

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

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


В деструкторе класса Engine нам также необходимо уничтожить объект Input, применив макрос SAFE_DELETE.

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

		// Уничтожаем связные списки со стейтами.
		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)
...
//-----------------------------------------------------------------------------
// Вводит движок в главный цикл выборки сообщений.
//-----------------------------------------------------------------------------
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;

				// Обновляем объект input, читаем ввод с клавиатуры и мыши.
				m_input->Update();

				// Проверяем нажатие кнопки F12,
				// и в этом случае принудительно завершаем работу приложения.
				if( m_input->GetKeyPress( DIK_F12 ) )
					PostQuitMessage( 0 );

				// Запрос вьюера текущего стейта (если таковой имеется).
				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 );
}
...

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


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

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

Итак, система пользовательского ввода полностью интегрирована в движок.
Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:

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

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

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

В Главе 1.6(external link) для проверки работоспособности движка в нашем Решении GameProject01 мы создали второй Проект Test, после компиляции которого получили исполняемое приложение (файл .EXE), показывающее окно. Применив полученные в текущей Главе знания на практике, модифицируем его исходный код, оснастив приложение новым "функционалом". Если по каким-то причинам Проект Test отсутствует в Решении GameProject01, создай его, следуя инструкциям Главы 1.6(external link) .
ОК, приступаем.

  • Открой для редактирования файл исходного кода 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, который содержит весь исходный код тестового приложения. Перед нами готовые системы стейтов и пользовательского ввода в действии.

Исследуем код

В самом начале листинга мы объявляем новый класс 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.
  • Во всплывающем контекстном меню выбираем пункт "Свойства".
  • В появившемся окне: Свойства конфигурации -> Компоновщик -> Ввод. Во всплывающем списке напротив пункта "Дополнительные зависимости" выбираем "Изменить...".
  • В появившемся окне "Дополнительные зависимости" в поле ввода должны быть указаны (обязательно в столбик и без запятых!) 3 библиотеки: winmm.lib, dxguid.lib и dinput8.lib.
  • Жмём ОК, ОК.
  • Сохрани изменения в Решении (Файл->Сохранить все).


Сразу после этого компилируем Проект Test:

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

Закрыть
noteНа заметку

При настройках по умолчанию таким образом перекомпилируется только Проект Test. Для компиляции всего Решения:

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


Компиляция завершилась успешно. Итоговый исполняемый двоичный файл (Test.exe) расположен на жёстком диске ПК в той же директории, что и библиотека движка.
В нашем случае, это: С:\Users\<Имя пользователя>\documents\visual studio 2010\Projects\GameProject01\Debug. (В разных ОС путь к файлу может отличаться от представленного).
Найди и запусти получившееся приложение.
Примечательно, что приложение теперь поддерживает аж 2 клавиши, дающие команду на закрытие окна и досрочное завершение работы (<Q> и <F12>). Обработчик одной из них (F12) размещён в коде, специфичном для движка (в функции Run, файла Engine.cpp; на случай, если игрокодер "прозевает" кнопку выхода из игры). А второй - в коде приложения Test, где при выполнении стейта TestState обрабатывается нажатие клавиши <q>:

Main.cpp (Проект Test)
...
//-----------------------------------------------------------------------------
// Test State Class
//-----------------------------------------------------------------------------
class TestState : public State
{
	//-------------------------------------------------------------------------
	// Функция Update для стейта TestState.
	//-------------------------------------------------------------------------
	virtual void Update( float elapsed )
	{
		// Проверяем, если пользователь хочет выйти из приложения.
		if( g_engine->GetInput()->GetKeyPress( DIK_Q ) )
			PostQuitMessage( 0 );
	};
};
...

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

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


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

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

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

No records to display

Хостинг