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