2012/11/26

コマンドプロンプトアプリケーションにおける終了時処理について

コマンドプロンプトアプリケーションにおける終了方法は、主に2つあります。

1つは、アプリケーションがプロセスを終え、自ら終了する場合。
もう一つは、ユーザが窓を閉じたり強制終了した場合です。

前者は普通、しっかり後処理されるプログラムであれば、正常に終了します。
問題なのは後者です。

プレイヤーが窓を閉じたり強制終了するというのは、割り込み処理であり、
本筋のプログラム実行中に、強制的に、
突然発生するものであるという事を念頭に置かなければなりません。


具体的な話として、
最近流行り(?)のオートセーブ機能付きのゲームを実現したいとします。
ここではゲーム終了時にゲームデータを保存します。

適当ですがこんな感じで書きました。(雰囲気です)
=============================================
int main(int argc, char* argv[])
{
  int ret = 0;

  // 初期化処理
  if(!GameInit()) {
    /* 何らかのエラー処理 */
    ret = -1;
  } else {
    // 実行処理(ゲームループ)
    if(!GameRunLoop()) {
      /* 実行時エラー */
      ret = -2;
    } else { 
      // データ保存処理
      GameSave();
    }
    // 終了時処理
    GameEnd();
  }

  return ret;
}
=============================================
ゲームループを抜けると、
実行中に異常検知さえなければ必ずデータ保存処理が実行されるため、
終了時にはオートセーブされるといった具合の作りです。
このソースだけ見ても特に大きな問題はないと思います。

問題は、ゲームループ中に窓を閉じられた場合です。
というよりこの作りからして、
初期化処理、保存処理、終了時処理は一瞬で終わるとした場合、
プレイヤーが窓を閉じるタイミングは、ほぼ100%ゲームループ中です。
そこで割り込みが発生するとどうなるでしょうか?

結論から言って上記の場合、以下のような悲惨な流れになります。
1.GameInit <= 実施
2.GameRunLoop <= 実施中に強制停止
3.GameSave <= 実施されず(頑張ったゲームデータが死亡)
4.GameEnd <= 実施されず(解放漏れ。最悪何かのハンドルを掴んだまま)

勿論意図するところとしては、1〜4本来全てを実施させたいのですが、
コンソールアプリケーションではこういう事が普通に起こります。

基本的にGUIでも同じことは言えますが、
GUIの場合、そもそもIDEを使用していることがほぼ当たり前で、
そういったIDEが終了処理を記述すべき場所を最初から提供していたりして、
注意不足でない限り、間違いは起こらないようになっています。

CUIはそういったものは用意されておらず、
つまり、最初から自分で意図して処理せねばなりません。


それほど詳細に調べ上げてた訳ではありませんが、
問題の内容をもう少し掘り下げてみます。

コマンドプロンプト(cmd.exe)に限った話をしますと、
まず、「閉じるボタン」ですが、
これは全てのスレッドを強制的に停止する悪魔のようなボタンです。
このボタンが押されると、割り込み要求を掛け、
その時点でプログラムはメインスレッドを停止しています。
割り込みスレッドをにて何らかの処理を挟み込まない限りなにも処理できません。

じゃあその割り込みスレッドに終了処理を挟み込めばいいと思うのが普通ですが、
しかし、簡単にはいきません。
それは、もうひとつの終了方法があるためです。

そのもうひとつの終了方法が「強制終了」です。
Ctrl + Alt + Delでタスクマネージャを起動してから良くやるアレですね。
強制終了を行うと、実は閉じるボタンとは違う動きをします。
そこが大きな問題になります。

強制終了ではメインスレッドに対して、終了要求をかけているだけです。
そこから一定の時間が経ってプログラムが自主的に終了しない場合、
ユーザに終了確認を取ります。ダイアログのアレです。
そこでユーザーが許可して、初めて全スレッドを停止します。

強制終了は閉じるボタンとは違い
タスクマネージャから強制終了をかけた瞬間に割り込み要求が発生します。
しかも、メインスレッドは割り込まれているだけで、停止はしていません。
そこが閉じるボタンとは全く違っていて、
つまり、割り込みスレッドが発生した後、
メインスレッドに割り込んだ位置へとプログラムカウンタを復帰し、
出来る限り処理を続行しようと努めます

その後問題なくプログラムが動き、ユーザーが強制終了を承認しない限り、
閉じるボタンのように本当に途中で強制終了することはありません。


正しい終了時処理前提として、システムの割り込みスレッドを捕捉し、
そこへユーザー処理を挟みこむ必要があるというのは確かですが、
以上のような「閉じるボタン」と「強制終了」の違いを理解していなければなりません


前置きが長くなりましたが、
この割り込みスレッドの中でユーザ定義の処理を挟み込むには、
以下のようにハンドラ関数をWindowsに事前通知しておく必要があります。
=============================================
#include <windows.h>

// 割り込みによるゲーム終了要求フラグ
static bool g_InterruptEndReq = false; 

int main(int argc, char* argv[])
{
  int ret = 0;

  // ハンドラ関数を登録
  SetConsoleCtrlHandler(HandlerRoutine, TRUE);

  // 初期化処理
  if(!GameInit()) {
    /* 何らかのエラー処理 */
    ret = -1;
  } else {
    // 実行処理(ゲームループ)
    // (内部ループ条件でg_InterruptEndReq == false判定)
    if(!GameRunLoop()) {
      /* 実行時エラー */
      ret = -2;
    } else { 
      // データ保存処理
      GameSave();
    }
    // 終了時処理
    GameEnd();
  }

  return ret;
}

