

Start: Montage, Eingabesystem, Anzeige.
Fortsetzung: Laufwerk, Batterie, Ton.
Teil 7: Text
Nachdem wir mit der Odroid Go-Codeebene fertig sind, können wir mit dem Erstellen des Spiels selbst beginnen.
Beginnen wir mit dem Zeichnen von Text auf dem Bildschirm, da dies eine reibungslose Einführung in verschiedene Themen sein wird, die für uns in Zukunft nützlich sein werden.
Dieser Teil unterscheidet sich geringfügig von den vorherigen, da auf Odroid Go nur sehr wenig Code ausgeführt wird. Der Großteil des Codes wird für unser erstes Tool sein.
Fliesen
In unserem Rendering-System werden wir Kacheln verwenden . Wir werden den 320x240-Bildschirm in ein Kachelraster mit jeweils 16x16 Pixel aufteilen. Dadurch wird ein Raster erstellt, das 20 Kacheln breit und 15 Kacheln hoch ist.
Statische Elemente wie Hintergründe und Text werden mithilfe des Kachelsystems gerendert, während dynamische Elemente wie Sprites unterschiedlich gerendert werden. Dies bedeutet, dass Hintergründe und Text nur an festen Stellen platziert werden können, während Sprites an einer beliebigen Stelle auf dem Bildschirm platziert werden können.

Ein 320x240-Rahmen kann, wie oben gezeigt, 300 Kacheln enthalten. Gelbe Linien zeigen die Ränder zwischen den Kacheln. Jede Kachel hat ein Textur-Symbol oder ein Hintergrundelement.

Das gezoomte Bild einer einzelnen Kachel zeigt den Bestandteil 256 Pixel, die durch graue Linien getrennt sind.
Schriftart
In der Regel wird beim Rendern von Schriftarten auf Desktop-Computern eine TrueType- Schriftart verwendet . Die Schriftart besteht aus Glyphen , die Zeichen darstellen.
Um eine Schriftart zu verwenden, laden Sie sie mit einer Bibliothek (z. B. FreeType ) und erstellen einen Schriftartenatlas mit Bitmap-gerasterten Versionen aller Glyphen, die beim Rendern abgetastet werden. Dies geschieht normalerweise im Voraus, nicht im Spiel selbst.
Im Spiel speichert der GPU-Speicher eine Textur mit einer gerasterten Schriftart und einer Beschreibung im Code, mit der Sie bestimmen können, wo sich die gewünschte Glyphe in der Textur befindet. Der Textwiedergabeprozess besteht darin, einen Teil der Textur mit einem Glyphen auf ein einfaches 2D-Quad zu rendern.
Wir verfolgen jedoch einen anderen Ansatz. Anstatt mit TTF-Dateien und -Bibliotheken zu kämpfen, erstellen wir unsere eigene einfache Schriftart.
Der Sinn eines herkömmlichen Schriftsystems wie TrueType besteht darin, eine Schrift in jeder Größe oder Auflösung rendern zu können, ohne die ursprüngliche Schriftdatei zu ändern. Dazu wird die Schriftart mit mathematischen Ausdrücken beschrieben.
Wir brauchen diese Vielseitigkeit jedoch nicht. Wir kennen die Anzeigeauflösung und die Schriftgröße, die wir benötigen, damit wir unsere eigene Schrift manuell rasteren können.
Dafür habe ich eine einfache 39-stellige Schriftart erstellt. Jedes Symbol belegt eine 16x16-Kachel. Ich bin kein professioneller Schriftdesigner, aber das Ergebnis passt perfekt zu mir.

Das Originalbild ist 160 x 64, aber hier habe ich den Maßstab für eine einfache Anzeige verdoppelt.
Dies hindert uns natürlich daran, Text in Sprachen zu schreiben, in denen die 26 Buchstaben des englischen Alphabets nicht verwendet werden....
Codieren Sie die Glyphe

