

Mulai: perakitan, sistem input, display.
Lanjutan: drive, baterai, suara.
Bagian 7: Teks
Sekarang setelah kita selesai dengan lapisan kode Odroid Go, kita dapat mulai membuat game itu sendiri.
Mari kita mulai dengan menggambar teks di layar karena ini akan menjadi pengantar yang mulus untuk beberapa topik yang akan berguna di masa mendatang.
Bagian ini akan sedikit berbeda dari yang sebelumnya karena hanya ada sedikit kode yang berjalan di Odroid Go. Sebagian besar kode akan terkait dengan alat pertama kami.
Ubin
Dalam sistem rendering kami, kami akan menggunakan ubin . Kami akan membagi layar 320x240 menjadi kotak petak, masing-masing berisi 16x16 piksel. Ini akan membuat kisi-kisi dengan lebar 20 ubin dan tinggi 15 ubin.
Elemen statis seperti latar belakang dan teks akan ditampilkan menggunakan sistem ubin, sedangkan elemen dinamis seperti sprite akan ditampilkan secara berbeda. Artinya latar belakang dan teks hanya dapat ditempatkan di lokasi tetap, sedangkan sprite dapat ditempatkan di mana saja di layar.

Satu bingkai 320x240, seperti yang ditunjukkan di atas, dapat berisi 300 ubin. Garis kuning menunjukkan batas antar ubin. Setiap ubin akan memiliki simbol tekstur atau elemen latar belakang.

Gambar yang diperbesar dari satu ubin menunjukkan konstituen 256 piksel yang dipisahkan oleh garis abu-abu.
Font
Biasanya font TrueType digunakan saat merender font di komputer desktop . Font terdiri dari mesin terbang yang mewakili karakter.
Untuk menggunakan font, Anda memuatnya menggunakan perpustakaan (seperti FreeType ) dan membuat atlas font yang berisi versi bitmap dari semua mesin terbang, yang kemudian diambil sampelnya saat rendering. Ini biasanya terjadi sebelumnya, bukan dalam game itu sendiri.
Dalam gim, memori GPU menyimpan satu tekstur dengan font raster dan deskripsi dalam kode yang memungkinkan Anda untuk menentukan di mana mesin terbang yang diinginkan berada dalam tekstur. Proses rendering teks terdiri dari rendering sebagian tekstur dengan mesin terbang ke quad 2D sederhana.
Namun, kami mengambil pendekatan yang berbeda. Alih-alih berkelahi dengan file dan pustaka TTF, kami akan membuat font sederhana kami sendiri.
Inti dari sistem font tradisional seperti TrueType adalah untuk dapat membuat font dalam berbagai ukuran atau resolusi tanpa mengubah file font asli. Ini dilakukan dengan mendeskripsikan font dengan ekspresi matematika.
Tapi kita tidak membutuhkan keserbagunaan seperti itu, kita tahu resolusi tampilan dan ukuran font yang kita butuhkan, jadi kita bisa meraster font kita sendiri secara manual.
Untuk ini saya membuat font 39 karakter sederhana. Setiap simbol menempati satu ubin 16x16. Saya bukan tipe desainer profesional, tetapi hasilnya sangat cocok untuk saya.

Gambar aslinya berukuran 160x64, tetapi di sini saya telah menggandakan skalanya agar mudah dilihat.
Tentunya hal ini akan mencegah kita untuk menulis teks dalam bahasa yang tidak menggunakan 26 huruf alfabet bahasa Inggris....
Encode mesin terbang tersebut

Melihat contoh untuk mesin terbang "A", kita dapat melihat bahwa itu adalah enam belas baris dengan panjang enam belas piksel. Di setiap baris, piksel aktif atau nonaktif. Kita dapat menggunakan fitur ini untuk menyandikan mesin terbang tanpa harus memuat bitmap font ke dalam memori dengan cara tradisional.
Setiap piksel dalam satu baris dapat dianggap sebagai satu bit, yaitu satu baris berisi 16 bit. Jika piksel aktif, maka bit aktif, dan sebaliknya. Artinya, pengkodean fretboard dapat disimpan sebagai enam belas bilangan bulat 16-bit.

