feat(场景): 添加BaseScene作为统一基础场景类
重构所有场景类继承自BaseScene,提供统一的视口适配功能 使用游戏逻辑分辨率(GAME_WIDTH/GAME_HEIGHT)替代直接获取窗口尺寸 优化资源加载和音效播放的错误处理
This commit is contained in:
parent
1b72a1c992
commit
3a9b44cbfe
|
|
@ -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缓存
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 构造函数
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 构造函数
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue