|
| 1 | +# 如何使用Application |
| 2 | + |
| 3 | +## 从哪里开始看 |
| 4 | + |
| 5 | +先看 `examples/application_template/main.cpp`,这是能直接编译的空白模板。 |
| 6 | +再看 `examples/color_triangle/main.cpp`:它只增加了资源创建、一个物体和移动回调。 |
| 7 | +最后看 `src/core/Application.cpp`,就能看到这些回调在什么时候被调用。 |
| 8 | + |
| 9 | +这轮新增的不是绘制能力,而是统一“怎样使用引擎”。无需学习类继承、虚函数或单例。 |
| 10 | + |
| 11 | +## 三个容易混淆的名字 |
| 12 | + |
| 13 | +- Application:管理一次应用运行,拥有窗口、输入、摄像机、渲染器和场景。 |
| 14 | +- Scene:管理物体。Application每帧调用它更新和绘制。 |
| 15 | +- GameObject:场景中的一个物体,保存身份、Transform、启用状态和可选的运动回调;它通过Renderable组件组合网格、材质和透明排序参考点。 |
| 16 | + |
| 17 | +`runExample`是示例层的辅助函数,不是引擎API。它统一资源目录、日志、Escape和`--smoke-test`。 |
| 18 | +自己的游戏可以直接使用Application,也可以先复制空白示例再修改。 |
| 19 | + |
| 20 | +## 资源应该放哪里 |
| 21 | + |
| 22 | +在初始化回调里创建GPU资源,此时OpenGL上下文已经就绪: |
| 23 | + |
| 24 | +```cpp |
| 25 | +auto shader = std::make_shared<Shader>(directory / "shaders/color.vert", directory / "shaders/color.frag"); |
| 26 | +auto mesh = std::make_shared<Mesh>(vertices); |
| 27 | +auto material = std::make_shared<Material>(shader); |
| 28 | +auto &object = application.scene().createObject("角色"); |
| 29 | +object.setRenderable(mesh, material); |
| 30 | +``` |
| 31 | + |
| 32 | +`setRenderable`仍是最简洁的兼容入口;需要直接操作渲染组件时,也可以写成: |
| 33 | + |
| 34 | +```cpp |
| 35 | +object.renderable().setSortOrigin({0.0f, 0.0f, 0.0f}); |
| 36 | +``` |
| 37 | +
|
| 38 | +Renderable只组合Mesh和Material,不复制网格数据,也不接管Transform或对象更新逻辑。 |
| 39 | +
|
| 40 | +对于磁盘上的Shader和Texture,推荐通过Application提供的ResourceManager加载: |
| 41 | +
|
| 42 | +```cpp |
| 43 | +auto shader = application.resources().loadShader( |
| 44 | + directory / "shaders/color.vert", directory / "shaders/color.frag"); |
| 45 | +auto texture = application.resources().loadTexture(directory / "assets/checker.ppm"); |
| 46 | +``` |
| 47 | + |
| 48 | +ResourceManager会按规范化路径缓存文件型资源。同一个Application中重复请求同一组Shader或同一张Texture时, |
| 49 | +会返回同一个 `shared_ptr`,避免重复编译和上传。程序生成的Mesh、Material以及没有文件路径的运行时图片, |
| 50 | +仍然由应用自己创建;ResourceManager不负责把它们强行纳入同一种缓存。 |
| 51 | + |
| 52 | +这里的 `shared_ptr` 是“共享持有”:初始化回调结束后,局部变量不再持有资源,但物体继续持有Mesh和Material,Material又持有Shader和Texture。所以它们不会提前消失。 |
| 53 | + |
| 54 | +不要在初始化函数里用局部 `Mesh mesh(...)` 和 `Material material(...)` 然后传给借用接口。局部对象会在函数返回时销毁,留下无效引用。借用接口仍可用于生命周期明确的低层测试,不是新应用的首选。 |
| 55 | + |
| 56 | +Application正常退出和异常退出都会清空自己的场景,窗口最后销毁。不要把GPU资源保存在比Application活得更久的全局变量中;`shared_ptr`并不会延长OpenGL上下文的生命。 |
| 57 | + |
| 58 | +## 场景保存和加载 |
| 59 | + |
| 60 | +`SceneSerializer`适合保存关卡中的基础结构:对象名称、启用状态、局部Transform、父子层级、透明排序参考点以及Mesh/Material文件引用。 |
| 61 | +文件是带版本号的可读文本,加载前会验证对象数量、数值、父索引和循环关系;验证失败不会修改目标Scene。 |
| 62 | + |
| 63 | +```cpp |
| 64 | +SceneSerializer::save(application.scene(), "level.scene"); |
| 65 | +SceneSerializer::load(application.scene(), "level.scene", application.resources()); |
| 66 | +``` |
| 67 | +
|
| 68 | +场景文件使用版本4格式时,`MESH`和`MATERIAL`字段保存相对于场景文件的路径,基础几何额外保存`PRIMITIVE`参数。 |
| 69 | +版本3格式时,`MESH`和`MATERIAL`字段保存相对于场景文件的路径。带资源引用的场景必须通过 |
| 70 | +`ResourceManager`加载;它会缓存OBJ、Material以及材质依赖的Shader和Texture。加载函数返回新对象的ID列表, |
| 71 | +应用仍需根据这些ID重新绑定不会被文件保存的 `setUpdateCallback` 行为。版本1/2的无资源场景仍可读取。 |
| 72 | +
|
| 73 | +加载采用“临时场景提交”规则:OBJ、Material、父子关系全部成功后才替换目标Scene;中途失败不会清空旧场景。 |
| 74 | +资源加载失败前已经进入ResourceManager的缓存项可以被后续请求复用,但不会进入目标Scene。 |
| 75 | +
|
| 76 | +## 输入动作映射 |
| 77 | +
|
| 78 | +Application提供可选的 `ActionMap`,把多个具体按键或鼠标按钮组合成稳定的动作名称: |
| 79 | +
|
| 80 | +```cpp |
| 81 | +application.actions().createAction("move_forward"); |
| 82 | +application.actions().bindKey("move_forward", Key::W); |
| 83 | +application.actions().bindKey("move_forward", Key::Up); |
| 84 | +
|
| 85 | +if (application.actions().isDown("move_forward", application.input())) |
| 86 | +{ |
| 87 | + // W或上方向键按住时执行移动。 |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +动作映射只保存名称和绑定关系,输入状态仍由Window/Input每帧采集。一个动作可以绑定多个键或鼠标按钮, |
| 92 | +查询使用OR语义;未知动作和非法绑定会报告错误。它不包含轴值、手柄或配置文件热重载,这些留到真正需要时再扩展。 |
| 93 | + |
| 94 | +## 每帧逻辑写在哪里 |
| 95 | + |
| 96 | +对象自己的运动用 `object.setUpdateCallback`: |
| 97 | + |
| 98 | +```cpp |
| 99 | +object.setUpdateCallback([](GameObject &self, float deltaTime) |
| 100 | +{ |
| 101 | + // 旋转单位为弧度,deltaTime单位为秒。 |
| 102 | + self.transform.rotateEuler({0.0f, deltaTime, 0.0f}); |
| 103 | +}); |
| 104 | +``` |
| 105 | + |
| 106 | +按键持续移动用 `isKeyDown`;一次性动作如开关用 `wasKeyPressed`。旧 `isKeyPressed` 为兼容保留,含义仍是“按住”。 |
| 107 | +对象回调中不能增删/清空Scene;需要增删时使用ApplicationCallbacks的update阶段,或先记录请求,下一轮应用update再处理。删掉对象后,其引用不能继续使用,长期记录请保存ObjectId并重新查找。 |
| 108 | + |
| 109 | +应用回调的顺序是:initialize只运行一次;之后每轮收集事件,执行onEvents,若可绘制则执行update、Scene::update、清屏、Scene::render、afterRender、交换缓冲。每个阶段结束后检查退出,不中断当前正在执行的回调或Scene遍历。 |
| 110 | + |
| 111 | +## 时间和暂停 |
| 112 | + |
| 113 | +- 初始化结束后才开始计时,首个更新dt为0。 |
| 114 | +- 最小化或帧缓冲为零:只处理事件,暂停更新和绘制;使用最多50毫秒的事件等待避免空转。 |
| 115 | +- 暂停恢复首帧dt为0,后续恢复正常。 |
| 116 | +- 平常卡顿时运动dt最多0.1秒,可通过ApplicationConfig::maximumDeltaTime修改。 |
| 117 | +- 真实时间通过frameTime().realDeltaTime / realElapsedTime读取;模拟时间通过deltaTime / simulationTime读取。真实时间包含暂停,模拟时间不包含暂停和被截断的部分。 |
| 118 | +- onEvents在本轮时间采样前执行,适合退出和窗口事件,不适合依赖本轮dt的运动。 |
| 119 | +- 失焦但没最小化时不自动暂停;隐藏测试窗口不等于最小化窗口。 |
| 120 | + |
| 121 | +## 运行与验证 |
| 122 | + |
| 123 | +```powershell |
| 124 | +E:/msys64/ucrt64/bin/cmake.exe --build --preset msys2-ucrt64-debug |
| 125 | +E:/msys64/ucrt64/bin/ctest.exe --preset msys2-ucrt64-debug |
| 126 | +./runtime/examples/application_template/application_template.exe |
| 127 | +``` |
| 128 | + |
| 129 | +空白模板显示蓝灰色背景是预期行为,不代表绘制失败。加入物体后把模板最后的ClearOnly改为DrawnScene,现有简单冒烟检查会要求窗口中点有区别于清屏的像素;若新示例的中点本来没有物体,需要为它设计对应的检查位置。 |
| 130 | + |
| 131 | +自动测试使用隐藏窗口和直接馈入的GLFW回调验证暂停接线,不模拟用户桌面操作。实机验收时请运行scene_objects:检查WASD、窗口缩放、最小化再恢复、Escape,以及关闭窗口;恢复后物体不应突然跳动。此项人工交互验收与自动测试分开记录。 |
| 132 | + |
| 133 | +`scene_objects` 的自由摄像机属于示例层控制器,文件位于 `examples/common/FreeCameraController.h`,没有进入 `src/` 框架层。WASD控制水平移动,Space/Ctrl控制升降,Shift临时加速;按住鼠标右键移动可以改变观察方向,R恢复初始位置。右键刚按下时会丢弃已有鼠标位移,失焦或暂停恢复时也会重置鼠标基准,避免摄像机突然跳转。 |
0 commit comments