index
キー入力と複数画面合成

keyscan.c キー入力に従い、背景画像の上に文字を出力するプログラムの例です。背景画像と、文字表示画像を同時に表示するため、モード0で、2枚の画面を重ね合わせています。文字スクロールの方法は、スクリーン制御の典型的テクニックです。スクロールする文字と、スクロールしない文字を同時に表示するためには、もう一枚画面を追加します。ところで、screen 構造体のx-y座標に値を代入すると、エミュレータ上で正しく動作しないようなので、実機で動作テストしてください。もし、原因が解ったら教えてください。
keyscan.c
/****************************************************************/
/*		Key scan and overlay image plane		*/
/****************************************************************/

#include "gba.h"
#include "sky.c"
#include "FontC64-08.c"

void init_screen(void);
void location(int, int);
void scroll(void);
void print(const char*);
void print_hex8(int);
void print_hex16(int);
sky.c にタイルモードによる背景画像データが入っています。データの内容については、ページの下のほうに説明があります。FontC64-08.c に文字フォントデータが入っています。テキスト表示のページで使用したものと同じです。
// Background palette base address pointer 0x05000000
hword* bcolor = (hword*) BG_PALETTE;

// Screen structures
screen plane[ 2 ] = {
	{ (hword*) VRAM_MAP(28), (hword*) VRAM_TILE(3), 0, 0},
	{ (hword*) VRAM_MAP(30), (hword*) VRAM_TILE(0), 0, 0},
};

