برمجة الألعاب المدمجة ESP32: الخطوط ونظام التجانب

صورة




البداية: التجميع ، نظام الإدخال ، العرض.



تابع: محرك ، بطارية ، صوت.



الجزء 7: نص



الآن بعد أن انتهينا من طبقة كود Odroid Go ، يمكننا البدء في بناء اللعبة نفسها.



لنبدأ برسم نص على الشاشة لأن هذا سيكون مقدمة سلسة للعديد من الموضوعات التي ستكون مفيدة في المستقبل.



سيكون هذا الجزء مختلفًا قليلاً عن الأجزاء السابقة نظرًا لوجود القليل جدًا من التعليمات البرمجية التي تعمل على Odroid Go. سيكون معظم الكود مرتبطًا بأداتنا الأولى.



البلاط



في نظام التقديم الخاص بنا ، سوف نستخدم البلاط . سنقسم شاشة 320 × 240 إلى شبكة من المربعات ، كل منها يحتوي على 16 × 16 بكسل. سيؤدي ذلك إلى إنشاء شبكة بعرض 20 قطعة وارتفاع 15 قطعة.



سيتم عرض العناصر الثابتة مثل الخلفيات والنص باستخدام نظام التجانب ، بينما سيتم عرض العناصر الديناميكية مثل النقوش المتحركة بشكل مختلف. هذا يعني أنه لا يمكن وضع الخلفيات والنصوص إلا في مواقع ثابتة ، بينما يمكن وضع النقوش المتحركة في أي مكان على الشاشة.





يمكن أن يحتوي إطار واحد بحجم 320 × 240 ، كما هو موضح أعلاه ، على 300 قطعة. تظهر الخطوط الصفراء الحدود بين المربعات. سيكون لكل بلاطة رمز نسيج أو عنصر خلفية.





تُظهر الصورة المكبرة للبلاط الفردي المكونة 256 بكسل مفصولة بخطوط رمادية.



الخط



عادةً ما يتم استخدام خط TrueType عند عرض الخطوط على أجهزة كمبيوتر سطح المكتب . يتكون الخط من صور رمزية تمثل الأحرف.



لاستخدام خط ، يمكنك تحميله باستخدام مكتبة (مثل FreeType ) وإنشاء أطلس خط يحتوي على إصدارات نقطية لجميع الصور الرسومية ، والتي يتم أخذ عينات منها عند العرض. يحدث هذا عادةً مسبقًا ، وليس في اللعبة نفسها.



في اللعبة ، تخزن ذاكرة وحدة معالجة الرسومات نسيجًا واحدًا بخط نقطي ووصف في الكود يسمح لك بتحديد مكان الصورة الرمزية المطلوبة في النسيج. تتكون عملية عرض النص من تقديم جزء من النسيج مع حرف رسومي إلى رباعي بسيط ثنائي الأبعاد.



ومع ذلك ، فإننا نتبع نهجًا مختلفًا. بدلاً من القتال مع ملفات ومكتبات TTF ، سننشئ خطنا البسيط.



الهدف من نظام الخطوط التقليدي مثل TrueType هو أن يكون قادرًا على تقديم خط بأي حجم أو دقة دون تعديل ملف الخط الأصلي. يتم تحقيق ذلك من خلال وصف الخط بتعبيرات رياضية.



لكننا لسنا بحاجة إلى مثل هذا التنوع ، فنحن نعرف دقة العرض وحجم الخط الذي نحتاجه ، حتى نتمكن من تنقيط خطنا يدويًا.



لهذا قمت بإنشاء خط بسيط من 39 حرفًا. كل رمز يحتل قطعة واحدة مقاس 16x16. أنا لست مصممًا محترفًا ، لكن النتيجة تناسبني تمامًا.





الصورة الأصلية هي 160 × 64 ، ولكن هنا ضاعفت الحجم لتسهيل المشاهدة.



بالطبع ، سيمنعنا هذا من كتابة نص بلغات لا تستخدم الحروف الـ 26 من الأبجدية الإنجليزية.
...

قم بتشفير الصورة الرمزية







بالنظر إلى المثال الخاص بالحرف الرسومي "A" ، يمكننا أن نرى أنه يتكون من ستة عشر سطراً بطول ستة عشر بكسل. في كل سطر ، يتم تشغيل البكسل أو إيقاف تشغيله. يمكننا استخدام هذه الميزة لتشفير حرف رسومي دون الحاجة إلى تحميل الصورة النقطية للخط في الذاكرة بالطريقة التقليدية.



