A game engine for top-down 2D RPG games.
rpg game-engine raylib c99

basic ui system, add license, readme, todo

Changed files
+329 -30
include
scripts
src
+1
include/keraforge.h
··· 5 5 #include <keraforge/actor.h> 6 6 #include <keraforge/fs.h> 7 7 #include <keraforge/input.h> 8 + #include <keraforge/ui.h> 8 9 #include <keraforge/math.h> 9 10 #include <keraforge/world.h> 10 11
+2 -2
include/keraforge/input.h
··· 9 9 10 10 #define KF_INPUTBIND_MAX UINT8_MAX 11 11 typedef u8 kf_inputbind_t; 12 - extern const kf_inputbind_t kf_inputbind_none; 12 + #define KF_INPUTBIND_NONE ((kf_inputbind_t)0) 13 13 14 14 struct _kf_inputbinds 15 15 { 16 - kf_inputbind_t count; 16 + kf_inputbind_t count; /* must start at 1. 0 is the `none` keybind. */ 17 17 char *id[KF_INPUTBIND_MAX]; 18 18 KeyboardKey key[KF_INPUTBIND_MAX]; 19 19 MouseButton mouse[KF_INPUTBIND_MAX];
+24
include/keraforge/ui.h
··· 1 + #ifndef __kf_ui__ 2 + #define __kf_ui__ 3 + 4 + #include <keraforge/input.h> 5 + 6 + struct kf_uiconfig 7 + { 8 + kf_inputbind_t select, cancel, up, down; 9 + char *fmt, *selectfmt; 10 + int fontsize; 11 + Color bg, fg; 12 + int panelheight; 13 + int xpadding, ypadding; 14 + }; 15 + 16 + struct kf_uiconfig *kf_ui_getconfig(void); 17 + 18 + int kf_ui_panel(char *title); 19 + 20 + int kf_ui_choice(char *title, char **choices, int nchoices, int *choice); 21 + int kf_ui_yesno(char *title, int *choice); 22 + int kf_ui_textinput(char *title, char *text); 23 + 24 + #endif
+47
license
··· 1 + === Keraforge - BSD 3-Clause === 2 + 3 + Copyright 2025 Emmeline Coats 4 + 5 + Redistribution and use in source and binary forms, with or without 6 + modification, are permitted provided that the following conditions are met: 7 + 8 + 1. Redistributions of source code must retain the above copyright notice, this 9 + list of conditions and the following disclaimer. 10 + 11 + 2. Redistributions in binary form must reproduce the above copyright notice, 12 + this list of conditions and the following disclaimer in the documentation 13 + and/or other materials provided with the distribution. 14 + 15 + 3. Neither the name of the copyright holder nor the names of its contributors 16 + may be used to endorse or promote products derived from this software 17 + without specific prior written permission. 18 + 19 + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 20 + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 + 30 + === Raylib - zlib === 31 + 32 + Copyright (c) 2013-2025 Ramon Santamaria (@raysan5) 33 + 34 + This software is provided "as-is", without any express or implied warranty. In no event 35 + will the authors be held liable for any damages arising from the use of this software. 36 + 37 + Permission is granted to anyone to use this software for any purpose, including commercial 38 + applications, and to alter it and redistribute it freely, subject to the following restrictions: 39 + 40 + 1. The origin of this software must not be misrepresented; you must not claim that you 41 + wrote the original software. If you use this software in a product, an acknowledgment 42 + in the product documentation would be appreciated but is not required. 43 + 44 + 2. Altered source versions must be plainly marked as such, and must not be misrepresented 45 + as being the original software. 46 + 47 + 3. This notice may not be removed or altered from any source distribution.
+66
readme
··· 1 + 2 + Keraforge 3 + ========= 4 + 5 + A game engine for top-down 2D RPG games. 6 + 7 + [Warning] 8 + Keraforge is still a work-in-progress. Expect breaking 9 + changes! 10 + 11 + Motive 12 + ------ 13 + 14 + There's already a large number of quality game engines 15 + and frameworks in the wild, so what does Keraforge do 16 + differently? 17 + 18 + Design: 19 + Keraforge is designed with a specific kind of game in 20 + mind: top-down, story-driven, handcrafted RPGs. If your 21 + dream game fits in this category, then Keraforge aims to 22 + help make it a reality. 23 + 24 + Simplicity: 25 + Game engines and frameworks always have a learning 26 + curve. Keraforge is no exception. What I can aim for, 27 + though, is keep the learning curve from being exponential 28 + and overwhelming users. I want Keraforge to allow anyone 29 + to share their story with an engine that gives them the 30 + ability to pour love into their work. 31 + 32 + Cost: 33 + Keraforge is 100% free (BSD 3-Clause), zero royalties, 34 + no up-front costs, and no paywalls. I want to give people 35 + the chance to create something beautiful, not to take 36 + their money. 37 + 38 + It's also important to discuss the cons of Keraforge. 39 + It's going to be fundamentally different from any other 40 + engine since it's made for a very specific style of game. 41 + This means that if your game does not fit this style, you 42 + might have more trouble. 43 + 44 + Usage 45 + ----- 46 + 47 + Pre-built binaries are not *yet* distributed. I'll start 48 + publishing binaries once the engine reaches a stable state. 49 + For now, you can compile it yourself. See the section on 50 + development below. 51 + 52 + If you want to see my development progress, see <todo>. 53 + 54 + Develop 55 + ------- 56 + 57 + Build "system" is contained in a folder of shell scripts, 58 + `scripts`. You can run these with the `run.sh` script to 59 + run multiple tasks in order. Generally: 60 + `sh run.sh build run` 61 + is all you'll need to run while developing. 62 + 63 + License 64 + ------- 65 + 66 + BSD 3-Clause, see <license>.
+6
scripts/_config.sh
··· 1 1 #!/usr/bin/env sh 2 2 set -e 3 3 4 + export CC="gcc" 5 + if [ $(command -v "ccache" &> /dev/null) ] 6 + then 7 + export CC="ccache $CC" 8 + fi 9 + 4 10 export CFLAGS="-Wall -Wextra -Werror -std=c99 -Iinclude/ -c" 5 11 export LFLAGS="-lraylib -lm -lGL -lpthread -ldl -lrt -lX11"
+5 -4
src/input.c
··· 4 4 #include <stdio.h> 5 5 #include <string.h> 6 6 7 - const kf_inputbind_t kf_inputbind_none = (kf_inputbind_t)-1; 8 - struct _kf_inputbinds kf_inputbinds = {0}; 7 + struct _kf_inputbinds kf_inputbinds = { 8 + .count = 1, 9 + }; 9 10 10 11 kf_inputbind_t kf_addinput(char *id, KeyboardKey key, MouseButton mouse, GamepadButton gamepad, GamepadAxis axis) 11 12 { 12 13 kf_inputbind_t i = kf_inputbinds.count; 13 - if (i >= KF_INPUTBIND_MAX || i == kf_inputbind_none) 14 + if (i >= KF_INPUTBIND_MAX) 14 15 { 15 16 fprintf(stderr, "error: max keybind count is 255.\n"); 16 17 return -1; ··· 37 38 } 38 39 } 39 40 40 - return kf_inputbind_none; 41 + return KF_INPUTBIND_NONE; 41 42 } 42 43 43 44 int kf_checkkeypress(kf_inputbind_t id) { return kf_inputbinds.key[id] != KEY_NULL && IsKeyPressed(kf_inputbinds.key[id]); }
+71 -24
src/main.c
··· 1 1 #include "keraforge/input.h" 2 + #include "keraforge/ui.h" 2 3 #include <raylib.h> 3 4 #include <raymath.h> 4 5 #include <keraforge.h> ··· 8 9 static Camera2D cam; 9 10 static f32 dt = 0; 10 11 static struct kf_vec2(u32) select = { 0, 0 }; 12 + static int selected_tile = 0; 13 + 14 + static struct 15 + { 16 + enum { menu_none, menu_palette, menu_escape } menu; 17 + } menu; 11 18 12 19 static kf_inputbind_t 13 20 inputbind_move_up, 14 21 inputbind_move_down, 15 22 inputbind_move_left, 16 - inputbind_move_right 23 + inputbind_move_right, 24 + inputbind_select, 25 + inputbind_cancel, 26 + inputbind_palette 17 27 ; 18 28 19 29 static void loadbinds() 20 30 { 21 - inputbind_move_up = kf_addinput("move_up", KEY_W, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 22 - inputbind_move_down = kf_addinput("move_down", KEY_S, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 23 - inputbind_move_left = kf_addinput("move_left", KEY_A, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 24 - inputbind_move_right = kf_addinput("move_right", KEY_D, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_UNKNOWN); 31 + inputbind_move_up = kf_addinput("move_up", KEY_UP, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y); 32 + inputbind_move_down = kf_addinput("move_down", KEY_DOWN, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_Y); 33 + inputbind_move_left = kf_addinput("move_left", KEY_LEFT, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X); 34 + inputbind_move_right = kf_addinput("move_right", KEY_RIGHT, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_UNKNOWN, GAMEPAD_AXIS_LEFT_X); 35 + inputbind_select = kf_addinput("select", KEY_Z, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_DOWN, GAMEPAD_AXIS_UNKNOWN); 36 + inputbind_cancel = kf_addinput("cancel", KEY_X, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, GAMEPAD_AXIS_UNKNOWN); 37 + inputbind_palette = kf_addinput("palette", KEY_TAB, MOUSE_BUTTON_UNKNOWN, GAMEPAD_BUTTON_RIGHT_FACE_UP, GAMEPAD_AXIS_UNKNOWN); 25 38 } 26 39 27 - static void _player_tick(struct kf_world *world, struct kf_actor *self) 40 + static void _player_tick_move(struct kf_actor *self) 28 41 { 29 - bool w = kf_checkinputdown(inputbind_move_up), s = kf_checkinputdown(inputbind_move_down); 30 - bool a = kf_checkinputdown(inputbind_move_left), d = kf_checkinputdown(inputbind_move_right); 42 + bool w = kf_checkinputdown(inputbind_move_up); 43 + bool s = kf_checkinputdown(inputbind_move_down); 44 + bool a = kf_checkinputdown(inputbind_move_left); 45 + bool d = kf_checkinputdown(inputbind_move_right); 46 + 31 47 struct kf_vec2(f32) v = {0, 0}; 32 48 33 49 if (w && s) ··· 48 64 kf_actor_addforce(self, kf_normalize_vec2(f32)(v)); 49 65 50 66 if (IsKeyPressed(KEY_SPACE) && self->speedmod <= 1.0f) 51 - self->speedmod = 3.0f; 67 + self->speedmod = 2.0f; 68 + } 69 + 70 + static void _player_tick(struct kf_world *world, struct kf_actor *self) 71 + { 72 + if (menu.menu == menu_none) 73 + _player_tick_move(self); 52 74 53 75 kf_actor_move(world, self, dt); 54 76 } ··· 72 94 SetTraceLogLevel(LOG_WARNING); 73 95 InitWindow(800, 600, "Keraforge"); 74 96 SetTargetFPS(60); 97 + SetExitKey(KEY_NULL); 75 98 99 + kf_tiles.key[0] = "grass"; 76 100 kf_tiles.color[0] = GREEN; 101 + kf_tiles.key[1] = "dirt"; 77 102 kf_tiles.color[1] = BROWN; 103 + kf_tiles.key[2] = "stone"; 78 104 kf_tiles.color[2] = GRAY; 79 105 kf_tiles.collide[2] = true; 80 106 kf_tiles.count = 3; 81 107 108 + struct kf_uiconfig *uiconfig = kf_ui_getconfig(); 109 + uiconfig->select = inputbind_select; 110 + uiconfig->cancel = inputbind_cancel; 111 + uiconfig->up = inputbind_move_up; 112 + uiconfig->down = inputbind_move_down; 113 + 82 114 if (!DirectoryExists("data")) 83 115 MakeDirectory("data"); 84 116 ··· 127 159 player->draw = _player_draw; 128 160 129 161 cam = (Camera2D){{GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f}, {0, 0}, 0, 2}; 130 - while (!WindowShouldClose()) 162 + int running = 1; 163 + while (!WindowShouldClose() && running) 131 164 { 132 165 if (IsWindowResized()) 133 166 { ··· 141 174 select.x = v.x / KF_TILE_SIZE_PX; 142 175 select.y = v.y / KF_TILE_SIZE_PX; 143 176 177 + if (kf_checkinputpress(inputbind_palette)) 178 + menu.menu = menu_palette; 179 + else if (IsKeyPressed(KEY_ESCAPE) && menu.menu == menu_none) 180 + menu.menu = menu_escape; 181 + 144 182 BeginDrawing(); 145 183 ClearBackground(WHITE); 146 184 ··· 151 189 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) 152 190 { 153 191 kf_tileid_t *t = kf_world_gettile(world, select.x, select.y); 154 - if (++(*t) >= kf_tiles.count) 155 - *t = 0; 156 - } 157 - else if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) 158 - { 159 - kf_tileid_t *t = kf_world_gettile(world, select.x, select.y); 160 - if (--(*t) >= kf_tiles.count) 161 - *t = 0; 162 - } 163 - else if (IsMouseButtonDown(MOUSE_BUTTON_MIDDLE)) 164 - { 165 - kf_tileid_t *t = kf_world_gettile(world, select.x, select.y); 166 - if (*t) 167 - *t = 0; 192 + *t = (kf_tileid_t)selected_tile; 168 193 } 169 194 DrawRectangleLines(select.x * KF_TILE_SIZE_PX, select.y * KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, KF_TILE_SIZE_PX, WHITE); 170 195 } 171 196 player->draw(world, player); 172 197 EndMode2D(); 198 + 199 + switch (menu.menu) 200 + { 201 + case menu_none: 202 + break; 203 + case menu_palette: 204 + if (kf_ui_choice("Select tile", &kf_tiles.key[0], kf_tiles.count, &selected_tile)) 205 + menu.menu = menu_none; 206 + break; 207 + case menu_escape: 208 + { 209 + static int result = 0; 210 + if (kf_ui_yesno("Exit game?", &result)) 211 + { 212 + menu.menu = menu_none; 213 + if (result == 0) 214 + running = 0; 215 + result = 0; 216 + } 217 + break; 218 + } 219 + } 173 220 174 221 DrawFPS(0, 0); 175 222 DrawText(TextFormat("%f", dt), 0, 20, 20, RED);
+82
src/ui.c
··· 1 + #include <keraforge.h> 2 + #include <raylib.h> 3 + 4 + static struct kf_uiconfig _kf_uiconfig = { 5 + .select = KF_INPUTBIND_NONE, 6 + .cancel = KF_INPUTBIND_NONE, 7 + .up = KF_INPUTBIND_NONE, 8 + .down = KF_INPUTBIND_NONE, 9 + .fmt = " %s ", 10 + .selectfmt = "> %s <", 11 + .fontsize = 20, 12 + .bg = BLACK, 13 + .fg = WHITE, 14 + .panelheight = 200, 15 + .xpadding = 8, 16 + .ypadding = 16, 17 + }; 18 + 19 + struct kf_uiconfig *kf_ui_getconfig(void) 20 + { 21 + return &_kf_uiconfig; 22 + } 23 + 24 + int kf_ui_panel(char *title) 25 + { 26 + int y = GetScreenHeight() - _kf_uiconfig.panelheight; 27 + DrawRectangle(0, y, GetScreenWidth(), _kf_uiconfig.panelheight, _kf_uiconfig.bg); 28 + 29 + if (title) 30 + { 31 + y += _kf_uiconfig.ypadding; 32 + DrawText(title, _kf_uiconfig.xpadding, y, _kf_uiconfig.fontsize, _kf_uiconfig.fg); 33 + y += _kf_uiconfig.fontsize; 34 + } 35 + 36 + return y; 37 + } 38 + 39 + int kf_ui_choice(char *title, char **choices, int nchoices, int *choice) 40 + { 41 + int y = kf_ui_panel(title); 42 + 43 + if (*choice >= nchoices) 44 + goto skip_text; 45 + 46 + for (int i = 0 ; i < nchoices ; i++) 47 + { 48 + char *c = choices[i]; 49 + 50 + DrawText( 51 + TextFormat(i == *choice ? _kf_uiconfig.selectfmt : _kf_uiconfig.fmt, c), 52 + _kf_uiconfig.xpadding, 53 + y, 54 + _kf_uiconfig.fontsize, 55 + _kf_uiconfig.fg 56 + ); 57 + 58 + y += _kf_uiconfig.fontsize; 59 + if (y >= GetScreenHeight()) 60 + break; 61 + } 62 + 63 + skip_text: 64 + 65 + if (kf_checkkeypress(_kf_uiconfig.select) || kf_checkkeypress(_kf_uiconfig.cancel)) 66 + return 1; 67 + else if (kf_checkkeypress(_kf_uiconfig.down) && *choice < nchoices) 68 + (*choice)++; 69 + else if (kf_checkkeypress(_kf_uiconfig.up) && *choice > 0) 70 + (*choice)--; 71 + 72 + return 0; 73 + } 74 + 75 + int kf_ui_yesno(char *title, int *choice) 76 + { 77 + static char *yesno[] = { "yes", "no" }; 78 + return kf_ui_choice(title, yesno, 2, choice); 79 + } 80 + 81 + int kf_ui_textinput(char *title, char *text); 82 +
+25
todo
··· 1 + 2 + Dots (.) are to-do 3 + Slashes (/) are in-progress 4 + Xs (X) are done 5 + Squiggly lines (~) are maybes 6 + 7 + Keraforge 1.0 8 + ------------- 9 + 10 + . Core 11 + . World 12 + / Tiles 13 + . Actors 14 + . Dialogue 15 + . Quests 16 + . Combat 17 + . Character Stats 18 + . (De)Buffs 19 + . Cutscenes 20 + . Engine 21 + . Map+Room editor 22 + . Character creator 23 + . Dialogue editor 24 + . Quest creator 25 + ~ Scripting