feat(场景): 添加BaseScene作为统一基础场景类

重构所有场景类继承自BaseScene,提供统一的视口适配功能
使用游戏逻辑分辨率(GAME_WIDTH/GAME_HEIGHT)替代直接获取窗口尺寸
优化资源加载和音效播放的错误处理
This commit is contained in:
ChestnutYueyue 2026-02-13 08:43:50 +08:00
parent 1b72a1c992
commit 3a9b44cbfe
19 changed files with 699 additions and 332 deletions

View File

@ -133,6 +133,55 @@ public:
/// 卸载指定音效
void unloadSound(const std::string &key);
// ------------------------------------------------------------------------
// 文本文件资源
// ------------------------------------------------------------------------
/// 加载文本文件(带缓存)
/// @param filepath 文件路径,支持 romfs:/ 前缀
/// @return 文件内容字符串,加载失败返回空字符串
std::string loadTextFile(const std::string &filepath);
/// 加载文本文件(指定编码)
/// @param filepath 文件路径
/// @param encoding 文件编码(默认 UTF-8
/// @return 文件内容字符串
std::string loadTextFile(const std::string &filepath, const std::string &encoding);
/// 通过key获取已缓存的文本内容
std::string getTextFile(const std::string &key) const;
/// 检查文本文件是否已缓存
bool hasTextFile(const std::string &key) const;
/// 卸载指定文本文件
void unloadTextFile(const std::string &key);
/// 清理所有文本文件缓存
void clearTextFileCache();
// ------------------------------------------------------------------------
// JSON 文件资源
// ------------------------------------------------------------------------
/// 加载并解析 JSON 文件
/// @param filepath 文件路径,支持 romfs:/ 前缀
/// @return JSON 字符串内容,加载或解析失败返回空字符串
/// @note 返回的是原始 JSON 字符串,需要自行解析
std::string loadJsonFile(const std::string &filepath);
/// 通过key获取已缓存的 JSON 内容
std::string getJsonFile(const std::string &key) const;
/// 检查 JSON 文件是否已缓存
bool hasJsonFile(const std::string &key) const;
/// 卸载指定 JSON 文件
void unloadJsonFile(const std::string &key);
/// 清理所有 JSON 文件缓存
void clearJsonFileCache();
// ------------------------------------------------------------------------
// 缓存清理
// ------------------------------------------------------------------------
@ -152,6 +201,8 @@ public:
size_t getTextureCacheSize() const;
size_t getFontCacheSize() const;
size_t getSoundCacheSize() const;
size_t getTextFileCacheSize() const;
size_t getJsonFileCacheSize() const;
// ------------------------------------------------------------------------
// LRU 缓存管理
@ -213,11 +264,17 @@ private:
mutable std::mutex textureMutex_;
mutable std::mutex fontMutex_;
mutable std::mutex soundMutex_;
mutable std::mutex textFileMutex_;
mutable std::mutex jsonFileMutex_;
// 资源缓存 - 使用弱指针实现自动清理
std::unordered_map<std::string, WeakPtr<FontAtlas>> fontCache_;
std::unordered_map<std::string, WeakPtr<Sound>> soundCache_;
// 文本文件缓存 - 使用强引用(字符串值类型)
std::unordered_map<std::string, std::string> textFileCache_;
std::unordered_map<std::string, std::string> jsonFileCache_;
// ============================================================================
// 纹理LRU缓存
// ============================================================================

View File

