先週分の復習

先週分のソースは
http://cid-b9b6c411563637af.skydrive.live.com/self.aspx/%e5%85%ac%e9%96%8b/%e6%97%a2%e4%bf%aeC++/05%e6%9c%8802%e6%97%a5/main.cpp
にアップロードしました。
先週分と言いつつ微妙に違ってますが見た目の動作は同じです。
詳しい説明をコメントとして書き込む手もあるのですが、あんまりコメントが増えるとコードの見通しが悪くなるので補足的説明はこちらにだけ書きます。


main.cppの先頭から読み始めましょう。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <assert.h>		// マクロassert()を使用するために必要。
#include "SDL.h"		// SDLの機能を利用するために必要。

ここはまぁ、いいか。
assert()について知らない人はググる

//
// エラー/デバッグ用出力。すぐにフラッシュされる。
// DEBUG_OUT(("format %d\n", num))
// のように括弧を2重にして使う。
//
#define TRACE_OUT(args)			do { printf args ; fflush(stdout); } while(false)
#if defined(DEBUG) || defined(_DEBUG)
	#define DEBUG_OUT(args)			do { printf("Dbg: "); printf args ; fflush(stdout); } while(false)
#else
	#define DEBUG_OUT(args)
#endif

先週:ERROR_OUT → 今週:TRACE_OUT
DEBUG_OUTはデバッグビルド時のみ有効でTRACE_OUTはリリースビルドでも有効な文字列出力関数としました。
まず、ファイルへの出力を行ったときはフラッシュするまで実際の書き込みは行われません。
openGame()内でstdout(標準出力:printfの出力先)をコンソールではなくファイル出力に設定していることに注意してください。
フラッシュもクローズもせずにプログラムが終了してしまうと、出力内容が書き込まれないままになってしまいます。
そこでこのマクロで出力後すぐにフラッシュが行われるようにするのです。


さて、まず「2重括弧」の理由について。
printf()は可変個引数をとる関数です。つまり引数の数が決まっていません。
なので、

#define	DEBUG_OUT(format)		printf(format)

のように定義すると、 DEBUG_OUT("x=%d\n", x) みたいな使い方はできません。
それならと

#define DEBUG_OUT1(format, arg0)		printf(format, arg0)

としたって、 DEBUG_OUT1("x=%d, y=%d\n", x, y) は不可能。
そこで、今回のようにDEBUG_OUTを定義して

DEBUG_OUT(("format %d\n", num))

のように呼び出し側で括弧を2重にしておくと、マクロの引数argsが ("format %d\n", num) に対応するため、

do { printf args                 ; fflush(stdout); } while(false)
↓
do { printf ("format %d\n", num) ; fflush(stdout); } while(false)

と展開され、好きな数の引数を与えることができるようになります。
こういうマクロのテクはあまり本に載っていません。
まぁ使い方が汚いので教えるべきでないってのももっともなんですが。


またdo-while文で囲っていることを不思議に思う人もいるでしょう。
もしdo-whileで囲っていない定義をして、次のように呼び出したらどうなるでしょうか?

if(cond)
	TRACE_OUT(("ここにきたお"));

展開すると

if(cond)
	printf("ここにきたお");
fflush(stdout);

ブロック無しのif文でコードの見た目に反した動作をするわけです。
さらに、do-whileは最後にセミコロンを要求する制御文です。

do {
	...
} while(cond); // 最後にセミコロン

もしdo-whileではなく、単なるブロック('{'と'}'の囲い)にしていたらどうなるでしょう?

if(cond)
	TRACE_OUT(("支点を板に吊るしてギリギリ太るカレーセット!"));
else
	...

展開すると

if(cond)
	{
		printf("支点を板に吊るしてギリギリ太るカレーセット!");
		fflush();
	}
;     // ←余分なセミコロンでif分がelse無しのまま終わってしまう!
else  // elseが宙に浮く
	...

do-while文を使っていればこういったエラーを回避できます。


DEBUG_OUTのほうは完成品としてビルド(リリースビルド)するときには無効になるのが望ましいマクロです。

#if defined(DEBUG) || defined(_DEBUG)
	#define DEBUG_OUT(args)			do { printf("Dbg: "); printf args ; fflush(stdout); } while(false)
#else
	#define DEBUG_OUT(args)
#endif

デバッグビルドであることを示すため慣習的に定義されるマクロDEBUGまたは_DEBUGがあったときは出力用のコードに置き換わりますが、それらのマクロが定義されていなかったときは空っぽ、つまり何もコードを生成しません。

#define SCREEN_WIDTH				640
#define SCREEN_HEIGHT				480
#define DEFAULT_COLOR_KEY			0x00ff00ff		// カラーキーとして指定する色。

定数はマクロ定義しとくと仕様変更楽でいいよ
ぐらいは初心者向けの本にも書いてあるでしょう。

// このサーフェイスの中身が画面に対応するものと思ってください。
// あちこちから参照するのでためらわずグローバル。
static SDL_Surface* screen;

// ゲーム中で使用する画像リソース。
static SDL_Surface *imageResources[ImageResource_ElementCount];

// 自機の位置
static Sint16 fighterX, fighterY;

先週: static int fighterX, fighterY;
今週: static Sint16 fighterX, fighterY;
SDL_Rect.xがSint16型なのでfighterX, fighterYの型を合わせました。
より小さい型へキャストするときは情報が失われることがあるため注意が必要です。


ちなみに、Sint16は

typedef unsigned short uint16_t;
...
typedef int16_t		Sint16;