Wenn wir uns das Beispiel für die Glyphe „A“ ansehen, sehen wir, dass es sechzehn Zeilen mit einer Länge von sechzehn Pixeln ist. In jeder Zeile ist ein Pixel entweder ein- oder ausgeschaltet. Mit dieser Funktion können wir eine Glyphe codieren, ohne die Bitmap für Schriftarten auf herkömmliche Weise in den Speicher laden zu müssen.
Jedes Pixel in einer Zeile kann als ein Bit betrachtet werden, dh eine Zeile enthält 16 Bits. Wenn das Pixel eingeschaltet ist, ist das Bit eingeschaltet und umgekehrt. Das heißt, die Griffbrettcodierung kann als 16 16-Bit-Ganzzahlen gespeichert werden.

In diesem Schema wird der Buchstabe "A" mit dem oben gezeigten Bild codiert. Die Zahlen links geben den 16-Bit-Zeichenfolgenwert an.
Die vollständige Glyphe wird in 32 Bytes (2 Bytes pro Zeile x 16 Zeilen) codiert. Es dauert 1248 Bytes, um alle 39 Zeichen zu codieren.
Eine andere Möglichkeit, das Problem zu lösen, bestand darin, die Bilddatei auf der Odroid Go SD-Karte zu speichern, sie bei der Initialisierung in den Speicher zu laden und beim Rendern von Text darauf zu verweisen, um die gewünschte Glyphe zu finden.
Die Bilddatei muss jedoch mindestens ein Byte pro Pixel (0x00 oder 0x01) verwenden, sodass die minimale Bildgröße (unkomprimiert) 10240 Byte (160 x 64) beträgt.
Unsere Methode spart nicht nur Speicher, sondern ermöglicht es uns auch, die Bytearrays der Schriftzeichen-Glyphen ganz einfach direkt in den Quellcode zu codieren, damit wir sie nicht aus der Datei laden müssen.
Ich bin mir ziemlich sicher, dass der ESP32 ein Bild in den Speicher laden und zur Laufzeit referenzieren kann, aber ich mochte die Idee, Kacheln direkt in Arrays wie dieses zu codieren. Es ist sehr ähnlich wie es auf dem NES implementiert ist.
Die Bedeutung von Schreibwerkzeugen
Das Spiel muss in Echtzeit mit einer Frequenz von mindestens 30 Bildern pro Sekunde ausgeführt werden. Dies bedeutet, dass alles im Spiel in 1/30 Sekunde verarbeitet werden sollte, was ungefähr 33 Millisekunden entspricht.
Um dieses Ziel zu erreichen, ist es am besten, die Daten nach Möglichkeit vorzuverarbeiten, damit sie ohne Verarbeitung im Spiel verwendet werden können. Es spart auch Speicher und Speicherplatz.
Oft gibt es eine Art Ressourcen-Pipeline, die die aus dem Tool zur Erstellung von Inhalten exportierten Rohdaten in eine Form umwandelt, die besser für das Spielen im Spiel geeignet ist.
Bei unserer Schriftart haben wir einen Zeichensatz in Aseprite erstelltDies kann als 160x64-Bilddatei exportiert werden.
Anstatt zu Beginn des Spiels ein Bild in den Speicher zu laden, können wir ein Tool erstellen, um die Daten in eine raum- und laufzeitoptimierte Form umzuwandeln, die im vorherigen Abschnitt beschrieben wurde.
Schriftverarbeitungswerkzeug
Wir müssen jedes der 39 Glyphen des Originalbilds in Byte-Arrays konvertieren, die den Zustand ihrer konstituierenden Pixel beschreiben (wie im Beispiel mit dem Zeichen „A“).
Wir können ein Array von vorverarbeiteten Bytes in eine Header-Datei einfügen, die im Spiel kompiliert und auf das Flash-Laufwerk geschrieben wird. ESP32 hat viel mehr Flash-Speicher als RAM, daher können wir dies nutzen, indem wir so viele Informationen wie möglich in die Spiel-Binärdatei kompilieren.
Zum ersten Mal können wir die Pixel-zu-Byte-Konvertierungsberechnungen manuell durchführen, und dies ist durchaus machbar (wenn auch langweilig). Wenn wir jedoch eine neue Glyphe hinzufügen oder eine alte ändern möchten, wird der Prozess eintönig, langwierig und fehleranfällig.
Und dies ist eine gute Gelegenheit, ein Werkzeug zu erstellen.
Das Tool lädt eine Bilddatei, generiert ein Byte-Array für jeden der Charaktere und schreibt sie in eine Header-Datei, die wir in das Spiel kompilieren können. Wenn wir die Glyphen der Schriftart ändern möchten (was ich schon oft getan habe) oder eine neue hinzufügen möchten, führen wir das Tool einfach erneut aus.
Der erste Schritt besteht darin, den Glyphensatz aus Aseprite in ein Format zu exportieren, das unser Tool leicht lesen kann. Wir verwenden das BMP-Dateiformat, da es einen einfachen Header hat, das Bild nicht komprimiert und es ermöglicht, das Bild in 1 Byte pro Pixel zu codieren.
In Aseprite habe ich ein Bild mit einer indizierten Palette erstellt, sodass jedes Pixel ein Byte ist, das den Index der Palette darstellt, der nur die Farben Schwarz (Index 0) und Weiß (Index 1) enthält. Die exportierte BMP-Datei behält diese Codierung bei: Ein deaktiviertes Pixel hat das Byte 0x0 und ein aktiviertes Pixel hat das Byte 0x1.
Unser Tool erhält fünf Parameter:
- BMP aus Aseprite exportiert
- Textdatei, die das Glyphenschema beschreibt
- Pfad zur generierten Ausgabedatei
- Breite jeder Glyphe
- Höhe jeder Glyphe
Die Glyphenschema-Beschreibungsdatei wird benötigt, um die visuellen Informationen des Bildes den Zeichen selbst im Code zuzuordnen.
Die Beschreibung des exportierten Schriftbilds sieht folgendermaßen aus:
ABCDEFGHIJ
KLMNOPQRST
UVWXYZ1234
567890:!?
Sie muss mit dem Schema im Bild übereinstimmen.
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]);
Das erste, was wir tun, ist die einfache Validierung und Analyse der Befehlszeilenargumente.
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);
Die Bilddatei wird zuerst gelesen.
Das BMP-Dateiformat verfügt über einen Header, der den Inhalt der Datei beschreibt. Insbesondere die Breite und Höhe des Bildes sind für uns wichtig, ebenso wie der Versatz in der Datei, in der die Bilddaten beginnen.
Wir erstellen eine Struktur, die das Schema dieses Headers beschreibt, damit der Header geladen werden kann und auf die gewünschten Werte über den Namen zugegriffen werden kann. Die Pragma-Pack-Zeile stellt sicher, dass der Struktur keine Füllbytes hinzugefügt werden, damit der Header beim Lesen aus der Datei korrekt übereinstimmt.
Das BMP-Format ist insofern etwas seltsam, als die Bytes nach dem Offset je nach verwendeter BMP-Spezifikation stark variieren können (Microsoft hat es viele Male aktualisiert). Mit headerSizeWir prüfen, welche Version des Headers verwendet wird.
Wir überprüfen, ob die ersten beiden Bytes des Headers BM entsprechen , da dies bedeutet, dass es sich um eine BMP-Datei handelt. Als nächstes überprüfen wir, ob die Bittiefe 8 beträgt, da wir erwarten, dass jedes Pixel ein Byte ist. Wir überprüfen auch, ob der Header 40 Byte groß ist, da dies bedeutet, dass die BMP-Datei die gewünschte Version ist.
Die Bilddaten werden nach dem Aufruf von fseek in den imageBuffer geladen , um zum Speicherort der durch Offset angegebenen Bilddaten zu gelangen .
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);
Wir lesen die Glyphenschema-Beschreibungsdatei in ein Array von Zeichenfolgen, die wir unten benötigen.
Zuerst zählen wir die Anzahl der Zeilen in der Datei, um zu wissen, wie viel Speicher für die Zeilen zugewiesen werden muss (ein Zeiger pro Zeile), und dann lesen wir die Datei in den Speicher.
Zeilenumbrüche werden abgeschnitten, damit sie die Zeilenlänge in Zeichen nicht erhöhen.
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");
Wir generieren eine Funktion namens GetGlyphIndex , die ein Zeichen nimmt und den Datenindex dieses Zeichens in der Glyphenzuordnung zurückgibt (die wir in Kürze generieren werden).
Das Tool geht iterativ die zuvor gelesene Schemabeschreibung durch und generiert eine switch-Anweisung, die das Zeichen dem Index zuordnet. Sie können Klein- und Großbuchstaben an denselben Wert binden und eine Bestätigung generieren, wenn Sie versuchen, ein Zeichen zu verwenden, das kein Glyphenzuordnungszeichen ist.
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");
Schließlich generieren wir die 16-Bit-Werte selbst für jedes der Glyphen.
Wir durchlaufen die Zeichen aus der Beschreibung von oben nach unten, von links nach rechts und erstellen dann 16 16-Bit-Werte für jede Glyphe, indem wir ihre Pixel im Bild durchlaufen. Wenn ein Pixel aktiviert ist, schreibt der Code in die Bitposition dieses Pixels 1, andernfalls - 0.
Leider ist der Code dieses Tools aufgrund der vielen Aufrufe von fprintf ziemlich hässlich , aber ich hoffe, dass die Bedeutung dessen, was darin geschieht, klar ist.
Das Tool kann dann ausgeführt werden, um die exportierte Schriftbilddatei zu verarbeiten:
./font_processor font.bmp font.txt font.h 16 16
Und es generiert die folgende (abgekürzte) Datei:
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, .
Sobald wir die Datei font.h mit den Glyphenbyte- Arrays gefüllt haben , können wir sie auf den Bildschirm zeichnen.
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;
}
}
}
}
}
Da wir die Hauptlast auf unser Tool übertragen haben, ist der Text-Rendering-Code selbst recht einfach.
Um eine Zeichenfolge zu rendern, durchlaufen wir die einzelnen Zeichen und überspringen ein Zeichen, wenn wir auf ein Leerzeichen stoßen.
Für jedes Nicht-Leerzeichen erhalten wir den Glyphenindex in der Glyphenzuordnung, damit wir sein Byte-Array erhalten können.
Um die Pixel in einem Glyphen zu überprüfen, durchlaufen wir 256 seiner Pixel (16 x 16) und überprüfen den Wert jedes Bits in jeder Zeile. Wenn das Bit eingeschaltet ist, schreiben wir die Farbe für dieses Pixel in den Bildpuffer. Wenn es nicht aktiviert ist, tun wir nichts.
Es lohnt sich normalerweise nicht, Daten in eine Header-Datei zu schreiben, da sich der Linker über mehrere Definitionen beschwert, wenn dieser Header in mehreren Quelldateien enthalten ist. Aber font.h wird nur durch die im Code enthalten sein text.c Datei , so wird es nicht zu Problemen führen.
Demo
Wir werden das Rendern von Text testen, indem wir das berühmte Pangram " Der schnelle braune Fuchs, der über den faulen Hund gesprungen ist" rendern , das alle von der Schriftart unterstützten Zeichen verwendet.
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)));
Wir rufen DrawText dreimal auf, damit die Linien auf verschiedenen Linien erscheinen, und erhöhen die Kachel-Y-Koordinate für jede Linie so, dass jede Linie unter der vorherigen gezeichnet wird. Wir werden auch für jede Linie eine andere Farbe einstellen, um die Farben zu testen.
Im Moment berechnen wir die Länge der Zeichenfolge manuell, aber in Zukunft werden wir diesen Aufwand beseitigen.