Dalam skema ini, huruf "A" dikodekan dengan gambar yang ditunjukkan di atas. Angka di sebelah kiri mewakili nilai string 16-bit.
Mesin terbang lengkap dikodekan dalam 32 byte (2 byte per baris x 16 baris). Dibutuhkan 1248 byte untuk menyandikan semua 39 karakter.
Cara lain untuk mengatasi masalah ini adalah dengan menyimpan file gambar ke kartu SD Odroid Go, memuatnya ke memori saat inisialisasi, dan kemudian mereferensikannya saat merender teks untuk menemukan mesin terbang yang Anda inginkan.
Tetapi file gambar harus menggunakan setidaknya satu byte per piksel (0x00 atau 0x01), jadi ukuran gambar minimum adalah (tidak dikompresi) 10240 byte (160 x 64).
Selain menghemat memori, metode kami memungkinkan kami untuk secara mudah menyandikan array byte dari mesin terbang font langsung ke kode sumber sehingga kami tidak perlu memuatnya dari file.
Saya cukup yakin ESP32 dapat menangani pemuatan gambar ke dalam memori dan mereferensikannya pada waktu proses, tetapi saya menyukai gagasan untuk mengkodekan ubin langsung ke dalam array seperti ini. Ini sangat mirip dengan cara penerapannya di SEN.
Pentingnya alat tulis
Permainan harus dijalankan secara real time dengan frekuensi minimal 30 frame per detik. Artinya, semua yang ada di game harus diproses dalam 1/30 detik, yaitu sekitar 33 milidetik.
Untuk membantu mencapai tujuan ini, yang terbaik adalah memproses data terlebih dahulu jika memungkinkan sehingga data dapat digunakan dalam game tanpa pemrosesan apa pun. Ini juga menghemat memori dan ruang penyimpanan.
Seringkali ada semacam pipa sumber daya yang mengambil data mentah yang diekspor dari alat pembuatan konten dan mengubahnya menjadi bentuk yang lebih cocok untuk dimainkan dalam permainan.
Dalam kasus font kami, kami memiliki satu set simbol yang dibuat di Asepriteyang dapat diekspor sebagai file gambar 160x64.
Alih-alih memuat gambar ke dalam memori saat permainan dimulai, kita dapat membuat alat untuk mengubah data menjadi lebih banyak ruang dan formulir yang dioptimalkan untuk waktu proses yang dijelaskan di bagian sebelumnya.
Alat pengolah font
Kita harus mengubah masing-masing dari 39 mesin terbang dari gambar asli menjadi array byte yang menggambarkan keadaan piksel konstituennya (seperti dalam contoh dengan karakter "A").
Kita dapat meletakkan array byte yang telah diproses sebelumnya ke dalam file header yang dikompilasi ke dalam game dan ditulis ke Flash drive-nya. ESP32 memiliki lebih banyak memori Flash daripada RAM, jadi kita dapat memanfaatkan ini dengan mengumpulkan informasi sebanyak mungkin ke dalam biner game.
Untuk pertama kalinya, kita dapat melakukan kalkulasi konversi piksel-ke-byte secara manual dan itu akan cukup bisa dilakukan (meskipun membosankan). Tetapi jika kita ingin menambahkan mesin terbang baru atau mengubah yang lama, prosesnya menjadi monoton, memakan waktu dan rawan kesalahan.
Dan ini adalah kesempatan bagus untuk membuat alat.
Alat ini akan memuat file gambar, menghasilkan array byte untuk setiap karakter, dan menulisnya ke file header yang dapat kita kompilasi ke dalam game. Jika kita ingin mengubah mesin terbang dari font (yang telah saya lakukan berkali-kali) atau menambahkan yang baru, kita cukup menjalankan ulang alat tersebut.
Langkah pertama adalah mengekspor kumpulan mesin terbang dari Aseprite dalam format yang dapat dibaca dengan mudah oleh alat kami. Kami menggunakan format file BMP karena memiliki header yang sederhana, tidak mengkompres gambar, dan memungkinkan gambar dikodekan dalam 1 byte per piksel.
Di Aseprite, saya membuat gambar dengan palet yang diindeks, jadi setiap piksel adalah satu byte yang mewakili indeks palet yang hanya berisi warna hitam (Indeks 0) dan putih (Indeks 1). File BMP yang diekspor mempertahankan pengkodean ini: piksel yang dinonaktifkan memiliki byte 0x0, dan piksel yang diaktifkan memiliki byte 0x1.
Alat kami akan menerima lima parameter:
- BMP diekspor dari Aseprite
- File teks yang menjelaskan skema mesin terbang
- Jalur ke file keluaran yang dihasilkan
- Lebar setiap mesin terbang
- Ketinggian setiap mesin terbang
File deskripsi skema mesin terbang diperlukan untuk memetakan informasi visual gambar ke karakter itu sendiri dalam kode.
Deskripsi gambar font yang diekspor terlihat seperti ini:
ABCDEFGHIJ
KLMNOPQRST
UVWXYZ1234
567890:!?
Ini harus sesuai dengan skema pada gambar.
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]);
Hal pertama yang kami lakukan adalah validasi dan parsing sederhana dari argumen baris perintah.
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);
File gambar dibaca terlebih dahulu.
Format file BMP memiliki header yang mendeskripsikan isi file. Secara khusus, lebar dan tinggi gambar penting bagi kami, serta offset dalam file tempat data gambar dimulai.
Kita akan membuat struct yang mendeskripsikan skema dari header ini sehingga header tersebut dapat dimuat dan nilai yang kita inginkan dapat diakses berdasarkan nama. Garis paket pragma memastikan bahwa tidak ada byte padding yang ditambahkan ke struct sehingga ketika header dibaca dari file, itu cocok dengan benar.
Format BMP agak aneh karena byte setelah offset dapat sangat bervariasi tergantung pada spesifikasi BMP yang digunakan (Microsoft memperbaruinya berkali-kali). Dengan headerSizekami memeriksa versi tajuk mana yang digunakan.
Kami memeriksa bahwa dua byte pertama dari header sama dengan BM , karena itu berarti file BMP. Selanjutnya, kami memeriksa bahwa kedalaman bit adalah 8 karena kami mengharapkan setiap piksel menjadi satu byte. Kami juga memeriksa bahwa header adalah 40 byte, karena itu berarti file BMP adalah versi yang kami inginkan.
Data gambar dimuat ke imageBuffer setelah fseek dipanggil untuk pergi ke lokasi data gambar yang ditentukan oleh 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);
Kami membaca file deskripsi skema mesin terbang ke dalam array string yang kami butuhkan di bawah ini.
Pertama, kami menghitung jumlah baris dalam file untuk mengetahui berapa banyak memori yang perlu dialokasikan untuk baris (satu penunjuk per baris), dan kemudian kami membaca file ke dalam memori.
Jeda baris dipotong agar tidak menambah panjang baris dalam karakter.
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");
Kami menghasilkan fungsi yang disebut GetGlyphIndex yang mengambil karakter dan mengembalikan indeks data karakter itu di peta mesin terbang (yang akan segera kami buat).
Alat ini berjalan secara berulang melalui deskripsi skema yang telah dibaca sebelumnya dan menghasilkan pernyataan sakelar yang cocok dengan karakter ke indeks. Hal ini memungkinkan Anda untuk huruf kecil mengikat dan karakter huruf besar dengan nilai yang sama dan menghasilkan menegaskan jika Anda mencoba untuk menggunakan karakter yang bukan karakter peta mesin terbang.
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");
Akhirnya, kami membuat sendiri nilai 16-bit untuk setiap mesin terbang.
Kami melintasi karakter dari deskripsi dari atas ke bawah, kiri ke kanan, dan kemudian membuat enam belas nilai 16-bit untuk setiap mesin terbang dengan melintasi pikselnya pada gambar. Jika sebuah piksel diaktifkan, maka kode akan menulis ke posisi bit piksel ini 1, jika tidak - 0.
Sayangnya, kode alat ini agak jelek karena banyaknya panggilan ke fprintf , tetapi saya berharap makna dari apa yang terjadi di dalamnya jelas.
Alat tersebut kemudian dapat dijalankan untuk memproses file gambar font yang diekspor:
./font_processor font.bmp font.txt font.h 16 16
Dan itu menghasilkan file berikut (disingkat):
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, .
Dengan mengisi file font.h dengan array byte dari mesin terbang, kita bisa mulai menggambarnya ke layar.
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;
}
}
}
}
}
Karena kami mentransfer beban utama ke alat kami, kode rendering teks itu sendiri akan cukup sederhana.
Untuk merender string, kami mengulang melalui karakter penyusunnya dan melewatkan karakter jika kami menemukan spasi.
Untuk setiap karakter non-spasi, kita mendapatkan indeks mesin terbang di peta mesin terbang sehingga kita bisa mendapatkan array byte-nya.
Untuk memeriksa piksel dalam mesin terbang, kami melakukan loop melalui 256 pikselnya (16x16) dan memeriksa nilai setiap bit di setiap baris. Jika bit aktif, maka kita tulis warna untuk piksel ini ke buffer frame. Jika tidak diaktifkan, maka kami tidak melakukan apa-apa.
Biasanya tidak ada gunanya menulis data ke file header karena jika header itu disertakan dalam beberapa file sumber, linker akan mengeluh tentang banyak definisi. Tetapi font.h hanya akan dimasukkan dalam kode oleh file text.c , jadi tidak akan menimbulkan masalah.
Demo
Kami akan menguji rendering teks dengan merender pangram terkenal The Quick Brown Fox Jumped Over The Lazy Dog , yang menggunakan semua karakter yang didukung oleh font tersebut.
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)));
Kami memanggil DrawText tiga kali untuk membuat garis muncul pada garis yang berbeda, dan menaikkan ubin Y untuk masing-masing sehingga setiap garis digambar di bawah yang sebelumnya. Kami juga akan mengatur warna berbeda untuk setiap baris untuk menguji warna.
Untuk saat ini, kami menghitung panjang string secara manual, tetapi di masa mendatang kami akan menghilangkan kerumitan ini.

