はじめに

私は趣味でゲーム制作をしているのですが、先日ゲームエンジニアの方とお話する機会がありました。
その際に、操作の気持ちよさやフラストレーションの緩和のために、先行入力の技術が使われていると教えていただきました。
私は先行入力といえば、アクションゲームのコンボやコマンドの実装で使うものとばかり思っていましたが、いろんなゲームで使われていると知り目から鱗でした。

というわけで練習がてら先行入力を実装してみようと思います。
今回実装したコードはこちらに一応置いてあります。

環境

C++
DXライブラリ

慣れているこの環境で。

先行入力を使わない場合

とりあえず何か動くものがないと始まらないので、こんなものをぱぱっと作りました。

demo1.gif

方向キーでカーソルを動かしてマス目を移動し、スペースキーで赤くするだけのプログラム。

見てもらうとわかると思いますが、カーソルの移動中はキー入力を受け付けなくなります。
ですので連続してキー入力を行うとうまくカーソルを動かせなかったり、スペースを押したつもりでもマスが赤くならなかったりしマス。
ユーザが入力したと思ったのに反映されていないというのは、ストレスですよね。

キー入力の該当コードだけ貼ります。

if (cursor_move_remaining == 0) {
  if (Input::GetKeyDown(KEY_INPUT_RIGHT)) {
    // カーソルを動かす処理
  }
  if (Input::GetKeyDown(KEY_INPUT_LEFT)) {
    // カーソルを動かす処理 
  }
  if (Input::GetKeyDown(KEY_INPUT_DOWN)) {
    // カーソルを動かす処理
  }
  if (Input::GetKeyDown(KEY_INPUT_UP)) {
    // カーソルを動かす処理
  }
  if (Input::GetKeyDown(KEY_INPUT_SPACE)) {
    // マスを赤くする/元に戻す処理
  }
}

カーソルが移動中でないなら、処理を受け付ける。
よくある原始的な実装です。

キー入力判定用のクラスはこんな感じ

class Input {
 private:
  static char key_buf[256];
  static int key_frame[256];

 public:
  static void UpdateKeyState() {
    GetHitKeyStateAll(key_buf);

    for (int i = 0; i < 256; i++) {
      if (key_frame[i] == -1) key_frame[i]++;

      if (key_buf[i] == 1) {
        key_frame[i]++;
      } else if (key_frame[i] > 0) {
        key_frame[i] = -1;
      }
    }
  }

  static bool GetKeyDown(int DX_KEY_CODE) {
    return key_frame[DX_KEY_CODE] == 1;
  }
  static bool GetKey(int DX_KEY_CODE) { return key_frame[DX_KEY_CODE] > 0; }
  static bool GetKeyUp(int DX_KEY_CODE) { return key_frame[DX_KEY_CODE] == -1; }
};

本題じゃないので適当に流し見してもらって大丈夫です。
DXライブラリのキー入力判定関数だけでは使いにくいので自前でラッピングします。
よくあるGetKeyDown、Upが使えます。
UpdateKeyStateを毎フレーム呼び出してやるのが前提条件です。

先行入力を実装してみる

さてここからが本題です。
先ほどのInputクラスに先行入力用のロジックを追加してみます。
基本的な考えとしては、入力された情報(どのキーが、何フレーム前に押されたか)を保存しておき、判定する際に猶予フレームを渡すことでそのフレーム内にキーが押されていたかを判定します。

主に追加・変更した部分を載せます。

class Input {
 public:
  // スタックするキー入力情報の構造体
  struct ListData {
    int code;
    int frame;
    ListData(int code) {
      this->code = code;
      frame = 0;
    }
  };

 private:
  static list<ListData> key_stack;
  static const int save_frame;

 public:
  static void UpdateKeyState() {
    GetHitKeyStateAll(key_buf);

    // スタックされた入力情報の、経過フレームを計算
    for (auto& data : key_stack) {
      data.frame++;
    }

    for (int i = 0; i < 256; i++) {
      if (key_frame[i] == -1) key_frame[i]++;

      if (key_buf[i] == 1) {
        key_frame[i]++;
        // 押された瞬間に入力情報をスタック
        if (key_frame[i] == 1) key_stack.emplace_back(ListData(i));
      } else if (key_frame[i] > 0) {
        key_frame[i] = -1;
      }
    }
    // 一定フレーム経った入力情報を削除
    key_stack.erase(
        remove_if(key_stack.begin(), key_stack.end(),
                  [](ListData data) { return data.frame >= save_frame; }),
        key_stack.end());
  }

