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