Tautan
Bagian 8: sistem ubin
Seperti yang disebutkan di bagian sebelumnya, kami akan membuat latar belakang game dari ubin. Objek dinamis di depan latar belakang akan menjadi sprite , yang akan kita lihat nanti. Contoh sprite adalah musuh, peluru, dan karakter pemain.
Kami akan menempatkan ubin 16x16 pada layar 320x240 dalam petak 20x15 tetap. Pada waktu tertentu, kami dapat menampilkan hingga 300 ubin di layar.
Penyangga Ubin
Untuk menyimpan ubin, kita harus menggunakan array statis, bukan memori dinamis, agar tidak khawatir tentang malloc dan gratis , kebocoran memori dan memori tidak mencukupi saat mengalokasikannya (Odroid adalah sistem tertanam dengan jumlah memori terbatas).
Jika kita ingin menyimpan tata letak petak di layar, dan total petak adalah 20x15, maka kita dapat menggunakan larik 20x15, di mana setiap elemen adalah indeks petak di "peta". Tilemap berisi grafik ubin itu sendiri.

Dalam diagram ini, angka di atas mewakili koordinat X ubin (dalam ubin), dan angka di sebelah kiri mewakili koordinat Y ubin (dalam ubin).
Dalam kode, dapat direpresentasikan seperti ini:
uint8_t tileBuffer[15][20];
Masalah dengan solusi ini adalah jika kita ingin mengubah apa yang ditampilkan di layar (dengan mengubah konten ubin), maka pemain akan melihat penggantian ubin.
Hal ini dapat diatasi dengan memperluas area penyangga sehingga Anda dapat menulis padanya saat berada di luar layar, dan saat ditampilkan, terlihat terus menerus.

