From fe094377295fb2daceae964d6951199788ee13d9 Mon Sep 17 00:00:00 2001 From: seventhback777 <903157209@qq.com> Date: Sat, 23 May 2026 18:18:09 +1000 Subject: [PATCH 1/2] feat: add game launch tracking and crash detection - Process.h/cpp: overload processRunning() to return exit code; add killProcess() to terminate the full game process group via SIGTERM; improve error reporting and forward game stdout to stderr for debugging - Menu.h/cpp: add m_launching, m_launchTime, m_launchError, m_lastExitCode to track game launch state and detect crashes; fix memory leak in destructor by freeing all ButtonNode/Button objects in the linked list; improve image load error reporting - Process.cpp: use waitpid(WNOHANG) instead of getpgid for non-blocking exit-code capture; set new process group with setpgid so kill() covers the full game tree - Option.h/cpp: add destructor declaration; fix include order - AboutScreen.h/cpp: add m_gitContributions field for contributor stats - ArcadeMachine.cpp, Button.cpp, program.cpp: corresponding updates --- include/AboutScreen.h | 3 +- include/Menu.h | 14 ++-- include/Option.h | 3 +- include/Process.h | 3 +- src/AboutScreen.cpp | 181 +++++++++++++++++++++++++----------------- src/ArcadeMachine.cpp | 54 +++++++++++-- src/Button.cpp | 2 +- src/Menu.cpp | 139 ++++++++++++++++++++++++++------ src/Option.cpp | 21 ++++- src/Process.cpp | 62 ++++++++++++--- src/program.cpp | 57 ++++++++++++- 11 files changed, 409 insertions(+), 130 deletions(-) diff --git a/include/AboutScreen.h b/include/AboutScreen.h index 806c79d..979dfc9 100644 --- a/include/AboutScreen.h +++ b/include/AboutScreen.h @@ -45,4 +45,5 @@ class AboutScreen { void main(); }; -#endif \ No newline at end of file +#endif + diff --git a/include/Menu.h b/include/Menu.h index 6e0d7a5..df5e8f0 100644 --- a/include/Menu.h +++ b/include/Menu.h @@ -36,12 +36,6 @@ class Menu { PROCESS_INFORMATION m_processInfo; // Unsigned int to store exit info DWORD m_exitCode; - // Holds the game path of selected game - LPCSTR m_gamePath; - // Holds the executable of selected game - LPSTR m_gameExe; - // Holds the game directory of selected game - LPCSTR m_gameDir; // m_handle for game window. HWND m_handle; #else @@ -79,6 +73,14 @@ class Menu { // Determines when game has started. bool m_gameStarted = false; + // True from launch attempt until confirmed running or failed. + bool m_launching = false; + // Timestamp (ms) when m_inGame was set — used to detect crashes. + unsigned int m_launchTime = 0; + // Holds error message to display after launch failure or crash. + std::string m_launchError = ""; + // Exit code of last game process (0 = normal, non-zero = crash). + int m_lastExitCode = 0; // Starting position of button x. const int m_posX = 700; // Position of button y. diff --git a/include/Option.h b/include/Option.h index dcd4824..f2b5df1 100644 --- a/include/Option.h +++ b/include/Option.h @@ -2,11 +2,11 @@ #define ARCADE_MACHINE_OPTION_H #include "AboutScreen.h" +#include "Configuration.h" #include "splashkit.h" #include #include "GridLayout.h" #include "Button.h" -#include "ArcadeMachine.h" #include "Selector.h" #include "OptionsScreenButton.h" #include "Audio.h" @@ -34,6 +34,7 @@ class Option public: Option(); + ~Option(); void createOptionsButtons(); void drawOptionsMenu(); diff --git a/include/Process.h b/include/Process.h index 04337df..bfffe8d 100644 --- a/include/Process.h +++ b/include/Process.h @@ -4,6 +4,7 @@ #include pid_t spawnProcess(std::string directory, std::string fileName); -bool processRunning(pid_t processId); +bool processRunning(pid_t processId, int &exitCode); +void killProcess(pid_t processId); #endif \ No newline at end of file diff --git a/src/AboutScreen.cpp b/src/AboutScreen.cpp index 37a339c..96dec9b 100644 --- a/src/AboutScreen.cpp +++ b/src/AboutScreen.cpp @@ -30,71 +30,87 @@ static const char *description[] = { static const std::string createdBy = "Created by"; AboutScreen::AboutScreen() { - this->m_shouldQuit = false; - this->m_titleX = ARCADE_MACHINE_RES_X; - this->m_title = std::string(title); + this->m_shouldQuit = false;//初始化退出标志为False + this->m_titleX = ARCADE_MACHINE_RES_X;//将标题的X位置定位在屏幕最右边 + this->m_title = std::string(title);//将m_title变成string风格的字符串,方便进行各种操作,如lenth() this->m_titleEnd = ((TITLE_FONT_CHAR_WIDTH * this->m_title.length())); this->m_titleEnd *= -1; - this->m_stars = std::vector(); + /*这里的设计涉及到打印逻辑,在打印标题的时候,每打印一个字符,m_titleX的位置就加一个TITLE_FONT_CHAR_WIDTH,所以标题的总长度应为TITLE_FONT_CHAR_WIDTH * this->m_title.length() + 也就是字符长度(m_title.length())乘以字符宽度(TITLE_FONT_CHAR_WIDTH),而当字符从右往左完全滚动出屏幕之时,第一个字符的X位置应该刚好是-标题总长度,因为此时 + 最右边的字符坐标应该为0,所以m_titleEnd的位置应该是-标题总长度=-((TITLE_FONT_CHAR_WIDTH * this->m_title.length()));,也就是37行m_titleEnd的最终值 + */ + this->m_stars = std::vector();//初始化数组m_stars this->m_contributorsIndex = 0; this->m_contributorTicker = 0; this->m_ticker = 0; for (int i=0; im_stars.push_back(star); } - - this->m_contributors = std::vector(); + //循环生成所有结构体(1024颗)s_star星星的循环,包括每个星星的X,Y位置,亮度,distance,并把这些结构体push进数组m_stars + + this->m_contributors = std::vector();//初始化数组m_contributors std::ifstream contributors("stats" ARCADE_MACHINE_PATH_SEP "contributors.txt"); + //使用ifstream(输入文件流)输入贡献者名单文件 ARCADE_MACHINE_PATH_SEP是为了应对不同系统风格的分隔符 std::string line; while (std::getline(contributors, line)) { if (line.length() > 0) this->m_contributors.push_back(line); } contributors.close(); - - this->m_linesOfCode = std::vector(); + //按行读取贡献者名单,并把贡献者名字加入到m_contributors数组里 + + this->m_linesOfCode = std::vector();//初始化m_linesOfCode数组 std::ifstream linesOfCode("stats" ARCADE_MACHINE_PATH_SEP "lines-of-code.txt"); while (std::getline(linesOfCode, line)) { if (line.length() > 0) this->m_linesOfCode.push_back(line); } - + //按行读取贡献者代码数量文件,并把贡献者代码数量加入到m_linesOfCode数组里 + this->m_gitContributions = std::vector(); std::ifstream contributions("stats" ARCADE_MACHINE_PATH_SEP "git.txt"); while (std::getline(contributions, line)) { if (line.length() > 0) this->m_gitContributions.push_back(line); } - + //同上两个代码块 } +//初始化构造函数 void AboutScreen::onExit() { if (music_playing()) stop_music(); } +//在退出以后关闭音乐播放的函数 void AboutScreen::readInput() { if (quit_requested() || key_down(ESCAPE_KEY)) this->m_shouldQuit = true; } +//将m_shouldQuit标识改成true的函数(如果用户请求) void AboutScreen::tick() { - this->shiftTitle(); - this->shiftStars(); + this->shiftTitle();//更新标题位置 + this->shiftStars();//更新星星位置 this->tickContributor(); // Every 1/4 second. @@ -102,65 +118,70 @@ void AboutScreen::tick() { if (! music_playing()) play_music("music_about"); } - - this->m_ticker++; + //每0.25秒检查一次,因为我们是60fps,15帧近似与0.25秒 + this->m_ticker++;//增加帧数记录 } +//每帧更新函数,包括位置音乐计时 void AboutScreen::render() { - clear_screen(COLOR_BLACK); + clear_screen(COLOR_BLACK);//清理屏幕使其成为黑色 - this->renderStars(); - this->renderTitle(); - this->renderDescription(); - this->renderContributor(); + this->renderStars();//渲染星星 + this->renderTitle();//渲染标题 + this->renderDescription();//渲染描述 + this->renderContributor();//渲染贡献者 - refresh_screen(); + refresh_screen();//刷新屏幕展现渲染结果 } - +//每调用一次依次执行:清屏,渲染星星,渲染标题,渲染描述,渲染贡献者,刷新屏幕,后渲染者在先渲染者之上 void AboutScreen::shiftTitle() { this->m_titleX -= 6; if (this->m_titleX < this->m_titleEnd) this->m_titleX = ARCADE_MACHINE_RES_X; } +//每次调用都让m_titleX向左平移6个单位,当m_titleXm_titleX; + double x = this->m_titleX;//一开始x=this->m_titleX也就是在屏幕最右方 for (int i=0; im_title.length(); ++i) { double y = TITLE_FONT_Y + (sin(x / 80) * 115); + //通过sin里带入X值,实现标题y的上下浮动,且因为每个字符的X值不同,所以浮动高度不同,又由于sin函数的特性,会导致看起来像波浪形状,浮动范围在上下115 double fontSize = TITLE_FONT_SIZE + (sin(x / 120) * 8); - + //同理,但是这里是为了改变字符大小,字符大小波动在上下8 color c = this->getRainbowShade(x); - + //getRainbowShade(x)通过随机字符颜色 draw_text( - this->m_title.substr(i, 1), - c, - "font_about", - fontSize, - x, - y + this->m_title.substr(i, 1), //单独取每个字符渲染 + c,//字符颜色 + "font_about",//字符字体 + fontSize,//字符大小 + x,//字符X位置 + y//字符Y位置 ); - x += TITLE_FONT_CHAR_WIDTH; + x += TITLE_FONT_CHAR_WIDTH;//两个字符间距 } + //(一个一个!)地渲染字符 } +//渲染标题,通过sin函数和getRainbowShade()还有一个一个字渲染的方法实现标题波浪形运行,字体忽大忽小,颜色彩虹的效果 void AboutScreen::shiftStars() { for (int i=0; im_stars.size(); ++i) { @@ -169,29 +190,35 @@ void AboutScreen::shiftStars() { this->m_stars[i].x = -10; } } +//每次调用shiftStars()时,为每一颗星星的X加上随机数distance,如果加完以后星星超出右边界,则把星星放到-10的位置,然后再飘回来,达到动态效果 void AboutScreen::renderStars() { for (auto star : this->m_stars) fill_rectangle(star.c, star.x, star.y, star.distance / 2, star.distance / 4); } +//渲染星星的函数fill_rectangle()最后两项是矩形宽高,所以这个渲染会出现近的星星更大更快,远的星星更小更慢的情况,所以这里的distance是深度/速度等级 void AboutScreen::renderDescription() { - double offset = sin(((double)this->m_ticker / 16)) * 12; - double offsetX = sin((double)this->m_ticker / 24) * 19; - double y = 480 + offset; - double x = 140 + offsetX; + double offset = sin(((double)this->m_ticker / 16)) * 12;//计算X方向波动的影响因子,范围在-12到12个像素点 + double offsetX = sin((double)this->m_ticker / 24) * 19;//计算Y方向波动的影响因子,范围在-12到12个像素点 + //计算XY方向波动的影响因子 + double y = 480 + offset;//计算Y方向总共的波动值 + double x = 140 + offsetX;//计算X方向总共的波动值 + //引入sin主导的波动影响因子,使得文本描述会周期性波动 int maxIOffset = 0; for (int i=0; im_linesOfCode.size(); ++i) { maxIOffset = (i * 32); draw_text(this->m_linesOfCode[i], COLOR_WHITE, "font_about", 18, x, y + (maxIOffset)); } - + //在描述的下面间隔32再次渲染linesOfCode的情况,也是每行间隔maxIOffset,32 y = y + maxIOffset + 64; draw_text("Contributions", COLOR_WHITE, "font_about", 18, x, y); y += 32; @@ -200,22 +227,23 @@ void AboutScreen::renderDescription() { maxIOffset = (i * 32); draw_text(this->m_gitContributions[i], COLOR_WHITE, "font_about", 18, x, y + (maxIOffset)); } - + //在linesOfCode的下面间隔32再次渲染Contributions的情况,也是每行间隔maxIOffset,32 } +//渲染描述,贡献者代码行数,贡献者情况 void AboutScreen::loop() { while (! this->m_shouldQuit) { - process_events(); + process_events();//调用splashkit.h里的事件处理函数 - this->readInput(); - this->tick(); - this->render(); + this->readInput();//检查有没有申请退出 + this->tick();//更新位置音乐计时器 + this->render();//渲染画面 - delay(1000 / 60); + delay(1000 / 60);//定下FPS为60 } } - +//循环函数 void AboutScreen::main() { // Clear music and start the about screen music. @@ -227,32 +255,35 @@ void AboutScreen::main() { // Tidy up once the loop is done. this->onExit(); } +//AboutScreen的入口函数 void AboutScreen::tickContributor() { this->m_contributorTicker++; if (this->m_contributorTicker >= CONTRIBUTION_TIME) { - this->m_contributorTicker = 0; - this->m_contributorsIndex = (this->m_contributorsIndex + 1) % this->m_contributors.size(); + this->m_contributorTicker = 0;//每过180帧重置,然后增加index换下一个人,在FPS为60的情况下它的时间为3s + this->m_contributorsIndex = (this->m_contributorsIndex + 1) % this->m_contributors.size();//取模防止越界且起到循环轮播效果,比如如果index现在是5,m_contributors size是4,那就取1轮播 } } +//实现循环轮播贡献者名字的支撑逻辑,一个3s void AboutScreen::renderContributor() { - int r = this->m_contributorTicker % CONTRIBUTION_TIME; - double ratio = 1; - double fontSize = 32; - double fontRatio = 1; + int r = this->m_contributorTicker % CONTRIBUTION_TIME;//r会始终保持在0-179的范围 + double ratio = 1;//字号透明度 + double fontSize = 32;//字号大小 + double fontRatio = 1;//字号倍率 if (r < CONTRIBUTION_FADE_TIME) { - ratio = r / (double)CONTRIBUTION_FADE_TIME; - fontRatio = 1 + ((1 - ratio) * 4); + ratio = r / (double)CONTRIBUTION_FADE_TIME;//随着r的增大,ratio会越来越大,也就是越来越亮直到1 + fontRatio = 1 + ((1 - ratio) * 4);//CONTRIBUTION_FADE_TIME是=30帧,也就是淡化阶段,时间为0.5s,随着ratio的增大,字体倍率会越来越小,也就是慢慢变小 + //实现淡入阶段效果,由大而淡变成小而亮 } else if ((CONTRIBUTION_TIME - r) < CONTRIBUTION_FADE_TIME) { - ratio = (CONTRIBUTION_TIME - r) / (double)CONTRIBUTION_FADE_TIME; - fontRatio = ratio; + ratio = (CONTRIBUTION_TIME - r) / (double)CONTRIBUTION_FADE_TIME;//随着r的增大,ratio会越来越小,也就是亮度越来越小 + fontRatio = ratio;//将字体倍率大小和亮度相等,实现淡出效果 } - fontSize = fontSize * fontRatio; + fontSize = fontSize * fontRatio;//实现字体大小和字体倍率之间的关系控制 if (ratio > 1.0) - ratio = 1.0; + ratio = 1.0;//限制ratio大小,防止出错 color c; c.r = ratio; @@ -266,15 +297,15 @@ void AboutScreen::renderContributor() { double y = 0; fontSize = 24; for (int i=0; im_ticker + (i * 4)) / (double)16) * (double)9; - x = sin(this->m_ticker / (double) 50) * (double)225; - double actualX = 1300 + x + (i * 28); + y = sin((this->m_ticker + (i * 4)) / (double)16) * (double)9;//让每个字符Y位置不同,上下9个像素 + x = sin(this->m_ticker / (double) 50) * (double)225;//让每个字符X位置产生变化,左右225个像素 + double actualX = 1300 + x + (i * 28);//计算每个字符的实际位置,间隔28 - fontRatio = sin((actualX + 190) / (double)80) * (double)1.5; + fontRatio = sin((actualX + 190) / (double)80) * (double)1.5;//让每个字符大小不一样,1-1.5之间 if (fontRatio < 1) - fontRatio = (double)1; + fontRatio = (double)1;//限制fontRatio大小,防止太小 - color c = this->getRainbowShade(actualX - 350); - draw_text(createdBy.substr(i, 1), c, "font_about", (double)24 * fontRatio, actualX, (double)500 + y); + color c = this->getRainbowShade(actualX - 350);//将字符位置丢给彩虹函数生成彩虹效果 + draw_text(createdBy.substr(i, 1), c, "font_about", (double)24 * fontRatio, actualX, (double)500 + y);//绘制 } -} \ No newline at end of file +}//绘制贡献者,彩虹效果,淡入淡出,位置波动 \ No newline at end of file diff --git a/src/ArcadeMachine.cpp b/src/ArcadeMachine.cpp index 3ccb8df..906ae36 100644 --- a/src/ArcadeMachine.cpp +++ b/src/ArcadeMachine.cpp @@ -342,15 +342,55 @@ void ArcadeMachine::playArcadeTeamIntro() */ void ArcadeMachine::playSplashKitIntro() { - // Pull the most recent version of the arcade-games repo. - do + const int MAX_RETRIES = 3; + const int RETRY_DELAY_MS = 30000; + + for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { - // Draw SplashKit productions screen - this->m_introSplashkit.drawTitlePage(); - draw_text("Loading...", COLOR_SLATE_GRAY, "font_text", 60, ARCADE_MACHINE_RES_X / 2 - 100, ARCADE_MACHINE_RES_Y / 2 + 350); + // Show attempt status on screen and terminal + std::string attemptMsg = "Connecting... (attempt " + std::to_string(attempt) + "/" + std::to_string(MAX_RETRIES) + ")"; + std::cerr << "[Git] " << attemptMsg << std::endl; + + process_events(); + clear_screen(); + m_introSplashkit.drawTitlePage(); + draw_text(attemptMsg, COLOR_SLATE_GRAY, "font_text", 40, ARCADE_MACHINE_RES_X / 2 - 220, ARCADE_MACHINE_RES_Y / 2 + 350); refresh_screen(); - - } while (!this->m_config.getFromGit("https://github.com/thoth-tech/arcade-games.git", "games")); + + if (this->m_config.getFromGit("https://github.com/thoth-tech/arcade-games.git", "games")) + { + std::cerr << "[Git] games loaded successfully on attempt " << attempt << std::endl; + return; + } + + // This attempt failed + if (attempt < MAX_RETRIES) + { + std::string retryMsg = "Failed. Retrying in 30 seconds... (" + std::to_string(attempt) + "/" + std::to_string(MAX_RETRIES) + ")"; + std::cerr << "[Git] attempt " << attempt << " failed. Retrying in 30 seconds..." << std::endl; + + process_events(); + clear_screen(); + m_introSplashkit.drawTitlePage(); + draw_text(retryMsg, COLOR_RED, "font_text", 40, ARCADE_MACHINE_RES_X / 2 - 300, ARCADE_MACHINE_RES_Y / 2 + 350); + refresh_screen(); + delay(RETRY_DELAY_MS); + } + } + + // All retries failed — show error on screen and terminal, then exit + std::string failMsg1 = "Failed to load games after " + std::to_string(MAX_RETRIES) + " attempts."; + std::string failMsg2 = "Check network connection and restart."; + std::cerr << "[Git] " << failMsg1 << " Exiting." << std::endl; + + process_events(); + clear_screen(); + m_introSplashkit.drawTitlePage(); + draw_text(failMsg1, COLOR_RED, "font_text", 40, ARCADE_MACHINE_RES_X / 2 - 300, ARCADE_MACHINE_RES_Y / 2 + 320); + draw_text(failMsg2, COLOR_RED, "font_text", 40, ARCADE_MACHINE_RES_X / 2 - 280, ARCADE_MACHINE_RES_Y / 2 + 370); + refresh_screen(); + delay(5000); + exit(EXIT_FAILURE); } /** diff --git a/src/Button.cpp b/src/Button.cpp index 948c0c4..85dfa58 100644 --- a/src/Button.cpp +++ b/src/Button.cpp @@ -52,7 +52,7 @@ Button::Button(Color c, float x, float y, int xCell, int yCell, float scale) sprite_set_x(this->m_btn, this->m_x - this->m_centreX); sprite_set_y(this->m_btn, this->m_y - this->m_centreY); // store the window centre point of button as location - this->m_btnLocation = center_point(this->m_btn); + this->m_btnLocation = point_at(this->m_x, this->m_y); // scale the sprite sprite_set_scale(this->m_btn, scale); } diff --git a/src/Menu.cpp b/src/Menu.cpp index 84423dd..3ce2b28 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -6,9 +6,19 @@ #endif #include +#include + +#if __cplusplus >= 201703L +#include +namespace fs = std::filesystem; +#else +#include +namespace fs = std::experimental::filesystem; +#endif Menu::Menu() { + this->m_tip = nullptr; this->m_db = new Database(); Table *gameDataTable = new Table("gameData", {{"gameName", "TEXT"}, {"startTime", "TEXT"}, {"endTime", "TEXT"}, {"rating", "TEXT"}, {"highScore", "TEXT"}}); this->m_db->createTable(gameDataTable); @@ -17,6 +27,7 @@ Menu::Menu() Menu::Menu(std::vector configs) { + this->m_tip = nullptr; this->m_games = configs; this->m_db = new Database(); Table *gameDataTable = new Table("gameData", {{"gameName", "TEXT"}, {"startTime", "TEXT"}, {"endTime", "TEXT"}, {"rating", "TEXT"}, {"highScore", "TEXT"}}); @@ -31,6 +42,24 @@ Menu::~Menu() { std::cout << "Destructor called on Menu\n"; std::cout << "Menu: clearing memory...\n"; + + // Walk the circular doubly-linked list and free every ButtonNode and its Button. + // getButtons() always returned an empty m_btns, so ArcadeMachine's destructor + // never freed these — they must be freed here. + if (m_button) { + ButtonNode *current = m_button->getNext(); + while (current != m_button) { + ButtonNode *next = current->getNext(); + delete current->button; + delete current; + current = next; + } + // Delete the head node last (after all others are gone) + delete m_button->button; + delete m_button; + m_button = nullptr; + } + delete m_db; delete m_tip; } @@ -80,15 +109,30 @@ void Menu::createButtons() for (int i = 0; i < m_gameImages.size(); i++) { + std::string image = m_gameImages[i]; + + // Load the game thumbnail bitmap with specific error reporting. + if (m_games[i].image().empty()) { + std::cerr << "[Menu] image load failed (empty path): " << m_games[i].title() << std::endl; + } else if (!fs::exists(image)) { + std::cerr << "[Menu] image load failed (file not found): " << image << std::endl; + } else { + if (!has_bitmap(image)) + load_bitmap(image, image); + if (!has_bitmap(image)) + std::cerr << "[Menu] image load failed (splashkit error): " << image << std::endl; + else + std::cerr << "[Menu] image loaded: " << image << std::endl; + } + if (i == 0) { - this->m_button = new ButtonNode(new GameScreenButton(Button::GAME, m_gameImages[0])); + this->m_button = new ButtonNode(new GameScreenButton(Button::GAME, image)); this->m_button->config = m_games[0]; this->m_button->stats = gameStats.getStats(this->m_db, m_games[0].title()); } else { - std::string image = m_gameImages[i]; this->m_button->addBefore(new ButtonNode(new GameScreenButton(Button::GAME, image))); this->m_button->getPrev()->config = m_games[i]; this->m_button->getPrev()->stats = gameStats.getStats(this->m_db, m_games[i].title()); @@ -146,6 +190,15 @@ void Menu::carouselHandler() checkGameExit(); + // ESC while in game: kill the game process + // Use key_typed() directly since game window may have stolen focus from selector + if (m_inGame && key_typed(ESCAPE_KEY)) { + std::cerr << "[Menu] user pressed ESC, killing game (PID: " << this->m_processId << ")" << std::endl; + killProcess(this->m_processId); + m_launching = false; + m_inGame = false; + } + if (this->m_button) { if (this->m_action == "escape" && m_overlayActive) @@ -156,16 +209,6 @@ void Menu::carouselHandler() { if (m_overlayActive) { - -#ifdef _WIN32 - // Get game path - m_gamePath = (this->m_button->config.folder() + "/" + this->m_button->config.win_exe()).c_str(); - // Get executable name - m_gameExe = strdup(this->m_button->config.win_exe().c_str()); - // Get game directory - m_gameDir = this->m_button->config.folder().c_str(); -#endif - // Set the center of the game this->m_x = m_centreX; this->m_y = m_centreY; @@ -189,15 +232,19 @@ void Menu::carouselHandler() m_gameData.setGameName(this->m_button->config.title()); #ifdef _WIN32 - // Get game path - m_gamePath = (this->m_button->config.folder() + "/" + this->m_button->config.win_exe()).c_str(); - // Get executable name - m_gameExe = strdup(this->m_button->config.win_exe().c_str()); - // Get game directory - m_gameDir = this->m_button->config.folder().c_str(); - - // Call method to open game executable - startGame(m_gamePath, m_gameExe, m_gameDir); + { + // Build persistent strings before passing to CreateProcessA. + // Storing .c_str() of a temporary causes a dangling pointer — + // the temporary is destroyed at the end of the assignment statement. + std::string gamePath = this->m_button->config.folder() + "/" + this->m_button->config.win_exe(); + std::string gameDir = this->m_button->config.folder(); + // CreateProcessA may write into lpCommandLine, so use a mutable buffer. + std::string gameExeStr = this->m_button->config.win_exe(); + std::vector gameExeBuf(gameExeStr.begin(), gameExeStr.end()); + gameExeBuf.push_back('\0'); + + startGame(gamePath.c_str(), gameExeBuf.data(), gameDir.c_str()); + } #else try { startGame(this->m_button->config.getExecutablePath()); @@ -238,9 +285,27 @@ void Menu::drawMenuPage() if (m_overlayActive && !m_menuSliding) drawOverlay(m_button->config, m_button->stats); - if (!m_inGame) + if (!m_inGame && this->m_tip) this->m_tip->draw(); + // Show launching animation until game process exits. + if (m_inGame && m_launching) { + int dots = (current_ticks() / 400) % 4; + std::string anim = std::string(dots, '.'); + std::string msg = "Starting " + this->m_button->config.title() + anim; + draw_text(msg, COLOR_WHITE, "font_text", 40, + m_centreX - 200, m_centreY - 20); + } + + // Show launch error message (fork failure or crash). + if (!m_inGame && !m_launchError.empty()) { + draw_text(m_launchError, COLOR_RED, "font_text", 30, + m_centreX - 300, m_centreY - 20); + // Clear after 3 seconds so the menu returns to normal. + if (current_ticks() - m_launchTime > 3000) + m_launchError = ""; + } + updateCarousel(); carouselHandler(); } @@ -452,8 +517,22 @@ void Menu::startGame(LPCSTR gamePath,LPSTR gameExe, LPCSTR gameDirectory) #else void Menu::startGame(struct s_ExecutablePath path) { if (!this->m_inGame) { + std::cerr << "[Menu] starting game: " << path.file << " from " << path.path << std::endl; + m_launching = true; + m_launchError = ""; + this->m_processId = spawnProcess(path.path, path.file); - this->m_inGame = true; + + if (this->m_processId == -1) { + std::cerr << "[Menu] failed to start game: fork failed" << std::endl; + m_launching = false; + m_launchTime = current_ticks(); + m_launchError = "Failed to start game — see terminal for details"; + } else { + std::cerr << "[Menu] game process spawned (PID: " << this->m_processId << ")" << std::endl; + this->m_inGame = true; + this->m_launchTime = current_ticks(); + } } } #endif @@ -481,7 +560,19 @@ void Menu::checkGameExit() m_gameData.setHighScore(highScore); } #else - if (! processRunning(this->m_processId)) { + if (! processRunning(this->m_processId, m_lastExitCode)) { + unsigned int runTime = current_ticks() - m_launchTime; + if (m_lastExitCode != 0) { + std::cerr << "[Menu] game crashed: " << this->m_button->config.title() + << " (PID: " << this->m_processId << ", exit code: " << m_lastExitCode + << ", ran for " << runTime << "ms)" << std::endl; + m_launchError = this->m_button->config.title() + " crashed (exit code: " + std::to_string(m_lastExitCode) + ")"; + m_launchTime = current_ticks(); + } else { + std::cerr << "[Menu] game exited normally: " << this->m_button->config.title() + << " (PID: " << this->m_processId << ", ran for " << runTime << "ms)" << std::endl; + } + m_launching = false; this->m_inGame = false; int highScore = 0; m_gameData.setEndTime(time(0)); diff --git a/src/Option.cpp b/src/Option.cpp index 9c284d3..708405e 100644 --- a/src/Option.cpp +++ b/src/Option.cpp @@ -2,6 +2,25 @@ Option::Option() {} +Option::~Option() +{ + // Free ButtonNode objects only — Button pointers are owned by m_optionsBtns, + // so do NOT delete button inside each node here (would double-free). + if (m_optionsButtonNode) { + ButtonNode *current = m_optionsButtonNode->getNext(); + while (current != m_optionsButtonNode) { + ButtonNode *next = current->getNext(); + delete current; + current = next; + } + delete m_optionsButtonNode; + m_optionsButtonNode = nullptr; + } + // Free Button objects via m_optionsBtns (the single owner) + for (auto& btn : m_optionsBtns) delete btn; + m_optionsBtns.clear(); +} + void Option::createOptionsButtons() { // Initialise grid @@ -70,7 +89,7 @@ void Option::drawOptionsMenu() this->m_optionsButtonNode = this->m_selectorOptionsMenu.checkKeyInput(this->m_optionsButtonNode); int x_pos = home.button->x() + (sprite_width(home.button->btn()) - 40); - int y_pos; + int y_pos = home.button->y() + ((sprite_width(home.button->btn())*0.5)-30); // default to home position if (this->m_optionsButtonNode->button->color() == "opts_home") y_pos = home.button->y() + ((sprite_width(home.button->btn())*0.5)-30); else if (this->m_optionsButtonNode->button->color() == "opts_sound") y_pos = sound.button->y() + ((sprite_width(sound.button->btn())*0.5)-30); diff --git a/src/Process.cpp b/src/Process.cpp index b536550..5df91f5 100644 --- a/src/Process.cpp +++ b/src/Process.cpp @@ -4,7 +4,11 @@ #include #include #include +#include #include +#include +#include +#include pid_t spawnProcess(std::string directory, std::string fileName) { @@ -13,20 +17,36 @@ pid_t spawnProcess(std::string directory, std::string fileName) { // First, fork the current process into a new process. // This is required to ensure that process execution occurs concurrently. pid_t processId = fork(); - if (processId > 0) + + if (processId == -1) { + std::cerr << "[Process] fork failed: " << strerror(errno) << std::endl; + return -1; + } + + if (processId > 0) { + std::cerr << "[Process] spawned game: " << directory << "/" << fileName << " (PID: " << processId << ")" << std::endl; return processId; + } + + // Create new process group so we can kill the entire game tree later + setpgid(0, 0); std::array buffer; // The working directory must be changed to the root directory of the game - // to ensure that SplashKit resources are correctly pathed when the process + // to ensure that SplashKit resources are correctly pathed when the process // executes. - chdir(directory.c_str()); + if (chdir(directory.c_str()) != 0) { + std::cerr << "[Process] chdir failed: " << directory << std::endl; + exit(EXIT_FAILURE); + } - std::string cmd = "./builds/" + fileName; + std::string exePath = "./builds/" + fileName; + chmod(exePath.c_str(), 0755); + std::string cmd = exePath + " 2>&1"; auto pipe = popen(cmd.c_str(), "r"); if (! pipe) { - std::cerr << "Error executing popen" << std::endl; + std::cerr << "[Process] popen failed: " << cmd << std::endl; exit(EXIT_FAILURE); } @@ -34,12 +54,13 @@ pid_t spawnProcess(std::string directory, std::string fileName) { // streams. This could be extended by piping them into shared // memory for consumption by the main process. while (fgets(buffer.data(), 128, pipe) != NULL) { - ; + std::cerr << "[Game] " << buffer.data(); } - pclose(pipe); + int ret = pclose(pipe); + int gameExitCode = WIFEXITED(ret) ? WEXITSTATUS(ret) : 1; - exit(EXIT_SUCCESS); + exit(gameExitCode); #endif @@ -48,13 +69,32 @@ pid_t spawnProcess(std::string directory, std::string fileName) { } -bool processRunning(pid_t processId) { +bool processRunning(pid_t processId, int &exitCode) { #ifndef _WIN32 - return getpgid(processId) >= 0; + int status; + pid_t result = waitpid(processId, &status, WNOHANG); + if (result == 0) return true; // still running + if (result == processId) { + if (WIFEXITED(status)) + exitCode = WEXITSTATUS(status); + else if (WIFSIGNALED(status)) + exitCode = 128 + WTERMSIG(status); // signal = non-zero = crash + else + exitCode = -1; + } + return false; #endif // TODO: Add Windows support. + exitCode = 0; return false; -} \ No newline at end of file +} + +void killProcess(pid_t processId) { +#ifndef _WIN32 + std::cerr << "[Process] killing game process group (PID: " << processId << ")" << std::endl; + kill(-processId, SIGTERM); +#endif +} diff --git a/src/program.cpp b/src/program.cpp index 8dac613..9c2ebb0 100644 --- a/src/program.cpp +++ b/src/program.cpp @@ -1,5 +1,15 @@ #include "Configuration.h" #include "ArcadeMachine.h" +#include + +#if __cplusplus >= 201703L +#include +namespace fs = std::filesystem; +#else +#include +namespace fs = std::experimental::filesystem; +#endif + #define PLAY_INTRO true #define LOAD_GAMES true @@ -7,15 +17,58 @@ int main(void) { // Load all resources set_resources_path("resources" ARCADE_MACHINE_PATH_SEP); + + if (!fs::exists("resources/bundles/resources.txt")) + std::cerr << "[Bundle] resources.txt not found — check resources/ directory" << std::endl; + load_resource_bundle("bundle", "resources.txt"); - // Instantiate Arcade Machine - ArcadeMachine Arcade; + if (has_resource_bundle("bundle")) + std::cerr << "[Bundle] resource bundle loaded successfully" << std::endl; + else + std::cerr << "[Bundle] resource bundle failed to load (unknown error)" << std::endl; // Open window and toggle border off. + // Note: bitmap/font checks must happen AFTER open_window() since SplashKit + // needs a render context to load graphical resources. open_window("arcade-machine", ARCADE_MACHINE_RES_X, ARCADE_MACHINE_RES_Y); window_toggle_border("arcade-machine"); + // Instantiate Arcade Machine + ArcadeMachine Arcade; + + // Check all critical bitmaps and fonts loaded correctly. + // Must happen after open_window() — SplashKit needs a render context. + std::vector bitmaps = { + "thoth", "intro_splashkit", "intro_thoth_tech", "intro_arcade_team", + "games_dashboard", "back_ground", "in_game_bgnd", "rating_bg", "options_thoth", + "btn_play", "btn_exit", "btn_opts", "play_hghlt", "exit_hghlt", "options_hghlt", + "game_hghlt", "opts_home", "opts_sound", "opts_display", "opts_stats", + "opts_home_hghlt", "opts_sound_hghlt", "opts_display_hghlt", "opts_stats_hghlt", + "backCurrentGame", "backGame_notSelected", "changeSound", "sound_notSelected", + "backMenu_notSelected", "backMenu", "cursor", "information", "star-gold", "star-black" + }; + std::vector fonts = { + "font_btn", "font_title", "font_text", "font_star", "font_about" + }; + int failCount = 0; + for (const auto& name : bitmaps) { + if (!has_bitmap(name)) { + std::cerr << "[Bundle] bitmap not loaded: " << name << std::endl; + failCount++; + } + } + for (const auto& name : fonts) { + if (!has_font(name)) { + std::cerr << "[Bundle] font not loaded: " << name << std::endl; + failCount++; + } + } + if (failCount == 0) + std::cerr << "[Bundle] all bitmaps and fonts loaded successfully" << std::endl; + else + std::cerr << "[Bundle] " << failCount << " resource(s) failed to load" << std::endl; + #if PLAY_INTRO == true // Play Thoth Tech intro Arcade.playThothTechIntro(); From 93d726e5c0d24e1159d1e2abf9e7aa2ff04ea99b Mon Sep 17 00:00:00 2001 From: HAOYU LIU Date: Sat, 23 May 2026 22:08:13 +1000 Subject: [PATCH 2/2] refactor: translate Chinese comments to English in AboutScreen.cpp --- src/AboutScreen.cpp | 204 ++++++++++++++++++++++---------------------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/src/AboutScreen.cpp b/src/AboutScreen.cpp index 96dec9b..139805b 100644 --- a/src/AboutScreen.cpp +++ b/src/AboutScreen.cpp @@ -30,61 +30,61 @@ static const char *description[] = { static const std::string createdBy = "Created by"; AboutScreen::AboutScreen() { - this->m_shouldQuit = false;//初始化退出标志为False - this->m_titleX = ARCADE_MACHINE_RES_X;//将标题的X位置定位在屏幕最右边 - this->m_title = std::string(title);//将m_title变成string风格的字符串,方便进行各种操作,如lenth() + this->m_shouldQuit = false; // Initialise quit flag to false + this->m_titleX = ARCADE_MACHINE_RES_X; // Set title X to the far right of the screen + this->m_title = std::string(title); // Convert to std::string for convenient operations this->m_titleEnd = ((TITLE_FONT_CHAR_WIDTH * this->m_title.length())); this->m_titleEnd *= -1; - /*这里的设计涉及到打印逻辑,在打印标题的时候,每打印一个字符,m_titleX的位置就加一个TITLE_FONT_CHAR_WIDTH,所以标题的总长度应为TITLE_FONT_CHAR_WIDTH * this->m_title.length() - 也就是字符长度(m_title.length())乘以字符宽度(TITLE_FONT_CHAR_WIDTH),而当字符从右往左完全滚动出屏幕之时,第一个字符的X位置应该刚好是-标题总长度,因为此时 - 最右边的字符坐标应该为0,所以m_titleEnd的位置应该是-标题总长度=-((TITLE_FONT_CHAR_WIDTH * this->m_title.length()));,也就是37行m_titleEnd的最终值 - */ - this->m_stars = std::vector();//初始化数组m_stars + /* Each character advances X by TITLE_FONT_CHAR_WIDTH; the title scrolls fully off-screen + when the first character reaches x = -(TITLE_FONT_CHAR_WIDTH * title.length()). */ + + + this->m_stars = std::vector(); // Initialise the m_stars array this->m_contributorsIndex = 0; this->m_contributorTicker = 0; this->m_ticker = 0; for (int i=0; im_stars.push_back(star); } - //循环生成所有结构体(1024颗)s_star星星的循环,包括每个星星的X,Y位置,亮度,distance,并把这些结构体push进数组m_stars + // Generates all 1024 s_star structs (X, Y, brightness, distance) and appends to m_stars - this->m_contributors = std::vector();//初始化数组m_contributors + this->m_contributors = std::vector(); // Initialise the m_contributors array std::ifstream contributors("stats" ARCADE_MACHINE_PATH_SEP "contributors.txt"); - //使用ifstream(输入文件流)输入贡献者名单文件 ARCADE_MACHINE_PATH_SEP是为了应对不同系统风格的分隔符 + // Open contributor list via ifstream; ARCADE_MACHINE_PATH_SEP handles OS path separators std::string line; while (std::getline(contributors, line)) { if (line.length() > 0) this->m_contributors.push_back(line); } contributors.close(); - //按行读取贡献者名单,并把贡献者名字加入到m_contributors数组里 + // Read contributor names line by line and append to m_contributors - this->m_linesOfCode = std::vector();//初始化m_linesOfCode数组 + this->m_linesOfCode = std::vector(); // Initialise the m_linesOfCode array std::ifstream linesOfCode("stats" ARCADE_MACHINE_PATH_SEP "lines-of-code.txt"); while (std::getline(linesOfCode, line)) { if (line.length() > 0) this->m_linesOfCode.push_back(line); } - //按行读取贡献者代码数量文件,并把贡献者代码数量加入到m_linesOfCode数组里 + // Read lines-of-code counts line by line and append to m_linesOfCode this->m_gitContributions = std::vector(); std::ifstream contributions("stats" ARCADE_MACHINE_PATH_SEP "git.txt"); @@ -92,25 +92,25 @@ AboutScreen::AboutScreen() { if (line.length() > 0) this->m_gitContributions.push_back(line); } - //同上两个代码块 + // Same pattern as the two blocks above } -//初始化构造函数 +// End of constructor void AboutScreen::onExit() { if (music_playing()) stop_music(); } -//在退出以后关闭音乐播放的函数 +// Stop music playback on exit void AboutScreen::readInput() { if (quit_requested() || key_down(ESCAPE_KEY)) this->m_shouldQuit = true; } -//将m_shouldQuit标识改成true的函数(如果用户请求) +// Set m_shouldQuit to true when the user requests quit void AboutScreen::tick() { - this->shiftTitle();//更新标题位置 - this->shiftStars();//更新星星位置 + this->shiftTitle(); // Advance title scroll position + this->shiftStars(); // Advance star positions this->tickContributor(); // Every 1/4 second. @@ -118,70 +118,70 @@ void AboutScreen::tick() { if (! music_playing()) play_music("music_about"); } - //每0.25秒检查一次,因为我们是60fps,15帧近似与0.25秒 - this->m_ticker++;//增加帧数记录 + // Check every 0.25 s (15 frames at 60 fps) + this->m_ticker++; // Increment frame counter } -//每帧更新函数,包括位置音乐计时 +// Per-frame update: scroll, stars, contributor ticker, and music void AboutScreen::render() { - clear_screen(COLOR_BLACK);//清理屏幕使其成为黑色 + clear_screen(COLOR_BLACK); // Clear screen to black - this->renderStars();//渲染星星 - this->renderTitle();//渲染标题 - this->renderDescription();//渲染描述 - this->renderContributor();//渲染贡献者 + this->renderStars(); // Render starfield + this->renderTitle(); // Render scrolling title + this->renderDescription(); // Render description text + this->renderContributor(); // Render contributor name - refresh_screen();//刷新屏幕展现渲染结果 + refresh_screen(); // Present the rendered frame } -//每调用一次依次执行:清屏,渲染星星,渲染标题,渲染描述,渲染贡献者,刷新屏幕,后渲染者在先渲染者之上 +// Each call: clear, stars, title, description, contributor, refresh void AboutScreen::shiftTitle() { this->m_titleX -= 6; if (this->m_titleX < this->m_titleEnd) this->m_titleX = ARCADE_MACHINE_RES_X; } -//每次调用都让m_titleX向左平移6个单位,当m_titleXm_titleX;//一开始x=this->m_titleX也就是在屏幕最右方 + double x = this->m_titleX; // Start at current scroll position for (int i=0; im_title.length(); ++i) { double y = TITLE_FONT_Y + (sin(x / 80) * 115); - //通过sin里带入X值,实现标题y的上下浮动,且因为每个字符的X值不同,所以浮动高度不同,又由于sin函数的特性,会导致看起来像波浪形状,浮动范围在上下115 + // Unique X per character produces wave shape via sin; amplitude +-115 px double fontSize = TITLE_FONT_SIZE + (sin(x / 120) * 8); - //同理,但是这里是为了改变字符大小,字符大小波动在上下8 + // Same principle for font size; amplitude +-8 px color c = this->getRainbowShade(x); - //getRainbowShade(x)通过随机字符颜色 + // Get rainbow colour keyed to character position draw_text( - this->m_title.substr(i, 1), //单独取每个字符渲染 - c,//字符颜色 - "font_about",//字符字体 - fontSize,//字符大小 - x,//字符X位置 - y//字符Y位置 + this->m_title.substr(i, 1), // Render one character at a time + c, // Character colour + "font_about", // Font + fontSize, // Font size + x, // X position + y // Y position ); - x += TITLE_FONT_CHAR_WIDTH;//两个字符间距 + x += TITLE_FONT_CHAR_WIDTH; // Advance X by one character width } - //(一个一个!)地渲染字符 + // Characters are rendered one at a time } -//渲染标题,通过sin函数和getRainbowShade()还有一个一个字渲染的方法实现标题波浪形运行,字体忽大忽小,颜色彩虹的效果 +// Renders title with wave motion, size variation, and rainbow colouring void AboutScreen::shiftStars() { for (int i=0; im_stars.size(); ++i) { @@ -190,35 +190,35 @@ void AboutScreen::shiftStars() { this->m_stars[i].x = -10; } } -//每次调用shiftStars()时,为每一颗星星的X加上随机数distance,如果加完以后星星超出右边界,则把星星放到-10的位置,然后再飘回来,达到动态效果 +// Each call moves every star right by its distance; stars past the right edge wrap to x=-10 void AboutScreen::renderStars() { for (auto star : this->m_stars) fill_rectangle(star.c, star.x, star.y, star.distance / 2, star.distance / 4); } -//渲染星星的函数fill_rectangle()最后两项是矩形宽高,所以这个渲染会出现近的星星更大更快,远的星星更小更慢的情况,所以这里的distance是深度/速度等级 +// fill_rectangle width/height scale with distance, giving a parallax depth effect void AboutScreen::renderDescription() { - double offset = sin(((double)this->m_ticker / 16)) * 12;//计算X方向波动的影响因子,范围在-12到12个像素点 - double offsetX = sin((double)this->m_ticker / 24) * 19;//计算Y方向波动的影响因子,范围在-12到12个像素点 - //计算XY方向波动的影响因子 - double y = 480 + offset;//计算Y方向总共的波动值 - double x = 140 + offsetX;//计算X方向总共的波动值 - //引入sin主导的波动影响因子,使得文本描述会周期性波动 + double offset = sin(((double)this->m_ticker / 16)) * 12; // Y oscillation, +-12 px + double offsetX = sin((double)this->m_ticker / 24) * 19; // X oscillation, +-19 px + // Compute XY oscillation offsets for a floating text effect + double y = 480 + offset; // Base Y with oscillation applied + double x = 140 + offsetX; // Base X with oscillation applied + // Sin-driven offsets make the text float in a gentle periodic motion int maxIOffset = 0; for (int i=0; im_linesOfCode.size(); ++i) { maxIOffset = (i * 32); draw_text(this->m_linesOfCode[i], COLOR_WHITE, "font_about", 18, x, y + (maxIOffset)); } - //在描述的下面间隔32再次渲染linesOfCode的情况,也是每行间隔maxIOffset,32 + // Render linesOfCode below description with the same 32 px row spacing y = y + maxIOffset + 64; draw_text("Contributions", COLOR_WHITE, "font_about", 18, x, y); y += 32; @@ -227,23 +227,23 @@ void AboutScreen::renderDescription() { maxIOffset = (i * 32); draw_text(this->m_gitContributions[i], COLOR_WHITE, "font_about", 18, x, y + (maxIOffset)); } - //在linesOfCode的下面间隔32再次渲染Contributions的情况,也是每行间隔maxIOffset,32 + // Render git contributions below linesOfCode with the same row spacing } -//渲染描述,贡献者代码行数,贡献者情况 +// Renders description, lines-of-code, and git contribution counts void AboutScreen::loop() { while (! this->m_shouldQuit) { - process_events();//调用splashkit.h里的事件处理函数 + process_events(); // Process input events via SplashKit - this->readInput();//检查有没有申请退出 - this->tick();//更新位置音乐计时器 - this->render();//渲染画面 + this->readInput(); // Check for quit request + this->tick(); // Update positions, music, and timers + this->render(); // Render the frame - delay(1000 / 60);//定下FPS为60 + delay(1000 / 60); // Cap at 60 fps } } -//循环函数 +// Main render loop void AboutScreen::main() { // Clear music and start the about screen music. @@ -255,35 +255,35 @@ void AboutScreen::main() { // Tidy up once the loop is done. this->onExit(); } -//AboutScreen的入口函数 +// Entry point for the about screen void AboutScreen::tickContributor() { this->m_contributorTicker++; if (this->m_contributorTicker >= CONTRIBUTION_TIME) { - this->m_contributorTicker = 0;//每过180帧重置,然后增加index换下一个人,在FPS为60的情况下它的时间为3s - this->m_contributorsIndex = (this->m_contributorsIndex + 1) % this->m_contributors.size();//取模防止越界且起到循环轮播效果,比如如果index现在是5,m_contributors size是4,那就取1轮播 + this->m_contributorTicker = 0; // Reset every 180 frames (3 s at 60 fps) + this->m_contributorsIndex = (this->m_contributorsIndex + 1) % this->m_contributors.size(); // Wrap index to cycle } } -//实现循环轮播贡献者名字的支撑逻辑,一个3s +// Cycles contributor display every 3 s void AboutScreen::renderContributor() { - int r = this->m_contributorTicker % CONTRIBUTION_TIME;//r会始终保持在0-179的范围 - double ratio = 1;//字号透明度 - double fontSize = 32;//字号大小 - double fontRatio = 1;//字号倍率 + int r = this->m_contributorTicker % CONTRIBUTION_TIME; // r stays in [0, 179] + double ratio = 1; // Opacity multiplier + double fontSize = 32; // Base font size + double fontRatio = 1; // Font size multiplier if (r < CONTRIBUTION_FADE_TIME) { - ratio = r / (double)CONTRIBUTION_FADE_TIME;//随着r的增大,ratio会越来越大,也就是越来越亮直到1 - fontRatio = 1 + ((1 - ratio) * 4);//CONTRIBUTION_FADE_TIME是=30帧,也就是淡化阶段,时间为0.5s,随着ratio的增大,字体倍率会越来越小,也就是慢慢变小 - //实现淡入阶段效果,由大而淡变成小而亮 + ratio = r / (double)CONTRIBUTION_FADE_TIME; // Fade in: ratio rises 0->1 + fontRatio = 1 + ((1 - ratio) * 4); // Shrink from 5x to 1x over the fade window + // Fade-in: starts large and transparent, ends small and opaque } else if ((CONTRIBUTION_TIME - r) < CONTRIBUTION_FADE_TIME) { - ratio = (CONTRIBUTION_TIME - r) / (double)CONTRIBUTION_FADE_TIME;//随着r的增大,ratio会越来越小,也就是亮度越来越小 - fontRatio = ratio;//将字体倍率大小和亮度相等,实现淡出效果 + ratio = (CONTRIBUTION_TIME - r) / (double)CONTRIBUTION_FADE_TIME; // Fade out: ratio falls to 0 + fontRatio = ratio; // Shrink with opacity during fade-out } - fontSize = fontSize * fontRatio;//实现字体大小和字体倍率之间的关系控制 + fontSize = fontSize * fontRatio; // Apply size multiplier if (ratio > 1.0) - ratio = 1.0;//限制ratio大小,防止出错 + ratio = 1.0; // Clamp to prevent overexposure color c; c.r = ratio; @@ -297,15 +297,15 @@ void AboutScreen::renderContributor() { double y = 0; fontSize = 24; for (int i=0; im_ticker + (i * 4)) / (double)16) * (double)9;//让每个字符Y位置不同,上下9个像素 - x = sin(this->m_ticker / (double) 50) * (double)225;//让每个字符X位置产生变化,左右225个像素 - double actualX = 1300 + x + (i * 28);//计算每个字符的实际位置,间隔28 + y = sin((this->m_ticker + (i * 4)) / (double)16) * (double)9; // Per-character Y wave, +-9 px + x = sin(this->m_ticker / (double) 50) * (double)225; // Group X oscillation, +-225 px + double actualX = 1300 + x + (i * 28); // Per-character X with 28 px spacing - fontRatio = sin((actualX + 190) / (double)80) * (double)1.5;//让每个字符大小不一样,1-1.5之间 + fontRatio = sin((actualX + 190) / (double)80) * (double)1.5; // Varying size ~1-1.5x if (fontRatio < 1) - fontRatio = (double)1;//限制fontRatio大小,防止太小 + fontRatio = (double)1; // Clamp minimum size - color c = this->getRainbowShade(actualX - 350);//将字符位置丢给彩虹函数生成彩虹效果 - draw_text(createdBy.substr(i, 1), c, "font_about", (double)24 * fontRatio, actualX, (double)500 + y);//绘制 + color c = this->getRainbowShade(actualX - 350); // Rainbow colour keyed to X position + draw_text(createdBy.substr(i, 1), c, "font_about", (double)24 * fontRatio, actualX, (double)500 + y); } -}//绘制贡献者,彩虹效果,淡入淡出,位置波动 \ No newline at end of file +} // Render contributor name with rainbow colour, fade in/out, and position oscillation \ No newline at end of file