2012/11/25

コンソール(コマンドプロンプト)でダブルバッファリング


ゲームにおける画面更新で使われるテクニックとして、
ダブルバッファリングは余りにも有名ですが、
それはそもそも窓があって、画像があって、その上で語られることだったりします。

ところで、
コンソールゲームというのは、実験や勉強に向いています。
GUIであるがゆえに起こる、デザインやらの細かな(ゲームプログラミング以外の)話を余り考える必要はなく、
本来したいことに集中できるからで、特にGUIを必要としないアルゴリズムの検討、検証などに便利です。


ただ、少し突っ込んでリアルタイムなゲームをつくろうとか、
そういう事をやろうとすると、最初に必ずげんなりする現象に見舞われます。

つまり、「画面がチラつく」と。

画面を更新する際、本当に簡単な実装をしようとすると、
Windowsでは、system("cls")として画面をクリアしてから、
ゲームオブジェクトなんかをprintfとかcoutとかすることになります。

しかし通常、それらは1つのスクリーンバッファ上で行われるため、
一瞬画面が消えて再描画がされてしまい、
リアルタイムにしようものなら、チラつきが我慢出来ないほどになったりします。


そんなこんなで今回、
そういった不満を解決するべく、
コマンドプロンプトでダブルバッファリングをしようというお話です。

まず前提として、
ここで紹介するのはあくまでもWindowsのコマンドプロンプトの話で、
加えてWin32APIを使用します。そのためWindowsのみの話になるので悪しからず。


さて、
コマンドプロンプトは一つのプロセスに対し、
スクリーンバッファを複数もつことが出来ます。
そしてそれら複数のバッファを、自由なタイミングで切り替えて表示することが出来ます。

具体的に、
「スクリーンバッファを複数もつ」というのは、
============================================================
// スクリーンバッファを作成
HANDLE hSrceen = CreateConsoleScreenBuffer(
    GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL
);
if(hSrceen == INVALID_HANDLE_VALUE) {
 /* エラー処理 */
}
============================================================
などとして必要な分だけHANDLEを確保することで実現されます。


そうして必要な数のスクリーンバッファを新たに作成し、
============================================================
// 対象のスクリーンバッファをアクティブ化
SetConsoleActiveScreenBuffer(hSrceen);
============================================================
として、指定のスクリーンバッファ(描画したいものを反映したバッファ)を
実際に表示するのスクリーンバッファとして指定します。


その選択されたスクリーンバッファに対して、
============================================================
const char* str = "hogehoge";
WORD cell;

// スクリーンバッファを指定して文字を書き込む
WriteConsole(hSrceen, str, strlen(str), &cell, NULL);
============================================================
として、指定のスクリーンバッファに文字(ゲームオブジェクト)を書き込みます


CreateConsoleScreenBufferで作成したスクリーンバッファは、
プログラム終了時などに後処理として、
============================================================
// スクリーンバッファを解放
CloseHandle(hSrceen);
hSrceen = NULL;
============================================================
などととしておきます。


以上に上げたAPIの他にもまだ、ゲームを作る上で必要なAPIがあります。
例えばタイトルを設定したり、
カーソルの表示を切り替えたり、
フォントの色を変えたり などなど・・・

そういった事に関しては、
MicrosoftのオンラインMSDNのコンソールAPIページが参考になります。
ここを参考にすれば大抵はなんとかなるかと思います。


ここで注意事項ですが、
Win32 APIであるCreateConsoleScreenBuffer()は、
作成されるスクリーンバッファのサイズが既にあるウィンドウの複製サイズとなっており、
コマンドプロンプトのウィンドウの大きさとイコールのサイズです。

一方、
============================================================
// 標準のスクリーンバッファを取得
HANDLE hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE);
if(hConsoleOut == INVALID_HANDLE_VALUE) {
 /* エラー処理 */
}
============================================================
として取得できるスクリーンバッファは、
初期状態では縦のバッファサイズがウィンドウサイズを越えているため、
スクロールバーが付いています。

よって、GetStdHandle()で取得した標準スクリーンバッファと、
CreateConsoleScreenBuffer()で作成したもう1つのスクリーンバッファで、
そのままダブルバッファリングをしようとすると、
例えばFPS制御しているプログラムでは、
スクロールバーが付いている画面と、付いていない画面が交互に表示されてしまい、
目も当てられない事になります。

因みに、GetStdHandle()でハンドルを取得した場合も、
後処理などでCloseHandle()を呼び出して解放する必要があります。


先程のバッファサイズの違いの問題を回避するには、
GetStdHandle()して取得した標準スクリーンバッファのサイズ、
及びをウィンドウサイズを
============================================================
COORD coord = { MAX_WIDTH, MAX_HEIGHT };
SMALL_RECT sr = { 0, 0, (MAX_WIDTH-1), (MAX_HEIGHT-1) };