Kotak abu-abu menunjukkan "jendela" yang terlihat di buffer ubin, yang ditampilkan di layar. Saat layar menampilkan apa yang ada di kotak abu-abu, konten semua kotak putih dapat diubah sehingga pemain tidak melihatnya.
Dalam kode, ini dapat dianggap sebagai larik yang berukuran dua kali ukuran X.
uint8_t tileBuffer[15][40];
Memilih palet
Untuk saat ini, kami akan menggunakan palet dengan empat nilai skala abu-abu.
Dalam format RGB888, tampilannya seperti:
- 0xFFFFFF (nilai putih / 100%).
- 0xABABAB (- / 67% )
- 0x545454 (- / 33% )
- 0x000000 ( / 0% )

Kami menghindari penggunaan warna untuk saat ini karena saya masih meningkatkan keterampilan artistik saya. Dengan menggunakan grayscale, saya bisa fokus pada kontras dan bentuk tanpa mengkhawatirkan teori warna. Bahkan palet kecil warna membutuhkan rasa artistik yang bagus.
Jika Anda ragu tentang kekuatan warna 2-bit grayscale, pikirkan Game Boy, yang hanya memiliki empat warna dalam paletnya. Layar Game Boy pertama diwarnai hijau, jadi keempat nilai ditampilkan dalam nuansa hijau, tetapi Game Boy Pocket menampilkannya sebagai skala abu-abu sejati.
Gambar di bawah untuk The Legend of Zelda: Link's Awakening menunjukkan seberapa banyak yang dapat Anda capai hanya dengan empat nilai jika Anda memiliki artis yang baik.