يمكن اعتبار كل بكسل في السطر على أنه بت واحد ، أي أن السطر يحتوي على 16 بت. إذا كان البكسل قيد التشغيل ، فسيتم تشغيل البتة والعكس صحيح. أي أنه يمكن تخزين ترميز لوحة الفريتس على هيئة ستة عشر عددًا صحيحًا من 16 بت.





في هذا المخطط ، يتم ترميز الحرف "A" بالصورة الموضحة أعلاه. تمثل الأرقام الموجودة على اليسار قيمة سلسلة 16 بت.



يتم ترميز الصورة الرمزية الكاملة في 32 بايت (2 بايت لكل سطر × 16 سطرًا). يستغرق ترميز جميع الأحرف البالغ عددها 39 حرفًا 1248 بايت.



هناك طريقة أخرى لحل المشكلة وهي حفظ ملف الصورة على بطاقة Odroid Go SD ، وتحميله في الذاكرة عند التهيئة ، ثم الرجوع إليه عند عرض النص للعثور على الصورة الرمزية التي تريدها.



لكن يجب أن يستخدم ملف الصورة بايت واحد على الأقل لكل بكسل (0x00 أو 0x01) ، لذلك سيكون الحد الأدنى لحجم الصورة (غير مضغوط) 10240 بايت (160 × 64).



بالإضافة إلى توفير الذاكرة ، تسمح لنا طريقتنا بتشفير مصفوفات بايت الخط glyph بشكل تافه مباشرة في الكود المصدر حتى لا نضطر إلى تحميلها من ملف.



أنا متأكد من أن ESP32 يمكنه التعامل مع تحميل صورة في الذاكرة والرجوع إليها في وقت التشغيل ، لكنني أحببت فكرة ترميز المربعات مباشرة في مصفوفات كهذه. إنه مشابه جدًا لكيفية تنفيذه على NES.



أهمية أدوات الكتابة



يجب تنفيذ اللعبة في الوقت الفعلي بمعدل لا يقل عن 30 إطارًا في الثانية. هذا يعني أن كل شيء في اللعبة يجب معالجته في 1/30 من الثانية ، أي حوالي 33 مللي ثانية.



للمساعدة في تحقيق هذا الهدف ، من الأفضل معالجة البيانات مسبقًا كلما أمكن ذلك بحيث يمكن استخدام البيانات في اللعبة دون أي معالجة. كما أنه يوفر الذاكرة ومساحة التخزين.



غالبًا ما يكون هناك نوع من أنواع الموارد التي تأخذ البيانات الأولية التي يتم تصديرها من أداة إنشاء المحتوى وتحويلها إلى نموذج أكثر ملاءمة للعب في اللعبة.



في حالة الخط لدينا ، لدينا مجموعة من الرموز التي تم إنشاؤها في Asepriteوالذي يمكن تصديره كملف صورة مقاس 160 × 64.



بدلاً من تحميل صورة في الذاكرة عند بدء اللعبة ، يمكننا إنشاء أداة لتحويل البيانات إلى نموذج محسّن لمساحة أكبر ووقت تشغيل أكبر موصوف في القسم السابق.



أداة معالجة الخطوط



يتعين علينا تحويل كل من 39 حرفًا رمزيًا للصورة الأصلية إلى مصفوفات بايت تصف حالة وحدات البكسل المكونة لها (كما في المثال مع الحرف "A").



يمكننا وضع مصفوفة من البايتات المعالجة مسبقًا في ملف رأس يتم تجميعه في اللعبة وكتابته على محرك أقراص فلاش. يحتوي ESP32 على ذاكرة فلاش أكبر بكثير من ذاكرة الوصول العشوائي ، لذلك يمكننا الاستفادة من ذلك من خلال تجميع أكبر قدر ممكن من المعلومات في ثنائي اللعبة.



لأول مرة ، يمكننا إجراء حسابات التحويل من البكسل إلى البايت يدويًا وسيكون ذلك ممكنًا تمامًا (وإن كان مملاً). ولكن إذا أردنا إضافة حرف رسومي جديد أو تغيير حرف قديم ، تصبح العملية رتيبة وتستغرق وقتًا طويلاً وعرضة للخطأ.



وهذه فرصة جيدة لإنشاء أداة.



ستقوم الأداة بتحميل ملف صورة ، وإنشاء مصفوفة بايت لكل حرف ، وكتابتها في ملف رأس يمكننا تجميعه في اللعبة. إذا أردنا تغيير الحروف الرسومية للخط (وهو ما قمت به عدة مرات) أو إضافة واحدة جديدة ، فسنقوم ببساطة بإعادة تشغيل الأداة.



