macOSでOpenGLプログラミング（1-8. キーボード操作をサポートする）

[macOSでOpenGLプログラミングの目次に戻る](http://qiita.com/sazameki/items/d060f20f621c658046d9)

# はじめに

[前回](https://qiita.com/sazameki/items/138e943286238691a417)は、フレームごとの経過時間を計測し、FPSの計算と、時間に基づくアニメーション記述ができるようにしました。

今回は、ユーザのキーボード入力を受け取って処理するための、Inputクラスを追加しましょう。キーボード操作ができるようになると、画面に表示しているポリゴンの移動や、それを表示するためのカメラの移動といったものを実装した時に、正常に動作していることがテストしやすくなります。


# 1. Inputクラスを追加する

ユーザ入力の情報を格納しておくInputクラスを作成するために、C++のファイルを用意しておきましょう。

GameLibraryグループの中のTime.mmを右クリックして、出てくるメニューから「New File…」を実行します。

　<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/166309/1f076bea-c278-5e21-4b7d-9388a64c1c59.png">

テンプレート選択画面で「macOS」の「C++ File」を選択して、「Next」ボタンを押します。

　<img width="360" src="https://qiita-image-store.s3.amazonaws.com/0/166309/f888145a-b592-4311-23c3-2059c3d65c3b.png">

次の画面ではファイル名に「Input」と入力し、「Next」ボタンを押して、Input.hppとInput.cppの2つのファイルを作成します。

　<img width="360" src="https://qiita-image-store.s3.amazonaws.com/0/166309/8e56004c-9cb3-efbd-9422-fbb53674f14a.png">

作成されたInput.hppとInput.cpp

　<img width="240" src="https://qiita-image-store.s3.amazonaws.com/0/166309/0e50ef8e-b688-d3aa-6e82-ff9f6684e913.png">

Input.hppに、Inputクラスの宣言と、そこで使用する定数の定義を書きます。

```cpp:Input.hpp
#ifndef Input_hpp
#define Input_hpp

#include <cstdlib>

struct KeyCode
{
    static const uint64_t   UpArrow;
    static const uint64_t   DownArrow;
    static const uint64_t   LeftArrow;
    static const uint64_t   RightArrow;

    static const uint64_t   Space;
    static const uint64_t   Escape;
    static const uint64_t   Return;
    static const uint64_t   Shift;

    static const uint64_t   A;
    /* 省略 */
    static const uint64_t   Z;

    static const uint64_t   Alpha1;
    /* 省略 */
    static const uint64_t   Alpha9;
    static const uint64_t   Alpha0;
};

class Input
{
    static uint64_t keyState;
    static uint64_t keyStateOld;
    static uint64_t keyDownStateTriggered;
    static uint64_t keyUpStateTriggered;

public:
    static bool GetKey(uint64_t keyCode);
    static bool GetKeyDown(uint64_t keyCode);
    static bool GetKeyUp(uint64_t keyCode);

public:
    static void ProcessKeyDown(uint64_t keyCode);
    static void ProcessKeyUp(uint64_t keyCode);
    static void Update();

};
```

押されているキー情報を格納しておくために、64ビットの符号なし整数 (uint64_t) を使って、ビットマスクでキー情報を管理します。このキー情報を管理するのが`keyState`変数です。そして1フレーム前に押されていたキー情報を管理しておく`keyStateOld`変数を使い、ビット計算することで、特定のキーが押された瞬間であるか、特定のキーが離された瞬間であるかといった情報が計算できます。これらの情報を管理するのが`keyDownStateTriggered`変数と`keyUpStateTriggered`変数です。

キーが押されているか、キーが押された瞬間であるか、キーが離された瞬間であるかをチェックするために、`GetKey()`関数、`GetKeyDown()`関数、`GetKeyUp()`関数を使用します。

`ProcessKeyDown()`関数と`ProcessKeyUp()`関数は、Cocoaのビューがキーが押されたことやキーが離されたことを感知したときにInputクラスにその情報を伝達するための関数です。そして`Update()`関数を毎フレーム呼び出すことで、前フレームとの差異を計算して、キーが押された瞬間とキーが離された瞬間を算出します。

これらの関数を実装するInput.cppは次の通りです。

まずキー定数の宣言部です。キーを表す定数の数が多いため、かなり省略して書いています。詳しくは記事の下部に載せているサンプルプロジェクトをダウンロードして確認してください。

```cpp:Input.cpp（定数宣言部）
const uint64_t KeyCode::UpArrow     = (1ULL << 0);
const uint64_t KeyCode::DownArrow   = (1ULL << 1);
const uint64_t KeyCode::LeftArrow   = (1ULL << 2);
const uint64_t KeyCode::RightArrow  = (1ULL << 3);

const uint64_t KeyCode::Space       = (1ULL << 4);
const uint64_t KeyCode::Escape      = (1ULL << 5);
const uint64_t KeyCode::Return      = (1ULL << 6);
const uint64_t KeyCode::Shift       = (1ULL << 7);

const uint64_t KeyCode::A           = (1ULL << 8);
/* 省略 */
const uint64_t KeyCode::Z           = (1ULL << 33);

const uint64_t KeyCode::Alpha1      = (1ULL << 34);
/* 省略 */
const uint64_t KeyCode::Alpha0      = (1ULL << 43);
```

現在の環境では、unsigned long long型が最小の64ビットの符号なし整数ですので、unsigned long long型の1を表す「1ULL」を左ビットシフトすることで、各キー定数のビットマスクを定義していきます。とにかく数が多いので、ビットシフトの桁数を間違えないように入力していきます。

次にキー情報の処理部分です。

```cpp:Input.cpp（メインの処理）
uint64_t Input::keyState                = 0ULL;
uint64_t Input::keyStateOld             = 0ULL;
uint64_t Input::keyDownStateTriggered   = 0ULL;
uint64_t Input::keyUpStateTriggered     = 0ULL;

bool Input::GetKey(uint64_t keyCode)
{
    return (keyState & keyCode)? true: false;
}

bool Input::GetKeyDown(uint64_t keyCode)
{
    return (keyDownStateTriggered & keyCode)? true: false;
}

bool Input::GetKeyUp(uint64_t keyCode)
{
    return (keyUpStateTriggered & keyCode)? true: false;
}

void Input::ProcessKeyDown(uint64_t keyCode)
{
    keyState |= keyCode;
}

void Input::ProcessKeyUp(uint64_t keyCode)
{
    keyState &= ~keyCode;
}

void Input::Update()
{
    keyDownStateTriggered = keyState & ~keyStateOld;
    keyUpStateTriggered = ~keyState & keyStateOld;
    keyStateOld = keyState;
}
```

`GetKey〜()`系の関数の実装では、特定のキーを表すビットマスクと現在のキーの状態の論理積を取った場合に、いずれかのビットが立っているかどうかをチェックして、`true`か`false`をリターンします。

`ProcessKeyDown()`関数では、指定されたキーのビットマスクを論理和で現在のキー状態に足し合わせます。`ProcessKeyUp()`関数では、指定されたキーのビットマスクを論理反転してから論理積で掛け合わせることによって、現在のキー状態からビットを下げます。

`Update()`関数では、現在のキー状態を表すビット (`keyState`) と、1つ前のキー状態を表すビットを反転したもの (`~keyStateOld`) を論理積 (`&`) で掛け算することによって、キーが押された瞬間であるかどうかを算出します。これと同じ理屈で、現在のキー状態を表すビットの反転 (`~keyState`) と、1つ前のキー状態を表すビット (`keyStateOld`) を論理積 (`&`) で掛け算することによって、キーが離された瞬間であるかどうかが算出できます。最後に、現在のキー状態を1つ前のキー状態として`keyStateOld`変数に代入して保存しておきます。

例として、次のような状態を考えてみましょう。

　<img width="440" src="https://qiita-image-store.s3.amazonaws.com/0/166309/8e7f7a56-7278-4094-21da-d6c9887266d6.png">


これに対して、1つ前のフレームのビットを反転させてから、現在のキー状態と論理積を計算してみます。

　<img width="440" src="https://qiita-image-store.s3.amazonaws.com/0/166309/fffd5329-9e41-2909-1ea3-f885f4baef2d.png">

0から1に変化したビットだけが1になって、このキーがこの瞬間に押されたことが分かります。

また、1つ前のキー状態と、現在キー状態をビット反転させたものの論理積を計算してみます。

　<img width="440" src="https://qiita-image-store.s3.amazonaws.com/0/166309/057b9da8-1818-b493-df7d-ef3e81be11d7.png">

1から0に変化したビットだけが1になって、このキーがこの瞬間に離されたことが分かります。



# 2. Inputクラスに処理を伝達するコードを書く

それでは、ユーザ入力を受け取って、その情報をInputクラスに伝達するコードを書いてみましょう。これを行うのはViewControllerクラスですが、InputクラスがC++なので、Objective-C++が使えるようにするために、「ViewController.m」ファイルの拡張子を「.mm」に変えておきます。

　<img width="220" src="https://qiita-image-store.s3.amazonaws.com/0/166309/b71a5c77-4895-cdde-503d-aa113797b010.png">

ViewController.mmの先頭では、Input.hppをインクルードして、ViewControllerクラスの変数としてbool型の`isShiftOn`変数を追加しておきます。この変数は、他のキーと違ってshiftキーは別の形で押されたことを検知するため、shiftキーが事前に押されていたかどうかを自前でチェックするために用意しています。`viewDidLoad`メソッドの最後で、この`isShiftOn`変数を`false`で初期化しています。

```objc++:ViewController.mm（前半）
#import "ViewController.h"
#import "MyGLView.h"
#include "Input.hpp"

@implementation ViewController {
    bool    isShiftOn;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    NSRect viewFrame = [self.view frame];
    MyGLView *glView = [[MyGLView alloc] initWithFrame:viewFrame];
    glView.translatesAutoresizingMaskIntoConstraints = YES;
    glView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
    [self.view addSubview:glView];

    isShiftOn = false;
}
```

キーが押された場合には、ViewControllerクラスの`keyDown:`メソッドが呼ばれますので、キーコードを取得して、何のキーが押されたかをひとつひとつチェックしていきます。そしてそのキーに対応した定数を使って、Inputクラスの`ProcessKeyDown()`関数を呼び出します。キーが離された場合には、`keyUp:`メソッドが呼ばれます。その処理は、`ProcessKeyDown()`関数ではなく`ProcessKeyUp()`関数を呼び出すところだけが異なります。


```objc++:ViewController.mm（後半）
- (void)keyDown:(NSEvent *)theEvent
{
    unsigned short keyCode = [theEvent keyCode];

    if (keyCode == 0x7e) {
        Input::ProcessKeyDown(KeyCode::UpArrow);
    }
    else if (keyCode == 0x7d) {
        Input::ProcessKeyDown(KeyCode::DownArrow);
    }
    /* 以下、省略 */
}

- (void)keyUp:(NSEvent *)theEvent
{
    unsigned short keyCode = [theEvent keyCode];

    if (keyCode == 0x7e) {
        Input::ProcessKeyUp(KeyCode::UpArrow);
    }
    else if (keyCode == 0x7d) {
        Input::ProcessKeyUp(KeyCode::DownArrow);
    }
    /* 以下、省略 */
}
```

以上の実装で、矢印キー、リターンキー、スペースキー、escキー、A〜Zの英字キー、0〜9の数字キーが処理できるようになります。

shiftキーが押された時には、`flagsChange:`メソッドが呼ばれます。注意しなければならないのは、commandキーやoptionキーが押された時にもこのメソッドが呼ばれるため、shiftキーが押され続けている間にcommandキーが押されたり離されたりしたことで、このメソッドが呼ばれることもあるということです。`modifierFlags`で取得できる値は、現在キーが押されているかどうかしか保持していません（ちなみに右のshiftキーと左のshiftキーのどちらが押されているかもこの値から取得できますが、今はそこまで細かく調べません）。そのことに起因した誤判定が起きるのを避けるために、`isShiftOn`変数を使って、本当にshiftキーが押されたり離されたりしたタイミングなのかどうかをチェックします。

```objc++:ViewController.mm（最後部）
- (void)flagsChanged:(NSEvent *)theEvent
{
    NSUInteger modifierFlags = [theEvent modifierFlags];

    if (modifierFlags & NSEventModifierFlagShift) {
        if (!isShiftOn) {
            Input::ProcessKeyDown(KeyCode::Shift);
            isShiftOn = true;
        }
    } else {
        if (isShiftOn) {
            Input::ProcessKeyUp(KeyCode::Shift);
            isShiftOn = false;
        }
    }
}
```

最後に忘れずに、MyGLViewクラスの`prepareOpenGL`メソッドの先頭で、MyGLViewビューが最初にキーボードなどのイベントを受け取ることを設定するために`-[NSWindow makeFirstResponder:]`メソッドを呼び出しておきます。これがないと、キーボードのイベントがViewControllerクラスに伝達されず、Inputクラスの情報も更新できなくなります。

```objc++:MyGLView.mm（prepareOpenGLメソッド）
- (void)prepareOpenGL
{
    [super prepareOpenGL];

    [self.window makeFirstResponder:self];

    /* 以下、省略 */
}
```

また、直前のフレームでキーボードが押されたかどうかを判定するためには、前述の`Input::Update()`関数を毎フレーム呼び出す必要があります。`-[MyGLView render]`メソッドの先頭に、そのためのコードを追加しましょう。

```objc++:MyGLView.mm（prepareOpenGLメソッド）
- (void)render
{
    Input::Update();
    ...
}
```

# 3. Inputクラスを利用するコードを書く

最後に、こうして用意したInputクラスを使って、ゲームの操作を行うコードをGameクラスに書きましょう。

上で実装したInputクラスが利用できるように、Game.hppの先頭でInput.hppをインクルードしておきます。

```cpp:Game.hpp（一部）
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include "Time.hpp"
#include "Input.hpp"
...
```

そして`Game::Render()`関数の実装を修正し、前回までは`value`変数を時間経過に応じて変化させていたのを、キーボードの左矢印キーと右矢印キーが押された時に、毎秒0.5の速度で`value`がマイナス方向やプラス方向に変化するように書き換えます。

```cpp:Game.cpp（Render()関数）
void Game::Render()
{
    if (Input::GetKey(KeyCode::LeftArrow)) {
        value -= 0.25f * Time::deltaTime;
    }

    if (Input::GetKey(KeyCode::RightArrow)) {
        value += 0.25f * Time::deltaTime;
    }

    if (value < 0.0f) {
        value = 0.0f;
    } else if (value > 1.0f) {
        value = 1.0f;
    }

    glClearColor(1.0f - value, value, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
}
```

以上で、左と右の矢印キーで色が変更できるようになりました。

　<img width="420" src="https://qiita-image-store.s3.amazonaws.com/0/166309/de52079a-b63b-2af9-433b-74a21af37762.png">

ここまでのプロジェクト：[MyGLGame_step1-8.zip](http://sazameki.jp/temp/MyGLGame_step1-8.zip)


# 4. まとめ

今回はキーボード入力を処理するための`Input`クラスを用意し、それを使ってゲーム内の変数の値を変更できるようにしました。

次回は`Input`クラスをさらに改良して、マウス操作をサポートできるようにしましょう。

---
次の記事：[macOSでOpenGLプログラミング（1-9. マウス操作をサポートする）](https://qiita.com/sazameki/items/68baa657b2f6488f7704)