feat: 添加窗口和输入模块支持并简化示例程序

重构应用类以支持窗口和输入模块,添加相关接口方法
移除碰撞演示示例,简化hello world示例为控制台输出
启用控制台子系统以便在mingw平台查看输出
This commit is contained in:
ChestnutYueyue 2026-02-28 22:30:48 +08:00
parent 418d2c8f92
commit ebf73a4492
8 changed files with 111 additions and 474 deletions

View File

@ -1,280 +0,0 @@
#include <cmath>
#include <extra2d.h>
using namespace extra2d;
// ============================================================================
// 碰撞测试节点 - 有实际边界框
// ============================================================================
class CollisionBox : public Node {
public:
CollisionBox(float width, float height, const Color &color)
: width_(width), height_(height), color_(color), isColliding_(false) {
// 启用空间索引,这是碰撞检测的关键
setSpatialIndexed(true);
}
void setColliding(bool colliding) { isColliding_ = colliding; }
Rect boundingBox() const override {
// 返回实际的矩形边界
Vec2 position = pos();
return Rect(position.x - width_ / 2, position.y - height_ / 2, width_,
height_);
}
void onRender(Renderer &renderer) override {
Vec2 position = pos();
// 绘制填充矩形
Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.8f) : color_;
renderer.fillRect(Rect(position.x - width_ / 2, position.y - height_ / 2,
width_, height_),
fillColor);
// 绘制边框
Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f)
: Color(1.0f, 1.0f, 1.0f, 0.5f);
float borderWidth = isColliding_ ? 3.0f : 2.0f;
renderer.drawRect(Rect(position.x - width_ / 2, position.y - height_ / 2,
width_, height_),
borderColor, borderWidth);
}
private:
float width_, height_;
Color color_;
bool isColliding_;
};
// ============================================================================
// 碰撞检测场景
// ============================================================================
class CollisionDemoScene : public Scene {
public:
void onEnter() override {
E2D_LOG_INFO("CollisionDemoScene::onEnter - 碰撞检测演示");
// 设置背景色
setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f));
// 获取屏幕中心
auto &app = Application::instance();
float centerX = app.getConfig().width / 2.0f;
float centerY = app.getConfig().height / 2.0f;
// 创建静态碰撞框
createStaticBoxes(centerX, centerY);
// 创建移动的中心方块
centerBox_ =
shared<CollisionBox>(80.0f, 80.0f, Color(0.2f, 0.6f, 1.0f, 0.8f));
centerBox_->setPosition(Vec2(centerX, centerY));
addChild(centerBox_);
// 加载字体并创建UI
loadFonts();
E2D_LOG_INFO("创建了 {} 个碰撞框", boxes_.size() + 1);
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 旋转中心方块
rotationAngle_ += rotationSpeed_ * dt;
if (rotationAngle_ >= 360.0f)
rotationAngle_ -= 360.0f;
// 让中心方块沿圆形路径移动
float radius = 150.0f;
float rad = rotationAngle_ * 3.14159f / 180.0f;
auto &app = Application::instance();
Vec2 center =
Vec2(app.getConfig().width / 2.0f, app.getConfig().height / 2.0f);
centerBox_->setPosition(Vec2(center.x + std::cos(rad) * radius,
center.y + std::sin(rad) * radius));
centerBox_->setRotation(rotationAngle_);
// 执行碰撞检测
performCollisionDetection();
// 更新UI文本
updateUI();
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用");
Application::instance().quit();
}
}
private:
/**
* @brief UI文本
*/
void loadFonts() {
auto &resources = Application::instance().resources();
titleFont_ = resources.loadFont("assets/font.ttf", 60, true);
infoFont_ = resources.loadFont("assets/font.ttf", 28, true);
// 创建标题文本
titleText_ = Text::create("碰撞检测演示", titleFont_);
titleText_->setPosition(50.0f, 30.0f);
titleText_->setTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f));
addChild(titleText_);
// 创建说明文本
descText_ = Text::create("蓝色方块旋转并检测碰撞", infoFont_);
descText_->setPosition(50.0f, 80.0f);
descText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(descText_);
collideHintText_ = Text::create("红色 = 检测到碰撞", infoFont_);
collideHintText_->setPosition(50.0f, 105.0f);
collideHintText_->setTextColor(Color(1.0f, 0.5f, 0.5f, 1.0f));
addChild(collideHintText_);
// 创建动态统计文本
collisionText_ = Text::create("", infoFont_);
collisionText_->setPosition(50.0f, 150.0f);
collisionText_->setTextColor(Color(1.0f, 1.0f, 0.5f, 1.0f));
addChild(collisionText_);
fpsText_ = Text::create("", infoFont_);
fpsText_->setPosition(50.0f, 175.0f);
fpsText_->setTextColor(Color(0.8f, 1.0f, 0.8f, 1.0f));
addChild(fpsText_);
// 创建退出提示文本
float screenHeight =
static_cast<float>(Application::instance().getConfig().height);
exitHintText_ = Text::create("按 + 键退出", infoFont_);
exitHintText_->setPosition(50.0f, screenHeight - 50.0f);
exitHintText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(exitHintText_);
}
/**
* @brief UI文本
*/
void updateUI() {
auto &app = Application::instance();
// 使用 setFormat 更新动态文本
collisionText_->setFormat("碰撞数: %zu", collisionCount_);
fpsText_->setFormat("FPS: %u", app.fps());
}
/**
* @brief
*/
void createStaticBoxes(float centerX, float centerY) {
// 创建围绕中心的静态碰撞框
std::vector<std::pair<Vec2, Color>> positions = {
{Vec2(centerX - 200, centerY - 150), Color(0.3f, 1.0f, 0.3f, 0.7f)},
{Vec2(centerX + 200, centerY - 150), Color(1.0f, 0.3f, 0.3f, 0.7f)},
{Vec2(centerX - 200, centerY + 150), Color(0.3f, 0.3f, 1.0f, 0.7f)},
{Vec2(centerX + 200, centerY + 150), Color(1.0f, 1.0f, 0.3f, 0.7f)},
{Vec2(centerX, centerY - 220), Color(1.0f, 0.3f, 1.0f, 0.7f)},
{Vec2(centerX, centerY + 220), Color(0.3f, 1.0f, 1.0f, 0.7f)},
};
for (const auto &[pos, color] : positions) {
auto box = shared<CollisionBox>(70.0f, 70.0f, color);
box->setPosition(pos);
addChild(box);
boxes_.push_back(box);
}
}
/**
* @brief
*/
void performCollisionDetection() {
// 清除之前的碰撞状态
centerBox_->setColliding(false);
for (auto &box : boxes_) {
box->setColliding(false);
}
// 使用空间索引进行碰撞检测
auto collisions = queryCollisions();
collisionCount_ = collisions.size();
// 标记碰撞的节点
for (const auto &[nodeA, nodeB] : collisions) {
if (auto boxA = dynamic_cast<CollisionBox *>(nodeA)) {
boxA->setColliding(true);
}
if (auto boxB = dynamic_cast<CollisionBox *>(nodeB)) {
boxB->setColliding(true);
}
}
}
Ptr<CollisionBox> centerBox_;
std::vector<Ptr<CollisionBox>> boxes_;
float rotationAngle_ = 0.0f;
float rotationSpeed_ = 60.0f; // 旋转速度(度/秒)
size_t collisionCount_ = 0;
// 字体资源
Ptr<FontAtlas> titleFont_;
Ptr<FontAtlas> infoFont_;
// UI 文本组件
Ptr<Text> titleText_;
Ptr<Text> descText_;
Ptr<Text> collideHintText_;
Ptr<Text> collisionText_;
Ptr<Text> fpsText_;
Ptr<Text> exitHintText_;
};
// ============================================================================
// 程序入口
// ============================================================================
int main(int argc, char **argv) {
// 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Easy2D 碰撞检测演示");
E2D_LOG_INFO("========================");
// 获取应用实例
auto &app = Application::instance();
// 配置应用
AppConfig config;
config.title = "Easy2D - 碰撞检测演示";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
// 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 进入场景
app.enterScene(shared<CollisionDemoScene>());
E2D_LOG_INFO("开始主循环...");
// 运行应用
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

View File

@ -1,76 +0,0 @@
-- ==============================================
-- Collision Demo 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("collision_demo")
set_kind("binary")
add_files("main.cpp")
add_includedirs("../../include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_targetdir("../../build/examples/collision_demo")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "collision_demo.nacp")
local nro_file = path.join(output_dir, "collision_demo.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "Collision Demo", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "collision_demo.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_targetdir("../../build/examples/collision_demo")
add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
end
target_end()

View File

@ -1,124 +1,27 @@
#include <extra2d.h>
#include <cstdio>
using namespace extra2d;
// ============================================================================
// Hello World 场景
// ============================================================================
/**
* @brief Hello World
* "Hello World"
*/
class HelloWorldScene : public Scene {
public:
/**
* @brief
*/
void onEnter() override {
E2D_LOG_INFO("HelloWorldScene::onEnter - 进入场景");
// 设置背景颜色为深蓝色
setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f));
// 加载字体(支持多种字体后备)
auto &resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 48, true);
if (!font_) {
E2D_LOG_ERROR("字体加载失败,文字渲染将不可用!");
return;
}
// 创建 "你好世界" 文本组件 - 使用节点位置
auto text1 = Text::create("你好世界", font_);
text1->setPosition(640.0f, 360.0f); // 屏幕中心
text1->setAnchor(0.5f, 0.5f); // 中心锚点,让文字中心对准位置
text1->setTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f));
addChild(text1);
// 创建提示文本组件 - 固定在屏幕底部
auto text2 = Text::create("退出按键START 按钮)", font_);
text2->setPosition(640.0f, 650.0f); // 屏幕底部
text2->setAnchor(0.5f, 0.5f);
text2->setTextColor(Color(1.0f, 1.0f, 0.0f, 1.0f));
addChild(text2);
// 创建带偏移的文本 - 使用相对位置
auto text3 = Text::create("偏移文本", font_);
text3->setPosition(50.0f, 50.0f); // 左上角偏移
text3->setAnchor(0.0f, 0.0f); // 左上角锚点,文字从指定位置开始显示
text3->setTextColor(Color(0.0f, 1.0f, 1.0f, 1.0f));
addChild(text3);
// 创建世界空间文本 - 随相机移动(默认行为)
auto text4 = Text::create("世界空间文本", font_);
text4->setPosition(100.0f, 100.0f); // 世界坐标
text4->setAnchor(0.0f, 0.0f); // 左上角锚点,文字从指定位置开始显示
text4->setTextColor(Color(1.0f, 0.5f, 0.5f, 1.0f));
addChild(text4);
}
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 检查退出按键
auto &input = Application::instance().input();
// 使用手柄 START 按钮退出 (GamepadButton::Start)
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用 (START 按钮)");
Application::instance().quit();
}
}
private:
Ptr<FontAtlas> font_; // 字体图集
};
// ============================================================================
// 程序入口
// ============================================================================
int main(int argc, char **argv) {
// 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Easy2D Hello World Demo");
E2D_LOG_INFO("========================");
// 获取应用实例
auto &app = Application::instance();
// 配置应用
// 创建应用(自动创建窗口)
auto app = Application::create();
AppConfig config;
config.title = "Easy2D - Hello World";
config.title = "Hello World";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
// 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
if (!app->init(config)) {
printf("Failed to initialize!\n");
return -1;
}
// 进入 Hello World 场景
app.enterScene(shared<HelloWorldScene>());
E2D_LOG_INFO("开始主循环...");
printf("Window: %dx%d\n", app->getWindowWidth(), app->getWindowHeight());
// 运行应用
app.run();
E2D_LOG_INFO("应用结束");
app->run();
// 自动销毁
return 0;
}

