A game engine for top-down 2D RPG games.
rpg
game-engine
raylib
c99
1#include "keraforge/bini.h"
2#include <keraforge.h>
3#include <raylib.h>
4#include <stdio.h>
5#include <stdlib.h>
6#include <string.h>
7#include <math.h>
8
9
10struct _kf_tiles kf_tiles = {0};
11
12
13static inline
14void _kf_updatetilebitmask(struct kf_world *world, u32 x, u32 y);
15
16
17kf_tileid_t kf_addtile(struct kf_tile_opts opts)
18{
19 KF_SANITY_CHECK(opts.key != NULL, "tile added without key");
20 KF_SANITY_CHECK(opts.sheet != NULL, "tile added without sheet");
21
22 kf_tileid_t id = ++kf_tiles.count;
23
24 kf_tiles.key[id] = opts.key;
25 kf_tiles.mapcol[id] = opts.mapcol;
26 kf_tiles.sheet[id] = opts.sheet;
27 kf_tiles.sprite[id] = opts.sprite;
28 kf_tiles.collide[id] = opts.collide;
29 kf_tiles.transparent[id] = opts.transparent;
30
31 return id;
32}
33
34struct kf_world *kf_world_new(u32 width, u32 height, kf_tileid_t fill)
35{
36 const size_t len = sizeof(struct kf_world) + sizeof(struct kf_tile)*width*height;
37 struct kf_world *world = malloc(len);
38 kf_loginfo("creating world: %lu bytes: %p", len, world);
39 world->revision = 0;
40 world->width = width;
41 world->height = height;
42 memset(world->map, 0, sizeof(struct kf_tile)*width*height);
43 for (size_t i = 0 ; i < width*height ; i++)
44 {
45 world->map[i].subid = fill;
46 world->map[i].id = fill;
47 }
48
49 u32 x;
50 for (u32 y = 0 ; y < height ; y++)
51 {
52 for (x = 0 ; x < width ; x++)
53 {
54 _kf_updatetilebitmask(world, x, y);
55 }
56 }
57
58 return world;
59}
60
61size_t kf_world_getsize(struct kf_world *world)
62{
63 return sizeof(struct kf_world) + sizeof(struct kf_tile)*world->width*world->height;
64}
65
66static inline
67void _kf_updatetilebitmask(struct kf_world *world, u32 x, u32 y)
68{
69 struct kf_tile *t = kf_world_gettile(world, x, y);
70 kf_tileid_t
71 n = y-1 >= world->height ? 0 : kf_world_gettile(world, x, y-1)->id,
72 w = x-1 >= world->width ? 0 : kf_world_gettile(world, x-1, y)->id,
73 e = x+1 >= world->width ? 0 : kf_world_gettile(world, x+1, y)->id,
74 s = y+1 >= world->height ? 0 : kf_world_gettile(world, x, y+1)->id;
75 t->data = 0b0000;
76 if (t->id == n) t->data |= KF_TILEMASK_NORTH;
77 if (t->id == w) t->data |= KF_TILEMASK_WEST;
78 if (t->id == e) t->data |= KF_TILEMASK_EAST;
79 if (t->id == s) t->data |= KF_TILEMASK_SOUTH;
80}
81
82void kf_world_updatetile(struct kf_world *world, u32 x, u32 y, bool update_neighbours)
83{
84 _kf_updatetilebitmask(world, x, y);
85 if (update_neighbours)
86 {
87 if (y-1 < world->height) kf_world_updatetile(world, x, y-1, false);
88 if (x-1 < world->width) kf_world_updatetile(world, x-1, y, false);
89 if (x+1 < world->width) kf_world_updatetile(world, x+1, y, false);
90 if (y+1 < world->height) kf_world_updatetile(world, x, y+1, false);
91 }
92}
93
94inline
95struct kf_vec2(u32) kf_getspritefordatum(kf_tiledatum_t t)
96{
97 static const
98 struct kf_vec2(u32) v[] = {
99 {0, 3}, /* 0b0000: */
100 {0, 2}, /* 0b0001: N */
101 {3, 3}, /* 0b0010: W */
102 {3, 2}, /* 0b0011: NW */
103 {1, 3}, /* 0b0100: E */
104 {1, 2}, /* 0b0101: EN */
105 {2, 3}, /* 0b0110: EW */
106 {2, 2}, /* 0b0111: EWN */
107 {0, 0}, /* 0b1000: S */
108 {0, 1}, /* 0b1001: SN */
109 {3, 0}, /* 0b1010: SW */
110 {3, 1}, /* 0b1011: SWN */
111 {1, 0}, /* 0b1100: SE */
112 {1, 1}, /* 0b1101: SEN */
113 {2, 0}, /* 0b1110: SEW */
114 {2, 1}, /* 0b1111: NESW */
115 };
116 return v[t];
117}
118
119struct kf_tile *kf_world_gettile(struct kf_world *world, u32 x, u32 y)
120{
121 return &world->map[y*world->width + x];
122}
123
124void kf_world_drawcolliders(struct kf_world *world, struct kf_actor *player, Camera2D camera)
125{
126 Rectangle r = {
127 player->pos.x + (player->size.x / 2) + player->sizeoffset.x,
128 player->pos.y + (player->size.y / 2) + player->sizeoffset.y,
129 player->size.x,
130 player->size.y
131 };
132
133 DrawRectangleLinesEx(r, 1, BLUE);
134
135 const Vector2 start = GetScreenToWorld2D((Vector2){0, 0}, camera);
136 const Vector2 end = GetScreenToWorld2D((Vector2){GetScreenWidth(), GetScreenHeight()}, camera);
137 const u32 sx = fmax(0, floorf(start.x / KF_TILE_SIZE_PX));
138 const u32 sy = fmax(0, floorf(start.y / KF_TILE_SIZE_PX));
139 const u32 ex = fmin(world->width, ceilf(end.x / KF_TILE_SIZE_PX));
140 const u32 ey = fmin(world->height, ceilf(end.y / KF_TILE_SIZE_PX));
141 const size_t down = world->width - ex + sx - 1; /* number of indexes to add to reach the next tile down */
142 struct kf_tile *tile = kf_world_gettile(world, sx, sy);
143
144 /* check if any tiles will collide with the actor's rect */
145 const f32 trx = sx * KF_TILE_SIZE_PX;
146 Rectangle tr = {
147 trx,
148 sy * KF_TILE_SIZE_PX,
149 KF_TILE_SIZE_PX,
150 KF_TILE_SIZE_PX,
151 }; /* tile rect */
152 u32 x;
153
154 for (u32 y = sy ; y <= ey ; y++)
155 {
156 for (x = sx ; x <= ex ; x++)
157 {
158 if (kf_tiles.collide[tile->id])
159 DrawRectangleLinesEx(tr, 1, CheckCollisionRecs(r, tr) ? RED : BLACK);
160
161 tile++; /* shift tile pointer to the right */
162 tr.x += KF_TILE_SIZE_PX;
163 }
164 tile += down; /* shift tile pointer down */
165 tr.x = trx;
166 tr.y += KF_TILE_SIZE_PX;
167 }
168}
169
170void kf_world_draw(struct kf_world *world, Camera2D camera)
171{
172 const Vector2 start = GetScreenToWorld2D((Vector2){0, 0}, camera);
173 const Vector2 end = GetScreenToWorld2D((Vector2){GetScreenWidth(), GetScreenHeight()}, camera);
174 const u32 sx = fmax(0, floorf(start.x / KF_TILE_SIZE_PX));
175 const u32 sy = fmax(0, floorf(start.y / KF_TILE_SIZE_PX));
176 const u32 ex = fmin(world->width, ceilf(end.x / KF_TILE_SIZE_PX));
177 const u32 ey = fmin(world->height, ceilf(end.y / KF_TILE_SIZE_PX));
178 const size_t down = world->width - ex + sx; /* number of indexes to add to reach the next tile down */
179 struct kf_tile *tile = kf_world_gettile(world, sx, sy);
180 u32 x;
181 for (u32 y = sy ; y < ey ; y++)
182 {
183 for (x = sx ; x < ex ; x++)
184 {
185 KF_SANITY_CHECK(tile->subid <= kf_tiles.count, "erroneous subtile on map at %u,%u: %u (count=%u)", x, y, tile->subid, kf_tiles.count);
186 KF_SANITY_CHECK(tile->id <= kf_tiles.count, "erroneous tile on map at %u,%u: %u (count=%u)", x, y, tile->id, kf_tiles.count);
187
188 /* 15: full tile, no subtile rendering needed (unless transparent) */
189 if ((tile->data != 15 || kf_tiles.transparent[tile->id]) && tile->subid && tile->id)
190 {
191 kf_drawsprite_wh(
192 kf_tiles.sheet[tile->subid],
193 x*KF_TILE_SIZE_PX,
194 y*KF_TILE_SIZE_PX,
195 KF_TILE_SIZE_PX,
196 KF_TILE_SIZE_PX,
197 kf_tiles.sprite[tile->subid].x + 2, /* 2,1 are offsets for the "middle" tile */
198 kf_tiles.sprite[tile->subid].y + 1
199 );
200 }
201
202 if (tile->id)
203 {
204 struct kf_vec2(u32) s = kf_getspritefordatum(tile->data);
205 kf_drawsprite_wh(
206 kf_tiles.sheet[tile->id],
207 x*KF_TILE_SIZE_PX,
208 y*KF_TILE_SIZE_PX,
209 KF_TILE_SIZE_PX,
210 KF_TILE_SIZE_PX,
211 kf_tiles.sprite[tile->id].x + s.x,
212 kf_tiles.sprite[tile->id].y + s.y
213 );
214 }
215
216 tile++; /* shift tile pointer to the right */
217 }
218
219 tile += down; /* shift tile pointer down */
220 }
221}
222
223#define _KF_MAPFILE_TMP "data/tmp/map.bin"
224#define _KF_MAPFILE_XZ "data/map.bin.xz"
225#define _KF_MAPFILE "data/map.bin"
226
227static
228void _kf_world_save_bs(struct kf_world *world, struct bini_stream *bs)
229{
230 bini_wiu(bs, world->revision);
231 bini_wiu(bs, world->width);
232 bini_wiu(bs, world->height);
233 struct kf_tile *t = &world->map[0];
234 for (size_t i = 0 ; i < world->width*world->height ; i++)
235 {
236 bini_wsu(bs, t->subid);
237 bini_wsu(bs, t->id);
238 bini_wcu(bs, t->data);
239 t++;
240 }
241}
242
243int kf_world_save(struct kf_world *world, bool compress, char *outfile)
244{
245 outfile = outfile ? outfile : (compress ? _KF_MAPFILE_TMP : _KF_MAPFILE);
246 struct bini_stream *bs = bini_new();
247 _kf_world_save_bs(world, bs);
248 if (!kf_writebin(outfile, bs->buffer, bs->len))
249 KF_THROW("failed to open %s", outfile);
250 bini_close(bs);
251
252 if (compress)
253 {
254 if (!kf_compress(_KF_MAPFILE_TMP, _KF_MAPFILE_XZ))
255 {
256 KF_THROW("failed to compress %s", _KF_MAPFILE_XZ);
257 return 0; /* unreachable */
258 }
259 /* we don't need this anymore, might as well toss it to save file storage. */
260 remove(_KF_MAPFILE_TMP);
261 }
262
263 return 1;
264}
265
266static
267void _kf_world_load_bs(struct kf_world **pworld, struct bini_stream *bs)
268{
269 u32 r = bini_riu(bs);
270 u32 w = bini_riu(bs);
271 u32 h = bini_riu(bs);
272 struct kf_world *world = malloc(sizeof(*world) + sizeof(struct kf_tile)*w*h);
273 world->revision = r;
274 world->width = w;
275 world->height = h;
276 struct kf_tile *t = &world->map[0];
277 for (size_t i = 0 ; i < world->width*world->height ; i++)
278 {
279 t->subid = bini_rsu(bs);
280 t->id = bini_rsu(bs);
281 t->data = bini_rcu(bs);
282 t++;
283 }
284 *pworld = world;
285}
286
287int kf_world_load(struct kf_world **pworld, bool compressed, char *infile)
288{
289 if (compressed) /* decompress before loading */
290 {
291 if (!kf_exists(_KF_MAPFILE_XZ))
292 {
293 KF_THROW("no such file: %s", _KF_MAPFILE_XZ);
294 return 0; /* unreachable */
295 }
296
297 kf_logdbg("decompressing %s to %s", _KF_MAPFILE_XZ, _KF_MAPFILE_TMP);
298 if (!kf_decompress(_KF_MAPFILE_XZ, _KF_MAPFILE_TMP))
299 {
300 KF_THROW("failed to decompress %s", _KF_MAPFILE_XZ);
301 return 0; /* unreachable */
302 }
303 }
304
305 infile = infile ? infile : (compressed ? _KF_MAPFILE_TMP : _KF_MAPFILE);
306 kf_logdbg("loading world: %s", infile);
307
308 size_t len = 0;
309 struct bini_stream bs = {
310 .mode = BINI_STREAM,
311 .buffer = kf_readbin(infile, &len),
312 };
313 if (!bs.buffer)
314 KF_THROW("failed to read/open %s", infile);
315 bs.cap = len;
316 bs.len = len;
317 kf_logdbg("loaded world into binary stream: len=%lu", len);
318 _kf_world_load_bs(pworld, &bs);
319 free(bs.buffer);
320 kf_logdbg("loaded world (%p): r=%d, wh=%dx%d, len=%lu", *pworld, (*pworld)->revision, (*pworld)->width, (*pworld)->height, len);
321
322 if (compressed)
323 {
324 /* we don't need this anymore, might as well toss it to save file storage. */
325 remove(_KF_MAPFILE_TMP);
326 }
327
328 if (!*pworld)
329 {
330 KF_THROW("failed to load world");
331 return 0; /* unreachable */
332 }
333
334 return 1;
335}
336