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); 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 getTextureCacheSize() const;
size_t getFontCacheSize() const; size_t getFontCacheSize() const;
size_t getSoundCacheSize() const; size_t getSoundCacheSize() const;
size_t getTextFileCacheSize() const;
size_t getJsonFileCacheSize() const;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// LRU 缓存管理 // LRU 缓存管理
@ -213,11 +264,17 @@ private:
mutable std::mutex textureMutex_; mutable std::mutex textureMutex_;
mutable std::mutex fontMutex_; mutable std::mutex fontMutex_;
mutable std::mutex soundMutex_; 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<FontAtlas>> fontCache_;
std::unordered_map<std::string, WeakPtr<Sound>> soundCache_; 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缓存 // 纹理LRU缓存
// ============================================================================ // ============================================================================

View File

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

View File

@ -856,4 +856,220 @@ size_t ResourceManager::getSoundCacheSize() const {
return soundCache_.size(); 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 } // namespace extra2d

View File

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

View File

@ -15,44 +15,48 @@ namespace flappybird {
*/ */
class GameOverLayer : public extra2d::Node { class GameOverLayer : public extra2d::Node {
public: public:
/** /**
* @brief * @brief
* @param score * @param score
*/ */
GameOverLayer(int score); GameOverLayer(int score);
/** /**
* @brief * @brief
*/ */
void onEnter() override; void onEnter() override;
/** /**
* @brief * @brief
* @param dt * @param dt
*/ */
void onUpdate(float dt) override; void onUpdate(float dt) override;
private: private:
/** /**
* @brief * @brief
* @param score * @param score
* @param screenHeight * @param screenHeight
*/ */
void initPanel(int score, float screenHeight); void initPanel(int score, float screenHeight);
/** /**
* @brief * @brief
*/ */
void initButtons(); void initButtons();
/** /**
* @brief * @brief
* @param score * @param score
* @return * @return
*/ */
extra2d::Ptr<extra2d::SpriteFrame> getMedal(int score); 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 } // namespace flappybird

View File

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

View File

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

View File

@ -4,7 +4,6 @@
#include "ResLoader.h" #include "ResLoader.h"
#include <json/json.hpp> #include <json/json.hpp>
#include <fstream>
namespace flappybird { namespace flappybird {
@ -13,84 +12,88 @@ std::map<std::string, ResLoader::ImageInfo> ResLoader::imageMap_;
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_; std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
void ResLoader::init() { void ResLoader::init() {
auto& resources = extra2d::Application::instance().resources(); auto &resources = extra2d::Application::instance().resources();
// 加载图集纹理 // 加载图集纹理
atlasTexture_ = resources.loadTexture("assets/images/atlas.png"); atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
if (!atlasTexture_) { if (!atlasTexture_) {
E2D_LOG_ERROR("无法加载图集纹理 atlas.png"); E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
return; 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 文件 E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size());
std::ifstream file("assets/images/atlas.json"); } catch (const std::exception &e) {
if (!file.is_open()) { E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what());
E2D_LOG_ERROR("无法打开 atlas.json 文件"); return;
return; }
}
// 解析 JSON 图集数据 // 加载音效
try { soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav");
nlohmann::json jsonData; soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav");
file >> jsonData; 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"]) { E2D_LOG_INFO("资源加载完成");
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) { extra2d::Ptr<extra2d::SpriteFrame>
auto it = imageMap_.find(name); ResLoader::getKeyFrame(const std::string &name) {
if (it == imageMap_.end()) { auto it = imageMap_.find(name);
E2D_LOG_WARN("找不到精灵帧: %s", name.c_str()); if (it == imageMap_.end()) {
return nullptr; E2D_LOG_WARN("找不到精灵帧: %s", name.c_str());
} return nullptr;
}
const ImageInfo& info = it->second; const ImageInfo &info = it->second;
E2D_LOG_INFO("加载精灵帧: name={}, w={}, h={}, x={}, y={}", E2D_LOG_INFO("加载精灵帧: name={}, w={}, h={}, x={}, y={}", name, info.width,
name, info.width, info.height, info.x, info.y); info.height, info.x, info.y);
// 检查纹理尺寸 // 检查纹理尺寸
if (atlasTexture_) { if (atlasTexture_) {
E2D_LOG_INFO("图集纹理尺寸: {}x{}", atlasTexture_->getWidth(), atlasTexture_->getHeight()); E2D_LOG_INFO("图集纹理尺寸: {}x{}", atlasTexture_->getWidth(),
} atlasTexture_->getHeight());
}
return extra2d::makePtr<extra2d::SpriteFrame>( return extra2d::makePtr<extra2d::SpriteFrame>(
atlasTexture_, atlasTexture_, extra2d::Rect(info.x, info.y, info.width, info.height));
extra2d::Rect(info.x, info.y, info.width, info.height)
);
} }
void ResLoader::playMusic(MusicType type) { void ResLoader::playMusic(MusicType type) {
auto it = soundMap_.find(type); auto it = soundMap_.find(type);
if (it != soundMap_.end() && it->second) { if (it == soundMap_.end()) {
it->second->play(); 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 } // namespace flappybird

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
#include "Pipe.h" #include "Pipe.h"
#include "ResLoader.h" #include "ResLoader.h"
#include "BaseScene.h"
namespace flappybird { namespace flappybird {
@ -18,8 +19,8 @@ void Pipe::onEnter() {
// 在 onEnter 中创建子节点,此时 weak_from_this() 可用 // 在 onEnter 中创建子节点,此时 weak_from_this() 可用
if (!topPipe_ && !bottomPipe_) { 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"); auto landFrame = ResLoader::getKeyFrame("land");
@ -62,8 +63,8 @@ extra2d::Rect Pipe::getBoundingBox() const {
float pipeWidth = 52.0f; float pipeWidth = 52.0f;
float halfWidth = pipeWidth / 2.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( return extra2d::Rect(
pos.x - halfWidth, pos.x - halfWidth,

View File

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