としてSDL側で定義されています。
intやshortの大きさ(ビット数)はCPUとコンパイラによって違いがあるため、どの環境でも同じビット数を確保できるようにSDLが違いを吸収しているのです。
Sint16と同じようにSint8, Uint8, Uint16, Sint32, Uint32も定義されています。
一応言っておくと"S"は"Signed"(符号あり)の略で"U"は"Unsigned"(符号なし)の略です。

// 画像リソースの識別番号。
// "ElementCount"を最後に定義しておくと、全部で幾つの画像リソースがあるのかが分かる。
enum ImageResource {
	ImageResource_Fighter,				// 自機の画像
	ImageResource_ElementCount,
};
...
//
// 画像リソースを読み込む。
//
static void loadImageResource(ImageResource id) {
	...

画像リソースの指定に列挙体を使用しています。
列挙体の要素の前に型名を付加するのは単なる趣味です。

//
// メインループを抜けプログラムを終了させる。
// closeGame()は呼ばれた瞬間にプログラムを終了させるが、exitMainLoop()はあくまでメインループを抜けるだけ。
//
static void exitMainLoop() {
	// 終了イベントをキューに追加し、main関数のイベント処理で取り出す。
	SDL_Event e;
	e.type = SDL_QUIT;
	SDL_PushEvent(&e);
}

実は先週からの最大の変更点。なんと関数追加。
先週のソースではEscキーが押されたときどうしていたでしょうか? closeExit()を呼んでいたはずです。
closeExit()は呼ばれた瞬間にプログラムを終了してしまうため、例えば終了前に演出を入れたいとか、或いはメインループを一周しないと初期化が完了せずメモリの解放すら行えないような状況(具体的にどういう状況なのか自分でも説明しづらいですが、今までに経験したような気がするので念のため)に対応できません。
「イベントをキューに追加」と言われても分からないかもしれませんが、ここは下のmain関数の説明へ先送りします。

//
// ゲームの終了処理を行う。
// 内部でexit()を呼ぶためここでアプリケーションは終了する。
// exitCode: exit()に渡す終了コード。
//
static void closeGame(int exitCode) {
	// 全てのリソースを解放。
	releaseAllResources();
	// 描画機能を閉じる。
	closeGraphics();
	// SDL終了。
	SDL_Quit();
	// しゅうりょー
	exit(exitCode);
}

exit()の引数は0またはEXIT_SUCCESSのとき正常終了、EXIT_FAILUREのとき以上終了を意味します。
こんな定数があったことを忘れていたので先週は直接数字を書いていましたが、今週はEXIT_SUCCESS、EXIT_FAILUREに置き換えています。

//
// ゲームの初期化を行う。
// 戻り値: 成功したらtrue、失敗したらfalse。
//
static bool openGame() {
	
	// 標準出力と標準エラー出力をテキストファイルに設定。
	// こうしておくとコマンドラインから起動しなくても出力内容を見れる。
	freopen("stdout.txt", "w", stdout);
	freopen("stderr.txt", "w", stderr);
	...

そもそもstdout、stderrというグローバル変数が標準で定義されていたことを知っていたでしょうか?
しかもそれがファイルであると知っていたでしょうか?
freopen()は文字通り(File RE-OPEN)ファイルを開きなおす関数です。
標準ではどちらもコンソールと関連付けられていますが、freopen()でテキストファイルへの出力へ変更することができるのです。
これもあんまり本には書かれないことの一つかと。

//
// 現在の状態を描画する。
//
static void draw() {
	// 画面を黒でクリア。
	SDL_FillRect(screen, NULL, 0x00000000);
	
	// 自機画像の描画。
	SDL_Rect destination;
	destination.x = fighterX;
	destination.y = fighterY;
	SDL_BlitSurface(imageResources[ImageResource_Fighter], NULL, screen, &destination);
	...

SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);
はdstのメンバ変数xとyしか利用しないことに注意してください。
そういうこともSDLのドキュメントに書いてあります。

int main(int argc, char *argv[]) {
	...
		// イベントの確認。詳細はSDLのドキュメントを見る。
		SDL_Event event;
		while ( SDL_PollEvent(&event) ) {
			switch (event.type) {
				case SDL_MOUSEMOTION:
					break;
				case SDL_MOUSEBUTTONDOWN:
					break;
				case SDL_KEYDOWN:
					break;
				case SDL_QUIT:
					done = true;
					break;
				default:
					break;
			}
		}
	...
}

説明を完全にすっぽかしていたイベント処理です。
アプリケーションが受け取ったイベント(マウスが動いたとか、マウスのボタンが押されたとか、キーが押されたとか)はイベントキューに追加されます。
毎ループごとに発生したイベントをSDL_PollEvent()でキューから取り出し適切に処理していくわけです。
Win32APIも同じ形式なので、使ったことのある人は何をやっているのかすぐに分かるでしょう。
さて、ここでexitMainLoop()の説明に戻ります。
exitMainLoop()では「終了イベント」(SDL_QUIT)をイベントキューに追加していたのでした。
普通はOSから受け取ったイベントがキューに追加されるのですが、自分でキューに追加することも可能です。
ちなみにSDL_QUITは通常ウィンドウを閉じたときなどに発生します。
exitMainLoop()で追加された終了イベントはここで取り出され、done = true となりループを抜けプログラム終了という流れになります。


SDL_PollEvent()から取り出すイベントはSDL_QUITしか利用しないので他は消してしまってもいいでしょう。


復習はこんな感じでいいですかね?
人が何を知っていて何を知らないか考えながら説明するのが苦手なもので何か大事なことを忘れているかもしれません。
質問などあればサークルで顔を合わせたときやここのコメントなどで聞いて下さい。