View File

@ -51,7 +51,8 @@ target("hello_world")
elseif is_plat("mingw") then
set_targetdir("../../build/examples/hello_world")
add_ldflags("-mwindows", {force = true})
-- 使用控制台子系统以便查看输出
-- add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)

View File

@ -8,6 +8,8 @@ namespace extra2d {
// 前向声明
class Context;
class WindowModule;
class InputModule;
/**
* @brief
@ -112,6 +114,31 @@ public:
*/
Context* getContext() const { return context_.get(); }
/**
* @brief
*/
WindowModule* getWindow() const { return windowModule_.get(); }
/**
* @brief
*/
InputModule* getInput() const { return inputModule_.get(); }
/**
* @brief
*/
int32 getWindowWidth() const;
/**
* @brief
*/
int32 getWindowHeight() const;
/**
* @brief
*/
const char* getWindowTitle() const;
private:
Application();
@ -119,6 +146,8 @@ private:
void update();
std::unique_ptr<Context> context_;
std::unique_ptr<WindowModule> windowModule_;
std::unique_ptr<InputModule> inputModule_;
AppConfig config_;

View File

@ -2,6 +2,8 @@
#include <context/context.h>
#include <event/events.h>
#include <platform/sdl2.h>
#include <platform/window.h>
#include <platform/input.h>
#include <utils/logger.h>
#include <chrono>
@ -84,6 +86,27 @@ bool Application::init(const AppConfig &config) {
return false;
}
// 创建窗口模块
windowModule_ = std::make_unique<WindowModule>();
WindowCfg wcfg;
wcfg.title = config_.title;
wcfg.width = config_.width;
wcfg.height = config_.height;
wcfg.fullscreen = config_.fullscreen;
wcfg.resizable = config_.resizable;
wcfg.vsync = config_.vsync;
wcfg.glMajor = config_.glMajor;
wcfg.glMinor = config_.glMinor;
if (!windowModule_->create(wcfg)) {
E2D_LOG_ERROR("Failed to create window");
return false;
}
windowModule_->setVisible(true);
// 创建输入模块
inputModule_ = std::make_unique<InputModule>();
initialized_ = true;
running_ = true;
@ -103,7 +126,11 @@ void Application::shutdown() {
E2D_LOG_INFO("Shutting down application...");
// 关闭上下文(会自动关闭模块和插件)
// 智能指针自动销毁窗口和输入模块
inputModule_.reset();
windowModule_.reset();
// 关闭上下文
context_.reset();
Sdl2::shutdown();
@ -155,6 +182,15 @@ void Application::resume() {
}
void Application::mainLoop() {
// 处理窗口事件
if (windowModule_) {
if (!windowModule_->pollEvents()) {
// 窗口关闭事件
quit();
return;
}
}
double currentTime = getTimeSeconds();
deltaTime_ = static_cast<float>(currentTime - lastFrameTime_);
lastFrameTime_ = currentTime;
@ -173,6 +209,11 @@ void Application::mainLoop() {
update();
}
// 交换缓冲区
if (windowModule_) {
windowModule_->swapBuffers();
}
// 帧率限制
if (!config_.vsync && config_.fpsLimit > 0) {
double frameEndTime = getTimeSeconds();
@ -192,4 +233,24 @@ void Application::update() {
}
}
int32 Application::getWindowWidth() const {
if (windowModule_) {
Size size = windowModule_->getSize();
return static_cast<int32>(size.w);
}
return config_.width;
}
int32 Application::getWindowHeight() const {
if (windowModule_) {
Size size = windowModule_->getSize();
return static_cast<int32>(size.h);
}
return config_.height;
}
const char* Application::getWindowTitle() const {
return config_.title.c_str();
}
} // namespace extra2d

View File

@ -91,11 +91,10 @@ includes("xmake/engine.lua")
-- 定义引擎库
define_extra2d_engine()
-- -- 示例程序目标(作为子项目)
-- if is_config("examples","true") then
-- includes("examples/hello_world", {rootdir = "examples/hello_world"})
-- includes("examples/collision_demo", {rootdir = "examples/collision_demo"})
-- end
-- 示例程序目标(作为子项目)
if is_config("examples","true") then
includes("examples/hello_world", {rootdir = "examples/hello_world"})
end
-- 测试套件目标(作为子项目)
if is_config("tests","true") then