A game engine for top-down 2D RPG games.
rpg game-engine raylib c99
1#include <keraforge.h> 2#include <raylib.h> 3#include <raymath.h> 4#include <stdio.h> 5#include <stdlib.h> 6 7 8static Camera2D cam; 9static struct kf_vec2(u32) select = { 0, 0 }; 10static int selected_tile = 0; 11static enum { 12 menu_none, 13 menu_palette, 14} menu; 15static int target_fps = 60; 16static enum { 17 modal_play, 18 modal_edit, 19} modal; 20static char *modals[] = { "play", "edit" }; 21static bool dirty = false; 22static bool preserve_mapdotbin = false; 23 24static kf_inputbind_t 25 inputbind_move_up, 26 inputbind_move_down, 27 inputbind_move_left, 28 inputbind_move_right, 29 inputbind_run, 30 inputbind_ui_up, 31 inputbind_ui_down, 32 inputbind_ui_left, 33 inputbind_ui_right, 34 inputbind_select, 35 inputbind_cancel, 36 inputbind_pause, 37 inputbind_palette, 38 inputbind_zoom_reset, 39 inputbind_zoom_in, 40 inputbind_zoom_out, 41 inputbind_toggle_fps_limit, 42 inputbind_toggle_editor 43; 44 45 46static 47void loadbinds() 48{ 49 inputbind_move_up = kf_addinput("move_up", KEY_W, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y); 50 inputbind_move_down = kf_addinput("move_down", KEY_S, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y); 51 inputbind_move_left = kf_addinput("move_left", KEY_A, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X); 52 inputbind_move_right = kf_addinput("move_right", KEY_D, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X); 53 inputbind_run = kf_addinput("run", KEY_LEFT_SHIFT, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, GAMEPAD_AXIS_UNKNOWN); 54 55 inputbind_ui_up = kf_addinput("ui_up", KEY_W, KEY_UP, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_UP, GAMEPAD_AXIS_UNKNOWN); 56 inputbind_ui_down = kf_addinput("ui_down", KEY_S, KEY_DOWN, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_DOWN, GAMEPAD_AXIS_UNKNOWN); 57 inputbind_ui_left = kf_addinput("ui_left", KEY_A, KEY_LEFT, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_LEFT, GAMEPAD_AXIS_UNKNOWN); 58 inputbind_ui_right = kf_addinput("ui_right", KEY_D, KEY_RIGHT, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_LEFT_FACE_RIGHT, GAMEPAD_AXIS_UNKNOWN); 59 60 inputbind_select = kf_addinput("select", KEY_E, KEY_ENTER, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_DOWN, GAMEPAD_AXIS_UNKNOWN); 61 inputbind_cancel = kf_addinput("cancel", KEY_Q, KEY_ESCAPE, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, GAMEPAD_AXIS_UNKNOWN); 62 63 inputbind_pause = kf_addinput("pause", KEY_ESCAPE, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_MIDDLE_RIGHT, GAMEPAD_AXIS_UNKNOWN); 64 inputbind_palette = kf_addinput("palette", KEY_TAB, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_UP, GAMEPAD_AXIS_UNKNOWN); 65 66 inputbind_zoom_reset = kf_addinput("zoom_reset", KEY_ZERO, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 67 inputbind_zoom_in = kf_addinput("zoom_in", KEY_EQUAL, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 68 inputbind_zoom_out = kf_addinput("zoom_out", KEY_MINUS, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 69 70 inputbind_toggle_fps_limit = kf_addinput("toggle_fps_limit", KEY_NINE, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 71 inputbind_toggle_editor = kf_addinput("toggle_editor", KEY_EIGHT, KEY_NULL, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 72} 73 74static 75void setmenu(int m) 76{ 77 menu = m; 78} 79 80static 81void _player_tick_move(struct kf_actor *self) 82{ 83 struct kf_vec2(f32) v = {0, 0}; 84 85 /* gamepad axis movement */ 86 f32 gpx = kf_getgamepadaxis(inputbind_move_left); 87 f32 gpy = kf_getgamepadaxis(inputbind_move_up); 88 if (gpx > kf_deadzone || gpx < -kf_deadzone || gpy > kf_deadzone || gpy < -kf_deadzone) 89 { 90 v.y = gpy; 91 v.x = gpx; 92 93 f32 angle = Vector2LineAngle(Vector2Zero(), (Vector2){gpx, gpy}) * RAD2DEG; 94 angle /= 90; 95 switch ((int)roundf(angle)) 96 { 97 case 0: self->pointing = kf_east; break; 98 case 1: self->pointing = kf_north; break; 99 case -2: /* fallthrough */ 100 case 2: self->pointing = kf_west; break; 101 case -1: self->pointing = kf_south; break; 102 } 103 104 goto done; 105 } 106 107 /* non-axis movement */ 108 bool w = kf_checkinputdown(inputbind_move_up); 109 bool s = kf_checkinputdown(inputbind_move_down); 110 bool a = kf_checkinputdown(inputbind_move_left); 111 bool d = kf_checkinputdown(inputbind_move_right); 112 113 if (a && d) { v.x = 0; } 114 else if (a) { v.x = -1; self->pointing = kf_west; } 115 else if (d) { v.x = 1; self->pointing = kf_east; } 116 117 if (w && s) { v.y = 0; } 118 else if (w) { v.y = -1; self->pointing = kf_north; } 119 else if (s) { v.y = 1; self->pointing = kf_south; } 120 121 v = kf_normalize_vec2(f32)(v); 122 123done: 124 if (v.x || v.y) 125 kf_actor_addforce(self, v); 126} 127 128static 129void _player_tick(struct kf_world *world, struct kf_actor *self) 130{ 131 if (menu == menu_none) 132 { 133 _player_tick_move(self); 134 135 if (kf_checkinputpress(inputbind_run)) 136 { 137 self->running = true; 138 self->speedmod = 1.5; 139 } 140 else if (kf_checkinputrelease(inputbind_run)) 141 { 142 self->running = false; 143 self->speedmod = 1; 144 } 145 } 146 147 kf_actor_move(world, self, kf_dts); 148} 149 150static 151void _player_draw(struct kf_world *world, struct kf_actor *self) 152{ 153 (void)world; 154 kf_actor_draw(self); 155 156 cam.target.x = self->pos.x + (self->size.x / 2); 157 cam.target.y = self->pos.y + (self->size.y / 2); 158} 159 160static 161void draw_palette(int *selected) 162{ 163 int px = 80, py = 80; 164 DrawRectangle(px, py, 400, 400, BLACK); 165 DrawText("tiles :3", px + 10, py + 10, 20, WHITE); 166 py += 40; 167 int x = 0, y = 0; 168 int s = KF_TILE_SIZE_PX * 2; 169 for (int i = 1 ; i <= kf_tiles.count ; i++) 170 { 171 Rectangle r = {px + x*s, py + y*s, s, s}; 172 kf_drawsprite_wh(kf_tiles.sheet[i], r.x, r.y, r.width, r.height, kf_tiles.sprite[i].x + 0, kf_tiles.sprite[i].y + 3); 173 174 if (*selected == i) 175 DrawRectangleLinesEx(r, 1, GOLD); 176 177 if (CheckCollisionPointRec(GetMousePosition(), r)) 178 { 179 DrawRectangleLinesEx(r, 1, WHITE); 180 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) 181 *selected = i; 182 } 183 184 x += 1; 185 if (x >= 8) 186 { 187 x = 0; 188 y++; 189 } 190 } 191 192 if (kf_checkinputpress(inputbind_cancel)) 193 setmenu(menu_none); 194}; 195 196 197int main(int argc, const char *argv[]) 198{ 199 (void)argc; 200 (void)argv; 201 202 SetTraceLogLevel(LOG_WARNING); 203 InitWindow(800, 600, "Keraforge"); 204 SetTargetFPS(target_fps); 205 SetExitKey(KEY_NULL); 206 207 loadbinds(); 208 209 struct kf_spritesheet terrain = kf_loadspritesheet("data/res/img/tile/terrain.png", 16, 16); 210 KF_ADDTILE( 211 .key = "grass", 212 .mapcol = GREEN, 213 .sheet = &terrain, 214 .sprite = {0, 0}, 215 ); 216 KF_ADDTILE( 217 .key = "sand", 218 .mapcol = YELLOW, 219 .sheet = &terrain, 220 .sprite = {4, 0}, 221 ); 222 KF_ADDTILE( 223 .key = "stone", 224 .mapcol = GRAY, 225 .sheet = &terrain, 226 .sprite = {0, 4}, 227 ); 228 KF_ADDTILE( 229 .key = "debug", 230 .mapcol = BLUE, 231 .sheet = &terrain, 232 .sprite = {4, 4}, 233 ); 234 KF_ADDTILE( 235 .key = "brick", 236 .mapcol = RED, 237 .sheet = &terrain, 238 .sprite = {8, 0}, 239 .collide = true, 240 ); 241 KF_ADDTILE( 242 .key = "ice", 243 .mapcol = BLUE, 244 .sheet = &terrain, 245 .sprite = {8, 4}, 246 .transparent = true, 247 ); 248 KF_ADDTILE( 249 .key = "dirt", 250 .mapcol = BROWN, 251 .sheet = &terrain, 252 .sprite = {12, 0}, 253 ); 254 kf_logdbg("loaded %d tiles", kf_tiles.count); 255 256 struct kf_uiconfig *uiconfig = kf_ui_getconfig(); 257 uiconfig->select = inputbind_select; 258 uiconfig->cancel = inputbind_cancel; 259 uiconfig->up = inputbind_ui_up; 260 uiconfig->down = inputbind_ui_down; 261 262 if (!DirectoryExists("data")) 263 MakeDirectory("data"); 264 265 struct kf_world *world = NULL; 266 kf_world_load(&world, true); 267 if (!world) 268 KF_THROW("failed to load world: %p", world); 269 270 struct kf_actor *player = kf_actor_new(kf_actor_loadspritesheet("data/res/img/char/template.png"), 10, 10, true); 271 player->sizeoffset.y = 6; 272 player->pos.x = world->width / 4.0f * KF_TILE_SIZE_PX; 273 player->pos.y = world->height / 4.0f * KF_TILE_SIZE_PX; 274 player->tick = _player_tick; 275 player->draw = _player_draw; 276 player->controlled = true; 277 278 cam = (Camera2D){0}; 279 cam.offset.x = GetScreenWidth() / 2.0f; 280 cam.offset.y = GetScreenHeight() / 2.0f; 281 cam.zoom = 2; 282 283 int running = 1; 284 while (!WindowShouldClose() && running) 285 { 286 if (IsWindowResized()) 287 { 288 cam.offset.x = GetScreenWidth() / 2.0f; 289 cam.offset.y = GetScreenHeight() / 2.0f; 290 } 291 292 player->tick(world, player); 293 294 Vector2 v = GetScreenToWorld2D(GetMousePosition(), cam); 295 select.x = v.x / KF_TILE_SIZE_PX; 296 select.y = v.y / KF_TILE_SIZE_PX; 297 298 if (kf_checkinputpress(inputbind_palette) && modal == modal_edit) 299 setmenu(menu_palette); 300 else if (kf_checkinputpress(inputbind_cancel) && menu == menu_none) 301 running = 0; 302 else if (kf_checkinputpress(inputbind_zoom_reset)) 303 cam.zoom = 2; 304 else if (kf_checkinputpress(inputbind_zoom_in) && cam.zoom < 3.50f) 305 cam.zoom += 0.25f; 306 else if (kf_checkinputpress(inputbind_zoom_out) && cam.zoom > 1.00f) 307 cam.zoom -= 0.25f; 308 else if (kf_checkinputpress(inputbind_toggle_fps_limit)) 309 { 310 target_fps = target_fps <= 0 ? 60 : 0; 311 SetTargetFPS(target_fps); 312 } 313 else if (kf_checkinputpress(inputbind_toggle_editor)) 314 { 315 if (modal == modal_edit) 316 modal = modal_play; 317 else 318 { 319 modal = modal_edit; 320 dirty = true; 321 } 322 } 323 324 BeginDrawing(); 325 ClearBackground(BLACK); 326 327 BeginMode2D(cam); 328 kf_world_draw(world, cam); 329 // kf_world_drawcolliders(world, player, cam); 330 if (modal == modal_edit && menu == menu_none && select.x < world->width && select.y < world->height) 331 { 332 struct kf_tile *t = kf_world_gettile(world, select.x, select.y); 333 if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) 334 { 335 t->id = (kf_tileid_t)selected_tile; 336 kf_world_updatetile(world, select.x, select.y, true); 337 } 338 else if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) 339 t->subid = (kf_tileid_t)selected_tile; 340 else if (IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE)) 341 selected_tile = t->id; 342 343 DrawRectangleLines(select.x * KF_TILE_SIZE_PX, select.y * KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, WHITE); 344 DrawText(TextFormat("%d [%d] (%d,%d)", t->id, t->data, select.x, select.y), select.x * KF_TILE_SIZE_PX, select.y * KF_TILE_SIZE_PX - 10, 10, BLACK); 345 } 346 player->draw(world, player); 347 EndMode2D(); 348 349 switch (menu) 350 { 351 case menu_none: 352 break; 353 case menu_palette: 354 draw_palette(&selected_tile); 355 break; 356 } 357 358 DrawFPS(0, 0); 359 DrawText(TextFormat("%f", kf_dts), 0, 20, 20, RED); 360 DrawText(TextFormat("%f", kf_s), 0, 40, 20, RED); 361 DrawText(TextFormat("%s", modals[modal]), 0, 60, 20, ORANGE); 362 363 EndDrawing(); 364 365 kf_frame++; 366 kf_dts = GetFrameTime(); 367 kf_dtms = kf_dts * 1000; 368 kf_s += kf_dts; 369 } 370 371 if (world) 372 { 373 if (dirty) 374 { 375 if (!kf_writebin("data/tmp/map.bin", (u8 *)world, kf_world_getsize(world))) 376 KF_THROW("failed to save map.bin"); 377 if (!kf_compress("data/tmp/map.bin", "data/map.bin.xz")) 378 KF_THROW("failed to compress map.bin into map.bin.xz"); 379 if (!preserve_mapdotbin) 380 remove("data/tmp/map.bin"); 381 } 382 free(world); 383 } 384 CloseWindow(); 385 386 return 0; 387}