ゲームプログラミング/入力
プラットフォームごとの入力の差異
[編集]入力機器は、利用者からの入力を計算機に与えるものを指します。代表的なものには、キーボードとマウスがあげられます。これらの機器に入力を行うと、計算機のCPUに割り込み(ハードウェア割り込み)を行い、計算機はこれらの機器から何らかの入力が行われた事を知ります。実際には機器から送られてくる信号は入力デバイスなどの種類によって異なる為、入力機器と計算機に共通の意味づけを持った信号を使う事で、計算機は実際に利用者によって行われた入力を知る事ができます。
キーボードやマウスからの入力はPC/AT互換機と呼ばれる種類のコンピュータでは統一されており、計算機はその仕様に従って信号を解釈します。しかし、西暦2000年以降の現代のコンピュータではこのような入力機器からの入力はOSによって扱われるのが一般的であり、ゲームプログラマーが入力信号の仕様を意識する必要は、あまりありません。これは入力機器からの信号を直接扱うよりも、人間にとってわかりやすい形にしてから入力を提示した方が、利用者にとって便利であることによります。
もし、これらの入力の仕様の詳細が必要ならOADG[1]の文献を参照してください。
なお、残念なことに、OSが入力機器からの入力を利用者に提示する方法は、OSごとに異なっています。
このため、ゲーム作品の発表において、どのOSで作品を発表するかを考える必要があります。
マイクロソフト社ウィンドウズのプログラム開発ソフト Visual Studio でも、アップル社スマホOSのiOSやグーグル社スマホOSのAndroidなど他OS用の開発環境も提供していますが、最終的に、iOSやAndroidなどの目的のOSのインストールされたスマートフォンなど現物のハードウェアを用意して、プログラマは動作確認を取る必要があります。
ゲームの開発ではこれらの非互換性が厄介な問題になります。
技術的に専門的なことを言えば、(ウィンドウズでない)UNIX系のOSでは入力機器からの信号を利用するために、デバイスファイルを利用します(UNIX/Linux入門も参照)。一方、Windowsでは、デバイスファイルは存在せず、別の方法を使います。
これは、各OSごとに入力を扱う方法を変えなくてはいけないからです。実際にはこの問題はいくつかの方法で回避されます。1つの方法としてプラットフォームによらない入力を提供するJavaのような言語を利用する事が出来ます。もしくは、プラットフォームごとの入力の差異を吸収するようなライブラリを利用する事です。C言語を利用する場合にはこのようなライブラリはいくつか知られています。代表的な例ではGTK+、Qt、SDLが挙げられます。これらはどれもWindows, Mac OS X, Linuxなどをサポートしており、これらに共通の入力を与えます。複数のプラットフォームで動作するようなプログラムを書きたい時にはこのようなライブラリを利用することを考えると良いでしょう。
なお、webブラウザ上で動くゲームの場合、ブラウザ側が入出力を処理してくれるので、OSにあまり依存しません。しかし、代わりにブラウザに依存します。
つまり,マイクロソフト社のwebブラウザ Internet Explorer 上で動いたJavaScriptプログラムは、必ずしも、FireFox や Google Chorome などの他ブラウザで動くとは、かぎりません。
このほか、ゲームエンジンのw:SDLのような、どのOS上でも動くツールもありますが、当然、ほかのゲームエンジン上ではSDL用プログラムは動きません。
結局、どのようなプラットフォーム上でゲームを発表するのかを、事前に考えておく必要があります。
Windows API の場合の基本
[編集]2021年の段階では、DirectXを使わずに Windows API だけでゲーム制作をする事例は、あまりありません。
ですが、まだこのwiki編集者たちのDirectXについての調査が追いついていないので、読者は当分のあいだ、Winodows APi の場合の説明で我慢してください。
基本
[編集]WindowsAPi で入力処理をプログラミングする場合、普通は、OSの提供する入力うけつけ機能を使う事になると思います。Windowsの場合、たとえばWindows API に、入力受け付けの機能があります。
Windows API では、イベント処理の定義部分に case WM_KEYDOWN:
を定義することで、簡単に入力受け付けをできます。
たとえば
case WM_KEYDOWN:
switch (wParam) {
case VK_UP: // 「上キーが入力されたら」という意味
/* ここに上キー入力後の処理を書く*/
break; // これは VK_UP からのbreak
} // これは switch (wParam) の終わりのカッコ
break; // これは WM_KEYDOWN: からのbreak
という構文で簡単に処理を定義できます。
Vkとは仮想キーという意味です。仮想と言いますが、実際にプログラミングで使いますので、あまり「仮想」という接頭語に深い意味は無いです。ここの名前「VK_UP」は、けっして勝手に変えてはいけません。Windowsがキーの種類を判断するのに使う名前なので、上キーなら VK_UP のままにします。
ほとんどのWindowsゲームで方向キーの受け付けを使うでしょうから、上記コード例をこのまま使ってしまいましょう。
なお、上下左右の方向キー受け付けのコードを書くなら、下記のようになります。
case WM_KEYDOWN:
switch (wParam) {
case VK_UP: // 「上キーが入力されたら」という意味
/* ここに上(↑)キー入力後の処理を書く*/
break; // これは VK_UP: からのbreak
case VK_DOWN:
/* ここに下(↓)キー入力後の処理を書く*/
break; // これは VK_DOWN: からのbreak
case VK_RIGHT:
/* ここに右(→)キー入力後の処理を書く*/
break; // これは VK_RIGHT からのbreak
case VK_LEFT:
/* ここに左(←)キー入力後の処理を書く*/
break; // これは VK_LEFT: からのbreak
} // これは switch (wParam) の終わりのカッコ
break; // これは WM_KEYDOWN: からのbreak
windows API で方向キー受付を使うなら、このままコードを使いまわしてしまいましょう。
Windowsでは、どこに書くか
[編集]で、上記のcase WM_KEYDOWN: のコードをどこに書く必要があるかというと、Visual Studio でWindows APIを開くと『[[]]』で紹介したようなコードが出てきて、その後半のほうに、
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
というコードがあるので、この case WM_PAINT:
の節の前後どちらかに書くとラクである。
なぜなら、普通のゲームでは、キーボード入力の直後に画面を書き換えるためのコードも必要になるので、画面描画を扱う case WM_PAINT:
が近くにあると見やすい。
なので、つまり結果として、下記のような構成にするとラクである。
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
EndPaint(hWnd, &ps);
}
break;
case WM_KEYDOWN:
switch (wParam) {
case VK_UP: // 「上キーが入力されたら」という意味
/* ここに上(↑)キー入力後の処理を書く*/
break; // これは switch (wParam) からのbreak
case VK_DOWN:
/* ここに下(↓)キー入力後の処理を書く*/
break; // これは switch (wParam) からのbreak
case VK_RIGHT:
/* ここに右(→)キー入力後の処理を書く*/
break; // これは switch (wParam) からのbreak
case VK_LEFT:
/* ここに左(←)キー入力後の処理を書く*/
break; // これは switch (wParam) からのbreak
} // これは switch (wParam) の終わりのカッコ
break; // これは WM_KEYDOWN: からのbreak
case WM_DESTROY:
PostQuitMessage(0);
break;
Visual Studio はけっして case WM_KEYDOWN: 節を自動生成してはくれないので、プログラマーがコピーペーストとかで case WM_KEYDOWN: 節を書き足す必要がある、
モードごとによる処理内容の違いの実装
[編集]どんなゲームでも、オープニング画面があるだろうし、ゲーム本編の画面もあるでしょう。また、多くのゲームに、ゲームオーバーの画面があるでしょう。
そして、そういった画面の種類ごとに、キー受け付け後の操作内容が違うのが普通です。
たとえば、ゲームオーバー画面で決定ボタン(たとえばzボタン)を押したら、オープニング画面に戻る機能がよくあると思います。
いっぽうで、オープニング画面でもし決定ボタンを押したあとにオープニング画面に戻ってしまったら、もはやゲームを開始できません。
このように、画面のモードの種類によって、キー受け付け後のプログラムの処理内容は異なります。
このようなモードの違いによるプログラム処理の違いを実装するには、下記のようにif文を使うとラクです。
あらかじめ、ゲームのモードを定義する変数をグローバル変数として用意おきましょう。
case WM_KEYDOWN:
if (game_mode == opening_mode) {
switch (wParam) {
case z:
/* ここにzキー入力後の処理を書く*/
game_mode = play_mode; // 一例
break; // これは switch (wParam) からのbreak
case VK_UP: // 「上キーが入力されたら」という意味
/* ここに上(↑)キー入力後の処理を書く*/
break; // これは switch (wParam) からのbreak
case VK_DOWN:
/* ここに下(↓)キー入力後の処理を書く*/
break; // これは switch (wParam) からのbreak
} // これは switch (wParam) の終わりのカッコ
}
if (game_mode == gameover_mode) {
switch (wParam) {
case 'Z': // 「zキーが入力されたら」という意味
game_mode = opening_mode;
break; // これは switch (wParam) からのbreak
} // これは switch (wParam) の終わりのカッコ
}
InvalidateRect(hWnd, NULL, FALSE);
UpdateWindow(hWnd);
break; // これは WM_KEYDOWN: からのbreak
上記のように、それぞれのモードごとに switch (wParam) {
を設置するのがラクです。(もしかしたら1つでも出来るかもしれないが、わざわざそうする必要が無い)
多くのゲームでは、キーを入力したあとに、画面を更新させたいのが普通でしょう。
画面を更新するには
InvalidateRect(hWnd, NULL, FALSE);
UpdateWindow(hWnd);
を書きます。このコードが実行されると、
case WM_PAINT:
の内容に従って画面が更新されます。
InvalidateRect(hWnd, NULL, FALSE);
の第3引数が FALSE
だと、呼び出し後は画面の上書きになります。
いっぽう、第3引数が TRUE TRUE
だと、つまり InvalidateRect(hWnd, NULL, TRUE);
だと、呼び出し時に画面をいったんクリアしてからの描画になります。
ゲームのモードごとに画面内容が違うのが普通のゲームでしょうから、case WM_PAINT:
節でもモードごとのそれぞれの処理をif文を使ってコードを書くことになります。
なので、たいていのゲームでは、きっと case WM_PAINT:
節のコードの構成は
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
if (game_mode == opening_mode) {
// ここにオープニング画面での描画内容を書く
}
if (game_mode == gameover_mode) {
// ここにゲームオーバー画面での描画内容を書く
}
EndPaint(hWnd, &ps);
}
break;
のような構成になるでしょう。
- まとめ
case WM_PAINT:
と case WM_KEYDOWN:
はおおむね、下記のような構成になるでしょう。
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
if (game_mode == opening_mode) {
// ここにオープニング画面での描画内容を書く
}
if (game_mode == gameover_mode) {
// ここにゲームオーバー画面での描画内容を書く
}
EndPaint(hWnd, &ps);
}
break;
case WM_KEYDOWN:
if (game_mode == opening_mode) {
switch (wParam) {
case VK_UP: // 「上キーが入力されたら」という意味
/* ここに上(↑)キー入力後の処理を書く*/
break; // これは VK_UP: からのbreak
case VK_DOWN:
/* ここに下(↓)キー入力後の処理を書く*/
break; // これは VK_DOWN: からのbreak
case VK_RIGHT:
/* ここに右(→)キー入力後の処理を書く*/
break; // これは VK_RIGHT からのbreak
case VK_LEFT:
/* ここに左(←)キー入力後の処理を書く*/
break; // これは VK_LEFT: からのbreak
} // これは switch (wParam) の終わりのカッコ
}
if (game_mode == gameover_mode) {
switch (wParam) {
case 'Z': // 「zキーが入力されたら」という意味
game_mode = opening_mode;
break;
} // これは switch (wParam) の終わりのカッコ
}
InvalidateRect(hWnd, NULL, FALSE);
UpdateWindow(hWnd);
break; // これは WM_KEYDOWN: からのbreak
アナタの自作したいゲームの内容に応じて、さらにモードを追加するなどして、プログラムを追加していくことになります。
その他の方法(非推奨)
[編集]なお、下記のようにC言語に入力受け付けの機能 getchar もありますが、しかし画面がいったん止まってしまうので使えませんし、そもそもコンソール画面で呼び出す関数なので、GUIウィンドウ用アプリから下記の関数を呼び出すのは困難です。
C言語の関数を利用した例
[編集]ゲームプログラミングとしてはまったく非実用的ですが、非常に簡単な入力の機能の実装は、C言語のgetchar関数などを利用して処理する事が出来ます。例えば、次の例は入力した文字をそのまま表示します。
- 注意
- このプログラムは2.6系列のLinux上で実行できることを確認していますが、他のプラットフォームでは異なった振舞いをするかもしれません。
#include <stdio.h>
int main(){
int c;
c = getchar();
printf ("あなたは%cと入力しました。\n", c);
return 0;
}
このプログラムの実行結果は次の様になります。これはgetcharの時点で'g'と入力した場合です。
$./a.out g あなたはgと入力しました。
この方法は簡単ですが欠点が大きいです。まず、getchar関数が実行された時点でこのプログラムは一旦停止して利用者が入力を行うのを待ちます。この性質は、例えばアクションゲームでは利用者の入力を待つためにゲームの動きが止まってしまう事に対応しており(実はスレッドを使って回避する方法もあるが)、不便です。
また、上の出力からは分からないのですが、上の例ではgを入力した後にEnterキーを押して入力が終了したことを計算機に知らせています。これは例えばキャラクターを動かす時に常にEnterキーで確定する必要がある事に対応しています。通常のゲームではこのような事は起こらないので、この事も問題です。
UNIX系のシステムでは(Linuxはこれに含まれる)これらを回避するための方法としてtcsetattr関数を使った方法が知られています。この方法を説明する事も出来るのですが、この方法には1つ厄介な問題があります。tcsetattr関数でカノニカルモードを取り止め、c_cc配列のMINとTIMEを0に設定します。この時上の2つの問題は解決され、Enterキーを押すことなく入力が受け付けられる上、プログラムが利用者の入力を待つこともありません。しかし、上の方法では、ShiftキーやCtrlキーなどの特殊なキーが押されたことを読み取る事が出来ません。また、矢印キーを押した時の結果はおそらくシステムごとに異なった結果を与えます。結局この方法では数字キーと英字キー以外の入力についてはうまく扱う事が出来ません。一方計算機上のゲームでは矢印キーを利用する例も多いので、これは問題です(例えば、SuperTux、gnibblesなど)。
実際には上であげたゲームはそれぞれSDL、GTK+というライブラリを使っているので、どのように入力を得ているのかを見るのは困難です。Linuxの場合について結果を述べると、上のライブラリはLinux上では、どちらもX Window Systemを利用して入力を得ています。Linux上のXはShiftキーなどの入力を扱うために、プログラム中でioctlコールを利用してデバイスファイル(ここではttyファイル)の性質を変更しています。実際にはLinux上でのtcsetattr関数もioctlコールの1つとして実装されているため、このioctl命令を使うことでtcsetattr関数を使った場合よりも多くの事ができます[2][3]。
これらのライブラリはそれぞれのプラットフォーム上で全てのキーを利用する手立てを提供しています。大抵のゲームではこれらのライブラリのいずれかを利用しているので、ここではそれらを用いて入力を扱うことにします。個々のライブラリの入力の扱いかたについては対応するドキュメントを参照してください。