diff --git a/CMakeLists.txt b/CMakeLists.txt index 57e44e7..1a9eb2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ set(SOURCE_FILES src/ui/managers/GameUIManager.cpp src/ui/managers/MainMenuUIManager.cpp src/graphics/ui/UIRenderer.cpp + src/graphics/font/BitmapFont.cpp ) @@ -54,6 +55,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf + SDL3_image::SDL3_image ) # ========== Windows: 复制 DLL ========== @@ -82,6 +84,9 @@ if (WIN32) COMMAND ${CMAKE_COMMAND} -E copy_if_different ${STDCPP_DLL_PATH} $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ COMMENT "Copying MinGW runtime DLLs" ) endif() diff --git a/cmake/SDL.cmake b/cmake/SDL.cmake index 2102187..7f6738e 100644 --- a/cmake/SDL.cmake +++ b/cmake/SDL.cmake @@ -25,3 +25,14 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(SDL3_ttf) + +# =========================== +# SDL3_image +# =========================== +FetchContent_Declare( + SDL3_image + GIT_REPOSITORY https://github.com/libsdl-org/SDL_image.git + GIT_TAG release-3.2.0 +) + +FetchContent_MakeAvailable(SDL3_image) \ No newline at end of file diff --git a/src/core/WindowManager.cpp b/src/core/WindowManager.cpp index bbf2db9..ed5b51c 100644 --- a/src/core/WindowManager.cpp +++ b/src/core/WindowManager.cpp @@ -11,14 +11,14 @@ WindowManager::~WindowManager() { } bool WindowManager::Initialize(GameConfig& config) { - m_width = config.windowWidth; - m_height = config.windowHeight; + m_logicalWidth = config.logicalWidth; + m_logicalHeight = config.logicalHeight; // 创建窗口(支持高DPI和横屏)[3,4](@ref) m_window = SDL_CreateWindow( "孢子棋", // 窗口标题,显示在标题栏上 - m_width, // 窗口的逻辑宽度(例如 800),用于统一布局,不受屏幕 DPI 影响 - m_height, // 窗口的逻辑高度(例如 600) - SDL_WINDOW_HIGH_PIXEL_DENSITY | // 启用高像素密度支持(HiDPI/Retina),确保在高分屏上画面清晰 + m_logicalWidth, // 窗口的逻辑宽度(例如 800),用于统一布局,不受屏幕 DPI 影响 + m_logicalHeight, // 窗口的逻辑高度(例如 600) + //SDL_WINDOW_HIGH_PIXEL_DENSITY | // 启用高像素密度支持(HiDPI/Retina),确保在高分屏上画面清晰 SDL_WINDOW_RESIZABLE // 允许用户调整窗口大小(可拉伸) ); if (!m_window) { @@ -34,14 +34,28 @@ bool WindowManager::Initialize(GameConfig& config) { "创建渲染器失败: %s", SDL_GetError()); return false; } + // 关键设置:启用像素模式 - // 设置逻辑呈现模式,实现分辨率自适应[3](@ref) + /* // 设置逻辑呈现模式,实现分辨率自适应[3](@ref) SDL_SetRenderLogicalPresentation(m_renderer, m_width, m_height, - SDL_LOGICAL_PRESENTATION_LETTERBOX); - SDL_SetWindowSize(m_window, m_width, m_height); + SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); + */ + SDL_SetWindowSize(m_window, UI::StartWindowWidth, UI::StartWindowHeight); + // 创建逻辑画布 + // RGBA8888: 32位色,8位红绿蓝和透明度 + // TARGET: 纹理可作为渲染目标 + m_logicalTexture = SDL_CreateTexture( + m_renderer, + SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_TARGET, + m_logicalWidth, + m_logicalHeight + ); + + return true; } @@ -51,13 +65,28 @@ void WindowManager::Shutdown() { } void WindowManager::Clear() { + // 1. 设置渲染目标为逻辑纹理 + SDL_SetRenderTarget(m_renderer, m_logicalTexture); // 设置画笔颜色 SDL_SetRenderDrawColor(m_renderer, 255, 255, 255, 255); - // 使用画笔颜色填充整个屏幕 + // 使用画笔颜色填充整个逻辑画布 SDL_RenderClear(m_renderer); + } void WindowManager::Present() { + + // 4. 切回默认渲染目标(窗口) + SDL_SetRenderTarget(m_renderer, nullptr); + SDL_RenderClear(m_renderer); // 清的是窗口(黑边 + SDL_FRect dstRect; + calculateDstRect(dstRect); + SDL_RenderTexture ( + m_renderer, + m_logicalTexture, // 源:你已经画好的逻辑画布 + nullptr, // srcRect:源区域(nullptr = 整张) + &dstRect // dstRect:贴到哪里 & 贴多大 + ); SDL_RenderPresent(m_renderer); } @@ -77,4 +106,20 @@ bool WindowManager::setFullscreen(bool isFullscreen) { m_isFullscreen = isFullscreen; return true; +} + +void WindowManager::calculateDstRect(SDL_FRect& dstRect) { + // 获取窗口的实际尺寸(像素) + SDL_GetWindowSize(m_window, &m_windowWidth, &m_windowHeight); + // 计算缩放比例,整数缩放 + int scaleX = m_windowWidth / m_logicalWidth; + int scaleY = m_windowHeight / m_logicalHeight; + int scale = (scaleX < scaleY) ? scaleX : scaleY; + + // 计算目标矩形的尺寸 + dstRect.w = static_cast(m_logicalWidth * scale); + dstRect.h = static_cast(m_logicalHeight * scale); + // 居中显示 + dstRect.x = static_cast((m_windowWidth - dstRect.w) / 2); + dstRect.y = static_cast((m_windowHeight - dstRect.h) / 2); } \ No newline at end of file diff --git a/src/core/WindowManager.h b/src/core/WindowManager.h index 01ec2bb..6d0c02c 100644 --- a/src/core/WindowManager.h +++ b/src/core/WindowManager.h @@ -23,7 +23,13 @@ public: private: SDL_Window* m_window; SDL_Renderer* m_renderer; - int m_width; - int m_height; + int m_logicalWidth; + int m_logicalHeight; + int m_windowWidth; + int m_windowHeight; bool m_isFullscreen = false; + SDL_Texture* m_logicalTexture = nullptr; + // 计算缩放后的目标矩形 + void calculateDstRect(SDL_FRect& dstRect); + }; \ No newline at end of file diff --git a/src/graphics/font/BitmapFont.cpp b/src/graphics/font/BitmapFont.cpp new file mode 100644 index 0000000..29bb1f8 --- /dev/null +++ b/src/graphics/font/BitmapFont.cpp @@ -0,0 +1,208 @@ +#include "BitmapFont.h" + +#include + +#include +#include +#include +#include +static bool startsWith(const std::string& s, const char* prefix) { + return s.size() >= std::strlen(prefix) && + s.compare(0, std::strlen(prefix), prefix) == 0; +} + +//////////////////////////////////////////////////////////// +// 构造 / 析构 +//////////////////////////////////////////////////////////// +BitmapFont::BitmapFont() = default; + +BitmapFont::~BitmapFont() { + for (SDL_Texture* tex : m_pages) { + SDL_DestroyTexture(tex); + } + m_pages.clear(); +} + +//////////////////////////////////////////////////////////// +// 加载 .fnt +//////////////////////////////////////////////////////////// +bool BitmapFont::load(const std::string& fntPath, SDL_Renderer* renderer) { + m_renderer = renderer; + + std::ifstream file(fntPath); + if (!file.is_open()) { + SDL_Log("BitmapFont: 无法打开 %s", fntPath.c_str()); + return false; + } + + // .fnt 所在目录(用于加载 .tga) + std::string baseDir = + std::filesystem::path(fntPath).parent_path().string(); + + std::string line; + while (std::getline(file, line)) { + + // common 行:行高 + if (startsWith(line, "common ")) { + sscanf(line.c_str(), + "common lineHeight=%d", + &m_lineHeight + ); + } + + // page 行:加载贴图 + else if (startsWith(line, "page ")) { + int id = 0; + char filename[256]{}; + + sscanf(line.c_str(), + "page id=%d file=\"%255[^\"]\"", + &id, filename + ); + + if ((int)m_pages.size() <= id) { + m_pages.resize(id + 1, nullptr); + } + + std::string texPath = baseDir + "/" + filename; + + SDL_Surface* surf = IMG_Load(texPath.c_str()); + if (!surf) { + SDL_Log("BitmapFont: 无法加载贴图 %s", texPath.c_str()); + return false; + } + + SDL_Texture* tex = + SDL_CreateTextureFromSurface(renderer, surf); + SDL_DestroySurface(surf); + + // 像素风关键:最近邻 + SDL_SetTextureScaleMode(tex, SDL_SCALEMODE_NEAREST); + + m_pages[id] = tex; + } + + // char 行:字形数据 + else if (startsWith(line, "char ")) { + uint32_t id = 0; + BitmapGlyph g{}; + + sscanf(line.c_str(), + "char id=%u x=%d y=%d width=%d height=%d " + "xoffset=%d yoffset=%d xadvance=%d page=%d", + &id, + &g.x, &g.y, + &g.w, &g.h, + &g.xOffset, &g.yOffset, + &g.xAdvance, + &g.page + ); + + m_glyphs[id] = g; + } + } + + return true; +} + +//////////////////////////////////////////////////////////// +// 绘制文本 +//////////////////////////////////////////////////////////// +void BitmapFont::drawText(const std::string& text, int x, int y) const { + int cursorX = x; + int cursorY = y; + + for (uint32_t cp : utf8ToCodepoints(text)) { + + // 换行 + if (cp == '\n') { + cursorX = x; + cursorY += m_lineHeight; + continue; + } + + auto it = m_glyphs.find(cp); + if (it == m_glyphs.end()) { + continue; + } + + const BitmapGlyph& g = it->second; + + SDL_FRect src{ + (float)g.x, + (float)g.y, + (float)g.w, + (float)g.h + }; + + SDL_FRect dst{ + (float)(cursorX + g.xOffset), + (float)(cursorY + g.yOffset), + (float)g.w, + (float)g.h + }; + + SDL_RenderTexture( + m_renderer, + m_pages[g.page], + &src, + &dst + ); + + cursorX += g.xAdvance; + } +} + +//////////////////////////////////////////////////////////// +// 行高 +//////////////////////////////////////////////////////////// +int BitmapFont::getLineHeight() const { + return m_lineHeight; +} + +//////////////////////////////////////////////////////////// +// UTF-8 → Unicode codepoint(最小但够用) +//////////////////////////////////////////////////////////// +std::vector +BitmapFont::utf8ToCodepoints(const std::string& text) { + std::vector result; + + for (size_t i = 0; i < text.size();) { + uint8_t c = (uint8_t)text[i]; + + if (c < 0x80) { + result.push_back(c); + i += 1; + } + else if ((c >> 5) == 0x6) { + uint32_t cp = + ((text[i] & 0x1F) << 6) | + (text[i + 1] & 0x3F); + result.push_back(cp); + i += 2; + } + else if ((c >> 4) == 0xE) { + uint32_t cp = + ((text[i] & 0x0F) << 12) | + ((text[i + 1] & 0x3F) << 6) | + (text[i + 2] & 0x3F); + result.push_back(cp); + i += 3; + } + else if ((c >> 3) == 0x1E) { + uint32_t cp = + ((text[i] & 0x07) << 18) | + ((text[i + 1] & 0x3F) << 12) | + ((text[i + 2] & 0x3F) << 6) | + (text[i + 3] & 0x3F); + result.push_back(cp); + i += 4; + } + else { + // 非法 UTF-8,跳过 + i += 1; + } + } + + return result; +} diff --git a/src/graphics/font/BitmapFont.h b/src/graphics/font/BitmapFont.h new file mode 100644 index 0000000..82e43cc --- /dev/null +++ b/src/graphics/font/BitmapFont.h @@ -0,0 +1,70 @@ +#pragma once + +/* + BitmapFont.h + -------------------------------- + Bitmap Font(BMFont / AngelCode)渲染器 + - 支持 .fnt(文本格式) + - 支持多 page(多张 .tga) + - 支持 UTF-8 / 中文 + - SDL3 / 像素风(NEAREST) + + 用法示例: + BitmapFont font; + font.load("assets/fonts/sanhan.fnt", renderer); + font.drawText("你好,像素世界", 10, 10); +*/ + +#include +#include +#include +#include + +//////////////////////////////////////////////////////////// +// 单个字形(对应 .fnt 中的 char 行) +//////////////////////////////////////////////////////////// +struct BitmapGlyph { + int x = 0; // 在贴图中的 x + int y = 0; // 在贴图中的 y + int w = 0; // 字形宽度 + int h = 0; // 字形高度 + + int xOffset = 0; // 绘制时 x 偏移 + int yOffset = 0; // 绘制时 y 偏移 + int xAdvance = 0;// 光标前进量 + + int page = 0; // 所属贴图页 +}; + +//////////////////////////////////////////////////////////// +// BitmapFont 类 +//////////////////////////////////////////////////////////// +class BitmapFont { +public: + BitmapFont(); + ~BitmapFont(); + + // 加载 .fnt 文件 + bool load(const std::string& fntPath, SDL_Renderer* renderer); + + // 绘制文本(UTF-8) + void drawText(const std::string& text, int x, int y) const; + + // 获取行高(用于换行 / UI 布局) + int getLineHeight() const; + +private: + // UTF-8 → Unicode codepoint + static std::vector utf8ToCodepoints(const std::string& text); + +private: + SDL_Renderer* m_renderer = nullptr; + + int m_lineHeight = 0; + + // Unicode → Glyph + std::unordered_map m_glyphs; + + // 所有 page 对应的纹理 + std::vector m_pages; +}; diff --git a/src/graphics/font/TextRenderer.cpp b/src/graphics/font/TextRenderer.cpp index 46aebfe..e388258 100644 --- a/src/graphics/font/TextRenderer.cpp +++ b/src/graphics/font/TextRenderer.cpp @@ -5,7 +5,8 @@ TextRenderer::TextRenderer(SDL_Renderer* renderer, FontManager* fontManager) : m_fontManager(fontManager), m_renderer(renderer) { - + //m_bitmapFont = std::make_unique(); + //m_bitmapFont->load("assets/fonts/sanhan.fnt", renderer); } TextRenderer::~TextRenderer() { @@ -62,6 +63,8 @@ void TextRenderer::renderText(const std::string& text, TextStyle style, int x, i static_cast(cached.width), static_cast(cached.height) }; SDL_RenderTexture(m_renderer, cached.texture, NULL, &dest); + + //m_bitmapFont->drawText(text, x, y); } TextRenderer::CachedText TextRenderer::createAndCacheTexture(const std::string& text, TextStyle style) { @@ -91,6 +94,7 @@ TextRenderer::CachedText TextRenderer::createAndCacheTexture(const std::string& SDL_Log("错误:无法创建纹理\n"); return result; } + // 设置纹理缩放模式为最近邻 SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); // 保存结果 result.texture = texture; diff --git a/src/graphics/font/TextRenderer.h b/src/graphics/font/TextRenderer.h index c1af8ab..20312d4 100644 --- a/src/graphics/font/TextRenderer.h +++ b/src/graphics/font/TextRenderer.h @@ -16,7 +16,8 @@ #include #include "Textstyle.h" #include "FontManager.h" - +#include "BitmapFont.h" +#include /** * @class TextRenderer * @brief 文本渲染器 @@ -163,5 +164,5 @@ private: * @see CachedText, m_cache */ CachedText createAndCacheTexture(const std::string& text, TextStyle style); - + //std::unique_ptr m_bitmapFont; }; diff --git a/src/graphics/ui/UIRenderer.cpp b/src/graphics/ui/UIRenderer.cpp index 909c90e..de8de0b 100644 --- a/src/graphics/ui/UIRenderer.cpp +++ b/src/graphics/ui/UIRenderer.cpp @@ -99,14 +99,14 @@ void UIRenderer::renderText(const Type& data) { auto [textW, textH] = m_textRenderer->getTextSize(m_text, m_textStyle); // 如果组件的宽高为 0,则使用文本尺寸填充(相当于自动调整控件大小) - float boxW = m_rect.w; - float boxH = m_rect.h; - if (boxW <= 0.0f) boxW = static_cast(textW); - if (boxH <= 0.0f) boxH = static_cast(textH); + int boxW = m_rect.w; + int boxH = m_rect.h; + if (boxW <= 0) boxW = textW; + if (boxH <= 0) boxH = textH; // 计算文本左上角坐标以实现居中 - int textX = static_cast(m_rect.x + (boxW - textW) / 2.0f); - int textY = static_cast(m_rect.y + (boxH - textH) / 2.0f); + int textX = m_rect.x + (boxW - textW) / 2; + int textY = m_rect.y + (boxH - textH) / 2; // 渲染文本(TextRenderer 的 x,y 为左上角) m_textRenderer->renderText(m_text, m_textStyle, textX, textY); diff --git a/src/ui/base/UIComponent.h b/src/ui/base/UIComponent.h index 8ceee82..8fa2c54 100644 --- a/src/ui/base/UIComponent.h +++ b/src/ui/base/UIComponent.h @@ -34,9 +34,13 @@ public: * @brief 设置组件位置 * @param x X坐标(屏幕像素) * @param y Y坐标(屏幕像素) + * @param w 宽度(屏幕像素) + * @param h 高度(屏幕像素) */ - virtual void setPosition(int x, int y) { + virtual void setRect(int x, int y, int w, int h) { + m_rect.w = static_cast(w); + m_rect.h = static_cast(h); m_rect.x = static_cast(x); m_rect.y = static_cast(y); } diff --git a/src/ui/components/Button.cpp b/src/ui/components/Button.cpp index afe0db3..7b8fc8f 100644 --- a/src/ui/components/Button.cpp +++ b/src/ui/components/Button.cpp @@ -1,24 +1,25 @@ #include "Button.h" -#include "graphics/font/TextRenderer.h" +//#include "graphics/font/TextRenderer.h" Button::Button() { } -Button::Button(TextRenderer* textRenderer) : m_textRenderer(textRenderer) { -} + Button::Button( const std::string& text, TextStyle style, int x, int y, - TextRenderer* textRenderer, + int w, + int h, + //TextRenderer* textRenderer, SDL_Color backgroundColor, int borderThickness, SDL_Color borderColor -) : m_textRenderer(textRenderer) +) //: m_textRenderer(textRenderer) { m_rect.x = static_cast(x); m_rect.y = static_cast(y); @@ -29,12 +30,14 @@ Button::Button( m_buttonData.borderColor = borderColor; // 如果提供了 TextRenderer,则立即测量文本并更新控件尺寸 - if (m_textRenderer) { + /*if (m_textRenderer) { auto [w, h] = m_textRenderer->getTextSize(text, style); m_rect.w = static_cast(w); m_rect.h = static_cast(h); m_buttonData.rect = m_rect; - } + }*/ + m_rect.w = static_cast(w); + m_rect.h = static_cast(h); } @@ -43,24 +46,24 @@ void Button::setText(const std::string& text, TextStyle style) { m_buttonData.textstytle = style; // 如果提供了 TextRenderer,则立即测量文本并更新控件尺寸 - if (m_textRenderer) { + /*if (m_textRenderer) { auto [w, h] = m_textRenderer->getTextSize(text, style); m_rect.w = static_cast(w); m_rect.h = static_cast(h); m_buttonData.rect = m_rect; - } + }*/ } void Button::setText(const std::string& text) { m_buttonData.text = text; // 如果提供了 TextRenderer,则立即测量文本并更新控件尺寸 - if (m_textRenderer) { + /*if (m_textRenderer) { auto [w, h] = m_textRenderer->getTextSize(text, m_buttonData.textstytle); m_rect.w = static_cast(w); m_rect.h = static_cast(h); m_buttonData.rect = m_rect; - } + }*/ } void Button::setBackgroundColor(SDL_Color normal) { diff --git a/src/ui/components/Button.h b/src/ui/components/Button.h index f96a2e5..a96790c 100644 --- a/src/ui/components/Button.h +++ b/src/ui/components/Button.h @@ -1,6 +1,7 @@ #pragma once #include "ui/base/UIRenderData.h" #include "ui/base/UIComponent.h" +#include "utils/Config.h" #include // 前向声明,避免在头文件包含过多实现细节 @@ -10,15 +11,15 @@ class Button : public UIComponent{ public: // 默认构造(不进行自动测量) Button(); - // 可以传入 TextRenderer 指针以便在 setText 时立即计算文字尺寸并更新 rect - explicit Button(TextRenderer* textRenderer); explicit Button( const std::string& text, TextStyle style = {"SourceHanSansSC-Regular.otf", 48, {0, 0, 0, 255}}, int x = 0, int y = 0, - TextRenderer* textRenderer = nullptr, + int w = UI::ButtonSize, + int h = UI::ButtonSize, + //TextRenderer* textRenderer = nullptr, SDL_Color backgroundColor = {200, 200, 200, 255}, int borderThickness = 0, SDL_Color borderColor = {0, 0, 0, 255} @@ -78,7 +79,7 @@ private: std::function m_callback; ButtonData m_buttonData; // 用于在 setText 时测量文本尺寸(非拥有) - TextRenderer* m_textRenderer = nullptr; + //TextRenderer* m_textRenderer = nullptr; }; diff --git a/src/ui/managers/GameUIManager.cpp b/src/ui/managers/GameUIManager.cpp index 0c6035f..29c9b4b 100644 --- a/src/ui/managers/GameUIManager.cpp +++ b/src/ui/managers/GameUIManager.cpp @@ -126,10 +126,10 @@ void GameUIManager::updateGameState(GameState state) { void GameUIManager::setupUIComponents() { // 这里可以添加更多的UI组件初始化逻辑 - auto button = std::make_unique