📜 ⬆️ ⬇️

Designing a system for reading data from input devices (Part Two)

In the first part of the article , steps were given to create a system for reading data from input devices based on aliases. But the first article did not describe the process of creating an application that would show the benefits of using such a system. This article will discuss the creation of a simple Pong game, designed for playing together, with the ability to reassign management and the ability to assign not one but several keys to an action, not limited to just the keyboard. We will consider not only the mouse and several connected joysticks, but also the ability to assign a key combination, such as the W + Left Mouse Button, i.e. maximum flexibility in working with input devices will be demonstrated.

To create a game, let's change the system code described in the first part.

First, we will consider working with joysticks, including handling the case when several joysticks are connected to the system. We will implement the work by means of XIpnut, since Work with this library is as simple as possible.

The following block is added to the file describing hardware aliases:
')
Alias ​​description
"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 } ] 


To work with the states themselves, we define the following arrays

 XINPUT_STATE joy_prev_states[XUSER_MAX_COUNT]; XINPUT_STATE joy_states[XUSER_MAX_COUNT]; bool joy_active[XUSER_MAX_COUNT]; 

When initializing, we say that there are no active joysticks:

 for (int i = 0; i< XUSER_MAX_COUNT; i++) { joy_active[i] = false; } 

In the function of the update, we collect the states from the joysticks that are currently connected:

 ... 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; } } ... 

We may have several joysticks, so when polling hardware aliases, it is necessary to transfer the index of the device from which we want to read data. The methods will look like this:

 bool GetHardwareAliasState(int alias, AliasAction action, int device_index); float GetHardwareAliasValue(int alias, bool delta, int device_index); 

At the same time, we assume that if device_index is -1, it means that if two joysticks are connected and we interrogate whether the A button is pressed, then the presses from both joysticks will be taken into account, i.e. we want to get active value from any connected joystick.

And now we give the handling code for hardware joysticks aliases:

Handling hardware aliases
 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; } 


The last thing that is necessary for full-fledged work with several devices is the ability to set the required device number in the alias itself. Therefore it is necessary to update the structure:

 struct AliasRefState { std::string name; int aliasIndex = -1; bool refer2hardware = false; int device_index = -1; //   }; 

Reading aliases now looks like this:

Read aliases
 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; } 


A file that describes aliases that handle deviations of sticks from two joysticks looks like this:

Alias ​​description
 { "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 ] } ] } ] } 


I described in detail the input of the code that works with joysticks in order to show how to organize the survey operation, if there are several devices of the same type. If you need to support the work with 4 keyboards and 3 mice, then questions about how to do this should arise.

Now consider the addition of the functionality that is needed to implement the control override. The first method we need is:

 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; } 

The method passes through all hardwired aliases, and if the alias becomes active, its string name will return. This method is necessary to track the key pressed while the key is expected to be pressed by the user in the control key settings menu.

And now we will describe the mechanism that allows you to do the opposite: say which hardware buttons are assigned for the alias. To do this, we describe the structure:

 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); }; 

This structure will store the name of the alias itself, its id, all associated aliases (for example, the W and Up keys are responsible for moving forward) and combinations of aliases (to perform a roll in the direction, you must press Left Shift and A). All this is filled in the constructor. Also, this structure has the IsContainHAlias ​​method defined to understand if the hardware alias is zapinden to this alias. This method may be necessary, for example, to avoid the assignment of an already assigned hardware alias. The implementation of these methods is as follows:

 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; } 

Now go to the implementation of the game itself. It consists of several screens: the start menu, the menu with the control override, the game itself with the pause menu. Since In all screens there is a menu, we describe the basic menu class, which will contain the functionality of navigating through the menu items and activating the menu item. The logic of each of the screens will be implemented in the classes derived from the class Menu.

First we give a file that describes the aliases that will be used in the menu itself:

Description of aliases for the 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"]} ] } ] } 


File of aliases that will be used to control the players bits:

Description of alias player management
 { "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 ] } ] } ] } 


Now we give the implementation of the base Menu class:

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()); } } }; 


We proceed to the implementation of the first screen, namely, the start screen. It will only have two items: Start and Controls. For this screen, the basic functionality is more than enough, so we only give the initialization and kalbeki for pressing each item:

 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)); 

The second screen, the implementation of which we will consider, is the control settings screen. Since we consider a simple game Pong, which is designed for the game together, then our task is to redefine the actions of moving up and down the cue ball for each player, i.e. There are a total of 4 actions. Therefore, we define an array in which we will store data about the aliasing, and initialize it:

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); } } } }; 


This code implements the assignment of an alias by polling GetActivatedKey. If the Menu.AddHotkey (Left Control) alias is active, then the shortcut is set. When the Menu.StopEdit (Escape) alias is activated, the alias job is terminated. When returning to the main menu, you need to save the mapping, and we do it in the callback:

Saving mapping
 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"); } 


The last step is the description of the class that implements the game screen:

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); } }; 


That's all. With a simple example, we demonstrated simplicity and flexibility when working with an input device polling system. The system is not cluttered with code and is not replete with unnecessary methods.

Link to an example of using a working system

Also, this system was written for an engine called Atum. The repository of all the sources of the engine - they have a lot of interesting things.

Source: https://habr.com/ru/post/343832/


All Articles