diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2025-04-09 10:24:04 +0200 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2025-04-09 10:24:04 +0200 |
| commit | fd523e10066e91e78e7520407f958c5a228fe80b (patch) | |
| tree | bc29c9e340f4a2a47229869a25d4606fdc436025 /content/notes | |
| parent | 588b1c8686be928f070966dcba18fbcaed6bae57 (diff) | |
| download | mitjafelicijan.com-fd523e10066e91e78e7520407f958c5a228fe80b.tar.gz | |
Added new note
Diffstat (limited to 'content/notes')
| -rw-r--r-- | content/notes/2025-04-08-embedding-game-assets-within-your-binary.md | 236 |
1 files changed, 236 insertions, 0 deletions
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 @@ | |||
| 1 | --- | ||
| 2 | title: Embedding game assets within your executable binary | ||
| 3 | url: embedding-game-assets-within-your-binary.html | ||
| 4 | date: 2025-04-08T16:13:13+02:00 | ||
| 5 | type: note | ||
| 6 | draft: false | ||
| 7 | tags: [gamedev] | ||
| 8 | --- | ||
| 9 | |||
| 10 | ## Why? | ||
| 11 | |||
| 12 | Normally, assets live outside the binary, but there is a valid reason to embed | ||
| 13 | them. Specially if you do not want to deal with reading and parsing external | ||
| 14 | files. This makes development and distribution much easier. | ||
| 15 | |||
| 16 | In this example, I'm using [raylib](https://github.com/raysan5/raylib) and C, | ||
| 17 | but this can also be done with [SDL](https://github.com/libsdl-org/SDL) or | ||
| 18 | other libraries. | ||
| 19 | |||
| 20 | Code for these notes is available as an | ||
| 21 | [embedding-binary-data.tar.xz](/assets/notes/embedding-binary-data.tar.xz) | ||
| 22 | tarball, but beware that this only includes the Linux build of raylib so please | ||
| 23 | change to appropriate operating system. | ||
| 24 | |||
| 25 | ## Project structure | ||
| 26 | |||
| 27 | We are going to keep it clean and simple here. I am using pre-build version of | ||
| 28 | raylib just to simplify compilation. All the code is located in the `main.c` | ||
| 29 | file, so refer to that. | ||
| 30 | |||
| 31 | ``` | ||
| 32 | ├── data Contains assets | ||
| 33 | │ ├── armor.png Image example | ||
| 34 | │ └── dejavusans-mono.ttf Font example | ||
| 35 | ├── libs | ||
| 36 | │ └── raylib-5.5_linux_amd64 Includes prebuild raylib | ||
| 37 | ├── main.c Main file | ||
| 38 | └── Makefile Build instruction | ||
| 39 | ``` | ||
| 40 | |||
| 41 | ## Main game loop | ||
| 42 | |||
| 43 | We first need to setup main game loop and get the game running. | ||
| 44 | |||
| 45 | ```c | ||
| 46 | #include <stdio.h> | ||
| 47 | #include "raylib.h" | ||
| 48 | |||
| 49 | int main(void) { | ||
| 50 | SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_WINDOW_HIGHDPI); | ||
| 51 | InitWindow(900, 400, "Embedding assets"); | ||
| 52 | SetTargetFPS(60); | ||
| 53 | |||
| 54 | while (!WindowShouldClose()) { | ||
| 55 | BeginDrawing(); | ||
| 56 | ClearBackground(BLACK); | ||
| 57 | EndDrawing(); | ||
| 58 | } | ||
| 59 | |||
| 60 | CloseWindow(); | ||
| 61 | return 0; | ||
| 62 | } | ||
| 63 | ``` | ||
| 64 | |||
| 65 | And then prepare basic `Makefile` to compile this. | ||
| 66 | |||
| 67 | ```make | ||
| 68 | RAYLIB_VER := 5.5_linux_amd64 | ||
| 69 | CC ?= cc | ||
| 70 | CFLAGS := -Wall -Wextra -Wunused -Wno-unused-parameter -Wswitch-enum -Wpedantic -Wno-bitwise-instead-of-logical | ||
| 71 | LDFLAGS := -I./libs -I./libs/raylib-$(RAYLIB_VER)/include ./libs/raylib-$(RAYLIB_VER)/lib/libraylib.a -lm | ||
| 72 | |||
| 73 | game: | ||
| 74 | $(CC) $(CFLAGS) -o game main.c $(LDFLAGS) | ||
| 75 | ``` | ||
| 76 | |||
| 77 | Now we can compile with `make game` and get the following. | ||
| 78 | |||
| 79 |  | ||
| 80 | |||
| 81 | ## Converting assets to C header files | ||
| 82 | |||
| 83 | Here we use [`xxd`](https://linux.die.net/man/1/xxd) which is used to make a | ||
| 84 | hexdump or do the reverse. This tool contains an interesting option `-i` which | ||
| 85 | allows us to output the file in C include file style. And this is exactly what | ||
| 86 | we need. | ||
| 87 | |||
| 88 | Modified `Makefile` now looks like. | ||
| 89 | |||
| 90 | ```make | ||
| 91 | RAYLIB_VER := 5.5_linux_amd64 | ||
| 92 | CC ?= cc | ||
| 93 | CFLAGS := -Wall -Wextra -Wunused -Wno-unused-parameter -Wswitch-enum -Wpedantic -Wno-bitwise-instead-of-logical | ||
| 94 | LDFLAGS := -I./libs -I./libs/raylib-$(RAYLIB_VER)/include ./libs/raylib-$(RAYLIB_VER)/lib/libraylib.a -lm | ||
| 95 | |||
| 96 | game: | ||
| 97 | $(CC) $(CFLAGS) -o game main.c $(LDFLAGS) | ||
| 98 | |||
| 99 | embed: | ||
| 100 | xxd -i data/armor.png > data/armor.h | ||
| 101 | xxd -i data/dejavusans-mono.ttf > data/dejavusans-mono.h | ||
| 102 | ``` | ||
| 103 | |||
| 104 | This converted binary data files into C header style files which contain the | ||
| 105 | array of bytes and the size of the array of bytes. This will be useful later | ||
| 106 | with raylib code. | ||
| 107 | |||
| 108 | An example of such a file (in our case armor.png) looks like this. | ||
| 109 | |||
| 110 | ```c | ||
| 111 | // data/armor.h | ||
| 112 | unsigned char data_armor_png[] = { | ||
| 113 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, | ||
| 114 | 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x01, 0x40, | ||
| 115 | 0x08, 0x06, 0x00, 0x00, 0x00, 0xcd, 0x90, 0xa5, 0xaa, 0x00, 0x00, 0x00, | ||
| 116 | 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, | ||
| 117 | 0x00, 0x06, 0x62, 0x4b, 0x47, 0x44, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, | ||
| 118 | 0xa0, 0xbd, 0xa7, 0x93, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, | ||
| 119 | 0x00, 0x00, 0x17, 0x12, 0x00, 0x00, 0x17, 0x12, 0x01, 0x67, 0x9f, 0xd2, | ||
| 120 | 0x52, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xdc, 0x0c, | ||
| 121 | 0x1b, 0x07, 0x30, 0x17, 0xb2, 0x1a, 0xee, 0xda, 0x00, 0x00, 0x20, 0x00, | ||
| 122 | ... | ||
| 123 | 0x81, 0x98, 0xa6, 0xb9, 0x2d, 0x37, 0xac, 0x6d, 0x57, 0xb0, 0xed, 0xea, | ||
| 124 | 0x86, 0xac, 0xa1, 0xa6, 0x6b, 0xf8, 0x54, 0x1f, 0x8e, 0xe3, 0x90, 0xcb, | ||
| 125 | 0xe6, 0x36, 0x7d, 0x4e, 0x6b, 0xab, 0xcb, 0x78, 0xbd, 0x5e, 0x6c, 0xc7, | ||
| 126 | 0x01, 0x8f, 0xa7, 0x5e, 0x46, 0x22, 0x37, 0x17, 0x76, 0x13, 0x4d, 0x6c, | ||
| 127 | 0xab, 0x0c, 0xb0, 0x89, 0x26, 0x9a, 0x68, 0xe2, 0x45, 0xa3, 0x99, 0x2b, | ||
| 128 | 0x34, 0xd1, 0x44, 0x13, 0xff, 0xb7, 0xf8, 0x27, 0x9b, 0x96, 0x5c, 0x0d, | ||
| 129 | 0x8b, 0xbb, 0x21, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, | ||
| 130 | 0xae, 0x42, 0x60, 0x82 | ||
| 131 | }; | ||
| 132 | unsigned int data_armor_png_len = 118324; | ||
| 133 | ``` | ||
| 134 | |||
| 135 | ## Embedding and compiling | ||
| 136 | |||
| 137 | Now it's time to include this files in our `main.c` code and use them. raylib's | ||
| 138 | API fits perfectly with this style of converting binary files. | ||
| 139 | |||
| 140 | ```c | ||
| 141 | #include <stdio.h> | ||
| 142 | #include "raylib.h" | ||
| 143 | |||
| 144 | #include "data/armor.h" | ||
| 145 | #include "data/dejavusans-mono.h" | ||
| 146 | |||
| 147 | #define FONT_SIZE 24 | ||
| 148 | |||
| 149 | int main(void) { | ||
| 150 | SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_WINDOW_HIGHDPI); | ||
| 151 | InitWindow(900, 400, "Embedding assets"); | ||
| 152 | SetTargetFPS(60); | ||
| 153 | |||
| 154 | // Load font from memory. | ||
| 155 | Font font = LoadFontFromMemory(".ttf", data_dejavusans_mono_ttf, data_dejavusans_mono_ttf_len, FONT_SIZE, NULL, 0); | ||
| 156 | SetTextureFilter(font.texture, TEXTURE_FILTER_TRILINEAR); // Font antialising. | ||
| 157 | |||
| 158 | // Load image from memory and create texture from it. | ||
| 159 | Image armor = LoadImageFromMemory(".png", data_armor_png, data_armor_png_len); | ||
| 160 | Texture2D armor_texture = LoadTextureFromImage(armor); | ||
| 161 | UnloadImage(armor); | ||
| 162 | |||
| 163 | while (!WindowShouldClose()) { | ||
| 164 | BeginDrawing(); | ||
| 165 | ClearBackground(BLACK); | ||
| 166 | |||
| 167 | // Draw the armor texture. | ||
| 168 | DrawTexture(armor_texture, 20, 20, WHITE); | ||
| 169 | |||
| 170 | // Draw some text on the screen. | ||
| 171 | DrawTextEx(font, "Hello embedded assets.", (Vector2){ 400, 20 }, FONT_SIZE, 0, WHITE); | ||
| 172 | DrawTextEx(font, "This is example how we can use embedded fonts.", (Vector2){ 400, 50 }, FONT_SIZE - 4, 0, WHITE); | ||
| 173 | |||
| 174 | EndDrawing(); | ||
| 175 | } | ||
| 176 | |||
| 177 | UnloadTexture(armor_texture); | ||
| 178 | CloseWindow(); | ||
| 179 | return 0; | ||
| 180 | } | ||
| 181 | ``` | ||
| 182 | |||
| 183 | This makes embedding assets quite straightforward. All we need to do is include | ||
| 184 | header files and then provide byte arrays to the appropriate functions. Like I | ||
| 185 | said raylib has a bunch of `FromMemory` functions that make this quite easy. | ||
| 186 | |||
| 187 | This produces a single binary `game` that includes both assets, so there is no | ||
| 188 | need to also copy over these assets. Mind you, these binaries can potentially | ||
| 189 | get quite big and if that is the problem, you could always compile this into a | ||
| 190 | `so` library and include that. This way you could create data packs for audio, | ||
| 191 | graphics, etc. and ship that alongside your game binary. | ||
| 192 | |||
| 193 |  | ||
| 194 | |||
| 195 | ## Honorable mention: C23-embed-directive | ||
| 196 | |||
| 197 | `#embed` is a preprocessor directive to include (binary) resources in the | ||
| 198 | build, where a resource is defined as a source of data accessible from the | ||
| 199 | translation environment. This has been introduces with the C23 standard so it's | ||
| 200 | quite new. | ||
| 201 | |||
| 202 | Speaking plainly, `#embed` allows inclusion of binary data in a program | ||
| 203 | executable image, as arrays of unsigned char or other types, without the need | ||
| 204 | for an external script run from a Makefile. | ||
| 205 | |||
| 206 | - I do like this approach, but some compilers might not support this feature, | ||
| 207 | and this is why I will be sticking with manual approach for now. | ||
| 208 | - One additional drawback is that every time you compile your game, all the | ||
| 209 | assets need to be re-read and converted. I have not heavily tested this, but | ||
| 210 | this could be a potential problem when you have a lot of assets. | ||
| 211 | - Not using Make could be detrimental to incremental builds. But this would | ||
| 212 | also need to be tested. I do not claim that this is a real problem. | ||
| 213 | |||
| 214 | Read more about [Binary resource | ||
| 215 | inclusion](https://en.cppreference.com/w/c/preprocessor/embed). | ||
| 216 | |||
| 217 | Below is a quick example how to use this new directive. | ||
| 218 | |||
| 219 | ```c | ||
| 220 | #include <stdint.h> | ||
| 221 | #include <stdio.h> | ||
| 222 | |||
| 223 | const uint8_t image_data[] = { | ||
| 224 | #embed "image.png" | ||
| 225 | }; | ||
| 226 | |||
| 227 | int main(void) { | ||
| 228 | printf("Image size: %d bytes\n", sizeof(image_data)); | ||
| 229 | return 0; | ||
| 230 | } | ||
| 231 | ``` | ||
| 232 | |||
| 233 | ## Credits | ||
| 234 | |||
| 235 | - https://opengameart.org/content/armor-icons-by-equipment-slot-with-transparency | ||
| 236 | - https://dejavu-fonts.github.io/ | ||
