Jump to content
  • Advertisement

_Engine_

Member
  • Content count

    91
  • Joined

  • Last visited

Community Reputation

311 Neutral

About _Engine_

  • Rank
    Member

Personal Information

Social

  • Github
    https://github.com/ENgineE777/

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

  1. _Engine_

    Atum Engine

    Finally I finished work at track based editor. This editor allows to create animation for objects. For example, simple cut scene can be created in the editor. Video of work process in editor can be at follow link:
  2. I think you just rename all DX11 interfaces and structures and this is not useful solution at all. For example, process of creation texture do not differ from case when is raw DX11 are used. All examples of usage of your API do not differ from usage of raw DX11. Rename it to examples of usage DX11 and no one will find difference. Also with such model when you will try to add, for example, Vulkan API you will be forced to emulate DX11 interface and this will lead to bunch of unnecessary code. You can not just rename all DX11 interfaces and say - Look every one I write brand new graphical API. That is not work at this way. If you really want to write something useful than you will need to add high level class like Buffer and Texture with do bunch of stuff like filling TextureDesc so all work with DX11 will be simplified.
  3. You talking about advantages of CryEngine 5 over Unreal Engine 4. But pointed only at price and availability of source code. But you said nothing about toolset and feature list of each. Main reason why people use technology is not price and access to source code but how useful that technology in development of their soft. UE 4 is much more comfortable game engine with great editor than CryEngine 5. That is why UE 4 dominating over CryEngine 5.
  4. 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.
  5. _Engine_

    Atum Engine

    Thank you for advise
  6. _Engine_

    Atum Engine

    Atum engine is a newcomer in a row of game engines. Most game engines focus on render techniques in features list. The main task of Atum is to deliver the best toolset; that’s why, as I hope, Atum will be a good light weighted alternative to Unity for indie games. Atum already has fully workable editor that has an ability to play test edited scene. All system code has simple ideas behind them and focuses on easy to use functionality. That’s why code is minimized as much as possible. Currently the engine consists from: - Scene Editor with ability to play test edited scene; - Powerful system for binding properties into the editor; - Render system based on DX11 but created as multi API; so, adding support of another GAPI is planned; - Controls system based on aliases; - Font system based on stb_truetype.h; - Support of PhysX 3.0, there are samples in repo that use physics; - Network code which allows to create server/clinet; there is some code in repo which allows to create a simple network game I plan to use this engine in multiplayer game - so, I definitely will evolve the engine. Also I plan to add support for mobile devices. And of course, the main focus is to create a toolset that will ease games creation. Link to repo on source code is - https://github.com/ENgineE777/Atum Video of work process in track based editor can be at follow link:
  7. When working on a game engine, one wants to configure it correctly from the beginning: in order not to painfully refactor it later. When I was developing my engine, for inspiration I surveyed source code of other game engines and came to the specific realization (you can find out more about it by reference in the end of the article). In the article I would like to come up with the decision how to architect a system which reads data from input devices. In this article I will review how to architect a system that reads data from input devices. This doesn't sound complicated: read data from mouse, keyboard, and joystick and call them in a correct place. That’s true, and common input handling code might look like this: //updating of data from input devices controls->Update() ... void Player::Move() { if (controls->MouseButonPressed(0)) { ... } if (controls->KeyPressed(KEY_SPACE)) { ... } if (controls->JoystickButtonPressed(0)) { ... } } What's wrong with this approach? First, if we want to read data from a certain device, for example from the joystick, we are using methods specific to the device type. Second, we have hard-coded inputs, i.e. we call a certain keyboard key direct in the game code and from the certain device. That’s not good because later we’ll have to expel this from code to make keys redefinition in menu. We’ll have to create a remapping system that allows us to quickly redefine key bindings. Thus, the simplest realization isn’t the best one. What can we propose to deal with this problem? The solution is simple: when calling input devices, use abstract names – “aliases”. They are defined in a separate file, and their names arise not from a key name with a bound action but from the action itself, for example, "ACTION_JUMP", "ACTION_SHOOT". To avoid work with aliases’ names, let’s add a method of getting alias’s identifier: int GetAlias(const char* name); States calling resolves into two methods: enum AliasAction { Active, Activated }; bool GetAliasState(int alias, AliasAction action); float GetAliasValue(int alias, bool delta); Let me explain why I use these two methods. When one calls key state, Boolean value is enough. But when one calls joystick’s stick state, it’s necessary to get numerical value. That’s why there are two methods. In case of state, we pass type of action in the second parameter. There are two types: Active (alias is active, for example, a key is pressed) and Activated (alias passed into active state). For example, we should process key of grenade throwing. That’s not constant action like walking, so we need recognition of the fact that throwing grenade key has been pressed; and if the key is still pressed – then not to respond to that fact. When calling alias’s numerical value, we pass Boolean flag as the second parameter, and it tells us if we need the value itself or the difference between current value and value of last frame. An example of the camera control code: void FreeCamera::Init() { proj.BuildProjection(45.0f * RADIAN, 600.0f / 800.0f, 1.0f, 1000.0f); angles = Vector2(0.0f, -0.5f); pos = Vector(0.0f, 6.0f, 0.0f); alias_forward = controls.GetAlias("FreeCamera.MOVE_FORWARD"); alias_strafe = controls.GetAlias("FreeCamera.MOVE_STRAFE"); alias_fast = controls.GetAlias("FreeCamera.MOVE_FAST"); alias_rotate_active = controls.GetAlias("FreeCamera.ROTATE_ACTIVE"); alias_rotate_x = controls.GetAlias("FreeCamera.ROTATE_X"); alias_rotate_y = controls.GetAlias("FreeCamera.ROTATE_Y"); alias_reset_view = controls.GetAlias("FreeCamera.RESET_VIEW"); } void FreeCamera::Update(float dt) { if (controls.GetAliasState(alias_reset_view)) { angles = Vector2(0.0f, -0.5f); pos = Vector(0.0f, 6.0f, 0.0f); } if (controls.GetAliasState(alias_rotate_active, Controls::Active)) { angles.x -= controls.GetAliasValue(alias_rotate_x, true) * 0.01f; angles.y -= controls.GetAliasValue(alias_rotate_y, true) * 0.01f; if (angles.y > HALF_PI) { angles.y = HALF_PI; } if (angles.y < -HALF_PI) { angles.y = -HALF_PI; } } float forward = controls.GetAliasValue(alias_forward, false); float strafe = controls.GetAliasValue(alias_strafe, false); float fast = controls.GetAliasValue(alias_fast, false); float speed = (3.0f + 12.0f * fast) * dt; Vector dir = Vector(cosf(angles.x), sinf(angles.y), sinf(angles.x)); pos += dir * speed * forward; Vector dir_strafe = Vector(dir.z, 0,-dir.x); pos += dir_strafe * speed * strafe; view.BuildView(pos, pos + Vector(cosf(angles.x), sinf(angles.y), sinf(angles.x)), Vector(0, 1, 0)); render.SetTransform(Render::View, view); proj.BuildProjection(45.0f * RADIAN, (float)render.GetDevice()->GetHeight() / (float)render.GetDevice()->GetWidth(), 1.0f, 1000.0f); render.SetTransform(Render::Projection, proj); } Note that we used prefix FreeCamera in aliases’ names. It was done so as to follow the certain naming rule which helps to understand which object the alias belongs to. If we don’t do so, in case of further development the amount of aliases would increase, and eventually we would get lost among lot of aliases which refers to each other. It’s impossible to manage such a situation because finding of mistaken alias’s definition would be very difficult and take a lot of time. So, introduction of naming rule is essential. Let’s continue with the most interesting part: definition of aliases themselves. They we’ll be held in json file. The file which describes aliases for camera looks in the following way: { "Aliases" : [ { "name" : "FreeCamera.MOVE_FORWARD", "AliasesRef" : [ { "names" : ["KEY_W"], "modifier" : 1.0 }, { "names" : ["KEY_I"], "modifier" : 1.0 }, { "names" : ["KEY_S"], "modifier" : -1.0 }, { "names" : ["KEY_K"], "modifier" : -1.0 } ]}, { "name" : "FreeCamera.MOVE_STRAFE", "AliasesRef" : [ { "names" : ["KEY_A"], "modifier" : -1.0 }, { "names" : ["KEY_J"], "modifier" : -1.0 }, { "names" : ["KEY_D"], "modifier" : 1.0 }, { "names" : ["KEY_L"], "modifier" : 1.0 } ]}, { "name" : "FreeCamera.MOVE_FAST", "AliasesRef" : [ { "names" : ["KEY_LSHIFT"] } ]}, { "name" : "FreeCamera.ROTATE_ACTIVE", "AliasesRef" : [ { "names" : ["MS_BTN1"] } ]}, { "name" : "FreeCamera.ROTATE_X", "AliasesRef" : [ { "names" : ["MS_X"] } ]}, { "name" : "FreeCamera.ROTATE_Y", "AliasesRef" : [ { "names" : ["MS_Y"] } ]}, { "name" : "FreeCamera.RESET_VIEW", "AliasesRef" : [ { "names" : ["KEY_R", "KEY_LCONTROL"] } ]} ] } Aliases are defined in a simple way: define alias’s name (parameter “name”) and an array of links to aliases (parameter “AliasesRef”). For each link to alias one may define a paramets “modificator”; this parameter will be used as a factor applying to the value which comes when calling method “GetAliasValue”. Aliases MOVE_FORWARD and MOVE_STRAFE use that parameter for joystick’s stick work imitation because joystick’s stick outputs value from range [-1..1] for each of two axes. To set keys combination (for example, hotkeys) parameter “names” is an array of names. Alias RESET_VIEW is an example of setting hotkey for a keys combination LCTRL + R. Let’s consider encountered names in links to aliases, for example, KEY_W, MS_BTN1. The fact is that in their work one needs links to certain keys; such keys are called “hardware aliases”. Consequently, there are two types of aliases in our system: user-defined (we work with them in code) and hardware aliases. Two methods are: bool GetAliasState(int alias, bool exclusive, AliasAction action); float GetAliasValue(int alias, bool delta); The methods accept identifiers of user-defined aliases gotten when calling a method “GetAlias”. Such a restriction was introduced in order not to use hardware aliases and all the time to use only user-defined aliases. If needed to insert debug hotkey which starts something debugging, one should use one of following methods: bool DebugKeyPressed(const char* name, AliasAction action); bool DebugHotKeyPressed(const char* name, const char* name2, const char* name3); Both methods accept names of hardware aliases. So, processing of debug hotkeys uses one of these two methods, and it’s simple to add setting which disables processing of all debug hotkeys; and one doesn’t need separate code to disable processing of debug hotkeys because a system will disable them by itself. Consequently, no debug functional will fall into release build. Let’s consider description of implementation closely. Further logic of code will be described. I used DirectInput to work with a keyboard and mouse, that’s why a code for work with DirectInput will be omitted. Let’s begin with description of hardware aliases structure: enum Device { Keyboard, Mouse, Joystick }; struct HardwareAlias { std::string name; Device device; int index; float value; }; Let’s describe aliases structure: struct AliasRefState { std::string name; int aliasIndex = -1; bool refer2hardware = false; }; struct AliasRef { float modifier = 1.0f; std::vector<AliasRefState> refs; }; struct Alias { std::string name; bool visited = false; std::vector<AliasRef> aliasesRef; }; Let’s implement methods, and begin with initialization method: bool Controls::Init(const char* name_haliases, bool allowDebugKeys) { this->allowDebugKeys = allowDebugKeys; //Init input devices and related stuff JSONReader* reader = new JSONReader(); if (reader->Parse(name_haliases)) { while (reader->EnterBlock("keyboard")) { haliases.push_back(HardwareAlias()); HardwareAlias& halias = haliases[haliases.size() - 1]; halias.device = Keyboard; reader->Read("name", halias.name); reader->Read("index", halias.index); debeugMap[halias.name] = (int)haliases.size() - 1; reader->LeaveBlock(); } while (reader->EnterBlock("mouse")) { haliases.push_back(HardwareAlias()); HardwareAlias& halias = haliases[(int)haliases.size() - 1]; halias.device = Mouse; reader->Read("name", halias.name); reader->Read("index", halias.index); debeugMap[halias.name] = (int)haliases.size() - 1; reader->LeaveBlock(); } } reader->Release(); return true; } For user-defined loading let’s describe method “LoadAliases”. The same method is used in case if a file, which describes aliases, was changed. For example, user redefined control scheme in settings: 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")) { aliasRef.refs.push_back(AliasRefState()); AliasRefState& ref = aliasRef.refs.back(); reader->Read("", ref.name); reader->LeaveBlock(); } reader->Read("modifier", aliasRef.modifier); reader->LeaveBlock(); } reader->LeaveBlock(); } ResolveAliases(); } reader->Release(); } There is method “ResolveAliases()” in loading code. There is linking of loaded aliases in this method. Linking code: void Controls::ResolveAliases() { for (auto& alias : aliases) { for (auto& aliasRef : alias.aliasesRef) { for (auto& ref : aliasRef.refs) { int index = GetAlias(ref.name.c_str()); if (index != -1) { ref.aliasIndex = index; ref.refer2hardware = false; } else { for (int l = 0; l < haliases.size(); l++) { if (StringUtils::IsEqual(haliases[l].name.c_str(), ref.name.c_str())) { ref.aliasIndex = l; ref.refer2hardware = true; break; } } } if (index == -1) { printf("alias %s has invalid reference %s", alias.name.c_str(), ref.name.c_str()); } } } } for (auto& alias : aliases) { CheckDeadEnds(alias); } } There is method CheckDeadEnds in linking method. The purpose of the method is to identify iterative references because such references can’t be processed, and one needs escape them. void Controls::CheckDeadEnds(Alias& alias) { alias.visited = true; for (auto& aliasRef : alias.aliasesRef) { for (auto& ref : aliasRef.refs) { if (ref.aliasIndex != -1 && !ref.refer2hardware) { if (aliases[ref.aliasIndex].visited) { ref.aliasIndex = -1; printf("alias %s has circular reference %s", alias.name.c_str(), ref.name.c_str()); } else { CheckDeadEnds(aliases[ref.aliasIndex]); } } } } alias.visited = false; } Let’s move to the method of getting states of hardware aliases: bool Controls::GetHardwareAliasState(int index, AliasAction action) { HardwareAlias& halias = haliases[index]; switch (halias.device) { case Keyboard: { //code that access to state of keyboard break; } case Mouse: { //code that access to state of mouse break; } } return false; } bool Controls::GetHardwareAliasValue(int index, bool delta) { HardwareAlias& halias = haliases[index]; switch (halias.device) { case Keyboard: { //code that access to state of keyboard break; } case Mouse: { //code that access to state of mouse break; } } return 0.0f; } And code for definition states of aliases themselves: bool Controls::GetAliasState(int index, AliasAction action) { if (index == -1 || index >= aliases.size()) { return 0.0f; } Alias& alias = aliases[index]; for (auto& aliasRef : alias.aliasesRef) { bool val = true; for (auto& ref : aliasRef.refs) { if (ref.aliasIndex == -1) { continue; } if (ref.refer2hardware) { val &= GetHardwareAliasState(ref.aliasIndex, Active); } else { val &= GetAliasState(ref.aliasIndex, Active); } } if (action == Activated && val) { val = false; for (auto& ref : aliasRef.refs) { if (ref.aliasIndex == -1) { continue; } if (ref.refer2hardware) { val |= GetHardwareAliasState(ref.aliasIndex, Activated); } else { val |= GetAliasState(ref.aliasIndex, Activated); } } } if (val) { return true; } } return false; } float Controls::GetAliasValue(int index, bool delta) { if (index == -1 || index >= aliases.size()) { return 0.0f; } Alias& alias = aliases[index]; for (auto& aliasRef : alias.aliasesRef) { float val = 0.0f; for (auto& ref : aliasRef.refs) { if (ref.aliasIndex == -1) { continue; } if (ref.refer2hardware) { val = GetHardwareAliasValue(ref.aliasIndex, delta); } else { val = GetAliasValue(ref.aliasIndex, delta); } } if (fabs(val) > 0.01f) { return val * aliasRef.modifier; } } return 0.0f; } And the last: definition of debug keys states: bool Controls::DebugKeyPressed(const char* name, AliasAction action) { if (!allowDebugKeys || !name) { return false; } if (debeugMap.find(name) == debeugMap.end()) { return false; } return GetHardwareAliasState(debeugMap[name], action); } bool Controls::DebugHotKeyPressed(const char* name, const char* name2, const char* name3) { if (!allowDebugKeys) { return false; } bool active = DebugKeyPressed(name, Active) & DebugKeyPressed(name2, Active); if (name3) { active &= DebugKeyPressed(name3, Active); } if (active) { if (DebugKeyPressed(name) | DebugKeyPressed(name2) | DebugKeyPressed(name3)) { return true; } } return false; } There is one more function for states update: void Controls::Update(float dt) { //update state of input devices } That’s all. The system worked out to be quite simple, and it has minimum amount of code. Still it solves the problem of getting states of input devices effectively. Reference to the example of well working system: github.com/ENgineE777/Controls. 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.
  8. Hi! Currently I am working at materials about creating of Game Editor. This materials entirely focused on how to create Game Editor from scratch. Material consist from several steps - each step add some feature set to base code. Now material contain only source code. Articles will be ready soon. Repository of material located at - https://github.com/ENgineE777/EditorSteps. I hope that this material will be useful for anyone who want to create own game engine. P.S. Code depends from https://github.com/ENgineE777/EUI
  9. Hi! Currently I am working at materials about creating Cross-Platform Render System. This materials entirely focused on architecture of Render System and how to create it from scratch. Material consist from several steps - each step add some feature set to base code. Now material contain only source code. Articles will be ready soon. Repository of material located at - https://github.com/ENgineE777/RS_Steps. I hope that this material will be useful for anyone who want to create own game engine.
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!