// バッファサイズ変更
SetConsoleScreenBufferSize(hConsoleOut, coord);
// ウィンドウサイズ変更
SetConsoleWindowInfo(hConsoleOut, TRUE, &sr); 
============================================================
などとするか、
そもそも、GetStdHandle()して取得した標準スクリーンバッファは使用しないことです。


上記のサイズ問題については良いとして、
もうひとつの問題があります。

標準のスクリーンバッファをいじるということは、
後々復帰処理の為に起動直後のバッファの内容を保持する必要が有るという問題です。

その問題から、私は標準のスクリーンバッファを使用しないことを推奨します。

そもそも、コマンドラインから「C:¥>game.exe」などとされた場合、
ゲーム中に標準のスクリーンバッファを使用していると、その内容を上書きしてしまいます。
標準のスクリーンバッファをクリアされるようなことは、
ユーザ兼プレイヤー側としては意図しない現象なのです。

何らかの作業中に、ちょっと寄り道してゲームを起動したら、ゲーム起動前に作業していた内容が全て消えていた、なんて事になるのですから。
そんな事はプレイヤーは想定していないというか、させること事自体おかしいのです。

ですから繰り返しますが、
標準のスクリーンバッファの内容を最初に保持するか、
または標準のスクリーンバッファには手をつけない作り方をしなければなりません。


さて、
ここまでの流れをまとめて、もう少し補足したプログラムとして、
初期に公開していた「カードチェイン」にダブルバッファリング対応したものをソース付きで公開します。
バージョン0.8.0から描画周りのベース処理がかなり改善されています。
ただし、ゲームの内容クラスであるCardChain.cppには一切手を付けていません。
(FPS対応はする必要がないゲームですので、していません。)

また、ダブルバッファリング(描画周り)以外の部分でも、特記すべき事項が2点あり、
それも既に適用していますが、そこはまた別の機会に書きたいと思います。

実行ファイルとソースのセットで、
ソースはVC++ 2010 Expressのプロジェクト付きです。

ゲームルールや操作方法、動作環、ライセンス、免責事項などについては、
前バージョンのカードチェインの説明と同様なので、
そちらをご参照ください。


カードチェイン ver. 0.9.0

▽ Download ▽
https://docs.google.com/open?id=0Bz1zlwZ1RkLWSFdZU3VaVEFiYWc

一括ダウンロード方法が不明な場合は、こちらを御覧ください。



以下、駄文です
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
実はダブルバッファリングをせずとも、「チラつきのない描画」はほぼ可能です。
ようは、「一度すべて消すからチラつく」ということです。
必要な部分を毎回上書きするように書き換えるか、
(※)ゲーム画面全体分の描画すべき情報をあらかじめ集約し、
それをもって一度だけのループで完成された画面の描画を行えば、
そもそもダブルバッファリングは不要です。
(※例えば、シングルバッファだと、
 フィールドマップを描画したのち、その上にキャラクターを描画しようしても、
 printfを同じ場所(セル)に2回適用した時点で時間差によるチラツキが発生します。
 その為、一度メモリ上にバッファを持って、その上でマップとキャラクターを統合すれば、
 そのバッファの内容を一度で描画する事ができます。
 応用して、常にゲーム画面全ての内容をそのメモリ上に構築するようにすれば、
 画面全体を1度のの描画で瞬時に書き換えられるため、チラつきはほぼ起こりません。)

が、それというのはそもそも、
コンソールゲームに完全に依存してしまう作りであり、
しかも、(※)そのゲームとは本来関係ない内容を、ゲーム側のソースに埋め込むことになります。
よって、移植性の乏しい物が出来上がってしまうのです。
また、一望しただけではよくわからないものになっている可能性もあります。
(※ただし、前述した描画用のメモリ管理を別機能として提供すればそのような問題は起こりませんが、
 実際にやろうとすると、カラフルなコンソールゲームである場合に不都合が生まれます。
 メモリ統合の際に文字毎の背景色を記憶するわけですが、
 それはマルチバイトのプロジェクトはともかとして、
 Unicodeプロジェクトを作成する場合、UTF-16』で全角半角の判定をし、
 その文字が1セル分なのか、2セル分なのかを判定し、背景色を適用しなければなりません。
 UTF-16では、S-JISやEUC-JPなどと違って、単純に半角と全角を判定できるものではありません。
 使用される文字が限定すればそういう事も出来ますが、
 どちらにしても、そういった制御を行うことで、パフォーマンスが落ちることは避けられませんし、
 非効率であることに変わりはないのです。)
そういうわけで、自由にバッファの内容を上書きできるダブルバッファリングは、
コンソールアプリケーションであっても有効なのです。


0 件のコメント:

コメントを投稿