Links
Teil 8: Das Fliesensystem
Wie im vorherigen Teil erwähnt, werden wir Spielhintergründe aus Kacheln erstellen. Die dynamischen Objekte vor dem Hintergrund sind Sprites , die wir uns später ansehen werden. Beispiele für Sprites sind Feinde, Kugeln und der Spielercharakter.
Wir werden 16x16 Kacheln auf einem 320x240 Bildschirm in einem festen 20x15 Raster platzieren. Zu jedem Zeitpunkt können wir bis zu 300 Kacheln auf dem Bildschirm anzeigen.
Fliesenpuffer
Zum Speichern von Kacheln sollten statische Arrays und kein dynamischer Speicher verwendet werden, um sich bei der Zuweisung keine Gedanken über Malloc und Free , Speicherlecks und Speichermangel zu machen (Odroid ist ein eingebettetes System mit einer begrenzten Speichermenge).
Wenn wir das Layout der Kacheln auf dem Bildschirm speichern möchten und die Gesamtzahl der Kacheln 20 x 15 beträgt, können wir ein 20 x 15-Array verwenden, in dem jedes Element ein Kachelindex in der "Karte" ist. Die Kachelkarte enthält die Kachelgrafiken selbst.

In diesem Diagramm repräsentieren die Zahlen oben die X-Koordinate der Kachel (in Kacheln) und die Zahlen links die Y-Koordinate der Kachel (in Kacheln).
Im Code kann es folgendermaßen dargestellt werden:
uint8_t tileBuffer[15][20];
Das Problem bei dieser Lösung ist, dass der Spieler das Ersatzplättchen sieht, wenn wir ändern möchten, was auf dem Bildschirm angezeigt wird (indem wir den Inhalt des Plättchens ändern).
Dies kann gelöst werden, indem der Pufferbereich erweitert wird, sodass Sie außerhalb des Bildschirms darauf schreiben können. Wenn er angezeigt wird, sieht er kontinuierlich aus.

