はじめに
前回では、マウス/キーボード入力時に発生するメッセージを通じて、ユーザー操作に対応しました。
アプリによっては、このメッセージ処理で充分に目的を果たせます。しかし、そうでないアプリもあるでしょう。
今回は、ウィンドウ関数を介さず、マウス/キーボード入力へ対応する方法を解説します。
マウス入力
まずはマウスの解説から入ります。
今回のサンプルでは新たにマウス用の構造体、および、クラスを追加しています。
CMyInput.h
// マウス入力構造体
typedef struct tag_mymousestate
{
POINT ptCursor;
SHORT nLeft;
SHORT nRight;
} MYMOUSESTATE;
POINT
はX座標、Y座標を表す構造体で、ptCursor
はマウスカーソルの座標値を持ちます。
nLeft
とnRight
はマウスの左右ボタンの押下状態を持ちます。
この新たに定義した構造体は、新たに定義したクラスのメンバ変数として使用します。
CMyInput.h
// マウス入力クラス
class CMyMouse
{
int nCurrentIndex;
MYMOUSESTATE ms[2];
MYMOUSESTATE
構造体を配列としている理由は、現在の状態と一つ前の状態を持つためです。
nCurrentIndex
はどちらの要素が現在のマウス状態かを指すインデックスとなります。
現在の状態と一つ前の状態と比較することで、マウスカーソルの移動量やボタンの状態遷移を判定することができます。
マウスのカーソル座標取得やボタン押下の状態取得はシンプルです。
CMyInput.cpp
// 現在のマウス状態をセット
void CMyMouse::SetMouseState()
{
// カーソル座標をセット
GetCursorPos(&ms[nCurrentIndex].ptCursor);
// ボタンの状態をセット
ms[nCurrentIndex].nLeft = GetAsyncKeyState(VK_LBUTTON);
ms[nCurrentIndex].nRight = GetAsyncKeyState(VK_RBUTTON);
}
GetCursorPos関数はマウスカーソルの座標を取得します。
注意点はディスプレイでの座標が取得されることです。
多くの場合は、ウィンドウのクライアント領域座標が欲しいと思われます。
そこで、ScreenToClient関数を使用します。この関数は、ディスプレイでの座標を指定ウィンドウのクライアント座標に変換します。
ScreenToClient(hwnd, &ms[nCurrentIndex].ptCursor);
GetAsyncKeyState関数は指定のキーの押下状態を16ビットの値で取得します。
戻り値の最上位ビットが1
の場合は、押下状態を指します。
また、最下位ビットが1
の場合は、押下状態へ遷移したことを意味します。ボタンが押されっぱなしの場合、このビットは0
となります。
GetAsyncKeyState関数の引数として指定されている VK_LBUTTON
VK_RBUTTON
はそれぞれマウスの左ボタンと右ボタンを表す仮想キーコードです。
なお、VK_LBUTTON
VK_RBUTTON
は物理的な左ボタン、右ボタンを意味します。
Windowsのマウス設定オプションから「主と副のボタンを切り替える」にチェックを入れても影響を受けません。
その他にもGetAsyncKeyState関数の注意点はありますが、詳細は改めて後述します。
ボタンの状態を取得した後は、実行したい処理に応じて判定するだけです。
SHORT nCurKey = 0, nOldKey = 0;
nCurKey = ms[nCurrentIndex].nLeft;
nOldKey = ms[1 - nCurrentIndex].nLeft;
// ボタンが押されているか
if(nCurKey & 0x8000)
...
// ボタンが押されっぱなしか
if((nCurKey & 0x8000) && (nOldKey & 0x8000))
...
// ボタンが放されたか
if(!(nCurKey & 0x8000) && (nOldKey & 0x8000))
...
// ボタンが押された瞬間か
if((nCurKey & 0x8000) && !(nOldKey & 0x8000))
...
キーボード入力
キーボードについても、マウスと同様の構造体、および、クラスを新規に追加しています。
CMyInput.h
// キーボード入力構造体
typedef struct tag_mykeyboardstate
{
SHORT nLeft; // テンキー4
SHORT nRight; // テンキー6
SHORT nUp; // テンキー8
SHORT nDown; // テンキー2
SHORT nButton1; // Zキー
SHORT nButton2; // Xキー
} MYKEYBOARDSTATE;
アプリで使用するキーの押下状態を保持する構造体です。
マウスと異なる点はカーソル座標がないことと、使用するキーが多いことのみです。
CMyInput.h
// キーボード入力クラス
class CMyKeyboard
{
int nCurrentIndex;
MYKEYBOARDSTATE mk[2];
こちらもマウスのときと同様です。
MYKEYBOARDSTATE
構造体を配列で扱い、新旧のキー状態を保持できるようにします。
キーの状態を取得する方法も同じで、GetAsyncKeyState関数を使用します。
CMyInput.cpp
// 現在のキー入力状態をセット
void CMyKeyboard::SetMyKeyState()
{
mk[nCurrentIndex].nLeft = GetAsyncKeyState(VK_NUMPAD4);
mk[nCurrentIndex].nRight = GetAsyncKeyState(VK_NUMPAD6);
mk[nCurrentIndex].nUp = GetAsyncKeyState(VK_NUMPAD8);
mk[nCurrentIndex].nDown = GetAsyncKeyState(VK_NUMPAD2);
mk[nCurrentIndex].nButton1 = GetAsyncKeyState(VK_Z);
mk[nCurrentIndex].nButton2 = GetAsyncKeyState(VK_X);
}
GetAsyncKeyState関数は、取得したいキーを一つずつ指定することになります。
マウスのようにボタンが少なかったり、当サンプルのように使用するキーが少ない場合は特に気にならないかと思います。
しかし、例えばタイピングゲームのように多くのキー状態を取得したい場合、一つずつ指定していくのは煩雑です。
この場合、すべての仮想キーの状態を取得するGetKeyboardState関数という選択肢もあります。
GetAsyncKeyStateとGetKeyboardState、どちらが自分のアプリに適しているのか。
まずは両者の特徴を並べてみましょう。
GetAsyncKeyState()
前述のとおり、指定されたキーの状態を16ビットの値で取得します。
一つのキーのみが取得対象のため、GetKeyboardStateに比べて高速です。
しかし、複数のキーを取得するためには何度も呼び出す必要があり、ある時点からは性能で劣ることになります。
そのほかの特徴として、関数名が表すようにAsync(非同期)でキーの状態を取得します。
これはWM_KEYDOWNメッセージ等と同期しないことを意味します。
つまり、他のウィンドウがフォーカスされている状態でも、キー状態の取得が行われます。
GetKeyboardState()
一方のGetKeyboardState関数は、一度の呼び出しですべての仮想キーの状態を取得します。
各キーの状態は8ビットの値で、押下状態のとき最上位ビットが1
となります。
256個のキー状態を取得する場合はGetAsyncKeyStateと比べ、はるかに高速です。
そして、メッセージと同期する点も異なります。
他のウィンドウにフォーカスがある状態では、キーの押下状態を表すビットは立ちません。
参考までにGetKeyboardStateに置き換えると、サンプルのソースコードは下記になります。
キー取得処理と8ビットでの判定処理に違いがありますね。
// 現在のキー入力状態をセット
void CMyKeyboard::SetMyKeyState()
{
BYTE byKeyState[256];
if(GetKeyboardState(byKeyState) == TRUE)
{
mk[nCurrentIndex].nLeft = byKeyState[VK_NUMPAD4];
mk[nCurrentIndex].nRight = byKeyState[VK_NUMPAD6];
mk[nCurrentIndex].nUp = byKeyState[VK_NUMPAD8];
mk[nCurrentIndex].nDown = byKeyState[VK_NUMPAD2];
mk[nCurrentIndex].nButton1 = byKeyState[VK_Z];
mk[nCurrentIndex].nButton2 = byKeyState[VK_X];
}
}
byKeyState[256]
は256個の仮想キーの状態を格納します。
配列のインデックスには、仮想キーの値をそのまま使用できます。
SHORT nCurKey = 0, nOldKey = 0;
nCurKey = mk[nCurrentIndex].nLeft;
nOldKey = mk[1 - nCurrentIndex].nLeft;
// ボタンが押されているか
if(nCurKey & 0x80)
...
// ボタンが押されっぱなしか
if((nCurKey & 0x80) && (nOldKey & 0x80))
...
// ボタンが放されたか
if(!(nCurKey & 0x80) && (nOldKey & 0x80))
...
// ボタンが押された瞬間か
if((nCurKey & 0x80) && !(nOldKey & 0x80))
...
取得したいキーの個数、リアルタイム性の重要度、ウィンドウのフォーカス等が使い分けのポイントとなるでしょう。
API | 長所 | 注意点 |
---|---|---|
GetAsyncKeyState | 高速、リアルタイム性 | 頻繁なコールで性能劣化 ウィンドウのフォーカス不問 |
GetKeyboardState | 多くのキー入力時に効率的 | 呼び出しが遅い ウィンドウのフォーカス必要 |
キー状態の取得間隔
GetAsyncKeyStateとGetKeyboardStateのいずれを使用するにしろ、ゲームの場合は共通した注意点があります。
それは、キー状態の取得間隔です。
サンプルのソースコードでは、メッセージループの中でアプリ固有の処理を実施しています。
アプリ固有の処理では「キー入力状態セット」->「キー入力状態に応じてスプライト状態を更新」->「背景描画」->「スプライト描画」->「再描画要求」->「キー入力状態セット」->...というサイクルを延々と繰り返します。
// フレームの更新
void CMyExample::UpdateFrame()
{
DWORD dwTickCount = GetTickCount();
// 自スプライト状態に応じたフレーム更新
switch(sprite.nState)
{
...
// 操作可能(立ち/歩き)状態
case SPRITE_STATE_STAY:
case SPRITE_STATE_WALK:
// フレーム更新周期
if(dwTickCount - dwLastTickCount >= MYEXAMPLE_REFLASH_RATE)
{
// キー入力状態セット
mykey.SetMyKeyState();
mymouse.SetMouseState();
// キー入力状態チェック
CheckKeyInput(); // スプライトの状態を更新
// 次のフレーム更新までのズレを調整し、更新間隔を一定に保つ
dwLastTickCount = dwTickCount - (dwTickCount - dwLastTickCount - MYEXAMPLE_REFLASH_RATE);
// キー状態遷移
mykey.SwitchCurrentIndex();
mymouse.SwitchCurrentIndex();
// 描画
DrawBackground();
DrawSprite(dwTickCount);
MoveOtherSpriteCoord();
DrawOtherSprite(dwTickCount);
InvalidateRect(hwnd, NULL, FALSE);
...
この処理における「キー入力状態セット」から次の「キー入力状態セット」がキーの取得間隔になります。
キーの取得間隔が長いと、ボタン押下を検知できない可能性があります(例えば、キーの取得間隔が1秒間隔として、ユーザーが0.1秒間だけボタンを押した場合)。
とは言え、キーの取得間隔が短ければよいというわけでもありません(ユーザーは1回しかボタンを押していないつもりでも、意図せず連打として扱われてしまうなど)。
どのような間隔が最適なのかは断定が困難ですのでMYEXAMPLE_REFLASH_RATE
と定数の定義を行い、調整を容易にしています。
いずれにしろ、ゲームのように操作性が重要となるアプリでは、キー入力を一定の間隔で行うほうが望ましいかと思われます。
今回のサンプルコードを実行すると、前回のメッセージ処理よりも操作性が向上していることが実感できることでしょう。
次回予告
第1回のウィンドウの生成から始まり、ビットマップの描画、タイマーにユーザー入力イベントと解説をしてきました。
これまでの内容を総動員すれば、シンプルなゲームならば作成できそうです。
次回はいよいよゲーム作りに着手していきましょう。
※なお、この記事を執筆している時点(2025.06.28現在)でも、どのようなゲームを作るか未定の状態です。
真面目に考えていないとアイデアは湧いてこないものですね。