docs: 更新多个API教程文档并添加示例代码

为多个API教程文档添加详细示例代码和说明,包括:
- 音频系统:添加Flappy Bird音效管理器实现
- 输入处理:补充鼠标输入检测和游戏输入处理示例
- UI系统:增加按钮启用/禁用功能及动画控制示例
- 节点系统:完善动画系统文档和示例
- 资源管理:添加精灵帧和资源加载器实现
- 场景系统:补充视口适配方案和基础场景类实现
This commit is contained in:
ChestnutYueyue 2026-02-13 08:56:27 +08:00
parent 3a9b44cbfe
commit 4107e0b12b
6 changed files with 779 additions and 4 deletions

View File

@ -1,14 +1,197 @@
# 02. 场景系统 # 02. 场景系统
Extra2D 的场景系统提供了游戏内容的分层管理和切换功能。本教程将详细介绍场景的生命周期、切换和过渡效果。 Extra2D 的场景系统提供了游戏内容的分层管理和切换功能。本教程将详细介绍场景的生命周期、切换和过渡效果,以及视口适配功能
## 完整示例 ## 完整示例
参考 `examples/push_box/` 中的实现: 参考 `examples/flappy_bird/` 中的实现:
- `BaseScene.h/cpp` - 基础场景类(视口适配)
- `StartScene.h/cpp` - 开始菜单场景 - `StartScene.h/cpp` - 开始菜单场景
- `PlayScene.h/cpp` - 游戏主场景 - `GameScene.h/cpp` - 游戏主场景
- `SuccessScene.h/cpp` - 通关场景 - `GameOverLayer.h/cpp` - 游戏结束层
## 视口适配(重要)
### 问题背景
在游戏开发中,不同设备有不同的屏幕分辨率。如果直接使用窗口坐标,游戏内容可能会变形或显示不完整。
Extra2D 提供了**居中视口适配**方案,让游戏在任意分辨率下都能正确显示:
- 游戏使用固定的逻辑分辨率(如 Flappy Bird 的 288×512
- 根据窗口大小自动缩放,保持宽高比
- 游戏内容居中显示,四周显示黑边
### 实现方案
参考 `examples/flappy_bird/BaseScene.h/cpp`
```cpp
// BaseScene.h
#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:
BaseScene();
void onEnter() override;
void onRender(extra2d::RenderBackend &renderer) override;
protected:
void updateViewport();
float scaledGameWidth_ = 0.0f;
float scaledGameHeight_ = 0.0f;
float viewportOffsetX_ = 0.0f;
float viewportOffsetY_ = 0.0f;
};
} // namespace flappybird
```
```cpp
// BaseScene.cpp
#include "BaseScene.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>();
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) {
updateViewport();
}
// 设置视口为居中区域
renderer.setViewport(
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
extra2d::Scene::onRender(renderer);
}
} // namespace flappybird
```
### 使用基础场景类
所有游戏场景继承 `BaseScene`,自动获得视口适配功能:
```cpp
// GameScene.h
class GameScene : public BaseScene {
public:
void onEnter() override;
void onUpdate(float dt) override;
};
// GameScene.cpp
void GameScene::onEnter() {
BaseScene::onEnter(); // 必须调用父类方法
// 使用游戏逻辑分辨率进行布局
float screenWidth = GAME_WIDTH; // 288.0f
float screenHeight = GAME_HEIGHT; // 512.0f
// 所有坐标都基于逻辑分辨率
auto bird = extra2d::makePtr<Bird>();
bird->setPosition(extra2d::Vec2(screenWidth / 2.0f - 50.0f, screenHeight / 2.0f));
addChild(bird);
}
void GameScene::onUpdate(float dt) {
// 游戏逻辑更新...
// 必须调用父类方法,确保子节点动画正常播放
BaseScene::onUpdate(dt);
}
```
### 视口适配原理图
```
┌─────────────────────────────────────────────┐
│ 窗口 (1280 x 720) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 黑边区域 │ │
│ │ │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ 游戏视口 (288 x 512) │ │ │
│ │ │ 自动缩放并居中显示 │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ 游戏内容 │ │ │ │
│ │ │ │ (逻辑分辨率) │ │ │ │
│ │ │ └─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────┘ │ │
│ │ │ │
│ │ 黑边区域 │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
```
### 关键要点
1. **定义游戏逻辑分辨率** - 如 `GAME_WIDTH = 288.0f, GAME_HEIGHT = 512.0f`
2. **在 `onEnter()` 中调用 `updateViewport()`** - 初始化视口
3. **在 `onRender()` 中设置渲染器视口** - 确保正确渲染区域
4. **所有坐标使用逻辑分辨率** - 不依赖窗口实际大小
5. **处理窗口大小变化** - 在 `onRender()` 中检测并更新
## 场景基础 ## 场景基础

View File

@ -564,6 +564,120 @@ void GameOverLayer::initPanel(int score, float screenHeight) {
}; };
``` ```
## 动画系统
### 动作类型
Extra2D 提供了丰富的动作类:
| 动作类 | 说明 |
|--------|------|
| `MoveTo` | 移动到指定位置 |
| `MoveBy` | 移动指定偏移量 |
| `ScaleTo` | 缩放到指定比例 |
| `ScaleBy` | 缩放指定比例 |
| `RotateTo` | 旋转到指定角度 |
| `RotateBy` | 旋转指定角度 |
| `FadeIn` | 淡入 |
| `FadeOut` | 淡出 |
| `FadeTo` | 淡化到指定透明度 |
| `Delay` | 延迟 |
| `Sequence` | 顺序执行多个动作 |
| `Spawn` | 同时执行多个动作 |
| `Loop` | 循环执行动作 |
| `CallFunc` | 回调函数 |
### 运行动作
```cpp
// 移动动画
auto moveAction = makePtr<MoveBy>(1.0f, Vec2(0.0f, -100.0f));
node->runAction(moveAction);
// 缩放动画
auto scaleAction = makePtr<ScaleTo>(0.5f, 2.0f);
node->runAction(scaleAction);
// 淡入淡出
auto fadeOut = makePtr<FadeOut>(0.3f);
auto fadeIn = makePtr<FadeIn>(0.3f);
node->runAction(fadeOut);
```
### 动画完成回调
使用 `setCompletionCallback` 在动画完成时执行回调:
```cpp
// 参考 examples/flappy_bird/GameOverLayer.cpp
void GameOverLayer::onEnter() {
Node::onEnter();
// 创建向上移动的动画
auto moveAction = extra2d::makePtr<extra2d::MoveBy>(
1.0f, extra2d::Vec2(0.0f, -screenHeight));
// 设置动画完成回调
moveAction->setCompletionCallback([this]() {
animationDone_ = true;
// 动画完成后启用按钮
if (restartBtn_)
restartBtn_->setEnabled(true);
if (menuBtn_)
menuBtn_->setEnabled(true);
});
runAction(moveAction);
}
```
### 顺序和并行动画
```cpp
// 顺序执行:先移动,再缩放,最后淡出
auto sequence = makePtr<Sequence>({
new MoveTo(1.0f, Vec2(100, 100)),
new ScaleTo(0.5f, 2.0f),
new FadeOut(0.3f)
});
node->runAction(sequence);
// 并行执行:同时移动和旋转
auto spawn = makePtr<Spawn>({
new MoveTo(1.0f, Vec2(100, 100)),
new RotateBy(1.0f, 360.0f)
});
node->runAction(spawn);
// 循环执行
auto loop = makePtr<Loop(new RotateBy(1.0f, 360.0f), 5); // 旋转5次
node->runAction(loop);
```
### 动画进度回调
```cpp
auto action = makePtr<MoveTo>(2.0f, Vec2(100, 100));
action->setProgressCallback([](float progress) {
// progress: 0.0 - 1.0
E2D_LOG_INFO("动画进度: {}%", progress * 100);
});
node->runAction(action);
```
### 停止动画
```cpp
// 停止所有动画
node->stopAllActions();
// 停止特定动画(需要先设置 tag
action->setTag(1);
node->runAction(action);
node->stopActionByTag(1);
```
## 下一步 ## 下一步
- [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载 - [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载

View File

@ -204,6 +204,194 @@ atlas->addTexture("icon", iconTexture);
atlas->pack(); // 执行打包 atlas->pack(); // 执行打包
``` ```
## 精灵帧SpriteFrame
### 什么是精灵帧?
精灵帧是纹理图集中的一个矩形区域,用于从单个大纹理中提取小图像。使用精灵帧可以:
- 减少纹理切换,提高渲染性能
- 方便管理动画帧和 UI 元素
- 支持从 JSON 文件加载精灵帧数据
### 完整示例:资源加载器
参考 `examples/flappy_bird/ResLoader.h/cpp`
```cpp
// ResLoader.h
#pragma once
#include <extra2d/extra2d.h>
#include <map>
#include <string>
namespace flappybird {
/**
* @brief 音频类型枚举
*/
enum class MusicType {
Click, // 按键声音
Hit, // 小鸟死亡声音
Fly, // 小鸟飞翔声音
Point, // 得分声音
Swoosh // 转场声音
};
/**
* @brief 资源加载器类
* 管理纹理图集、精灵帧和音频资源的加载
*/
class ResLoader {
public:
static void init();
static extra2d::Ptr<extra2d::SpriteFrame> getKeyFrame(const std::string& name);
static void playMusic(MusicType type);
private:
struct ImageInfo {
float width, height, x, y;
};
static extra2d::Ptr<extra2d::Texture> atlasTexture_;
static std::map<std::string, ImageInfo> imageMap_;
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_;
};
} // namespace flappybird
```
```cpp
// ResLoader.cpp
#include "ResLoader.h"
#include <json/json.hpp>
namespace flappybird {
extra2d::Ptr<extra2d::Texture> ResLoader::atlasTexture_;
std::map<std::string, ResLoader::ImageInfo> ResLoader::imageMap_;
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
void ResLoader::init() {
auto &resources = extra2d::Application::instance().resources();
// 加载图集纹理
atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
if (!atlasTexture_) {
E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
return;
}
// 加载 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;
}
E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size());
} catch (const std::exception &e) {
E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what());
return;
}
// 加载音效
soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav");
soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav");
soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav");
soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav");
soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav");
E2D_LOG_INFO("资源加载完成");
}
extra2d::Ptr<extra2d::SpriteFrame>
ResLoader::getKeyFrame(const std::string &name) {
auto it = imageMap_.find(name);
if (it == imageMap_.end()) {
E2D_LOG_WARN("找不到精灵帧: %s", name.c_str());
return nullptr;
}
const ImageInfo &info = it->second;
return extra2d::makePtr<extra2d::SpriteFrame>(
atlasTexture_, extra2d::Rect(info.x, info.y, info.width, info.height));
}
void ResLoader::playMusic(MusicType type) {
auto it = soundMap_.find(type);
if (it != soundMap_.end() && it->second) {
it->second->play();
}
}
} // namespace flappybird
```
### 使用精灵帧创建精灵
```cpp
// 参考 examples/flappy_bird/GameScene.cpp
void GameScene::onEnter() {
BaseScene::onEnter();
// 从图集获取精灵帧
auto bgFrame = ResLoader::getKeyFrame("bg_day");
if (bgFrame) {
// 使用精灵帧创建精灵
auto background = extra2d::Sprite::create(
bgFrame->getTexture(),
bgFrame->getRect());
background->setAnchor(extra2d::Vec2(0.0f, 0.0f));
background->setPosition(extra2d::Vec2(0.0f, 0.0f));
addChild(background);
}
// 创建按钮
auto buttonFrame = ResLoader::getKeyFrame("button_play");
if (buttonFrame) {
auto button = extra2d::Button::create();
button->setBackgroundImage(buttonFrame->getTexture(), buttonFrame->getRect());
button->setAnchor(extra2d::Vec2(0.5f, 0.5f));
button->setPosition(extra2d::Vec2(screenWidth / 2.0f, 300.0f));
button->setOnClick([]() {
// 处理点击
});
addChild(button);
}
}
```
### JSON 图集格式
```json
{
"sprites": [
{ "name": "bg_day", "x": 0, "y": 0, "width": 288, "height": 512 },
{ "name": "bird0_0", "x": 288, "y": 0, "width": 34, "height": 24 },
{ "name": "button_play", "x": 322, "y": 0, "width": 116, "height": 70 }
]
}
```
## 字体加载 ## 字体加载
### 基本用法 ### 基本用法

View File

@ -76,6 +76,109 @@ player->setPosition(player->getPosition() + leftStick * speed * dt);
input.setStickDeadZone(0.2f); input.setStickDeadZone(0.2f);
``` ```
## 鼠标输入
### 鼠标按钮检测
```cpp
auto& input = Application::instance().input();
// 检测鼠标按钮按下(单次触发)
if (input.isMousePressed(MouseButton::Left)) {
// 左键刚按下
}
if (input.isMousePressed(MouseButton::Right)) {
// 右键刚按下
}
// 检测鼠标按钮状态(持续触发)
if (input.isMouseDown(MouseButton::Left)) {
// 左键保持按下
}
// 检测鼠标按钮释放
if (input.isMouseReleased(MouseButton::Left)) {
// 左键刚释放
}
```
### 鼠标位置
```cpp
// 获取鼠标位置(屏幕坐标)
Vec2 mousePos = input.getMousePosition();
// 获取鼠标在游戏视口中的位置(考虑视口适配)
// 参考 examples/flappy_bird 的 BaseScene 实现
```
### 鼠标按钮枚举
| 枚举值 | 说明 |
|--------|------|
| `MouseButton::Left` | 左键 |
| `MouseButton::Right` | 右键 |
| `MouseButton::Middle` | 中键 |
### 完整示例Flappy Bird 输入处理
参考 `examples/flappy_bird/GameScene.cpp`
```cpp
void GameScene::onUpdate(float dt) {
if (!gameOver_) {
auto &input = extra2d::Application::instance().input();
// 同时支持手柄 A 键和鼠标左键
if (input.isButtonPressed(extra2d::GamepadButton::A) ||
input.isMousePressed(extra2d::MouseButton::Left)) {
if (!started_) {
started_ = true;
startGame();
}
bird_->jump();
}
// 游戏逻辑更新...
}
BaseScene::onUpdate(dt);
}
```
### 完整示例Game Over 界面输入
参考 `examples/flappy_bird/GameOverLayer.cpp`
```cpp
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
// 动画完成后才响应输入
if (!animationDone_)
return;
auto &input = extra2d::Application::instance().input();
// A 键重新开始游戏
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
// B 键返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
}
```
## 完整示例 ## 完整示例
### 菜单导航 ### 菜单导航

View File

@ -149,6 +149,103 @@ menuBtn->setPosition(centerX, centerY);
addChild(menuBtn); addChild(menuBtn);
``` ```
### 按钮启用/禁用
Widget 基类提供了 `setEnabled()` 方法控制按钮的交互状态:
```cpp
// 禁用按钮
button->setEnabled(false);
// 启用按钮
button->setEnabled(true);
// 检查按钮状态
bool isEnabled = button->isEnabled();
```
### 完整示例:动画完成后启用按钮
参考 `examples/flappy_bird/GameOverLayer.cpp`
```cpp
// GameOverLayer.h
class GameOverLayer : public extra2d::Node {
public:
GameOverLayer(int score);
void onEnter() override;
void onUpdate(float dt) override;
private:
void initButtons();
int score_ = 0;
bool animationDone_ = false;
extra2d::Ptr<extra2d::Button> restartBtn_;
extra2d::Ptr<extra2d::Button> menuBtn_;
};
// GameOverLayer.cpp
void GameOverLayer::onEnter() {
Node::onEnter();
// 初始化按钮(初始禁用)
initButtons();
// 创建动画
auto moveAction = extra2d::makePtr<extra2d::MoveBy>(
1.0f, extra2d::Vec2(0.0f, -screenHeight));
// 动画完成后启用按钮
moveAction->setCompletionCallback([this]() {
animationDone_ = true;
if (restartBtn_)
restartBtn_->setEnabled(true);
if (menuBtn_)
menuBtn_->setEnabled(true);
});
runAction(moveAction);
}
void GameOverLayer::initButtons() {
auto restartFrame = ResLoader::getKeyFrame("button_restart");
if (restartFrame) {
restartBtn_ = extra2d::Button::create();
restartBtn_->setBackgroundImage(restartFrame->getTexture(),
restartFrame->getRect());
restartBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
restartBtn_->setPosition(extra2d::Vec2(0.0f, 360.0f));
restartBtn_->setEnabled(false); // 初始禁用
restartBtn_->setOnClick([]() {
// 处理点击
});
addChild(restartBtn_);
}
// 菜单按钮类似...
}
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
// 动画完成后才响应手柄输入
if (!animationDone_)
return;
auto &input = extra2d::Application::instance().input();
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
// 重新开始
}
}
```
### 使用场景
- **动画播放期间**:禁用按钮,防止用户过早操作
- **加载过程中**:禁用按钮,显示加载状态
- **条件限制**:当条件不满足时禁用按钮(如未选择关卡)
## 文本Text ## 文本Text
### 创建文本 ### 创建文本

View File

@ -90,6 +90,96 @@ audio.unloadAllSounds();
## 完整示例 ## 完整示例
### Flappy Bird 音效管理器
参考 `examples/flappy_bird/ResLoader.h/cpp`
```cpp
// ResLoader.h
#pragma once
#include <extra2d/extra2d.h>
#include <map>
namespace flappybird {
enum class MusicType {
Click, // 按键声音
Hit, // 小鸟死亡声音
Fly, // 小鸟飞翔声音
Point, // 得分声音
Swoosh // 转场声音
};
class ResLoader {
public:
static void init();
static void playMusic(MusicType type);
private:
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_;
};
} // namespace flappybird
```
```cpp
// ResLoader.cpp
#include "ResLoader.h"
namespace flappybird {
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
void ResLoader::init() {
auto &resources = extra2d::Application::instance().resources();
// 加载所有音效
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");
}
void ResLoader::playMusic(MusicType type) {
auto it = soundMap_.find(type);
if (it != soundMap_.end() && it->second) {
it->second->play();
}
}
} // namespace flappybird
```
### 在游戏中使用
```cpp
// GameScene.cpp - 得分时播放音效
if (pipeX <= birdX) {
score_++;
scoreNumber_->setNumber(score_);
firstPipe->scored = true;
ResLoader::playMusic(MusicType::Point); // 播放得分音效
}
// bird.cpp - 跳跃时播放音效
void Bird::jump() {
velocity_.y = jumpForce_;
ResLoader::playMusic(MusicType::Fly); // 播放飞翔音效
}
// GameOverLayer.cpp - 按钮点击时播放音效
restartBtn_->setOnClick([]() {
ResLoader::playMusic(MusicType::Click); // 播放点击音效
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
});
```
### 推箱子音效管理器
```cpp ```cpp
// audio_manager.h // audio_manager.h
#pragma once #pragma once