Die grauen Quadrate zeigen das sichtbare "Fenster" im Kachelpuffer an, das auf dem Bildschirm gerendert wird. Während auf dem Bildschirm angezeigt wird, was sich in den grauen Quadraten befindet, kann der Inhalt aller weißen Quadrate so geändert werden, dass der Spieler ihn nicht sieht.
Im Code kann dies als ein Array angesehen werden, das in X doppelt so groß ist.
uint8_t tileBuffer[15][40];
Auswahl einer Palette
Im Moment verwenden wir eine Palette mit vier Graustufenwerten.
Im RGB888-Format sehen sie folgendermaßen aus:
- 0xFFFFFF (Weiß / 100% Wert).
- 0xABABAB (- / 67% )
- 0x545454 (- / 33% )
- 0x000000 ( / 0% )

Wir vermeiden es vorerst, Farben zu verwenden, weil ich meine künstlerischen Fähigkeiten immer noch verbessere. Durch die Verwendung von Graustufen kann ich mich auf Kontrast und Form konzentrieren, ohne mich um die Farbtheorie kümmern zu müssen. Selbst eine kleine Farbpalette erfordert einen guten künstlerischen Geschmack.
Wenn Sie Zweifel an der Stärke der 2-Bit-Graustufenfarbe haben, denken Sie an den Game Boy, dessen Palette nur vier Farben enthält. Der erste Game Boy-Bildschirm war grün getönt, sodass die vier Werte als Grüntöne angezeigt wurden, aber die Game Boy-Tasche zeigte sie als echte Graustufen an.
Das Bild unten für The Legend of Zelda: Link's Awakening zeigt, wie viel Sie mit nur vier Werten erreichen können, wenn Sie einen guten Künstler haben.