تتمثل الخطوة الأولى في تصدير مجموعة الحروف الرسومية من Aseprite بتنسيق يمكن لأداتنا قراءتها بسهولة. نستخدم تنسيق ملف BMP لأنه يحتوي على رأس بسيط ولا يضغط الصورة ويسمح بتشفير الصورة في 1 بايت لكل بكسل.



في Aseprite ، قمت بإنشاء صورة بلوحة مفهرسة ، لذا فإن كل بكسل عبارة عن بايت واحد يمثل فهرس اللوحة الذي يحتوي على ألوان الأسود (الفهرس 0) والأبيض (الفهرس 1) فقط. يحتفظ ملف BMP المُصدَّر بهذا الترميز: البيكسل المعطل به بايت 0x0 ، والبكسل الممكّن به بايت 0x1.



ستتلقى أداتنا خمس معلمات:



  • تم تصدير BMP من Aseprite
  • ملف نصي يصف مخطط الصورة الرمزية
  • المسار إلى ملف الإخراج الذي تم إنشاؤه
  • عرض كل حرف رسومي
  • ارتفاع كل حرف رسومي


ملف وصف مخطط الصورة الرمزية مطلوب لتعيين المعلومات المرئية للصورة إلى الأحرف نفسها في الكود.



يبدو وصف صورة الخط التي تم تصديرها على النحو التالي:



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 على رأس يصف محتويات الملف. على وجه الخصوص ، يعد عرض الصورة وارتفاعها مهمين بالنسبة لنا ، بالإضافة إلى الإزاحة في الملف حيث تبدأ بيانات الصورة.



سننشئ هيكلًا يصف مخطط هذا الرأس بحيث يمكن تحميل الرأس ويمكن الوصول إلى القيم التي نريدها بالاسم. على خط حزمة PRAGMA يضمن أن أي بايت الحشو تضاف إلى البنية بحيث عند قراءة رأس من الملف، فإنه مباريات بشكل صحيح.



يعد تنسيق BMP غريبًا بعض الشيء من حيث أن البايتات بعد الإزاحة يمكن أن تختلف اختلافًا كبيرًا اعتمادًا على مواصفات BMP المستخدمة (قامت Microsoft بتحديثها عدة مرات). مع headerSizeنتحقق من إصدار الرأس قيد الاستخدام.



نتحقق من أن أول وحدتي بايت من الرأس تساوي BM ، لأن هذا يعني أنه ملف BMP. بعد ذلك ، نتحقق من أن عمق البت هو 8 لأننا نتوقع أن يكون كل بكسل بايت واحدًا. نتحقق أيضًا من أن الرأس 40 بايت ، لأن هذا يعني أن ملف BMP هو الإصدار الذي نريده.



يتم تحميل بيانات الصورة في imageBuffer بعد استدعاء fseek للانتقال إلى موقع بيانات الصورة المحدد بواسطة الإزاحة .



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);


نقرأ ملف وصف مخطط الصورة الرمزية في مصفوفة من السلاسل التي نحتاجها أدناه.



أولاً ، نحسب عدد الأسطر في الملف لمعرفة مقدار الذاكرة التي يجب تخصيصها للخطوط (مؤشر واحد لكل سطر) ، ثم نقرأ الملف في الذاكرة.



يتم قطع فواصل الأسطر بحيث لا تزيد من طول السطر بالأحرف.



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 تأخذ حرفًا وتعيد فهرس البيانات لهذا الحرف في خريطة الصورة الرمزية (التي سننشئها قريبًا).



تتنقل الأداة بشكل متكرر عبر وصف المخطط الذي تمت قراءته مسبقًا وتقوم بإنشاء بيان تبديل يطابق الحرف بالفهرس. يسمح لك بربط الأحرف الصغيرة والأحرف الكبيرة بنفس القيمة وإنشاء تأكيد إذا حاولت استخدام حرف ليس حرف مخطط رسومي.



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 بت لكل حرف رسومي عن طريق اجتياز وحدات البكسل الخاصة به في الصورة. إذا تم تمكين البكسل ، فإن الكود يكتب إلى موضع البت لهذا البكسل 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 بمصفوفات بايت glyph ، يمكننا البدء في رسمها على الشاشة.



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 بكسل (16 × 16) والتحقق من قيمة كل بت في كل سطر. إذا كان البت في وضع التشغيل ، فسنكتب لون هذا البكسل إلى مخزن الإطار المؤقت. إذا لم يتم تمكينه ، فلن نفعل شيئًا.



