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