feat(flappy_bird): 添加Flappy Bird游戏实现

新增Flappy Bird游戏完整实现,包含以下功能:
- 游戏场景管理(启动、开始、游戏、结束场景)
- 小鸟物理效果和动画
- 水管生成和移动逻辑
- 碰撞检测和得分系统
- 音效和资源管理
- 适配Switch和PC平台

修复渲染系统问题:
- 修正精灵渲染时的纹理坐标计算
- 添加变换矩阵栈支持
- 优化节点世界变换处理

调整构建系统:
- 添加xmake构建配置
- 支持资源打包和部署
This commit is contained in:
ChestnutYueyue 2026-02-12 21:50:21 +08:00
parent cb67abe0bb
commit 8b2482415e
41 changed files with 2297 additions and 33 deletions

View File

@ -5,6 +5,7 @@
#include <extra2d/graphics/opengl/gl_sprite_batch.h>
#include <glad/glad.h>
#include <vector>
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<Texture> createTexture(int width, int height, const uint8_t* pixels, int channels) override;
Ptr<Texture> loadTexture(const std::string& filepath) override;
@ -65,6 +71,7 @@ private:
GLuint shapeVbo_;
glm::mat4 viewProjection_;
std::vector<glm::mat4> transformStack_;
Stats stats_;
bool vsync_;

View File

@ -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;
// ------------------------------------------------------------------------
// 纹理
// ------------------------------------------------------------------------

View File

@ -101,6 +101,7 @@ public:
// ------------------------------------------------------------------------
void setBackgroundImage(Ptr<Texture> normal, Ptr<Texture> hover = nullptr,
Ptr<Texture> pressed = nullptr);
void setBackgroundImage(Ptr<Texture> 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<Texture> imgNormal_;
Ptr<Texture> imgHover_;
Ptr<Texture> 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);

View File

@ -1,5 +1,3 @@
#include <algorithm>
#include <cctype>
#include <extra2d/animation/ani_parser.h>
#include <extra2d/animation/sprite_frame_cache.h>
#include <extra2d/utils/logger.h>

View File

@ -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<Texture> GLRenderer::createTexture(int width, int height,
const uint8_t *pixels, int channels) {
return makePtr<GLTexture>(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));

View File

@ -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;
}

View File

@ -37,6 +37,9 @@ void Scene::renderContent(RenderBackend &renderer) {
if (!isVisible())
return;
// 在渲染前批量更新所有节点的世界变换
batchUpdateTransforms();
Camera *activeCam = getActiveCamera();
if (activeCam) {
renderer.setViewProjection(activeCam->getViewProjectionMatrix());

View File

@ -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<RenderCommand> &commands,
@ -103,15 +118,27 @@ void Sprite::generateRenderCommand(std::vector<RenderCommand> &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<RenderCommand> &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));
}

View File

@ -239,6 +239,7 @@ void Button::setBackgroundImage(Ptr<Texture> normal, Ptr<Texture> 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<float>(normal->getWidth()),
@ -246,6 +247,21 @@ void Button::setBackgroundImage(Ptr<Texture> normal, Ptr<Texture> hover,
}
}
void Button::setBackgroundImage(Ptr<Texture> 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<float>(imgPressed_->getWidth()), static_cast<float>(imgPressed_->getHeight()));
} else if (hovered_ && imgHover_) {
texture = imgHover_.get();
srcRect = useTextureRect_ ? imgHoverRect_ : Rect(0, 0, static_cast<float>(imgHover_->getWidth()), static_cast<float>(imgHover_->getHeight()));
} else if (imgNormal_) {
texture = imgNormal_.get();
srcRect = useTextureRect_ ? imgNormalRect_ : Rect(0, 0, static_cast<float>(imgNormal_->getWidth()), static_cast<float>(imgNormal_->getHeight()));
}
if (!texture)
return;
Vec2 imageSize(static_cast<float>(texture->getWidth()),
static_cast<float>(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_);

View File

@ -93,7 +93,7 @@ Vec2 Widget::getRenderPosition() const {
case CoordinateSpace::World:
default:
// 世界空间:使用节点的世界位置
return getPosition();
return convertToWorldSpace(extra2d::Vec2::Zero());
}
}

View File

@ -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<float>(app.getConfig().width);
float screenHeight = static_cast<float>(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<extra2d::MoveBy>(
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<Number>();
scoreNumber->setLittleNumber(score);
scoreNumber->setPosition(
extra2d::Vec2(80.0f, -15.0f)); // 相对于面板的坐标,右侧对齐
panel->addChild(scoreNumber);
// 显示最高分
auto bestNumber = extra2d::makePtr<Number>();
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<GameScene>(),
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<StartScene>(),
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<GameScene>(),
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<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
}
extra2d::Ptr<extra2d::SpriteFrame> 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

View File

@ -0,0 +1,58 @@
// ============================================================================
// GameOverLayer.h - 游戏结束层
// 描述: 显示游戏结束界面、得分和奖牌
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
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<extra2d::SpriteFrame> getMedal(int score);
int score_ = 0; // 本局得分
};
} // namespace flappybird

View File

