В первой части статьи были приведены шаги по созданию системы для считывания данных с устройств ввода, основанной на алиасах. Но в первой статье не был описан процесс создания приложения, который бы показывал преимущества использования подобной системы. В этой статье будет рассмотрено создание простой игры Pong, рассчитанной на игру вдвоем, с возможностью переназначить управление и возможностью назначить на действие не одну, а несколько клавиш, не ограничиваясь только клавиатурой. Мы рассмотрим не только мышь и несколько подключенных джойстиков, но и возможность назначать комбинацию клавиш, например W + Left Mouse Button, т.е. будет продемонстрирована максимальная гибкость в работе с устройствами ввода.
Для создания игры немного изменим код системы, описанной в первой части.
Вначале рассмотрим работу с джойстиками, включая обработку случая, когда к системе подключено несколько джойстиков. Реализовывать работу будем средствами XIpnut, т.к. работа с этой библиотекой максимально простая.
В файл, описывающий хардварные алиасы, добавляется такой блок:
Для работы с сами стейтами определяем следующие массивы
При инициализации говорим, что нет активных джойстиков:
В функции апдейта забираем стейты с подключенных на данный момент джойстиков:
Джойстиков у нас может быть несколько, поэтому при опросе хардварных алиасов необходимо передавать индекс устройства, с которого мы хотим считать данные. Методы будут выглядеть так:
При этом мы предполагаем, что если device_index равен -1, то это значит, что если подключены два джойстика и мы опрашиваем, нажата ли кнопка A, то нажатия с обоих джойстиков будут учтены, т.е. мы хотим получить активное значение с любого подключенного джойстика.
А теперь приведем код обработки хардварных алиасов джойстиков:
Последнее, что необходимо для полноценной работы с несколькими устройствами, — это возможность задавать требуемый номер устройства в самом алиасе. Потому необходимо обновить структуру:
Считывание алиасов теперь выглядит так:
Файл, описывающий алиасы, обрабатывающие отклонения стиков с двух джойстиков, выглядит так:
Я подробно описал ввод кода, работающего с джойстиками, для того чтобы показать, как организовывать работу опроса, если однотипных устройств несколько. Если вам будет нужно поддержать работу с 4 клавиатурами и 3 мышками, то вопросов, как это сделать, возникнуть не должно.
Теперь рассмотрим добавление функционала, который необходим для реализации переопределения управления. Первый метод, которым нам потребуется:
Метод проходит по всем хардварным алиасам, и если алиас стал активным, то вернется его строковое имя. Этот метод необходим для отслеживания нажатой клавиши в тот момент, когда в меню настройки клавиши управления ожидается нажатие клавиши от пользователя.
А теперь опишем механизм, который позволит делать обратное действие: говорить, какие именно хардварные кнопки назначены за алиасом. Для этого опишем структуру:
Эта структура будет хранить имя самого алиаса, его id, все проассоциированные алиасы (например клавиша W и Up отвечают за перемещение вперед) и комбинации алиасов (для выполнения кувырка в сторону надо нажать Left Shift и A). Заполняется все это в конструкторе. Также у этой структуры определен метод IsContainHAlias, чтобы понять, забинден ли хардварный алиас к данному алиасу. Этот метод может понадобиться, например, чтобы избежать назначения уже назначенного хардварного алиаса. Реализация этих методов следующая:
Теперь переходим к реализации самой игры. Она состоит из нескольких экранов: стартовое меню, меню с переопределением управления, сама игра с меню паузы. Т.к. во всех экранах есть меню, опишем базовый класс меню, который будет содержать функционал перемещения по элементам меню и активации элемента меню. Логику же каждого из экранов будем реализовывать в классах наследниках от класса Menu.
Вначале приведем файл, описывающий алиасы, которые будут использованы в самом меню:
Файл алиасов, которые будут использованы для управления битами игроков:
Теперь приведем реализацию базового класса Menu:
Переходим к реализации первого экрана, а именно — стартового экрана. В нем будут только два пункта: Start и Controls. Для этого экрана базового функционала более чем достаточно, поэтому приведем только инициализацию и калбеки на нажатие каждого пункта:
Второй экран, реализацию которого мы рассмотрим, — это экран настройки управления. Т.к. мы рассматриваем простую игру Pong, которая рассчитана на игру вдвоем, то наша задача — переопределить действия движения вверх и вниз битка для каждого игрока, т.е. есть суммарно 4 действия. Потому определяем массив, в котором будем хранить данные о мепинге алиаса, и проинициализируем его:
Данный код реализует назначение алиаса посредством опроса GetActivatedKey. Если активен алиас Menu.AddHotkey (Left Control), то задается сочетание клавиш. При активации алиаса Menu.StopEdit (Escаpe) происходит прекращение задания алиаса. При возврате в основное меню нужно произвести сохранение мепинга, и мы делаем это в колбеке:
Последний шаг — описание класса, реализующего игровой экран:
На этом все. На простом примере мы продемонстрировали простоту и гибкость при работе с системой опроса устройств ввода. Система не загромождена кодом и не изобилует лишними методами.
> Ссылка на пример использования работающей системы
Также эта система была написана для движка под названием Atum. Репозиторий всех исходников движка — в них много чего интересного.
Для создания игры немного изменим код системы, описанной в первой части.
Вначале рассмотрим работу с джойстиками, включая обработку случая, когда к системе подключено несколько джойстиков. Реализовывать работу будем средствами XIpnut, т.к. работа с этой библиотекой максимально простая.
В файл, описывающий хардварные алиасы, добавляется такой блок:
Описание алиасов
"joystick" :
[
{ "name" : "JOY_DPAD_UP", "index" : 1 },
{ "name" : "JOY_DPAD_DOWN", "index" : 2 },
{ "name" : "JOY_DPAD_LEFT", "index" : 4 },
{ "name" : "JOY_DPAD_RIGHT", "index" : 8 },
{ "name" : "JOY_START", "index" : 16 },
{ "name" : "JOY_BACK", "index" : 32 },
{ "name" : "JOY_LEFT_THUMB", "index" : 64 },
{ "name" : "JOY_RIGHT_THUMB", "index" : 128 },
{ "name" : "JOY_LEFT_SHOULDER", "index" : 256 },
{ "name" : "JOY_RIGHT_SHOULDER", "index" : 512 },
{ "name" : "JOY_A", "index" : 4096 },
{ "name" : "JOY_B", "index" : 8192 },
{ "name" : "JOY_X", "index" : 16384 },
{ "name" : "JOY_Y", "index" : 32768 },
{ "name" : "JOY_LEFT_STICK_H", "index" : 100 },
{ "name" : "JOY_LEFT_STICK_NEGH", "index" : 101 },
{ "name" : "JOY_LEFT_STICK_V", "index" : 102 },
{ "name" : "JOY_LEFT_STICK_NEGV", "index" : 103 },
{ "name" : "JOY_LEFT_TRIGER", "index" : 104 },
{ "name" : "JOY_RIGHT_STICK_H", "index" : 105 },
{ "name" : "JOY_RIGHT_STICK_NEGH", "index" : 106 },
{ "name" : "JOY_RIGHT_STICK_V", "index" : 107 },
{ "name" : "JOY_RIGHT_STICK_NEGV", "index" : 108 },
{ "name" : "JOY_RIGHT_TRIGER", "index" : 109 }
]
Для работы с сами стейтами определяем следующие массивы
XINPUT_STATE joy_prev_states[XUSER_MAX_COUNT];
XINPUT_STATE joy_states[XUSER_MAX_COUNT];
bool joy_active[XUSER_MAX_COUNT];
При инициализации говорим, что нет активных джойстиков:
for (int i = 0; i< XUSER_MAX_COUNT; i++)
{
joy_active[i] = false;
}
В функции апдейта забираем стейты с подключенных на данный момент джойстиков:
...
for (DWORD i = 0; i < XUSER_MAX_COUNT; i++)
{
if (joy_active[i])
{
memcpy(&joy_prev_states[i], &joy_states[i], sizeof(XINPUT_STATE));
}
ZeroMemory(&joy_states[i], sizeof(XINPUT_STATE));
if (XInputGetState(i, &joy_states[i]) == ERROR_SUCCESS)
{
if (!joy_active[i])
{
memcpy(&joy_prev_states[i], &joy_states[i], sizeof(XINPUT_STATE));
}
joy_active[i] = true;
}
else
{
joy_active[i] = false;
}
}
...
Джойстиков у нас может быть несколько, поэтому при опросе хардварных алиасов необходимо передавать индекс устройства, с которого мы хотим считать данные. Методы будут выглядеть так:
bool GetHardwareAliasState(int alias, AliasAction action, int device_index);
float GetHardwareAliasValue(int alias, bool delta, int device_index);
При этом мы предполагаем, что если device_index равен -1, то это значит, что если подключены два джойстика и мы опрашиваем, нажата ли кнопка A, то нажатия с обоих джойстиков будут учтены, т.е. мы хотим получить активное значение с любого подключенного джойстика.
А теперь приведем код обработки хардварных алиасов джойстиков:
Обработка хардварных алиасов
bool Controls::GetHardwareAliasState(int index, AliasAction action, int device_index)
{
HardwareAlias& halias = haliases[index];
switch (halias.device)
{
case Joystick:
{
if (halias.index<100 || halias.index > 109)
{
for (int i = 0; i < XUSER_MAX_COUNT; i++)
{
if (!joy_active[i])
{
continue;
}
bool res = false;
if (device_index != -1 && device_index != i)
{
continue;
}
int index = i;
if (action == Activated)
{
res = (!(joy_prev_states[index].Gamepad.wButtons & halias.index) &&
joy_states[index].Gamepad.wButtons & halias.index);
}
if (action == Active)
{
res = joy_states[index].Gamepad.wButtons & halias.index;
}
if (res)
{
return true;
}
}
}
else
{
float val = GetHardwareAliasValue(index, false, device_index);
if (action == Active)
{
return val > 0.99f;
}
float prev_val = val - GetHardwareAliasValue(index, true, device_index);
return (val > 0.99f) && (prev_val < 0.99f);
}
break;
}
...
}
return false;
}
inline float GetJoyTrigerValue(float val)
{
return val / 255.0f;
}
inline float GetJoyStickValue(float val)
{
val = fmaxf(-1, (float)val / 32767);
float deadzone = 0.05f;
val = (abs(val) < deadzone ? 0 : (abs(val) - deadzone) * (val / abs(val)));
return val /= 1.0f - deadzone;
}
float Controls::GetHardwareAliasValue(int index, bool delta, int device_index)
{
HardwareAlias& halias = haliases[index];
switch (halias.device)
{
case Joystick:
{
if (halias.index >= 100 && halias.index <= 109)
{
float val = 0.0f;
for (int i = 0; i < XUSER_MAX_COUNT; i++)
{
if (!joy_active[i])
{
continue;
}
if (device_index != -1 && device_index != i)
{
continue;
}
int index = i;
if (halias.index == 100 || halias.index == 101)
{
val = GetJoyStickValue((float)joy_states[index].Gamepad.sThumbLX);
if (delta)
{
val = val - GetJoyStickValue((float)joy_prev_states[index].Gamepad.sThumbLX);
}
if (halias.index == 101)
{
val = -val;
}
}
else
if (halias.index == 102 || halias.index == 103)
{
val = GetJoyStickValue((float)joy_states[index].Gamepad.sThumbLY);
if (delta)
{
val = val - GetJoyStickValue((float)joy_prev_states[index].Gamepad.sThumbLY);
}
if (halias.index == 103)
{
val = -val;
}
}
else
if (halias.index == 104)
{
val = GetJoyTrigerValue((float)joy_states[index].Gamepad.bLeftTrigger);
if (delta)
{
val = val - GetJoyTrigerValue((float)joy_prev_states[index].Gamepad.bLeftTrigger);
}
}
else
if (halias.index == 105 || halias.index == 106)
{
val = GetJoyStickValue((float)joy_states[index].Gamepad.sThumbRX);
if (delta)
{
val = val - GetJoyStickValue((float)joy_prev_states[index].Gamepad.sThumbRX);
}
if (halias.index == 106)
{
val = -val;
}
}
else
if (halias.index == 107 || halias.index == 108)
{
val = GetJoyStickValue((float)joy_states[index].Gamepad.sThumbRY);
if (delta)
{
val = val - GetJoyStickValue((float)joy_prev_states[index].Gamepad.sThumbRY);
}
if (halias.index == 108)
{
val = -val;
}
}
else
if (halias.index == 109)
{
val = GetJoyTrigerValue((float)joy_states[index].Gamepad.bRightTrigger);
if (delta)
{
val = val - GetJoyTrigerValue((float)joy_prev_states[index].Gamepad.bRightTrigger);
}
}
if (fabs(val) > 0.01f)
{
break;
}
}
return val;
}
else
{
return GetHardwareAliasState(index, Active, device_index) ? 1.0f : 0.0f;
}
break;
}
...
}
return 0.0f;
}
Последнее, что необходимо для полноценной работы с несколькими устройствами, — это возможность задавать требуемый номер устройства в самом алиасе. Потому необходимо обновить структуру:
struct AliasRefState
{
std::string name;
int aliasIndex = -1;
bool refer2hardware = false;
int device_index = -1; // добавили поле
};
Считывание алиасов теперь выглядит так:
Считывание алиасов
bool Controls::LoadAliases(const char* name_aliases)
{
JSONReader* reader = new JSONReader();
bool res = false;
if (reader->Parse(name_aliases))
{
res = true;
while (reader->EnterBlock("Aliases"))
{
std::string name;
reader->Read("name", name);
int index = GetAlias(name.c_str());
Alias* alias;
if (index == -1)
{
aliases.push_back(Alias());
alias = &aliases.back();
alias->name = name;
aliasesMap[name] = (int)aliases.size() - 1;
}
else
{
alias = &aliases[index];
alias->aliasesRef.clear();
}
while (reader->EnterBlock("AliasesRef"))
{
alias->aliasesRef.push_back(AliasRef());
AliasRef& aliasRef = alias->aliasesRef.back();
while (reader->EnterBlock("names"))
{
string name;
if (reader->IsString("") && reader->Read("", name))
{
aliasRef.refs.push_back(AliasRefState());
aliasRef.refs.back().name = name;
}
else
{
if (aliasRef.refs.size() != 0)
{
reader->Read("", aliasRef.refs.back().device_index);
}
}
reader->LeaveBlock();
}
reader->Read("modifier", aliasRef.modifier);
reader->LeaveBlock();
}
reader->LeaveBlock();
}
ResolveAliases();
}
reader->Release();
return res;
}
Файл, описывающий алиасы, обрабатывающие отклонения стиков с двух джойстиков, выглядит так:
Описание алиасов
{
"Aliases" : [
{
"name" : "Player1.Up",
"AliasesRef" : [
{
"names" : [
"JOY_LEFT_STICK_V",
0
]
}
]
},
{
"name" : "Player1.Down",
"AliasesRef" : [
{
"names" : [
"JOY_LEFT_STICK_NEGV",
0
]
}
]
},
{
"name" : "Player2.Up",
"AliasesRef" : [
{
"names" : [
"JOY_LEFT_STICK_V",
1
]
}
]
},
{
"name" : "Player2.Down",
"AliasesRef" : [
{
"names" : [
"JOY_LEFT_STICK_NEGV",
1
]
}
]
}
]
}
Я подробно описал ввод кода, работающего с джойстиками, для того чтобы показать, как организовывать работу опроса, если однотипных устройств несколько. Если вам будет нужно поддержать работу с 4 клавиатурами и 3 мышками, то вопросов, как это сделать, возникнуть не должно.
Теперь рассмотрим добавление функционала, который необходим для реализации переопределения управления. Первый метод, которым нам потребуется:
const char* Controls::GetActivatedKey(int& device_index)
{
for (auto& halias : haliases)
{
int index = &halias - &haliases[0];
int count = 1;
if (halias.device == Joystick)
{
count = XUSER_MAX_COUNT;
}
for (device_index = 0; device_index<count; device_index++)
{
if (GetHardwareAliasState(index, Activated, device_index))
{
return halias.name.c_str();
}
}
}
return nullptr;
}
Метод проходит по всем хардварным алиасам, и если алиас стал активным, то вернется его строковое имя. Этот метод необходим для отслеживания нажатой клавиши в тот момент, когда в меню настройки клавиши управления ожидается нажатие клавиши от пользователя.
А теперь опишем механизм, который позволит делать обратное действие: говорить, какие именно хардварные кнопки назначены за алиасом. Для этого опишем структуру:
struct AliasMappig
{
std::string name;
int alias = -1;
struct BindName
{
int device_index = -1;
std::string name;
};
std::vector<std::vector<BindName>> bindedNames;
AliasMappig(const char* name);
bool IsContainHAlias(const char* halias);
};
Эта структура будет хранить имя самого алиаса, его id, все проассоциированные алиасы (например клавиша W и Up отвечают за перемещение вперед) и комбинации алиасов (для выполнения кувырка в сторону надо нажать Left Shift и A). Заполняется все это в конструкторе. Также у этой структуры определен метод IsContainHAlias, чтобы понять, забинден ли хардварный алиас к данному алиасу. Этот метод может понадобиться, например, чтобы избежать назначения уже назначенного хардварного алиаса. Реализация этих методов следующая:
Controls::AliasMappig::AliasMappig(const char* name)
{
this->name = name;
this->alias = controls.GetAlias(name);
if (this->alias != -1)
{
Alias& alias = controls.aliases[this->alias];
int count = alias.aliasesRef.size();
if (count)
{
bindedNames.resize(count);
for (auto& bindedName : bindedNames)
{
int index = &bindedName - &bindedNames[0];
int bind_count = alias.aliasesRef[index].refs.size();
if (bind_count)
{
bindedName.resize(bind_count);
for (auto& bndName : bindedName)
{
int bind_index = &bndName - &bindedName[0];
bndName.name = alias.aliasesRef[index].refs[bind_index].name;
bndName.device_index = alias.aliasesRef[index].refs[bind_index].device_index;
}
}
}
}
}
}
bool Controls::AliasMappig::IsContainHAlias(const char* halias)
{
for (auto bindedName : bindedNames)
{
for (auto bndName : bindedName)
{
if (StringUtils::IsEqual(bndName.name.c_str(), halias))
{
return true;
}
}
}
return false;
}
Теперь переходим к реализации самой игры. Она состоит из нескольких экранов: стартовое меню, меню с переопределением управления, сама игра с меню паузы. Т.к. во всех экранах есть меню, опишем базовый класс меню, который будет содержать функционал перемещения по элементам меню и активации элемента меню. Логику же каждого из экранов будем реализовывать в классах наследниках от класса Menu.
Вначале приведем файл, описывающий алиасы, которые будут использованы в самом меню:
Описание алиасов для меню
{
"Aliases" : [
{
"name" : "Menu.Up",
"AliasesRef" : [
{ "names" : ["KEY_UP"]},
{ "names" : ["JOY_LEFT_STICK_V"] }
]
},
{
"name" : "Menu.Down",
"AliasesRef" : [
{ "names" : ["KEY_DOWN"]},
{ "names" : ["JOY_LEFT_STICK_NEGV"] }
]
},
{
"name" : "Menu.Action",
"AliasesRef" : [
{ "names" : ["KEY_RETURN"]},
{ "names" : ["JOY_A"] }
]
}
,
{
"name" : "Menu.AddHotkey",
"AliasesRef" : [
{ "names" : ["KEY_LCONTROL"]}
]
}
,
{
"name" : "Menu.StopEdit",
"AliasesRef" : [
{ "names" : ["KEY_ESCAPE"]}
]
}
,
{
"name" : "Menu.PauseGame",
"AliasesRef" : [
{ "names" : ["KEY_ESCAPE"]}
]
}
]
}
Файл алиасов, которые будут использованы для управления битами игроков:
Описание алиасов управление игроков
{
"Aliases" : [
{
"name" : "Player1.Up",
"AliasesRef" : [
{
"names" : [
"JOY_LEFT_STICK_V",
0
]
}
]
},
{
"name" : "Player1.Down",
"AliasesRef" : [
{
"names" : [
"JOY_LEFT_STICK_NEGV",
0
]
}
]
},
{
"name" : "Player2.Up",
"AliasesRef" : [
{
"names" : [
"KEY_P",
0
]
}
]
},
{
"name" : "Player2.Down",
"AliasesRef" : [
{
"names" : [
"KEY_L",
0
]
}
]
}
]
}
Теперь приведем реализацию базового класса Menu:
class Menu
class Menu
{
public:
typedef void(*MunuItemAction)();
static int alias_menu_up;
static int alias_menu_down;
static int alias_menu_act;
static int alias_add_hotkey;
static int alias_pause_game;
static int alias_stop_edit;
int sel_elemenet = 0;
struct Item
{
Vector2 pos;
std::string text;
int data = -1;
MunuItemAction action;
Item(Vector2 pos, const char* text, MunuItemAction action, int data = -1)
{
this->pos = pos;
this->text = text;
this->action = action;
this->data = data;
}
};
std::vector<Item> items;
virtual void Work(float dt)
{
DrawElements();
if (controls.GetAliasState(alias_menu_down))
{
sel_elemenet++;
if (sel_elemenet >= items.size())
{
sel_elemenet = 0;
}
}
if (controls.GetAliasState(alias_menu_up))
{
sel_elemenet--;
if (sel_elemenet < 0)
{
sel_elemenet = items.size() - 1;
}
}
if (controls.GetAliasState(alias_menu_act) && items[sel_elemenet].action)
{
items[sel_elemenet].action();
}
}
void DrawElements()
{
for (auto& item : items)
{
int index = &item - &items[0];
Color color = COLOR_WHITE;
if (index == sel_elemenet)
{
color = COLOR_GREEN;
}
render.DebugPrintText(item.pos, color, item.text.c_str());
}
}
};
Переходим к реализации первого экрана, а именно — стартового экрана. В нем будут только два пункта: Start и Controls. Для этого экрана базового функционала более чем достаточно, поэтому приведем только инициализацию и калбеки на нажатие каждого пункта:
void ShowControls()
{
cur_menu = &controls_menu;
}
void ShowGame()
{
cur_menu = &game_menu;
game_menu.ResetGame();
}
..
start_menu.items.push_back(Menu::Item(Vector2(365, 200), "Start", ShowGame));
start_menu.items.push_back(Menu::Item(Vector2(350, 250), "Controls", ShowControls));
Второй экран, реализацию которого мы рассмотрим, — это экран настройки управления. Т.к. мы рассматриваем простую игру Pong, которая рассчитана на игру вдвоем, то наша задача — переопределить действия движения вверх и вниз битка для каждого игрока, т.е. есть суммарно 4 действия. Потому определяем массив, в котором будем хранить данные о мепинге алиаса, и проинициализируем его:
class ControlsMenu
vector<Controls::AliasMappig> controlsMapping;
...
controlsMapping.push_back(Controls::AliasMappig("Player1.Up"));
controlsMapping.push_back(Controls::AliasMappig("Player1.Down"));
controlsMapping.push_back(Controls::AliasMappig("Player2.Up"));
controlsMapping.push_back(Controls::AliasMappig("Player2.Down"));
controlsMapping.push_back(Controls::AliasMappig("Menu.AddHotkey"));
controls_menu.items.push_back(Menu::Item(Vector2(300, 100), "Up", nullptr, 0));
controls_menu.items.push_back(Menu::Item(Vector2(300, 150), "Down", nullptr, 1));
controls_menu.items.push_back(Menu::Item(Vector2(300, 300), "Up", nullptr, 2));
controls_menu.items.push_back(Menu::Item(Vector2(300, 350), "Down", nullptr, 3));
controls_menu.items.push_back(Menu::Item(Vector2(370, 450), "Back", HideControls));
...
class ControlsMenu : public Menu
{
int sel_mapping = -1;
bool first_key = false;
bool make_hotkey = false;
public:
virtual void Work(float dt)
{
if (sel_mapping == -1)
{
Menu::Work(dt);
if (controls.GetAliasState(alias_menu_act))
{
sel_mapping = items[sel_elemenet].data;
if (sel_mapping != -1)
{
first_key = true;
}
}
}
else
{
make_hotkey = controls.GetAliasState(alias_add_hotkey, Controls::Active);
DrawElements();
if (controls.GetAliasState(alias_stop_edit))
{
sel_mapping = -1;
}
else
{
int device_index;
const char* key = controls.GetActivatedKey(device_index);
if (key && !controlsMapping[4].IsContainHAlias(key))
{
bool allow = true;
if (first_key)
{
controlsMapping[sel_mapping].bindedNames.clear();
first_key = false;
}
else
{
allow = !controlsMapping[sel_mapping].IsContainHAlias(key);
}
if (allow)
{
Controls::AliasMappig::BindName bndName;
bndName.name = key;
bndName.device_index = device_index;
if (first_key || !make_hotkey)
{
vector<Controls::AliasMappig::BindName> names;
names.push_back(bndName);
controlsMapping[sel_mapping].bindedNames.push_back(names);
}
else
{
controlsMapping[sel_mapping].bindedNames.back().push_back(bndName);
}
}
}
}
}
if (sel_mapping != -1)
{
render.DebugPrintText(Vector2(180, 510), COLOR_YELLOW, "Hold Left CONTROL to create key combination");
render.DebugPrintText(Vector2(200, 550), COLOR_YELLOW, "Press ESCAPE to stop adding keys to alias");
}
render.DebugPrintText(Vector2(360, 50), COLOR_WHITE, "Player 1");
render.DebugPrintText(Vector2(360, 250), COLOR_WHITE, "Player 2");
for (auto& item : items)
{
int index = &item - &items[0];
if (item.data != -1)
{
Color color = COLOR_WHITE;
if (index == sel_elemenet)
{
color = COLOR_GREEN;
}
char text[1024];
text[0] = 0;
if (item.data != sel_mapping || !first_key)
{
for (auto& bindedName : controlsMapping[item.data].bindedNames)
{
if (text[0] != 0)
{
StringUtils::Cat(text, 1024, ", ");
}
for (auto& bndName : bindedName)
{
int index = &bndName - &bindedName[0];
if (index != 0)
{
StringUtils::Cat(text, 1024, " + ");
}
StringUtils::Cat(text, 1024, bndName.name.c_str());
}
}
}
if (item.data == sel_mapping)
{
if (text[0] != 0)
{
if (!make_hotkey)
{
StringUtils::Cat(text, 1024, ", ");
}
else
{
StringUtils::Cat(text, 1024, " + ");
}
}
StringUtils::Cat(text, 1024, "_");
}
render.DebugPrintText(item.pos + Vector2(80, 0), color, text);
}
}
}
};
Данный код реализует назначение алиаса посредством опроса GetActivatedKey. Если активен алиас Menu.AddHotkey (Left Control), то задается сочетание клавиш. При активации алиаса Menu.StopEdit (Escаpe) происходит прекращение задания алиаса. При возврате в основное меню нужно произвести сохранение мепинга, и мы делаем это в колбеке:
Сохранение меппинга
void SaveMapping()
{
JSONWriter* writer = new JSONWriter();
writer->Start("settings/controls/game_pc");
writer->StartArray("Aliases");
for (auto cntrl : controlsMapping)
{
writer->StartBlock(nullptr);
writer->Write("name", cntrl.name.c_str());
writer->StartArray("AliasesRef");
for (auto& bindedName : cntrl.bindedNames)
{
writer->StartBlock(nullptr);
writer->StartArray("names");
for (auto& bndName : bindedName)
{
writer->Write(nullptr, bndName.name.c_str());
writer->Write(nullptr, bndName.device_index);
}
writer->FinishArray();
writer->FinishBlock();
}
writer->FinishArray();
writer->FinishBlock();
}
writer->FinishArray();
writer->Release();
}
void HideControls()
{
cur_menu = &start_menu;
SaveMapping();
controls.LoadAliases("settings/controls/game_pc");
}
Последний шаг — описание класса, реализующего игровой экран:
class GameMenu
class GameMenu : public Menu
{
bool paused = false;
float player_speed = 500.0f;
float player_size = 16.0f * 4.0f;
Vector2 ball_start_pos = Vector2(400.0f, 300.0f);
float ball_speed = 450.0f;
float ball_radius = 8.0f;
float player1_pos;
float player2_pos;
Vector2 ball_pos;
Vector2 ball_dir;
int player1_score;
int player2_score;
public:
void ResetBall()
{
ball_pos = ball_start_pos;
ball_dir.x = rnd_range(-1.0f, 1.0f);
ball_dir.y = rnd_range(-1.0f, 1.0f);
ball_dir.Normalize();
}
void ResetGame()
{
player1_pos = 300.0f - player_size * 0.5f;
player2_pos = 300.0f - player_size * 0.5f;
player1_score = 0;
player2_score = 0;
ResetBall();
paused = false;
}
void UpdatePlayer(float dt, int index, float &pos)
{
if (controls.GetAliasState(controlsMapping[index + 0].alias, Controls::Active))
{
pos -= dt * player_speed;
if (pos < 0.0f)
{
pos = 0.0f;
}
}
if (controls.GetAliasState(controlsMapping[index + 1].alias, Controls::Active))
{
pos += dt * player_speed;
if (pos > 600.0f - player_size)
{
pos = 600.0f - player_size;
}
}
}
void UpdateBall(float dt)
{
ball_pos += ball_dir * ball_speed * dt;
if (ball_pos.y < ball_radius)
{
ball_pos.y = ball_radius;
ball_dir.y = -ball_dir.y;
}
if (ball_pos.y > 600 - ball_radius)
{
ball_pos.y = 600 - ball_radius;
ball_dir.y = -ball_dir.y;
}
if (player1_pos < ball_pos.y &&
ball_pos.y < player1_pos + player_size && ball_pos.x < 15.0f + ball_radius)
{
ball_pos.x = 16.0f + ball_radius;
ball_dir.x = 1.0;
ball_dir.y = (ball_pos.y - (player1_pos + player_size * 0.5f)) / player_size;
ball_dir.Normalize();
}
if (player2_pos < ball_pos.y &&
ball_pos.y < player2_pos + player_size && ball_pos.x > 785.0f - ball_radius)
{
ball_pos.x = 784.0f - ball_radius;
ball_dir.x = -1.0;
ball_dir.y = (ball_pos.y - (player2_pos + player_size * 0.5f)) / player_size;
ball_dir.Normalize();
}
if (ball_pos.x < 0)
{
player2_score++;
ResetBall();
}
if (ball_pos.x > 800)
{
player1_score++;
ResetBall();
}
}
void DrawPlayer(Vector2 pos)
{
for (int i = 0; i < 4; i++)
{
render.DebugPrintText(pos + Vector2(0, i * 16.0f), COLOR_WHITE, "8");
}
}
virtual void Work(float dt)
{
if (paused)
{
Menu::Work(dt);
}
else
{
UpdatePlayer(dt, 0, player1_pos);
UpdatePlayer(dt, 2, player2_pos);
UpdateBall(dt);
if (controls.GetAliasState(alias_pause_game))
{
paused = true;
}
}
DrawPlayer(Vector2(3, player1_pos));
DrawPlayer(Vector2(785, player2_pos));
render.DebugPrintText(ball_pos - Vector2(ball_radius), COLOR_WHITE, "O");
char str[16];
StringUtils::Printf(str, 16, "%i", player1_score);
render.DebugPrintText(Vector2(375, 20.0f), COLOR_WHITE, str);
render.DebugPrintText(Vector2(398, 20.0f), COLOR_WHITE, ":");
StringUtils::Printf(str, 16, "%i", player2_score);
render.DebugPrintText(Vector2(415, 20.0f), COLOR_WHITE, str);
}
};
На этом все. На простом примере мы продемонстрировали простоту и гибкость при работе с системой опроса устройств ввода. Система не загромождена кодом и не изобилует лишними методами.
> Ссылка на пример использования работающей системы
Также эта система была написана для движка под названием Atum. Репозиторий всех исходников движка — в них много чего интересного.