読者です 読者をやめる 読者になる 読者になる

ゆとりーなの日記

日記的な事を書いて行くと思はれる

Win32API入力特集

題名の通りです。Win32APIを使ってゲームで使うであろうキーボードの入力に関するごにょごにょです。Win32を使うとなれば、関数を使ってキーの状態を毎フレーム毎に確認していく方法とメッセージを使ったコールバック的な方法の二種類が考えられると思いますが、今回は強欲に両方やります。
キー入力で欲しい情報と云えば、キーが押されているかどうか、離された瞬間かどうか、押された瞬間かどうか(いわゆるトリガ)、最初は押された瞬間しか検出しないけどあまりにしつこく押しっぱなしだと押された事にする(いわゆるリピートトリガ)が取れればいいと思うので、それに関して取ってみましょう。

関数で毎フレーム毎にチェックする方式

まずは関数版です。GetAsyncKeyState関数を使います。こいつは関数が呼ばれた時のキーの状態を教えてくれます。キーボードのキー全部でこいつを呼んでいたら無駄そうですので、今回は上下左右、Z、Xキーのみの検出とします。

// 検出するキーの仮想キーコードの配列
std::array<int, 6> key = {VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN, 'Z', 'X'};
// キーのリピート情報を保持する配列
std::array<int, 6> repeat;
// どれくらいでリピートトリガと認めるかの数値
int repeat_cnt = 30;
// 押されていればtrueのbit配列
std::bitset<6> down_bit;
// 前回の押されていればtrueのbit配列
std::bitset<6> before_bit;
// 離された瞬間ならtrueのbit配列
std::bitset<6> up_bit;
// 押された瞬間ならtrueのbit配列
std::bitset<6> trig_bit;

void pool() {
  // 前回の情報を記録
  before_bit = down_bit;
  boost::for_each(boost::irange(0, 6), [&](const int x) {
    // 今回のキーの情報を取得
    down_bit[x] = GetAsyncKeyState(key[x]) < 0;
    // 押されていたらリピート回数を増やす
    repeat[x] = down_bit[x] ? (std::min)(repeat_cnt, repeat[x] + 1) : 0;
  });
  // トリガに関してはこのbit演算で取れる
  trig_bit = down_bit & ~before_bit;
  // 離された瞬間かどうかはこのbit演算で取れる
  up_bit = ~down_bit & before_bit;
}

// 毎フレーム呼ばれる関数
void update() {
  // キー情報更新
  pool();
  // 取り敢えず出力
  boost::for_each(boost::irange(0, 6), [&](const int x) {
    std::cout << std::boolalpha;
    std::cout << down_bit[x] << ",";
    std::cout << up_bit[x] << ",";
    std::cout << trig_bit[x] << ",";
    // リピートトリガはこれで取れる
    std::cout << (trig_bit[x] || repeat[x] == repeat_cnt) << std::endl;
  });
}

グローバル変数なのは適度にクラスに纏めるとかした方がいいと思いますが今回は面倒なのでこれで。bit演算に関しては多分これでいいと思うのですが、念の為各自確認しておいた方がいい気はします。あと、up_bitとtrig_bitは各キー毎にdown_bitとbefore_bitの値を使って出すのであれば要らないかもしれません。

メッセージを使ってコールバック方式

続いてコールバック方式です。WM_KEYDOWNとWM_KEYUPメッセージを使います。
WM_KEYDOWNはデフォルトでリピートトリガ的な挙動をするので少々面倒です。

// 押された瞬間に呼ばれるコールバック
std::function<void (int)> trig = [](const int x) {std::cout << "triger:" << x << std::endl;};
// リピートトリガ的に呼ばれるコールバック
std::function<void (int)> down = [](const int x) {std::cout << "down:" << x << std::endl;};
// 離された瞬間に呼ばれるコールバック
std::function<void (int)> up = [](const int x) {std::cout << "up:" << x << std::endl;};

LRESULT CALLBACK procedure(const HWND window_handle, const UINT message, const WPARAM wp, const LPARAM lp) {
  switch (message) {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    case WM_KEYDOWN:
      // リピートトリガ的に呼ばれる
      // wpが仮想キーコード
      down(wp);
      // lpの30bit目が0なら前回押されていないことになるのでこれでトリガが取れる
      if (!(lp & (1 << 30))) {
        trig(wp);
      }
      break;
    case WM_KEYUP:
      // 離された瞬間に呼ばれる
      // wpが仮想キーコード
      up(wp);
      break;
    default:
      return DefWindowProc(window_handle, message, wp, lp);
  }
  return 0;
}

で、これだと押されている間に呼ばれるコールバックというのが実現できません。簡単な解決策としては、押された瞬間に呼ばれるコールバックで押されたflagを立て、離された瞬間に呼ばれるコールバックでflagを折るとかして押されている間を検出するとかするのがいいのですかね。