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