------- scene viewport ----------
》》》》做了两件事:设置视口和设置相机比例
》》》》为什么要设置 m_ViewportSize 为 glm::vec2 而不是 ImVec2 ?
因为后面需要进行 != 运算,而 ImVec2 没有这个运算符的定义,只有 glm::vec2 有这个运算符的定义。
所以需要用 ImVec2 接收 GetContentRegionAvail 返回的 ImVec2类型的 panelSize,然后将两者进行比较。
》》》》发现一个问题
其中,无论对 m_Framebuffer 是否调用 Resize,其渲染结果和响应好像都是一样的,并没有什么影响(实际上这应该对图像的分辨率有一定影响,但为何我没有发现什么明确特征?)。
而且不调用Framebuffer->Resize的话,调整窗口大小的时候图像并不会出现闪烁的现象。(所以说闪烁正是因为帧缓冲对纹理附件的刷新而导致的)
》》》》另一个问题
》》》》值得一提的是,相机的纵横比更新函数参数需要为 float 类型的,而不是 uint 类型,否则会导致窗口尺寸过小时无渲染结果。
-----------ImGui Layer Events---------
》》》》发现一个问题:
Hazel中有一次维护是删除 inline 关键字的,我大致看了眼,觉得没有必要,就没有提交到 Nut,只是添加到待办里面了,这导致一个问题。
操作:
在简化了Input.h之后,只剩下了5个函数的声明,而且这些函数在简化前都是内联函数,在.h文件中就已经定义过了。
建议:
所以在删除掉了定义之后,还应该删除inline关键字,我们要确保使用 inline 关键字的时候就对函数在头文件中定义,否则不添加inline关键字,避免出现错误。
如果仅仅删除了定义,但是没有删除inline关键字,就会出现 LNK2019 的报错,比如:
->
"public: static bool __cdecl Nut::Input::IsKeyPressed(int)" (?IsKeyPressed@Input@Nut@@SA_NH@Z),
函数 "public: void __cdecl Nut::OrthoGraphicCameraController::OnUpdate(class Nut::Timestep)" (?OnUpdate@OrthoGraphicCameraController@Nut@@QEAAXVTimestep@2@@Z) 中引用了该符号。
问题:
OrthoCameraController本应使用函数,可是为什么会查找不到,或者说对这个函数链接失败呢?
原因:
这正是因为我在头文件中只声明了函数为 inline,然后没在头文件中定义这个函数,而是在 CPP 文件中定义它。
此时编译器会在编译时找不到这个函数的定义,因为头文件已经告诉编译器这是一个 inline 函数,并期望在头文件中找到它的实现。
这样会导致链接错误或重复定义错误,这全都由于 CPP 文件中的定义与头文件中的 inline 声明不匹配。
》》》》提醒:
记得不要写成 ImGui::IsWindowFocused; :)
----Where to go + Code review(前瞻与代码审核)------
》》》》Cherno 所做的
Cherno 在这一集前8分钟修复了一个小Bug,然后就算是开始审核代码了,基本上讲了自己对游戏引擎的理解与期望,还有接下来的进程。
》》》》我将提交一些维护代码,因为这一集也没什么要做的。
》》》》调整ImGui窗口大小时闪烁的原因是:我们在绘制ImGui窗口时同步更新了FrameBuffer和Camera
我们应该先更新,后绘制。
问题出现原理以及解决方法:
在 OnImGuiRender 函数中处理窗口大小变化时,你会在每一帧的渲染过程中检查窗口尺寸并同时处理窗口尺寸。因为在窗口调整时你重新创建了帧缓冲(Framebuffer),那么在调整过程中的某些渲染操作可能会使用未完全准备好的新帧缓冲,这就会导致显示的内容不稳定,从而产生闪烁。
将窗口大小的调整逻辑提前放在 Onupdate 函数中,可以确保在每一帧的渲染之前已经完成了所有的帧缓冲调整。这意味着当 ImGuiRender 执行时,帧缓冲已经是正确的状态,减少了因帧缓冲调整导致的闪烁现象。
未准备好的帧缓冲概念:
- 帧缓冲(Framebuffer)重建过程:
当窗口大小变化时,通常需要重新创建或调整帧缓冲的尺寸,以适应新的窗口尺寸。这个过程包括删除旧的帧缓冲对象并创建新的对象,同时可能需要重新分配或调整与之关联的纹理和深度缓冲区。
- 未准备好的帧缓冲:
在帧缓冲重新创建或调整的过程中,新的帧缓冲可能尚未完全配置和初始化。例如,新的纹理可能尚未正确分配或绑定,或者深度缓冲区的设置尚未完成。在这个过渡期间,帧缓冲可能处于一个不稳定的状态,无法正确显示内容。
》》》另外,还要注意一个问题:
第一种逻辑更新方式是不可取的,因为 ImGui::GetContentRefionAvail() 获取的是当前ImGui窗口的面板大小,需要在 ImGui 窗口绘制范围内进行使用,否则获取的Window值为nullptr, 即没有找到可获取的ImGui窗口。
第二种方式可取,因为每一次m_Viewport在ImGui窗口事件触发时更新后,每当下一次绘制开始执行OnUpdate函数,m_ViewportSize已经是新窗口尺寸,而specification中存储的是旧窗口尺寸。
这时触发Resize函数,随后帧缓冲m_Framebuffer更新,相应的帧缓冲m_Framebuffer中存储的specification也会更新为新窗口尺寸。
下一次窗口大小改变时,也是类似的操作。
逻辑:
需要注意的是,这里的m_Viewport值是在Onupdate函数执行后更新的,也就是说,图像的更新逻辑为:当前绘制时先判断逻辑,然后执行绘制。检测窗口尺寸变化的代码确实在绘制函数OnImGuiRender中,不过没有直接绘制被更新的帧,而是将新窗口尺寸保留在全局变量m_ViewportSize中,在下一次绘制开启前先在Onupdate更新窗体逻辑,然后在绘制函数中更新实际窗口尺寸。(简而言之,就是:当前帧检测到变化,但不更新,在下一帧开始时,发送变化值并执行更新)
------------ECS(实体系统)---------
》》》》76,77主要讲了EnTT的设计理念,使用方法以及使用案例,不是很难,我尽可能的做一些笔记以理解,同时上传77集的示例。
》》》》ECS 的概念:
实体组件系统(ECS)是一种设计模式,常用于游戏开发和其他需要高性能和灵活性的应用程序中。它将对象分解为“实体”、“组件”和“系统”三个主要部分。
实体是唯一的标识符,组件是数据容器,而系统是处理组件数据的逻辑模块。
》》实体,组件,系统之间的关系:
1.实体 (Entity)
定义:实体是系统中唯一的标识符,通常是一个简单的ID(比如unsigned int)。它本身不包含任何数据或逻辑。
作用:实体作为其他数据(即组件)和逻辑(即系统)的载体,用来标识和操作这些数据或逻辑。
2. 组件 (Component)
定义:组件是包含数据的结构体或类,但不包含逻辑。每种组件代表一种特定的数据类型,例如位置、速度、健康值等。
作用:组件用于存储实体的状态信息。每个实体可以拥有一个或多个组件,从而描述它的各种属性。
3. 系统 (System)
定义:系统包含处理特定类型组件数据的逻辑。系统会遍历所有具有所需组件的实体,执行相应的操作。
作用:系统负责更新和处理组件数据,实现游戏逻辑或其他功能。每个系统通常专注于一个特定的任务,如物理模拟、渲染或AI决策等。
》》关系和使用方式
关系:
实体与组件:
实体是组件的容器,通过附加不同类型的组件来描述实体的属性和行为。实体本身没有实际的数据,只是一个标识符。
组件与系统:
系统会查询所有拥有特定组件集合的实体,并对这些实体的组件数据进行处理。例如,物理系统可能会查找所有拥有位置和速度组件的实体,并更新它们的位置数据。
使用方式:
创建实体:实例化一个新的实体并为其分配唯一标识符。
添加组件:为实体附加一个或多个组件。每个组件存储实体的特定数据。
定义系统:实现系统逻辑,这些系统会在每一帧或特定的事件触发时运行。
更新:在每一帧或游戏循环中,系统会遍历实体并更新组件数据,实现游戏逻辑。
示例
假设你在开发一个简单的游戏,其中有角色实体(Player)和敌人实体(Enemy)。
实体:
playerEntity 和 enemyEntity。
组件:
PositionComponent:存储实体的位置数据(x, y)。
VelocityComponent:存储实体的速度数据(vx, vy)。
HealthComponent:存储实体的健康值。
系统:
MovementSystem:遍历所有具有 PositionComponent 和 VelocityComponent 的实体,并根据速度更新位置。
HealthSystem:遍历所有具有 HealthComponent 的实体,处理伤害、恢复等健康相关的逻辑。
》》ECS可以看做是一种数据结构吗,有什么优点?
与其看做是数据结构,不如称之为是一种设计模式。
优点:
性能:ECS通过将数据(组件)分开存储,提高了缓存效率、减少内存碎片,从而提高性能。
灵活性:ECS使得添加、删除和修改组件变得容易,而且系统将数据和行为分离的方式也使得数据和逻辑的耦合度降低,这有助于维护和扩展系统。使系统更加灵活和可扩展。
》》》》下载头文件 :(https://github.com/skypjack/entt/tree/master/single_include/entt )
这个库只需要加载头文件,因为entt是一个模板库,不需要链接。
》》》》entt::registry 的原理与机制
概念:
entt 的 entt::registry 是一个高度优化的数据结构,用于高效地管理实体及其组件。
结构:
- 存放实体的结构
实体ID:每个实体都有一个唯一的ID,这个ID用于标识实体,可以在内部数据结构中索引和检索。
实体状态管理:entt::registry 维护一个实体池(entity pool)来跟踪实体的创建和销毁。通常是通过位图或其他类似的数据结构来管理实体。
- 管理组件的结构
组件存储:entt::registry 为每种组件类型维护一个独立的存储结构,通常是一个类似于数组或向量的容器。每个组件的存储容器按所属于的 实体ID进行索引,从而实现快速访问。
组件映射:为了支持快速的组件查询和操作,entt::registry 使用组件映射(component map)来跟踪哪些实体拥有特定的组件。
这种映射通常基于组件的签名(signature)来实现,签名是实体拥有的组件的集合。
组件更新:entt::registry 允许高效的组件添加、删除和更新。组件的存储和管理通常采取增量更新的方式,以提高性能。
存储结构图示:
假设我们有两个实体(ID=1 和 ID=2),以及两个组件(位置和速度)。
实体管理:entt::registry 维护一个实体池,跟踪所有有效的实体ID(比如,1 和 2)。
+-------------------+
| 实体池 |
+-------------------+
| 位图 |
| [1] [2] [ ] [ ] | // 实体ID 1 和 2 是有效的
+-------------------+
位置组件:用一个数组或向量存储位置数据,比如 { {100, 200}, {300, 400} },其中 {100, 200} 是玩家的位置,{300, 400} 是敌人的位置。
速度组件:用另一个数组或向量存储速度数据,比如 { {10, 0}, {5, -2} },其中 {10, 0} 是玩家的速度,{5, -2} 是敌人的速度。
+-------------------+
| 位置组件存储|
+-------------------+
| {100, 200} | // 实体ID=1 的位置
| {300, 400} | // 实体ID=2 的位置
+-------------------+
+-------------------+
|速度组件存储 |
+-------------------+
| {10, 0} | // 实体ID=1 的速度
| {5, -2} | // 实体ID=2 的速度
+-------------------+
位置映射:entt::registry 使用一个哈希表将实体ID映射到位置数据。例如,ID=1 映射到 {100, 200},ID=2 映射到 {300, 400}。
速度映射:类似地,ID=1 映射到 {10, 0},ID=2 映射到 {5, -2}。
+-----------------------------+
| 组件映射(哈希表) |
+-----------------------------+
| 位置: |
| ID=1 -> {100, 200} |
| ID=2 -> {300, 400} |
| |
| 速度: |
| ID=1 -> {10, 0} |
| ID=2 -> {5, -2} |
+-----------------------------+
》》entt::registry的查询与更新:
#include <entt/entt.hpp>
#include <iostream>
// 定义组件
struct Position {
float x, y;
};
struct Velocity {
float vx, vy;
};
int main() {
// 创建一个注册表
entt::registry registry;
// 使用 registry 创建实体并返回句柄
auto entity = registry.create();
// 为实体添加组件
registry.emplace<Position>(entity, 10.0f, 20.0f);
registry.emplace<Velocity>(entity, 1.0f, 2.0f);
// 查询组件
if (auto* pos = registry.try_get<Position>(entity)) {
std::cout << "Position: (" << pos->x << ", " << pos->y << ")\n";
} else {
std::cout << "Entity has no Position component.\n";
}
if (auto* vel = registry.try_get<Velocity>(entity)) {
std::cout << "Velocity: (" << vel->vx << ", " << vel->vy << ")\n";
} else {
std::cout << "Entity has no Velocity component.\n";
}
// 更新组件
if (auto* pos = registry.try_get<Position>(entity)) {
pos->x += 5.0f; // 增加 x 坐标
pos->y -= 3.0f; // 减少 y 坐标
}
if (auto* vel = registry.try_get<Velocity>(entity)) {
vel->vx *= 2.0f; // 增加 x 速度
vel->vy *= 0.5f; // 减少 y 速度
}
return 0;
}
》》》》新版 Entt 的 entt::registry 好像没有 has 这个成员函数.
》》》》结构化绑定:
auto group = m_Registry.group<TransformComponent, MeshComponent>();
for (auto entity : group)
{
auto& [transform, mesh] = group.get<TransformComponent, MeshComponent>(entity);
}
这就是就是所谓的结构化绑定, 可以查看 Cherno C++ 系列的视频。
BiliBili 【75】【Cherno C++】【中字】C++的结构化绑定_哔哩哔哩_bilibili
Youtube:STRUCTURED BINDINGS in C++
Structured binding: 结构化绑定
语法: | auto [var1, var2, ...] = expression; |
auto 表示变量的类型将由编译器自动推导。
[var1, var2, ...] 是结构化绑定的变量列表,用于接收解构后的值。
expression 是一个可以解构的对象,通常是一个 std::tuple、std::pair 或自定义类型。
》》》》On_Construct 函数的意义:
on_construct 是用于注册回调函数,当某个组件类型的组件被添加到实体时,会触发这些回调。它允许你在组件创建时执行特定的操作。
connect<&OnTransformConstruct>(): 将 OnTransformConstruct 函数连接到这个信号。每当 TransformComponent 被添加到实体时,OnTransformConstruct 就会被调用。
----Entities And Components-----------
@Cherno
》》》》感觉没啥要记的
auto group = m_Registry.group<TransformComponent>(entt::get<SpriteComponent>);
m_Registry.group<TransformComponent>:定义了一个组,其中的实体必须具有TransformComponent组件。
entt::get<SpriteComponent>:这是一个额外的条件,表示要包括SpriteComponent组件。
这样,group中的实体不仅必须有TransformComponent,还必须有SpriteComponent。
---------------Entity class-------------
》》》》强指针与弱指针的区别
强指针: | 定义: 强指针是指在程序中直接引用对象的指针或引用。当一个对象有一个或多个强指针指向它时,该对象的生命周期是由这些强指针管理的,直到所有强指针都被释放或指向其他对象。 特性: 只要存在一个强指针指向对象,该对象就不会被回收或释放。强指针会延长对象的生命周期,防止对象在被引用期间被销毁。 |
弱指针: | 定义: 弱指针是一种不会阻止对象被回收的指针。它通常用于引用那些可能会被销毁的对象,避免强引用循环所导致的内存泄漏。 特性: 弱指针不会改变对象的引用计数,这意味着即使存在弱指针指向对象,只要没有强指针指向对象,该对象依然会被回收。弱指针通常与强指针一起使用,以避免内存泄漏问题。 |
内存管理:
强指针 | 会增加对象的引用计数,使对象在所有强指针都被释放之前不会被回收。 |
弱指针 | 不会影响对象的引用计数,它只是一个辅助指针,用于访问对象,但不阻止对象的回收。 |
例如:
在观察者模式的设计中,弱指针可以在观察对象时不干预对象生命周期。也就是说 std::weak_ptr 可以通过避免持有对象的强引用来实现有效的内存管理。
》》》》registry.has( ) 函数被重命名了 现在叫 all_of
You can find it with : https://github.com/skypjack/entt/issues/690
》》》》关于 operator bool() const
这是一个类型转换运算符(type conversion operator), 其作用是允许类 Entity 对象在需要布尔值的上下文中(如条件语句)被隐式地转换为 bool 类型。
例如:
operator bool() const { return m_EntityHandle != 0; }
在这里,m_EntityHandle 是一个表示实体的句柄(handle)。
如果 m_EntityHandle 为 0 (null 或 空),则m_EntityHandle != 0 返回 false ,可以认为 Entity 对象无效(可能表示它已经被销毁或未初始化)。
否则,若 m_EntityHandle 不为空, 则m_EntityHandle != 0 结果为 true, 可以认为它是有效的。
----------------Camera system------------
》》》》Cherno的意思好像是:在游戏编辑时,使用一种摄像机。但在游戏运行时,使用另外一种摄像机。当然这两种摄像机有别。
我的理解是,游戏在编辑的时候,需要编辑者全面/自由的观察游戏设计样貌,所以摄像机相对复杂一点,因为它要有能力在局部空间/世界空间中位移。
而游戏运行时,大部分时间需要以一个固定的视角游玩,所以摄像机不用太复杂。
》》》》primary 的作用
Primary 用于确定当前摄像机是否为主摄像机。也就是说当你使用此摄像机的时候,这个摄像机会被标记为主摄像机(当下正在使用的摄像机)。
根据这个标识,我们可以正确的对当下所观察的事物进行处理。
》》》》Group 和 View 的区别:
参考:( http://t.csdnimg.cn/TZiz5 )
不同之处:
1. 基本概念
view:
概念:用于访问具有特定组件的实体。你可以创建一个视图来遍历所有包含某种或多种组件的实体。
特点:视图是相对轻量级的,并且对组件的访问是直接的。
视图通常是以某种组件的组合为基础进行过滤的。
group:
概念:是一个更复杂的数据结构,允许你对实体进行更多的分类和管理。
特点:group 不仅可以过滤组件,还可以根据实体的组件组合创建逻辑组。
group 可以指定需要的组件和额外的条件,以便管理和操作那些具有特定组件组合的实体。
2. 用法和功能
view:
用法:用于遍历符合组件条件的实体。
特点:访问速度较快,因为它不涉及复杂的分组逻辑,只是简单地提供对符合条件的实体的访问。
例子:
auto view = m_Registry.view<TransformComponent>();
for (auto entity : view) {
auto& transform = view.get<TransformComponent>(entity);
// 对实体进行操作
}
group:
用法:用于将实体分组到一个更复杂的集合中,考虑了多个组件及其组合。
特点:group提供了更高层次的管理功能,可以在创建时定义更复杂的过滤条件和数据访问逻辑。
例子:
auto group = m_Registry.group<TransformComponent>(entt::get<SpriteComponent>);
for (auto entity : group) {
auto& transform = group.get<TransformComponent>(entity);
auto& sprite = group.get<SpriteComponent>(entity);
// 对实体进行操作
}
3. 性能和优化
view:
view主要依赖于组件的数据结构和缓存,通常较为高效,适合用于简单的组件过滤和遍历操作。
group:
由于其包含了更多的逻辑和条件,它可能在创建时需要额外的计算和管理开销。
但在需要处理更复杂的组件组合和分类时,group可以提供更好的性能优化。
总结:
使用view时,你是在基于组件的简单过滤进行操作,适合轻量级的遍历和访问。
使用group时,你可以进行更复杂的组件组合和条件过滤,更适合需要进行复杂数据管理和分类的场景。
这取决于你的具体需求和性能要求。
》》》》为什么这一集之后,键盘上的 wasd 对摄像机控制失效了。
因为之前绘制的时候,通过 BeginScene( OrthographicCamera camera ) 传入的是一个 OrthographicCamera,而且我们在更新的时候 CameraController 操控的也是 OrthographicCamera (每一次响应wasd,都在通过 UpdateMatrix( ) 更新 OrthographicCamera)。
但是在此次绘制的实体中,通过 BeginScene( Camera camera ) 我们传入的都是新类型 Camera,并且在 CameraController 的更新函数中没有对 Camera 类型摄像机进行更新的函数,这就导致在仅绘制 Camera 类型摄像机时,WASD 不起作用。
》》》》为什么Cherno在代码中总是获取位移矩阵的第四列(Transform[3]),以此对物体进行操纵呢?
因为OpenGL中的位移矩阵是列主序的,所以位移向量存储在矩阵的第4列(代码中表示为 0,1,2,3 这 4 个标号中的第 4 个:也就是 3)
参考:( 变换 - LearnOpenGL CN (learnopengl-cn.github.io) )
》》》》一个提示:
Camera 中的投影矩阵被用为 BeginScene 的一个参数,所以要确保运行之前已经为 CameraComponent 的成员 Camera 添加了数据。
》》》》一个理解:
这一集需要我们重载 Renderer2D::BeginScene()
Cherno 是这样做的:
我是这样做的
其实差不多,我在填入参数时就将 transform 位移矩阵转换成了 view 观察矩阵.
》》接下来看看重载 BeginScene() 这个函数目的:
- 在先前的 Renderer2D::BeginScene() 中,我们传入的是 OrthographicCamera:
这么做的用意是,可以直接通过 OrthographicCamera 对象的成员函数获取视图投影矩阵 ViewProjectionMatrix,然后将其上传为统一变量,以便计算与呈现渲染结果。
- 但是在游戏运行时,我们只想进行简单的计算,并使用简单的 Camera类型(仅仅包含投影矩阵: ProjectionMatrix),这时候想要绘制一个图形,在使用 Renderer2D::BeginScene() 时,就需要进行重载,将 Camera 类型对象作为参数的一员。
所以我们还需要传入一个位移矩阵 transform 在 Renderer2D::BeginScene() 临时的计算一下视图投影矩阵 ViewProjectionMatrix,然后将其上传为统一变量。
- 值得一提的是,这个视图矩阵 viewMatrix 就是由位移矩阵 transform 进行转置运算得来的,这一点在之前的 OrthographicCamera.cpp 中也可见端倪:
》》》》纠正两个Cherno的错误:
1.视图的命名是不是使用错了?
2.BeginScene() 的参数是不是填错了?
》》》》又发现一个问题:绘制的图像在位移之后,其初始位置的图像却一直在绘制,没有清除。
后来我发现原因出在这里:(注释掉之后运行情况正常)
由于我为这两个摄像机实体都添加了 sprite 组件,所以在绘制图形的时候,额外绘制了当前摄像机比例下的两个图形。但不可否认的是,新添加的两个图像由于 CPU 运行顺序,导致两个新图在绘制时都是覆盖在旧图之上的。
如果解除1的注释,但不解除2的注释,效果是这样的。(初始位置多绘制一个图像)
如果解除2的注释,但不解除1的注释,效果是这样的。(总有一个紫色的新图像覆盖在旧的蓝图像之上)
为何会是这两句代码导致错误呢,其中的逻辑是什么?
想不出来,我现在想睡觉。
----Scene Camera (fixed aspect ratio)------
》》》》在C++中,整数除法和浮点数除法有什么区别吗
整数除法:操作数都是整数(int, long, short等),结果也是整数。
浮点数除法:操作数至少有一个是浮点数(float, double, long double),结果是浮点数。
》》》》什么类 可以访问 其他类中 protected 类型的成员变量?
- 派生类可以访问基类(父类)中的 protected 成员。
< 派生类(子类)不能直接访问基类(父类)的 private 成员 >
- 友元类和友元函数可以访问类的 protected 成员。
- 类内部的成员函数可以访问该类的 protected 成员。
》》》》关于子类与父类构造函数的关系。
前提:子类在没有显示地调用父类的构造函数时,编译器依旧会尝试隐式调用父类的默认构造函数。如果父类没有定义默认构造函数,那么编译器就会产生错误。
原因:
- 子类与父类构造函数的结构:
子类的构造函数会首先调用父类的构造函数,以确保父类部分被正确初始化。子类构造函数可以通过初始化列表显式指定调用哪个父类构造函数。
- 初始化顺序:父类的构造函数先于子类的构造函数执行
即使子类的构造函数中没有显式调用父类构造函数,父类的构造函数也会被自动调用。
所以如果父类没有默认构造函数,子类构造函数就必须提供一个有效的父类构造函数调用。
》》》》Button & Bottom:button是按钮,bottom是底部。
》》》》为什么在正交矩阵的计算中,对于 bottom 和 top 这两个参数不用乘以纵横比 AspectRatio?
因为 m_OrthographicSize 已经算是定义了视口的高度。所以在确定的高度之下,只需要对宽度(从 Left 到 Right)进行比例换算即可得到正确的视觉效果。
如果只对高度(从 Bottom 到 Top )乘以纵横比 AspectRatio,但不对宽度进行计算,结果应该是相似的,只不过会变扁一点。
》》》》一个提醒
错误缘由:
这个错误是由于你尝试用不兼容的方式初始化 Nut::CameraComponent 对象。
在 Entt 库的代码中,它期望某种特定的初始化方式初始化对象,但你的代码使用了不匹配的方式。
排除错误:
这里就需要检查 Nut::CameraComponent 的构造函数或 Entt 库的文档。
纠正错误:
由于更换了 CameraComponent 中的成员 Camera ( Nut::Camera -> SceneCamera ),我们需要删除 AddComponent 的参数 glm::ortho(…)
(
- 因为子类 SceneCamera 的所有构造都不需要填入参数, 而且父类的默认构造函数也不需要填入参数,
所以在此处为 CameraCompoonent 的成员 SceneCamera Camera 初始化的时候不需要填入数据。
- 而且在默认情况下,我们还为父类的成员:投影矩阵 ProjectionMatrix 添加了一个默认值 glm::mat4(1.0f),以防没有填入参数对其带来未定义的错误。
随后我们在 Camera 的子类 SceneCamera 中更新了投影矩阵,投影矩阵主要在 SceneCamera 中定义。( 投影矩阵由 glm::ortho( ) 中填入的参数决定并定义。)
)。
》》》》另一个提示:
由于我们去除了为摄像机组件 CameraComponent 填入的正交矩阵 glm::ortho(…), 所以现在两个摄像机实体 m_CameraEntity 和 m_SecondCamera 在空间中看起来是一样的(大小、比例…)。
( “GameEngine6”页上的一个图片【 来自:》》》》一个提醒 】 )
这是因为我们的投影矩阵在之前由摄像机组件 CameraComponent 填入的正交矩阵 glm::ortho(…)决定(本来填入的 glm::ortho(…) 直接为组件中的成员 Nut::Camera Camera 进行初始化,现在组件中的成员改为了 Nut::SceneCamera Camera,后者不需要填入矩阵参数),而现在由父类 SceneCamera 中的函数 UpdateProjection( ) 决定。
( “GameEngine6”页上的图片 【 来自:》》》》一个提醒 】 )
一开始,组件成员为 Nut::Camera Camera 时,填入的正交矩阵 glm::ortho 直接被父类 Camera 的投影矩阵 m_ProjectionMatrix 所用;现在组件成员改为 Nut::SceneCamera Camera 之后,不用填入正交矩阵 glm::ortho,但是这样也散失了灵活性,因为现在子类 SceneCamera 中从父类 Camera 继承的 m_ProjectionMatrix 被固定的数据计算出来,而且上传为统一变量。
在 UpdateProjection( ) 函数中,所有的数据由私有成员计算得来,这些成员在类对象初始化时就已经定义了,而且每一个对象的默认值都一样:
父类 Camera 或子类 SceneCamera 所计算并更新的投影矩阵 m_ProjectionMatrix 在这里被获取并上传至统一变量。
↓
----Native Scripting (本机脚本)-----
》》》》十分抱歉因为前两天出去玩,后面又有事情耽搁,一直没更新。现在我试着恢复到工作状态中来。
》》》》什么是脚本?
概念:
在编程中,脚本(Script)通常指的是一种用于自动化任务的程序代码。
脚本语言通常是解释性语言,意味着它们不需要编译成机器代码,可以直接由解释器逐行执行。
常用于:
自动化任务,系统管理,网页开发,数据分析,测试等目的。
常见脚本语言:
常见的脚本语言包括Python、JavaScript、Bash、Perl和Ruby。
》》》》本机脚本和普通的脚本有什么不同?
普通脚本(Managed Scripts)以 Unity 为例 | 1.语言:通常使用 C# 编写。 2.运行环境:运行在 Unity 的 Mono 或 .NET 运行时环境中。这些脚本是托管代码,由 Unity 的托管环境处理。 3.编译:在 Unity 编辑器中,C# 脚本被编译成 .NET 程序集(DLLs)。 4.接口:通过 Unity 的 MonoBehaviour 类和 Unity 的 API 访问和操作游戏对象和组件。 5.调试:可以通过 Unity 编辑器的调试工具或 Visual Studio 等 IDE 进行调试。 6.特性:因为采用的语言 C# ,使其易于编写和维护,因为它们利用了 .NET 的垃圾回收和其他高级语言功能。 |
本机脚本(Native Scripts) | 1.语言:通常使用 C++ 或 C 编写。这些脚本通过 Unity 的本机插件接口(Native Plugin Interface)集成。 2.运行环境:直接运行在操作系统的本机环境中,而不是 Unity 的托管环境中。 3.编译:需要编译成平台特定的动态链接库(DLLs、so 文件、dylib 文件等)。 4.接口:通过 Unity 提供的本机插件接口进行交互。Unity 允许通过 DllImport 等机制调用本机插件中的函数。 5.调试:调试可能会更加复杂,因为它涉及到不同的工具和环境,通常需要使用本机开发工具(如 Visual Studio 的 C++ 部分)。 6.特性:C++ 提供了对更底层硬件和系统资源的访问,可以优化性能,但编写和维护更为复杂。还需要处理内存管理和其他低级问题。 |
使用场景
普通脚本: | 适合大多数游戏逻辑、用户界面、输入处理等应用场景。 |
本机脚本: | 适合需要高性能计算、平台特定功能、或与现有本机代码库集成的场景。 例如,进行复杂的数学计算或直接操作硬件等。 |
总结
普通脚本提供了更高层次的抽象和易用性,而本机脚本则提供了更低层次的控制和优化能力。
》》》》函数指针:
Check it with:
BiliBili: 【58】【Cherno C++】【中字】C++的函数指针_哔哩哔哩_bilibili
YouTube: Function Pointers in C++
》》》》std::function
概念:
std::function 是 C++ 标准库中的一个可调用对象封装器,定义在 <functional> 头文件中。
它可以存储和调用任意类型的可调用对象,包括函数指针、Lambda 表达式、函数对象(即重载了 operator() 的类),甚至是绑定了参数的函数。
使用:
定义:声明一个 std::function 对象,并指定其接受的参数和返回类型。 |
|
赋值:将函数指针、Lambda 表达式或函数对象等等赋值给 std::function 对象。 |
|
使用:直接调用 std::function 对象,就像调用普通函数一样。 |
|
》》》》Lambda 表达式
Lambda 表达式在 C++ 中是定义匿名函数的简洁方式。
基本语法: | [capture](parameters) -> return_type { body } |
Lambda 表达式的组成部分
捕获列表 [capture]: | 指定 Lambda 表达式可以访问的外部变量。(决定了 Lambda 表达式如何访问外部变量) |
参数列表 (parameters): | 定义 Lambda 表达式的参数(决定 Lambda 表达式接受哪些参数) |
返回类型 -> return_type: | 指定 Lambda 表达式的返回类型(如果编译器无法自动推断的话)。这部分是可选的。 |
函数体 { body }: | Lambda 表达式的实现部分。 |
捕获列表 [capture] 决定了 Lambda 表达式如何捕获外部变量。你可以通过不同的方式来捕获这些变量:
[&]: | 以引用的方式捕获所有外部变量。这意味着 Lambda 表达式使用并修改外部变量。 |
[=]: | 以值的方式捕获所有外部变量。这意味着 Lambda 表达式会创建外部变量的副本,并且对这些副本的修改不会影响外部变量。 |
[&var]: | 以引用的方式捕获指定的变量 var,其他外部变量不被捕获。 |
[=, &var]: | 以值的方式捕获所有外部变量,但以引用的方式捕获指定的变量 var。 |
You Can Check it with:
BiliBili: 【59】【Cherno C++】【中字】C++的lambda_哔哩哔哩_bilibili
YouTube: Lambdas in C++
》》》》关于代码的一些理解
》》Instance 的作用?
因为我们计划通过脚本组件中的OnCreateFunction、OnDestroyFunction、OnUpdateFunction 来获取脚本类中自定义的 3 个函数。所以为了OnCreateFunction、OnDestroyFunction、OnUpdateFunction 能够获取到指定脚本类中的函数,我们需要在调用函数时获取到合适的脚本类对象,然后在OnCreateFunction、OnDestroyFunction、OnUpdateFunction 中调用该对象的成员函数。
》》为什么不能将脚本类中的函数 Create、Update、destroy 直接作为脚本组件的成员呢,而非要使用三个函数调用脚本类中的这三个函数?
个人理解:
1. 首先,如果是在脚本组件中定义了三个成员函数,分别用于传入脚本类成员函数,这样在初始化的时候调用起来就很麻烦,想象一下一个组件就要多调用好几次用于传入的函数。
2.既然我们创建了一个脚本类,就可以直接通过类名来传递类中的成员函数(这个类中只有公用的成员函数,是一个工具类)。不过我们在 Bind 函数中一次性调用脚本类的成员函数,只调用一次Bind 便可以自动处理脚本类中的所有函数。
》》为什么 Instance 需要是指针类型?
首先,我们需要接受一个脚本类句柄(也就是一个脚本类对象)来设置脚本组件中的函数,所以我们需要一个 ScriptableEntity 对象。可是这个对象 Instance 为什么需要是指针类型呢?
1.因为我们初始化的时候将其指定为 nullptr(空指针), Instance 不是指针类型的话这样的定义就是错误的。
2.我们使用 new、delete 进行生命周期的管理,这要求 Instance 是指针变量。
》》向下转换的概念及用例
向下转换:指的是将父类的指针或引用转换为子类的指针或引用。
这样做可以方便的操作子类的成员函数。比如此处,T 是 ScriptableEntity 的子类,当 ScriptableEntity* 类型的变量 instance 被传入的时候,instance->只能调用 ScriptableEntity 的成员函数 GetComponent( ) ,如果将 ScriptableEntity* 类型的变量 instance 向下转换为子类 T* 类型的变量,instance-> 就能调用 T 类的成员函数 OnCreate( )。
》》注意:
((T*)instance)和 (T*)instance 是有区别的:
1. ((T*)instance)->OnCreate() : 将 instance 强制转换为 T* 类型,然后通过 -> 运算符调用 T 类型的 OnCreate() 成员函数。
2. (T*)instance->OnCreate():受运算符优先级影响,这行代码首先对 instance 使用 -> 运算符,然后将结果转换为 T* 类型,这并不能调用到 T 类型的成员函数。
》》为什么有的函数需要 [&] 捕获外部变量,有的函数则是 [ ] 不捕获外部变量?
对于 Instantiate 和 DestroyInstance ,他们需要访问 Lambda 表达式作用域之外的 Instance 成员变量,所以使用 [&] 捕获 Lambda 外部变量(通常是 this 指针),并将其作为引用。
对于 OnCreate … ,这三个函数只需要使用传递给他们的参数,没有访问或修改外部变量,因此捕获列表可以为空。
》》》》enTT 中的 view 有一个成员函数 each,这个函数是什么,有什么用,怎么使用?
概述:在 enTT 中, view 用于返回具有特定组件的所有 实体,而 each 函数可以为 view 获取的所有实体执行用户指定的操作。这个操作通常是通过回调函数(比如 lambda 表达式)来实现的。
功能:
each 函数会遍历视图中的所有实体和它们的组件,并对每个实体及其组件执行指定的操作。这个操作是通过传入的回调函数实现的。
参数:
each 函数接受一个回调函数作为参数。回调函数通常是一个 lambda 表达式,表达式中指定两个参数:实体 ID 和对应的组件引用。
ADDED NEW: ( I haven't try that yet) 2024-9-10 21:43
》》》》位移矩阵的[3][0], [3][1], [3][2] 是 x,y,z
---------Native Scripting ( with virtual function) -------
》》》》What is the V-Table?
概念:
在 C++ 中,虚函数表(V-table)是支持多态性的一个机制。当类中包含虚函数时,编译器会为该类生成一个虚函数表。
结构:
虚函数表是一个指针数组,每个元素指向某个类中虚函数的具体实现。
工作原理:
虚函数表的生成: | 每个包含虚函数的类都有一个虚函数表。虚函数表的元素按照虚函数在类中声明的顺序排列,每个元素指向虚函数的实际实现。 |
虚表指针(vptr): | 每个对象实例中包含一个指向其虚函数表的指针(vptr)。这个指针在对象创建时被初始化为指向相应类的虚函数表。 |
函数调用: | 当通过基类指针或引用调用虚函数时,程序会通过 vptr 查找虚函数表,并调用正确的实现。这允许在运行时动态绑定到正确的函数实现,从而实现多态性。 |
示例:
如果两个不同的子类都继承了相同的父类,并对父类中的虚函数进行了不同的定义,这时虚函数表就为不同子类的变量调用相应的虚函数,以防程序运行错误。
》》》》代码理解:为什么移动主相机的时候,幕后的相机也做了相同的位移?(请查看注释以便理解)
为了实现每个实体使用一样的脚本规范,但各自移动的效果,我选择这样的方式:从回调函数中顺便获取 CameraComponent ,然后通过 primary 作为限制条件进行更新。
》》》》这一次提交中
1. 我将脚本类的定义放在其他文件中,然后在EditorLayer.cpp中包含其头文件,这样更加整齐。
2.1 这个脚本类(子类/派生类)的定义我就放在ScriptableEntity(父类/基类)的代码下面,这样更加简洁。
2.2 我将脚本类的声明和定义设置在两个文件中,分离开来。
3. 我现在将脚本组件的更新操作放在Scene.cpp新建的OnScript函数中,而不是放在Scene.cpp的OnUpdate函数中,这样便于理解。
》》我在描述(description)中这样总结:
> Scene.cpp&Scene.h: Separate NativeScript's update from Scene::OnUpdate( ), now it was in new function--->Scene::OnScript( ).
> Scene.cpp: Update functions calling ways.
I also added a condition to make every entity moving individually, check it in view.each().
> Component.h: Changed args in std::funciton ,delete std::function which OnCreateFunction/OnDestroyFunction/OnUpdateFucntion used.
And because of this changes, we also need to change Scene::OnScript( ) so that it can running correctly.
> EditorLayer.cpp: Removed the defination of ScriptCameraController and define it in external file ( Now in ScriptableEntity.h & ScriptableEntity.cpp), looks better.
And we use new script update function:OnScript( ).
> ScriptableEntity.h: Declaring ScriptableEntity class and ScriptCameraController class.
> ScriptableEntity.cpp: Defining ScriptableEntity class and ScriptCameraController class.
译文:
> Scene.cpp&Scene.h:将 NativeScript 的更新与 Scene::OnUpdate( ) 分开,现在它位于新函数--->Scene::OnScript( ) 中。
> Scene.cpp:更新函数调用方式。
我还添加了一个条件,使每个实体单独移动,在 view.each() 中检查它。
> Component.h:更改了 std::funciton 中的参数,删除了 OnCreateFunction/OnDestroyFunction/OnUpdateFucntion 使用的 std::function。
并且由于这些变化,我们还需要更改 Scene::OnScript( ) 以便它能够正确运行。
> EditorLayer.cpp:删除了 ScriptCameraController 的定义并将其定义在外部文件中(现在在 ScriptableEntity.h 和 ScriptableEntity.cpp 中),看起来更好。
并且我们使用了新的脚本更新函数:OnScript( )。
> ScriptableEntity.h:声明 ScriptableEntity 类和 ScriptCameraController 类。
> ScriptableEntity.cpp:定义ScriptableEntity类和ScriptCameraController类。
-------Scene Hierarchy panel-----------
》》》》关于 Trello board:我用其记录了待办的事务,一般是一些维护。
接下来的七八集会比较硬核,比较枯燥(虽然没有这么夸张)。大部分时间在处理 ImGui,我会进行细致的学习。
最近很久没有更新,一部分原因是时间安排,一部分原因是我喜欢先看一部分视频然后再去实现代码,也许我会快节奏的上传代码,也许不会,但我尽量理解所有逻辑。
》》》》Scene Hierarchy:场景的层次结构
》》》》enTT basic_registry::each() 现在还能使用吗?
问题:
为了获取当前注册表中所有实体,我对 entt::registry 调用 each() 成员函数,但实现代码的过程中,我发现 entt::registry 已经没有 each() 成员函数,这让我倍感疑惑。
思考与解释:
在仔细查找与考证之后,我猜想是enTT更新了entt.hpp文件,或者将basic_registry::each()放在其他文件中去了。总之entt.hpp中已经找不到basic_registry::each()的定义。
考证:
Cherno 4 年前使用的 entt.hpp 中,我找到:
可是在2024-6-11日更新的entt.hpp中,没有basic_registry作用域下的each()函数,只有basic_view、basic_group等作用域下的each()函数
所以我不得已改变之前的实现方法。
解决方案1:
由于我们在 CreateEntity() 函数中对每一个实体都默认添加了 TagComponent,所以我通过 view() 获取所有包含 TagComponent 的实体(既全部实体),然后通过 each() 函数遍历这些实体。效果与之前相当。
值得一提的是:
m_Context->m_Registry.view<>(); 这样的代码在语法上应该是正确的,但在实际使用中不允许空模板参数,我不是很明白为什么。
解决方案2:(正解)
其实YouTube下的评论还是很有帮助的
》》帖子/文档
查看论坛: (c++ - How can I access all entities in an entt::registry? - Stack Overflow )为啥昨天就没看到这个帖子呢?
》》》》下载doxygen: https://www.doxygen.nl/
- 先前版本的doxygen文档:
不知道怎么的我翻阅到entt先前的文档,然后查阅了先前的设计。找到了这个阴魂不散的each
不过这好像是entt.hpp之前版本的数据,所以each()的使用方法参考性不大。于是我决定下载一个doxygen看看能不能识别一下现如今最新的entt.hpp.
下载网址: https://www.doxygen.nl/ (可以查看笔记“概念与操作”中的:这里有细致步骤 》》》》doxygen 下载doxygen: https://www.doxygen.nl/ )
- 最新版本的 enTT 构架:
进入浏览器,按照指示选项卡打开,查看
在所有class中,我们找到basic_registry,并打开。
在这里,你可以查找类型、成员函数等等。
可以看到,最新版本的 entt 中,entt::basic_registry 并没有 each() 函数
》》》》ImGui::TreeNodeEx() 函数
概念:
ImGui::TreeNodeEx 用于在 ImGui 的 UI 中创建一个树节点,这个节点可以显示为展开或折叠状态,并且可以包含子节点。这个函数也具有一些额外的功能,允许你设置节点的样式、图标、标志等。
函数签名
bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0);
bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* label);
参数说明
ptr_id: | 用于指定树节点的唯一 ID。通常情况下,你可以使用 label 作为 ID,如果你有自定义的 ID 则可以使用这个参数。 |
flags: | 树节点的标志,用于控制节点的行为(如是否可以被折叠、是否显示图标等)。 |
label: | 树节点的标签,显示在节点的前面。 |
函数返回值:
ImGui::TreeNodeEx 的返回值表示该节点是否被展开。具体来说:
返回 true, | 则节点被展开,意味着可以显示其子节点。 |
返回 false, | 则节点未展开,子节点不可见。 |
常用的 ImGuiTreeNodeFlags 标志
ImGuiTreeNodeFlags_DefaultOpen: | 默认节点是展开的。 |
ImGuiTreeNodeFlags_Framed: | 节点带有边框。 |
ImGuiTreeNodeFlags_SpanAllColumns: | 节点标签占据整行。 |
ImGuiTreeNodeFlags_Leaf: | 节点是叶子节点(没有子节点)。 |
ImGuiTreeNodeFlags_Bullet: | 显示子节点的标记(如圆点)。 |
MORE |
》》为什么 Flags 的定义是 1<<0 的形式 ?为什么设置 Flags 的时候需要将上述 Flag 进行或( '|' )运算?
1<<0 表示 1 这个二进制数字左移 0 个单位,1 << 1 指 1 左移 1 个单位,1 << 2 同理。所有Flag 被定义成二进制(0000, 0001, 0010,0100,1000 … 直到1000 0000 0000 0000)。
这时如果对其中几个 Flag 进行或运算,得到的结果正好可以包含所选取的 Flag 数据。(Eg: 或运算 (OR): 001 | 101 ----> 101 )
》》》》Flags 中的三元运算符是为了计算什么结果,为了达到什么效果?
前提:在后续的代码中,我们通过代码得知使用者是否用鼠标单击确定了某一个实体,如果该实体被选中,m_SelectionContext就会被更新为选中的实体。
解释:
在实体未被选中前,entity有值,但m_SelectionContext为空,所以 Flags 中的 ImGuiTreeNodeFlags_Selected 不会相应。如果实体被选中,则三元运算符成立, ImGuiTreeNodeFlags_Selected 相应,效果是高亮指定节点。
》》》》树节点的ID为什么需要这样转换?
- (uint64_t)entity:
将 uint32_t 类型的 entity 转换为 uint64_t 类型。这样做的目的是将 entity 转换为一个较大的整数类型,避免在某些平台上直接将 uint32_t 转为 void* 时可能出现的问题。
(例如,64 位系统上的 void* 大小通常是 64 位,而 32 位系统上的 void* 大小是 32 位。为了确保在所有平台上转换的一致性,将 uint32_t 转换为 uint64_t 可以避免由于直接将 32 位值强制转换为 64 位指针可能出现的数据丢失或对齐问题。)
- (void*)(uint64_t)entity:
将 uint64_t 类型的 entity 转换为 void*。这是因为 ImGui 的 TreeNodeEx 函数要求节点 ID 是 void* 类型的。
》》》》在运算符 != 的设计中,return !(*this == other);是什么意思?为什么?
1. return !(*this == other); 这句代码的意思是什么?
*this == other | 表示调用刚才设计的 operator== 方法来检查当前对象和 other 对象是否相等。 |
return !(*this == other); | 这个表达式表示:返回当前对象和 other 对象是否不相等的结果。 |
2.为什么在定义中使用 *this 而不是 this?
this | This 在 c++ 中一般是指向对象的指针,在这里是指向当前 Entity 对象的指针(Nut::Entity*)。 |
*this | *在这里起到解引用的作用,指的是解引用 this 指针之后得到的当前对象的引用 (Nut::Entity)。 |
而我们的运算符重载是作用在 Nut::Entity 上的。
》》》》Where? Here! (我在哪里用过这个运算符重载来着?)
》》》》为什么在初始化 entity 的时候,填入的参数需要是Ref<Scene>::get() ?
分析:
Entity类在初始化的时候需要填入uint32_t类型的ID 和 Scene*类型的场景指针,但是 m_Context 现在是 Ref<Scene>,这是一个智能指针 std::shared_ptr, 而不是Scene* 这种裸指针。
解决方案:
也就是说Ref<Scene>现在是不可用的指针类型,所以我们需要获取指向 scene 类型对象的原始指针:Scene*,这就需要使用 std::_Ptr_base<Nut::Scene>::get() 函数。
定义:
》》》》一个想法:我觉得没必要让现有的两个摄像机独立移动。
因为在游戏引擎的实际使用中(例如Unity),你在世界空间(world camera)下对所有物体进行的调整,在其他摄像机视角下都应该是同步的。
Eg.你在世界空间中调整了游戏角色 'Cheryes' 的初始位置,那么在游戏运行时,你使用的运行时摄像机所看到的,也应该是游戏角色 'Cheryes' 被调整后的位置。
》》》》注意:需要确保在绘制 TreeNode 之后调用 ImGui::IsItemClicked(),否则导致绘制错误(当鼠标单击一个节点时,该节点之下的节点也会高亮响应)
WRONG
RIGHT
AND… ImGui::IsItemClicked() 是个函数
绘制错误原因:
当你调用 ImGui::IsItemClicked() 时,它检查的是当前帧中是否有用户点击事件发生在上一个绘制的项上。
如果你在绘制 TreeNode 之前调用 IsItemClicked(),它会检测点击事件,但是此时 TreeNode 还没有实际绘制出来。因此,这个函数可能会错误地报告点击状态,因为它基于之前的状态,而不是你当前正在绘制的节点。
结论(解决方案):
所以确保在绘制 TreeNode 之后调用 ImGui::IsItemClicked(),这样你可以确保点击状态是基于实际绘制的节点。
----------Properties Panel--------------
》》》》ImGui::InputText( )
函数释义:
ImGui::InputText 是 ImGui 的一个函数,用于显示一个文本输入框。
函数签名:
ImGui::InputText("Tag", buffer, sizeof(buffer));
函数参数:
"Tag" 是输入框的标签,用于在界面上标识输入框。
buffer 是一个字符数组(或类似的缓冲区),用于临时存储用户在输入框中输入的文本。
sizeof(buffer) 是缓冲区的大小,确保输入文本不会超过这个大小。
函数返回值:
ImGui::InputText 函数会返回一个布尔值(bool)。
如果用户在输入框中更改了内容,返回值为 true;如果用户没有更改内容或者输入框没有被操作,返回值为 false。
所以我们使用条件判断是否被更改,若被更改,则对 tag 进行数据更新。
if ( ImGui::InputText("Tag", buffer, sizeof(buffer) ) )
{
tag = std::string(buffer);
}
》》》》(void*)typeid(TransformComponent).hash_code() 的作用
- typeid 运算符
功能: | typeid 是一个运算符,用于在运行时获取对象或类型的类型信息。它返回一个 std::type_info 对象,该对象包含了类型的元数据。 |
用法: | typeid(Expression) 可以用来获取一个表达式的类型信息,typeid(Type) 获取一个类型的类型信息。 |
参数注意: | 参数必须是一个类型或表达式 |
- std::type_info
定义: | std::type_info 是一个类,用于描述 C++ 中的类型。 |
主要功能: | name(): 返回一个指向表示类型名称的 C 风格字符串的指针。 hash_code(): 返回一个无符号整数,表示类型的哈希值。这个值是类型的唯一标识符.( 其具体值和算法是实现定义的,不同的编译器可能会有不同的实现 ) |
- typeid().hash_code() 的作用
唯一标识: | typeid(Type).hash_code() 生成一个类型的哈希值,可以用作唯一标识符。这在需要对类型进行区分的情况下非常有用,比如 ImGui 的树节点 ID。 |
稳定性: | hash_code() 返回某个类型的唯一标识(唯一哈希值),这在同一程序的执行期间是稳定的,即同一类型的哈希值不会改变。 |
- 关系和使用
typeid 生成 std::type_info 对象: | typeid 运算符生成一个 std::type_info 对象,通过它可以访问类型的信息。 |
hash_code 用于获取哈希值: | std::type_info 对象的 hash_code() 成员函数用于获取该类型的哈希值。通常用于类型的唯一标识。 |
》》什么是元数据?
概念:元数据(Metadata)是指关于数据的数据。它提供了关于某一数据集的结构、内容、格式、来源和上下文等信息。元数据通常用来帮助用户理解和管理数据,方便数据的搜索、组织和使用。
》》》》ImGui::IsMouseDown( ) 的定义?
参数的定义:注意查看注释。
》》IsMouseDown 和 IsMouseClicked 的区别
IsMouseDown() 的效果是敲击一次鼠标,便一直执行某事件直至条件改变。
而IsMouseClicked()的效果是敲击一次鼠标,则某事件运行只一次,随后停止等待下一次鼠标的敲击(即事件只发生在函数触发的那一帧)。
》》》》strcpy 和 strcpy_s 的区别
strcpy: |
|
strcpy_s: |
|
》》》》待改进:
当你在Scene Hierarchy 窗口中单击鼠标左键时,有两种情况:
此时鼠标在窗口的节点上单击 | 则m_SelectionContext被更新为Entity |
鼠标在窗口内的其他地方单击(比如空白处) | 则m_SelectionContext被更新为空 |
但是当单击节点所处行时,本应高亮的节点行在更新过程中取消了高亮,即节点所处行中,有部分范围被认为是空白,而不认为该范围属于节点。
》》值得一提:
Clip-Camera 在此时,ImGui::DragFloat 对图像的位移不起作用,因为在 Scene.cpp 中 Scene::OnUpdate() 只对主相机进行渲染,而Clip-Camera是第二相机(Primary 为 false),所以数据上的更改不会体现出来,看起来像是图像没有移动一样。
-------Camera component UI---------
》》》》一些函数的概念:
ImGui::BeginCombo( ) | 释义:用于创建下拉选择框 签名:bool ImGui::BeginCombo(const char* label, const char* previewValue, ImGuiComboFlags flags = 0); 参数:
下拉框的标签,用于识别这个组合框。
显示在组合框上方的预览值,通常是当前选中的项的名称。
用于设置组合框的行为,如是否支持多选、是否显示箭头等。 可以使用以下标志: ImGuiComboFlags_None:无标志。 ImGuiComboFlags_PopupAlignLeft:对齐方式。 ImGuiComboFlags_NoPreview:不显示预览。 其他可用的组合标志。 返回值:返回值指示组合框是否被打开。 如果返回 true,表示组合框当前处于打开状态。 如果返回 false,则表示组合框未打开。 |
ImGui::SetItemDefaultFocus( ) | 释义:用于设置当前项目为默认聚焦项的函数。 在弹出窗口或菜单打开时,调用此函数可以确保某个特定的选项在用户打开时即被高亮显示,从而提高用户体验。 签名:bool ImGui::Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, ImVec2 size = ImVec2(0, 0)); 参数:
可选择项的文本标签。
布尔值,指示该项是否应被视为选中。
可选的标志,用于控制项的行为,例如是否允许多选。
可选择项的大小。 返回值:没有返回值。 它的主要作用是设置当前项为默认聚焦项,使其在下次导航时自动获得焦点。 |
ImGui::Selectable( ) | 释义:用于创建可选择项的函数。 它通常用于列表、菜单或组合框中,让用户能够选择其中的一项。 签名:bool ImGui::Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, ImVec2 size = ImVec2(0, 0)); 参数:
显示在可选择项旁边的文本标签。
指示该项当前是否被选中(默认为 false)。
用于控制可选择项的行为的标志,例如是否允许多选、是否禁用等。
指定可选择项的大小(默认为 (0, 0),表示自动适应)。 返回值: true 表示该项被选中(用户点击了该项),否则返回 false。 |
ImGui::Checkbox( ) | 释义:用于创建复选框的函数,允许用户在两种状态之间切换(选中或未选中)。 它的主要用途是获取布尔值输入,通常用于设置开关或选项。 签名:bool ImGui::Checkbox(const char* label, bool* v) 参数:
复选框的标签,显示在复选框旁边。
指向一个布尔变量的指针,用于存储复选框的状态。 返回值:该函数会更新传入的布尔变量,反映复选框的当前状态。 |
》》》》使用提示:(视频中Cherno并没有遇到这种问题,可能是我的疏漏)
两个摄像机只有一个可以被标识为主相机,而我们对当前的主相机进行操作,此时需要注意:使用perspective (透视投影)的摄像机类型时,为了呈现出正确结果(或者说在合适的视角下才能观察到透视中的深度变换),我们需要进行一些操作。
无论你是先对物体进行位移,还是先对摄像机实体进行透视的转换,你都需要在第一次切换摄像机之后拉伸一下屏幕中的视口,然后调整摄像机实体的 z 轴。
导致这个问题的原因是没有刷新视口。(不过依旧要调整一下矩阵的 z 轴才能看到正确结果)
具体原因是这样的,我们知道在 EditorLayer 中,只有在“Viewport”窗口被调整大小的时候,才会调用 OnViewportResize( ) 函数来更新视口中的渲染结果。
但是在我们对投影矩阵进行切换的时候,"viewport" 窗口的大小并没有变化,这导致此时并不会对视口中的渲染结果进行更新。
所以我们需要在 combo box 切换矩阵类型的时候,对视口进行更新,尽管此时没有调整 "viewport" 窗口的大小。
》》为了解决这个问题,我们需要在 Hierarchy Panel 中获取到正确的 "Viewport" 窗口大小,然后根据大小进行视口的更新。
我便在 EditorLayer 中创建了一个单例,然后在 Hierarchy Panel 中通过这个单例使用 EditorLayer 的成员函数。
你可能会问我为什么这么做?
这样做的原因是,我们使用窗口 Viewport 并在其中使用 ImGui::Image() 来渲染帧缓冲中的结果,那么如果要对视口(对渲染结果)进行刷新, 我们就需要获取到 Viewport 窗口的 Size。
如果不在 ImGui::Begin("viewport") 和 ImGui::End() 之间使用获取大小的函数:ImGui::GetContentRegionAvail(), 反而在其他窗口的范围内使用ImGui::GetContentRegionAvail() ,就会导致获取的大小来自其他窗口,这是错误的。
所以我需要从 EditorLayer 中拿到正确的 ViewportSize,然后在 HierarchyPanel 中对当下的场景 m_Context 使用 OnViewportResize() 函数,这样便能在调换矩阵类型的时候,对渲染结果进行更新。
》》》》一些设计上的闲聊:(关于 Combo box 的高亮/焦点显示)
我总觉得 ImGui::SetItemDefaultFocus() 有点多余,因为我认为 ImGui::SetItemDefaultFocus() 和高亮显示一个选项的效果差不多,这恰好和 ImGui::Seletable() 的第二个参数重复。
但是 SetItemDefaultFocus() 实际上是一个用于设置当前项为默认焦点的函数,焦点和高亮是有区别的。(当你打开 Combo Box,不使用鼠标选择选项,而是用键盘上下方向键选择选项,你就会发现不同)
Eg.焦点1
Eg.焦点2
Eg.高亮1
Eg.高亮2
》》设计闲聊(关于单例的 Get() 函数)
》Instance 是一个指针类型的变量,那么在 static EditorLayer& Get(){ return *s_Instance; } 中, '*s_Instance' 和 'EditorLayer&'中的 '*','&' 是什么意思?有什么作用?
- *s_Instance
前提: s_Instance 是一个指向 EditorLayer 类型的指针。
解释: 使用 * 操作符对 s_Instance 进行解引用,意味着我们获取的是指针所指向的对象。如果 s_Instance 是一个有效的指针,那么 *s_Instance 将返回一个 EditorLayer 类型的对象的引用。
- EditorLayer&
EditorLayer& 是一个引用类型,表示函数返回一个引用类型的变量。而且返回引用可以避免复制对象,并且允许调用者直接操作原始对象。
》》》》什么是透视矩阵中的FOV?什么是 Vertical FOV?什么是 Horizontal FOV?
You Can Check The Image With https://images.app.goo.gl/ZfQ9JsY8txMRdbL78
定义:
在许多3D应用和游戏中,FOV 常用于表示相机的整体视野,但在一些上下文中,尤其是需要精确控制的情况下,会特别指出 Vertical FOV 和 Horizontal FOV。
-
Vertical FOV:
专指相机在垂直方向上的视场角,影响场景在上下方向的可视范围。
Horizontal FOV:
专指相机在水平方向上的视场角,影响场景左右方向的可视范围。
计算:
不过垂直视场角和水平视场角是可以相互转换计算的:比如
常见的垂直视场角值
一般推荐值: | 对于大多数3D应用和游戏,垂直视场角通常在 60° 到 90° 之间。 |
60°: | 适用于较为逼真的视图,常用于模拟和一些角色扮演游戏。 |
75°-90°: | 更广的视野,适合快节奏的第一人称射击游戏。 |
改变 FOV 的影响
-
更大 FOV:
效果:可以看到更多的场景,适合广角镜头效果。
缺点:可能导致物体在视野边缘变形(视角畸变)。
更小 FOV:
效果:视野范围变窄,适合强调特定对象或细节。
缺点:提供更逼真的透视效果,但会使场景看起来更拥挤。
--------Drawing component UI----------
》》》》关于图像混合出错的情况(大约在原视频14分钟)
在代码中,RedSquare 是后绘制的实体。这导致 Alpha 值设置的透明度只有 Blue Square 能够使用出来
问题理解:
关于混合的相关知识,可以查看:( 混合 - LearnOpenGL CN)
--------Transform Component UI------
》》》》这一集的难点大多涉及到 ImGui 函数的使用和 ImGui 函数的设计逻辑,So let's check that out.
》》》》我有一个疑问,为什么这里需要将 Rotation 处理三次?
Glm::rotate() 函数这是因为我们现在需要处理图像绕三个轴旋转的情况,而不是像之前一样只处理绕Z轴旋转的情况了。
之前的:
现在的:
》》》》一些函数:
ImGui::PushID() | 是一个用于为 ImGui 界面元素生成唯一 ID 的函数。 参数:可以接受一个整数、字符串或指针,推送一个 ID 到栈中,使得后续的 ImGui 元素在使用相同的 ID 时不会发生冲突。 |
提示: | 使用 PushID( ) 和相应的 PopID( ) 可以帮助你管理和区分不同的元素,特别是在循环或复杂的界面中。因为这样可以确保每个元素的状态(如焦点、选中状态等)是独立的。 |
ImGui::Columns(2); | 是一个用于创建多列布局的函数,它会将当前窗口分成指定数量的列(在这里是 2 列)。在调用此函数后,接下来的 ImGui 元素会按列布局。 |
ImGui::NextColumn(); | 用于切换到下一列,允许你在定义的列中添加更多元素。使用它时,如果你在第一列后调用 NextColumn(),将会移动到第二列,继续添加元素。 |
ImGui::SetColumnWidth(0, columnWidth); | 用于特定列宽度的函数。 参数: columnIndex:指定要设置宽度的列的索引(从 0 开始)。 width:要设置的列宽度,单位为像素。 这个函数通常在设置列布局后使用,以确保列的宽度符合你的需求。 |
使用示例: |
|
ImGui::PushMultiItemsWidths (3, ImGui::CalcItemWidth()) | 用于设置多个 ImGui 元素的宽度的函数。具体来说,它能够为后续的多个控件(在这里是 3 个)推送一个统一的宽度设置。(一般是为每个控件平均分配填入的总宽度) 第一个参数 itemCount:表示你将要设置宽度的项目数量。 第二个参数 itemWidth:每个控件的宽度。此处填 ImGui::CalcItemWidth(),这个宽度通常会基于当前窗口的大小和布局返回数值 统一宽度:这个函数可以确保多个相关元素(例如多个输入框或按钮)在视觉上对齐,提供更好的体验。 动态调整:使用 CalcItemWidth() 使得宽度根据窗口大小自动调整,避免在窗口大小变化时出现布局问题。
|
使用示例: |
|
ImGui::CalcItemWidth() | 用于计算当前控件宽度的函数,它根据当前布局和可用空间动态返回一个合适的宽度值。 |
PushStyleVar 和 PushStyleColor 在修改样式上的不同 | PushStyleVar功能: 用于推送一个样式变量的值,通常是用于控制布局和控件的外观。 PushStyleColor功能:用于推送一个颜色样式变量的值,通常是用于修改控件的颜色。 |
ImGui::PushStyleVar( ) | ImGuiStyleVar_ItemSpacing:这是一个样式变量,表示项目之间的间距。 ImVec2{ 0, 0 }:这个值表示项目之间的水平和垂直间距都设置为 0。 用途: 通过设置 ItemSpacing 为 0,可以使得元素之间没有间距,从而使它们更加紧凑,适用于希望节省空间的布局。 示例:
|
ImGui::PushStyleColor( ) | ImGuiCol_Button:指定要改变颜色的元素,这里是按钮。 ImVec4{ 0.8f, 0.1f, 0.15f, 1.0f }:表示按钮的新颜色,使用 RGBA 格式。 用途: 通过改变按钮的颜色,可以使界面更具个性化,或者用于强调特定的操作。 示例:
|
提示: | 记得在适当的时候调用 PopStyleVar 或 PopStyleColor 来恢复之前的设置。 |
注意: | 如何确定函数设计的样式是哪一个范围的? 堆栈界限:ImGui::PushStyleColor() 使用堆栈的概念来管理样式。每次调用 PushStyleColor() 时,它会将当前颜色推入堆栈。对应的 PopStyleColor() 调用则会弹出最近的颜色设置。 堆栈的界限是通过调用的次数来管理的,因此你可以通过调用 PopStyleColor() 来返回到之前的状态。 是否需要将 PushStyleColor( )/PushStyleVar( ) 写在实际渲染控件的代码之前? 调用顺序:是的,PushStyleColor() 需要在控件绘制之前调用。这样可以确保在该控件绘制期间使用新的颜色设置。如果你在控件绘制后调用,效果将不会反映在该控件上。因此,正确的顺序是先推送样式,然后绘制控件,最后弹出样式。 |
参数参考表1: | |
参数参考表2: |
ImGui::SameLine( ) | 用于将后续控件放置在同一行的函数。 它的主要作用是让用户在同一行上排列多个控件,而不是默认的垂直排列。 使用方法:在调用 SameLine() 之后,紧接着的控件将会被放置在上一控件的右侧。你可以在任何需要的地方调用这个函数,只要你想要控件在同一行显示。 |
示例 |
|
》》》》关于 ImGui::DragFloat("##X", &values.x); 中的 ##X
什么是 ImGui 中的 ## ?
1. 控件标识符的意义
在 ImGui 中,每个控件(例如按钮、滑块等)需要一个唯一的标识符。这个标识符能够帮助 ImGui 跟踪每个控件的状态、位置和其他属性。
如果没有唯一标识符,ImGui 将无法正确管理多个控件的状态,尤其是在同一窗口中动态生成多个相似控件时。
举个例子:
如果多个控件使用相同的视觉标签(如 "Button"),ImGui 将只会创建一个控件,而不是多个。这意味着只有最后一个控件的状态会被保留,前面的控件将被覆盖。
通过使用 ##,即使视觉标签相同,只要标识符不同,ImGui 就会将它们视为独立的控件。
2. ## 的原理
将视觉标签与内部标识符分离:
使用 ## 用来分隔控件的显示标签和其标识符。例如,"Button##UniqueID" 中,"Button" 是用户在界面上看到的文本,而 UniqueID 是 ImGui 用来跟踪该控件的内部标识符。
3. 实际示例
ImGui::Button("Button##1");
ImGui::Button("Button##2");
在这个例子中:
第一个按钮的显示文本是 "Button",但它的唯一标识符是 "Button##1"。
第二个按钮的显示文本也是 "Button",但它的唯一标识符是 "Button##2"。
4. ## 可以单独使用吗,比如 ##X ? 此时控件上显示什么?
##X 可以单独使用。在这种情况下,控件上不会显示任何文本,因为 ## 前面没有标签。控件的标识符是 X
》》》》关于按钮尺寸 ButtonSize 的设计:
float lineHeight = GImGui->Font->FontSize + GImGui->Style.FramePadding.y * 2.0f;
ImVec2 buttonSize = { lineHeight + 3.0f, lineHeight };
代码解析
计算 lineHeight:(行高)
GImGui->Font->FontSize: | 这是当前使用的字体的大小。文本的高度实际上也是按钮的基本高度的一部分。 |
GImGui->Style.FramePadding.y: | 这是按钮的垂直内边距(padding),用于给按钮内部内容(如文本)留出空间。FramePadding.y 的值会增加到按钮的高度中,以确保文本不会紧贴按钮的边缘。 |
GImGui->Style.FramePadding.y * 2.0f: | 这里乘以 2 是因为按钮的顶部和底部都有同样的内边距,以此来确保文本在按钮中间且上下边界不与按钮的上下边缘粘连。 |
计算 buttonSize:(按钮大小)
buttonSize.x (按钮宽度)设置为 lineHeight + 3.0f,这里的 3.0f 是额外增加的宽度,视觉效果更好,一定的区域大小也方便使用者点击。
buttonSize.y (按钮高度)设置为 lineHeight。
》》》》GImGui 是什么?
GImGui 在 ImGui 中是一个全局变量,类型为一个指向 ImGui 上下文的指针。GImGui 通常用于访问 ImGui 的状态和功能,它允许你在自定义的代码中直接访问 ImGui 的内部数据和功能。
( In imgui.cpp)
(Part of ImGuiContext in imgui_internal.cpp)
》》》》标准化颜色选择器:
https://rgbcolorpicker.com/ is very good.
》》》》度数转化的设计:
前两步就是把数据进行单位上的转换,然后交给一个临时变量,通过使用 DragFloat 控制临时变量(以度数为单位的角度)实现手动调整数据的交互操作。此时虽然在控件上我们调整的是度数制的角度,但是我们可以获取到弧度制之下角度的数值变化。
第三步是重点:将调整过后的角度(度数制)转换到弧度制上,然后重新传递给位移组件(现在应该称之为变换组件)中的旋转成员变量 Rotation,如此一来我们成功的调整了角度。调整旋转角度之后的图像将会在 OnUpdate() 函数调用之后呈现出渲染结果。
---- Adding/Removing Entity UI & Adding/Removing Component UI-----
》》》》Check-List
》》》》我突然意识到,摄像机和物体的移动逻辑应该是相反的(虽然我不知道这是否需要实现)
(比如按下 A 键,对于物体来说是向右移,但对于摄像机来说是左移。对于同一个物体来说,移动物体和移动摄像机的逻辑应该是相反的)
Eg.
Camera类中:
我们为Camera类型的摄像机mainCamera传入了求逆(inverse)之后的矩阵
OrthographicCamera 中:
OrthographicCamera类中 Update() 函数也进行了求逆操作:使用逆矩阵为正交相机类型的摄像机更新内置的视图矩阵。
》》》》ImGui 函数:
BeginPopupContextItem( ) | 用于创建上下文菜单(Context menu)的入口。它通常在一个 UI 元素(如按钮或其他可交互控件)被右击时使用,以便显示相应的弹出菜单。适用于需要为特定 UI 元素提供右键操作的场景。 label: 一个字符串,用作弹出菜单的标识符,通常传入一个唯一的字符串。 此函数通常与 ImGui::BeginPopup( ) 和 ImGui::EndPopup( ) 配合使用。 上下文关联: ImGui::BeginPopupContextItem( ) 会根据上一个渲染的控件(如按钮、输入框等)来确定触发上下文菜单的控件。你可以在调用该函数之前渲染你的控件,这样 ImGui 可以知道哪个控件被右键点击。 传递标识符: 当你调用 BeginPopupContextItem("MyPopup") 时,"MyPopup" 是一个唯一的标识符,用于识别这个弹出菜单。ImGui 内部会跟踪这个标识符和最近的控件之间的关系。
|
ImGui::MenuItem( ) | 用于在菜单或上下文菜单中创建一个可选择的菜单项,构建复杂的菜单结构。用户可以点击这些菜单项来执行相关的操作。 label: 菜单项的显示文本。 selected:(可选)如果设置为 true,菜单项将被标记为选中。 enabled:(可选)如果设置为 false,菜单项将变为不可用(灰色)。 支持动态状态(例如,是否选中或启用)。 可以与 ImGui::BeginMenu 和 ImGui::EndMenu 配合使用,形成完整的菜单。
|
用法示例:
示例1:
// 创建一个按钮
if (ImGui::Button("Right Click Me"))
{
// 按钮被点击时可能会处理某些操作
}
// 按钮检测右键点击,然后开始打开上下文菜单(Context Menu)
if (ImGui::BeginPopupContextItem("MyContextMenu"))
{
if (ImGui::MenuItem("Option 1"))
{
// 处理选项1
}
if (ImGui::MenuItem("Option 2"))
{
// 处理选项2
}
ImGui::EndPopup();
}
示例2:(引出一些疑问:)
// 创建一个按钮
if (ImGui::Button("Add Component"))
// 按钮检测鼠标点击,然后打开弹出菜单(Popup Menu)
ImGui::OpenPopup("AddComponent");
// 如果OpenPopup() 允许打开窗口
if (ImGui::BeginPopup("AddComponent"))
{
if (ImGui::MenuItem("Camera"))
{
// 处理选项1
ImGui::CloseCurrentPopup();
}
if (ImGui::MenuItem("Sprite Renderer"))
{
// 处理选项2
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
》》》OpenPopup( )有什么用?和BeginPopup()有什么关系?
ImGui::OpenPopup( ) | 标记一个弹出窗口(Popup)为“打开”状态。 该函数是 void,没有返回任何值。 当你调用 ImGui::OpenPopup(name) 时,它会将指定名称的弹出窗口设置为打开,并允许在下一帧中渲染。虽然该函数没有返回值,但它的调用效果会影响后续的窗口逻辑。
|
ImGui::BeginPopup( ) | 函数用于创建一个弹出窗口(Popup Menu) const char* name:弹出窗口的唯一标识符。通常是一个字符串,必须在同一帧中保持唯一。你可以使用字符串字面量或字符串变量。 ImGuiWindowFlags flags = 0:可选的窗口标志。可以用来设置弹出窗口的一些属性,比如是否可关闭、是否有滚动条等。常用的标志包括: ImGuiWindowFlags_NoTitleBar:没有标题栏。 ImGuiWindowFlags_AlwaysAutoResize:窗口自动调整大小以适应内容。 ImGuiWindowFlags_Modal:模态弹窗。 bool:函数返回一个布尔值,表示弹出窗口是否成功打开。如果弹出窗口被成功打开,则返回 true,否则返回 false。 如果弹出窗口的名字与其他窗口重复,或者该弹出窗口没有被显示(例如,没有在之前调用 ImGui::OpenPopup(name)),则会返回 false。
|
关系: | ImGui::BeginPopup( ) 与 ImGui::OpenPopup( ) 存在一定关系,并且会受到其影响。 ImGui::BeginPopup( ) 返回值受 OpenPopup( ) 影响。 如果指定名称的弹出菜单已经被请求打开(即之前调用过 ImGui::OpenPopup("menu_name")),并且该菜单尚未关闭,则 BeginPopup 返回 true,允许你在其内部渲染菜单项。 ImGui::OpenPopup( ) 是请求打开某个弹出菜单的函数。只有在调用了这个函数后,对应的 ImGui::BeginPopup( ) 才会返回 true,并开始渲染菜单。 如果你没有调用 OpenPopup( ),即使你调用了 BeginPopup( ),它也会返回 false,因此无法进入菜单项的渲染逻辑。
|
》》》BeginPopupContextItem() 和 BeginPopup() 打开菜单的逻辑有什么不同?
为什么 BeginPopupContextItem() 可以直接在目标控件之后调用,而 BeginPopup() 还需要额外使用 OpenPopup() 判断控件是否允许打开菜单?BeginPopupContextItem() 不需要 OpenPopup() 这个函数吗?
触发机制:
- BeginPopup():
需要程序员在代码中定义何时打开弹出菜单,通常结合其他控件的逻辑(例如按钮点击之后,使用 OpenPopup() 允许菜单打开)。
- BeginPopupContextItem():
不需要程序员在代码中定义何时打开弹出菜单,函数会自动处理右键点击事件,适合用在按钮、文本等控件上,但无需按钮添加额外的事件处理逻辑(只要在控件之后使用 BeginPopupContextItem( ) 即可 )。
总结:
所以说,BeginPopupContextItem() 打开菜单的逻辑是自动的。BeginPopupContextItem() 不需要在前面显式调用 ImGui::OpenPopup("MyPopup"),因为它会自动在用户右键点击时打开对应的弹出菜单。
而 BeginPopup() 打开菜单的逻辑是手动的,BeginPopup() 需要在之前显式调用 OpenPopup(),控件触发之后得到打开菜单的许可,为按钮添加上打开菜单的逻辑。
》》》CloseCurrentPopup() 和EndPopup() 分别是什么函数?有什么作用?有什么不同,为什么要分别调用?
ImGui::CloseCurrentPopup() | 该函数用于关闭当前打开的弹出菜单(popup)。 void ImGui::CloseCurrentPopup(); 无参数: CloseCurrentPopup() 不接受任何参数。 无返回值: 此函数没有返回值 (void)。它的作用是直接关闭当前活动的弹出菜单或模态窗口。 用途: 当你希望在某个交互(例如点击某个按钮或进行其他操作)后关闭当前的弹出菜单时,可以调用此函数。 上下文: 该函数只能在当前弹出菜单的上下文中调用。如果没有打开的弹出菜单,它不会产生任何效果。
|
ImGui::EndPopup() | 该函数用于结束弹出菜单的定义。 void ImGui::EndPopup(); 无参数: EndPopup() 不接受任何参数。 无返回值: 此函数没有返回值 (void)。它的作用是结束当前的弹出菜单或模态窗口的渲染。 用途: EndPopup() 用于标记弹出菜单的结束。每次调用 BeginPopup() 时,必须对应一次调用 EndPopup(),以确保正确的渲染和状态管理。 上下文: 该函数只能在匹配的 BeginPopup() 调用之后使用。如果没有在 BeginPopup() 的上下文中调用 EndPopup(),则可能导致未定义行为。
|
函数的不同之处: | 使用场景: 当用户选择了某个菜单项并执行操作之后(例如 "Camera" 或 "Sprite Renderer"),可以调用这个函数关闭菜单。它会立即终止当前弹出菜单的显示。 使用场景: 在你调用 ImGui::BeginPopup() 后,需要用这个函数来标记菜单的结束。这是结构性要求,确保 ImGui 知道你已经完成了对该弹出菜单内容的定义。
|
ImGui::CloseCurrentPopup(); | 用来关闭当前显示的弹出菜单。注重于交互行为(渲染要求:关闭菜单显示,不再渲染菜单) |
ImGui::EndPopup(); | 用来结束当前代码中菜单的定义。注重于框架结构(代码要求:标记菜单结束,进行后台处理) |
注意: | 通常情况下,CloseCurrentPopup 是在某个选择操作后调用的,而 EndPopup 则是在所有菜单项定义完成后调用的。 |
《《《 拓展:(Popup Menu 和 Context Menu )Popup Menu 和 Context Menu 是两种常见的用户界面元素、
Popup Menu | Popup Menu 是一种浮动的菜单,可以在用户界面中的任何位置显示,用来提供额外的选项或功能。 通常与当前上下文无关,与某个操作(如按钮点击、特定事件)相关联。 通过用户的点击操作(如按钮、图标等)来触发,需要手动添加触发逻辑。
用户点击一个按钮,弹出一个菜单,包含多个操作选项。
|
Context Menu | Context Menu 是一种特殊类型的 Popup Menu,在用户鼠标点击的具体位置打开,提供与所点击对象直接相关的操作。 它与用户当前的上下文密切相关(用户当前正在操作或关注的对象)。
通过右键点击某个 UI 元素(如文件、文本、图标等)来打开,不需要手动添加逻辑。
用户在桌面上右键点击一个文件,弹出一个菜单,显示“打开”、“删除”、“重命名”等与该文件相关的选项。
|
》》》》发现一个问题:(关于代码为什么没有中断)
前提:在 DrawEntityNode() 函数中,我们会在删除实体之后将 m_SelectionContext 同步清空。
原因:这是因为如果在某种情况下触发事件,使得 DrawEntityNode() 删除了 entity,DrawComponents() 函数中使用的 m_SelecationContext 会因为访问已经被销毁的内存而导致中断错误。
(DrawEntityNode() 中,我们使 m_SelectionContext = entity; 如果删除了 entity,那么 m_SelectionContext 会因为访问被销毁的内存而报错,因为我们在 DrawComponents() 中使用了错误的 m_SelectionContext )
调用顺序如下:
可是在我的代码中,就算我没有在 DrawEntityNode() 中清空m_Selecation,也不会导致报错中断:
我猜这可能是因为 DrawTreeNode() 函数中使用的是复制传递,而不是引用传递。这导致 DrawTreeNode() 尽管删除了 entity,但 m_SelectionContext 中的值依然可以保持有效,在随后的使用中 m_SelectionContext 可以一直保持可用的数值,所以不会中断报错。
可是我这里的代码和 Cherno 没有什么差别,为什么 Cherno 可以正常的触发断点而我不可以呢? 希望能够发现错误或者原因之所在(只可能是代码遗漏/一些仓库或规范的更新所导致的)。
》》》》ImGui::BeginPopupContextWindow( ) 函数。
ImGui::BeginPopupContextWindow() 版本高于V1.87,提供三个参数的函数重载 | ImGui::BeginPopupContextWindow(const char* str_id, ImGuiMouseButton mouse_button = 1, bool also_over_items = false); str_id: 类型:const char* 用途:为弹出菜单指定一个唯一的标识符。 mouse_button: 类型:ImGuiMouseButton 默认值:1(代表右键) 用途:指定触发弹出菜单的鼠标按钮。可设置为 0(左键)、1(右键)或 2(中键)。 also_over_items: 类型:bool 默认值:false 用途:如果设置为 true,弹出菜单将在窗口内部的任何项上被右键点击所触发。这意味着即使是在子项上点击右键,也可以显示弹出菜单。
|
ImGui::BeginPopupContextWindow() 版本低于V1.87,只能使用两个参数的重载 | 用于创建上下文菜单。它通常用于响应用户的右键点击(或其他指定按钮),并显示一个弹出菜单。 ImGui::BeginPopupContextWindow(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 0); str_id: 类型:const char* 默认值:NULL 用途:用于为弹出菜单指定一个唯一的标识符。如果多个弹出菜单使用相同的 ID,则只有最后一个被打开。可以使用字符串常量或动态生成的字符串作为 ID。 popup_flags: 类型:ImGuiPopupFlags 默认值:0 用途:用于设置弹出菜单的行为。可以选择的标志包括:
|
BeginPopupContextItem() 和 BeginPopupContextwindow() 有什么不同? | 分析: BeginPopupContextWindow 是针对整个窗口的右键菜单。 BeginPopupContextItem 是针对特定 UI 元素的右键菜单。 结论: 如果你希望在用户右键点击窗口任何地方时显示菜单,使用 BeginPopupContextWindow( )。 如果你希望只在某个具体元素上右键点击时显示菜单,使用 BeginPopupContextItem( )。 |
》》》》代码中的问题/设计(BeginPopupContextWindow() 的参数、使用上的 bug)
问题一:函数使用问题解决
Cherno 在程序中创建 PopupContextWindow 时,使用新版 ImGui 中的重载,即 PopupContextWindow() 拥有三个参数 |
|
但由于我的 ImGui 版本低于1.87,所以函数没有第三个参数可以使用,如果需要实现类似的效果,就需要手动设置 ImGuiPopupFlags |
|
那么这个参数实现的是什么效果呢?
| |
|
则会得到以下的结果:
如上述所示,如果不进行特别要求(禁止窗口进行覆盖操作 -> ImGuiPopupFlags_NoOpenOverItems),那么程序在运行时将会出现一个错误:无论右键单击树节点/窗口空白处,都只会触发 PopupContextWindow 设置的弹出菜单,这其中的原因正是没有进行额外的设置。
除非我们通过参数禁止弹出菜单将在窗口内部的任何项上被右键点击所触发,即:如果有其他弹出窗口已经打开,新的弹出窗口将不会显示。
问题二:鼠标单击树节点右侧空白时,触发的是 ContextWindow 而非(亟待解决)
这可能与树节点的判定范围有一定关系。
》》》》绘制时出现的问题:(按钮被窗口覆盖了一部分)
尽管我计算了合适的按钮的大小,并使用计算出来的按钮宽高长度绘制按钮。但是为何设置按钮位置的时候,我明明使用窗口宽度减去了按钮宽度,按钮依旧被窗口覆盖了一部分?
原因:
内边距和外边距: | ImGui 窗口通常有默认的内边距(Style.WindowPadding)和外边距(Style.ItemSpacing)。如果不考虑这些因素,计算的宽度可能会导致按钮超出可见区域。 而且 GetWindowWidth() 返回的是包括边框的宽度,而按钮实际上可能需要稍微往左移动一点。 |
解决方案:
1.使用 GetContentRegionAvail(): | ImGui::GetContentRegionAvail() 返回的是窗口中可用区域的宽度(不包括任何边框、标题栏和内边距),我们可以使用 ImGui::GetContentRegionAvail().x 获取分量,计算可用空间。 |
Eg. |
|
2.调整按钮位置: | 如果你知道按钮的宽度,可以稍微减小计算结果来留出一些空间. |
Eg. |
|
-------- Make Editor look good -------
》》》》My different
因为我在处理 CameraController 的时候使用了 SceneHierarchyPanel 的非静态成员变量 m_Context ,所以我需要更改一下捕获方式。
》》》》关于 ImGuiTreeNodeFlags_Framed & ImGuiTreeNodeFlags_FramePadding 的定义
- 默认情况:
- ImGuiTreeNodeFlags_Framed
作用: 这个标志使树节点具有一个框架(frame)样式。这意味着树节点会有一个边框,看起来像是一个独立的可点击区域,通常用于视觉上将节点与背景区分开。
使用场景: 如果你想要节点在视觉上更突出,或者希望它们看起来像按钮,可以使用这个标志。
- ImGuiTreeNodeFlags_FramePadding
作用: 这个标志会在树节点的内容周围添加内边距(padding)。这会增加节点内容与其边框之间的空间,使得节点的内容看起来不那么拥挤。
使用场景: 当你希望树节点中的文本或其他内容(如图标)与边框保持一定距离,从而提高可读性和美观时,可以使用这个标志。