diff --git a/Extra2D/include/extra2d/resource/resource_manager.h b/Extra2D/include/extra2d/resource/resource_manager.h index 8c6af02..4b59853 100644 --- a/Extra2D/include/extra2d/resource/resource_manager.h +++ b/Extra2D/include/extra2d/resource/resource_manager.h @@ -133,6 +133,55 @@ public: /// 卸载指定音效 void unloadSound(const std::string &key); + // ------------------------------------------------------------------------ + // 文本文件资源 + // ------------------------------------------------------------------------ + + /// 加载文本文件(带缓存) + /// @param filepath 文件路径,支持 romfs:/ 前缀 + /// @return 文件内容字符串,加载失败返回空字符串 + std::string loadTextFile(const std::string &filepath); + + /// 加载文本文件(指定编码) + /// @param filepath 文件路径 + /// @param encoding 文件编码(默认 UTF-8) + /// @return 文件内容字符串 + std::string loadTextFile(const std::string &filepath, const std::string &encoding); + + /// 通过key获取已缓存的文本内容 + std::string getTextFile(const std::string &key) const; + + /// 检查文本文件是否已缓存 + bool hasTextFile(const std::string &key) const; + + /// 卸载指定文本文件 + void unloadTextFile(const std::string &key); + + /// 清理所有文本文件缓存 + void clearTextFileCache(); + + // ------------------------------------------------------------------------ + // JSON 文件资源 + // ------------------------------------------------------------------------ + + /// 加载并解析 JSON 文件 + /// @param filepath 文件路径,支持 romfs:/ 前缀 + /// @return JSON 字符串内容,加载或解析失败返回空字符串 + /// @note 返回的是原始 JSON 字符串,需要自行解析 + std::string loadJsonFile(const std::string &filepath); + + /// 通过key获取已缓存的 JSON 内容 + std::string getJsonFile(const std::string &key) const; + + /// 检查 JSON 文件是否已缓存 + bool hasJsonFile(const std::string &key) const; + + /// 卸载指定 JSON 文件 + void unloadJsonFile(const std::string &key); + + /// 清理所有 JSON 文件缓存 + void clearJsonFileCache(); + // ------------------------------------------------------------------------ // 缓存清理 // ------------------------------------------------------------------------ @@ -152,6 +201,8 @@ public: size_t getTextureCacheSize() const; size_t getFontCacheSize() const; size_t getSoundCacheSize() const; + size_t getTextFileCacheSize() const; + size_t getJsonFileCacheSize() const; // ------------------------------------------------------------------------ // LRU 缓存管理 @@ -213,11 +264,17 @@ private: mutable std::mutex textureMutex_; mutable std::mutex fontMutex_; mutable std::mutex soundMutex_; + mutable std::mutex textFileMutex_; + mutable std::mutex jsonFileMutex_; // 资源缓存 - 使用弱指针实现自动清理 std::unordered_map> fontCache_; std::unordered_map> soundCache_; + // 文本文件缓存 - 使用强引用(字符串值类型) + std::unordered_map textFileCache_; + std::unordered_map jsonFileCache_; + // ============================================================================ // 纹理LRU缓存 // ============================================================================ diff --git a/Extra2D/src/audio/sound.cpp b/Extra2D/src/audio/sound.cpp index 197938a..b5dad3a 100644 --- a/Extra2D/src/audio/sound.cpp +++ b/Extra2D/src/audio/sound.cpp @@ -1,5 +1,6 @@ #include #include +#include namespace extra2d { @@ -20,22 +21,20 @@ Sound::~Sound() { bool Sound::play() { if (!chunk_) { + E2D_LOG_WARN("Sound::play() failed: chunk is null for {}", name_); return false; } - // 如果已在播放,先停止 - if (channel_ >= 0 && Mix_Playing(channel_)) { - Mix_HaltChannel(channel_); - } - int loops = looping_ ? -1 : 0; - channel_ = Mix_PlayChannel(-1, chunk_, loops); // -1 = 自动分配通道 + int newChannel = Mix_PlayChannel(-1, chunk_, loops); - if (channel_ < 0) { + if (newChannel < 0) { + E2D_LOG_WARN("Sound::play() failed: no free channel for {} ({})", name_, Mix_GetError()); return false; } - // 设置音量 + channel_ = newChannel; + int mixVol = static_cast(volume_ * MIX_MAX_VOLUME); Mix_Volume(channel_, mixVol); diff --git a/Extra2D/src/resource/resource_manager.cpp b/Extra2D/src/resource/resource_manager.cpp index 0ba545d..29e6c4e 100644 --- a/Extra2D/src/resource/resource_manager.cpp +++ b/Extra2D/src/resource/resource_manager.cpp @@ -856,4 +856,220 @@ size_t ResourceManager::getSoundCacheSize() const { return soundCache_.size(); } +// ============================================================================ +// 文本文件资源 +// ============================================================================ + +std::string ResourceManager::loadTextFile(const std::string &filepath) { + return loadTextFile(filepath, "UTF-8"); +} + +std::string ResourceManager::loadTextFile(const std::string &filepath, const std::string &encoding) { + (void)encoding; // 目前只支持 UTF-8 + + std::lock_guard lock(textFileMutex_); + + // 检查缓存 + auto it = textFileCache_.find(filepath); + if (it != textFileCache_.end()) { + E2D_LOG_TRACE("ResourceManager: text file cache hit: {}", filepath); + return it->second; + } + + // 解析资源路径 + std::string resolvedPath = resolveResourcePath(filepath); + if (resolvedPath.empty()) { + E2D_LOG_ERROR("ResourceManager: text file not found: {}", filepath); + return ""; + } + + // 打开文件 + FILE *file = nullptr; +#ifdef _WIN32 + errno_t err = fopen_s(&file, resolvedPath.c_str(), "rb"); + if (err != 0 || !file) { + E2D_LOG_ERROR("ResourceManager: failed to open text file: {}", resolvedPath); + return ""; + } +#else + file = fopen(resolvedPath.c_str(), "rb"); + if (!file) { + E2D_LOG_ERROR("ResourceManager: failed to open text file: {}", resolvedPath); + return ""; + } +#endif + + // 获取文件大小 + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + if (fileSize <= 0) { + fclose(file); + E2D_LOG_WARN("ResourceManager: text file is empty: {}", resolvedPath); + return ""; + } + + // 读取文件内容 + std::string content; + content.resize(fileSize); + size_t readSize = fread(&content[0], 1, fileSize, file); + fclose(file); + + if (readSize != static_cast(fileSize)) { + E2D_LOG_ERROR("ResourceManager: failed to read text file: {}", resolvedPath); + return ""; + } + + // 缓存内容 + textFileCache_[filepath] = content; + E2D_LOG_DEBUG("ResourceManager: loaded text file: {} ({} bytes)", filepath, content.size()); + + return content; +} + +std::string ResourceManager::getTextFile(const std::string &key) const { + std::lock_guard lock(textFileMutex_); + auto it = textFileCache_.find(key); + if (it != textFileCache_.end()) { + return it->second; + } + return ""; +} + +bool ResourceManager::hasTextFile(const std::string &key) const { + std::lock_guard lock(textFileMutex_); + return textFileCache_.find(key) != textFileCache_.end(); +} + +void ResourceManager::unloadTextFile(const std::string &key) { + std::lock_guard lock(textFileMutex_); + auto it = textFileCache_.find(key); + if (it != textFileCache_.end()) { + textFileCache_.erase(it); + E2D_LOG_DEBUG("ResourceManager: unloaded text file: {}", key); + } +} + +void ResourceManager::clearTextFileCache() { + std::lock_guard lock(textFileMutex_); + size_t count = textFileCache_.size(); + textFileCache_.clear(); + E2D_LOG_INFO("ResourceManager: cleared {} text files from cache", count); +} + +size_t ResourceManager::getTextFileCacheSize() const { + std::lock_guard lock(textFileMutex_); + return textFileCache_.size(); +} + +// ============================================================================ +// JSON 文件资源 +// ============================================================================ + +std::string ResourceManager::loadJsonFile(const std::string &filepath) { + std::lock_guard lock(jsonFileMutex_); + + // 检查缓存 + auto it = jsonFileCache_.find(filepath); + if (it != jsonFileCache_.end()) { + E2D_LOG_TRACE("ResourceManager: JSON file cache hit: {}", filepath); + return it->second; + } + + // 解析资源路径 + std::string resolvedPath = resolveResourcePath(filepath); + if (resolvedPath.empty()) { + E2D_LOG_ERROR("ResourceManager: JSON file not found: {}", filepath); + return ""; + } + + // 打开文件 + FILE *file = nullptr; +#ifdef _WIN32 + errno_t err = fopen_s(&file, resolvedPath.c_str(), "rb"); + if (err != 0 || !file) { + E2D_LOG_ERROR("ResourceManager: failed to open JSON file: {}", resolvedPath); + return ""; + } +#else + file = fopen(resolvedPath.c_str(), "rb"); + if (!file) { + E2D_LOG_ERROR("ResourceManager: failed to open JSON file: {}", resolvedPath); + return ""; + } +#endif + + // 获取文件大小 + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + if (fileSize <= 0) { + fclose(file); + E2D_LOG_WARN("ResourceManager: JSON file is empty: {}", resolvedPath); + return ""; + } + + // 读取文件内容 + std::string content; + content.resize(fileSize); + size_t readSize = fread(&content[0], 1, fileSize, file); + fclose(file); + + if (readSize != static_cast(fileSize)) { + E2D_LOG_ERROR("ResourceManager: failed to read JSON file: {}", resolvedPath); + return ""; + } + + // 简单验证 JSON 格式(检查是否以 { 或 [ 开头) + size_t firstValid = content.find_first_not_of(" \t\n\r"); + if (firstValid == std::string::npos || + (content[firstValid] != '{' && content[firstValid] != '[')) { + E2D_LOG_WARN("ResourceManager: file may not be valid JSON: {}", filepath); + // 不阻止加载,只是警告 + } + + // 缓存内容 + jsonFileCache_[filepath] = content; + E2D_LOG_DEBUG("ResourceManager: loaded JSON file: {} ({} bytes)", filepath, content.size()); + + return content; +} + +std::string ResourceManager::getJsonFile(const std::string &key) const { + std::lock_guard lock(jsonFileMutex_); + auto it = jsonFileCache_.find(key); + if (it != jsonFileCache_.end()) { + return it->second; + } + return ""; +} + +bool ResourceManager::hasJsonFile(const std::string &key) const { + std::lock_guard lock(jsonFileMutex_); + return jsonFileCache_.find(key) != jsonFileCache_.end(); +} + +void ResourceManager::unloadJsonFile(const std::string &key) { + std::lock_guard lock(jsonFileMutex_); + auto it = jsonFileCache_.find(key); + if (it != jsonFileCache_.end()) { + jsonFileCache_.erase(it); + E2D_LOG_DEBUG("ResourceManager: unloaded JSON file: {}", key); + } +} + +void ResourceManager::clearJsonFileCache() { + std::lock_guard lock(jsonFileMutex_); + size_t count = jsonFileCache_.size(); + jsonFileCache_.clear(); + E2D_LOG_INFO("ResourceManager: cleared {} JSON files from cache", count); +} + +size_t ResourceManager::getJsonFileCacheSize() const { + std::lock_guard lock(jsonFileMutex_); + return jsonFileCache_.size(); +} + } // namespace extra2d diff --git a/Extra2D/src/scene/scene.cpp b/Extra2D/src/scene/scene.cpp index 42bd6ef..adb6050 100644 --- a/Extra2D/src/scene/scene.cpp +++ b/Extra2D/src/scene/scene.cpp @@ -13,8 +13,7 @@ void Scene::setViewportSize(float width, float height) { viewportSize_ = Size(width, height); if (defaultCamera_) { defaultCamera_->setViewport(0, width, height, 0); - } - if (camera_) { + } else if (camera_) { camera_->setViewport(0, width, height, 0); } } @@ -119,7 +118,7 @@ std::vector> Scene::queryCollisions() const { } void Scene::collectRenderCommands(std::vector &commands, - int parentZOrder) { + int parentZOrder) { if (!isVisible()) return; diff --git a/examples/flappy_bird/BaseScene.cpp b/examples/flappy_bird/BaseScene.cpp new file mode 100644 index 0000000..c267502 --- /dev/null +++ b/examples/flappy_bird/BaseScene.cpp @@ -0,0 +1,81 @@ +// ============================================================================ +// BaseScene.cpp - Flappy Bird 基础场景实现 +// ============================================================================ + +#include "BaseScene.h" +#include + +namespace flappybird { + +BaseScene::BaseScene() { + // 设置背景颜色为黑色(窗口四周会显示这个颜色) + setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f)); +} + +void BaseScene::onEnter() { + extra2d::Scene::onEnter(); + // 计算并更新视口 + updateViewport(); +} + +void BaseScene::updateViewport() { + auto &app = extra2d::Application::instance(); + float windowWidth = static_cast(app.window().getWidth()); + float windowHeight = static_cast(app.window().getHeight()); + + // 计算游戏内容在窗口中的居中位置 + // 保持游戏原始宽高比,进行"黑边"适配 + float scaleX = windowWidth / GAME_WIDTH; + float scaleY = windowHeight / GAME_HEIGHT; + // 使用较小的缩放比例,确保游戏内容完整显示在窗口中 + float scale = std::min(scaleX, scaleY); + + scaledGameWidth_ = GAME_WIDTH * scale; + scaledGameHeight_ = GAME_HEIGHT * scale; + // 计算居中偏移,使游戏内容在窗口中水平和垂直居中 + viewportOffsetX_ = (windowWidth - scaledGameWidth_) * 0.5f; + viewportOffsetY_ = (windowHeight - scaledGameHeight_) * 0.5f; + + // 设置视口大小为游戏逻辑分辨率 + setViewportSize(GAME_WIDTH, GAME_HEIGHT); + + // 创建并设置相机 + auto camera = extra2d::makePtr(); + // 设置正交投影,覆盖整个游戏逻辑区域 + // 注意:对于2D游戏,Y轴向下增长,所以bottom > top + camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f); + setCamera(camera); +} + +void BaseScene::onRender(extra2d::RenderBackend &renderer) { + // 检查窗口大小是否改变,如果改变则更新视口 + auto &app = extra2d::Application::instance(); + float currentWindowWidth = static_cast(app.window().getWidth()); + float currentWindowHeight = static_cast(app.window().getHeight()); + + // 如果窗口大小改变,重新计算视口 + float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f; + float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f; + if (std::abs(currentWindowWidth - expectedWidth) > 1.0f || + std::abs(currentWindowHeight - expectedHeight) > 1.0f) { + E2D_LOG_INFO("BaseScene::onRender - window size changed from ({} x {}) to " + "({} x {}), updating viewport", + expectedWidth, expectedHeight, currentWindowWidth, + currentWindowHeight); + updateViewport(); + } + + // 设置视口为居中区域 + E2D_LOG_INFO( + "BaseScene::onRender - setting viewport: x={}, y={}, width={}, height={}", + static_cast(viewportOffsetX_), static_cast(viewportOffsetY_), + static_cast(scaledGameWidth_), static_cast(scaledGameHeight_)); + renderer.setViewport( + static_cast(viewportOffsetX_), static_cast(viewportOffsetY_), + static_cast(scaledGameWidth_), static_cast(scaledGameHeight_)); + + // 调用父类的 onRender 进行实际渲染 + extra2d::Scene::onRender(renderer); +} + +} // namespace flappybird diff --git a/examples/flappy_bird/BaseScene.h b/examples/flappy_bird/BaseScene.h new file mode 100644 index 0000000..7ce3d43 --- /dev/null +++ b/examples/flappy_bird/BaseScene.h @@ -0,0 +1,51 @@ +// ============================================================================ +// BaseScene.h - Flappy Bird 基础场景类 +// 描述: 提供统一的居中视口适配功能,所有游戏场景都应继承此类 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +// 游戏逻辑分辨率(原始 Flappy Bird 尺寸) +static constexpr float GAME_WIDTH = 288.0f; +static constexpr float GAME_HEIGHT = 512.0f; + +/** + * @brief Flappy Bird 基础场景类 + * 所有游戏场景都应继承此类,以获得统一的居中视口适配功能 + */ +class BaseScene : public extra2d::Scene { +public: + /** + * @brief 构造函数 + */ + BaseScene(); + + /** + * @brief 场景进入时调用 + */ + void onEnter() override; + + /** + * @brief 渲染时调用,设置居中视口 + * @param renderer 渲染后端 + */ + void onRender(extra2d::RenderBackend &renderer) override; + +protected: + /** + * @brief 更新视口计算,使游戏内容在窗口中居中显示 + */ + void updateViewport(); + + // 视口适配参数(用于在窗口中居中显示游戏内容) + float scaledGameWidth_ = 0.0f; // 缩放后的游戏宽度 + float scaledGameHeight_ = 0.0f; // 缩放后的游戏高度 + float viewportOffsetX_ = 0.0f; // 视口水平偏移 + float viewportOffsetY_ = 0.0f; // 视口垂直偏移 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/GameOverLayer.cpp b/examples/flappy_bird/GameOverLayer.cpp index 13eb3fe..1a75167 100644 --- a/examples/flappy_bird/GameOverLayer.cpp +++ b/examples/flappy_bird/GameOverLayer.cpp @@ -3,6 +3,7 @@ // ============================================================================ #include "GameOverLayer.h" +#include "BaseScene.h" #include "GameScene.h" #include "Number.h" #include "ResLoader.h" @@ -19,9 +20,9 @@ void GameOverLayer::onEnter() { Node::onEnter(); // 在 onEnter 中初始化,此时 weak_from_this() 可用 - auto &app = extra2d::Application::instance(); - float screenWidth = static_cast(app.getConfig().width); - float screenHeight = static_cast(app.getConfig().height); + // 使用游戏逻辑分辨率 + float screenWidth = GAME_WIDTH; + float screenHeight = GAME_HEIGHT; // 整体居中(x 坐标相对于屏幕中心) setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight)); @@ -45,6 +46,15 @@ void GameOverLayer::onEnter() { // 创建向上移动的动画(从屏幕底部移动到正常位置) auto moveAction = extra2d::makePtr( 1.0f, extra2d::Vec2(0.0f, -screenHeight)); + moveAction->setCompletionCallback([this]() { + animationDone_ = true; + if (restartBtn_) + restartBtn_->setEnabled(true); + if (menuBtn_) + menuBtn_->setEnabled(true); + if (shareBtn_) + shareBtn_->setEnabled(true); + }); runAction(moveAction); } @@ -107,63 +117,60 @@ void GameOverLayer::initPanel(int score, float screenHeight) { } void GameOverLayer::initButtons() { - // 创建重新开始按钮(y=360) auto restartFrame = ResLoader::getKeyFrame("button_restart"); if (restartFrame) { - auto restartBtn = extra2d::Button::create(); - restartBtn->setBackgroundImage(restartFrame->getTexture(), - restartFrame->getRect()); - restartBtn->setAnchor(extra2d::Vec2(0.5f, 0.5f)); - restartBtn->setPosition( - extra2d::Vec2(0.0f, 360.0f)); // x=0 表示相对于中心点 - restartBtn->setOnClick([]() { + restartBtn_ = extra2d::Button::create(); + restartBtn_->setBackgroundImage(restartFrame->getTexture(), + restartFrame->getRect()); + restartBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + restartBtn_->setPosition(extra2d::Vec2(0.0f, 360.0f)); + restartBtn_->setEnabled(false); + restartBtn_->setOnClick([]() { ResLoader::playMusic(MusicType::Click); auto &app = extra2d::Application::instance(); app.scenes().replaceScene(extra2d::makePtr(), extra2d::TransitionType::Fade, 0.5f); }); - addChild(restartBtn); + addChild(restartBtn_); } - // 创建返回主菜单按钮(y=420) auto menuFrame = ResLoader::getKeyFrame("button_menu"); if (menuFrame) { - auto menuBtn = extra2d::Button::create(); - menuBtn->setBackgroundImage(menuFrame->getTexture(), menuFrame->getRect()); - menuBtn->setAnchor(extra2d::Vec2(0.5f, 0.5f)); - menuBtn->setPosition(extra2d::Vec2(0.0f, 420.0f)); // x=0 表示相对于中心点 - menuBtn->setOnClick([]() { + menuBtn_ = extra2d::Button::create(); + menuBtn_->setBackgroundImage(menuFrame->getTexture(), menuFrame->getRect()); + menuBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + menuBtn_->setPosition(extra2d::Vec2(0.0f, 420.0f)); + menuBtn_->setEnabled(false); + menuBtn_->setOnClick([]() { ResLoader::playMusic(MusicType::Click); auto &app = extra2d::Application::instance(); app.scenes().replaceScene(extra2d::makePtr(), extra2d::TransitionType::Fade, 0.5f); }); - addChild(menuBtn); + addChild(menuBtn_); } - // 创建分享按钮(y=480,在 MENU 按钮下方) auto shareFrame = ResLoader::getKeyFrame("button_share"); if (shareFrame) { - auto shareBtn = extra2d::Button::create(); - shareBtn->setBackgroundImage(shareFrame->getTexture(), - shareFrame->getRect()); - shareBtn->setAnchor(extra2d::Vec2(0.5f, 0.5f)); - shareBtn->setPosition(extra2d::Vec2(0.0f, 460.0f)); // x=0 表示相对于中心点 - shareBtn->setOnClick([]() { - ResLoader::playMusic(MusicType::Click); - // TODO: 实现分享功能 - }); - addChild(shareBtn); + shareBtn_ = extra2d::Button::create(); + shareBtn_->setBackgroundImage(shareFrame->getTexture(), + shareFrame->getRect()); + shareBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + shareBtn_->setPosition(extra2d::Vec2(0.0f, 460.0f)); + shareBtn_->setEnabled(false); + shareBtn_->setOnClick([]() { ResLoader::playMusic(MusicType::Click); }); + addChild(shareBtn_); } } void GameOverLayer::onUpdate(float dt) { Node::onUpdate(dt); - // 检测手柄按键 + if (!animationDone_) + return; + auto &input = extra2d::Application::instance().input(); - // A 键重新开始游戏 if (input.isButtonPressed(extra2d::GamepadButton::A)) { ResLoader::playMusic(MusicType::Click); auto &app = extra2d::Application::instance(); @@ -171,7 +178,6 @@ void GameOverLayer::onUpdate(float dt) { extra2d::TransitionType::Fade, 0.5f); } - // B 键返回主菜单 if (input.isButtonPressed(extra2d::GamepadButton::B)) { ResLoader::playMusic(MusicType::Click); auto &app = extra2d::Application::instance(); diff --git a/examples/flappy_bird/GameOverLayer.h b/examples/flappy_bird/GameOverLayer.h index f2806ee..4feda4d 100644 --- a/examples/flappy_bird/GameOverLayer.h +++ b/examples/flappy_bird/GameOverLayer.h @@ -15,44 +15,48 @@ namespace flappybird { */ class GameOverLayer : public extra2d::Node { public: - /** - * @brief 构造函数 - * @param score 本局得分 - */ - GameOverLayer(int score); + /** + * @brief 构造函数 + * @param score 本局得分 + */ + GameOverLayer(int score); - /** - * @brief 进入场景时调用 - */ - void onEnter() override; + /** + * @brief 进入场景时调用 + */ + void onEnter() override; - /** - * @brief 每帧更新时调用 - * @param dt 时间间隔 - */ - void onUpdate(float dt) override; + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔 + */ + void onUpdate(float dt) override; private: - /** - * @brief 初始化得分面板 - * @param score 本局得分 - * @param screenHeight 屏幕高度 - */ - void initPanel(int score, float screenHeight); + /** + * @brief 初始化得分面板 + * @param score 本局得分 + * @param screenHeight 屏幕高度 + */ + void initPanel(int score, float screenHeight); - /** - * @brief 初始化按钮 - */ - void initButtons(); + /** + * @brief 初始化按钮 + */ + void initButtons(); - /** - * @brief 根据得分获取奖牌 - * @param score 得分 - * @return 奖牌精灵帧 - */ - extra2d::Ptr getMedal(int score); + /** + * @brief 根据得分获取奖牌 + * @param score 得分 + * @return 奖牌精灵帧 + */ + extra2d::Ptr getMedal(int score); - int score_ = 0; // 本局得分 + int score_ = 0; // 本局得分 + bool animationDone_ = false; // 动画是否完成 + extra2d::Ptr restartBtn_; // 重新开始按钮 + extra2d::Ptr menuBtn_; // 菜单按钮 + extra2d::Ptr shareBtn_; // 分享按钮 }; } // namespace flappybird diff --git a/examples/flappy_bird/GameScene.cpp b/examples/flappy_bird/GameScene.cpp index 235ee0f..7b27383 100644 --- a/examples/flappy_bird/GameScene.cpp +++ b/examples/flappy_bird/GameScene.cpp @@ -10,20 +10,15 @@ namespace flappybird { GameScene::GameScene() { - auto &app = extra2d::Application::instance(); - auto &config = app.getConfig(); - setViewportSize(static_cast(config.width), - static_cast(config.height)); - // 设置背景颜色为黑色 - setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f)); + // 基类 BaseScene 已经处理了视口设置和背景颜色 } void GameScene::onEnter() { - extra2d::Scene::onEnter(); + BaseScene::onEnter(); - auto &app = extra2d::Application::instance(); - float screenWidth = static_cast(app.getConfig().width); - float screenHeight = static_cast(app.getConfig().height); + // 游戏坐标系:使用游戏逻辑分辨率 + float screenWidth = GAME_WIDTH; + float screenHeight = GAME_HEIGHT; // 添加背景(使用左上角锚点,与原游戏一致) auto bgFrame = ResLoader::getKeyFrame("bg_day"); @@ -91,69 +86,52 @@ void GameScene::onEnter() { } void GameScene::onUpdate(float dt) { - // 注意:这里要先调用父类的 onUpdate,以确保 GameOverLayer 的动画能播放 - extra2d::Scene::onUpdate(dt); + if (!gameOver_) { + if (!bird_) + return; - // 游戏结束后不再更新游戏逻辑(但子节点的动画继续) - if (gameOver_) - return; + auto &input = extra2d::Application::instance().input(); - if (!bird_) - return; - - auto &input = extra2d::Application::instance().input(); - - // 检测跳跃按键(A键或空格) - if (input.isButtonPressed(extra2d::GamepadButton::A) || - input.isMousePressed(extra2d::MouseButton::Left)) { - if (!started_) { - // 游戏还没开始,开始游戏 - started_ = true; - startGame(); + if (input.isButtonPressed(extra2d::GamepadButton::A) || + input.isMousePressed(extra2d::MouseButton::Left)) { + if (!started_) { + started_ = true; + startGame(); + } + bird_->jump(); } - bird_->jump(); - } - // 游戏已经开始 - if (started_) { - // 模拟小鸟下落 - bird_->fall(dt); + if (started_) { + bird_->fall(dt); - // 检查得分(小鸟飞过水管) - if (pipes_) { - Pipe *firstPipe = pipes_->getPipe(0); - if (firstPipe && !firstPipe->scored) { - float birdX = bird_->getPosition().x; - float pipeX = firstPipe->getPosition().x; - if (pipeX <= birdX) { - // 小鸟飞过了水管 - score_++; - scoreNumber_->setNumber(score_); - firstPipe->scored = true; - ResLoader::playMusic(MusicType::Point); + if (pipes_) { + Pipe *firstPipe = pipes_->getPipe(0); + if (firstPipe && !firstPipe->scored) { + float birdX = bird_->getPosition().x; + float pipeX = firstPipe->getPosition().x; + if (pipeX <= birdX) { + score_++; + scoreNumber_->setNumber(score_); + firstPipe->scored = true; + ResLoader::playMusic(MusicType::Point); + } } } - } - // 检查碰撞 - if (bird_->isLiving() && checkCollision()) { - onHit(); - } + if (bird_->isLiving() && checkCollision()) { + onHit(); + } - // 检查是否撞到地面(原游戏使用 123 作为地面高度) - auto &app = extra2d::Application::instance(); - float screenHeight = static_cast(app.getConfig().height); - - if (screenHeight - bird_->getPosition().y <= 123.0f) { - // 小鸟撞到地面 - bird_->setPosition( - extra2d::Vec2(bird_->getPosition().x, screenHeight - 123.0f)); - bird_->setStatus(Bird::Status::Still); - onHit(); - - gameOver(); + if (bird_->isLiving() && GAME_HEIGHT - bird_->getPosition().y <= 123.0f) { + bird_->setPosition( + extra2d::Vec2(bird_->getPosition().x, GAME_HEIGHT - 123.0f)); + bird_->setStatus(Bird::Status::Still); + onHit(); + } } } + + BaseScene::onUpdate(dt); } void GameScene::startGame() { @@ -232,18 +210,16 @@ void GameScene::onHit() { scoreNumber_->setVisible(false); } - // 设置游戏结束标志 - gameOver_ = true; - - // 延迟显示游戏结束界面 gameOver(); } void GameScene::gameOver() { + if (gameOver_) + return; + started_ = false; gameOver_ = true; - // 显示游戏结束层 auto gameOverLayer = extra2d::makePtr(score_); addChild(gameOverLayer); } diff --git a/examples/flappy_bird/GameScene.h b/examples/flappy_bird/GameScene.h index 01b1e80..0e6fa3b 100644 --- a/examples/flappy_bird/GameScene.h +++ b/examples/flappy_bird/GameScene.h @@ -5,7 +5,7 @@ #pragma once -#include +#include "BaseScene.h" #include "Bird.h" #include "Pipes.h" #include "Ground.h" @@ -17,7 +17,7 @@ namespace flappybird { * @brief 游戏主场景类 * 游戏的核心场景,处理游戏逻辑、碰撞检测和得分 */ -class GameScene : public extra2d::Scene { +class GameScene : public BaseScene { public: /** * @brief 构造函数 diff --git a/examples/flappy_bird/ResLoader.cpp b/examples/flappy_bird/ResLoader.cpp index 8a01ee8..7b63a18 100644 --- a/examples/flappy_bird/ResLoader.cpp +++ b/examples/flappy_bird/ResLoader.cpp @@ -4,7 +4,6 @@ #include "ResLoader.h" #include -#include namespace flappybird { @@ -13,84 +12,88 @@ std::map ResLoader::imageMap_; std::map> ResLoader::soundMap_; void ResLoader::init() { - auto& resources = extra2d::Application::instance().resources(); + auto &resources = extra2d::Application::instance().resources(); - // 加载图集纹理 - atlasTexture_ = resources.loadTexture("assets/images/atlas.png"); - if (!atlasTexture_) { - E2D_LOG_ERROR("无法加载图集纹理 atlas.png"); - return; + // 加载图集纹理 + atlasTexture_ = resources.loadTexture("assets/images/atlas.png"); + if (!atlasTexture_) { + E2D_LOG_ERROR("无法加载图集纹理 atlas.png"); + return; + } + + // 使用资源管理器加载 JSON 文件 + std::string jsonContent = resources.loadJsonFile("assets/images/atlas.json"); + if (jsonContent.empty()) { + E2D_LOG_ERROR("无法加载 atlas.json 文件"); + return; + } + + // 解析 JSON 图集数据 + try { + nlohmann::json jsonData = nlohmann::json::parse(jsonContent); + + for (const auto &sprite : jsonData["sprites"]) { + std::string name = sprite["name"]; + float x = sprite["x"]; + float y = sprite["y"]; + float width = sprite["width"]; + float height = sprite["height"]; + + ImageInfo info = {width, height, x, y}; + imageMap_[name] = info; } - // 读取 atlas.json 文件 - std::ifstream file("assets/images/atlas.json"); - if (!file.is_open()) { - E2D_LOG_ERROR("无法打开 atlas.json 文件"); - return; - } + E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size()); + } catch (const std::exception &e) { + E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what()); + return; + } - // 解析 JSON 图集数据 - try { - nlohmann::json jsonData; - file >> jsonData; - - for (const auto& sprite : jsonData["sprites"]) { - std::string name = sprite["name"]; - float x = sprite["x"]; - float y = sprite["y"]; - float width = sprite["width"]; - float height = sprite["height"]; - - ImageInfo info = { width, height, x, y }; - imageMap_[name] = info; - } - - E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size()); - } catch (const std::exception& e) { - E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what()); - file.close(); - return; - } - - file.close(); + // 加载音效 + soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav"); + soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav"); + soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav"); + soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav"); + soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav"); - // 加载音效 - soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav"); - soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav"); - soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav"); - soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav"); - soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav"); - - E2D_LOG_INFO("资源加载完成"); + E2D_LOG_INFO("资源加载完成"); } -extra2d::Ptr ResLoader::getKeyFrame(const std::string& name) { - auto it = imageMap_.find(name); - if (it == imageMap_.end()) { - E2D_LOG_WARN("找不到精灵帧: %s", name.c_str()); - return nullptr; - } +extra2d::Ptr +ResLoader::getKeyFrame(const std::string &name) { + auto it = imageMap_.find(name); + if (it == imageMap_.end()) { + E2D_LOG_WARN("找不到精灵帧: %s", name.c_str()); + return nullptr; + } - const ImageInfo& info = it->second; - E2D_LOG_INFO("加载精灵帧: name={}, w={}, h={}, x={}, y={}", - name, info.width, info.height, info.x, info.y); - - // 检查纹理尺寸 - if (atlasTexture_) { - E2D_LOG_INFO("图集纹理尺寸: {}x{}", atlasTexture_->getWidth(), atlasTexture_->getHeight()); - } - - return extra2d::makePtr( - atlasTexture_, - extra2d::Rect(info.x, info.y, info.width, info.height) - ); + const ImageInfo &info = it->second; + E2D_LOG_INFO("加载精灵帧: name={}, w={}, h={}, x={}, y={}", name, info.width, + info.height, info.x, info.y); + + // 检查纹理尺寸 + if (atlasTexture_) { + E2D_LOG_INFO("图集纹理尺寸: {}x{}", atlasTexture_->getWidth(), + atlasTexture_->getHeight()); + } + + return extra2d::makePtr( + atlasTexture_, extra2d::Rect(info.x, info.y, info.width, info.height)); } void ResLoader::playMusic(MusicType type) { - auto it = soundMap_.find(type); - if (it != soundMap_.end() && it->second) { - it->second->play(); - } + auto it = soundMap_.find(type); + if (it == soundMap_.end()) { + E2D_LOG_WARN("ResLoader::playMusic: sound type not found"); + return; + } + if (!it->second) { + E2D_LOG_WARN("ResLoader::playMusic: sound pointer is null"); + return; + } + if (!it->second->play()) { + E2D_LOG_WARN("ResLoader::playMusic: failed to play sound"); + } } } // namespace flappybird diff --git a/examples/flappy_bird/SplashScene.cpp b/examples/flappy_bird/SplashScene.cpp index 2874cd5..ca328dd 100644 --- a/examples/flappy_bird/SplashScene.cpp +++ b/examples/flappy_bird/SplashScene.cpp @@ -3,67 +3,47 @@ // ============================================================================ #include "SplashScene.h" -#include "StartScene.h" #include "ResLoader.h" +#include "StartScene.h" +#include namespace flappybird { SplashScene::SplashScene() { - // 设置视口大小 - auto& app = extra2d::Application::instance(); - auto& config = app.getConfig(); - setViewportSize(static_cast(config.width), static_cast(config.height)); + // 基类 BaseScene 已经处理了视口设置和背景颜色 } void SplashScene::onEnter() { - extra2d::Scene::onEnter(); + BaseScene::onEnter(); - // 设置黑色背景 - setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f)); - - auto viewport = getViewportSize(); - float centerX = viewport.width / 2.0f; - float centerY = viewport.height / 2.0f; - - // 尝试加载 splash 图片 - auto splashFrame = ResLoader::getKeyFrame("splash"); - if (splashFrame) { - auto splash = extra2d::Sprite::create(splashFrame->getTexture(), splashFrame->getRect()); - splash->setAnchor(0.5f, 0.5f); - splash->setPosition(centerX, centerY); - addChild(splash); - } else { - // 如果 splash 加载失败,尝试加载 title 图片作为备用 - auto titleFrame = ResLoader::getKeyFrame("title"); - if (titleFrame) { - auto title = extra2d::Sprite::create(titleFrame->getTexture(), titleFrame->getRect()); - title->setAnchor(0.5f, 0.5f); - title->setPosition(centerX, centerY); - addChild(title); - } - } - - // 播放转场音效 - ResLoader::playMusic(MusicType::Swoosh); + // 尝试加载 splash 图片 + auto splashFrame = ResLoader::getKeyFrame("splash"); + if (splashFrame) { + auto splash = extra2d::Sprite::create(splashFrame->getTexture(), + splashFrame->getRect()); + splash->setAnchor(0.5f, 0.5f); + // splash 图片是全屏的(288x512),将其中心放在游戏区域中心 + splash->setPosition(GAME_WIDTH / 2.0f, GAME_HEIGHT / 2.0f); + addChild(splash); + } + // 播放转场音效 + ResLoader::playMusic(MusicType::Swoosh); } void SplashScene::onUpdate(float dt) { - extra2d::Scene::onUpdate(dt); + BaseScene::onUpdate(dt); - // 计时 - timer_ += dt; - if (timer_ >= delay_) { - gotoStartScene(); - } + // 计时 + timer_ += dt; + if (timer_ >= delay_) { + gotoStartScene(); + } } void SplashScene::gotoStartScene() { - auto& app = extra2d::Application::instance(); - app.scenes().replaceScene( - extra2d::makePtr(), - extra2d::TransitionType::Fade, - 0.5f - ); + auto &app = extra2d::Application::instance(); + app.scenes().replaceScene(extra2d::makePtr(), + extra2d::TransitionType::Fade, 0.5f); } } // namespace flappybird diff --git a/examples/flappy_bird/SplashScene.h b/examples/flappy_bird/SplashScene.h index cf25dcd..57e64e6 100644 --- a/examples/flappy_bird/SplashScene.h +++ b/examples/flappy_bird/SplashScene.h @@ -5,7 +5,7 @@ #pragma once -#include +#include "BaseScene.h" namespace flappybird { @@ -13,32 +13,32 @@ namespace flappybird { * @brief 启动场景类 * 显示游戏 Logo,短暂延迟后进入主菜单 */ -class SplashScene : public extra2d::Scene { +class SplashScene : public BaseScene { public: - /** - * @brief 构造函数 - */ - SplashScene(); + /** + * @brief 构造函数 + */ + SplashScene(); - /** - * @brief 场景进入时调用 - */ - void onEnter() override; + /** + * @brief 场景进入时调用 + */ + void onEnter() override; - /** - * @brief 每帧更新时调用 - * @param dt 时间间隔(秒) - */ - void onUpdate(float dt) override; + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override; private: - /** - * @brief 跳转到开始场景 - */ - void gotoStartScene(); + /** + * @brief 跳转到开始场景 + */ + void gotoStartScene(); - float timer_ = 0.0f; // 计时器 - const float delay_ = 2.0f; // 延迟时间(秒) + float timer_ = 0.0f; // 计时器 + const float delay_ = 2.0f; // 延迟时间(秒) }; } // namespace flappybird diff --git a/examples/flappy_bird/StartScene.cpp b/examples/flappy_bird/StartScene.cpp index 19fd9e5..ae43a51 100644 --- a/examples/flappy_bird/StartScene.cpp +++ b/examples/flappy_bird/StartScene.cpp @@ -12,21 +12,15 @@ namespace flappybird { StartScene::StartScene() { - auto &app = extra2d::Application::instance(); - auto &config = app.getConfig(); - setViewportSize(static_cast(config.width), - static_cast(config.height)); + // 基类 BaseScene 已经处理了视口设置和背景颜色 } void StartScene::onEnter() { - extra2d::Scene::onEnter(); + BaseScene::onEnter(); - // 设置背景颜色为黑色(防止透明) - setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f)); - - auto &app = extra2d::Application::instance(); - float screenWidth = static_cast(app.getConfig().width); - float screenHeight = static_cast(app.getConfig().height); + // 使用游戏逻辑分辨率 + float screenWidth = GAME_WIDTH; + float screenHeight = GAME_HEIGHT; // 添加背景(使用左上角锚点) auto bgFrame = ResLoader::getKeyFrame("bg_day"); @@ -125,7 +119,7 @@ void StartScene::onEnter() { } void StartScene::onUpdate(float dt) { - extra2d::Scene::onUpdate(dt); + BaseScene::onUpdate(dt); // 检测 A 键或空格开始游戏 auto &input = extra2d::Application::instance().input(); diff --git a/examples/flappy_bird/StartScene.h b/examples/flappy_bird/StartScene.h index 855d623..01cc56d 100644 --- a/examples/flappy_bird/StartScene.h +++ b/examples/flappy_bird/StartScene.h @@ -5,7 +5,7 @@ #pragma once -#include +#include "BaseScene.h" namespace flappybird { @@ -13,7 +13,7 @@ namespace flappybird { * @brief 开始场景类 * 游戏主菜单,包含开始游戏按钮和版权信息 */ -class StartScene : public extra2d::Scene { +class StartScene : public BaseScene { public: /** * @brief 构造函数 diff --git a/examples/flappy_bird/ground.cpp b/examples/flappy_bird/ground.cpp index 00bf3e6..1592b68 100644 --- a/examples/flappy_bird/ground.cpp +++ b/examples/flappy_bird/ground.cpp @@ -4,14 +4,15 @@ #include "Ground.h" #include "ResLoader.h" +#include "BaseScene.h" namespace flappybird { Ground::Ground() { moving_ = true; - auto& app = extra2d::Application::instance(); - float screenHeight = static_cast(app.getConfig().height); + // 使用游戏逻辑高度,而不是窗口高度 + float screenHeight = GAME_HEIGHT; // 获取地面纹理帧 auto landFrame = ResLoader::getKeyFrame("land"); diff --git a/examples/flappy_bird/main.cpp b/examples/flappy_bird/main.cpp index 00c39c9..14cff1e 100644 --- a/examples/flappy_bird/main.cpp +++ b/examples/flappy_bird/main.cpp @@ -4,51 +4,50 @@ // 描述: 经典的 Flappy Bird 游戏实现 // ============================================================================ -#include -#include "SplashScene.h" #include "ResLoader.h" +#include "SplashScene.h" +#include using namespace extra2d; /** * @brief 程序入口 */ -int main(int argc, char **argv) -{ - // 初始化日志系统 - Logger::init(); - Logger::setLevel(LogLevel::Debug); +int main(int argc, char **argv) { + // 初始化日志系统 + Logger::init(); + Logger::setLevel(LogLevel::Debug); - E2D_LOG_INFO("========================"); - E2D_LOG_INFO("Extra2D FlappyBird"); - E2D_LOG_INFO("========================"); + E2D_LOG_INFO("========================"); + E2D_LOG_INFO("Extra2D FlappyBird"); + E2D_LOG_INFO("========================"); - // 获取应用实例 - auto &app = Application::instance(); + // 获取应用实例 + auto &app = Application::instance(); - // 配置应用 - AppConfig config; - config.title = "Extra2D - FlappyBird"; - config.width = 288; // 原始游戏宽度 - config.height = 512; // 原始游戏高度 - config.vsync = true; - config.fpsLimit = 60; + // 配置应用 + AppConfig config; + config.title = "Extra2D - FlappyBird"; + config.width = 1280; // 窗口宽度 (720P 分辨率) + config.height = 720; // 窗口高度 (720P 分辨率) + config.vsync = true; + config.fpsLimit = 60; - // 初始化应用 - if (!app.init(config)) { - E2D_LOG_ERROR("应用初始化失败!"); - return -1; - } + // 初始化应用 + if (!app.init(config)) { + E2D_LOG_ERROR("应用初始化失败!"); + return -1; + } - // 初始化资源加载器 - flappybird::ResLoader::init(); + // 初始化资源加载器 + flappybird::ResLoader::init(); - // 进入启动场景 - app.enterScene(makePtr()); + // 进入启动场景 + app.enterScene(makePtr()); - E2D_LOG_INFO("开始主循环..."); - app.run(); + E2D_LOG_INFO("开始主循环..."); + app.run(); - E2D_LOG_INFO("应用结束"); - return 0; + E2D_LOG_INFO("应用结束"); + return 0; } diff --git a/examples/flappy_bird/pipe.cpp b/examples/flappy_bird/pipe.cpp index 3f80045..1f7432a 100644 --- a/examples/flappy_bird/pipe.cpp +++ b/examples/flappy_bird/pipe.cpp @@ -4,6 +4,7 @@ #include "Pipe.h" #include "ResLoader.h" +#include "BaseScene.h" namespace flappybird { @@ -18,8 +19,8 @@ void Pipe::onEnter() { // 在 onEnter 中创建子节点,此时 weak_from_this() 可用 if (!topPipe_ && !bottomPipe_) { - auto& app = extra2d::Application::instance(); - float screenHeight = static_cast(app.getConfig().height); + // 使用游戏逻辑高度 + float screenHeight = GAME_HEIGHT; // 获取地面高度 auto landFrame = ResLoader::getKeyFrame("land"); @@ -62,8 +63,8 @@ extra2d::Rect Pipe::getBoundingBox() const { float pipeWidth = 52.0f; float halfWidth = pipeWidth / 2.0f; - auto& app = extra2d::Application::instance(); - float screenHeight = static_cast(app.getConfig().height); + // 使用游戏逻辑高度 + float screenHeight = GAME_HEIGHT; return extra2d::Rect( pos.x - halfWidth, diff --git a/examples/flappy_bird/pipes.cpp b/examples/flappy_bird/pipes.cpp index fed2ff4..7326175 100644 --- a/examples/flappy_bird/pipes.cpp +++ b/examples/flappy_bird/pipes.cpp @@ -3,6 +3,7 @@ // ============================================================================ #include "Pipes.h" +#include "BaseScene.h" namespace flappybird { @@ -76,9 +77,8 @@ void Pipes::addPipe() { // 设置水管位置 if (pipeCount_ == 0) { // 第一个水管在屏幕外 130 像素处 - auto& app = extra2d::Application::instance(); pipe->setPosition(extra2d::Vec2( - static_cast(app.getConfig().width) + 130.0f, + GAME_WIDTH + 130.0f, 0.0f )); } else {