ファイル分割

次は画面上に文字を描画できるようにしようと思うのですが、その前に描画関連の機能を別ファイルに切り出します。
ファイルを分割しておくとそれぞれのソースファイル内で関数や変数の参照が閉じるため、いわゆる「モジュール化」「カプセル化」を行うことができます。
おそらく今後も描画関連機能は拡張されていきますから、早めに分割してしまいましょう。


C++でのファイル分割について注意しなければならないのはヘッダファイルの作成です。
知っての通り、関数・構造体やクラスを利用するためにはそれを定義したヘッダファイルを読み込む必要がありますので、分割したらそれに対応するヘッダファイルを作ります。


ところで、HeaderA.hがHeaderB.hをインクルードしていて、Source.cppがHeaderA.hとHeaderB.hをどちらも直接インクルードしていたらどうなるでしょうか。

Source.cpp →HeaderA.h HeaderB.h
HeaderB.h

HeaderB.hが2重に読み込まれます。
C/C++コンパイラは賢くないので、同名の関数や構造体を定義しようとしたとして2回目のHeaderB.hのインクルードでコンパイルエラーとなるわけです。
これを避けるために次のような定型文がよく使われるので、今回もこれに習いましょう。

#ifndef HEADERB_H      // マクロHEADERB_Hが定義されていなかったら
#define HEADERB_H      // マクロHEADERB_Hを定義する

(HeaderB.hの中身)

#endif // HEADERB_H    // どこで始まったifの終わりなのか書いておくと分かりやすい

最初の読み込みではHEADERB_Hが定義されていないので中身が有効になりますが、2度目の読み込みでは既にHEADERB_Hが定義されているので1行目ではじかれるわけですね。
念のため言っておくと定義するマクロはHEADERB_Hでなければならないなんてことはありません。
HOGEHOGEでも何でもいいのですが単にファイルごとに分かりやすい名前をってだけです。


では新規に「graphics.cpp」と「graphics.h」、さらに「main.h」をプロジェクトに追加してください。
main.cppも外部に公開するマクロや関数はヘッダファイルに書く必要がありますので。
main.hはどのソースファイルからも読み込むものとし、プログラムの中心機能と全体で必要とされる雑多なものを担当させます。


最初にさっきの2重読込防止マクロを書き込んでください。

#ifndef MAIN_H
#define MAIN_H


#endif // MAIN_H
#ifndef GRAPHICS_H
#define GRAPHiCS_H


#endif // GRAPHICS_H


ではmain.cppのうちさしあたって外部からも参照しなければならないものをmain.hとgraphics.hへ、さらに描画関連機能をgraphics.cへ移転します。

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

いちいち読み込むのが面倒なので、これらはmain.hに書いてしまいます。
代わりにmain.cppでmain.hをインクルード。

//
// エラー/デバッグ用出力。すぐにフラッシュされる。
// 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

全体で使いたいのでmain.hへ。

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

描画関連機能なのでgraphics.hへ。

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

画像なので描画関連と言えなくもないのですが、むしろリソース管理として分類しmain.hへ。

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

描画関連機能(というか変数)なのでgraphics.cppへ。graphics.hじゃないですよ。

//
// グラフィックス機能の終了処理を行う。
// 今のところ中身はない。openがあるのにcloseが無いのは気持ちが悪かったので作っただけ。
//
void closeGraphics() { }

中身空っぽですけど、体裁的にgraphics.cppへ。
main.cppから呼ぶことになるのでgraphics.hにプロトタイプ宣言を追加します。

void closeGraphics();

ただし注意。
ここまで説明していませんでしたが、関数をstaticで定義するとその関数を定義したソースファイル以外からは参照できなくなります。
staticで修飾した関数ならば一つの実行ファイル中に幾つも同じ名前で存在することができますが、されていないと全体で1つだけです。
closeGraphicsはgraphics.cppで定義されmain.cppで呼び出す関数なので、staticの修飾は除去します。

//
// グラフィックス機能の初期化を行う。
// 戻り値: 成功したらtrue、失敗したらfalse。
//
bool openGraphics() {
	// 今のところ中身はこれだけ
	screen = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 0, SDL_SWSURFACE);
    if (screen == NULL) {
		TRACE_OUT(("%dx%dの画面モードに設定できませんでした。: %s\n", SCREEN_WIDTH, SCREEN_HEIGHT, SDL_GetError()));
		return false;
	}
	return true;
}

同様にstaticを除去しgraphics.cppへ。プロトタイプ宣言をgraphics.hへ。

//
// 現在の状態を描画する。
//
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_UpdateRect(screen, 0, 0, screen->w, screen->h);
}

移動させるわけではありませんが、さっき変数screenをgraphics.cppへ移動させてしまったため書き換えが必要です。
外部から変数screenを参照しなくても描画できるようにgraphics.cppで関数を定義するのも一つの手です。
例えば

void drawSurface(SDL_Surface* *src, SDL_Rect *srcrect, SDL_Rect *dstrect) {
	SDL_BlitSurface(src, srcrect, screen, dstrect);
}

というように定義することができます。
この方が仕様変更に柔軟だったりするかもしれませんが、練習で製作しているゲームで必要以上に凝る必要もないでしょう。
今回は「extern」を使用してgraphics.cppの変数screenをどのソースファイルからも参照できるようにします。


関数と同じく変数も頭にstaticが付いていると外のソースから参照できないので、取り除きます。
― graphics.cpp ―

SDL_Surface* screen;

さらにgraphics.hにexternでの変数宣言を追加。
― graphics.h ―

extern SDL_Surface* screen;

これでmain.cppでも変数screenが参照できるようになります。


ファイル分割作業はとりあえず完了です。



  • おまけ

― main.h ―

#ifdef INTERNALIZE_GLOBAL             // extern⇔intern だと、やっぱ英語的に変?
    #define   global
#else
    #define   global            extern
#endif

こんなマクロを用意して
― graphics.h ―

global SDL_Surface* screen;

― graphics.c ―

#define INTERNALIZE_GLOBAL
#include "graphics.h"
#undef INTERNALIZE_GLOBAL

とすると、externする数が増えたとき書くのが楽になります。

に行くとこの手のマクロ活用法もちらほら。自分は本で持っていますので部内に読みたい人がいれば貸します。