diff --git a/Extra2D/include/extra2d/graphics/opengl/gl_renderer.h b/Extra2D/include/extra2d/graphics/opengl/gl_renderer.h index 7152d27..6775e9c 100644 --- a/Extra2D/include/extra2d/graphics/opengl/gl_renderer.h +++ b/Extra2D/include/extra2d/graphics/opengl/gl_renderer.h @@ -5,6 +5,7 @@ #include #include +#include namespace extra2d { @@ -30,6 +31,11 @@ public: void setBlendMode(BlendMode mode) override; void setViewProjection(const glm::mat4& matrix) override; + // 变换矩阵栈 + void pushTransform(const glm::mat4& transform) override; + void popTransform() override; + glm::mat4 getCurrentTransform() const override; + Ptr createTexture(int width, int height, const uint8_t* pixels, int channels) override; Ptr loadTexture(const std::string& filepath) override; @@ -65,6 +71,7 @@ private: GLuint shapeVbo_; glm::mat4 viewProjection_; + std::vector transformStack_; Stats stats_; bool vsync_; diff --git a/Extra2D/include/extra2d/graphics/render_backend.h b/Extra2D/include/extra2d/graphics/render_backend.h index fb75f2c..83dbe5d 100644 --- a/Extra2D/include/extra2d/graphics/render_backend.h +++ b/Extra2D/include/extra2d/graphics/render_backend.h @@ -61,6 +61,13 @@ public: virtual void setBlendMode(BlendMode mode) = 0; virtual void setViewProjection(const glm::mat4 &matrix) = 0; + // ------------------------------------------------------------------------ + // 变换矩阵栈 + // ------------------------------------------------------------------------ + virtual void pushTransform(const glm::mat4 &transform) = 0; + virtual void popTransform() = 0; + virtual glm::mat4 getCurrentTransform() const = 0; + // ------------------------------------------------------------------------ // 纹理 // ------------------------------------------------------------------------ diff --git a/Extra2D/include/extra2d/ui/button.h b/Extra2D/include/extra2d/ui/button.h index a9d804e..afea3c9 100644 --- a/Extra2D/include/extra2d/ui/button.h +++ b/Extra2D/include/extra2d/ui/button.h @@ -101,6 +101,7 @@ public: // ------------------------------------------------------------------------ void setBackgroundImage(Ptr normal, Ptr hover = nullptr, Ptr pressed = nullptr); + void setBackgroundImage(Ptr texture, const Rect &rect); void setBackgroundImageScaleMode(ImageScaleMode mode); void setCustomSize(const Vec2 &size); void setCustomSize(float width, float height); @@ -162,8 +163,12 @@ private: Ptr imgNormal_; Ptr imgHover_; Ptr imgPressed_; + Rect imgNormalRect_; + Rect imgHoverRect_; + Rect imgPressedRect_; ImageScaleMode scaleMode_ = ImageScaleMode::Original; bool useImageBackground_ = false; + bool useTextureRect_ = false; // 边框 Color borderColor_ = Color(0.6f, 0.6f, 0.6f, 1.0f); diff --git a/Extra2D/src/animation/ani_parser.cpp b/Extra2D/src/animation/ani_parser.cpp index 06878ee..29fd3b1 100644 --- a/Extra2D/src/animation/ani_parser.cpp +++ b/Extra2D/src/animation/ani_parser.cpp @@ -1,5 +1,3 @@ -#include -#include #include #include #include diff --git a/Extra2D/src/graphics/opengl/gl_renderer.cpp b/Extra2D/src/graphics/opengl/gl_renderer.cpp index d9555e8..e457323 100644 --- a/Extra2D/src/graphics/opengl/gl_renderer.cpp +++ b/Extra2D/src/graphics/opengl/gl_renderer.cpp @@ -135,6 +135,27 @@ void GLRenderer::setViewProjection(const glm::mat4 &matrix) { viewProjection_ = matrix; } +void GLRenderer::pushTransform(const glm::mat4 &transform) { + if (transformStack_.empty()) { + transformStack_.push_back(transform); + } else { + transformStack_.push_back(transformStack_.back() * transform); + } +} + +void GLRenderer::popTransform() { + if (!transformStack_.empty()) { + transformStack_.pop_back(); + } +} + +glm::mat4 GLRenderer::getCurrentTransform() const { + if (transformStack_.empty()) { + return glm::mat4(1.0f); + } + return transformStack_.back(); +} + Ptr GLRenderer::createTexture(int width, int height, const uint8_t *pixels, int channels) { return makePtr(width, height, pixels, channels); @@ -160,8 +181,8 @@ void GLRenderer::drawSprite(const Texture &texture, const Rect &destRect, // 纹理坐标计算 float u1 = srcRect.origin.x / texW; float u2 = (srcRect.origin.x + srcRect.size.width) / texW; - float v1 = 1.0f - (srcRect.origin.y / texH); - float v2 = 1.0f - ((srcRect.origin.y + srcRect.size.height) / texH); + float v1 = srcRect.origin.y / texH; + float v2 = (srcRect.origin.y + srcRect.size.height) / texH; data.texCoordMin = glm::vec2(glm::min(u1, u2), glm::min(v1, v2)); data.texCoordMax = glm::vec2(glm::max(u1, u2), glm::max(v1, v2)); diff --git a/Extra2D/src/scene/node.cpp b/Extra2D/src/scene/node.cpp index c0f843c..c566eff 100644 --- a/Extra2D/src/scene/node.cpp +++ b/Extra2D/src/scene/node.cpp @@ -243,9 +243,8 @@ glm::mat4 Node::getLocalTransform() const { localTransform_ = glm::scale(localTransform_, glm::vec3(scale_.x, scale_.y, 1.0f)); - // Apply anchor point offset - localTransform_ = glm::translate(localTransform_, - glm::vec3(-anchor_.x, -anchor_.y, 0.0f)); + // 注意:锚点偏移在渲染时处理,不在本地变换中处理 + // 这样可以避免锚点偏移被父节点的缩放影响 transformDirty_ = false; } diff --git a/Extra2D/src/scene/scene.cpp b/Extra2D/src/scene/scene.cpp index f47a5ba..42bd6ef 100644 --- a/Extra2D/src/scene/scene.cpp +++ b/Extra2D/src/scene/scene.cpp @@ -37,6 +37,9 @@ void Scene::renderContent(RenderBackend &renderer) { if (!isVisible()) return; + // 在渲染前批量更新所有节点的世界变换 + batchUpdateTransforms(); + Camera *activeCam = getActiveCamera(); if (activeCam) { renderer.setViewProjection(activeCam->getViewProjectionMatrix()); diff --git a/Extra2D/src/scene/sprite.cpp b/Extra2D/src/scene/sprite.cpp index 1d3e239..185f483 100644 --- a/Extra2D/src/scene/sprite.cpp +++ b/Extra2D/src/scene/sprite.cpp @@ -76,11 +76,23 @@ void Sprite::onDraw(RenderBackend &renderer) { float width = textureRect_.width(); float height = textureRect_.height(); - auto pos = getPosition(); + // 使用世界变换来获取最终的位置 + auto worldTransform = getWorldTransform(); + + // 从世界变换矩阵中提取位置(第四列) + float worldX = worldTransform[3][0]; + float worldY = worldTransform[3][1]; + + // 从世界变换矩阵中提取缩放 + float worldScaleX = + glm::length(glm::vec2(worldTransform[0][0], worldTransform[0][1])); + float worldScaleY = + glm::length(glm::vec2(worldTransform[1][0], worldTransform[1][1])); + auto anchor = getAnchor(); - auto scale = getScale(); + // 锚点由 RenderBackend 在绘制时处理,这里只传递位置和尺寸 - Rect destRect(pos.x, pos.y, width * scale.x, height * scale.y); + Rect destRect(worldX, worldY, width * worldScaleX, height * worldScaleY); // Adjust source rect for flipping Rect srcRect = textureRect_; @@ -93,8 +105,11 @@ void Sprite::onDraw(RenderBackend &renderer) { srcRect.size.height = -srcRect.size.height; } - renderer.drawSprite(*texture_, destRect, srcRect, color_, getRotation(), - getAnchor()); + // 从世界变换矩阵中提取旋转角度 + float worldRotation = std::atan2(worldTransform[0][1], worldTransform[0][0]); + + renderer.drawSprite(*texture_, destRect, srcRect, color_, worldRotation, + anchor); } void Sprite::generateRenderCommand(std::vector &commands, @@ -103,15 +118,27 @@ void Sprite::generateRenderCommand(std::vector &commands, return; } - // 计算目标矩形(与 onDraw 一致) + // 计算目标矩形(与 onDraw 一致,使用世界变换) float width = textureRect_.width(); float height = textureRect_.height(); - auto pos = getPosition(); + // 使用世界变换来获取最终的位置 + auto worldTransform = getWorldTransform(); + + // 从世界变换矩阵中提取位置(第四列) + float worldX = worldTransform[3][0]; + float worldY = worldTransform[3][1]; + + // 从世界变换矩阵中提取缩放 + float worldScaleX = + glm::length(glm::vec2(worldTransform[0][0], worldTransform[0][1])); + float worldScaleY = + glm::length(glm::vec2(worldTransform[1][0], worldTransform[1][1])); + auto anchor = getAnchor(); - auto scale = getScale(); + // 锚点由 RenderBackend 在绘制时处理,这里只传递位置和尺寸 - Rect destRect(pos.x, pos.y, width * scale.x, height * scale.y); + Rect destRect(worldX, worldY, width * worldScaleX, height * worldScaleY); // 调整源矩形(翻转) Rect srcRect = textureRect_; @@ -124,12 +151,15 @@ void Sprite::generateRenderCommand(std::vector &commands, srcRect.size.height = -srcRect.size.height; } + // 从世界变换矩阵中提取旋转角度 + float worldRotation = std::atan2(worldTransform[0][1], worldTransform[0][0]); + // 创建渲染命令 RenderCommand cmd; cmd.type = RenderCommandType::Sprite; cmd.layer = zOrder; - cmd.data = - SpriteCommandData{texture_.get(), destRect, srcRect, color_, getRotation(), anchor, 0}; + cmd.data = SpriteCommandData{texture_.get(), destRect, srcRect, color_, + worldRotation, anchor, 0}; commands.push_back(std::move(cmd)); } diff --git a/Extra2D/src/ui/button.cpp b/Extra2D/src/ui/button.cpp index 0324d4c..a135930 100644 --- a/Extra2D/src/ui/button.cpp +++ b/Extra2D/src/ui/button.cpp @@ -239,6 +239,7 @@ void Button::setBackgroundImage(Ptr normal, Ptr hover, imgHover_ = hover ? hover : normal; imgPressed_ = pressed ? pressed : (hover ? hover : normal); useImageBackground_ = (normal != nullptr); + useTextureRect_ = false; if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original && normal) { setSize(static_cast(normal->getWidth()), @@ -246,6 +247,21 @@ void Button::setBackgroundImage(Ptr normal, Ptr hover, } } +void Button::setBackgroundImage(Ptr texture, const Rect &rect) { + imgNormal_ = texture; + imgHover_ = texture; + imgPressed_ = texture; + imgNormalRect_ = rect; + imgHoverRect_ = rect; + imgPressedRect_ = rect; + useImageBackground_ = (texture != nullptr); + useTextureRect_ = true; + + if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original) { + setSize(rect.size.width, rect.size.height); + } +} + void Button::setBackgroundImageScaleMode(ImageScaleMode mode) { scaleMode_ = mode; if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original && @@ -308,19 +324,23 @@ Vec2 Button::calculateImageSize(const Vec2 &buttonSize, const Vec2 &imageSize) { void Button::drawBackgroundImage(RenderBackend &renderer, const Rect &rect) { Texture *texture = nullptr; + Rect srcRect; + if (pressed_ && imgPressed_) { texture = imgPressed_.get(); + srcRect = useTextureRect_ ? imgPressedRect_ : Rect(0, 0, static_cast(imgPressed_->getWidth()), static_cast(imgPressed_->getHeight())); } else if (hovered_ && imgHover_) { texture = imgHover_.get(); + srcRect = useTextureRect_ ? imgHoverRect_ : Rect(0, 0, static_cast(imgHover_->getWidth()), static_cast(imgHover_->getHeight())); } else if (imgNormal_) { texture = imgNormal_.get(); + srcRect = useTextureRect_ ? imgNormalRect_ : Rect(0, 0, static_cast(imgNormal_->getWidth()), static_cast(imgNormal_->getHeight())); } if (!texture) return; - Vec2 imageSize(static_cast(texture->getWidth()), - static_cast(texture->getHeight())); + Vec2 imageSize(srcRect.size.width, srcRect.size.height); Vec2 buttonSize(rect.size.width, rect.size.height); Vec2 drawSize = calculateImageSize(buttonSize, imageSize); @@ -329,7 +349,7 @@ void Button::drawBackgroundImage(RenderBackend &renderer, const Rect &rect) { Rect destRect(drawPos.x, drawPos.y, drawSize.x, drawSize.y); - renderer.drawSprite(*texture, destRect, Rect(0, 0, imageSize.x, imageSize.y), + renderer.drawSprite(*texture, destRect, srcRect, Colors::White, 0.0f, Vec2::Zero()); } @@ -459,6 +479,7 @@ void Button::onDrawWidget(RenderBackend &renderer) { } if (useImageBackground_) { + // 使用图片背景时不绘制纯色背景和边框 drawBackgroundImage(renderer, rect); } else { renderer.endSpriteBatch(); @@ -477,19 +498,20 @@ void Button::onDrawWidget(RenderBackend &renderer) { } renderer.beginSpriteBatch(); - } - renderer.endSpriteBatch(); + // 纯色背景模式下才绘制边框 + renderer.endSpriteBatch(); - if (borderWidth_ > 0.0f) { - if (roundedCornersEnabled_) { - drawRoundedRect(renderer, rect, borderColor_, cornerRadius_); - } else { - renderer.drawRect(rect, borderColor_, borderWidth_); + if (borderWidth_ > 0.0f) { + if (roundedCornersEnabled_) { + drawRoundedRect(renderer, rect, borderColor_, cornerRadius_); + } else { + renderer.drawRect(rect, borderColor_, borderWidth_); + } } - } - renderer.beginSpriteBatch(); + renderer.beginSpriteBatch(); + } if (font_ && !text_.empty()) { Vec2 textSize = font_->measureText(text_); diff --git a/Extra2D/src/ui/widget.cpp b/Extra2D/src/ui/widget.cpp index 15384b7..83b117b 100644 --- a/Extra2D/src/ui/widget.cpp +++ b/Extra2D/src/ui/widget.cpp @@ -93,7 +93,7 @@ Vec2 Widget::getRenderPosition() const { case CoordinateSpace::World: default: // 世界空间:使用节点的世界位置 - return getPosition(); + return convertToWorldSpace(extra2d::Vec2::Zero()); } } diff --git a/examples/flappy_bird/GameOverLayer.cpp b/examples/flappy_bird/GameOverLayer.cpp new file mode 100644 index 0000000..13eb3fe --- /dev/null +++ b/examples/flappy_bird/GameOverLayer.cpp @@ -0,0 +1,197 @@ +// ============================================================================ +// GameOverLayer.cpp - 游戏结束层实现 +// ============================================================================ + +#include "GameOverLayer.h" +#include "GameScene.h" +#include "Number.h" +#include "ResLoader.h" +#include "StartScene.h" + +namespace flappybird { + +GameOverLayer::GameOverLayer(int score) : score_(score) { + // 注意:不要在构造函数中创建子节点 + // 因为此时 weak_from_this() 还不能使用 +} + +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); + + // 整体居中(x 坐标相对于屏幕中心) + setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight)); + + // 显示 "Game Over" 文字(y=120,从顶部开始) + auto gameOverFrame = ResLoader::getKeyFrame("text_game_over"); + if (gameOverFrame) { + auto gameOver = extra2d::Sprite::create(gameOverFrame->getTexture(), + gameOverFrame->getRect()); + gameOver->setAnchor(extra2d::Vec2(0.5f, 0.0f)); + gameOver->setPosition(extra2d::Vec2(0.0f, 120.0f)); // x=0 表示相对于中心点 + addChild(gameOver); + } + + // 初始化得分面板 + initPanel(score_, screenHeight); + + // 初始化按钮 + initButtons(); + + // 创建向上移动的动画(从屏幕底部移动到正常位置) + auto moveAction = extra2d::makePtr( + 1.0f, extra2d::Vec2(0.0f, -screenHeight)); + runAction(moveAction); +} + +void GameOverLayer::initPanel(int score, float screenHeight) { + // 显示得分板(在屏幕中间) + auto panelFrame = ResLoader::getKeyFrame("score_panel"); + if (!panelFrame) + return; + + auto panel = + extra2d::Sprite::create(panelFrame->getTexture(), panelFrame->getRect()); + panel->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + panel->setPosition( + extra2d::Vec2(0.0f, screenHeight / 2.0f)); // x=0 表示相对于中心点 + addChild(panel); + + // 获取最高分(从存储中读取) + static int bestScore = 0; + if (score > bestScore) { + bestScore = score; + } + + // 显示 "New" 标记(如果破了记录) + if (score >= bestScore && score > 0) { + auto newFrame = ResLoader::getKeyFrame("new"); + if (newFrame) { + auto newSprite = + extra2d::Sprite::create(newFrame->getTexture(), newFrame->getRect()); + newSprite->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + // 调整位置使其在面板内部,靠近 BEST 分数 + newSprite->setPosition( + extra2d::Vec2(30.0f, 25.0f)); // 相对于面板的坐标,在 BEST 右侧 + panel->addChild(newSprite); + } + } + + // 显示奖牌 + auto medalFrame = getMedal(score); + if (medalFrame) { + auto medal = extra2d::Sprite::create(medalFrame->getTexture(), + medalFrame->getRect()); + medal->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + medal->setPosition(extra2d::Vec2(54.0f, 68.0f)); // 相对于面板的坐标 + panel->addChild(medal); + } + + // 显示本局得分 + auto scoreNumber = extra2d::makePtr(); + scoreNumber->setLittleNumber(score); + scoreNumber->setPosition( + extra2d::Vec2(80.0f, -15.0f)); // 相对于面板的坐标,右侧对齐 + panel->addChild(scoreNumber); + + // 显示最高分 + auto bestNumber = extra2d::makePtr(); + bestNumber->setLittleNumber(bestScore); + bestNumber->setPosition( + extra2d::Vec2(80.0f, 25.0f)); // 相对于面板的坐标,右侧对齐 + panel->addChild(bestNumber); +} + +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([]() { + ResLoader::playMusic(MusicType::Click); + auto &app = extra2d::Application::instance(); + app.scenes().replaceScene(extra2d::makePtr(), + extra2d::TransitionType::Fade, 0.5f); + }); + 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([]() { + ResLoader::playMusic(MusicType::Click); + auto &app = extra2d::Application::instance(); + app.scenes().replaceScene(extra2d::makePtr(), + extra2d::TransitionType::Fade, 0.5f); + }); + 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); + } +} + +void GameOverLayer::onUpdate(float dt) { + Node::onUpdate(dt); + + // 检测手柄按键 + auto &input = extra2d::Application::instance().input(); + + // A 键重新开始游戏 + if (input.isButtonPressed(extra2d::GamepadButton::A)) { + ResLoader::playMusic(MusicType::Click); + auto &app = extra2d::Application::instance(); + app.scenes().replaceScene(extra2d::makePtr(), + extra2d::TransitionType::Fade, 0.5f); + } + + // B 键返回主菜单 + if (input.isButtonPressed(extra2d::GamepadButton::B)) { + ResLoader::playMusic(MusicType::Click); + auto &app = extra2d::Application::instance(); + app.scenes().replaceScene(extra2d::makePtr(), + extra2d::TransitionType::Fade, 0.5f); + } +} + +extra2d::Ptr GameOverLayer::getMedal(int score) { + if (score < 10) { + return nullptr; // 无奖牌 + } else if (score < 20) { + return ResLoader::getKeyFrame("medals_0"); // 铜牌 + } else if (score < 30) { + return ResLoader::getKeyFrame("medals_1"); // 银牌 + } else if (score < 50) { + return ResLoader::getKeyFrame("medals_2"); // 金牌 + } else { + return ResLoader::getKeyFrame("medals_3"); // 钻石奖牌 + } +} + +} // namespace flappybird diff --git a/examples/flappy_bird/GameOverLayer.h b/examples/flappy_bird/GameOverLayer.h new file mode 100644 index 0000000..f2806ee --- /dev/null +++ b/examples/flappy_bird/GameOverLayer.h @@ -0,0 +1,58 @@ +// ============================================================================ +// GameOverLayer.h - 游戏结束层 +// 描述: 显示游戏结束界面、得分和奖牌 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +/** + * @brief 游戏结束层类 + * 显示游戏结束后的得分面板和按钮 + */ +class GameOverLayer : public extra2d::Node { +public: + /** + * @brief 构造函数 + * @param score 本局得分 + */ + GameOverLayer(int score); + + /** + * @brief 进入场景时调用 + */ + void onEnter() override; + + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔 + */ + void onUpdate(float dt) override; + +private: + /** + * @brief 初始化得分面板 + * @param score 本局得分 + * @param screenHeight 屏幕高度 + */ + void initPanel(int score, float screenHeight); + + /** + * @brief 初始化按钮 + */ + void initButtons(); + + /** + * @brief 根据得分获取奖牌 + * @param score 得分 + * @return 奖牌精灵帧 + */ + extra2d::Ptr getMedal(int score); + + int score_ = 0; // 本局得分 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/GameScene.cpp b/examples/flappy_bird/GameScene.cpp new file mode 100644 index 0000000..235ee0f --- /dev/null +++ b/examples/flappy_bird/GameScene.cpp @@ -0,0 +1,251 @@ +// ============================================================================ +// GameScene.cpp - 游戏主场景实现 +// ============================================================================ + +#include "GameScene.h" +#include "GameOverLayer.h" +#include "ResLoader.h" +#include "input.h" + +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)); +} + +void GameScene::onEnter() { + extra2d::Scene::onEnter(); + + auto &app = extra2d::Application::instance(); + float screenWidth = static_cast(app.getConfig().width); + float screenHeight = static_cast(app.getConfig().height); + + // 添加背景(使用左上角锚点,与原游戏一致) + auto bgFrame = ResLoader::getKeyFrame("bg_day"); + if (bgFrame) { + auto background = + extra2d::Sprite::create(bgFrame->getTexture(), bgFrame->getRect()); + background->setAnchor(extra2d::Vec2(0.0f, 0.0f)); + background->setPosition(extra2d::Vec2(0.0f, 0.0f)); + addChild(background); + } + + // 添加水管(初始时隐藏,游戏开始后才显示) + auto pipes = extra2d::makePtr(); + pipes_ = pipes.get(); + pipes->setVisible(false); + addChild(pipes); + + // 添加小鸟(在屏幕中间偏左位置) + auto bird = extra2d::makePtr(); + bird->setPosition( + extra2d::Vec2(screenWidth / 2.0f - 50.0f, screenHeight / 2.0f)); + bird_ = bird.get(); + addChild(bird); + + // 添加地面 + auto ground = extra2d::makePtr(); + ground_ = ground.get(); + addChild(ground); + + // 添加得分(屏幕顶部中央) + auto scoreNumber = extra2d::makePtr(); + scoreNumber->setPosition(extra2d::Vec2(screenWidth / 2.0f, 50.0f)); + scoreNumber->setNumber(0); + scoreNumber_ = scoreNumber.get(); + addChild(scoreNumber); + + // 添加 ready 图片(屏幕中央偏上) + auto readyFrame = ResLoader::getKeyFrame("text_ready"); + if (readyFrame) { + readySprite_ = extra2d::Sprite::create(readyFrame->getTexture(), + readyFrame->getRect()); + readySprite_->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + readySprite_->setPosition( + extra2d::Vec2(screenWidth / 2.0f, screenHeight / 2.0f - 70.0f)); + addChild(readySprite_); + } + + // 添加教程图片(屏幕中央偏下) + auto tutorialFrame = ResLoader::getKeyFrame("tutorial"); + if (tutorialFrame) { + tutorialSprite_ = extra2d::Sprite::create(tutorialFrame->getTexture(), + tutorialFrame->getRect()); + tutorialSprite_->setAnchor(extra2d::Vec2(0.5f, 0.5f)); + tutorialSprite_->setPosition( + extra2d::Vec2(screenWidth / 2.0f, screenHeight / 2.0f + 30.0f)); + addChild(tutorialSprite_); + } + + // 播放转场音效 + ResLoader::playMusic(MusicType::Swoosh); + + // 初始化状态 + started_ = false; + score_ = 0; +} + +void GameScene::onUpdate(float dt) { + // 注意:这里要先调用父类的 onUpdate,以确保 GameOverLayer 的动画能播放 + extra2d::Scene::onUpdate(dt); + + // 游戏结束后不再更新游戏逻辑(但子节点的动画继续) + if (gameOver_) + return; + + 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(); + } + bird_->jump(); + } + + // 游戏已经开始 + 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 (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(); + } + } +} + +void GameScene::startGame() { + // 隐藏 ready 和 tutorial 图片 + if (readySprite_) { + readySprite_->setVisible(false); + } + if (tutorialSprite_) { + tutorialSprite_->setVisible(false); + } + + // 显示并开始移动水管 + if (pipes_) { + pipes_->setVisible(true); + pipes_->start(); + } + + // 设置小鸟状态 + if (bird_) { + bird_->setStatus(Bird::Status::StartToFly); + } +} + +bool GameScene::checkCollision() { + if (!bird_ || !pipes_) + return false; + + extra2d::Rect birdBox = bird_->getBoundingBox(); + + // 检查与每个水管的碰撞 + for (int i = 0; i < 3; ++i) { + Pipe *pipe = pipes_->getPipe(i); + if (!pipe) + continue; + + // 检查与上水管的碰撞 + extra2d::Rect topBox = pipe->getTopPipeBox(); + if (birdBox.intersects(topBox)) { + return true; + } + + // 检查与下水管的碰撞 + extra2d::Rect bottomBox = pipe->getBottomPipeBox(); + if (birdBox.intersects(bottomBox)) { + return true; + } + } + + return false; +} + +void GameScene::onHit() { + if (!bird_->isLiving()) + return; + + // 小鸟死亡 + bird_->die(); + + // 停止地面滚动 + if (ground_) { + ground_->stop(); + } + + // 停止水管移动 + if (pipes_) { + pipes_->stop(); + } + + // 停止小鸟动画 + if (bird_) { + bird_->setStatus(Bird::Status::Still); + } + + // 隐藏得分 + if (scoreNumber_) { + scoreNumber_->setVisible(false); + } + + // 设置游戏结束标志 + gameOver_ = true; + + // 延迟显示游戏结束界面 + gameOver(); +} + +void GameScene::gameOver() { + started_ = false; + gameOver_ = true; + + // 显示游戏结束层 + auto gameOverLayer = extra2d::makePtr(score_); + addChild(gameOverLayer); +} + +} // namespace flappybird diff --git a/examples/flappy_bird/GameScene.h b/examples/flappy_bird/GameScene.h new file mode 100644 index 0000000..01b1e80 --- /dev/null +++ b/examples/flappy_bird/GameScene.h @@ -0,0 +1,72 @@ +// ============================================================================ +// GameScene.h - 游戏主场景 +// 描述: 游戏的核心场景,包含小鸟、水管、地面和得分系统 +// ============================================================================ + +#pragma once + +#include +#include "Bird.h" +#include "Pipes.h" +#include "Ground.h" +#include "Number.h" + +namespace flappybird { + +/** + * @brief 游戏主场景类 + * 游戏的核心场景,处理游戏逻辑、碰撞检测和得分 + */ +class GameScene : public extra2d::Scene { +public: + /** + * @brief 构造函数 + */ + GameScene(); + + /** + * @brief 场景进入时调用 + */ + void onEnter() override; + + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override; + +private: + /** + * @brief 开始游戏 + */ + void startGame(); + + /** + * @brief 处理碰撞事件 + */ + void onHit(); + + /** + * @brief 游戏结束 + */ + void gameOver(); + + /** + * @brief 检查小鸟与水管的碰撞 + * @return 是否发生碰撞 + */ + bool checkCollision(); + + Bird* bird_ = nullptr; // 小鸟 + Pipes* pipes_ = nullptr; // 水管管理器 + Ground* ground_ = nullptr; // 地面 + Number* scoreNumber_ = nullptr; // 得分显示 + extra2d::Ptr readySprite_; // "Get Ready" 提示 + extra2d::Ptr tutorialSprite_; // 操作教程提示 + + bool started_ = false; // 游戏是否已开始 + bool gameOver_ = false; // 游戏是否已结束 + int score_ = 0; // 当前得分 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/Number.cpp b/examples/flappy_bird/Number.cpp new file mode 100644 index 0000000..39f09e1 --- /dev/null +++ b/examples/flappy_bird/Number.cpp @@ -0,0 +1,66 @@ +// ============================================================================ +// Number.cpp - 数字显示类实现 +// ============================================================================ + +#include "Number.h" +#include "ResLoader.h" + +namespace flappybird { + +Number::Number() : number_(0) { +} + +void Number::setNumber(int number) { + number_ = number; + createNumberSprites(number, "number_big_"); +} + +void Number::setLittleNumber(int number) { + number_ = number; + createNumberSprites(number, "number_medium_"); +} + +void Number::createNumberSprites(int number, const std::string& prefix) { + // 清除之前的数字精灵 + removeAllChildren(); + + // 获取数字 0 的高度作为参考 + auto zeroFrame = ResLoader::getKeyFrame(prefix + "0"); + float digitHeight = zeroFrame ? zeroFrame->getRect().size.height : 36.0f; + + // 收集所有数字位 + std::vector digits; + if (number == 0) { + digits.push_back(0); + } else { + while (number > 0) { + digits.push_back(number % 10); + number /= 10; + } + } + + // 计算总宽度 + float totalWidth = 0.0f; + std::vector digitWidths; + for (int digit : digits) { + auto frame = ResLoader::getKeyFrame(prefix + std::to_string(digit)); + float width = frame ? frame->getRect().size.width : 24.0f; + digitWidths.push_back(width); + totalWidth += width; + } + + // 创建数字精灵并居中排列 + float currentX = -totalWidth / 2.0f; + for (size_t i = 0; i < digits.size(); ++i) { + auto frame = ResLoader::getKeyFrame(prefix + std::to_string(digits[i])); + if (frame) { + auto digitSprite = extra2d::Sprite::create(frame->getTexture(), frame->getRect()); + digitSprite->setAnchor(extra2d::Vec2(0.0f, 0.0f)); + digitSprite->setPosition(extra2d::Vec2(currentX, -digitHeight / 2.0f)); + addChild(digitSprite); + } + currentX += digitWidths[i]; + } +} + +} // namespace flappybird diff --git a/examples/flappy_bird/Number.h b/examples/flappy_bird/Number.h new file mode 100644 index 0000000..db75767 --- /dev/null +++ b/examples/flappy_bird/Number.h @@ -0,0 +1,52 @@ +// ============================================================================ +// Number.h - 数字显示类 +// 描述: 将整数数字转换为精灵图片显示 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +/** + * @brief 数字显示类 + * 用于显示得分,将整数转换为对应的数字图片 + */ +class Number : public extra2d::Node { +public: + /** + * @brief 构造函数 + */ + Number(); + + /** + * @brief 设置显示的数字(大号) + * @param number 要显示的数字 + */ + void setNumber(int number); + + /** + * @brief 设置显示的数字(小号) + * @param number 要显示的数字 + */ + void setLittleNumber(int number); + + /** + * @brief 获取当前数字 + * @return 当前数字 + */ + int getNumber() const { return number_; } + +private: + /** + * @brief 创建数字精灵 + * @param number 数字值 + * @param prefix 数字图片前缀("number_big_" 或 "number_medium_") + */ + void createNumberSprites(int number, const std::string& prefix); + + int number_ = 0; // 当前数字 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/ResLoader.cpp b/examples/flappy_bird/ResLoader.cpp new file mode 100644 index 0000000..8a01ee8 --- /dev/null +++ b/examples/flappy_bird/ResLoader.cpp @@ -0,0 +1,96 @@ +// ============================================================================ +// ResLoader.cpp - 资源加载器实现 +// ============================================================================ + +#include "ResLoader.h" +#include +#include + +namespace flappybird { + +extra2d::Ptr ResLoader::atlasTexture_; +std::map ResLoader::imageMap_; +std::map> ResLoader::soundMap_; + +void ResLoader::init() { + auto& resources = extra2d::Application::instance().resources(); + + // 加载图集纹理 + atlasTexture_ = resources.loadTexture("assets/images/atlas.png"); + if (!atlasTexture_) { + E2D_LOG_ERROR("无法加载图集纹理 atlas.png"); + return; + } + + // 读取 atlas.json 文件 + std::ifstream file("assets/images/atlas.json"); + if (!file.is_open()) { + E2D_LOG_ERROR("无法打开 atlas.json 文件"); + 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"); + + 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; + } + + 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(); + } +} + +} // namespace flappybird diff --git a/examples/flappy_bird/ResLoader.h b/examples/flappy_bird/ResLoader.h new file mode 100644 index 0000000..c22301e --- /dev/null +++ b/examples/flappy_bird/ResLoader.h @@ -0,0 +1,63 @@ +// ============================================================================ +// ResLoader.h - 资源加载器 +// 描述: 管理游戏资源的加载和访问 +// ============================================================================ + +#pragma once + +#include +#include +#include + +namespace flappybird { + +/** + * @brief 音频类型枚举 + */ +enum class MusicType { + Click, // 按键声音 + Hit, // 小鸟死亡声音 + Fly, // 小鸟飞翔声音 + Point, // 得分声音 + Swoosh // 转场声音 +}; + +/** + * @brief 资源加载器类 + * 管理纹理图集、精灵帧和音频资源的加载 + */ +class ResLoader { +public: + /** + * @brief 初始化资源加载器 + */ + static void init(); + + /** + * @brief 获取精灵帧 + * @param name 帧名称 + * @return 精灵帧指针 + */ + static extra2d::Ptr getKeyFrame(const std::string& name); + + /** + * @brief 播放音效 + * @param type 音效类型 + */ + static void playMusic(MusicType type); + +private: + /** + * @brief 图片信息结构 + * 对应 atlas.txt 格式: 元素名 width height x y + */ + struct ImageInfo { + float width, height, x, y; + }; + + static extra2d::Ptr atlasTexture_; // 图集纹理 + static std::map imageMap_; // 图片信息映射 + static std::map> soundMap_; // 音效映射 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/SplashScene.cpp b/examples/flappy_bird/SplashScene.cpp new file mode 100644 index 0000000..2874cd5 --- /dev/null +++ b/examples/flappy_bird/SplashScene.cpp @@ -0,0 +1,69 @@ +// ============================================================================ +// SplashScene.cpp - 启动场景实现 +// ============================================================================ + +#include "SplashScene.h" +#include "StartScene.h" +#include "ResLoader.h" + +namespace flappybird { + +SplashScene::SplashScene() { + // 设置视口大小 + auto& app = extra2d::Application::instance(); + auto& config = app.getConfig(); + setViewportSize(static_cast(config.width), static_cast(config.height)); +} + +void SplashScene::onEnter() { + extra2d::Scene::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); +} + +void SplashScene::onUpdate(float dt) { + extra2d::Scene::onUpdate(dt); + + // 计时 + timer_ += dt; + if (timer_ >= delay_) { + gotoStartScene(); + } +} + +void SplashScene::gotoStartScene() { + 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 new file mode 100644 index 0000000..cf25dcd --- /dev/null +++ b/examples/flappy_bird/SplashScene.h @@ -0,0 +1,44 @@ +// ============================================================================ +// SplashScene.h - 启动场景 +// 描述: 显示游戏 Logo,2秒后自动跳转到开始场景 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +/** + * @brief 启动场景类 + * 显示游戏 Logo,短暂延迟后进入主菜单 + */ +class SplashScene : public extra2d::Scene { +public: + /** + * @brief 构造函数 + */ + SplashScene(); + + /** + * @brief 场景进入时调用 + */ + void onEnter() override; + + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override; + +private: + /** + * @brief 跳转到开始场景 + */ + void gotoStartScene(); + + 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 new file mode 100644 index 0000000..19fd9e5 --- /dev/null +++ b/examples/flappy_bird/StartScene.cpp @@ -0,0 +1,151 @@ +// ============================================================================ +// StartScene.cpp - 开始菜单场景实现 +// ============================================================================ + +#include "StartScene.h" +#include "Bird.h" +#include "GameScene.h" +#include "Ground.h" +#include "ResLoader.h" +#include "extra2d/event/input_codes.h" + +namespace flappybird { + +StartScene::StartScene() { + auto &app = extra2d::Application::instance(); + auto &config = app.getConfig(); + setViewportSize(static_cast(config.width), + static_cast(config.height)); +} + +void StartScene::onEnter() { + extra2d::Scene::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); + + // 添加背景(使用左上角锚点) + auto bgFrame = ResLoader::getKeyFrame("bg_day"); + if (bgFrame) { + auto background = + extra2d::Sprite::create(bgFrame->getTexture(), bgFrame->getRect()); + background->setAnchor(0.0f, 0.0f); + background->setPosition(0.0f, 0.0f); + addChild(background); + E2D_LOG_INFO("背景已添加: size={} x {}", bgFrame->getRect().size.width, + bgFrame->getRect().size.height); + } else { + E2D_LOG_ERROR("无法加载背景图片"); + } + + // 添加地面 + auto ground = extra2d::makePtr(); + addChild(ground); + + // 添加标题图片(在上方) + auto titleFrame = ResLoader::getKeyFrame("title"); + if (titleFrame) { + auto title = extra2d::Sprite::create(titleFrame->getTexture(), + titleFrame->getRect()); + title->setAnchor(0.5f, 0.5f); + // 标题在屏幕上方 + title->setPosition(screenWidth / 2.0f, 150.0f); + addChild(title); + E2D_LOG_INFO("标题已添加: size={} x {}", titleFrame->getRect().size.width, + titleFrame->getRect().size.height); + } else { + E2D_LOG_ERROR("无法加载标题图片"); + } + + // 添加小鸟(在标题下方) + auto bird = extra2d::makePtr(); + bird->setAnchor(0.5f, 0.5f); + bird->setPosition(screenWidth / 2.0f, screenHeight / 2.0f); + bird->setStatus(Bird::Status::Idle); + addChild(bird); + + // 添加开始按钮 - 在小鸟下方 + auto playFrame = ResLoader::getKeyFrame("button_play"); + if (playFrame) { + float btnWidth = playFrame->getRect().size.width; + float btnHeight = playFrame->getRect().size.height; + + playBtn_ = extra2d::Button::create(); + playBtn_->setBackgroundImage(playFrame->getTexture(), playFrame->getRect()); + // 使用世界坐标,中心锚点 + playBtn_->setAnchor(0.5f, 0.5f); + // PLAY 按钮在小鸟下方 + playBtn_->setPosition(screenWidth / 2.0f, + screenHeight - playBtn_->getSize().height - 100.0f); + playBtn_->setOnClick([this]() { + ResLoader::playMusic(MusicType::Click); + startGame(); + }); + addChild(playBtn_); + } + + // 添加分享按钮 - 在 PLAY 按钮下方,靠近地面 + auto shareFrame = ResLoader::getKeyFrame("button_share"); + if (shareFrame) { + float btnWidth = shareFrame->getRect().size.width; + float btnHeight = shareFrame->getRect().size.height; + + shareBtn_ = extra2d::Button::create(); + shareBtn_->setBackgroundImage(shareFrame->getTexture(), + shareFrame->getRect()); + // 使用世界坐标,中心锚点 + shareBtn_->setAnchor(0.5f, 0.5f); + // SHARE 按钮在 PLAY 按钮下方,靠近地面 + shareBtn_->setPosition(screenWidth / 2.0f, + screenHeight - shareBtn_->getSize().height - 80.0f); + shareBtn_->setOnClick([this]() { + ResLoader::playMusic(MusicType::Click); + // 分享功能暂不实现 + }); + addChild(shareBtn_); + } + + // 添加 copyright 图片(在底部) + auto copyrightFrame = ResLoader::getKeyFrame("brand_copyright"); + if (copyrightFrame) { + auto copyright = extra2d::Sprite::create(copyrightFrame->getTexture(), + copyrightFrame->getRect()); + copyright->setAnchor(0.5f, 0.5f); + // Copyright 在屏幕底部 + copyright->setPosition(screenWidth / 2.0f, screenHeight - 20.0f); + addChild(copyright); + } + + // 播放转场音效 + ResLoader::playMusic(MusicType::Swoosh); +} + +void StartScene::onUpdate(float dt) { + extra2d::Scene::onUpdate(dt); + + // 检测 A 键或空格开始游戏 + auto &input = extra2d::Application::instance().input(); + if (input.isButtonPressed(extra2d::GamepadButton::A)) { + ResLoader::playMusic(MusicType::Click); + startGame(); + } + + // 检测 BACK 键退出游戏 + if (input.isButtonPressed(extra2d::GamepadButton::Start)) { + ResLoader::playMusic(MusicType::Click); + auto &app = extra2d::Application::instance(); + app.quit(); + } +} + +void StartScene::startGame() { + auto &app = extra2d::Application::instance(); + app.scenes().replaceScene(extra2d::makePtr(), + extra2d::TransitionType::Fade, 0.5f); +} + +} // namespace flappybird diff --git a/examples/flappy_bird/StartScene.h b/examples/flappy_bird/StartScene.h new file mode 100644 index 0000000..855d623 --- /dev/null +++ b/examples/flappy_bird/StartScene.h @@ -0,0 +1,53 @@ +// ============================================================================ +// StartScene.h - 开始菜单场景 +// 描述: 显示游戏标题、开始按钮和版权信息 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +/** + * @brief 开始场景类 + * 游戏主菜单,包含开始游戏按钮和版权信息 + */ +class StartScene : public extra2d::Scene { +public: + /** + * @brief 构造函数 + */ + StartScene(); + + /** + * @brief 场景进入时调用 + */ + void onEnter() override; + + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override; + +private: + /** + * @brief 创建菜单按钮 + */ + void createMenuButtons(); + + /** + * @brief 开始游戏 + */ + void startGame(); + + extra2d::Ptr playBtn_; // 开始按钮 + extra2d::Ptr shareBtn_; // 分享按钮 + extra2d::Ptr title_; // 标题精灵 + float titleFinalY_ = 0.0f; // 标题最终Y位置 + float titleAnimTime_ = 0.0f; // 标题动画时间 + static constexpr float TITLE_ANIM_DURATION = 0.5f; // 标题动画持续时间 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/bird.cpp b/examples/flappy_bird/bird.cpp new file mode 100644 index 0000000..a7cdae9 --- /dev/null +++ b/examples/flappy_bird/bird.cpp @@ -0,0 +1,188 @@ +// ============================================================================ +// Bird.cpp - 小鸟类实现 +// ============================================================================ + +#include "Bird.h" +#include "ResLoader.h" + +namespace flappybird { + +Bird::Bird() { + // 注意:不要在构造函数中调用 initAnimations() + // 因为此时 weak_from_this() 还不能使用 + setStatus(Status::Idle); +} + +void Bird::onEnter() { + Node::onEnter(); + // 在 onEnter 中初始化动画,此时 weak_from_this() 可用 + if (!animSprite_) { + initAnimations(); + } +} + +Bird::~Bird() = default; + +void Bird::initAnimations() { + // 随机选择小鸟颜色(0-2) + int colorMode = extra2d::randomInt(0, 2); + std::string prefix = "bird" + std::to_string(colorMode) + "_"; + + // 创建动画片段 + auto clip = extra2d::AnimationClip::create("bird_fly"); + + // 添加动画帧序列: 0 -> 1 -> 2 -> 1 + // 注意:每个颜色只有 0, 1, 2 三个帧,没有 3 + int frameSequence[] = {0, 1, 2, 1}; + for (int frameIndex : frameSequence) { + auto frameSprite = ResLoader::getKeyFrame(prefix + std::to_string(frameIndex)); + if (frameSprite) { + extra2d::AnimationFrame frame; + frame.spriteFrame = frameSprite; + frame.delay = 100.0f; // 100毫秒 = 0.1秒 + clip->addFrame(std::move(frame)); + } else { + E2D_LOG_WARN("无法加载动画帧: {}{}", prefix, frameIndex); + } + } + + // 创建动画精灵 + if (clip->getFrameCount() > 0) { + clip->setLooping(true); + animSprite_ = extra2d::AnimatedSprite::create(clip); + // 精灵图动画不应应用帧变换(避免覆盖节点位置) + animSprite_->setApplyFrameTransform(false); + animSprite_->play(); + addChild(animSprite_); + E2D_LOG_INFO("小鸟动画创建成功: 颜色={}, 帧数={}, running={}, animSprite父节点={}", + colorMode, clip->getFrameCount(), isRunning(), + animSprite_->getParent() ? "有" : "无"); + } else { + E2D_LOG_ERROR("小鸟动画创建失败: 没有找到任何动画帧"); + } +} + +void Bird::onUpdate(float dt) { + extra2d::Node::onUpdate(dt); + + // 处理闲置动画(上下浮动) + if (status_ == Status::Idle) { + idleTimer_ += dt; + idleOffset_ = std::sin(idleTimer_ * 5.0f) * 4.0f; + } +} + +void Bird::onRender(extra2d::RenderBackend& renderer) { + // 动画精灵会自动渲染,这里只需要处理旋转和偏移 + if (animSprite_) { + animSprite_->setRotation(rotation_); + + // 应用闲置偏移 + if (status_ == Status::Idle) { + animSprite_->setPosition(extra2d::Vec2(0.0f, idleOffset_)); + } else { + animSprite_->setPosition(extra2d::Vec2(0.0f, 0.0f)); + } + } + + // 调用父类的 onRender 来渲染子节点 + Node::onRender(renderer); +} + +void Bird::fall(float dt) { + if (!living_) return; + + // 更新垂直位置 + extra2d::Vec2 pos = getPosition(); + pos.y += speed_ * dt; + setPosition(pos); + + // 应用重力 + speed_ += gravity * dt; + + // 限制顶部边界 + if (pos.y < 0) { + pos.y = 0; + setPosition(pos); + speed_ = 0; + } + + // 根据速度计算旋转角度 + // 上升时抬头(-15度),下降时低头(最大90度) + if (speed_ < 0) { + rotation_ = -15.0f; + } else { + rotation_ = std::min(90.0f, speed_ * 0.15f); + } +} + +void Bird::jump() { + if (!living_) return; + + // 给小鸟向上的速度 + speed_ = -jumpSpeed; + + // 设置状态为飞行 + setStatus(Status::Fly); + + // 播放音效 + ResLoader::playMusic(MusicType::Fly); +} + +void Bird::die() { + living_ = false; + + // 播放死亡音效 + ResLoader::playMusic(MusicType::Hit); +} + +void Bird::setStatus(Status status) { + status_ = status; + + switch (status) { + case Status::Still: + // 停止所有动画 + if (animSprite_) { + animSprite_->pause(); + } + break; + + case Status::Idle: + // 开始闲置动画 + if (animSprite_) { + animSprite_->setPlaybackSpeed(1.0f); // 正常速度 + animSprite_->play(); + } + idleTimer_ = 0.0f; + break; + + case Status::StartToFly: + // 停止闲置动画,加速翅膀扇动 + idleOffset_ = 0.0f; + if (animSprite_) { + animSprite_->setPlaybackSpeed(2.0f); // 2倍速度 = 0.05秒每帧 + } + break; + + case Status::Fly: + // 飞行状态 + break; + + default: + break; + } +} + +extra2d::Rect Bird::getBoundingBox() const { + extra2d::Vec2 pos = getPosition(); + // 小鸟碰撞框大小约为 24x24 + float halfSize = 12.0f; + return extra2d::Rect( + pos.x - halfSize, + pos.y - halfSize, + halfSize * 2.0f, + halfSize * 2.0f + ); +} + +} // namespace flappybird diff --git a/examples/flappy_bird/bird.h b/examples/flappy_bird/bird.h new file mode 100644 index 0000000..560b815 --- /dev/null +++ b/examples/flappy_bird/bird.h @@ -0,0 +1,116 @@ +// ============================================================================ +// Bird.h - 小鸟类 +// 描述: 玩家控制的小鸟角色,包含飞行动画和物理效果 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +/** + * @brief 小鸟类 + * 游戏主角,包含飞行动画、重力模拟和状态管理 + */ +class Bird : public extra2d::Node { +public: + /** + * @brief 小鸟状态枚举 + */ + enum class Status { + Still, // 静止不动 + Idle, // 上下浮动(菜单展示) + StartToFly, // 开始飞行 + Fly // 飞行中 + }; + + /** + * @brief 构造函数 + */ + Bird(); + + /** + * @brief 析构函数 + */ + ~Bird(); + + /** + * @brief 每帧更新 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override; + + /** + * @brief 渲染小鸟 + * @param renderer 渲染后端 + */ + void onRender(extra2d::RenderBackend& renderer) override; + + /** + * @brief 进入场景时调用 + */ + void onEnter() override; + + /** + * @brief 模拟下落 + * @param dt 时间间隔(秒) + */ + void fall(float dt); + + /** + * @brief 跳跃 + */ + void jump(); + + /** + * @brief 死亡 + */ + void die(); + + /** + * @brief 设置小鸟状态 + * @param status 新状态 + */ + void setStatus(Status status); + + /** + * @brief 获取当前状态 + * @return 当前状态 + */ + Status getStatus() const { return status_; } + + /** + * @brief 检查是否存活 + * @return 是否存活 + */ + bool isLiving() const { return living_; } + + /** + * @brief 获取边界框(用于碰撞检测) + * @return 边界框 + */ + extra2d::Rect getBoundingBox() const override; + +private: + /** + * @brief 初始化动画 + */ + void initAnimations(); + + bool living_ = true; // 是否存活 + float speed_ = 0.0f; // 垂直速度 + float rotation_ = 0.0f; // 旋转角度 + Status status_ = Status::Idle; // 当前状态 + + // 动画相关 + extra2d::Ptr animSprite_; // 动画精灵 + float idleTimer_ = 0.0f; // 闲置动画计时器 + float idleOffset_ = 0.0f; // 闲置偏移量 + + // 物理常量 + static constexpr float gravity = 1440.0f; // 重力加速度 + static constexpr float jumpSpeed = 432.0f; // 跳跃初速度 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/ground.cpp b/examples/flappy_bird/ground.cpp new file mode 100644 index 0000000..00bf3e6 --- /dev/null +++ b/examples/flappy_bird/ground.cpp @@ -0,0 +1,76 @@ +// ============================================================================ +// Ground.cpp - 地面类实现 +// ============================================================================ + +#include "Ground.h" +#include "ResLoader.h" + +namespace flappybird { + +Ground::Ground() { + moving_ = true; + + auto& app = extra2d::Application::instance(); + float screenHeight = static_cast(app.getConfig().height); + + // 获取地面纹理帧 + auto landFrame = ResLoader::getKeyFrame("land"); + if (!landFrame) return; + + // 获取地面纹理和矩形 + auto texture = landFrame->getTexture(); + auto rect = landFrame->getRect(); + float groundWidth = rect.size.width; + float groundHeight = rect.size.height; + + // 创建第一块地面 + ground1_ = extra2d::Sprite::create(texture, rect); + ground1_->setAnchor(extra2d::Vec2(0.0f, 1.0f)); // 锚点设在左下角 + ground1_->setPosition(extra2d::Vec2(0.0f, screenHeight)); + addChild(ground1_); + + // 创建第二块地面,紧挨在第一块右边 + ground2_ = extra2d::Sprite::create(texture, rect); + ground2_->setAnchor(extra2d::Vec2(0.0f, 1.0f)); + ground2_->setPosition(extra2d::Vec2(groundWidth - 1.0f, screenHeight)); + addChild(ground2_); +} + +void Ground::onUpdate(float dt) { + extra2d::Node::onUpdate(dt); + + if (!moving_) return; + if (!ground1_ || !ground2_) return; + + // 获取地面宽度(从纹理矩形获取) + float groundWidth = ground1_->getTextureRect().size.width; + + // 移动两块地面 + extra2d::Vec2 pos1 = ground1_->getPosition(); + extra2d::Vec2 pos2 = ground2_->getPosition(); + + pos1.x -= speed * dt; + pos2.x -= speed * dt; + + // 当地面完全移出屏幕左侧时,重置到右侧 + if (pos1.x <= -groundWidth) { + pos1.x = pos2.x + groundWidth - 1.0f; + } + if (pos2.x <= -groundWidth) { + pos2.x = pos1.x + groundWidth - 1.0f; + } + + ground1_->setPosition(pos1); + ground2_->setPosition(pos2); +} + +void Ground::stop() { + moving_ = false; +} + +float Ground::getHeight() const { + auto landFrame = ResLoader::getKeyFrame("land"); + return landFrame ? landFrame->getRect().size.height : 112.0f; +} + +} // namespace flappybird diff --git a/examples/flappy_bird/ground.h b/examples/flappy_bird/ground.h new file mode 100644 index 0000000..1f23986 --- /dev/null +++ b/examples/flappy_bird/ground.h @@ -0,0 +1,48 @@ +// ============================================================================ +// Ground.h - 地面类 +// 描述: 游戏底部不断向左滚动的地面 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +/** + * @brief 地面类 + * 游戏底部的滚动地面,由两块地面拼接而成 + */ +class Ground : public extra2d::Node { +public: + /** + * @brief 构造函数 + */ + Ground(); + + /** + * @brief 每帧更新 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override; + + /** + * @brief 停止地面滚动 + */ + void stop(); + + /** + * @brief 获取地面高度 + * @return 地面高度 + */ + float getHeight() const; + +private: + extra2d::Ptr ground1_; // 第一块地面 + extra2d::Ptr ground2_; // 第二块地面 + + static constexpr float speed = 120.0f; // 滚动速度(像素/秒) + bool moving_ = true; // 是否正在滚动 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/main.cpp b/examples/flappy_bird/main.cpp new file mode 100644 index 0000000..00c39c9 --- /dev/null +++ b/examples/flappy_bird/main.cpp @@ -0,0 +1,54 @@ +// ============================================================================ +// FlappyBird - Extra2D 示例程序 +// 作者: Extra2D Team +// 描述: 经典的 Flappy Bird 游戏实现 +// ============================================================================ + +#include +#include "SplashScene.h" +#include "ResLoader.h" + +using namespace extra2d; + +/** + * @brief 程序入口 + */ +int main(int argc, char **argv) +{ + // 初始化日志系统 + Logger::init(); + Logger::setLevel(LogLevel::Debug); + + E2D_LOG_INFO("========================"); + E2D_LOG_INFO("Extra2D FlappyBird"); + E2D_LOG_INFO("========================"); + + // 获取应用实例 + auto &app = Application::instance(); + + // 配置应用 + AppConfig config; + config.title = "Extra2D - FlappyBird"; + config.width = 288; // 原始游戏宽度 + config.height = 512; // 原始游戏高度 + config.vsync = true; + config.fpsLimit = 60; + + // 初始化应用 + if (!app.init(config)) { + E2D_LOG_ERROR("应用初始化失败!"); + return -1; + } + + // 初始化资源加载器 + flappybird::ResLoader::init(); + + // 进入启动场景 + app.enterScene(makePtr()); + + E2D_LOG_INFO("开始主循环..."); + app.run(); + + E2D_LOG_INFO("应用结束"); + return 0; +} diff --git a/examples/flappy_bird/pipe.cpp b/examples/flappy_bird/pipe.cpp new file mode 100644 index 0000000..3f80045 --- /dev/null +++ b/examples/flappy_bird/pipe.cpp @@ -0,0 +1,112 @@ +// ============================================================================ +// Pipe.cpp - 水管类实现 +// ============================================================================ + +#include "Pipe.h" +#include "ResLoader.h" + +namespace flappybird { + +Pipe::Pipe() { + scored = false; + // 注意:不要在构造函数中创建子节点 + // 因为此时 weak_from_this() 还不能使用 +} + +void Pipe::onEnter() { + Node::onEnter(); + + // 在 onEnter 中创建子节点,此时 weak_from_this() 可用 + if (!topPipe_ && !bottomPipe_) { + auto& app = extra2d::Application::instance(); + float screenHeight = static_cast(app.getConfig().height); + + // 获取地面高度 + auto landFrame = ResLoader::getKeyFrame("land"); + float landHeight = landFrame ? landFrame->getRect().size.height : 112.0f; + + // 随机生成水管高度 + // 范围:与屏幕顶部最小距离不小于 100 像素 + // 与屏幕底部最小距离不小于地面上方 100 像素 + float minHeight = 100.0f; + float maxHeight = screenHeight - landHeight - 100.0f - gapHeight_; + float height = static_cast(extra2d::randomInt(static_cast(minHeight), static_cast(maxHeight))); + + // 创建上水管 + auto topFrame = ResLoader::getKeyFrame("pipe_above"); + if (topFrame) { + topPipe_ = extra2d::Sprite::create(topFrame->getTexture(), topFrame->getRect()); + topPipe_->setAnchor(extra2d::Vec2(0.5f, 1.0f)); // 锚点设在底部中心 + topPipe_->setPosition(extra2d::Vec2(0.0f, height - gapHeight_ / 2.0f)); + addChild(topPipe_); + } + + // 创建下水管 + auto bottomFrame = ResLoader::getKeyFrame("pipe_below"); + if (bottomFrame) { + bottomPipe_ = extra2d::Sprite::create(bottomFrame->getTexture(), bottomFrame->getRect()); + bottomPipe_->setAnchor(extra2d::Vec2(0.5f, 0.0f)); // 锚点设在顶部中心 + bottomPipe_->setPosition(extra2d::Vec2(0.0f, height + gapHeight_ / 2.0f)); + addChild(bottomPipe_); + } + } +} + +Pipe::~Pipe() = default; + +extra2d::Rect Pipe::getBoundingBox() const { + // 返回整个水管的边界框(包含上下两根) + extra2d::Vec2 pos = getPosition(); + + // 水管宽度约为 52 + float pipeWidth = 52.0f; + float halfWidth = pipeWidth / 2.0f; + + auto& app = extra2d::Application::instance(); + float screenHeight = static_cast(app.getConfig().height); + + return extra2d::Rect( + pos.x - halfWidth, + 0.0f, + pipeWidth, + screenHeight + ); +} + +extra2d::Rect Pipe::getTopPipeBox() const { + if (!topPipe_) return extra2d::Rect(); + + extra2d::Vec2 pos = getPosition(); + extra2d::Vec2 topPos = topPipe_->getPosition(); + + // 上水管尺寸 + float pipeWidth = 52.0f; + float pipeHeight = 320.0f; + + return extra2d::Rect( + pos.x - pipeWidth / 2.0f, + pos.y + topPos.y - pipeHeight, + pipeWidth, + pipeHeight + ); +} + +extra2d::Rect Pipe::getBottomPipeBox() const { + if (!bottomPipe_) return extra2d::Rect(); + + extra2d::Vec2 pos = getPosition(); + extra2d::Vec2 bottomPos = bottomPipe_->getPosition(); + + // 下水管尺寸 + float pipeWidth = 52.0f; + float pipeHeight = 320.0f; + + return extra2d::Rect( + pos.x - pipeWidth / 2.0f, + pos.y + bottomPos.y, + pipeWidth, + pipeHeight + ); +} + +} // namespace flappybird diff --git a/examples/flappy_bird/pipe.h b/examples/flappy_bird/pipe.h new file mode 100644 index 0000000..3ceda9b --- /dev/null +++ b/examples/flappy_bird/pipe.h @@ -0,0 +1,59 @@ +// ============================================================================ +// Pipe.h - 水管类 +// 描述: 游戏中的障碍物,由上下两根水管组成 +// ============================================================================ + +#pragma once + +#include + +namespace flappybird { + +/** + * @brief 水管类 + * 由上下两根水管组成的障碍物 + */ +class Pipe : public extra2d::Node { +public: + /** + * @brief 构造函数 + */ + Pipe(); + + /** + * @brief 析构函数 + */ + ~Pipe(); + + /** + * @brief 进入场景时调用 + */ + void onEnter() override; + + /** + * @brief 获取边界框(用于碰撞检测) + * @return 边界框 + */ + extra2d::Rect getBoundingBox() const override; + + /** + * @brief 获取上水管边界框 + * @return 边界框 + */ + extra2d::Rect getTopPipeBox() const; + + /** + * @brief 获取下水管边界框 + * @return 边界框 + */ + extra2d::Rect getBottomPipeBox() const; + + bool scored = false; // 是否已计分 + +private: + extra2d::Ptr topPipe_; // 上水管 + extra2d::Ptr bottomPipe_; // 下水管 + float gapHeight_ = 120.0f; // 间隙高度 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/pipes.cpp b/examples/flappy_bird/pipes.cpp new file mode 100644 index 0000000..fed2ff4 --- /dev/null +++ b/examples/flappy_bird/pipes.cpp @@ -0,0 +1,106 @@ +// ============================================================================ +// Pipes.cpp - 水管管理器实现 +// ============================================================================ + +#include "Pipes.h" + +namespace flappybird { + +Pipes::Pipes() { + pipeCount_ = 0; + moving_ = false; + + // 初始化水管数组 + for (int i = 0; i < maxPipes; ++i) { + pipes_[i] = nullptr; + } + + // 注意:不要在构造函数中添加水管 + // 因为此时 weak_from_this() 还不能使用 +} + +void Pipes::onEnter() { + Node::onEnter(); + // 在 onEnter 中初始化水管,此时 weak_from_this() 可用 + if (pipeCount_ == 0) { + addPipe(); + addPipe(); + addPipe(); + } +} + +Pipes::~Pipes() = default; + +void Pipes::onUpdate(float dt) { + extra2d::Node::onUpdate(dt); + + if (!moving_) return; + + // 移动所有水管 + for (int i = 0; i < pipeCount_; ++i) { + if (pipes_[i]) { + extra2d::Vec2 pos = pipes_[i]->getPosition(); + pos.x -= pipeSpeed * dt; + pipes_[i]->setPosition(pos); + } + } + + // 检查最前面的水管是否移出屏幕 + if (pipes_[0] && pipes_[0]->getPosition().x <= -30.0f) { + // 移除第一个水管(通过名称查找并移除) + // 由于 removeChild 需要 Ptr,我们使用 removeChildByName 或直接操作 + // 这里我们直接移除第一个子节点(假设它是水管) + auto children = getChildren(); + if (!children.empty()) { + removeChild(children[0]); + } + + // 将后面的水管前移 + for (int i = 0; i < pipeCount_ - 1; ++i) { + pipes_[i] = pipes_[i + 1]; + } + pipes_[pipeCount_ - 1] = nullptr; + pipeCount_--; + + // 添加新水管 + addPipe(); + } +} + +void Pipes::addPipe() { + if (pipeCount_ >= maxPipes) return; + + // 创建新水管 + auto pipe = extra2d::makePtr(); + + // 设置水管位置 + if (pipeCount_ == 0) { + // 第一个水管在屏幕外 130 像素处 + auto& app = extra2d::Application::instance(); + pipe->setPosition(extra2d::Vec2( + static_cast(app.getConfig().width) + 130.0f, + 0.0f + )); + } else { + // 其他水管在前一个水管后方 + float prevX = pipes_[pipeCount_ - 1]->getPosition().x; + pipe->setPosition(extra2d::Vec2(prevX + pipeSpacing, 0.0f)); + } + + // 保存水管指针 + pipes_[pipeCount_] = pipe.get(); + pipeCount_++; + + // 添加到场景 + addChild(pipe); +} + +void Pipes::start() { + moving_ = true; +} + +void Pipes::stop() { + moving_ = false; +} + +} // namespace flappybird diff --git a/examples/flappy_bird/pipes.h b/examples/flappy_bird/pipes.h new file mode 100644 index 0000000..11dd88d --- /dev/null +++ b/examples/flappy_bird/pipes.h @@ -0,0 +1,71 @@ +// ============================================================================ +// Pipes.h - 水管管理器 +// 描述: 管理多个水管的生成、移动和回收 +// ============================================================================ + +#pragma once + +#include +#include "Pipe.h" + +namespace flappybird { + +/** + * @brief 水管管理器类 + * 管理游戏中的所有水管,负责生成、移动和回收 + */ +class Pipes : public extra2d::Node { +public: + /** + * @brief 构造函数 + */ + Pipes(); + + /** + * @brief 析构函数 + */ + ~Pipes(); + + /** + * @brief 每帧更新 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override; + + /** + * @brief 进入场景时调用 + */ + void onEnter() override; + + /** + * @brief 开始移动水管 + */ + void start(); + + /** + * @brief 停止移动水管 + */ + void stop(); + + /** + * @brief 获取当前水管数组 + * @return 水管指针数组 + */ + Pipe* getPipe(int index) { return (index >= 0 && index < 3) ? pipes_[index] : nullptr; } + +private: + /** + * @brief 添加一根新水管 + */ + void addPipe(); + + static constexpr int maxPipes = 3; // 最大水管数量 + static constexpr float pipeSpeed = 120.0f; // 水管移动速度(像素/秒) + static constexpr float pipeSpacing = 200.0f; // 水管间距 + + Pipe* pipes_[maxPipes]; // 水管数组 + int pipeCount_ = 0; // 当前水管数量 + bool moving_ = false; // 是否正在移动 +}; + +} // namespace flappybird diff --git a/examples/flappy_bird/romfs/assets/images/atlas.json b/examples/flappy_bird/romfs/assets/images/atlas.json new file mode 100644 index 0000000..ce507c0 --- /dev/null +++ b/examples/flappy_bird/romfs/assets/images/atlas.json @@ -0,0 +1,78 @@ +{ + "sprites": [ + {"name": "splash", "width": 288, "height": 512, "x": 292, "y": 515}, + {"name": "bg_day", "width": 288, "height": 512, "x": 0, "y": 0}, + {"name": "bg_night", "width": 288, "height": 512, "x": 292, "y": 0}, + {"name": "bird0_0", "width": 34, "height": 24, "x": 5, "y": 982}, + {"name": "bird0_1", "width": 34, "height": 24, "x": 61, "y": 982}, + {"name": "bird0_2", "width": 34, "height": 24, "x": 117, "y": 982}, + {"name": "bird1_0", "width": 34, "height": 24, "x": 173, "y": 982}, + {"name": "bird1_1", "width": 34, "height": 24, "x": 229, "y": 658}, + {"name": "bird1_2", "width": 34, "height": 24, "x": 229, "y": 710}, + {"name": "bird2_0", "width": 34, "height": 24, "x": 229, "y": 762}, + {"name": "bird2_1", "width": 34, "height": 24, "x": 229, "y": 814}, + {"name": "bird2_2", "width": 34, "height": 24, "x": 229, "y": 866}, + {"name": "black", "width": 32, "height": 32, "x": 584, "y": 412}, + {"name": "blink_00", "width": 10, "height": 10, "x": 276, "y": 682}, + {"name": "blink_01", "width": 10, "height": 10, "x": 276, "y": 734}, + {"name": "blink_02", "width": 10, "height": 10, "x": 276, "y": 786}, + {"name": "brand_copyright", "width": 126, "height": 14, "x": 884, "y": 182}, + {"name": "button_ok", "width": 80, "height": 28, "x": 924, "y": 84}, + {"name": "button_pause", "width": 26, "height": 28, "x": 242, "y": 612}, + {"name": "button_rate", "width": 74, "height": 48, "x": 924, "y": 0}, + {"name": "button_resume", "width": 26, "height": 28, "x": 668, "y": 284}, + {"name": "button_score", "width": 116, "height": 70, "x": 822, "y": 234}, + {"name": "button_restart", "width": 116, "height": 70, "x": 702, "y": 316}, + {"name": "button_share_big", "width": 116, "height": 70, "x": 822, "y": 316}, + {"name": "button_play", "width": 116, "height": 70, "x": 702, "y": 234}, + {"name": "button_menu", "width": 80, "height": 28, "x": 924, "y": 52}, + {"name": "button_share", "width": 80, "height": 28, "x": 584, "y": 284}, + {"name": "land", "width": 336, "height": 112, "x": 584, "y": 0}, + {"name": "medals_0", "width": 44, "height": 44, "x": 224, "y": 954}, + {"name": "medals_1", "width": 44, "height": 44, "x": 224, "y": 906}, + {"name": "medals_2", "width": 44, "height": 44, "x": 242, "y": 564}, + {"name": "medals_3", "width": 44, "height": 44, "x": 242, "y": 516}, + {"name": "new", "width": 32, "height": 14, "x": 224, "y": 1002}, + {"name": "number_big_0", "width": 24, "height": 36, "x": 992, "y": 120}, + {"name": "number_big_1", "width": 16, "height": 36, "x": 272, "y": 910}, + {"name": "number_big_2", "width": 24, "height": 36, "x": 584, "y": 320}, + {"name": "number_big_3", "width": 24, "height": 36, "x": 612, "y": 320}, + {"name": "number_big_4", "width": 24, "height": 36, "x": 640, "y": 320}, + {"name": "number_big_5", "width": 24, "height": 36, "x": 668, "y": 320}, + {"name": "number_big_6", "width": 24, "height": 36, "x": 584, "y": 368}, + {"name": "number_big_7", "width": 24, "height": 36, "x": 612, "y": 368}, + {"name": "number_big_8", "width": 24, "height": 36, "x": 640, "y": 368}, + {"name": "number_big_9", "width": 24, "height": 36, "x": 668, "y": 368}, + {"name": "number_medium_0", "width": 16, "height": 20, "x": 272, "y": 612}, + {"name": "number_medium_1", "width": 16, "height": 20, "x": 272, "y": 954}, + {"name": "number_medium_2", "width": 16, "height": 20, "x": 272, "y": 978}, + {"name": "number_medium_3", "width": 16, "height": 20, "x": 260, "y": 1002}, + {"name": "number_medium_4", "width": 16, "height": 20, "x": 1002, "y": 0}, + {"name": "number_medium_5", "width": 16, "height": 20, "x": 1002, "y": 24}, + {"name": "number_medium_6", "width": 16, "height": 20, "x": 1008, "y": 52}, + {"name": "number_medium_7", "width": 16, "height": 20, "x": 1008, "y": 84}, + {"name": "number_medium_8", "width": 16, "height": 20, "x": 584, "y": 484}, + {"name": "number_medium_9", "width": 16, "height": 20, "x": 620, "y": 412}, + {"name": "number_small_0", "width": 12, "height": 14, "x": 276, "y": 646}, + {"name": "number_small_1", "width": 12, "height": 14, "x": 276, "y": 664}, + {"name": "number_small_2", "width": 12, "height": 14, "x": 276, "y": 698}, + {"name": "number_small_3", "width": 12, "height": 14, "x": 276, "y": 716}, + {"name": "number_small_4", "width": 12, "height": 14, "x": 276, "y": 750}, + {"name": "number_small_5", "width": 12, "height": 14, "x": 276, "y": 768}, + {"name": "number_small_6", "width": 12, "height": 14, "x": 276, "y": 802}, + {"name": "number_small_7", "width": 12, "height": 14, "x": 276, "y": 820}, + {"name": "number_small_8", "width": 12, "height": 14, "x": 276, "y": 854}, + {"name": "number_small_9", "width": 12, "height": 14, "x": 276, "y": 872}, + {"name": "number_context_10", "width": 12, "height": 14, "x": 992, "y": 164}, + {"name": "pipe_above_2", "width": 52, "height": 320, "x": 0, "y": 646}, + {"name": "pipe_below_2", "width": 52, "height": 320, "x": 56, "y": 646}, + {"name": "pipe_above", "width": 52, "height": 320, "x": 112, "y": 646}, + {"name": "pipe_below", "width": 52, "height": 320, "x": 168, "y": 646}, + {"name": "score_panel", "width": 238, "height": 126, "x": 0, "y": 516}, + {"name": "text_game_over", "width": 204, "height": 54, "x": 784, "y": 116}, + {"name": "text_ready", "width": 196, "height": 62, "x": 584, "y": 116}, + {"name": "title", "width": 178, "height": 48, "x": 702, "y": 182}, + {"name": "tutorial", "width": 114, "height": 98, "x": 584, "y": 182}, + {"name": "white", "width": 32, "height": 32, "x": 584, "y": 448} + ] +} \ No newline at end of file diff --git a/examples/flappy_bird/romfs/assets/images/atlas.png b/examples/flappy_bird/romfs/assets/images/atlas.png new file mode 100644 index 0000000..05e5ce1 Binary files /dev/null and b/examples/flappy_bird/romfs/assets/images/atlas.png differ diff --git a/examples/flappy_bird/romfs/assets/sound/click.wav b/examples/flappy_bird/romfs/assets/sound/click.wav new file mode 100644 index 0000000..aef35cd Binary files /dev/null and b/examples/flappy_bird/romfs/assets/sound/click.wav differ diff --git a/examples/flappy_bird/romfs/assets/sound/fly.wav b/examples/flappy_bird/romfs/assets/sound/fly.wav new file mode 100644 index 0000000..940adf2 Binary files /dev/null and b/examples/flappy_bird/romfs/assets/sound/fly.wav differ diff --git a/examples/flappy_bird/romfs/assets/sound/hit.wav b/examples/flappy_bird/romfs/assets/sound/hit.wav new file mode 100644 index 0000000..0fe1cf7 Binary files /dev/null and b/examples/flappy_bird/romfs/assets/sound/hit.wav differ diff --git a/examples/flappy_bird/romfs/assets/sound/point.wav b/examples/flappy_bird/romfs/assets/sound/point.wav new file mode 100644 index 0000000..eb3961a Binary files /dev/null and b/examples/flappy_bird/romfs/assets/sound/point.wav differ diff --git a/examples/flappy_bird/romfs/assets/sound/swoosh.wav b/examples/flappy_bird/romfs/assets/sound/swoosh.wav new file mode 100644 index 0000000..e218eac Binary files /dev/null and b/examples/flappy_bird/romfs/assets/sound/swoosh.wav differ diff --git a/examples/flappy_bird/xmake.lua b/examples/flappy_bird/xmake.lua new file mode 100644 index 0000000..5984382 --- /dev/null +++ b/examples/flappy_bird/xmake.lua @@ -0,0 +1,92 @@ +-- ============================================== +-- FlappyBird 示例 - Xmake 构建脚本 +-- 支持平台: MinGW (Windows), Nintendo Switch +-- ============================================== + +-- 获取当前脚本所在目录(示例根目录) +local example_dir = os.scriptdir() + +-- 可执行文件目标 +target("flappy_bird") + set_kind("binary") + add_files("*.cpp") + add_includedirs("../../Extra2D/include") + add_includedirs("../../Extra2D/include/json") + add_deps("extra2d") + + -- 使用与主项目相同的平台配置 + if is_plat("switch") then + set_targetdir("../../build/examples/flappy_bird") + + -- 构建后生成 NRO 文件 + after_build(function (target) + local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro" + local elf_file = target:targetfile() + local output_dir = path.directory(elf_file) + local nacp_file = path.join(output_dir, "flappy_bird.nacp") + local nro_file = path.join(output_dir, "flappy_bird.nro") + local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe") + local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe") + + if os.isfile(nacptool) and os.isfile(elf2nro) then + os.vrunv(nacptool, {"--create", "FlappyBird", "Extra2D Team", "1.0.0", nacp_file}) + local romfs = path.join(example_dir, "romfs") + if os.isdir(romfs) then + os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs}) + else + os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file}) + end + print("Generated NRO: " .. nro_file) + end + end) + + -- 打包时将 NRO 文件复制到 package 目录 + after_package(function (target) + local nro_file = path.join(target:targetdir(), "flappy_bird.nro") + local package_dir = target:packagedir() + if os.isfile(nro_file) and package_dir then + os.cp(nro_file, package_dir) + print("Copied NRO to package: " .. package_dir) + end + end) + + elseif is_plat("mingw") then + set_targetdir("../../build/examples/flappy_bird") + -- add_ldflags("-mwindows", {force = true}) + + -- 复制资源到输出目录 + after_build(function (target) + local romfs = path.join(example_dir, "romfs") + if os.isdir(romfs) then + local target_dir = path.directory(target:targetfile()) + local assets_dir = path.join(target_dir, "assets") + + -- 创建 assets 目录 + if not os.isdir(assets_dir) then + os.mkdir(assets_dir) + end + + -- 复制所有资源文件(包括子目录) + os.cp(path.join(romfs, "assets/**"), assets_dir) + print("Copied assets from " .. romfs .. " to " .. assets_dir) + else + print("Warning: romfs directory not found at " .. romfs) + end + end) + + -- 打包时将资源复制到 package 目录 + after_package(function (target) + local target_dir = path.directory(target:targetfile()) + local assets_dir = path.join(target_dir, "assets") + local package_dir = target:packagedir() + if os.isdir(assets_dir) and package_dir then + local package_assets = path.join(package_dir, "assets") + if not os.isdir(package_assets) then + os.mkdir(package_assets) + end + os.cp(path.join(assets_dir, "**"), package_assets) + print("Copied assets to package: " .. package_assets) + end + end) + end +target_end() diff --git a/examples/push_box/xmake.lua b/examples/push_box/xmake.lua index cd62cf8..9d680df 100644 --- a/examples/push_box/xmake.lua +++ b/examples/push_box/xmake.lua @@ -51,7 +51,7 @@ target("push_box") elseif is_plat("mingw") then set_targetdir("../../build/examples/push_box") - add_ldflags("-mwindows", {force = true}) + -- add_ldflags("-mwindows", {force = true}) -- 复制资源到输出目录 after_build(function (target) diff --git a/xmake.lua b/xmake.lua index 1d8b574..392cc8f 100644 --- a/xmake.lua +++ b/xmake.lua @@ -91,7 +91,7 @@ if is_config("examples","true") then includes("examples/spatial_index_demo", {rootdir = "examples/spatial_index_demo"}) includes("examples/collision_demo", {rootdir = "examples/collision_demo"}) includes("examples/push_box", {rootdir = "examples/push_box"}) - -- includes("examples/flappy_bird", {rootdir = "examples/flappy_bird"}) + includes("examples/flappy_bird", {rootdir = "examples/flappy_bird"}) end -- ==============================================