From fd523e10066e91e78e7520407f958c5a228fe80b Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Wed, 9 Apr 2025 10:24:04 +0200 Subject: Added new note --- ...-08-embedding-game-assets-within-your-binary.md | 236 +++++++++++++++++++++ static/assets/notes/embedding-assets.png | Bin 0 -> 136141 bytes static/assets/notes/embedding-binary-data.tar.xz | Bin 0 -> 1222696 bytes static/assets/notes/embedding-window.png | Bin 0 -> 5689 bytes templates/base.html | 27 +-- 5 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 content/notes/2025-04-08-embedding-game-assets-within-your-binary.md create mode 100644 static/assets/notes/embedding-assets.png create mode 100644 static/assets/notes/embedding-binary-data.tar.xz create mode 100644 static/assets/notes/embedding-window.png diff --git a/content/notes/2025-04-08-embedding-game-assets-within-your-binary.md b/content/notes/2025-04-08-embedding-game-assets-within-your-binary.md new file mode 100644 index 0000000..bb709bb --- /dev/null +++ b/content/notes/2025-04-08-embedding-game-assets-within-your-binary.md @@ -0,0 +1,236 @@ +--- +title: Embedding game assets within your executable binary +url: embedding-game-assets-within-your-binary.html +date: 2025-04-08T16:13:13+02:00 +type: note +draft: false +tags: [gamedev] +--- + +## Why? + +Normally, assets live outside the binary, but there is a valid reason to embed +them. Specially if you do not want to deal with reading and parsing external +files. This makes development and distribution much easier. + +In this example, I'm using [raylib](https://github.com/raysan5/raylib) and C, +but this can also be done with [SDL](https://github.com/libsdl-org/SDL) or +other libraries. + +Code for these notes is available as an +[embedding-binary-data.tar.xz](/assets/notes/embedding-binary-data.tar.xz) +tarball, but beware that this only includes the Linux build of raylib so please +change to appropriate operating system. + +## Project structure + +We are going to keep it clean and simple here. I am using pre-build version of +raylib just to simplify compilation. All the code is located in the `main.c` +file, so refer to that. + +``` +├── data Contains assets +│   ├── armor.png Image example +│   └── dejavusans-mono.ttf Font example +├── libs +│   └── raylib-5.5_linux_amd64 Includes prebuild raylib +├── main.c Main file +└── Makefile Build instruction +``` + +## Main game loop + +We first need to setup main game loop and get the game running. + +```c +#include +#include "raylib.h" + +int main(void) { + SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_WINDOW_HIGHDPI); + InitWindow(900, 400, "Embedding assets"); + SetTargetFPS(60); + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(BLACK); + EndDrawing(); + } + + CloseWindow(); + return 0; +} +``` + +And then prepare basic `Makefile` to compile this. + +```make +RAYLIB_VER := 5.5_linux_amd64 +CC ?= cc +CFLAGS := -Wall -Wextra -Wunused -Wno-unused-parameter -Wswitch-enum -Wpedantic -Wno-bitwise-instead-of-logical +LDFLAGS := -I./libs -I./libs/raylib-$(RAYLIB_VER)/include ./libs/raylib-$(RAYLIB_VER)/lib/libraylib.a -lm + +game: + $(CC) $(CFLAGS) -o game main.c $(LDFLAGS) +``` + +Now we can compile with `make game` and get the following. + +![Basic window](/assets/notes/embedding-window.png) + +## Converting assets to C header files + +Here we use [`xxd`](https://linux.die.net/man/1/xxd) which is used to make a +hexdump or do the reverse. This tool contains an interesting option `-i` which +allows us to output the file in C include file style. And this is exactly what +we need. + +Modified `Makefile` now looks like. + +```make +RAYLIB_VER := 5.5_linux_amd64 +CC ?= cc +CFLAGS := -Wall -Wextra -Wunused -Wno-unused-parameter -Wswitch-enum -Wpedantic -Wno-bitwise-instead-of-logical +LDFLAGS := -I./libs -I./libs/raylib-$(RAYLIB_VER)/include ./libs/raylib-$(RAYLIB_VER)/lib/libraylib.a -lm + +game: + $(CC) $(CFLAGS) -o game main.c $(LDFLAGS) + +embed: + xxd -i data/armor.png > data/armor.h + xxd -i data/dejavusans-mono.ttf > data/dejavusans-mono.h +``` + +This converted binary data files into C header style files which contain the +array of bytes and the size of the array of bytes. This will be useful later +with raylib code. + +An example of such a file (in our case armor.png) looks like this. + +```c +// data/armor.h +unsigned char data_armor_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x01, 0x40, + 0x08, 0x06, 0x00, 0x00, 0x00, 0xcd, 0x90, 0xa5, 0xaa, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x06, 0x62, 0x4b, 0x47, 0x44, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, + 0xa0, 0xbd, 0xa7, 0x93, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, + 0x00, 0x00, 0x17, 0x12, 0x00, 0x00, 0x17, 0x12, 0x01, 0x67, 0x9f, 0xd2, + 0x52, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xdc, 0x0c, + 0x1b, 0x07, 0x30, 0x17, 0xb2, 0x1a, 0xee, 0xda, 0x00, 0x00, 0x20, 0x00, + ... + 0x81, 0x98, 0xa6, 0xb9, 0x2d, 0x37, 0xac, 0x6d, 0x57, 0xb0, 0xed, 0xea, + 0x86, 0xac, 0xa1, 0xa6, 0x6b, 0xf8, 0x54, 0x1f, 0x8e, 0xe3, 0x90, 0xcb, + 0xe6, 0x36, 0x7d, 0x4e, 0x6b, 0xab, 0xcb, 0x78, 0xbd, 0x5e, 0x6c, 0xc7, + 0x01, 0x8f, 0xa7, 0x5e, 0x46, 0x22, 0x37, 0x17, 0x76, 0x13, 0x4d, 0x6c, + 0xab, 0x0c, 0xb0, 0x89, 0x26, 0x9a, 0x68, 0xe2, 0x45, 0xa3, 0x99, 0x2b, + 0x34, 0xd1, 0x44, 0x13, 0xff, 0xb7, 0xf8, 0x27, 0x9b, 0x96, 0x5c, 0x0d, + 0x8b, 0xbb, 0x21, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, + 0xae, 0x42, 0x60, 0x82 +}; +unsigned int data_armor_png_len = 118324; +``` + +## Embedding and compiling + +Now it's time to include this files in our `main.c` code and use them. raylib's +API fits perfectly with this style of converting binary files. + +```c +#include +#include "raylib.h" + +#include "data/armor.h" +#include "data/dejavusans-mono.h" + +#define FONT_SIZE 24 + +int main(void) { + SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_WINDOW_HIGHDPI); + InitWindow(900, 400, "Embedding assets"); + SetTargetFPS(60); + + // Load font from memory. + Font font = LoadFontFromMemory(".ttf", data_dejavusans_mono_ttf, data_dejavusans_mono_ttf_len, FONT_SIZE, NULL, 0); + SetTextureFilter(font.texture, TEXTURE_FILTER_TRILINEAR); // Font antialising. + + // Load image from memory and create texture from it. + Image armor = LoadImageFromMemory(".png", data_armor_png, data_armor_png_len); + Texture2D armor_texture = LoadTextureFromImage(armor); + UnloadImage(armor); + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(BLACK); + + // Draw the armor texture. + DrawTexture(armor_texture, 20, 20, WHITE); + + // Draw some text on the screen. + DrawTextEx(font, "Hello embedded assets.", (Vector2){ 400, 20 }, FONT_SIZE, 0, WHITE); + DrawTextEx(font, "This is example how we can use embedded fonts.", (Vector2){ 400, 50 }, FONT_SIZE - 4, 0, WHITE); + + EndDrawing(); + } + + UnloadTexture(armor_texture); + CloseWindow(); + return 0; +} +``` + +This makes embedding assets quite straightforward. All we need to do is include +header files and then provide byte arrays to the appropriate functions. Like I +said raylib has a bunch of `FromMemory` functions that make this quite easy. + +This produces a single binary `game` that includes both assets, so there is no +need to also copy over these assets. Mind you, these binaries can potentially +get quite big and if that is the problem, you could always compile this into a +`so` library and include that. This way you could create data packs for audio, +graphics, etc. and ship that alongside your game binary. + +![Basic window](/assets/notes/embedding-assets.png) + +## Honorable mention: C23-embed-directive + +`#embed` is a preprocessor directive to include (binary) resources in the +build, where a resource is defined as a source of data accessible from the +translation environment. This has been introduces with the C23 standard so it's +quite new. + +Speaking plainly, `#embed` allows inclusion of binary data in a program +executable image, as arrays of unsigned char or other types, without the need +for an external script run from a Makefile. + +- I do like this approach, but some compilers might not support this feature, + and this is why I will be sticking with manual approach for now. +- One additional drawback is that every time you compile your game, all the + assets need to be re-read and converted. I have not heavily tested this, but + this could be a potential problem when you have a lot of assets. +- Not using Make could be detrimental to incremental builds. But this would + also need to be tested. I do not claim that this is a real problem. + +Read more about [Binary resource +inclusion](https://en.cppreference.com/w/c/preprocessor/embed). + +Below is a quick example how to use this new directive. + +```c +#include +#include + +const uint8_t image_data[] = { + #embed "image.png" +}; + +int main(void) { + printf("Image size: %d bytes\n", sizeof(image_data)); + return 0; +} +``` + +## Credits + +- https://opengameart.org/content/armor-icons-by-equipment-slot-with-transparency +- https://dejavu-fonts.github.io/ diff --git a/static/assets/notes/embedding-assets.png b/static/assets/notes/embedding-assets.png new file mode 100644 index 0000000..e424cc6 Binary files /dev/null and b/static/assets/notes/embedding-assets.png differ diff --git a/static/assets/notes/embedding-binary-data.tar.xz b/static/assets/notes/embedding-binary-data.tar.xz new file mode 100644 index 0000000..15e3ff4 Binary files /dev/null and b/static/assets/notes/embedding-binary-data.tar.xz differ diff --git a/static/assets/notes/embedding-window.png b/static/assets/notes/embedding-window.png new file mode 100644 index 0000000..cb8c5c2 Binary files /dev/null and b/static/assets/notes/embedding-window.png differ diff --git a/templates/base.html b/templates/base.html index 956241a..2b11ba7 100644 --- a/templates/base.html +++ b/templates/base.html @@ -54,7 +54,7 @@ img, video { border: var(--border-width) solid var(--primary-color); padding: 0.5em; } img.no-border, video.no-border { border: 0; padding: 0; } - pre { background: var(--code-color)!important; border: var(--border-width) solid var(--primary-color); text-wrap: wrap; padding: 1.0em; line-height: 170%; } + pre { background: var(--code-color)!important; border: var(--border-width) solid var(--primary-color); text-wrap: wrap; padding: 1.0em; line-height: 170%; text-wrap: nowrap; overflow-x: auto; } code { background: var(--code-color); padding: 0 0.2em; } pre code { padding: 0; } @@ -64,30 +64,9 @@ article iframe { margin: 0!important; } pre code span { display: initial !important; } - /*html { scroll-padding: 2rem; font-family: sans-serif; } - body { display: grid; gap: 2em; grid-template-columns: 16em auto; max-width: 1200px; line-height: 130%; } - h1, h2, h3, h4, h5, h6 { line-height: 120%; } - a { color: blue; text-decoration: none; } - a:hover { text-decoration: underline; } - img, video, audio { max-width: 100%; } - pre { background: #f5f5f5 !important; text-wrap: wrap; padding: 0.5em; } - code { background: #f5f5f5; } - li { break-inside: avoid-column; } - table { width: 100%; } - figure { margin: 0; } - .flow-two-column > ul { column-count: 2; column-gap: 3em; } - .flow-three-column > ul { column-count: 3; column-gap: 3em; } - .tags { display: inline-flex; gap: 0.2em; padding-right: 0.4em; font-family: monospace; } - .tags mark { padding: 0 0.2em; } - @media only screen and (max-width: 860px) { - body { display: block; } - main { margin: 1em 0; } - .flow-two-column > ul { column-count: 1; } - .flow-three-column > ul { column-count: 1; } + /*@media only screen and (max-width: 860px) { .hide-on-mobile { display: none; } - } - article iframe { margin: 0!important; } - pre code span { display: initial !important; }*/ + }*/ -- cgit v1.2.3