int page = 0;				// page number
背景パレットの先頭アドレス 0x05000000は、2 Byte 配列 bcolor にポインタとして設定されています。screen は、gba.h の中で、定義された構造体(マップデータ、タイルデータ、x座標、y座標)です。x, y 座標は、現在画面制御対象となっているタイルの座標を示します。複数画面を重ね合わせる場合に、画面毎に記憶している必要があります。ここでは、背景用と文字表示用の2画面を重ね合わせますので、screen 型の構造体 plane を2個用意しています。2枚の画面のどちらに対して制御を行うのかを指定するため、グローバル変数 page も用意しておきます。plane [ page ] のようにして、制御対象の画面を呼び出します。
int main() {

	int key, prev;

	// Initialize text and background screen
	init_screen();
init_screen() 関数は、背景と文字フォントの初期設定を行います。key は、現在のキーステータスレジスタの値、prev は、一つ前のキーステータスレジスタの値です。使い方は、次に説明します。
	// Key scan loop
	while (1) {
		key = gba_reg(KEY_STA);
		if ((key & KEY_ALL) == KEY_ALL) {
			prev = -1;
			continue;
		}
		else if (key == prev)
			continue;
キーステータスのチェックを繰り返す無限ループです。通常、キー操作は、後述の割り込みで検出するのですが、他にタイミングが重要な処理を行う必要がなければ、この方式で十分です。まず、gba_reg( KEY_STA) マクロで、アドレス 0x0400 0130 のキーステータスレジスタから、値を読み出します。キーステータスレジスタは、負論理で表されていますので、キー入力がなければ、全てのキービットが1のはずですから、key & KEY_ALL(全キーに対するビットマスク)は、KEY_ALL と等しくなります。つまり、キー入力がなければ、prev = -1 を実行して、ループ先頭に戻ります。もし、キーが押されていたら、key と prev を比較して、一致したらループ先頭に戻ります。つまり、前と同じキーが押し続けられていたら、これを無視します。カーソルキーでスクロールするような場合は、この判定は必要ありません。先に、prev = -1 を代入していたのは、-1 という値は、キー入力では発生しない値なので、確実に最初のキー入力を捉えるための手法だということがわかります。
		prev = key;
		key = key ^ KEY_ALL;	// logic polarity
新しくキーが押されていたら(つまり上の判定をパスしたら)、現在のキーステータスを prev に保存して、key と KEY_ALL の EXOR をとります。これは、キーステータスを論理反転することになります。論理回路が得意ではない人は、1111 と 適当な値、例えば、1101 の EXOR を取って考えてみてください。
		// key code display on plane
		location(2, plane[ page ].y);
		print("Key status : ");
		print_hex16(key);
		print("\n");
	};
}
location 関数は、制御対象となるタイルの座標を指定します。つまり、マップ上の、この座標に文字を打ち出すわけです。print 関数は、文字列を表示する関数です。print_hex16 は、32 bit 整数を入力して、4桁16進数を表示する関数です。PC等のプログラミングでは、printf のような何でも表示できる便利な関数がライブラリの中に含まれていますが、こういった汎用的関数は、無駄な機能が多く含まれているので、非常に多くのメモリを食ってしまうため、ゲーム機や組み込みシステムでは使用しません。必要な機能だけを持った関数を自分で作る必要があるわけです。ここでも、print, print_hex16 のような単機能の表示関数を作成します。しかし、printf は、あればやはり便利です。
// Screen Initialization
void init_screen(void) {

	int i, row, col, bit, val;

	// Initialize palettes
	for (i = 0; i < 256; i++)
    	bcolor[ i ] = sky_pal[ i ];
背景画像のタイルで使用する256色のパレットデータ配列 sky_pal[] を、0x05000000から始まる背景パレット領域に書き込んでいます。上の方のグローバル変数の宣言を確認してください。
	// Setup tiles of font set
	for (i = 0; i < 96; i++) {
		for (row = 0; row < 8; row++) {
			for (col = 7; col >= 0; col--) {
				bit = (Font[ i ][ row ] & (1 << col)) ? 119 : 0;
				if (col % 2)
					val = bit;
				else
					plane[ 0 ].tile[((i + 32) * 32) + \
					(row * 4) + ((7 - col)/2)] = val + (bit << 8);
			}
		}
	}
ここは、テキスト表示のページで説明している文字フォントデータの読み込みと全く同じです。文字の色は、パレットを確認してもらうるとわかりますが、パレット番号119が黒、パレット番号0が透明色です。画面は、plane[0] を指定しています。
	// Setup tiles for background picture
	for (i = 0; i < (32 * 20 * 32); i++)
		plane[ 1 ].tile[ i ] = sky_data[ 2*i ] + (sky_data[ 2*i + 1 ] << 8);

	// Setup the map for background picture
	for (i = 0; i < 32 * 20; i++)
		plane[ 1 ].map[ i ] = sky_map[ i ];
背景画像のタイルデータ sky_data[] とマップデータ sky_map[] を、構造体 plane[1] に書き込んでいます。plane [0] は、文字表示用です。タイルデータのの読み込みの際、2*i 番目のデータと、2*i +1 番目のデータを 8 bit シフトしたものを足し合わせてから、tile[i] に代入していますが、これは、sky_data[] が、8 bit 単位で表現されているのに対して、tile[] が、hword つまり 16 bit で宣言されているため、sky_data[] を 16 bit 一組にしてから代入しています。
	//Setup background mode and LCD control register
	gba_reg(BG0_CTL)  =	LCD_SIZE00   |
							LCD_COLOR256 |
							LCD_BGTILE(3)|
							LCD_BGMAP(28);
	gba_reg(BG1_CTL)  =	LCD_SIZE00   |
							LCD_COLOR256 |
							LCD_BGTILE(0)|
							LCD_BGMAP(30);
	gba_reg(LCD_CTL)  =	LCD_BG0 |
							LCD_BG1 |
							LCD_MODE0;
}
gba_reg() マクロで、BG0, BG1 の背景コントロールレジスタと、アドレス 0x04000000 のLCDコントロールレジスタを設定しています。BG0 を文字表示用の plane[0]、BG1を背景画像表示用の plane[1] に割り当てています。デフォルトでは、画面 BG0 > BG1> BG2 > BG3 の順で優先度が高いので、この場合は、背景画像の上に文字が現れます。逆にすると、背景の透明色が割り当てられた部分以外では、文字は見えません。どちらの画面も、LCD_SIZE00 即ち 32x32 tile の大きさで、256色表示に設定しています。タイルデータとマップデータのVRAM配置は、下図のようになるはずですので、エミュレータで確認してください。
Memory map
// Present position of tile character
void location(int xx, int yy) {

	if (xx < 0)
		plane[ page ].x = 0;
	else if (xx >= 30)
		plane[ page ].x = 30 - 1;
	else
		plane[ page ].x = xx;

	if (yy < 0)
		plane[ page ].y = 0;
	else if (yy >= 20)
		plane[ page ].y = 20 - 1;
	else
		plane[ page ].y = yy;
}
背景と文字表示画面独立に、制御対象となるタイルの (x, y) 位置を指定します。
// Scroll up
void scroll(void) {

	int i, j;

	// Scroll
	for (i = 1; i < 20; i++) {
		for (j = 0; j < 30; j++)
			plane[ page ].map[ (i - 1) * 32 + j ] = \
			plane[ page ].map[ i * 32 + j ];
		}
画面を一行スクロールアップします。実は、各行のマップデータ配列を、一つ前の行のマップデータ配列に代入しなおしているだけです。
		// End line
	for (j = 0; j < 30; j++)
		plane[ page ].map[ 19 * 32 + j ] = 0x20; // SP in bottom line
}
画面の最終ラインについては、前のデータがないので、 とりあえず 0x20 を代入してあります。0x20 は、ASCII コード表で何に当たるか調べてください。
// Print function
void print(const char* str) {

	const char* pt = str;

	while (*pt) {
		if (*pt == '\n') {
			plane[ page ].x = 0;
			if (plane[ page ].y < (20 - 1))
				plane[ page ].y++;
			else
				scroll();
			pt++;
			continue;
		}
まず、受け取った文字列ポインタ pt の中身が空になるまでループを回します。次に、改行 '\n' をチェックして、改行なら、x = 0 にし、さらに y が18以下なら y 座標をインクリメント、19ならインクリメントするとLCD表示からはみ出るので、scroll 関数を実行します。ここまでが、改行に対する処理です。
		plane[ page ].map[ plane[ page ].y * 32 + plane[ page ].x ] = *pt++;
		if (plane[ page ].x < (30 - 1))
			plane[ page ].x++;
		else {
			plane[ page ].x = 0;
			if (plane[ page ].y < (20 - 1))
				plane[ page ].y++;
			else
				scroll();
		}
	}
}
改行しない場合は、指定された (x, y) 座標に対応する map[] のタイルインデクスとして、文字列ポインタの中身を出力して、ポインタを1文字インクリメントします。文字列ポインタの中には、8 bit の ASCII コードが入っているので、対応する文字フォントが、マップインデクスに割り当てられることになります。次に表示する文字の座標が、LCD画面をはみ出さないかどうかチェックして、ループ先頭に戻ります。
// Print 1Byte HEX
void print_hex8(int val) {
	char buf[ 3 ];

	buf[ 0 ] = "0123456789ABCDEF"[ (val & 0xF0) >> 4 ];
	buf[ 1 ] = "0123456789ABCDEF"[ val & 0x0F ];
	buf[ 2 ] = 0; // Null
	print(buf);
}
8 bit の整数(16bit 変数 val の下位8bitを使用)を 0-Fの16進数2桁表示する関数です。各桁ごとに、数値に対応する文字コードを buf[] に代入して、print 関数に引き渡します。
// Print 2Byte HEX
void print_hex16(int val) {
	print_hex8((val & 0xFF00) >> 8);
	print_hex8(val & 0x00FF);
}
16 bit の整数を 0-Fの16進数4桁表示する関数です。8 bit 単位に分解して、print_hex8 に引き渡します。