// ハンドラ関数
BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)
{
  BOOL ret = FASLE;

  // ウィンドウの終了イベント検出時
  // ※「閉じるボタン」でも、「強制終了」でも同じフラグ
  if(dwCtrlType == CTRL_CLOSE_EVENT) {
    // 割り込みによるゲーム終了フラグを立てる
    g_InterruptEndReq = true;
    ret = TRUE; // ハンドラ関数をの連続呼び出しを終了する
  }
  return ret;
}
=============================================
あくまで雰囲気ソースですが、
このようにすると、Windowsはコマンドプロンプトを閉じようとした場合、
ハンドラ関数へ特定のイベント処理をコールバックします。
そこで終了要求のフラグを設定し、
ゲームを終了しなければならないことを、ゲームループに通知します。

察しのよい方はお気づきかもしれませんが、
先ほどまでの説明の通り行くと、
このプログラムでは「閉じるボタン」を押された際には終了処理が発生しません。
そのままプログラムは落ちていしまいます。
なぜなら「閉じるボタン」からのはハンドラ関数終了時には、
メインスレッドが死んでおり、戻る場所がないからです。

しかし、こうしなければならない理由があります。

「強制終了」の場合、ハンドラ関数が呼び出された時点では、
メインスレッドの終了が確定していないため、
ハンドラ関数終了後、割り込みが発生したプログラムカウンタ位置に戻ります

つまりどういう事かというと、
ハンドラ関数で直接終了処理を行ってしまった場合
その後のメインスレッドの処理では当然復帰した位置から処理を継続しようとするため
終了処理にて破壊されたアドレスやら変数やらを使用することになってしまうのです。

ですから、コマンドプロンプトのゲームに関して言うなら、
ハンドラ関数で投げて良いのは終了要求のみだということになります。


また、上記ソースのコメントにもあるように、
ハンドラ関数では「閉じるボタン」か「強制終了」かを区別せず、
両方ともウィンドウを閉じるというイベントとして通知されてきます
そのために、ウィンドウの終了方法についての判定は出来ません。

ですので、ハンドラ関数で終了処理をしてしまうと、
「閉じるボタン」は良いかもしれませんが、
「強制終了」時は、ほぼ確実にメモリアクセス違反を起こします。


では「閉じるボタン」の処置はどうするか?

たどり着いた答えとしては、
閉じるボタンを無効にする意外に安全な道は無いということです。

あくまでここでつくろうとしているのはゲームですから、
幸い、ゲーム内メニューから終了することや、
ESCキーで終了するような動作はおおよそ一般的に考えて良いと思います。


そういうわけで、
=============================================
// 閉じるボタンを無効化
HMENU hmenu = GetSystemMenu(hConsole, FALSE);
RemoveMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);
=============================================
として、メニューから閉じるボタンを破棄します。


(2016/01/02 コメントを頂き訂正)
なお、コマンドプロンプトウィンドウのハンドル(上記hConsole)を得る為
直接的なAPIは存在しませんので、以下のように取得します。
以下のAPIを呼び出します。
=============================================
HWND APIENTRY GetConsoleWindow(VOID)
=============================================


古い方法では、以下のような関数を定義・利用することで取得可能です。
(MS公式の資料に基づく取得方法なので、
 恐らくは上記のAPIも同様の動きかと思います。)
=============================================
HWND GetConsoleWindowHandle()
{
  HWND hwnd;
  char newTitle[1024]; // ハンドル取得用ウィンドウタイトル
  char oldTitle[1024]; // オリジナルウィンドウタイトル

  // 現在のウィンドウタイトルを取得
  GetConsoleTitle(oldTitle, sizeof oldTitle);

  // ユニークなタイトルに変更し、ウィンドウハンドルを特定
  sprintf(newTitle, "%d/%d\0", GetTickCount(), GetCurrentProcessId());
  SetConsoleTitle(newTitle);
  Sleep(40); // 誤作動防止に必要
  hwnd = FindWindow(NULL, newTitle);

  // タイトルを戻す
  SetConsoleTitle(oldTitle);

  return hwnd;
}
=============================================

これをアプリケーションの初期化時に組み込むことで、
閉じるボタンは無効となります。
つまりはハンドラ関数は強制終了時にのみ対応すれば良く、
閉じるボタンとの動作の違いに悩まされることは無くなります。


長くなりましたが、以上の仕組みによって、
コマンドプロンプトのゲームの終了時処理を※正しく動作させることが出来ます。


(※ただし、FPSを採用していない(キー入力待ちが発生する)ゲームでは、
 キー入力待ちが終わらない限り、この仕組みでは、
 必ずしもゲームを即時、正常終了させることは出来ません。
 これは当然のことで、キー入力待ちの処理を標準関数やAPI側で処理すると、
 自分では終了タイミングを制御できないからです。
 そこで、コンソールウィンドウをアクティブにして、
 そこへ無理矢理SendInputなどをしてみましたが動作しませんでした。
 これについては暇があればもう少し調査したいですが、
 FPS制御を導入すれば、必然的にハンドラ関数で出した終了要求が
 ゲームループで検知されて、終了処理が走るため、
 簡単に解決してしまう話なので特に対策不要と考えています。)

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などと違って、単純に半角と全角を判定できるものではありません。
 使用される文字が限定すればそういう事も出来ますが、
 どちらにしても、そういった制御を行うことで、パフォーマンスが落ちることは避けられませんし、
 非効率であることに変わりはないのです。)
そういうわけで、自由にバッファの内容を上書きできるダブルバッファリングは、
コンソールアプリケーションであっても有効なのです。