  static bool GetKeyPrecede(int DX_KEY_CODE, int delay) {
    auto result =
        find_if(key_stack.begin(), key_stack.end(),
                [DX_KEY_CODE, delay](ListData data) {
                  return data.code == DX_KEY_CODE && data.frame < delay;
                });
    if (result != key_stack.end()) {
      key_stack.erase(result);
      return true;
    } else {
      return false;
    }
  }

これを使って先ほどのキー操作受付部分を書き換えます。

if (cursor_move_remaining == 0) {
  if (Input::GetKeyPrecede(KEY_INPUT_RIGHT, 20)) {
    // カーソルを動かす処理 
  } else if (Input::GetKeyPrecede(KEY_INPUT_LEFT, 20)) {
    // カーソルを動かす処理 
  } else if (Input::GetKeyPrecede(KEY_INPUT_DOWN, 20)) {
    // カーソルを動かす処理 
  } else if (Input::GetKeyPrecede(KEY_INPUT_UP, 20)) {
    // カーソルを動かす処理 
  } else if (Input::GetKeyPrecede(KEY_INPUT_SPACE, 20)) {
    // マスを赤くする/元に戻す処理 
  }
} 

条件のところが変わっただけですね。

これで20フレーム以内に入力された操作も反映されるようになります。

demo2.gif

さらに使いやすく

とりあえず先行入力は実装できましたが、少しまだ問題がありました。
猶予フレーム内に入力されたものであれば入力順を考慮せず実行してしまいます。
上記の条件文だと、← →と入力した場合、if文が先にあるのが→の入力の受付なので、ユーザは←を先に入力したにもかかわらず、→が優先されカーソルは→に動いてしまいます。

というわけで猶予フレーム内に押されたキーの順序関係を保持した情報を返す機能が必要になりそうです。
Inputクラスに追加したコードが以下。

static list<int> GetKeyList(int delay) {
  list<int> find_list;
  for (auto data : key_stack) {
    if (data.frame < delay) {
      find_list.emplace_back(data.code);
    }
  }
  return find_list;
}

static void DeleteKeyDataFirst(int code) {
  key_stack.erase(
      remove_if(key_stack.begin(), key_stack.end(),
        [code](ListData data) { return data.code == code; }),
      key_stack.end());
}

このGetKeyList関数で猶予フレーム内に入力されたすべての情報を、順序を保ったまま取得します。
DeleteKeyDataFirstは入力を受付済みの入力情報を削除するために用意してあります。

これを使ってカーソル操作の条件文も書き換えます。

if (cursor_move_remaining == 0) {
  auto code_list = Input::GetKeyList(20);
  for (auto code : code_list) {
    if (code == KEY_INPUT_RIGHT) {
      // カーソルを動かす処理
    } else if (code == KEY_INPUT_LEFT) {
      // カーソルを動かす処理
    } else if (code == KEY_INPUT_DOWN) {
      // カーソルを動かす処理
    } else if (code == KEY_INPUT_UP) {
      // カーソルを動かす処理
    } else if (code == KEY_INPUT_SPACE) {
      // マスを赤くする/元に戻す処理
    } else {
      continue;
    }
    Input::DeleteKeyDataFirst(code);
    break;
  }
}

猶予フレーム内に入力されたキー情報を取得し、それを先頭から参照することで入力順を考慮した実装ができます。
今回の場合、一つのキー操作を受け付けたらそのままbreakして他の情報を無視するようにしています。

で、このようになりました。

demo3.gif

ジグザグにキーを入力しても、ちゃんと順序通りにカーソルが移動してくれています。

おわりに

とりあえず動くものが実装できました。
ここから発展させれば格ゲーのコマンド入力判定も作れそうですね。
特に何も参考にせず実装したので、「こういうやり方の方がいいよ!」っていうのがありましたら是非コメントください。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.