sky.c
const unsigned char sky_data[32*20*64] = {
0x76,0x76,0x76,0x76,0x76,0x76,0x76,0x76,
0x76,0x76,0x76,0x76,0x76,0x76,0x76,0x76,
 :
 :
};

const unsigned short sky_pal[256] = {
0x7FFF,0x7FFF,0x7FFF,0x7FFF,0x7FFF,0x7FFF,0x7FFF,0x7FFF,
0x7FFF,0x7FDF,0x7FDF,0x7FDF,0x7FDF,0x7FDF,0x7FDF,0x7FDE,
 :
 :
};

const unsigned short sky_map[640] = {
0x0000,0x0001,0x0002,0x0003,0x0004,0x0005,0x0006,0x0007,
0x0008,0x0009,0x000A,0x000B,0x000C,0x000D,0x000E,0x000F,
 :
 :
};
画像データファイル sky.c の内容。タイルモードでは、パレットデータ配列(sky_pal[256])、タイルデータ配列(sky_data[32*20*64])、マップデータ配列(sky_map[640])の3種類の配列に分けて画像データが表現されます。これにより画像の格納に必要なメモリが節約されます。

まず、フォトレタッチやお絵かきツールを使って、GBAの仮想画面サイズ 256 x 160 pix に変更し(LCDサイズの 240 x 160 ではなく、仮想画面サイズの 256 x 160 にするところがポイントです, 240 x 160 にするとVRAMへのデータの受け渡しが大変面倒になります)、次に、タイルモードの最高色数256色に減色します。bmp2rgb というWindows BMP形式から上記のRGBデータ配列に変換するツールを利用して、BMPからデータ配列に変換すると、上記3種類の配列が出力されます。sky_data[] がタイルデータですが、このツールの出力は、8 bit 単位の配列となっているので、16 bit 幅の VRAM に読み込むときには注意が必要です。

フォントについては、テキスト表示のページを参照してください。


お問い合わせはこちらまで: kitagawa@is.t.kanazawa-u.ac.jp

(c) Kanazawa Univ., 2004