Untuk saat ini, grafik ubin akan terlihat seperti empat kotak dengan batas satu piksel di luar dan dengan sudut terpotong. Setiap kotak akan memiliki salah satu warna di palet kami.
Memotong sudut adalah perubahan kecil, tetapi ini memungkinkan Anda untuk membedakan antara setiap ubin, yang berguna untuk merender mesh.

Alat palet
Kami akan menyimpan palet dalam format file Palet JASC, yang mudah dibaca, mudah diurai dengan alat, dan didukung oleh Aseprite.
Paletnya terlihat seperti ini
JASC-PAL
0100
4
255 255 255
171 171 171
84 84 84
0 0 0
Dua baris pertama ditemukan di setiap file PAL. Baris ketiga adalah jumlah item di palet. Sisa garis lainnya adalah nilai dari elemen merah, hijau dan biru dari palet.
Alat palet membaca file, mengubah setiap warna menjadi RGB565, membalik urutan byte, dan menulis nilai baru ke file header yang berisi palet dalam array.
Kode untuk membaca dan menulis file ini mirip dengan kode yang digunakan pada artikel ketujuh, dan pemrosesan warna dilakukan seperti ini:
// 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;
}
Fungsi strtok membagi string sesuai dengan pembatas. Tiga nilai warna dipisahkan oleh satu spasi, jadi kami menggunakannya. Kemudian kami membuat nilai RGB565 dengan menggeser bit dan membalik urutan byte, seperti yang kami lakukan di bagian ketiga artikel.
./palette_processor grey.pal grey.h
Output alat tersebut terlihat seperti ini:
uint16_t palette[4] =
{
0xFFFF,
0x55AD,
0xAA52,
0x0000,
};
Alat pengolah ubin
Kami juga membutuhkan alat yang menampilkan data ubin dalam format yang diharapkan oleh game. Nilai setiap piksel dalam file BMP adalah indeks palet. Kami akan menyimpan notasi tidak langsung ini sehingga ubin 16x16 (256) byte membutuhkan satu byte per piksel. Selama menjalankan program, kita akan menemukan warna ubin di palet.
Alat tersebut membaca file, melintasi piksel, dan menulis indeksnya ke array di header.
Kode untuk membaca dan menulis file juga mirip dengan kode di alat font, dan pembuatan larik terkait terjadi di sini:
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;
}
}
}
Indeks diperoleh dari posisi piksel di file BMP dan kemudian ditulis ke file sebagai elemen array 16x16.
./tile_processor black.bmp black.h
Keluaran alat saat memproses ubin hitam terlihat seperti ini:
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,
};
Jika Anda melihat lebih dekat, Anda dapat memahami tampilan ubin hanya dengan indeksnya. Setiap 3 berarti hitam dan setiap 0 berarti putih.
Bingkai jendela
Sebagai contoh, kita dapat membuat "level" sederhana (dan sangat pendek) yang memenuhi seluruh buffer ubin. Kami memiliki empat ubin berbeda, dan tidak perlu khawatir tentang grafik, kami hanya menggunakan skema di mana masing-masing dari empat ubin memiliki warna berbeda dalam skala abu-abu.

Kami menyusun empat ubin dalam kisi tingkat 40x15 untuk menguji sistem kami.
Angka-angka di atas menunjukkan indeks kolom dari framebuffer. Angka-angka di bawah ini adalah indeks kolom jendela bingkai. Angka-angka di sebelah kiri adalah garis dari setiap penyangga (tidak ada gerakan jendela vertikal).
Untuk pemain, semuanya akan terlihat seperti yang ditunjukkan pada video di atas. Ketika jendela dipindahkan ke kanan, bagi pemain tampak bahwa latar belakang bergeser ke kiri.
Demo

Angka di sudut kiri atas adalah nomor kolom dari tepi kiri jendela penyangga ubin, dan nomor di sudut kanan atas adalah nomor kolom dari tepi kanan jendela penyangga ubin.
Sumber
Kode sumber untuk seluruh proyek ada di sini .