@ -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<float>(config.width),
static_cast<float>(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<float>(app.getConfig().width);
float screenHeight = static_cast<float>(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_ = pipes.get();
pipes->setVisible(false);
addChild(pipes);
// 添加小鸟(在屏幕中间偏左位置)
auto bird = extra2d::makePtr<Bird>();
bird->setPosition(
extra2d::Vec2(screenWidth / 2.0f - 50.0f, screenHeight / 2.0f));
bird_ = bird.get();
addChild(bird);
// 添加地面
auto ground = extra2d::makePtr<Ground>();
ground_ = ground.get();
addChild(ground);
// 添加得分(屏幕顶部中央)
auto scoreNumber = extra2d::makePtr<Number>();
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<float>(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<GameOverLayer>(score_);
addChild(gameOverLayer);
}
} // namespace flappybird

View File

@ -0,0 +1,72 @@
// ============================================================================
// GameScene.h - 游戏主场景
// 描述: 游戏的核心场景,包含小鸟、水管、地面和得分系统
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
#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<extra2d::Sprite> readySprite_; // "Get Ready" 提示
extra2d::Ptr<extra2d::Sprite> tutorialSprite_; // 操作教程提示
bool started_ = false; // 游戏是否已开始
bool gameOver_ = false; // 游戏是否已结束
int score_ = 0; // 当前得分
};
} // namespace flappybird

View File

@ -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<int> 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<float> 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

View File

@ -0,0 +1,52 @@
// ============================================================================
// Number.h - 数字显示类
// 描述: 将整数数字转换为精灵图片显示
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
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

View File

@ -0,0 +1,96 @@
// ============================================================================
// ResLoader.cpp - 资源加载器实现
// ============================================================================
#include "ResLoader.h"
#include <json/json.hpp>
#include <fstream>
namespace flappybird {
extra2d::Ptr<extra2d::Texture> ResLoader::atlasTexture_;
std::map<std::string, ResLoader::ImageInfo> ResLoader::imageMap_;
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> 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<extra2d::SpriteFrame> 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<extra2d::SpriteFrame>(
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

View File

@ -0,0 +1,63 @@
// ============================================================================
// ResLoader.h - 资源加载器
// 描述: 管理游戏资源的加载和访问
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
#include <map>
#include <string>
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<extra2d::SpriteFrame> 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<extra2d::Texture> atlasTexture_; // 图集纹理
static std::map<std::string, ImageInfo> imageMap_; // 图片信息映射
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_; // 音效映射
};
} // namespace flappybird

View File

@ -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<float>(config.width), static_cast<float>(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<StartScene>(),
extra2d::TransitionType::Fade,
0.5f
);
}
} // namespace flappybird

View File

@ -0,0 +1,44 @@
// ============================================================================
// SplashScene.h - 启动场景
// 描述: 显示游戏 Logo2秒后自动跳转到开始场景
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
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

View File

@ -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<float>(config.width),
static_cast<float>(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<float>(app.getConfig().width);
float screenHeight = static_cast<float>(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<Ground>();
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>();
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<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
} // namespace flappybird

View File

@ -0,0 +1,53 @@
// ============================================================================
// StartScene.h - 开始菜单场景
// 描述: 显示游戏标题、开始按钮和版权信息
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
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<extra2d::Button> playBtn_; // 开始按钮
extra2d::Ptr<extra2d::Button> shareBtn_; // 分享按钮
extra2d::Ptr<extra2d::Sprite> title_; // 标题精灵
float titleFinalY_ = 0.0f; // 标题最终Y位置
float titleAnimTime_ = 0.0f; // 标题动画时间
static constexpr float TITLE_ANIM_DURATION = 0.5f; // 标题动画持续时间
};
} // namespace flappybird

View File

@ -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

116
examples/flappy_bird/bird.h Normal file
View File

@ -0,0 +1,116 @@
// ============================================================================
// Bird.h - 小鸟类
// 描述: 玩家控制的小鸟角色,包含飞行动画和物理效果
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
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<extra2d::AnimatedSprite> animSprite_; // 动画精灵
float idleTimer_ = 0.0f; // 闲置动画计时器
float idleOffset_ = 0.0f; // 闲置偏移量
// 物理常量
static constexpr float gravity = 1440.0f; // 重力加速度
static constexpr float jumpSpeed = 432.0f; // 跳跃初速度
};
} // namespace flappybird

View File

@ -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<float>(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

View File

@ -0,0 +1,48 @@
// ============================================================================
// Ground.h - 地面类
// 描述: 游戏底部不断向左滚动的地面
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
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<extra2d::Sprite> ground1_; // 第一块地面
extra2d::Ptr<extra2d::Sprite> ground2_; // 第二块地面
static constexpr float speed = 120.0f; // 滚动速度(像素/秒)
bool moving_ = true; // 是否正在滚动
};
} // namespace flappybird

View File

@ -0,0 +1,54 @@
// ============================================================================
// FlappyBird - Extra2D 示例程序
// 作者: Extra2D Team
// 描述: 经典的 Flappy Bird 游戏实现
// ============================================================================
#include <extra2d/extra2d.h>
#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<flappybird::SplashScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

View File

@ -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<float>(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<float>(extra2d::randomInt(static_cast<int>(minHeight), static_cast<int>(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<float>(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

View File

@ -0,0 +1,59 @@
// ============================================================================
// Pipe.h - 水管类
// 描述: 游戏中的障碍物,由上下两根水管组成
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
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<extra2d::Sprite> topPipe_; // 上水管
extra2d::Ptr<extra2d::Sprite> bottomPipe_; // 下水管
float gapHeight_ = 120.0f; // 间隙高度
};
} // namespace flappybird

View File

@ -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<Node>,我们使用 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<Pipe>();
// 设置水管位置
if (pipeCount_ == 0) {
// 第一个水管在屏幕外 130 像素处
auto& app = extra2d::Application::instance();
pipe->setPosition(extra2d::Vec2(
static_cast<float>(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

View File

@ -0,0 +1,71 @@
// ============================================================================
// Pipes.h - 水管管理器
// 描述: 管理多个水管的生成、移动和回收
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
#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

View File

@ -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}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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()

View File

@ -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)

View File

@ -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
-- ==============================================