Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

Architecture of System for Reading Input Devices (Part 2)

$
0
0
In the first part of the article I told about the steps in architecture of a system which reads data from input devices. The system is based on aliases. But in the first article I didn’t describe an application creation process and didn’t show advantages of using such a system. In this article we’ll consider creation of simple game Pong for 2 players. It has the option to assign to action more than one key of keyboard, and not only of keyboard. We’ll consider a mouse and several connected joysticks. Also we’ll consider an option to assign keys combination, for example, W + Left Mouse Button. Therefore, we’ll demonstrate the the maximum of flexibility at work with input devices. For a game creation let’s change slightly the code of the system that was described in the first part. First, let’s consider working with joysticks including the case when several joysticks are connected to the system. We’ll implement the work with XIpnut tools because dealing with this library is very simple. Into the file which describes hardware aliases we’ll add the following: [ { "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 } ] When dealing with the states themselves, let’s 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 set that there are no active joysticks: for (int i = 0; i< XUSER_MAX_COUNT; i++) { joy_active[i] = false; } In an update function we get states from currently connected joysticks: 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, that’s why when reading hardware aliases we need to pass the index of the device which data we want to read. We use the following methods: bool GetHardwareAliasState(int alias, AliasAction action, int device_index); float GetHardwareAliasValue(int alias, bool delta, int device_index); Let’s consider the case when device_index is equal to -1. In this case if two joysticks are connected and we read if the button A is pressed, then pressed buttons from both of joysticks will be registered. Therefore, we want to get active value from any of connected joysticks. Now let’s consider the code processing hardware aliases of joysticks: 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; } Finally, let’s consider the option to define required device number in the alias itself. This is the ultimate option needed to deal with several devices appropriately. So, we need to update the structure: struct AliasRefState { std::string name; int aliasIndex = -1; bool refer2hardware = false; int device_index = -1; // filed added }; Now aliases reading looks like: 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; } The file describing aliases which process sticks movements of two joysticks looks like: { "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 code which works with joysticks because I wanted to demonstrate how to organize getting states when working with several devices of the same type. If you work with 4 keyboards and 3 mice, it should be clear how to do it. Now let’s consider adding of functional which is needed for implementation of control scheme redefining. The first method which we need is the following: 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 hardware aliases. If the alias became active then it returns its string name. This method is necessary for pressed button detecting in the moment when the button pressing by user in menu of redefining control scheme is expected. Now let’s describe the mechanics which allow us to execute a reverse action: get list of hardware buttons assigned to alias. A structure for that: 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 keeps the name of the aliases itself, its id, all associated aliases (for example, key W and Up are responsible for moving forward) and combinations of aliases (to make a roll is needed to press Left Shift and A). All of these are defined in a constructor. Also the method IsContainHAlias is defined in this structure to understand if hardware alias is binded to this alias. This method could be helpful, for example, to escape repeated assigning of assigned hardware alias. Implementation of these methods: 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 let’s go to the game implementation. It consists of several screens: start menu, menu of redefining control scheme, the game itself with pause menu. Since there is the menu in all screens, let’s describe a base class of menu which contains the functional of moving among menu elements and activation of menu element. The logic of each screen will be implemented in child classes of class Menu. The file describing aliases which are used in the menu is the following: { "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"]} ] } ] } The file of aliases which are used for management of players’ bats: { "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 let’s describe implementation of base 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()); } } }; Let’s go to first screen implementation - the start screen. There are two elements: Start and Controls. These elements are enough for the screen of base functional. There are initializing and call backs for pressing of each element in the following: 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)); Let’s consider the second screen: the screen of control settings. Since we consider the simple game Pong for 2 players, then our goal is to redefine actions of bat movement up and down for each player; four actions in sum. Let’s define an array which keeps data of aliases mapping and initialize it: 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 definition of aliases through GetActivatedKey. If alias Menu.AddHotkey (Left Control) is active then combination keys if defined. When alias Menu.StopEdit (Escаpe) is activated, definition of alias is ended. When going back to the main menu, it’s necessary to keep mapping - we’ll do it in call back: 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 to define the class which implements game screen: 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. In this simple example we demonstrated ease and flexibility at work with the system of reading from input devices. The system is clear and doesn’t contain excess methods. Reference to the example of well working system: github.com/ENgineE777/Controls2. Also this system was created That’s all. In this simple example we demonstrated ease and flexibility at work with the system of reading from input devices. The system is clear and doesn’t contain excess methods. Moreover, this system was created for Atum engine. Repository of all engine sources: github.com/ENgineE777/Atum - you’ll fine there more where that came from.

Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>