ESP32組み込みゲームプログラミング:フォントとタイルシステム

画像




開始:組み立て、入力システム、ディスプレイ。



続く:ドライブ、バッテリー、サウンド。



パート7:テキスト



Odroid Goレイヤーのコードが完成したので、ゲーム自体の構築を開始できます。



画面にテキストを描画することから始めましょう。これは、将来役立ついくつかのトピックをスムーズに紹介するためです。



Odroid Goで実行されるコードが非常に少ないため、この部分は前の部分とは少し異なります。ほとんどのコードは、最初のツールに関連しています。



タイル



レンダリングシステムでは、タイルを使用します320x240の画面を、それぞれが16x16ピクセルを含むタイルのグリッドに分割します。これにより、幅20タイル、高さ15タイルのグリッドが作成されます。



背景やテキストなどの静的要素はタイルシステムを使用してレンダリングされますが、スプライトなどの動的要素は異なる方法でレンダリングされます。つまり、背景とテキストは固定された場所にのみ配置でき、スプライトは画面のどこにでも配置できます。





上に示すように、1つの320x240フレームには、300個のタイルを含めることができます。黄色の線はタイル間の境界を示しています。各タイルには、テクスチャシンボルまたは背景要素があります。





単一のタイルのズーム画像は、灰色の線で区切られた256ピクセルの構成要素を示しています。



フォント



通常、TrueTypeフォントは、デスクトップコンピューターでフォントをレンダリングするときに使用されます。フォントは、文字を表すグリフ構成されています