عادة لا يستحق الأمر كتابة البيانات في ملف رأس لأنه إذا تم تضمين هذا الرأس في ملفات مصدر متعددة ، فسوف يشكو الرابط من تعريفات متعددة. ولكن لن يتم تضمين font.h إلا في الكود بواسطة ملف text.c ، لذلك لن يسبب مشاكل.


تجريبي



سنختبر عرض النص عن طريق تقديم pangram الشهير The Quick 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 ثلاث مرات حتى تظهر الخطوط على خطوط مختلفة ، ونزيد إحداثي Y لكل منها بحيث يتم رسم كل سطر أسفل الخط السابق. سنقوم أيضًا بتعيين لون مختلف لكل سطر لاختبار الألوان.



في الوقت الحالي ، نحسب طول السلسلة يدويًا ، لكن في المستقبل سنتخلص من هذه المتاعب.





صورة


الروابط





الجزء 8: نظام البلاط



كما ذكرنا في الجزء السابق ، سنقوم بإنشاء خلفيات ألعاب من المربعات. ستكون الكائنات الديناميكية الموجودة أمام الخلفية عبارة عن نقوش متحركة ، والتي سننظر إليها لاحقًا. ومن أمثلة العفاريت الأعداء والرصاص وشخصية اللاعب.



سنضع 16 × 16 بلاطة على شاشة 320 × 240 في شبكة ثابتة 20 × 15. في أي وقت ، سنكون قادرين على عرض ما يصل إلى 300 قطعة على الشاشة.



بلاط العازلة



إلى البلاط مخزن، يجب علينا استخدام المصفوفات ثابتة، لا ذاكرة ديناميكية، حتى لا تقلق malloc و مجانا ، تسرب الذاكرة وذاكرة كافية عند تخصيص عليه (Odroid هو نظام مضمن مع كمية محدودة من الذاكرة).



إذا أردنا تخزين تخطيط المربعات على الشاشة ، ومجموع المربعات 20 × 15 ، فيمكننا استخدام مصفوفة 20 × 15 ، حيث يكون كل عنصر هو فهرس بلاطة في "الخريطة". يحتوي Tilemap على رسومات التجانب نفسها.





في هذا الرسم التخطيطي ، تمثل الأرقام الموجودة في الأعلى إحداثي X للتجانب (في مربعات) ، والأرقام الموجودة على اليسار تمثل الإحداثي Y للبلاط (في مربعات).



في الكود ، يمكن تمثيله على النحو التالي:



uint8_t tileBuffer[15][20];


تكمن مشكلة هذا الحل في أنه إذا أردنا تغيير ما يتم عرضه على الشاشة (عن طريق تغيير محتويات اللوحة) ، فسيرى اللاعب القطعة البديلة.



يمكن حل ذلك عن طريق توسيع منطقة التخزين بحيث يمكنك الكتابة إليها عندما تكون خارج الشاشة ، وعند عرضها ، تبدو مستمرة.





تشير المربعات الرمادية إلى "النافذة" المرئية في مخزن التجانب المؤقت ، والتي يتم عرضها على الشاشة. بينما تعرض الشاشة ما هو موجود في المربعات الرمادية ، يمكن تغيير محتويات جميع المربعات البيضاء بحيث لا يراها اللاعب.



في الكود ، يمكن اعتبار هذا كمصفوفة ضعف الحجم في X.



uint8_t tileBuffer[15][40];


اختيار لوحة



في الوقت الحالي ، سنستخدم لوحة من أربع قيم تدرج الرمادي.



بصيغة RGB888 ، تبدو كما يلي:



  • 0xFFFFFF (قيمة بيضاء / 100٪).
  • 0xABABAB (- / 67% )
  • 0x545454 (- / 33% )
  • 0x000000 ( / 0% )




نتجنب استخدام الألوان في الوقت الحالي لأنني ما زلت أحسن مهاراتي الفنية. باستخدام التدرج الرمادي ، يمكنني التركيز على التباين والشكل دون القلق بشأن نظرية اللون. حتى لوحة الألوان الصغيرة تتطلب ذوقًا فنيًا جيدًا.



