はじめに
前回は、フレームごとの経過時間を計測し、FPSの計算と、時間に基づくアニメーション記述ができるようにしました。
今回は、ユーザのキーボード入力を受け取って処理するための、Inputクラスを追加しましょう。キーボード操作ができるようになると、画面に表示しているポリゴンの移動や、それを表示するためのカメラの移動といったものを実装した時に、正常に動作していることがテストしやすくなります。
1. Inputクラスを追加する
ユーザ入力の情報を格納しておくInputクラスを作成するために、C++のファイルを用意しておきましょう。
GameLibraryグループの中のTime.mmを右クリックして、出てくるメニューから「New File…」を実行します。
テンプレート選択画面で「macOS」の「C++ File」を選択して、「Next」ボタンを押します。
次の画面ではファイル名に「Input」と入力し、「Next」ボタンを押して、Input.hppとInput.cppの2つのファイルを作成します。
作成されたInput.hppとInput.cpp
Input.hppに、Inputクラスの宣言と、そこで使用する定数の定義を書きます。
#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は次の通りです。
まずキー定数の宣言部です。キーを表す定数の数が多いため、かなり省略して書いています。詳しくは記事の下部に載せているサンプルプロジェクトをダウンロードして確認してください。
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」を左ビットシフトすることで、各キー定数のビットマスクを定義していきます。とにかく数が多いので、ビットシフトの桁数を間違えないように入力していきます。
次にキー情報の処理部分です。
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
変数に代入して保存しておきます。
例として、次のような状態を考えてみましょう。
これに対して、1つ前のフレームのビットを反転させてから、現在のキー状態と論理積を計算してみます。
0から1に変化したビットだけが1になって、このキーがこの瞬間に押されたことが分かります。
また、1つ前のキー状態と、現在キー状態をビット反転させたものの論理積を計算してみます。
1から0に変化したビットだけが1になって、このキーがこの瞬間に離されたことが分かります。
2. Inputクラスに処理を伝達するコードを書く
それでは、ユーザ入力を受け取って、その情報をInputクラスに伝達するコードを書いてみましょう。これを行うのはViewControllerクラスですが、InputクラスがC++なので、Objective-C++が使えるようにするために、「ViewController.m」ファイルの拡張子を「.mm」に変えておきます。
ViewController.mmの先頭では、Input.hppをインクルードして、ViewControllerクラスの変数としてbool型のisShiftOn
変数を追加しておきます。この変数は、他のキーと違ってshiftキーは別の形で押されたことを検知するため、shiftキーが事前に押されていたかどうかを自前でチェックするために用意しています。viewDidLoad
メソッドの最後で、このisShiftOn
変数をfalse
で初期化しています。
#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()
関数を呼び出すところだけが異なります。
- (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キーが押されたり離されたりしたタイミングなのかどうかをチェックします。
- (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クラスの情報も更新できなくなります。
- (void)prepareOpenGL
{
[super prepareOpenGL];
[self.window makeFirstResponder:self];
/* 以下、省略 */
}
また、直前のフレームでキーボードが押されたかどうかを判定するためには、前述のInput::Update()
関数を毎フレーム呼び出す必要があります。-[MyGLView render]
メソッドの先頭に、そのためのコードを追加しましょう。
- (void)render
{
Input::Update();
...
}
3. Inputクラスを利用するコードを書く
最後に、こうして用意したInputクラスを使って、ゲームの操作を行うコードをGameクラスに書きましょう。
上で実装したInputクラスが利用できるように、Game.hppの先頭でInput.hppをインクルードしておきます。
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include "Time.hpp"
#include "Input.hpp"
...
そしてGame::Render()
関数の実装を修正し、前回まではvalue
変数を時間経過に応じて変化させていたのを、キーボードの左矢印キーと右矢印キーが押された時に、毎秒0.5の速度でvalue
がマイナス方向やプラス方向に変化するように書き換えます。
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);
}
以上で、左と右の矢印キーで色が変更できるようになりました。
ここまでのプロジェクト:MyGLGame_step1-8.zip
4. まとめ
今回はキーボード入力を処理するためのInput
クラスを用意し、それを使ってゲーム内の変数の値を変更できるようにしました。
次回はInput
クラスをさらに改良して、マウス操作をサポートできるようにしましょう。