フォントを使用するには、ライブラリ(FreeTypeなどをロードして、すべてのグリフのビットマップラスターバージョンを含むフォントアトラス作成します。これらのバージョンは、レンダリング時にサンプリングされます。これは通常、ゲーム自体ではなく、事前に発生します。



ゲームでは、GPUメモリは、ラスター化されたフォントとコード内の説明を含む1つのテクスチャを格納します。これにより、目的のグリフがテクスチャのどこにあるかを判別できます。テキストレンダリングプロセスは、テクスチャの一部をグリフで単純な2Dクワッドにレンダリングすることで構成されます。



ただし、別のアプローチを採用しています。TTFファイルやライブラリと戦う代わりに、独自のシンプルなフォントを作成します。



TrueTypeのような従来のフォントシステムのポイントは、元のフォントファイルを変更せずに、任意のサイズまたは解像度でフォントをレンダリングできることです。これは、数式でフォントを記述することによって実現されます。



しかし、そのような汎用性は必要ありません。必要な表示解像度とフォントサイズがわかっているので、独自のフォントを手動でラスター化できます。



このために、私は単純な39文字のフォントを作成しました。各シンボルは1つの16x16タイルを占めます。私はプロのタイプデザイナーではありませんが、結果は私にぴったりです。





元の画像は160x64ですが、ここでは見やすいようにスケールを2倍にしました。



もちろん、これにより、英語のアルファベットの26文字を使用しない言語でテキストを書くことができなくなります。
..。

グリフをエンコードする







「A」グリフの例を見ると、16ピクセルの長さの16行であることがわかります。各行で、ピクセルはオンまたはオフのいずれかです。この機能を使用すると、従来の方法でフォントビットマップをメモリにロードしなくても、グリフをエンコードできます。



ラインの各ピクセルは1ビットと考えることができます。つまり、ラインには16ビットが含まれます。ピクセルがオンの場合、ビットはオンであり、その逆も同様です。つまり、フレットボードエンコーディングは16個の16ビット整数として格納できます。





このスキームでは、文字「A」は上記の画像でエンコードされます。左側の数字は16ビットの文字列値を表しています。



完全なグリフは32バイト(1行あたり2バイトx 16行)でエンコードされます。 39文字すべてをエンコードするには1248バイトかかります。



この問題を解決するもう1つの方法は、画像ファイルをOdroid Go SDカードに保存し、初期化時にメモリにロードしてから、テキストをレンダリングするときに参照して、必要なグリフを見つけることでした。



ただし、画像ファイルはピクセルごとに少なくとも1バイト(0x00または0x01)を使用する必要があるため、最小画像サイズは(非圧縮)10240バイト(160 x 64)になります。



メモリの節約に加えて、この方法では、フォントグリフバイト配列をソースコードに直接エンコードするのが非常に簡単なので、ファイルからロードする必要がありません。



ESP32が画像のメモリへの読み込みと実行時の参照を処理できると確信していますが、タイルをこのような配列に直接エンコードするというアイデアは気に入りました。これは、NESでの実装方法と非常によく似ています。



ツールを書くことの重要性



ゲームは、少なくとも毎秒30フレームの頻度でリアルタイムで実行する必要があります。これは、ゲーム内のすべての処理が1/30秒、つまり約33ミリ秒で実行される必要があることを意味します。



この目標を達成するには、可能な限りデータを前処理して、データを処理せずにゲーム内で使用できるようにするのが最善です。また、メモリとストレージスペースを節約します。



多くの場合、コンテンツ作成ツールからエクスポートされた生データを取得して、ゲームでのプレイにより適した形式に変換する、ある種のリソースパイプラインがあります。



フォントの場合、Asepriteで作成された一連のシンボルがあります160x64の画像ファイルとしてエクスポートできます。



ゲームの開始時にイメージをメモリにロードする代わりに、前のセクションで説明したように、データをよりスペースとランタイムに最適化された形式に変換するツールを作成できます。



フォント処理ツール



元の画像の39個のグリフのそれぞれを、構成ピクセルの状態を表すバイト配列に変換する必要があります(「A」文字の例のように)。



前処理されたバイトの配列を、ゲームにコンパイルされてFlashドライブに書き込まれるヘッダーファイルに入れることができます。 ESP32にはRAMよりもはるかに多くのフラッシュメモリがあるため、できるだけ多くの情報をゲームバイナリにコンパイルすることで、これを利用できます。



初めて、手動でピクセルからバイトへの変換を行うことができ、それはかなり実行可能になります(退屈ですが)。ただし、新しいグリフを追加したり、古いグリフを変更したりする場合、プロセスは単調で時間がかかり、エラーが発生しやすくなります。



そして、これはツールを作成する良い機会です。



このツールは、画像ファイルをロードし、各文字のバイト配列を生成して、ゲームにコンパイルできるヘッダーファイルに書き込みます。フォントのグリフを変更したい場合(私は何度も行っています)、または新しいものを追加したい場合は、ツールを再実行するだけです。



最初のステップは、ツールが簡単に読み取れる形式でAsepriteからグリフセットをエクスポートすることです。BMPファイル形式を使用するは、ヘッダーが単純で、画像を圧縮せず、画像を1ピクセルあたり1バイトでエンコードできるためです。



Asepriteでは、インデックス付きパレットを使用して画像を作成したため、各ピクセルは、黒(インデックス0)と白(インデックス1)の色のみを含むパレットのインデックスを表す1バイトです。エクスポートされたBMPファイルはこのエンコーディングを保持します。無効なピクセルのバイト数は0x0で、有効なピクセルのバイト数は0x1です。



私たちのツールは5つのパラメータを受け取ります:



  • AsepriteからエクスポートされたBMP
  • グリフスキームを説明するテキストファイル
  • 生成された出力ファイルへのパス
  • 各グリフの幅
  • 各グリフの高さ


グリフスキーマ記述ファイルは、画像の視覚情報をコード内の文字自体にマッピングするために必要です。



エクスポートされたフォントイメージの説明は次のよう



ABCDEFGHIJ
KLMNOPQRST
UVWXYZ1234
567890:!?


になります。イメージのスキーマと一致する必要があります。



if (argc != 6)
{
	fprintf(stderr, "Usage: %s <input image> <layout file> <output header> <glyph width> <glyph height>\n", argv[0]);

	return 1;
}

const char* inFilename = argv[1];
const char* layoutFilename = argv[2];
const char* outFilename = argv[3];
const int glyphWidth = atoi(argv[4]);
const int glyphHeight = atoi(argv[5]);


最初に行うことは、コマンドライン引数の単純な検証と解析です。



FILE* inFile = fopen(inFilename, "rb");
assert(inFile);

#pragma pack(push,1)
struct BmpHeader
{
	char magic[2];
	uint32_t totalSize;
	uint32_t reserved;
	uint32_t offset;
	uint32_t headerSize;
	int32_t width;
	int32_t height;
	uint16_t planes;
	uint16_t depth;
	uint32_t compression;
	uint32_t imageSize;
	int32_t horizontalResolution;
	int32_t verticalResolution;
	uint32_t paletteColorCount;
	uint32_t importantColorCount;
} bmpHeader;
#pragma pack(pop)

// Read the BMP header so we know where the image data is located
fread(&bmpHeader, 1, sizeof(bmpHeader), inFile);
assert(bmpHeader.magic[0] == 'B' && bmpHeader.magic[1] == 'M');
assert(bmpHeader.depth == 8);
assert(bmpHeader.headerSize == 40);

// Go to location in file of image data
fseek(inFile, bmpHeader.offset, SEEK_SET);

// Read in the image data
uint8_t* imageBuffer = malloc(bmpHeader.imageSize);
assert(imageBuffer);
fread(imageBuffer, 1, bmpHeader.imageSize, inFile);

int imageWidth = bmpHeader.width;
int imageHeight = bmpHeader.height;

fclose(inFile);


最初に画像ファイルが読み取られます。



BMPファイル形式には、ファイルの内容を説明するヘッダーがあります。特に、画像の幅と高さ、および画像データが開始するファイル内のオフセットは重要です。



このヘッダーのスキーマを説明する構造を作成して、ヘッダーをロードし、必要な値に名前でアクセスできるようにします。プラグマパックラインヘッダがファイルから読み込まれたとき、それは正確に一致するようにパディングバイトが構造体に追加されていないことを保証します。



BMP形式は、オフセット後のバイトが使用されるBMP仕様に応じて大きく異なる可能性があるという点で少し奇妙です(Microsoftは何度も更新しました)。headerSize使用されているヘッダーのバージョンを確認します。



ヘッダーの最初の2バイトがBMと等しいことを確認します。これは、それがBMPファイルであることを意味するためです。次に、各ピクセルが1バイトであると想定しているため、ビット深度が8であることを確認します。また、ヘッダーが40バイトであることも確認します。これは、BMPファイルが必要なバージョンであることを意味します。fseekが呼び出された



、画像データがimageBufferロードされ、offsetで指定された画像データの場所に移動します



FILE* layoutFile = fopen(layoutFilename, "r");
assert(layoutFile);


// Count the number of lines in the file
int layoutRows = 0;
while (!feof(layoutFile))
{
	char c = fgetc(layoutFile);

	if (c == '\n')
	{
		++layoutRows;
	}
}


// Return file position indicator to start
rewind(layoutFile);


// Allocate enough memory for one string pointer per row
char** glyphLayout = malloc(sizeof(*glyphLayout) * layoutRows);
assert(glyphLayout);


// Read the file into memory
for (int rowIndex = 0; rowIndex < layoutRows; ++rowIndex)
{
	char* line = NULL;
	size_t len = 0;

	getline(&line, &len, layoutFile);


	int newlinePosition = strlen(line) - 1;

	if (line[newlinePosition] == '\n')
	{
		line[newlinePosition] = '\0';
	}


	glyphLayout[rowIndex] = line;
}

fclose(layoutFile);


グリフスキーマ記述ファイルを、以下で必要な文字列の配列に読み込みます。



まず、ファイル内の行数をカウントして、行に割り当てる必要のあるメモリの量(1行に1つのポインタ)を確認してから、ファイルをメモリに読み込みます。



改行は、文字単位の行の長さが長くならないように切り捨てられます。



fprintf(outFile, "int GetGlyphIndex(char c)\n");
fprintf(outFile, "{\n");
fprintf(outFile, "	switch (c)\n");
fprintf(outFile, "	{\n");

int glyphCount = 0;
for (int row = 0; row < layoutRows; ++row)
{
	int glyphsInRow = strlen(glyphLayout[row]);

	for (int glyph = 0; glyph < glyphsInRow; ++glyph)
	{
		char c = glyphLayout[row][glyph];

		fprintf(outFile, "		");

		if (isalpha(c))
		{
			fprintf(outFile, "case '%c': ", tolower(c));
		}

		fprintf(outFile, "case '%c': { return %d; break; }\n", c, glyphCount);

		++glyphCount;
	}
}

fprintf(outFile, "		default: { assert(NULL); break; }\n");
fprintf(outFile, "	}\n");
fprintf(outFile, "}\n\n");


GetGlyphIndexという 関数を生成します。この関数は、文字を受け取り、その文字のデータインデックスをグリフマップに返します(これはまもなく生成されます)。



このツールは、以前に読み取ったスキーマの説明を繰り返しウォークスルーし、文字をインデックスに一致させるswitchステートメントを生成します。小文字と大文字を同じ値にバインドし、グリフマップ文字ではない文字を使用しようとするとアサートを生成できます。



fprintf(outFile, "static const uint16_t glyphMap[%d][%d] =\n", glyphCount, glyphHeight);
fprintf(outFile, "{\n");

for (int y = 0; y < layoutRows; ++y)
{
	int glyphsInRow = strlen(glyphLayout[y]);

	for (int x = 0; x < glyphsInRow; ++x)
	{
		char c = glyphLayout[y][x];

		fprintf(outFile, "	// %c\n", c);
		fprintf(outFile, "	{\n");
		fprintf(outFile, "	");

		int count = 0;

		for (int row = y * glyphHeight; row < (y + 1) * glyphHeight; ++row)
		{
			uint16_t val = 0;

			for (int col = x * glyphWidth; col < (x + 1) * glyphWidth; ++col)
			{
				// BMP is laid out bottom-to-top, but we want top-to-bottom (0-indexed)
				int y = imageHeight - row - 1;

				uint8_t pixel = imageBuffer[y * imageWidth + col];

				int bitPosition = 15 - (col % glyphWidth);
				val |= (pixel << bitPosition);
			}

			fprintf(outFile, "0x%04X,", val);
			++count;

			// Put a newline after four values to keep it orderly
			if ((count % 4) == 0)
			{
				fprintf(outFile, "\n");
				fprintf(outFile, "	");
				count = 0;
			}
		}

		fprintf(outFile, "},\n\n");
	}
}

fprintf(outFile, "};\n");


最後に、グリフごとに16ビットの値を自分で生成します。



説明から文字を上から下、左から右にトラバースし、画像内のピクセルをトラバースすることで、グリフごとに16個の16ビット値を作成します。ピクセルが有効になっている場合、コードはこのピクセル1のビット位置に書き込みます。それ以外の場合は-0です。



残念ながら、このツールのコードはfprintfの呼び出しが多いためかなり醜いですが、そこで起こっていることの意味が明確であることを願っています。



次に、ツールを実行して、エクスポートされたフォントイメージファイルを処理できます。



./font_processor font.bmp font.txt font.h 16 16


そして、次の(短縮された)ファイルを生成します。



static const int GLYPH_WIDTH = 16;
static const int GLYPH_HEIGHT = 16;


int GetGlyphIndex(char c)
{
	switch (c)
	{
		case 'a': case 'A': { return 0; break; }
		case 'b': case 'B': { return 1; break; }
		case 'c': case 'C': { return 2; break; }

		[...]

		case '1': { return 26; break; }
		case '2': { return 27; break; }
		case '3': { return 28; break; }

		[...]

		case ':': { return 36; break; }
		case '!': { return 37; break; }
		case '?': { return 38; break; }
		default: { assert(NULL); break; }
	}
}

static const uint16_t glyphMap[39][16] =
{
	// A
	{
	0x0000,0x7FFE,0x7FFE,0x7FFE,
	0x781E,0x781E,0x781E,0x7FFE,
	0x7FFE,0x7FFE,0x781E,0x781E,
	0x781E,0x781E,0x781E,0x0000,
	},

	// B
	{
	0x0000,0x7FFC,0x7FFE,0x7FFE,
	0x780E,0x780E,0x7FFE,0x7FFE,
	0x7FFC,0x780C,0x780E,0x780E,
	0x7FFE,0x7FFE,0x7FFC,0x0000,
	},

	// C
	{
	0x0000,0x7FFE,0x7FFE,0x7FFE,
	0x7800,0x7800,0x7800,0x7800,
	0x7800,0x7800,0x7800,0x7800,
	0x7FFE,0x7FFE,0x7FFE,0x0000,
	},


	[...]


	// 1
	{
	0x0000,0x01E0,0x01E0,0x01E0,
	0x01E0,0x01E0,0x01E0,0x01E0,
	0x01E0,0x01E0,0x01E0,0x01E0,
	0x01E0,0x01E0,0x01E0,0x0000,
	},

	// 2
	{
	0x0000,0x7FFE,0x7FFE,0x7FFE,
	0x001E,0x001E,0x7FFE,0x7FFE,
	0x7FFE,0x7800,0x7800,0x7800,
	0x7FFE,0x7FFE,0x7FFE,0x0000,
	},

	// 3
	{
	0x0000,0x7FFE,0x7FFE,0x7FFE,
	0x001E,0x001E,0x3FFE,0x3FFE,
	0x3FFE,0x001E,0x001E,0x001E,
	0x7FFE,0x7FFE,0x7FFE,0x0000,
	},


	[...]


	// :
	{
	0x0000,0x0000,0x3C00,0x3C00,
	0x3C00,0x3C00,0x0000,0x0000,
	0x0000,0x0000,0x3C00,0x3C00,
	0x3C00,0x3C00,0x0000,0x0000,
	},

	// !
	{
	0x0000,0x3C00,0x3C00,0x3C00,
	0x3C00,0x3C00,0x3C00,0x3C00,
	0x3C00,0x3C00,0x0000,0x0000,
	0x3C00,0x3C00,0x3C00,0x0000,
	},

	// ?
	{
	0x0000,0x7FFE,0x7FFE,0x7FFE,
	0x781E,0x781E,0x79FE,0x79FE,
	0x01E0,0x01E0,0x0000,0x0000,
	0x01E0,0x01E0,0x01E0,0x0000,
	},
};


, switch , GetGlyphIndex O(1), , , 39 if.



, . - .



, .



-, char c int, .




font.h ファイルにグリフのバイト配列を入力することで、それらを画面に描画し始めることができます。



static const int MAX_GLYPHS_PER_ROW = LCD_WIDTH / GLYPH_WIDTH;
static const int MAX_GLYPHS_PER_COL = LCD_HEIGHT / GLYPH_HEIGHT;

void DrawText(uint16_t* framebuffer, char* string, int length, int x, int y, uint16_t color)
{
	assert(x + length < MAX_GLYPHS_PER_ROW);
	assert(y < MAX_GLYPHS_PER_COL);

	for (int charIndex = 0; charIndex < length; ++charIndex)
	{
		char c = string[charIndex];

		if (c == ' ')
		{
			continue;
		}

		int xStart = GLYPH_WIDTH * (x + charIndex);
		int yStart = GLYPH_HEIGHT * y;

		for (int row = 0; row < GLYPH_HEIGHT; ++row)
		{
			for (int col = 0; col < GLYPH_WIDTH; ++col)
			{
				int bitPosition = 1U << (15U - col);
				int glyphIndex = GetGlyphIndex(c);

				uint16_t pixel = glyphMap[glyphIndex][row] & bitPosition;

				if (pixel)
				{
					int screenX = xStart + col;
					int screenY = yStart + row;

					framebuffer[screenY * LCD_WIDTH + screenX] = color;
				}
			}
		}
	}
}


主な負荷をツールに転送したため、テキストレンダリングコード自体は非常に単純になります。



文字列をレンダリングするには、その構成文字をループし、スペースが見つかった場合は文字をスキップします。



スペース以外の文字ごとに、グリフマップでグリフインデックスを取得して、そのバイト配列を取得できるようにします。



グリフのピクセルを確認するには、256個のピクセル(16x16)をループして、各行の各ビットの値を確認します。ビットがオンの場合、そのピクセルの色をフレームバッファーに書き込みます。有効になっていない場合は、何もしません。



ヘッダーが複数のソースファイルに含まれている場合、リンカーは複数の定義について文句を言うため、通常、ヘッダーファイルにデータを書き込むことは価値がありません。ただし、font.htext.cファイルによってのみコードに含まれるため、問題は発生しません。


デモ



フォントでサポートされているすべての文字を使用 する有名なパングラムTheQuick Brown Fox Jumped Over The Lazy Dogをレンダリングして、テキストのレンダリングをテストします



DrawText(gFramebuffer, "The Quick Brown Fox", 19, 0, 5, SWAP_ENDIAN_16(RGB565(0xFF, 0, 0)));
DrawText(gFramebuffer, "Jumped Over The:", 16, 0, 6, SWAP_ENDIAN_16(RGB565(0, 0xFF, 0)));
DrawText(gFramebuffer, "Lazy Dog?!", 10, 0, 7, SWAP_ENDIAN_16(RGB565(0, 0, 0xFF)));


DrawTextを3回 呼び出して、線を異なる線に表示し、それぞれのYタイルをインクリメントして、各線が前の線の下に描画されるようにします。また、ラインごとに異なる色を設定して、色をテストします。



今のところ、文字列の長さを手動で計算していますが、将来的にはこの面倒な作業を取り除く予定です。





画像


リンク





パート8:タイルシステム



前のパートで述べたように、タイルからゲームの背景を作成します。背景の前にある動的オブジェクトはスプライトなります。これについては後で説明します。スプライトの例としては、敵、弾丸、プレイヤーキャラクターがあります。



固定された20x15グリッドの320x240画面に16x16タイルを配置します。いつでも、最大300個のタイルを画面に表示できます。



タイルバッファ



タイルを保存するには、動的メモリではなく静的アレイを使用して、malloc空き、メモリリーク、および割り当て時のメモリ不足を心配しないようにする必要があります(Odroidはメモリ量が限られた組み込みシステムです)。



画面上のタイルのレイアウトを保存する必要があり、タイルの合計が20x15である場合、各要素が「マップ」のタイルインデックスである20x15配列を使用できます。タイルマップには、タイルグラフィック自体が含まれています。





この図では、上の数字はタイルのX座標(タイル単位)を表し、左側の数字はタイルのY座標(タイル単位)を表します。



コードでは、次のように表すことができます。



uint8_t tileBuffer[15][20];


このソリューションの問題は、画面に表示される内容を変更したい場合(タイルの内容を変更することにより)、プレーヤーに交換用のタイルが表示されることです。



これは、バッファ領域を拡張して、画面外にあるときに書き込むことができ、表示されたときに連続して見えるようにすることで解決できます。





灰色の四角は、画面に表示されるタイルバッファに表示される「ウィンドウ」を示します。画面には灰色の四角の内容が表示されますが、すべての白い四角の内容を変更して、プレーヤーに表示されないようにすることができます。



コードでは、これはXの2倍のサイズの配列と考えることができます。



uint8_t tileBuffer[15][40];


パレットの選択



今のところ、4つのグレースケール値のパレットを使用します。



RGB888形式では、次のようになります。



  • 0xFFFFFF(白/ 100%値)。
  • 0xABABAB (- / 67% )
  • 0x545454 (- / 33% )
  • 0x000000 ( / 0% )




私はまだ芸術的なスキルを向上させているので、今のところ色の使用は避けています。グレースケールを使用することで、色の理論を気にせずにコントラストと形状に集中できます。小さな色のパレットでさえ、優れた芸術的な味が必要です。



2ビットのグレースケールカラーの強さについて疑問がある場合は、パレットに4色しかないゲームボーイを考えてみてください。ゲームボーイの最初の画面は緑色に着色されていたため、4つの値は緑色の色合いで表示されていましたが、ゲームボーイポケットでは真のグレースケールで表示されていました。



のTheLegend of Zelda:Link's Awakeningの画像は、優れたアーティストがいる場合に4つの値でどれだけ達成できるかを示しています。





今のところ、タイルのグラフィックは4つの正方形のように見え、外側に1ピクセルの境界線があり、角が切り取られています。各正方形には、パレットの色の1つがあります。



コーナーの切り捨ては小さな変更ですが、個々のタイルを区別できるため、メッシュのレンダリングに役立ちます。





パレットツール



パレットは、読みやすく、ツールで簡単に解析でき、AsepriteでサポートされているJASCパレットファイル形式で保存します。



パレットはこんな感じ



JASC-PAL
0100
4
255 255 255
171 171 171
84 84 84
0 0 0


最初の2行は、すべてのPALファイルにあります。3行目は、パレット内のアイテムの数です。残りの行は、パレットの赤、緑、青の要素の値です。



パレットツールはファイルを読み取り、各色をRGB565に変換し、バイト順序を逆にして、配列にパレットを含むヘッダーファイルに新しい値を書き込みます。



ファイルの読み取りと書き込みのコードは、7番目の記事で使用されているコードと同様であり、色処理は次のように実行されます。



// Each line is of form R G B
for (int i = 0; i < paletteSize; ++i)
{
	getline(&line, &len, inFile);

	char* tok = strtok(line, " ");
	int red = atoi(tok);

	tok = strtok(NULL, " ");
	int green = atoi(tok);

	tok = strtok(NULL, " ");
	int blue = atoi(tok);

	uint16_t rgb565 =
		  ((red >> 3u) << 11u)
		| ((green >> 2u) << 5u)
		| (blue >> 3u);

	uint16_t endianSwap = ((rgb565 & 0xFFu) << 8u) | (rgb565 >> 8u);

	palette[i] = endianSwap;
}


strtok関数は、区切り文字に応じて文字列を分割します。3つの色の値は1つのスペースで区切られているため、それを使用します。次に、記事の3番目の部分で行ったように、ビットをシフトしてバイト順序を逆にすることにより、RGB565値を作成します。



./palette_processor grey.pal grey.h


ツールの出力は次のようになります。



uint16_t palette[4] =
{
	0xFFFF,
	0x55AD,
	0xAA52,
	0x0000,
};


タイル処理ツール



また、ゲームで期待される形式でタイルデータを出力するツールも必要です。BMPファイルの各ピクセルの値はパレットインデックスです。16x16(256)バイトのタイルがピクセルごとに1バイトを占めるように、この間接表記を維持します。プログラムの実行中に、パレットにタイルの色が表示されます。



ツールはファイルを読み取り、ピクセルをトラバースし、それらのインデックスをヘッダーの配列に書き込みます。



ファイルの読み取りと書き込みのコードもフォント処理ツールのコードと同様であり、対応する配列の作成はここで行われます。



for (int row = 0; row < tileHeight; ++row)
{
	for (int col = 0; col < tileWidth; ++col)
	{
		// BMP is laid out bottom-to-top, but we want top-to-bottom (0-indexed)
		int y =  tileHeight - row - 1;

		uint8_t paletteIndex = tileBuffer[y * tileWidth + col];

		fprintf(outFile, "%d,", paletteIndex);
		++count;

		// Put a newline after sixteen values to keep it orderly
		if ((count % 16) == 0)
		{
			fprintf(outFile, "\n");
			fprintf(outFile, "	");

			count = 0;
		}
	}
}


インデックスはBMPファイルのピクセル位置から取得され、16x16配列要素としてファイルに書き込まれます。



./tile_processor black.bmp black.h


黒いタイルを処理するときのツールの出力は次のようになります。



static const uint8_t tile[16][16] =
{
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,3,3,3,3,3,3,3,3,3,3,3,3,0,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,
    0,0,3,3,3,3,3,3,3,3,3,3,3,3,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};


よく見ると、インデックスだけでタイルの見た目がわかります。すべての3つの黒とあらゆる手段0手段白。



フレームウィンドウ



例として、タイルバッファ全体を満たす単純な(そして非常に短い)「レベル」を作成できます。4つの異なるタイルがあり、グラフィックスについて心配する必要はありません。4つのタイルのそれぞれがグレースケールで異なる色を持つスキームを使用します。





システムをテストするために、40x15レベルのグリッドに4つのタイルを配置します。





上記の数値は、フレームバッファの列インデックスを示しています。以下の番号は、フレームウィンドウの列のインデックスです。左側の数字は、各バッファーの行です(ウィンドウの垂直方向の移動はありません)。





プレーヤーにとって、すべてが上のビデオに示されているように見えます。ウィンドウを右に動かすと、背景が左にずれているように見えます。



デモ





左上隅の番号はタイルバッファウィンドウの左端の列番号であり、右上隅の番号はタイルバッファウィンドウの右端の列番号です。



ソース



プロジェクト全体のソースコードはこちらです。



All Articles