В первой части статьи были приведены шаги по созданию системы для считывания данных с устройств ввода, основанной на алиасах. Но в первой статье не был описан процесс создания приложения, который бы показывал преимущества использования подобной системы. В этой статье будет рассмотрено создание простой игры Pong, рассчитанной на игру вдвоем, с возможностью переназначить управление и возможностью назначить на действие не одну, а несколько клавиш, не ограничиваясь только клавиатурой. Мы рассмотрим не только мышь и несколько подключенных джойстиков, но и возможность назначать комбинацию клавиш, например W + Left Mouse Button, т.е. будет продемонстрирована максимальная гибкость в работе с устройствами ввода.

Для создания игры немного изменим код системы, описанной в первой части.

Вначале рассмотрим работу с джойстиками, включая обработку случая, когда к системе подключено несколько джойстиков. Реализовывать работу будем средствами 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. Репозиторий всех исходников движка — в них много чего интересного.

Комментарии (0)