From ab3dd829b869e5f6cc1fbeada89821fbb19c5159 Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Thu, 26 Feb 2026 09:16:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=AA=97=E5=8F=A3=E5=9B=BE=E6=A0=87):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AA=97=E5=8F=A3=E5=9B=BE=E6=A0=87=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B9=B6=E5=AE=8C=E5=96=84=E7=9B=B8=E5=85=B3=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加窗口图标功能,包括: 1. 支持通过配置文件设置窗口图标 2. 添加Windows资源文件处理 3. 更新各平台构建脚本以处理图标资源 4. 添加图标使用文档说明 5. 优化Application类结构 同时修复了Switch平台构建脚本中的团队名称错误 --- AGENTS.md | 216 ++++++++++++++++++ .../include/frostbite2D/core/application.h | 69 ++---- .../src/frostbite2D/core/application.cpp | 175 +++++++++++++- Frostbite2D/src/frostbite2D/core/window.cpp | 20 ++ Frostbite2D/src/main.cpp | 68 ++++-- assets/icons/README.md | 46 ++++ assets/icons/app.ico | Bin 0 -> 4286 bytes assets/icons/icon.bmp | Bin 0 -> 15532 bytes platform/linux.lua | 43 ++-- platform/switch.lua | 8 +- platform/windows.lua | 39 +++- resources/app.rc | 3 + 12 files changed, 579 insertions(+), 108 deletions(-) create mode 100644 AGENTS.md create mode 100644 assets/icons/README.md create mode 100644 assets/icons/app.ico create mode 100644 assets/icons/icon.bmp create mode 100644 resources/app.rc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ec1cfad --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,216 @@ +# Frostbite2D Agent Guide + +This guide helps agentic coding agents work effectively with the Frostbite2D 2D game engine codebase. + +## Build Commands + +### Basic Build +```bash +xmake build +``` + +### Clean Build +```bash +xmake clean +xmake build +``` + +### Debug Build +```bash +xmake build -m debug +``` + +### Release Build +```bash +xmake build -m release +``` + +### Platform-Specific Build +```bash +xmake build -p windows +xmake build -p linux +xmake build -p switch +``` + +### Configuration +```bash +xmake f -c # Show current configuration +xmake f # Interactive configuration +``` + +## Testing +Currently no automated test framework is configured. Manual testing is performed by running the built binary. + +## Project Structure +``` +Frostbite2D/ +├── include/frostbite2D/ # Public headers +│ ├── core/ # Core engine classes +│ ├── types/ # Type definitions +│ ├── utils/ # Utility classes +│ └── platform/ # Platform-specific code +└── src/frostbite2D/ # Implementation files +``` + +## Code Style Guidelines + +### Naming Conventions +- **Classes/Structs**: PascalCase (`Application`, `Window`, `Vec2`) +- **Methods/Functions**: camelCase (`readTextFile`, `getFileName`) +- **Member Variables**: camelCase with trailing underscore (`width_`, `height_`, `running_`) +- **Constants**: UPPER_CASE (`PI_F`, `DEG_TO_RAD`) or PascalCase static methods (`Vec2::Zero()`) +- **Namespaces**: lowercase (`frostbite2D`, `math`) +- **Template Parameters**: PascalCase (`T`, `Args`, `Sig`) +- **Type Aliases**: using keyword with PascalCase (`using int8 = std::int8_t;`) + +### Imports and Includes +- **Engine headers**: Use path relative to frostbite2D root + ```cpp + #include + #include + ``` +- **Third-party libraries**: Use vendor prefixes + ```cpp + #include + #include + #include + ``` +- **Standard library**: Use angle brackets + ```cpp + #include + #include + #include + ``` + +### Formatting +- **Indentation**: 2 spaces +- **Brace Style**: K&R (opening brace on same line) + ```cpp + void myFunction() { + // body + } + ``` +- **Pointer/Reference Style**: Type on left, not centered + ```cpp + void func(T* ptr); // Good + void func(T *ptr); // Avoid + void func(T& ref); // Good + ``` +- **Const Placement**: After type + ```cpp + const std::string& str; // Good + std::string const& str; // Avoid + ``` + +### Types +- **Use custom type aliases**: `int8`, `int32`, `uint8`, `uint64`, `Ptr`, `SharedPtr` +- **Smart pointers**: Prefer `Ptr` alias over `std::shared_ptr` +- **constexpr**: Use for compile-time constants +- **Struct vs Class**: + - Use `struct` for POD types, configurations, and data-only types + - Use `class` for objects with methods/encapsulation +- **Default member initialization**: Initialize at declaration + ```cpp + bool running_ = false; + int width_ = 1280; + ``` + +### Error Handling +- **Return bool for success/failure operations** (especially I/O) +- **Use SDL_Log/SDL_LogError for logging** +- **Use std::optional for optional returns** +- **Avoid exceptions** (not observed in codebase) +- **Return early on failure**: Check conditions and return false immediately + +### Singletons +- Use static instance pattern with private constructor: + ```cpp + static Application& get(); + private: + Application() = default; + ``` + +### Const Correctness +- Mark methods `const` if they don't modify state +- Pass by const reference for large types: `const std::string&` +- Return const references to member data when appropriate + +### Documentation +- **Doxygen-style comments** with `@brief` tag +- **Use `///` for single-line comments in headers** +- **Section separators** use `// ---------------------------------------------------------------------------` +- **Chinese comments** are present and should be preserved/maintained + ```cpp + /** + * @brief 读取文本文件 + * @param path 文件路径 + * @param outContent 输出文件内容 + * @return 读取成功返回 true + */ + bool readTextFile(const std::string& path, std::string& outContent); + ``` + +### Platform Handling +- Use `#ifdef` guards for platform-specific code: + ```cpp + #ifdef __SWITCH__ + switchInit(); + #endif + ``` +- Supported platforms: Windows, Linux, macOS, Switch +- Platform configs in `platform/` directory + +### STL Usage +- Prefer STL algorithms over manual loops +- Use `std::filesystem` for file operations (already wrapped in Asset class) +- Use `std::optional` for nullable returns +- Use `std::function` for callbacks +- Use `std::vector` for dynamic arrays +- Use `std::string` for text + +### OpenGL/Graphics +- GLM library for math operations +- Glad for OpenGL function loading +- Keep OpenGL-specific code minimal and well-encapsulated + +## Dependencies +- **SDL2**: Windowing, input, platform abstraction +- **GLM**: Math library (vectors, matrices) +- **Glad**: OpenGL loader +- **STB**: Image/text utilities (single-header libs) + +## Key Patterns + +### Module Pattern +```cpp +void Application::use(Module& m) { + modules_.push_back(&m); +} +``` + +### Configuration Pattern +Use configuration structs with default values: +```cpp +struct WindowConfig { + uint32_t width = 640; + uint32_t height = 480; + std::string title = "Frostbite2D Game"; +}; +``` + +### Path Handling +Always use the Asset singleton for file operations - it handles platform-specific path issues: +```cpp +Asset& asset = Asset::get(); +asset.setWorkingDirectory("path/to/assets"); +std::string content; +if (asset.readTextFile("file.txt", content)) { + // success +} +``` + +## Common Issues +- **Path separators**: Use Asset class, it handles platform differences +- **Encoding**: Project uses UTF-8, Asset handles Windows UTF-8 paths +- **Memory leaks**: Use smart pointers, ensure proper cleanup in destructors +- **OpenGL context**: Initialize only after window creation \ No newline at end of file diff --git a/Frostbite2D/include/frostbite2D/core/application.h b/Frostbite2D/include/frostbite2D/core/application.h index 9688cc7..a4ce912 100644 --- a/Frostbite2D/include/frostbite2D/core/application.h +++ b/Frostbite2D/include/frostbite2D/core/application.h @@ -73,13 +73,6 @@ public: */ bool init(const AppConfig& config); - /** - * @brief 使用配置文件初始化 - * @param configPath 配置文件路径 - * @return 初始化成功返回 true - */ - bool init(const std::string& configPath); - /** * @brief 关闭应用程序 */ @@ -117,54 +110,6 @@ public: */ bool isRunning() const { return running_; } - // /** - // * @brief 获取窗口 - // * @return 窗口引用 - // */ - // IWindow& window() { return *window_; } - - // /** - // * @brief 获取渲染器 - // * @return 渲染器引用 - // */ - // RenderBackend& renderer(); - - // /** - // * @brief 获取场景服务 - // * @return 场景服务共享指针 - // */ - // SharedPtr scenes(); - - // /** - // * @brief 获取计时器服务 - // * @return 计时器服务共享指针 - // */ - // SharedPtr timers(); - - // /** - // * @brief 获取事件服务 - // * @return 事件服务共享指针 - // */ - // SharedPtr events(); - - // /** - // * @brief 获取相机服务 - // * @return 相机服务共享指针 - // */ - // SharedPtr camera(); - - // /** - // * @brief 进入场景 - // * @param scene 场景指针 - // */ - // void enterScene(Ptr scene); - - /** - * @brief 获取帧间隔时间 - * @return 帧间隔时间(秒) - */ - float deltaTime() const { return deltaTime_; } - /** * @brief 获取总运行时间 * @return 总运行时间(秒) @@ -183,6 +128,18 @@ public: */ const AppConfig& getConfig() const; + /** + * @brief 获取窗口 + * @return 窗口指针 + */ + Window* getWindow() const { return window_; } + + /** + * @brief 获取 delta time + * @return 帧间隔时间(秒) + */ + float deltaTime() const { return deltaTime_; } + private: Application() = default; ~Application(); @@ -225,6 +182,7 @@ private: std::vector modules_; Window* window_ = nullptr; + AppConfig config_; bool initialized_ = false; bool running_ = false; @@ -237,6 +195,7 @@ private: int frameCount_ = 0; float fpsTimer_ = 0.0f; int currentFps_ = 0; + const float fpsUpdateInterval_ = 1.0f; }; diff --git a/Frostbite2D/src/frostbite2D/core/application.cpp b/Frostbite2D/src/frostbite2D/core/application.cpp index e6d17da..cffde98 100644 --- a/Frostbite2D/src/frostbite2D/core/application.cpp +++ b/Frostbite2D/src/frostbite2D/core/application.cpp @@ -1,4 +1,5 @@ #include +#include #include #include namespace frostbite2D { @@ -35,6 +36,8 @@ bool Application::init(const AppConfig &config) { return true; } + config_ = config; + // 平台相关初始化 #ifdef __SWITCH__ switchInit(); @@ -47,13 +50,25 @@ bool Application::init(const AppConfig &config) { return false; } + if (!initCoreModules()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize core modules"); + shutdown(); + return false; + } + + initialized_ = true; return true; } void Application::shutdown() { if (!initialized_) return; - + + running_ = false; + shouldQuit_ = true; + + destroyAllModules(); + if (window_) { window_->destroy(); window_ = nullptr; @@ -69,8 +84,162 @@ void Application::shutdown() { } Application::~Application() { - if (initialized_) { - shutdown(); + shutdown(); +} + +bool Application::initCoreModules() { + registerCoreServices(); + return true; +} + +void Application::registerCoreServices() { + SDL_Log("Registering core services..."); +} + +void Application::setupAllModules() { + SDL_Log("Setting up modules (%zu)...", modules_.size()); + + for (auto* module : modules_) { + if (module && !module->isInitialized()) { + module->setupModule(); + module->setInitialized(true); + SDL_Log("Module '%s' initialized", module->getName()); + } } } + +void Application::destroyAllModules() { + SDL_Log("Destroying modules (%zu)...", modules_.size()); + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->destroyModule(); + module->setInitialized(false); + } + } +} + +void Application::run() { + if (!initialized_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Application not initialized!"); + return; + } + + if (running_) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Application already running!"); + return; + } + + SDL_Log("Starting application..."); + running_ = true; + shouldQuit_ = false; + paused_ = false; + + setupAllModules(); + + lastFrameTime_ = SDL_GetPerformanceCounter() / static_cast(SDL_GetPerformanceFrequency()); + totalTime_ = 0.0f; + deltaTime_ = 0.0f; + frameCount_ = 0; + fpsTimer_ = 0.0f; + currentFps_ = 0; + + mainLoop(); + + destroyAllModules(); + running_ = false; + SDL_Log("Application stopped"); +} + +void Application::quit() { + SDL_Log("Quit requested"); + shouldQuit_ = true; +} + +void Application::pause() { + if (!paused_) { + SDL_Log("Application paused"); + paused_ = true; + } +} + +void Application::resume() { + if (paused_) { + SDL_Log("Application resumed"); + paused_ = false; + lastFrameTime_ = SDL_GetPerformanceCounter() / static_cast(SDL_GetPerformanceFrequency()); + } +} + +void Application::mainLoop() { + while (!shouldQuit_) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + shouldQuit_ = true; + break; + } + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->handleEvent(); + } + } + } + + if (!paused_) { + update(); + } + + render(); + } +} + +void Application::update() { + double currentTime = SDL_GetPerformanceCounter() / static_cast(SDL_GetPerformanceFrequency()); + deltaTime_ = static_cast(currentTime - lastFrameTime_); + lastFrameTime_ = currentTime; + totalTime_ += deltaTime_; + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->onUpdate(); + } + } + + frameCount_++; + fpsTimer_ += deltaTime_; + if (fpsTimer_ >= fpsUpdateInterval_) { + currentFps_ = static_cast(frameCount_ / fpsTimer_); + frameCount_ = 0; + fpsTimer_ = 0.0f; + } +} + +void Application::render() { + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->beforeRender(); + } + } + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->onRender(); + } + } + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->afterRender(); + } + } + + if (window_) { + window_->swap(); + } +} + +const AppConfig& Application::getConfig() const { + return config_; +} } // namespace frostbite2D \ No newline at end of file diff --git a/Frostbite2D/src/frostbite2D/core/window.cpp b/Frostbite2D/src/frostbite2D/core/window.cpp index 5258b45..4617c03 100644 --- a/Frostbite2D/src/frostbite2D/core/window.cpp +++ b/Frostbite2D/src/frostbite2D/core/window.cpp @@ -1,6 +1,7 @@ #include "SDL_log.h" #include #include +#include #include @@ -84,6 +85,25 @@ bool Window::create(const WindowConfig &cfg) { fullscreen_ = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0; vsync_ = cfg.vsync; +#ifndef __SWITCH__ + if (!cfg.icon.file_path.empty()) { + Asset& asset = Asset::get(); + std::string resolvedPath = asset.resolveAssetPath(cfg.icon.file_path); + SDL_Surface* icon = SDL_LoadBMP(resolvedPath.c_str()); + if (icon) { + SDL_SetWindowIcon(sdlWindow_, icon); + SDL_FreeSurface(icon); + } else { + SDL_Log("Failed to load window icon: %s", resolvedPath.c_str()); + } + } +#if defined(_WIN32) + if (cfg.icon.resource_id != 0) { + SDL_Log("Loading icon from resource ID: %u", cfg.icon.resource_id); + } +#endif +#endif + return true; } diff --git a/Frostbite2D/src/main.cpp b/Frostbite2D/src/main.cpp index c727d9e..208625f 100644 --- a/Frostbite2D/src/main.cpp +++ b/Frostbite2D/src/main.cpp @@ -2,40 +2,76 @@ #include #include #include +#include #include #include using namespace frostbite2D; -int main(int argc, char **argv) { +class TestModule : public Module { +public: + const char* getName() const override { return "TestModule"; } + void setupModule() override { + SDL_Log("TestModule: setupModule()"); + } + + void onUpdate() override { + static int frameCount = 0; + frameCount++; + + if (frameCount % 120 == 0) { + SDL_Log("TestModule: onUpdate() - Frame %d, FPS: %d", frameCount, Application::get().fps()); + } + + if (frameCount > 600) { + SDL_Log("TestModule: Requesting quit after 600 frames"); + Application::get().quit(); + } + } + + void beforeRender() override { + glClearColor(0.2f, 0.3f, 0.8f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + } + + void onRender() override { + } + + void afterRender() override { + } + + void destroyModule() override { + SDL_Log("TestModule: destroyModule()"); + } +}; + +int main(int argc, char **argv) { AppConfig config = AppConfig::createDefault(); - config.appName = "Frostbite2D Render Test"; + config.appName = "Frostbite2D Test App"; config.appVersion = "1.0.0"; config.windowConfig.width = 800; config.windowConfig.height = 600; - config.windowConfig.title = "Frostbite2D - OpenGL Render Test"; + config.windowConfig.title = "Frostbite2D - Application Test"; Application &app = Application::get(); + + TestModule testModule; + app.use(testModule); + if (!app.init(config)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Failed to initialize application!"); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize application!"); return -1; } - Asset &asset = Asset::get(); - // asset.setWorkingDirectory("I:/DOF/骑士团"); - asset.setWorkingDirectory("/switch/testgame"); - std::string content; - if (asset.readTextFile("test.txt", content)) { - SDL_Log("test.txt content: %s", content.c_str()); - } else { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read test.txt!"); - } + SDL_Log("Application initialized successfully"); + SDL_Log("Running main loop..."); + + app.run(); app.shutdown(); - SDL_Log("程序正常退出"); + SDL_Log("Application exited normally"); return 0; -} +} \ No newline at end of file diff --git a/assets/icons/README.md b/assets/icons/README.md new file mode 100644 index 0000000..133e8c3 --- /dev/null +++ b/assets/icons/README.md @@ -0,0 +1,46 @@ +# Icon Setup Guide + +## Window Icon (运行时窗口图标) + +### Windows/Linux +- Place `icon.bmp` (32x32 or larger) in `assets/icons/` +- Set in code: `config.windowConfig.icon = Icon("assets/icons/icon.bmp");` + +### Switch +- Place `icon.jpg` (256x256 JPEG) in `assets/icons/icon.jpg` +- Automatically included in NRO build process + +## Application Icon (程序文件图标) + +### Windows (.exe 图标) +需要创建 `.ico` 文件并放置在 `assets/icons/app.ico` + +#### 创建 .ico 文件的方法: + +**方法 1: 使用在线工具** +1. 访问 https://icoconvert.com/ +2. 上传 PNG/JPG 图标(推荐 256x256) +3.将下载的 .ico 文件保存到 `assets/icons/app.ico` + +**方法 2: 使用 GIMP** +1. 打开图像 +2. 调整为 256x256 +3. 文件 → 导出为 → `app.ico` +4. 保存到 `assets/icons/app.ico` + +**方法 3: 使用 ImageMagick** +```bash +convert icon.png -define icon:auto-resize=256,128,96,64,48,32,16 app.ico +``` + +#### Linux (.desktop 图标) +- 将图标复制到 `/usr/share/icons/hicolor/256x256/apps/yourapp.png` +- 创建 `.desktop` 文件指定 Icon 字段 + +## Usage Example +```cpp +AppConfig config = AppConfig::createDefault(); +config.windowConfig.icon = Icon("assets/icons/icon.bmp"); +Application& app = Application::get(); +app.init(config); +``` \ No newline at end of file diff --git a/assets/icons/app.ico b/assets/icons/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..50a3bc74dd4c6b884cea1ab90f7bd6bad40a12e4 GIT binary patch literal 4286 zcmchXZBSHI7{|{>jbe>fOx85=VuE1fTcT+itfhjhfUqJ6yFmB`^4>y>LK=Rk84zmh zLo;Pfeo3+#KbAASxTpx3I;2SQR=$j&Sc<#L3eVol?SFRnY!_r@owCdP_Id8Pm-Bm` zbI-kuY48^k!uT)y@=3;~GR7hRhS+kz$DIeR9iQyNHQ@X2@-Q7!qYcwVtqxstq&PV7 z$G3y@uT=!;HGm0xpZWfGs-5mU1o2&PB_?!@WC~7_DueXb;H?3n!Y|_eXZL<}4DVAI z+YZhFi32~uPw+e7lRz6N0s+9Y7Ce8@1M`dZ_D3EMegK0&2E^p;!{Vw{**jtUgx#&@UCijUr^UDUL9}< zzlgIi_95s4!@!~LzcMh9%4cNIwq>=Hyx|xnWL+TL#x|Ngw^M?P(+Uc})4=EF0gv$W z$e)K690oS{WmC{P+8L5c`H{P6W8!y|klE^V^QcR`Ky&7H5_}w9!yq^c62S!E%cD^H z-TCUrqi3v4jSXGflsmVWGIU?jI%5mPXSKQfyoP)Iq7E6}JK!LQ_Nf5eWGJ+NJIq*Y zXUS}0C^E;OUu3G&_y*< z?6TvuHvTjv7C*l30i14mP_)@_n;T2K60Ng9c-+r4F=G_U)$m!b)SC8B4 z5biPkyarbtj@Va@$+0n~JLWCuC<_bgo+^AIdh@#d&y>EUyjfCw_hMGcdC8F3?J(rs zLeExb-=ch$`)i+&t#tw>u(tdo_ql!69G)G9dUV3;W6G zk3UJqgl00Pwvu6Muj~2eI`^OCeC~_PP4K0GN!+VugmBaeJhMR^=mp&C0K^&T#~Sc; z;B~A!MMgurBW+WUtcz}LUlsXN8GO&GojmvDfM*Bj0SfTAk$dqsIZ($i>gY9WIQK>B zrrv#qVq4()v`frTWM%1j{m%E*7dO8Lo=3qFP!Bw7k9;h@47U}0ZH#MNiaj|Ud$9xj z3bueLKzuKJsskSJ&P_zF2Mu5dNUHUhTx;KS0x<>n8`FN{^9O-0pn$vJ2+)DYfTxb< zrfIz{2RyM6B%Bi)|t`h^ax7RM!{b}#4?Xw5f-h42> z+T$(v_zSTA3M|s*JyzQBt&OU$*a=>OR{}17-+%8nmbjjOcBSQaQ@u@kuVaAfZovW_OWu;A6`8$gCW4z{h_Zv9BdQ; zI4=TifY-p+qYDH9Z`uX3mGiMdaEUR$D%^{df7su_?YPy?dAoZ|YsR(yCzy6{8q;F(dr>f^ Hn6-ZcKyYME literal 0 HcmV?d00001 diff --git a/assets/icons/icon.bmp b/assets/icons/icon.bmp new file mode 100644 index 0000000000000000000000000000000000000000..8b3ed6f3950df2263fe4c8858502fb65fcc76702 GIT binary patch literal 15532 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 + +1 ICON "assets/icons/app.ico" \ No newline at end of file