diff --git a/app.c b/app.c index 4734e3e..dd0b94a 100644 --- a/app.c +++ b/app.c @@ -11,6 +11,7 @@ #include "render_bundle.h" #include "render_component.h" #include "mapper_misc.h" +#include "ui.h" #include #include @@ -27,6 +28,9 @@ App *app_NewApp() { app->camera = camera_NewSystem(app); app->particle = particle_NewSystem(app); app->time = gametime_NewSystem(app); + app->ui = ui_NewSystem(app); + ui_RebuildUI(app->ui); + ui_PushState(app->ui, ui_Running); app->switch_level = NULL; app->timescale = 1.0; @@ -57,6 +61,7 @@ void app_DeleteApp(App *app) { camera_DeleteSystem(app->camera); particle_DeleteSystem(app->particle); gametime_DeleteSystem(app->time); + ui_DeleteSystem(app->ui); free(app); } @@ -72,15 +77,18 @@ void app_Advance(App *app, Duration deltaTime) { input_Advance(app->input); + Duration delta_game = deltaTime; if (1.0 - app->timescale > EPS) - deltaTime.microseconds = deltaTime.microseconds * app->timescale; + delta_game.microseconds = delta_game.microseconds * app->timescale; if (!app->paused) { - gametime_Advance(app->time, deltaTime); - particle_Advance(app->particle, deltaTime); - player_Advance(app->player, deltaTime); - physics_Advance(app->physics, deltaTime); - entity_Advance(app->entity, deltaTime); - camera_Advance(app->camera, deltaTime); + gametime_Advance(app->time, delta_game); + particle_Advance(app->particle, delta_game); + player_Advance(app->player, delta_game); + physics_Advance(app->physics, delta_game); + entity_Advance(app->entity, delta_game); + camera_Advance(app->camera, delta_game); } + + ui_Advance(app->ui, deltaTime); } diff --git a/app.h b/app.h index f951afc..206affe 100644 --- a/app.h +++ b/app.h @@ -7,6 +7,7 @@ #include "input.h" #include "camera.h" #include "particle.h" +#include "ui.h" #include "types.h" #include "util/vector.h" @@ -25,6 +26,7 @@ typedef struct _App { System_Camera *camera; System_Particle *particle; System_GameTime *time; + System_UI *ui; char *switch_level; double timescale; diff --git a/app_render.cpp b/app_render.cpp index 9d19e5a..5b436e7 100644 --- a/app_render.cpp +++ b/app_render.cpp @@ -7,6 +7,7 @@ #include "easyx.h" #include "render_bundle.h" #include "render_component.h" +#include "ui.h" #include "util/tree.h" #include "types.h" #include "render_util.h" @@ -21,10 +22,7 @@ TimePoint since = time_Now(); } // namespace -extern "C" { - - -void app_Render(App *app) { +extern "C" void app_Render(App *app) { render_SetModes(render_ModeDefault, time_Now()); setbkcolor(app->clear_color); cleardevice(); @@ -176,11 +174,13 @@ void app_Render(App *app) { // If paused, display a text if (app->paused) - render_DrawTextEx("Game Paused", box2(SCREEN_WIDTH / 2 - 10, 100, 20, 100), DT_CENTER | DT_NOCLIP); + render_DrawTextEx("Game Paused", box2(SCREEN_WIDTH / 2.0 - 10, 100, 20, 100), DT_CENTER | DT_NOCLIP); if (1.0 - app->timescale > EPS) { char buf[128]; snprintf(buf, sizeof(buf), "*** TIMESCALE %.2lf ***", app->timescale); - render_DrawTextEx(buf, box2(SCREEN_WIDTH / 2 - 10, 50, 20, 100), DT_CENTER | DT_NOCLIP); + render_DrawTextEx(buf, box2(SCREEN_WIDTH / 2.0 - 10, 50, 20, 100), DT_CENTER | DT_NOCLIP); } -} + + // Draw UI + ui_Render(app->ui); } diff --git a/input.c b/input.c index 0f6fd6d..eab3495 100644 --- a/input.c +++ b/input.c @@ -1,6 +1,7 @@ #include "input.h" #include "app.h" +#include "ui.h" #include #include @@ -75,11 +76,15 @@ void input_Advance(System_Input *sys) { } if (sys->keys[input_Key_Escape] == JustPressed) { - if (!sys->super->paused) - fprintf(stderr, "[input_Advance] Pausing\n"); - else - fprintf(stderr, "[input_Advance] Unpausing\n"); - sys->super->paused = !sys->super->paused; + if (ui_CurrentState(sys->super->ui) == ui_Running) { + // Pause + ui_PushState(sys->super->ui, ui_Paused); + sys->super->paused = true; + } else if (ui_CurrentState(sys->super->ui) == ui_Paused) { + // Unpause + ui_PopState(sys->super->ui); + sys->super->paused = false; + } } } diff --git a/types.h b/types.h index 9b76c29..0008650 100644 --- a/types.h +++ b/types.h @@ -11,8 +11,8 @@ extern "C" { #endif -#define SCREEN_WIDTH 1600 -#define SCREEN_HEIGHT 900 +#define SCREEN_WIDTH 1536 +#define SCREEN_HEIGHT 864 #define WARN(fmt, ...) fprintf(stderr, "[WARN][%s] " fmt "\n", __func__, ##__VA_ARGS__) #define INFO(fmt, ...) fprintf(stderr, "[%s] " fmt "\n", __func__, ##__VA_ARGS__) diff --git a/ui.c b/ui.c new file mode 100644 index 0000000..0a319dc --- /dev/null +++ b/ui.c @@ -0,0 +1,127 @@ + +#include "ui.h" +#include "input.h" +#include "app.h" +#include "types.h" +#include "util/vector.h" +#include + + +const char *ui_StateTitle[ui_StateCount] = { + "Running", + "Pause Menu", + "Jack's Escape (ver. 3.141592) - Title", + "Select Level", + "Options"}; + + +static inline double dabs(double x) { return x > 0 ? x : -x; } +static inline double dmin(double x, double y) { return x < y ? x : y; } +static inline double dmax(double x, double y) { return x > y ? x : y; } + + +System_UI *ui_NewSystem(App *super) { + System_UI *sys = zero_malloc(sizeof(System_UI)); + sys->super = super; + sys->state = vector_Create(sizeof(ui_State)); + for (int i = 0; i < ui_StateCount; i++) + sys->parts[i] = vector_Create(sizeof(ui_Part)); + return sys; +} +void ui_DeleteSystem(System_UI *sys) { + for (int i = 0; i < ui_StateCount; i++) + for (int j = 0; j < vector_Size(sys->parts[i]); j++) { + ui_Part *part = (ui_Part *)vector_At(sys->parts[i], j); + part->free(sys, part, part->user); + } + for (int i = 0; i < ui_StateCount; i++) + vector_Destroy(sys->parts[i]); + vector_Destroy(sys->state); + free(sys); +} + + +void ui_PushPart(System_UI *sys, ui_State layer, ui_Part part) { + vector_Push(sys->parts[layer], &part); +} + + +static inline double fadeto(double from, double to, Duration deltaTime) { + if (dabs(from - to) < EPS) + return to; + else + return from + (to - from) * UI_BACKGROUND_FADE_MOVE_SPEED * duration_Seconds(deltaTime); +} + + +static void _ui_UpdateBgTarget(System_UI *sys) { + if (vector_Size(sys->state) == 0) { + WARN("state stack is empty"); + return; + } + + ui_State layer = *(ui_State *)vector_Back(sys->state); + if (vector_Size(sys->parts[layer]) == 0) { + // No UI target, set to (0,0), (0,0) + memset(&sys->bg_target, 0, sizeof(sys->bg_target)); + return; + } + + Vec2 min = {.x = SCREEN_WIDTH, .y = SCREEN_HEIGHT}, max = {.x = 0, .y = 0}; + for (int i = 0; i < vector_Size(sys->parts[layer]); i++) { + ui_Part *part = (ui_Part *)vector_At(sys->parts[layer], i); + min.x = dmin(min.x, part->box.lefttop.x); + min.y = dmin(min.y, part->box.lefttop.y); + max.x = dmax(max.x, part->box.lefttop.x + part->box.size.x); + max.y = dmax(max.y, part->box.lefttop.y + part->box.size.y); + } + sys->bg_target.lefttop = vec2(min.x - UI_PADDING, min.y - UI_PADDING); + sys->bg_target.size = vec2(max.x - min.x + 2 * UI_PADDING, max.y - min.y + 2 * UI_PADDING); +} + +void ui_Advance(System_UI *sys, Duration deltaTime) { + Vec2 lt = sys->bg.lefttop, rb = vec2_Add(sys->bg.lefttop, sys->bg.size); + Vec2 lt_target = sys->bg_target.lefttop, rb_target = vec2_Add(sys->bg_target.lefttop, sys->bg_target.size); + lt.x = fadeto(lt.x, lt_target.x, deltaTime); + lt.y = fadeto(lt.y, lt_target.y, deltaTime); + rb.x = fadeto(rb.x, rb_target.x, deltaTime); + rb.y = fadeto(rb.y, rb_target.y, deltaTime); + + sys->bg.lefttop = lt; + sys->bg.size = vec2_Minus(rb, lt); + + for (int i = 0; i < ui_StateCount; i++) + for (int j = 0; j < vector_Size(sys->parts[i]); j++) { + ui_Part *part = (ui_Part *)vector_At(sys->parts[i], j); + // Hover check + if (box2_Contains(part->box, input_MousePosition(sys->super->input))) { + part->hovered = true; + part->progress = dmin(part->progress + UI_BUTTON_HOVER_SPEED * duration_Seconds(deltaTime), 1.0); + } else { + part->hovered = false; + part->progress = dmax(part->progress - UI_BUTTON_HOVER_SPEED * duration_Seconds(deltaTime), 0.0); + } + + part->update(sys, part, part->user, deltaTime); + } +} + + +void ui_PushState(System_UI *sys, ui_State push_state) { + vector_Push(sys->state, &push_state); + _ui_UpdateBgTarget(sys); +} +void ui_PopState(System_UI *sys) { + if (vector_Size(sys->state) == 0) + WARN("state stack empty"); + else { + vector_Pop(sys->state, NULL); + _ui_UpdateBgTarget(sys); + } +} + +ui_State ui_CurrentState(System_UI *sys) { + if (vector_Size(sys->state) == 0) + WARN("state stack empty"); + return *(ui_State *)vector_Back(sys->state); +} diff --git a/ui.h b/ui.h index 537e21a..be6b761 100644 --- a/ui.h +++ b/ui.h @@ -15,26 +15,86 @@ typedef enum { ui_Running, ui_Paused, ui_TitleMenu, + ui_SceneSelect, + ui_Options, ui_StateCount // Keep at last -} ui_States; +} ui_State; + +extern const char *ui_StateTitle[ui_StateCount]; // Forward declarations -typedef struct _ui_Button ui_Button; typedef struct _System_UI System_UI; // a Part of the UI. -typedef struct { - void *user; // user data - void (*draw)(System_UI *sys, void *user); // called when the UI is to be drawn +typedef struct _ui_Part { + Box2 box; // bounding box of the UI element + float progress; // hover process, from 0 to 1 + bool hovered; // Whether the part is hovered + uintptr_t user; // user data + void (*draw)(System_UI *sys, struct _ui_Part *part, uintptr_t user); // called when the UI is to be drawn + void (*update)(System_UI *sys, struct _ui_Part *part, uintptr_t user, Duration deltatime); // called when the element is to be updated + void (*free)(System_UI *sys, struct _ui_Part *part, uintptr_t user); // called when the element is to be deleted } ui_Part; // UI system with its own state machine. typedef struct _System_UI { App *super; - vector_Vector *parts[ui_StateCount]; + vector_Vector *parts[ui_StateCount]; // A vector of ui_Parts + vector_Vector *state; // A stack of ui_States + + Box2 bg, bg_target; } System_UI; +System_UI *ui_NewSystem(App *super); +void ui_DeleteSystem(System_UI *sys); + +// Clears and refills every menu with its widgets +void ui_RebuildUI(System_UI *sys); + +void ui_PushPart(System_UI *sys, ui_State layer, ui_Part part); + +void ui_Advance(System_UI *sys, Duration deltaTime); +void ui_Render(System_UI *sys); + +void ui_PushState(System_UI *sys, ui_State push_state); +void ui_PopState(System_UI *sys); + +ui_State ui_CurrentState(System_UI *sys); + + +#define UI_PADDING (20) +#define UI_BACKGROUND (0) + +#define UI_BACKGROUND_FADE_MOVE_SPEED (20.0) +#define UI_BUTTON_HOVER_SPEED (5.0) + +#define UI_BUTTON_BACKGROUND (RGB(50, 50, 50)) +#define UI_BUTTON_HOVERED (RGB(75, 75, 75)) +#define UI_BUTTON_CLICKED (RGB(90, 90, 90)) + +typedef void (*ui_ActionCallback)(System_UI *sys, ui_Part *part, uintptr_t data); + +// An Action to pop the UI state stack once +void ui_Action_PopState(System_UI *sys, ui_Part *part, uintptr_t data); +// An Action to push the state, casted directly from data +void ui_Action_PushState(System_UI *sys, ui_Part *part, uintptr_t data); + + +// Allocated & passed in as user data for ui part +typedef struct _ui_Button { + ui_ActionCallback callback; + uintptr_t callback_data; + + uint32_t bg, bg_hover, clicked; + char *label; // Allocated & copied + bool left_aligned; +} ui_Button; + +ui_Part ui_Button_New(Box2 box, const char *label, ui_ActionCallback callback, uintptr_t data); +ui_Part ui_Button_NewLeftAligned(Box2 box, const char *label, ui_ActionCallback callback, uintptr_t data); +// ui_Part ui_Button_NewColored(Box2 box, const char *label, uint32_t bg, uint32_t hover, uint32_t clicked, ui_ActionCallback callback, uintptr_t data); + #ifdef __cplusplus } diff --git a/ui_action.c b/ui_action.c new file mode 100644 index 0000000..26f5b41 --- /dev/null +++ b/ui_action.c @@ -0,0 +1,10 @@ + +#include "ui.h" + + +void ui_Action_PopState(System_UI *sys, ui_Part *part, uintptr_t data) { + ui_PopState(sys); +} +void ui_Action_PushState(System_UI *sys, ui_Part *part, uintptr_t data) { + ui_PushState(sys, (ui_State)data); +} diff --git a/ui_build.c b/ui_build.c new file mode 100644 index 0000000..62574fd --- /dev/null +++ b/ui_build.c @@ -0,0 +1,29 @@ + +#include "ui.h" +#include "app.h" +#include "util/vector.h" + + +static void _ui_Action_ResumeGame(System_UI *sys, ui_Part *part, uintptr_t data) { + ui_Action_PopState(sys, part, data); + sys->super->paused = false; +} + + +#define PUSH_UI(part) \ + _p0 = (part); \ + vector_Push(p, &_p0); + +void ui_RebuildUI(System_UI *sys) { + vector_Vector *p; + ui_Part _p0; + + + // Paused + p = sys->parts[ui_Paused]; + vector_Clear(p); + PUSH_UI(ui_Button_New( + box2_FromCenter(vec2(SCREEN_WIDTH / 2.0, 300), vec2(400, 50)), + "Resume Game", + &_ui_Action_ResumeGame, 0)) +} diff --git a/ui_button.c b/ui_button.c new file mode 100644 index 0000000..63153c5 --- /dev/null +++ b/ui_button.c @@ -0,0 +1,45 @@ + +#include "input.h" +#include "types.h" +#include "ui.h" +#include "app.h" + + +#ifndef RGB +#define RGB(r, g, b) ((r) | ((g) << 8) | ((b) << 16)) +#endif + + +void _ui_Button_Draw(System_UI *sys, ui_Part *part, uintptr_t user); // Defined in ui_render.cpp +void _ui_Button_Update(System_UI *sys, ui_Part *part, uintptr_t user, Duration deltatime) { + ui_Button *b = (ui_Button *)user; + if (part->hovered && sys->super->input->keys[input_Key_LeftMouse] == JustReleased) + b->callback(sys, part, b->callback_data); +} +void _ui_Button_Free(System_UI *sys, ui_Part *part, uintptr_t user) { + ui_Button *b = (ui_Button *)user; + free(b->label); + free(b); +} + + +ui_Part ui_Button_New(Box2 box, const char *label, ui_ActionCallback callback, uintptr_t data) { + ui_Button *b = zero_malloc(sizeof(ui_Button)); + b->callback = callback; + b->callback_data = data; + b->bg = UI_BUTTON_BACKGROUND; + b->bg_hover = UI_BUTTON_HOVERED; + b->clicked = UI_BUTTON_CLICKED; + b->label = copy_malloc(label); + b->left_aligned = false; + + ui_Part part = { + .box = box, + .progress = 0.0f, + .user = (uintptr_t)b, + .draw = &_ui_Button_Draw, + .update = &_ui_Button_Update, + .free = &_ui_Button_Free}; + return part; +} +ui_Part ui_Button_NewLeftAligned(Box2 box, const char *label, ui_ActionCallback callback, uintptr_t data); diff --git a/ui_render.cpp b/ui_render.cpp new file mode 100644 index 0000000..38e38c1 --- /dev/null +++ b/ui_render.cpp @@ -0,0 +1,75 @@ + +#include "input.h" +#include "render_util.h" +#include "types.h" +#include "ui.h" +#include "app.h" + +#include + + +extern "C" void ui_Render(System_UI *sys) { + // Background filled rectangle + if (sys->bg.size.x > EPS && sys->bg.size.y > EPS) { + setfillcolor(UI_BACKGROUND); + solidrectangle( + (int)round(sys->bg.lefttop.x), + (int)round(sys->bg.lefttop.y), + (int)round(sys->bg.lefttop.x + sys->bg.size.x), + (int)round(sys->bg.lefttop.y + sys->bg.size.y)); + } + if (vector_Size(sys->state) == 0) + return; + + ui_State layer = *(ui_State *)vector_Back(sys->state); + // Title string + setbkcolor(UI_BACKGROUND); + render_DrawText( + (int)round(sys->bg.lefttop.x), + (int)round(sys->bg.lefttop.y) - TEXTHEIGHT, + ui_StateTitle[layer]); + + // UI elements + // Skip them if the width of background is less than 90% of the target + if (sys->bg.size.x > sys->bg_target.size.x * 0.9) + for (int i = 0; i < vector_Size(sys->parts[layer]); i++) { + ui_Part *part = (ui_Part *)vector_At(sys->parts[layer], i); + part->draw(sys, part, part->user); + } +} + +extern "C" void _ui_Button_Draw(System_UI *sys, ui_Part *part, void *user) { + ui_Button *b = (ui_Button *)user; + + uint32_t color_final = 0; + + if (part->hovered && input_IsPressed(sys->super->input->keys[input_Key_LeftMouse])) + // Hovered & pressed + color_final = b->clicked; + else { + // Fade the color + int r0 = b->bg & 0xff; + int g0 = (b->bg & 0xff00) >> 8; + int b0 = (b->bg & 0xff0000) >> 16; + + int r1 = b->bg_hover & 0xff; + int g1 = (b->bg_hover & 0xff00) >> 8; + int b1 = (b->bg_hover & 0xff0000) >> 16; + + color_final = RGB( + (int)((r1 - r0) * sqrt(part->progress) + r0), + (int)((g1 - g0) * sqrt(part->progress) + g0), + (int)((b1 - b0) * sqrt(part->progress) + b0)); + } + + setbkcolor(color_final); + setfillcolor(color_final); + settextcolor(RGB(255, 255, 255)); + + solidrectangle( + (int)round(part->box.lefttop.x), + (int)round(part->box.lefttop.y), + (int)round(part->box.lefttop.x + part->box.size.x), + (int)round(part->box.lefttop.y + part->box.size.y)); + render_DrawTextEx(b->label, part->box, (b->left_aligned ? DT_LEFT : DT_CENTER) | DT_VCENTER | DT_SINGLELINE); +} diff --git a/util/vector.c b/util/vector.c index e6db647..4a58989 100644 --- a/util/vector.c +++ b/util/vector.c @@ -3,6 +3,7 @@ #include "string.h" #include "stdlib.h" +#include "assert.h" vector_Vector *vector_Create(uintptr_t objectSize) { @@ -19,6 +20,7 @@ vector_Vector *vector_Create(uintptr_t objectSize) { // Resizes the underlying buffer to a new capacity static inline void __vector_Rebuffer(vector_Vector *vec, uintptr_t newcap) { + ASSERT(newcap >= vec->size); void *newbuf = malloc(newcap); memcpy(newbuf, vec->data, vec->size); free(vec->data); @@ -50,7 +52,7 @@ bool vector_Pop(vector_Vector *vec, void *out_data) { void vector_Append(vector_Vector *vec, const void *data, uintptr_t n) { uintptr_t oldsize = vec->size, addsize = vec->objectSize * n; - vector_Resize(vec, oldsize + addsize); + vector_Resize(vec, vector_Size(vec) + n); if (data) memcpy(vec->data + oldsize, data, addsize); @@ -59,7 +61,7 @@ void vector_Append(vector_Vector *vec, const void *data, uintptr_t n) { } void vector_Resize(vector_Vector *vec, uintptr_t size) { - uintptr_t newsize = size; + uintptr_t newsize = size * vec->objectSize; if (newsize > vec->cap) { // grow the buffer exponentially uint64_t newcap = vec->cap; @@ -101,3 +103,7 @@ void *vector_At(vector_Vector *vec, uintptr_t i) { void *vector_Data(vector_Vector *vec) { return vec->data; } + +void *vector_Back(vector_Vector *vec) { + return vector_At(vec, vector_Size(vec) - 1); +} diff --git a/util/vector.h b/util/vector.h index d59d61f..31fbd05 100644 --- a/util/vector.h +++ b/util/vector.h @@ -68,6 +68,11 @@ void *vector_At(vector_Vector *vec, uintptr_t i); // Data returns the data buffer. void *vector_Data(vector_Vector *vec); +// Back returns the last element in the vector. +// +// No boundary test is performed. +void *vector_Back(vector_Vector *vec); + #ifdef __cplusplus }