Im Moment sehen die Kachelgrafiken wie vier Quadrate mit einem Rand von einem Pixel außerhalb und abgeschnittenen Ecken aus. Jedes Quadrat hat eine der Farben in unserer Palette.
Das Abschneiden von Ecken ist eine kleine Änderung, aber Sie können zwischen einzelnen Kacheln unterscheiden, was zum Rendern des Netzes hilfreich ist.

Palettenwerkzeug
Wir werden die Palette im Dateiformat der JASC-Palette speichern, das einfach zu lesen, mit Tools leicht zu analysieren und von Aseprite unterstützt wird.
Die Palette sieht so aus
JASC-PAL
0100
4
255 255 255
171 171 171
84 84 84
0 0 0
Die ersten beiden Zeilen befinden sich in jeder PAL-Datei. Die dritte Zeile gibt die Anzahl der Elemente in der Palette an. Die restlichen Zeilen sind die Werte der roten, grünen und blauen Elemente der Palette.
Das Palettenwerkzeug liest die Datei, konvertiert jede Farbe in RGB565, kehrt die Bytereihenfolge um und schreibt die neuen Werte in die Header-Datei, die die Palette in einem Array enthält.
Der Code zum Lesen und Schreiben der Datei ähnelt dem in Teil 7 dieses Artikels verwendeten Code, und die Farbverarbeitung erfolgt wie folgt:
// 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;
}
Die strtok-Funktion teilt den String nach den Trennzeichen auf. Die drei Farbwerte sind durch ein Leerzeichen getrennt, also verwenden wir das. Wir erstellen dann den RGB565-Wert, indem wir die Bits verschieben und die Bytereihenfolge umkehren, wie wir es im dritten Teil des Artikels getan haben.
./palette_processor grey.pal grey.h
Die Ausgabe des Tools sieht folgendermaßen aus:
uint16_t palette[4] =
{
0xFFFF,
0x55AD,
0xAA52,
0x0000,
};
Fliesenverarbeitungswerkzeug
Wir brauchen auch ein Tool, das Kacheldaten in dem vom Spiel erwarteten Format ausgibt. Der Wert jedes Pixels in der BMP-Datei ist ein Palettenindex. Wir behalten diese indirekte Notation bei, sodass eine 16x16 (256) Byte-Kachel ein Byte pro Pixel belegt. Während der Ausführung des Programms finden wir die Farbe der Kachel in der Palette.
Das Tool liest die Datei, durchläuft die Pixel und schreibt ihre Indizes in ein Array im Header.
Der Code zum Lesen und Schreiben der Datei ähnelt auch dem Code im Schriftverarbeitungswerkzeug, und die Erstellung des entsprechenden Arrays erfolgt hier:
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;
}
}
}
Der Index wird von der Pixelposition in der BMP-Datei erhalten und dann als 16x16-Array-Element in die Datei geschrieben.
./tile_processor black.bmp black.h
Die Ausgabe des Werkzeugs bei der Verarbeitung einer schwarzen Kachel sieht folgendermaßen aus:
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,
};
Wenn Sie genau hinschauen, können Sie das Erscheinungsbild einer Kachel einfach anhand der Indizes verstehen. Jede 3 bedeutet schwarz und jede 0 bedeutet weiß.
Rahmenfenster
Als Beispiel können wir eine einfache (und extrem kurze) "Ebene" erstellen, die den gesamten Kachelpuffer ausfüllt. Wir haben vier verschiedene Kacheln, und um uns keine Sorgen um die Grafiken zu machen, verwenden wir einfach ein Schema, bei dem jede der vier Kacheln eine andere Farbe in Graustufen hat.

Wir ordnen vier Kacheln in einem Raster von 40 x 15 Ebenen an, um unser System zu testen.
Die obigen Zahlen geben die Spaltenindizes des Framebuffers an. Die folgenden Zahlen sind die Indizes der Rahmenfensterspalten. Die Zahlen links sind die Zeilen jedes Puffers (keine vertikale Fensterbewegung).
Für den Player sieht alles so aus, wie im obigen Video gezeigt. Wenn das Fenster nach rechts verschoben wird, erscheint dem Player der Hintergrund nach links verschoben.
Demo

Die Nummer in der oberen linken Ecke ist die Spaltennummer am linken Rand des Kachelpufferfensters, und die Nummer in der oberen rechten Ecke ist die Spaltennummer am rechten Rand des Kachelpufferfensters.
Quelle
Der Quellcode für das gesamte Projekt ist hier .