text_nextframe now takes deltatime instead of recomputing it, the scene command struct got an overhaul and now it's much easier to write and read

This commit is contained in:
maksw committed 2025-11-13 15:48:58 +01:00
1 parent e56d88ad08
commit 612c4e694b
3 files changed
+204 -172

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ void text_push(text_t* text,
void text_show_all_current(text_t* text);
bool text_is_animating(text_t* text);
// advance, call every frame, returns false when done
bool text_nextframe(text_t* text);
bool text_nextframe(text_t* text, float deltatime);
uint32_t* text_getfb(text_t* text);
void text_clear(text_t* text);
}
+118 -105
View File
@@ -4,7 +4,7 @@
#include <unordered_map>
#include <string>
#include <chrono>
#include <vector> // <-- NEW: For tracking multiple animations
#include <vector>
#include <window.h>
#include <text.h>
#include <compositor.h>
@@ -22,6 +22,7 @@ enum CommandType {
CMD_SET_Z,
CMD_SET_SCALE,
CMD_SPEAK,
CMD_CLEAR,
CMD_WAIT,
CMD_BACKGROUND,
};
@@ -33,23 +34,74 @@ enum TransitionType {
TRANS_SLIDE_RIGHT,
};
struct Command {
CommandType type;
const char* character_name;
int x, y;
int z;
float scale;
const char* sprite_name;
const char* speaker;
const char* text;
struct CmdShow {
const char* character;
const char* sprite;
int x, y, z;
TransitionType transition;
float duration_ms;
};
bool blocking; // NEW: If true, wait for this animation to complete before next command
struct CmdHide {
const char* character;
TransitionType transition;
float duration_ms;
};
struct CmdMove {
const char* character;
int x, y;
TransitionType transition;
float duration_ms;
};
struct CmdChangeSprite {
const char* character;
const char* sprite;
};
struct CmdSetZ {
const char* character;
int z;
};
struct CmdSetScale {
const char* character;
float scale;
TransitionType transition;
float duration_ms;
};
struct CmdSpeak {
const char* speaker;
const char* text;
float text_speed;
};
struct CmdWait {};
struct CmdClear {};
struct CmdBackground {
const char* filename;
TransitionType transition;
float duration_ms;
};
struct Command {
CommandType type;
bool blocking;
union {
CmdShow show;
CmdHide hide;
CmdMove move;
CmdChangeSprite change_sprite;
CmdSetZ set_z;
CmdSetScale set_scale;
CmdSpeak speak;
CmdWait wait;
CmdClear clear;
CmdBackground background;
};
};
struct Scene {
@@ -364,47 +416,50 @@ public:
float execute(const Command* cmd) {
switch (cmd->type) {
case CMD_SHOW:
char_manager->show_character(cmd->character_name, cmd->x, cmd->y,
cmd->z, cmd->sprite_name,
cmd->transition, cmd->duration_ms);
return cmd->duration_ms / 1000.0f;
char_manager->show_character(cmd->show.character, cmd->show.x, cmd->show.y,
cmd->show.z, cmd->show.sprite,
cmd->show.transition, cmd->show.duration_ms);
return cmd->show.duration_ms / 1000.0f;
case CMD_HIDE:
char_manager->hide_character(cmd->character_name,
cmd->transition, cmd->duration_ms);
return cmd->duration_ms / 1000.0f;
char_manager->hide_character(cmd->hide.character,
cmd->hide.transition, cmd->hide.duration_ms);
return cmd->hide.duration_ms / 1000.0f;
case CMD_MOVE:
char_manager->move_character(cmd->character_name, cmd->x, cmd->y,
cmd->transition, cmd->duration_ms);
return cmd->duration_ms / 1000.0f;
char_manager->move_character(cmd->move.character, cmd->move.x, cmd->move.y,
cmd->move.transition, cmd->move.duration_ms);
return cmd->move.duration_ms / 1000.0f;
case CMD_CHANGE_SPRITE:
char_manager->change_sprite(cmd->character_name, cmd->sprite_name);
char_manager->change_sprite(cmd->change_sprite.character, cmd->change_sprite.sprite);
break;
case CMD_SET_Z:
char_manager->set_z(cmd->character_name, cmd->z);
char_manager->set_z(cmd->set_z.character, cmd->set_z.z);
break;
case CMD_SET_SCALE:
char_manager->set_scale(cmd->character_name, cmd->scale,
cmd->transition, cmd->duration_ms);
return cmd->duration_ms / 1000.0f;
char_manager->set_scale(cmd->set_scale.character, cmd->set_scale.scale,
cmd->set_scale.transition, cmd->set_scale.duration_ms);
return cmd->set_scale.duration_ms / 1000.0f;
case CMD_SPEAK:
text_clear(name_text);
text_clear(dialogue_text);
if (cmd->speaker) {
text_push(name_text, cmd->speaker, 34.0f, 0.0f, "assets/Vera.ttf", true);
if (cmd->speak.speaker) {
text_push(name_text, cmd->speak.speaker, 34.0f, 0.0f, "assets/Vera.ttf", true);
}
if (cmd->text) {
text_push(dialogue_text, cmd->text, 24.0f, 2.0f, "assets/Vera.ttf", true);
if (cmd->speak.text) {
text_push(dialogue_text, cmd->speak.text, 24.0f, cmd->speak.text_speed, "assets/Vera.ttf", true);
}
compositor_mark_content_dirty(compositor, name_id);
compositor_mark_content_dirty(compositor, dialogue_id);
break;
case CMD_CLEAR:
text_clear(name_text);
text_clear(dialogue_text);
break;
case CMD_WAIT:
case CMD_BACKGROUND:
break;
@@ -477,71 +532,29 @@ int main() {
// --- Scene Script ---
Command scene_commands[] = {
{CMD_SHOW, "maksw", 100, POS_Y, 2, 1.0f, "neutral", nullptr, nullptr, TRANS_SLIDE_LEFT, 500.0f, false},
{CMD_SHOW, "wikor", 800, POS_Y, 1, 1.0f, "neutral", nullptr, nullptr, TRANS_SLIDE_RIGHT, 500.0f, true},
{.type = CMD_SHOW, .blocking = false, .show = {.character = "maksw", .sprite = "neutral", .x = 100, .y = POS_Y, .z = 2, .transition = TRANS_SLIDE_LEFT, .duration_ms = 500.0f } },
{.type = CMD_SHOW, .blocking = true, .show = {.character = "wikor", .sprite = "neutral", .x = 800, .y = POS_Y, .z = 1, .transition = TRANS_SLIDE_RIGHT, .duration_ms = 500.0f } },
// Maksw starts talking
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
"Wow, it's been a really long week, hasn't it? The sky looks different today.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = "Maksw", .text = "Wow, it's been a really long week, hasn't it? The sky looks different today.", .text_speed = 30.0f } },
{.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = "", .text = "\nline 2", .text_speed = 0.0f } },
{.type = CMD_WAIT, .blocking = true, .wait = {} },
{.type = CMD_CLEAR, .blocking = true, .clear = {} },
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
"I mean, just look at how the clouds are stretching all the way to the horizon. Feels almost cinematic.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = "Maksw", .text = "I mean, just look at how the clouds are stretching all the way to the horizon. Feels almost cinematic.", .text_speed = 30.0f } },
{.type = CMD_WAIT, .blocking = true, .wait = {} },
{.type = CMD_CLEAR, .blocking = true, .clear = {} },
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
"And the air, it smells like rain... or maybe just damp leaves? Either way, kinda refreshing.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{.type = CMD_SET_SCALE, .blocking = false, .set_scale = {.character = "maksw", .scale = 1.0f, .transition = TRANS_FADE, .duration_ms = 200.0f } },
{.type = CMD_SET_Z, .blocking = false, .set_z = {.character = "maksw", .z = 1 } },
{.type = CMD_SET_Z, .blocking = false, .set_z = {.character = "wikor", .z = 3 } },
{.type = CMD_SET_SCALE, .blocking = false, .set_scale = {.character = "wikor", .scale = 1.05f, .transition = TRANS_FADE, .duration_ms = 200.0f } },
// Wikor responds
{CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false},
{CMD_SET_Z, "maksw", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false},
{CMD_SET_Z, "wikor", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false},
{CMD_SET_SCALE, "wikor", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false},
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov",
"I see what you mean. It's almost like the clouds are in slow motion today. Peaceful, but a bit eerie too.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = u8"admirał wiktorov", .text = "I see what you mean. It's almost like the clouds are in slow motion today. Peaceful, but a bit eerie too.", .text_speed = 30.0f } },
{.type = CMD_WAIT, .blocking = true, .wait = {} },
{.type = CMD_CLEAR, .blocking = true, .clear = {} },
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov",
"I almost feel like grabbing a cup of tea and just watching the sky for hours. But I have a feeling that won't happen.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov",
"Also, did you notice that weird bird earlier? It looked like it was practicing aerobatics or something.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
// Maksw continues
{CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false},
{CMD_SET_Z, "wikor", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false},
{CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false},
{CMD_SET_SCALE, "maksw", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false},
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
"Yeah, that bird was definitely showing off. Maybe it's training for some secret bird Olympics.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
"Honestly, between the weather, the clouds, and the bird, it feels like the whole world hit the pause button for a moment.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
"But then reality hits. Bugs to fix, emails to answer, the usual chaos. Can't just float in a dream forever.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
"Still, I like moments like this. Makes me appreciate the little things more. You know?", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
// Wikor closes it
{CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false},
{CMD_SET_Z, "maksw", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false},
{CMD_SET_Z, "wikor", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false},
{CMD_SET_SCALE, "wikor", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false},
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov",
"Absolutely. Alright, back to work. But next time the sky looks like this, we should take a proper break.", TRANS_NONE, 0, true},
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
{CMD_HIDE, "maksw", 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_FADE, 500.0f, false},
{CMD_HIDE, "wikor", 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_FADE, 500.0f, true},
{.type = CMD_HIDE, .blocking = false, .hide = {.character = "maksw", .transition = TRANS_FADE, .duration_ms = 500.0f } },
{.type = CMD_HIDE, .blocking = true, .hide = {.character = "wikor", .transition = TRANS_FADE, .duration_ms = 500.0f } },
};
Scene scene = { scene_commands, sizeof(scene_commands) / sizeof(Command), 0 };
@@ -551,7 +564,7 @@ int main() {
bool running = true;
AppState current_state = STATE_IDLE;
AnimationTracker anim_tracker; // NEW: Track multiple animations
AnimationTracker anim_tracker; // Track multiple animations
auto last_frame_time = std::chrono::high_resolution_clock::now();
@@ -580,13 +593,6 @@ int main() {
frame_time_sum = 0.0f;
}
compositor_tick(compositor, delta_time_sec);
text_nextframe(name_text);
text_nextframe(dialogue_text);
compositor_mark_content_dirty(compositor, name_id);
compositor_mark_content_dirty(compositor, dialogue_id);
// Update animation tracker
anim_tracker.update(delta_time_sec);
@@ -640,7 +646,7 @@ int main() {
scene.next();
// NEW: Check blocking flag
// Check blocking flag
if (cmd->blocking) {
if (cmd->type == CMD_WAIT) {
current_state = STATE_WAITING_FOR_INPUT;
@@ -655,6 +661,13 @@ int main() {
// If non-blocking, loop continues immediately to next command
}
compositor_tick(compositor, delta_time_sec);
text_nextframe(name_text, delta_time_sec);
text_nextframe(dialogue_text, delta_time_sec);
compositor_mark_content_dirty(compositor, name_id);
compositor_mark_content_dirty(compositor, dialogue_id);
const uint32_t* fb = compositor_compose(compositor);
window_blit(window, fb);
}
+85 -66
View File
@@ -670,8 +670,9 @@ extern "C" {
// --- Update & Animation ---
// Advances the text animation and renders new characters.
bool text_nextframe(text_t* text) {
// Advances the text animation and renders new characters.
// deltatime is the time elapsed since the last frame (in seconds).
bool text_nextframe(text_t* text, float deltatime) {
if (!text) return true;
if (text->queue_index >= text->text_queue.size()) {
return true; // Nothing to do
@@ -704,94 +705,112 @@ extern "C" {
stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap);
float baseline = ascent * scale;
// Update animation progress
// --- Update animation progress using deltatime ---
size_t total_cp = utf8_count_codepoints(params->text);
if (params->speed > 0.0f && text->char_index < (int)total_cp) {
text->char_progress += params->speed;
text->char_index = static_cast<int>(text->char_progress);
// The animation speed (params->speed) is now characters per second.
// Multiply by deltatime to get the progress to advance this frame.
text->char_progress += params->speed * deltatime;
// Only update char_index if it has advanced past the current index.
// This prevents the animation from advancing multiple characters in a single frame
// if the deltatime is large, but still allows continuous float progress.
int new_char_index = static_cast<int>(text->char_progress);
// We only want to draw characters *up to* the new index, but we
// still need to check if the new index is greater than the last frame's index.
if (new_char_index > text->char_index) {
text->char_index = new_char_index;
}
}
else if (params->speed <= 0.0f) {
text->char_index = (int)total_cp;
}
// Cap char_index to the total number of codepoints
text->char_index = (int)std::min((size_t)text->char_index, total_cp);
// Only draw NEW characters
size_t draw_start = text->last_rendered_char; // cp index
size_t draw_end = text->char_index; // cp index
size_t draw_end = text->char_index; // cp index
// Drawing loop - only for NEW characters
for (const auto& line : params->lines) {
// Skip lines that are completely rendered
if (line.end_cp_index <= draw_start) {
continue;
}
// Skip lines that haven't started yet
if (line.start_cp_index >= draw_end) {
break;
}
text->current_x = text->padding_x;
text->current_y = line.y_offset;
// Calculate x position up to where we need to draw
// Iterate through the line bytes and sum advances for cps before draw_start
size_t byte_i = line.start_byte_index;
size_t cp_i = line.start_cp_index;
while (byte_i < line.end_byte_index && cp_i < draw_start) {
uint32_t cp = utf8_next(params->text, byte_i);
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
if (glyph) {
text->current_x += glyph->advance;
}
++cp_i;
}
// Draw only NEW characters on this line
byte_i = line.start_byte_index;
cp_i = line.start_cp_index;
// advance byte_i to the first drawable codepoint (max(line.start_cp_index, draw_start))
while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) {
utf8_next(params->text, byte_i);
++cp_i;
}
// Now iterate and draw until cp reaches min(line.end_cp_index, draw_end)
while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) {
uint32_t cp = utf8_next(params->text, byte_i);
if (cp == (uint32_t)'\n') { ++cp_i; continue; }
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
if (!glyph) {
++cp_i;
// Check if any new characters should be drawn
if (draw_start < draw_end) {
// Drawing loop - only for NEW characters
for (const auto& line : params->lines) {
// Skip lines that are completely rendered
if (line.end_cp_index <= draw_start) {
continue;
}
int x_base = static_cast<int>(text->current_x + glyph->xoff);
int y_base = static_cast<int>(text->current_y + baseline + glyph->yoff);
if (params->outline) {
draw_glyph(text, x_base, y_base, params->color_outline, glyph, params->size, true);
// Skip lines that haven't started yet
if (line.start_cp_index >= draw_end) {
break;
}
draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false);
text->current_x += glyph->advance;
++cp_i;
text->current_x = text->padding_x;
text->current_y = line.y_offset;
// Calculate x position up to where we need to draw
// Iterate through the line bytes and sum advances for cps before draw_start
size_t byte_i = line.start_byte_index;
size_t cp_i = line.start_cp_index;
while (byte_i < line.end_byte_index && cp_i < draw_start) {
uint32_t cp = utf8_next(params->text, byte_i);
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
if (glyph) {
text->current_x += glyph->advance;
}
++cp_i;
}
// Draw only NEW characters on this line
byte_i = line.start_byte_index;
cp_i = line.start_cp_index;
// advance byte_i to the first drawable codepoint (max(line.start_cp_index, draw_start))
while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) {
utf8_next(params->text, byte_i);
++cp_i;
}
// Now iterate and draw until cp reaches min(line.end_cp_index, draw_end)
while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) {
uint32_t cp = utf8_next(params->text, byte_i);
if (cp == (uint32_t)'\n') { ++cp_i; continue; }
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
if (!glyph) {
++cp_i;
continue;
}
int x_base = static_cast<int>(text->current_x + glyph->xoff);
int y_base = static_cast<int>(text->current_y + baseline + glyph->yoff);
if (params->outline) {
draw_glyph(text, x_base, y_base, params->color_outline, glyph, params->size, true);
}
draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false);
text->current_x += glyph->advance;
++cp_i;
}
}
// Update tracking only if we drew something new
text->last_rendered_char = (int)text->char_index;
}
// Update tracking
text->last_rendered_char = (int)text->char_index;
// Handle block transition
if (text->char_index >= (int)utf8_count_codepoints(params->text)) {
if (text->char_index >= (int)total_cp) {
text->queue_index++;
text->char_index = 0;
text->char_progress = 0.0f;
text->last_rendered_char = 0; // Reset for next block
text->current_y += text->last_line_height; // Advance y-pos
// Only advance Y if there was a previous block rendered (to prevent double-spacing on first block)
if (text->queue_index > 1) {
text->current_y += text->last_line_height; // Advance y-pos
}
}
// Check if queue is finished