إذا كنت تشك في قوة اللون الرمادي ذو 2 بت ، ففكر في Game Boy ، الذي يحتوي على أربعة ألوان فقط في لوح ألوانه. كانت شاشة Game Boy الأولى ملونة باللون الأخضر ، لذلك تم عرض أربع قيم على شكل ظلال من اللون الأخضر ، لكن Game Boy Pocket عرضها على أنها تدرجات رمادية حقيقية.



توضح الصورة أدناه لـ The Legend of Zelda: Link's Awakening مقدار ما يمكنك تحقيقه بأربع قيم فقط إذا كان لديك فنان جيد.





في الوقت الحالي ، ستبدو رسومات المربعات مثل أربعة مربعات بحد بكسل واحد بالخارج وزوايا مقطوعة. سيكون لكل مربع أحد الألوان في لوحة الألوان الخاصة بنا.



يعد اقتطاع الزوايا تغييرًا طفيفًا ، ولكنه يسمح لك بالتمييز بين المربعات الفردية ، وهو أمر مفيد في عرض الشبكة.





أداة لوحة



سنخزن اللوحة بتنسيق ملف JASC Palette ، والذي يسهل قراءته ، ويسهل تحليله باستخدام الأدوات ، ويدعمه Aseprite.



تبدو اللوحة هكذا



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


تم العثور على أول سطرين في كل ملف PAL. السطر الثالث هو عدد العناصر في اللوحة. باقي الخطوط هي قيم العناصر الحمراء والخضراء والزرقاء للوحة.



تقوم أداة اللوحة بقراءة الملف ، وتحويل كل لون إلى RGB565 ، وتعكس ترتيب البايت ، وتكتب القيم الجديدة إلى ملف رأس يحتوي على اللوحة في مصفوفة.



الكود الخاص بقراءة الملف وكتابته يشبه الكود المستخدم في المقال السابع ، وتتم معالجة الألوان على النحو التالي:



// 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 انشقاقات السلسلة وفقا لمحددات. يتم فصل قيم الألوان الثلاثة بمسافة واحدة ، لذلك نستخدمها. نقوم بعد ذلك بإنشاء قيمة RGB565 عن طريق تحويل البتات وعكس ترتيب البايت ، كما فعلنا في الجزء الثالث من المقالة.



./palette_processor grey.pal grey.h


يبدو إخراج الأداة كما يلي:



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


أداة معالجة البلاط



نحتاج أيضًا إلى أداة تُخرج بيانات المربعات بالتنسيق الذي تتوقعه اللعبة. قيمة كل بكسل في ملف BMP هي فهرس لوح. سنحتفظ بهذا الترميز غير المباشر بحيث تشغل بلاطة 16 × 16 (256) بايت بايت واحدًا لكل بكسل. أثناء تنفيذ البرنامج ، سنجد لون البلاط في اللوحة.



تقرأ الأداة الملف ، وتعبر وحدات البكسل ، وتكتب فهارسها إلى مصفوفة في الرأس.



يشبه رمز قراءة الملف وكتابته أيضًا الكود الموجود في أداة معالجة الخط ، ويتم إنشاء المصفوفة المقابلة هنا:



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 ثم كتابته إلى الملف كعنصر صفيف 16 × 16.



./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 تعني الأبيض.



نافذة الإطار



كمثال ، يمكننا إنشاء "مستوى" بسيط (وقصير للغاية) يملأ المخزن المؤقت للبلاط بالكامل. لدينا أربعة مربعات مختلفة ، ولا داعي للقلق بشأن الرسومات ، فنحن نستخدم مخططًا حيث يكون لكل من المربعات الأربعة لون مختلف في التدرج الرمادي.





نرتب أربعة بلاطات في شبكة بمستوى 40x15 لاختبار نظامنا.





تشير الأرقام أعلاه إلى فهارس أعمدة المخزن المؤقت للإطار. الأرقام أدناه هي فهارس أعمدة نافذة الإطار. الأرقام الموجودة على اليسار هي خطوط كل مخزن مؤقت (لا توجد حركة نافذة عمودية).





بالنسبة للاعب ، سيبدو كل شيء كما هو موضح في الفيديو أعلاه. عندما يتم نقل النافذة إلى اليمين ، يظهر للاعب أن الخلفية قد تحولت إلى اليسار.



تجريبي





الرقم الموجود في الزاوية اليسرى العلوية هو رقم عمود الحافة اليسرى من نافذة المخزن المؤقت للبلاط ، والرقم الموجود في الزاوية اليمنى العليا هو رقم عمود الحافة اليمنى من نافذة المخزن المؤقت للبلاط.



مصدر



الكود المصدري للمشروع بأكمله موجود هنا .



All Articles