@ -1,5 +1,6 @@
#include <SDL2/SDL_mixer.h>
#include <extra2d/audio/sound.h>
#include <extra2d/utils/logger.h>
namespace extra2d {
@ -20,22 +21,20 @@ Sound::~Sound() {
bool Sound::play() {
if (!chunk_) {
E2D_LOG_WARN("Sound::play() failed: chunk is null for {}", name_);
return false;
}
// 如果已在播放,先停止
if (channel_ >= 0 && Mix_Playing(channel_)) {
Mix_HaltChannel(channel_);
}
int loops = looping_ ? -1 : 0;
channel_ = Mix_PlayChannel(-1, chunk_, loops); // -1 = 自动分配通道
int newChannel = Mix_PlayChannel(-1, chunk_, loops);
if (channel_ < 0) {
if (newChannel < 0) {
E2D_LOG_WARN("Sound::play() failed: no free channel for {} ({})", name_, Mix_GetError());
return false;
}
// 设置音量
channel_ = newChannel;
int mixVol = static_cast<int>(volume_ * MIX_MAX_VOLUME);
Mix_Volume(channel_, mixVol);

View File

@ -856,4 +856,220 @@ size_t ResourceManager::getSoundCacheSize() const {
return soundCache_.size();
}
// ============================================================================
// 文本文件资源
// ============================================================================
std::string ResourceManager::loadTextFile(const std::string &filepath) {
return loadTextFile(filepath, "UTF-8");
}
std::string ResourceManager::loadTextFile(const std::string &filepath, const std::string &encoding) {
(void)encoding; // 目前只支持 UTF-8
std::lock_guard<std::mutex> lock(textFileMutex_);
// 检查缓存
auto it = textFileCache_.find(filepath);
if (it != textFileCache_.end()) {
E2D_LOG_TRACE("ResourceManager: text file cache hit: {}", filepath);
return it->second;
}
// 解析资源路径
std::string resolvedPath = resolveResourcePath(filepath);
if (resolvedPath.empty()) {
E2D_LOG_ERROR("ResourceManager: text file not found: {}", filepath);
return "";
}
// 打开文件
FILE *file = nullptr;
#ifdef _WIN32
errno_t err = fopen_s(&file, resolvedPath.c_str(), "rb");
if (err != 0 || !file) {
E2D_LOG_ERROR("ResourceManager: failed to open text file: {}", resolvedPath);
return "";
}
#else
file = fopen(resolvedPath.c_str(), "rb");
if (!file) {
E2D_LOG_ERROR("ResourceManager: failed to open text file: {}", resolvedPath);
return "";
}
#endif
// 获取文件大小
fseek(file, 0, SEEK_END);
long fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
if (fileSize <= 0) {
fclose(file);
E2D_LOG_WARN("ResourceManager: text file is empty: {}", resolvedPath);
return "";
}
// 读取文件内容
std::string content;
content.resize(fileSize);
size_t readSize = fread(&content[0], 1, fileSize, file);
fclose(file);
if (readSize != static_cast<size_t>(fileSize)) {
E2D_LOG_ERROR("ResourceManager: failed to read text file: {}", resolvedPath);
return "";
}
// 缓存内容
textFileCache_[filepath] = content;
E2D_LOG_DEBUG("ResourceManager: loaded text file: {} ({} bytes)", filepath, content.size());
return content;
}
std::string ResourceManager::getTextFile(const std::string &key) const {
std::lock_guard<std::mutex> lock(textFileMutex_);
auto it = textFileCache_.find(key);
if (it != textFileCache_.end()) {
return it->second;
}
return "";
}
bool ResourceManager::hasTextFile(const std::string &key) const {
std::lock_guard<std::mutex> lock(textFileMutex_);
return textFileCache_.find(key) != textFileCache_.end();
}
void ResourceManager::unloadTextFile(const std::string &key) {
std::lock_guard<std::mutex> lock(textFileMutex_);
auto it = textFileCache_.find(key);
if (it != textFileCache_.end()) {
textFileCache_.erase(it);
E2D_LOG_DEBUG("ResourceManager: unloaded text file: {}", key);
}
}
void ResourceManager::clearTextFileCache() {
std::lock_guard<std::mutex> lock(textFileMutex_);
size_t count = textFileCache_.size();
textFileCache_.clear();
E2D_LOG_INFO("ResourceManager: cleared {} text files from cache", count);
}
size_t ResourceManager::getTextFileCacheSize() const {
std::lock_guard<std::mutex> lock(textFileMutex_);
return textFileCache_.size();
}
// ============================================================================
// JSON 文件资源
// ============================================================================
std::string ResourceManager::loadJsonFile(const std::string &filepath) {
std::lock_guard<std::mutex> lock(jsonFileMutex_);
// 检查缓存
auto it = jsonFileCache_.find(filepath);
if (it != jsonFileCache_.end()) {
E2D_LOG_TRACE("ResourceManager: JSON file cache hit: {}", filepath);
return it->second;
}
// 解析资源路径
std::string resolvedPath = resolveResourcePath(filepath);
if (resolvedPath.empty()) {
E2D_LOG_ERROR("ResourceManager: JSON file not found: {}", filepath);
return "";
}
// 打开文件
FILE *file = nullptr;
#ifdef _WIN32
errno_t err = fopen_s(&file, resolvedPath.c_str(), "rb");
if (err != 0 || !file) {
E2D_LOG_ERROR("ResourceManager: failed to open JSON file: {}", resolvedPath);
return "";
}
#else
file = fopen(resolvedPath.c_str(), "rb");
if (!file) {
E2D_LOG_ERROR("ResourceManager: failed to open JSON file: {}", resolvedPath);
return "";
}
#endif
// 获取文件大小
fseek(file, 0, SEEK_END);
long fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
if (fileSize <= 0) {
fclose(file);
E2D_LOG_WARN("ResourceManager: JSON file is empty: {}", resolvedPath);
return "";
}
// 读取文件内容
std::string content;
content.resize(fileSize);
size_t readSize = fread(&content[0], 1, fileSize, file);
fclose(file);
if (readSize != static_cast<size_t>(fileSize)) {
E2D_LOG_ERROR("ResourceManager: failed to read JSON file: {}", resolvedPath);
return "";
}
// 简单验证 JSON 格式(检查是否以 { 或 [ 开头)
size_t firstValid = content.find_first_not_of(" \t\n\r");
if (firstValid == std::string::npos ||
(content[firstValid] != '{' && content[firstValid] != '[')) {
E2D_LOG_WARN("ResourceManager: file may not be valid JSON: {}", filepath);
// 不阻止加载,只是警告
}
// 缓存内容
jsonFileCache_[filepath] = content;
E2D_LOG_DEBUG("ResourceManager: loaded JSON file: {} ({} bytes)", filepath, content.size());
return content;
}
std::string ResourceManager::getJsonFile(const std::string &key) const {
std::lock_guard<std::mutex> lock(jsonFileMutex_);
auto it = jsonFileCache_.find(key);
if (it != jsonFileCache_.end()) {
return it->second;
}
return "";
}
bool ResourceManager::hasJsonFile(const std::string &key) const {
std::lock_guard<std::mutex> lock(jsonFileMutex_);
return jsonFileCache_.find(key) != jsonFileCache_.end();
}
void ResourceManager::unloadJsonFile(const std::string &key) {
std::lock_guard<std::mutex> lock(jsonFileMutex_);
auto it = jsonFileCache_.find(key);
if (it != jsonFileCache_.end()) {
jsonFileCache_.erase(it);
E2D_LOG_DEBUG("ResourceManager: unloaded JSON file: {}", key);
}
}
void ResourceManager::clearJsonFileCache() {
std::lock_guard<std::mutex> lock(jsonFileMutex_);
size_t count = jsonFileCache_.size();
jsonFileCache_.clear();
E2D_LOG_INFO("ResourceManager: cleared {} JSON files from cache", count);
}
size_t ResourceManager::getJsonFileCacheSize() const {
std::lock_guard<std::mutex> lock(jsonFileMutex_);
return jsonFileCache_.size();
}
} // namespace extra2d

View File

@ -13,8 +13,7 @@ void Scene::setViewportSize(float width, float height) {
viewportSize_ = Size(width, height);
if (defaultCamera_) {
defaultCamera_->setViewport(0, width, height, 0);
}
if (camera_) {
} else if (camera_) {
camera_->setViewport(0, width, height, 0);
}
}
@ -119,7 +118,7 @@ std::vector<std::pair<Node *, Node *>> Scene::queryCollisions() const {
}
void Scene::collectRenderCommands(std::vector<RenderCommand> &commands,
int parentZOrder) {
int parentZOrder) {
if (!isVisible())
return;

View File

@ -0,0 +1,81 @@
// ============================================================================
// BaseScene.cpp - Flappy Bird 基础场景实现
// ============================================================================
#include "BaseScene.h"
#include <extra2d/utils/logger.h>
namespace flappybird {
BaseScene::BaseScene() {
// 设置背景颜色为黑色(窗口四周会显示这个颜色)
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
}
void BaseScene::onEnter() {
extra2d::Scene::onEnter();
// 计算并更新视口
updateViewport();
}
void BaseScene::updateViewport() {
auto &app = extra2d::Application::instance();
float windowWidth = static_cast<float>(app.window().getWidth());
float windowHeight = static_cast<float>(app.window().getHeight());
// 计算游戏内容在窗口中的居中位置
// 保持游戏原始宽高比,进行"黑边"适配
float scaleX = windowWidth / GAME_WIDTH;
float scaleY = windowHeight / GAME_HEIGHT;
// 使用较小的缩放比例,确保游戏内容完整显示在窗口中
float scale = std::min(scaleX, scaleY);
scaledGameWidth_ = GAME_WIDTH * scale;
scaledGameHeight_ = GAME_HEIGHT * scale;
// 计算居中偏移,使游戏内容在窗口中水平和垂直居中
viewportOffsetX_ = (windowWidth - scaledGameWidth_) * 0.5f;
viewportOffsetY_ = (windowHeight - scaledGameHeight_) * 0.5f;
// 设置视口大小为游戏逻辑分辨率
setViewportSize(GAME_WIDTH, GAME_HEIGHT);
// 创建并设置相机
auto camera = extra2d::makePtr<extra2d::Camera>();
// 设置正交投影,覆盖整个游戏逻辑区域
// 注意对于2D游戏Y轴向下增长所以bottom > top
camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f);
setCamera(camera);
}
void BaseScene::onRender(extra2d::RenderBackend &renderer) {
// 检查窗口大小是否改变,如果改变则更新视口
auto &app = extra2d::Application::instance();
float currentWindowWidth = static_cast<float>(app.window().getWidth());
float currentWindowHeight = static_cast<float>(app.window().getHeight());
// 如果窗口大小改变,重新计算视口
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
E2D_LOG_INFO("BaseScene::onRender - window size changed from ({} x {}) to "
"({} x {}), updating viewport",
expectedWidth, expectedHeight, currentWindowWidth,
currentWindowHeight);
updateViewport();
}
// 设置视口为居中区域
E2D_LOG_INFO(
"BaseScene::onRender - setting viewport: x={}, y={}, width={}, height={}",
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
renderer.setViewport(
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
// 调用父类的 onRender 进行实际渲染
extra2d::Scene::onRender(renderer);
}
} // namespace flappybird

View File

@ -0,0 +1,51 @@
// ============================================================================
// BaseScene.h - Flappy Bird 基础场景类
// 描述: 提供统一的居中视口适配功能,所有游戏场景都应继承此类
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
namespace flappybird {
// 游戏逻辑分辨率(原始 Flappy Bird 尺寸)
static constexpr float GAME_WIDTH = 288.0f;
static constexpr float GAME_HEIGHT = 512.0f;
/**
* @brief Flappy Bird
*
*/
class BaseScene : public extra2d::Scene {
public:
/**
* @brief
*/
BaseScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param renderer
*/
void onRender(extra2d::RenderBackend &renderer) override;
protected:
/**
* @brief 使
*/
void updateViewport();
// 视口适配参数(用于在窗口中居中显示游戏内容)
float scaledGameWidth_ = 0.0f; // 缩放后的游戏宽度
float scaledGameHeight_ = 0.0f; // 缩放后的游戏高度
float viewportOffsetX_ = 0.0f; // 视口水平偏移
float viewportOffsetY_ = 0.0f; // 视口垂直偏移
};
} // namespace flappybird

View File

@ -3,6 +3,7 @@
// ============================================================================
#include "GameOverLayer.h"
#include "BaseScene.h"
#include "GameScene.h"
#include "Number.h"
#include "ResLoader.h"
@ -19,9 +20,9 @@ void GameOverLayer::onEnter() {
Node::onEnter();
// 在 onEnter 中初始化,此时 weak_from_this() 可用
auto &app = extra2d::Application::instance();
float screenWidth = static_cast<float>(app.getConfig().width);
float screenHeight = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑分辨率
float screenWidth = GAME_WIDTH;
float screenHeight = GAME_HEIGHT;
// 整体居中x 坐标相对于屏幕中心)
setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight));
@ -45,6 +46,15 @@ void GameOverLayer::onEnter() {
// 创建向上移动的动画(从屏幕底部移动到正常位置)
auto moveAction = extra2d::makePtr<extra2d::MoveBy>(
1.0f, extra2d::Vec2(0.0f, -screenHeight));
moveAction->setCompletionCallback([this]() {
animationDone_ = true;
if (restartBtn_)
restartBtn_->setEnabled(true);
if (menuBtn_)
menuBtn_->setEnabled(true);
if (shareBtn_)
shareBtn_->setEnabled(true);
});
runAction(moveAction);
}
@ -107,63 +117,60 @@ void GameOverLayer::initPanel(int score, float screenHeight) {
}
void GameOverLayer::initButtons() {
// 创建重新开始按钮y=360
auto restartFrame = ResLoader::getKeyFrame("button_restart");
if (restartFrame) {
auto restartBtn = extra2d::Button::create();
restartBtn->setBackgroundImage(restartFrame->getTexture(),
restartFrame->getRect());
restartBtn->setAnchor(extra2d::Vec2(0.5f, 0.5f));
restartBtn->setPosition(
extra2d::Vec2(0.0f, 360.0f)); // x=0 表示相对于中心点
restartBtn->setOnClick([]() {
restartBtn_ = extra2d::Button::create();
restartBtn_->setBackgroundImage(restartFrame->getTexture(),
restartFrame->getRect());
restartBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
restartBtn_->setPosition(extra2d::Vec2(0.0f, 360.0f));
restartBtn_->setEnabled(false);
restartBtn_->setOnClick([]() {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
});
addChild(restartBtn);
addChild(restartBtn_);
}
// 创建返回主菜单按钮y=420
auto menuFrame = ResLoader::getKeyFrame("button_menu");
if (menuFrame) {
auto menuBtn = extra2d::Button::create();
menuBtn->setBackgroundImage(menuFrame->getTexture(), menuFrame->getRect());
menuBtn->setAnchor(extra2d::Vec2(0.5f, 0.5f));
menuBtn->setPosition(extra2d::Vec2(0.0f, 420.0f)); // x=0 表示相对于中心点
menuBtn->setOnClick([]() {
menuBtn_ = extra2d::Button::create();
menuBtn_->setBackgroundImage(menuFrame->getTexture(), menuFrame->getRect());
menuBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
menuBtn_->setPosition(extra2d::Vec2(0.0f, 420.0f));
menuBtn_->setEnabled(false);
menuBtn_->setOnClick([]() {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
});
addChild(menuBtn);
addChild(menuBtn_);
}
// 创建分享按钮y=480在 MENU 按钮下方)
auto shareFrame = ResLoader::getKeyFrame("button_share");
if (shareFrame) {
auto shareBtn = extra2d::Button::create();
shareBtn->setBackgroundImage(shareFrame->getTexture(),
shareFrame->getRect());
shareBtn->setAnchor(extra2d::Vec2(0.5f, 0.5f));
shareBtn->setPosition(extra2d::Vec2(0.0f, 460.0f)); // x=0 表示相对于中心点
shareBtn->setOnClick([]() {
ResLoader::playMusic(MusicType::Click);
// TODO: 实现分享功能
});
addChild(shareBtn);
shareBtn_ = extra2d::Button::create();
shareBtn_->setBackgroundImage(shareFrame->getTexture(),
shareFrame->getRect());
shareBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
shareBtn_->setPosition(extra2d::Vec2(0.0f, 460.0f));
shareBtn_->setEnabled(false);
shareBtn_->setOnClick([]() { ResLoader::playMusic(MusicType::Click); });
addChild(shareBtn_);
}
}
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
// 检测手柄按键
if (!animationDone_)
return;
auto &input = extra2d::Application::instance().input();
// A 键重新开始游戏
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
@ -171,7 +178,6 @@ void GameOverLayer::onUpdate(float dt) {
extra2d::TransitionType::Fade, 0.5f);
}
// B 键返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();

View File

@ -15,44 +15,48 @@ namespace flappybird {
*/
class GameOverLayer : public extra2d::Node {
public:
/**
* @brief
* @param score
*/
GameOverLayer(int score);
/**
* @brief
* @param score
*/
GameOverLayer(int score);
/**
* @brief
*/
void onEnter() override;
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
* @param score
* @param screenHeight
*/
void initPanel(int score, float screenHeight);
/**
* @brief
* @param score
* @param screenHeight
*/
void initPanel(int score, float screenHeight);
/**
* @brief
*/
void initButtons();
/**
* @brief
*/
void initButtons();
/**
* @brief
* @param score
* @return
*/
extra2d::Ptr<extra2d::SpriteFrame> getMedal(int score);
/**
* @brief
* @param score
* @return
*/
extra2d::Ptr<extra2d::SpriteFrame> getMedal(int score);
int score_ = 0; // 本局得分
int score_ = 0; // 本局得分
bool animationDone_ = false; // 动画是否完成
extra2d::Ptr<extra2d::Button> restartBtn_; // 重新开始按钮
extra2d::Ptr<extra2d::Button> menuBtn_; // 菜单按钮
extra2d::Ptr<extra2d::Button> shareBtn_; // 分享按钮
};
} // namespace flappybird

View File

@ -10,20 +10,15 @@
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));
// 基类 BaseScene 已经处理了视口设置和背景颜色
}
void GameScene::onEnter() {
extra2d::Scene::onEnter();
BaseScene::onEnter();
auto &app = extra2d::Application::instance();
float screenWidth = static_cast<float>(app.getConfig().width);
float screenHeight = static_cast<float>(app.getConfig().height);
// 游戏坐标系:使用游戏逻辑分辨率
float screenWidth = GAME_WIDTH;
float screenHeight = GAME_HEIGHT;
// 添加背景(使用左上角锚点,与原游戏一致)
auto bgFrame = ResLoader::getKeyFrame("bg_day");
@ -91,69 +86,52 @@ void GameScene::onEnter() {
}
void GameScene::onUpdate(float dt) {
// 注意:这里要先调用父类的 onUpdate以确保 GameOverLayer 的动画能播放
extra2d::Scene::onUpdate(dt);
if (!gameOver_) {
if (!bird_)
return;
// 游戏结束后不再更新游戏逻辑(但子节点的动画继续)
if (gameOver_)
return;
auto &input = extra2d::Application::instance().input();
if (!bird_)
return;
auto &input = extra2d::Application::instance().input();
// 检测跳跃按键A键或空格
if (input.isButtonPressed(extra2d::GamepadButton::A) ||
input.isMousePressed(extra2d::MouseButton::Left)) {
if (!started_) {
// 游戏还没开始,开始游戏
started_ = true;
startGame();
if (input.isButtonPressed(extra2d::GamepadButton::A) ||
input.isMousePressed(extra2d::MouseButton::Left)) {
if (!started_) {
started_ = true;
startGame();
}
bird_->jump();
}
bird_->jump();
}
// 游戏已经开始
if (started_) {
// 模拟小鸟下落
bird_->fall(dt);
if (started_) {
bird_->fall(dt);
// 检查得分(小鸟飞过水管)
if (pipes_) {
Pipe *firstPipe = pipes_->getPipe(0);
if (firstPipe && !firstPipe->scored) {
float birdX = bird_->getPosition().x;
float pipeX = firstPipe->getPosition().x;
if (pipeX <= birdX) {
// 小鸟飞过了水管
score_++;
scoreNumber_->setNumber(score_);
firstPipe->scored = true;
ResLoader::playMusic(MusicType::Point);
if (pipes_) {
Pipe *firstPipe = pipes_->getPipe(0);
if (firstPipe && !firstPipe->scored) {
float birdX = bird_->getPosition().x;
float pipeX = firstPipe->getPosition().x;
if (pipeX <= birdX) {
score_++;
scoreNumber_->setNumber(score_);
firstPipe->scored = true;
ResLoader::playMusic(MusicType::Point);
}
}
}
}
// 检查碰撞
if (bird_->isLiving() && checkCollision()) {
onHit();
}
if (bird_->isLiving() && checkCollision()) {
onHit();
}
// 检查是否撞到地面(原游戏使用 123 作为地面高度)
auto &app = extra2d::Application::instance();
float screenHeight = static_cast<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();
if (bird_->isLiving() && GAME_HEIGHT - bird_->getPosition().y <= 123.0f) {
bird_->setPosition(
extra2d::Vec2(bird_->getPosition().x, GAME_HEIGHT - 123.0f));
bird_->setStatus(Bird::Status::Still);
onHit();
}
}
}
BaseScene::onUpdate(dt);
}
void GameScene::startGame() {
@ -232,18 +210,16 @@ void GameScene::onHit() {
scoreNumber_->setVisible(false);
}
// 设置游戏结束标志
gameOver_ = true;
// 延迟显示游戏结束界面
gameOver();
}
void GameScene::gameOver() {
if (gameOver_)
return;
started_ = false;
gameOver_ = true;
// 显示游戏结束层
auto gameOverLayer = extra2d::makePtr<GameOverLayer>(score_);
addChild(gameOverLayer);
}

View File

@ -5,7 +5,7 @@
#pragma once
#include <extra2d/extra2d.h>
#include "BaseScene.h"
#include "Bird.h"
#include "Pipes.h"
#include "Ground.h"
@ -17,7 +17,7 @@ namespace flappybird {
* @brief
*
*/
class GameScene : public extra2d::Scene {
class GameScene : public BaseScene {
public:
/**
* @brief

View File

@ -4,7 +4,6 @@
#include "ResLoader.h"
#include <json/json.hpp>
#include <fstream>
namespace flappybird {
@ -13,84 +12,88 @@ 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();
auto &resources = extra2d::Application::instance().resources();
// 加载图集纹理
atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
if (!atlasTexture_) {
E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
return;
// 加载图集纹理
atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
if (!atlasTexture_) {
E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
return;
}
// 使用资源管理器加载 JSON 文件
std::string jsonContent = resources.loadJsonFile("assets/images/atlas.json");
if (jsonContent.empty()) {
E2D_LOG_ERROR("无法加载 atlas.json 文件");
return;
}
// 解析 JSON 图集数据
try {
nlohmann::json jsonData = nlohmann::json::parse(jsonContent);
for (const auto &sprite : jsonData["sprites"]) {
std::string name = sprite["name"];
float x = sprite["x"];
float y = sprite["y"];
float width = sprite["width"];
float height = sprite["height"];
ImageInfo info = {width, height, x, y};
imageMap_[name] = info;
}
// 读取 atlas.json 文件
std::ifstream file("assets/images/atlas.json");
if (!file.is_open()) {
E2D_LOG_ERROR("无法打开 atlas.json 文件");
return;
}
E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size());
} catch (const std::exception &e) {
E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what());
return;
}
// 解析 JSON 图集数据
try {
nlohmann::json jsonData;
file >> jsonData;
// 加载音效
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");
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("资源加载完成");
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;
}
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);
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());
}
// 检查纹理尺寸
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)
);
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();
}
auto it = soundMap_.find(type);
if (it == soundMap_.end()) {
E2D_LOG_WARN("ResLoader::playMusic: sound type not found");
return;
}
if (!it->second) {
E2D_LOG_WARN("ResLoader::playMusic: sound pointer is null");
return;
}
if (!it->second->play()) {
E2D_LOG_WARN("ResLoader::playMusic: failed to play sound");
}
}
} // namespace flappybird

View File

@ -3,67 +3,47 @@
// ============================================================================
#include "SplashScene.h"
#include "StartScene.h"
#include "ResLoader.h"
#include "StartScene.h"
#include <extra2d/utils/logger.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));
// 基类 BaseScene 已经处理了视口设置和背景颜色
}
void SplashScene::onEnter() {
extra2d::Scene::onEnter();
BaseScene::onEnter();
// 设置黑色背景
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
auto viewport = getViewportSize();
float centerX = viewport.width / 2.0f;
float centerY = viewport.height / 2.0f;
// 尝试加载 splash 图片
auto splashFrame = ResLoader::getKeyFrame("splash");
if (splashFrame) {
auto splash = extra2d::Sprite::create(splashFrame->getTexture(), splashFrame->getRect());
splash->setAnchor(0.5f, 0.5f);
splash->setPosition(centerX, centerY);
addChild(splash);
} else {
// 如果 splash 加载失败,尝试加载 title 图片作为备用
auto titleFrame = ResLoader::getKeyFrame("title");
if (titleFrame) {
auto title = extra2d::Sprite::create(titleFrame->getTexture(), titleFrame->getRect());
title->setAnchor(0.5f, 0.5f);
title->setPosition(centerX, centerY);
addChild(title);
}
}
// 播放转场音效
ResLoader::playMusic(MusicType::Swoosh);
// 尝试加载 splash 图片
auto splashFrame = ResLoader::getKeyFrame("splash");
if (splashFrame) {
auto splash = extra2d::Sprite::create(splashFrame->getTexture(),
splashFrame->getRect());
splash->setAnchor(0.5f, 0.5f);
// splash 图片是全屏的(288x512),将其中心放在游戏区域中心
splash->setPosition(GAME_WIDTH / 2.0f, GAME_HEIGHT / 2.0f);
addChild(splash);
}
// 播放转场音效
ResLoader::playMusic(MusicType::Swoosh);
}
void SplashScene::onUpdate(float dt) {
extra2d::Scene::onUpdate(dt);
BaseScene::onUpdate(dt);
// 计时
timer_ += dt;
if (timer_ >= delay_) {
gotoStartScene();
}
// 计时
timer_ += dt;
if (timer_ >= delay_) {
gotoStartScene();
}
}
void SplashScene::gotoStartScene() {
auto& app = extra2d::Application::instance();
app.scenes().replaceScene(
extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade,
0.5f
);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
} // namespace flappybird

View File

@ -5,7 +5,7 @@
#pragma once
#include <extra2d/extra2d.h>
#include "BaseScene.h"
namespace flappybird {
@ -13,32 +13,32 @@ namespace flappybird {
* @brief
* Logo
*/
class SplashScene : public extra2d::Scene {
class SplashScene : public BaseScene {
public:
/**
* @brief
*/
SplashScene();
/**
* @brief
*/
SplashScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void gotoStartScene();
/**
* @brief
*/
void gotoStartScene();
float timer_ = 0.0f; // 计时器
const float delay_ = 2.0f; // 延迟时间(秒)
float timer_ = 0.0f; // 计时器
const float delay_ = 2.0f; // 延迟时间(秒)
};
} // namespace flappybird

View File

@ -12,21 +12,15 @@
namespace flappybird {
StartScene::StartScene() {
auto &app = extra2d::Application::instance();
auto &config = app.getConfig();
setViewportSize(static_cast<float>(config.width),
static_cast<float>(config.height));
// 基类 BaseScene 已经处理了视口设置和背景颜色
}
void StartScene::onEnter() {
extra2d::Scene::onEnter();
BaseScene::onEnter();
// 设置背景颜色为黑色(防止透明)
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
auto &app = extra2d::Application::instance();
float screenWidth = static_cast<float>(app.getConfig().width);
float screenHeight = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑分辨率
float screenWidth = GAME_WIDTH;
float screenHeight = GAME_HEIGHT;
// 添加背景(使用左上角锚点)
auto bgFrame = ResLoader::getKeyFrame("bg_day");
@ -125,7 +119,7 @@ void StartScene::onEnter() {
}
void StartScene::onUpdate(float dt) {
extra2d::Scene::onUpdate(dt);
BaseScene::onUpdate(dt);
// 检测 A 键或空格开始游戏
auto &input = extra2d::Application::instance().input();

View File

@ -5,7 +5,7 @@
#pragma once
#include <extra2d/extra2d.h>
#include "BaseScene.h"
namespace flappybird {
@ -13,7 +13,7 @@ namespace flappybird {
* @brief
*
*/
class StartScene : public extra2d::Scene {
class StartScene : public BaseScene {
public:
/**
* @brief

View File

@ -4,14 +4,15 @@
#include "Ground.h"
#include "ResLoader.h"
#include "BaseScene.h"
namespace flappybird {
Ground::Ground() {
moving_ = true;
auto& app = extra2d::Application::instance();
float screenHeight = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑高度,而不是窗口高度
float screenHeight = GAME_HEIGHT;
// 获取地面纹理帧
auto landFrame = ResLoader::getKeyFrame("land");

View File

@ -4,51 +4,50 @@
// 描述: 经典的 Flappy Bird 游戏实现
// ============================================================================
#include <extra2d/extra2d.h>
#include "SplashScene.h"
#include "ResLoader.h"
#include "SplashScene.h"
#include <extra2d/extra2d.h>
using namespace extra2d;
/**
* @brief
*/
int main(int argc, char **argv)
{
// 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
int main(int argc, char **argv) {
// 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Extra2D FlappyBird");
E2D_LOG_INFO("========================");
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Extra2D FlappyBird");
E2D_LOG_INFO("========================");
// 获取应用实例
auto &app = Application::instance();
// 获取应用实例
auto &app = Application::instance();
// 配置应用
AppConfig config;
config.title = "Extra2D - FlappyBird";
config.width = 288; // 原始游戏宽度
config.height = 512; // 原始游戏高度
config.vsync = true;
config.fpsLimit = 60;
// 配置应用
AppConfig config;
config.title = "Extra2D - FlappyBird";
config.width = 1280; // 窗口宽度 (720P 分辨率)
config.height = 720; // 窗口高度 (720P 分辨率)
config.vsync = true;
config.fpsLimit = 60;
// 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 初始化资源加载器
flappybird::ResLoader::init();
// 初始化资源加载器
flappybird::ResLoader::init();
// 进入启动场景
app.enterScene(makePtr<flappybird::SplashScene>());
// 进入启动场景
app.enterScene(makePtr<flappybird::SplashScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
E2D_LOG_INFO("应用结束");
return 0;
}

View File

@ -4,6 +4,7 @@
#include "Pipe.h"
#include "ResLoader.h"
#include "BaseScene.h"
namespace flappybird {
@ -18,8 +19,8 @@ void Pipe::onEnter() {
// 在 onEnter 中创建子节点,此时 weak_from_this() 可用
if (!topPipe_ && !bottomPipe_) {
auto& app = extra2d::Application::instance();
float screenHeight = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑高度
float screenHeight = GAME_HEIGHT;
// 获取地面高度
auto landFrame = ResLoader::getKeyFrame("land");
@ -62,8 +63,8 @@ extra2d::Rect Pipe::getBoundingBox() const {
float pipeWidth = 52.0f;
float halfWidth = pipeWidth / 2.0f;
auto& app = extra2d::Application::instance();
float screenHeight = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑高度
float screenHeight = GAME_HEIGHT;
return extra2d::Rect(
pos.x - halfWidth,

View File

@ -3,6 +3,7 @@
// ============================================================================
#include "Pipes.h"
#include "BaseScene.h"
namespace flappybird {
@ -76,9 +77,8 @@ void Pipes::addPipe() {
// 设置水管位置
if (pipeCount_ == 0) {
// 第一个水管在屏幕外 130 像素处
auto& app = extra2d::Application::instance();
pipe->setPosition(extra2d::Vec2(
static_cast<float>(app.getConfig().width) + 130.0f,
GAME_WIDTH + 130.0f,
0.0f
));
} else {