HEngine游戏引擎-61-90
六十一、打开/保存文件对话框
前言
此节目的
可以有询问对话框保存、解析场景
- 新场景:不绘制的实体,创建一个空白的场景
- 保存场景:有对话框,问保存到本地哪个位置
- 加载场景:有对话框,从本地哪个位置加载场景,重新创建新场景
关键代码
EditorLayer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30void EditorLayer::NewScene()
{
// 创建新场景 ,这段代码可解决 多次加载场景,会将新场景的实体和当前场景的实体一起呈现的bug
m_ActiveScene = CreateRef<Scene>();
m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
m_SceneHierarchyPanel.SetContext(m_ActiveScene);
}
void EditorLayer::OpenScene()
{
std::string filepath = FileDialogs::OpenFile("HEngine Scene (*.hengine)\0*.hengine\0");
if (!filepath.empty())
{
m_ActiveScene = CreateRef<Scene>();
m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
m_SceneHierarchyPanel.SetContext(m_ActiveScene);
SceneSerializer serializer(m_ActiveScene);
serializer.Deserialize(filepath);
}
}
void EditorLayer::SaveSceneAs()
{
std::string filepath = FileDialogs::SaveFile("HEngine Scene (*.hengine)\0*.hengine\0");
if (!filepath.empty())
{
SceneSerializer serializer(m_ActiveScene);
serializer.Serialize(filepath);
}
}新建PlatformUtils.cpp
1
2
3
4
5
6
7
8
9
10namespace HEngine
{
class FileDialogs
{
public:
//These return empty strings if cancelled
static std::string OpenFile(const char* filter);
static std::string SaveFile(const char* filter);
};
}WindowsPlatformUtils.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42std::string FileDialogs::OpenFile(const char* filter)
{
OPENFILENAMEA ofn; // Windows API 结构体,用于存储对话框的信息
CHAR szFile[260] = { 0 }; // 存储选中文件的路径
ZeroMemory(&ofn, sizeof(OPENFILENAME)); // 清空结构体数据
ofn.lStructSize = sizeof(OPENFILENAME); // 结构体大小
ofn.hwndOwner = glfwGetWin32Window((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow()); // 获取窗口句柄
ofn.lpstrFile = szFile; // 指定缓冲区存储文件路径
ofn.nMaxFile = sizeof(szFile); // 设置最大文件路径长度
ofn.lpstrFilter = filter; // 设置文件筛选器(如 `"Text Files\0*.txt\0All Files\0*.*\0"`)
ofn.nFilterIndex = 1; // 默认选择第一个筛选项
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; // 设置对话框标志
// 打开文件选择对话框
if (GetOpenFileNameA(&ofn) == TRUE)
{
return ofn.lpstrFile; // 返回选中的文件路径
}
return {}; // 用户取消时返回空字符串
}
std::string FileDialogs::SaveFile(const char* filter)
{
OPENFILENAMEA ofn;
CHAR szFile[260] = { 0 };
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = glfwGetWin32Window((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow());
ofn.lpstrFile = szFile;
ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = filter;
ofn.nFilterIndex = 1;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
// 打开文件保存对话框
if (GetSaveFileNameA(&ofn) == TRUE)
{
return ofn.lpstrFile; // 返回保存的文件路径
}
return {}; // 用户取消时返回空字符串
}EditorLayer.cpp 快捷键功能实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39void EditorLayer::OnEvent(Event& e)
{
EventDispatcher dispatcher(e);
dispatcher.Dispatch<KeyPressedEvent>(HE_BIND_EVENT_FN(EditorLayer::OnKeyPressed));
}
bool EditorLayer::OnKeyPressed(KeyPressedEvent& e)
{
if (e.GetRepeatCount() > 0) {
return false;
}
bool control = Input::IsKeyPressed(Key::LeftControl) || Input::IsKeyPressed(Key::RightControl);
bool shift = Input::IsKeyPressed(Key::LeftShift) || Input::IsKeyPressed(Key::RightShift);
switch (e.GetKeyCode()) {
case Key::N: {
if (control) {
NewScene();
}
break;
}
case Key::O: {
if (control) {
OpenScene();
}
break;
}
case Key::S: {
if (control && shift) {
SaveSceneAs();
}
// 保存当前场景:要有一个记录当前场景的路径。
if (control) {
}
break;
}
}
return false;
}
六十二、添加gizmos
前言
目的
为实现像大多3D软件那种,点击物体,会有那种拖动、缩放、旋转的辅助小程序。利用开源的imguizmo库,网址
介绍imguizmo
ImGizmo是一个建立在Dear ImGui之上的小型(.h和.cpp)库,允许你操作(目前是旋转和平移)4x4浮点矩阵,没有其他依赖性。编写时考虑到了即时模式(IM)的理念。
关键代码
EditorLayer.cpp
1 | void EditorLayer::OnImGuiRender() |
六十三、编辑时的摄像机
前言
目的
实现像Unity编辑时有一个编辑摄像机呈现画面给开发人员,运行时有一个主摄像机呈现游戏画面给玩家。
实现细节
- 编辑时摄像机是透视投影摄像机
- 编辑时摄像机缩小不会透过实体,这样更简单实现
- 编辑时摄像机越接近实体,它的放大范围越小
- 不同FPS第一人称摄像机,这个编辑式摄像机是围绕一个点旋转平移的摄像机
编辑时摄像机大致运行流程
设置好编辑时摄像机参数:宽高比、视角角度、近、远
1
m_EditorCamera = EditorCamera(30.0f, 1.778f, 0.1f, 1000.0f);
在EditorLayer中的OnUpdate与OnEvent
1
2
3
4
5
6m_EditorCamera.OnUpdate(ts);
void EditorLayer::OnEvent(Event& e)
{
m_EditorCamera.OnEvent(e);
}在Scene中的OnUpdate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24m_ActiveScene->OnUpdateEditor(ts, m_EditorCamera);
void Scene::OnUpdateEditor(Timestep ts, EditorCamera& camera)
{
Renderer2D::BeginScene(camera);
auto group = m_Registry.group<TransformComponent>(entt::get<SpriteRendererComponent>);
for (auto entity : group)
{
auto [transform, sprite] = group.get<TransformComponent, SpriteRendererComponent>(entity);
Renderer2D::DrawQuad(transform.GetTransform(), sprite.Color);
}
Renderer2D::EndScene();
}
void Renderer2D::BeginScene(const EditorCamera& camera)
{
HE_PROFILE_FUNCTION();
glm::mat4 viewProj = camera.GetViewProjection();
s_Data.TextureShader->Bind();
s_Data.TextureShader->SetMat4("u_ViewProjection", viewProj);
StartBatch();
}EditorLayer.cpp 将gizmos关联编辑时的摄像机投影与视图矩阵,而不是运行时的摄像机
1
2
3// Camera - editor 编辑时的摄像机矩阵
const glm::mat4& cameraProjection = m_EditorCamera.GetProjection();
glm::mat4 cameraView = m_EditorCamera.GetViewMatrix();
编辑时摄像机 移动与缩放 实现
为什么不讲旋转,不太会,而且此节大都是我自己推的,大概率有误,仅提供参考,忽全信
编辑时摄像机参数略讲
1
2
3
4
5
6glm::vec3 m_Position = { 0.0f, 0.0f, 10.0f }; // 摄像机的位置
glm::vec3 m_FocalPoint = { 0.0f, 0.0f, 0.0f }; // 焦点的位置为原点
glm::vec2 m_InitialMousePosition = { 0.0f, 0.0f };// 记录当前鼠标位置,为了计算移动鼠标后焦点的位置
float m_Distance = 10.0f;// 控制摄像机的z位置,为实现缩放效果当鼠标按着中键移动。目的是为了移动焦点m_FocalPoint
- 用移动后的鼠标坐标减去当前的鼠标坐标,得到偏移量是向量且有正负(可以实现斜着移动)
- 获取方向向量与偏移量相乘得到焦点的最终要平移的数值delta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void EditorCamera::OnUpdate(Timestep ts)
{
if (Input::IsKeyPressed(Key::LeftAlt))
{
const glm::vec2& mouse{ Input::GetMouseX(), Input::GetMouseY() };
glm::vec2 delta = (mouse - m_InitialMousePosition) * 0.003f;
m_InitialMousePosition = mouse;
if (Input::IsMouseButtonPressed(Mouse::ButtonMiddle))
MousePan(delta);
else if (Input::IsMouseButtonPressed(Mouse::ButtonLeft))
MouseRotate(delta);
else if (Input::IsMouseButtonPressed(Mouse::ButtonRight))
MouseZoom(delta.y);
}
UpdateView();
}1
2
3
4
5
6
7
8
9
10
11// 移动焦点,从原点000移动到xy0点
void EditorCamera::MousePan(const glm::vec2& delta)
{
auto [xSpeed, ySpeed] = PanSpeed();
//加上负号是因为,焦点往右方向物体要左移
m_FocalPoint += -GetRightDirection() * delta.x * xSpeed * m_Distance;
// 不加负号是因为,焦点往上方向(0,1,0)移动到(0,1,0),使得物体下移效果
m_FocalPoint += GetUpDirection() * delta.y * ySpeed * m_Distance;
}1
2
3
4
5
6
7
8
9
10
11
12// 获得旋转矩阵沿着y轴的向量。
// 返回vec3(0, 1, 0);或者 vec3(0, -1, 0)得到是往上还是往下的向量
glm::vec3 EditorCamera::GetUpDirection() const
{
return glm::rotate(GetOrientation(), glm::vec3(0.0f, 1.0f, 0.0f));
}
// 获得旋转矩阵沿着x轴的向量。
// 返回vec3(1, 0, 0)(右);或者 vec3(-1, 0, 0)(左)得到是往右还是往左的向量
glm::vec3 EditorCamera::GetRightDirection() const
{
return glm::rotate(GetOrientation(), glm::vec3(1.0f, 0.0f, 0.0f));
}
六十四、重构帧缓冲类
前言
要实现什么
完善Gizmos,点击场景的物体,即会显示gizmos,而不用点击hierarchy的实体才会显示。
实现思路
光栅化输出到界面有每一个像素对应的实体ID(这节要实现的)
此节要完成
由下面的思路三得出:
- 修改和优化帧缓冲类,能附加多个不同类别缓冲区。
- 使得帧缓冲区可以附加两个颜色纹理,实现一个渲染通道有两个渲染目标,并且imgui可以显示出第二个渲染目标
如何将实体ID值附加到像素上
思路一
方法
使用Uniform
问题
当前场景使用批处理,虽然场景显示了3个实体,但实际上调用一次drawcall属于一个对象,无法用Uniform确定当前一个整体的分块。
除非每一个实体调用一个drawcall,这样不使用批处理效率会降低,不行。
思路二
方法
将实体ID附加到顶点缓冲区中。
实现Demo
在顶点缓冲布局中添加实体ID,这样每个顶点都有一个自己的EntityID值,再将这个ID值作为像素的颜色值,这样就可以成功在当前实体里的每个像素都有这个ID值,即在代码位置为:
1
2
3
4
5
6
7
8
9
10
11
12struct QuadVertex {
glm::vec3 Position;
.....
// Editor-only;
int EntityID;
};
// 2.1设置顶点缓冲区布局
s_Data.QuadVertexBuffer->SetLayout({
{HEngine::ShaderDataType::Float3, "a_Position"},
.....
{HEngine::ShaderDataType::int, "a_EntityID"}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void Renderer2D::DrawQuad(const glm::mat4& transform, const glm::vec4& color, const int entityId)
{
if (s_Data.QuadIndexCount >= Renderer2DData::MaxIndices) {
NextBatch();
}
constexpr size_t quadVertexCount = 4;
......
// quad的左下角为起点
for (size_t i = 0; i < quadVertexCount; i++) {
s_Data.QuadVertexBufferPtr->Position = transform * s_Data.QuadVertexPosition[i];
......
s_Data.QuadVertexBufferPtr->EntityId = entityId;// 在这写
s_Data.QuadVertexBufferPtr++;
}
s_Data.QuadIndexCount += 6;// 每一个quad用6个索引
s_Data.Stats.QuadCount++;
}问题
由于使用了动态顶点缓冲区,即又要重写RendererAPI,可是本身RendererAPI就有很多代码,若再重写,将会混乱
例如:
当前项目的结构是,一个渲染通道(drawcall)对应一个渲染目标(帧缓冲的缓冲附件),这个渲染目标(帧缓冲的缓冲附件)已经输出了颜色(这个缓冲区只有颜色值),那实体ID值应该渲染到哪个渲染目标上(帧缓冲的缓冲附件)上呢?
解决思路二的问题
方法
当前项目的渲染目标是帧缓冲,帧缓冲可以附加多个缓冲区,所以只需要将实体ID缓冲区随已经存在的颜色缓冲区之后附加到帧缓冲即可
帧缓冲类修改
创建纹理
原先是
1
glCreateTextures(GL_TEXTURE_2D, 1, &m_ColorAttachment);;
修改后是
1
2
3
4
5
6// 2.1创建纹理。m_ColorAttachments.data()是地址,可以创建多个缓冲区
Utils::CreateTextures(multisample, m_ColorAttachments.data(), m_ColorAttachments.size());
static void CreateTextures(bool multisampled, uint32_t* outID, uint32_t count) {// outID 是vector的起始位置
glCreateTextures(TextureTarget(multisampled), count, outID);;
}若m_ColorAttachments.size()是2,且m_ColorAttachments[0] = 3, 那么m_ColorAttachments[1] = 4,3和4是缓冲区ID
颜色纹理附加到帧缓冲
原先是
1
2// 1.1颜色纹理缓冲区附加到帧缓冲
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_ColorAttachment, 0);修改后是
1
2// 颜色纹理缓冲区附加到帧缓冲:第二个参数重要,可以附加多个颜色纹理缓冲区!**
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multisampled), id, 0);由于附加多个颜色纹理缓冲区到帧缓冲中,所以要用glDrawBuffers 定义多个渲染目标
1
2
3
4
5
6
7
8
9
10
11// 新加的!!!指定要Draw的颜色缓冲区列表
if (m_ColorAttachments.size() > 1) {
HE_CORE_ASSERT(m_ColorAttachments.size() <= 4);
// 这里id对应上面,颜色纹理附加到帧缓冲的ID
GLenum buffers[4] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 ,GL_COLOR_ATTACHMENT2 ,GL_COLOR_ATTACHMENT3 };
// 定义**多个渲染目标**
glDrawBuffers(m_ColorAttachments.size(), buffers);
}else if(m_ColorAttachments.empty()){
// 只有深度缓冲
glDrawBuffer(GL_NONE);
}若只使用第一个颜色纹理缓冲区,不用上面代码,但使用第二个颜色纹理缓冲区必须要有
1
2// 只使用两个颜色纹理缓冲区可以不用后面两个
GLenum buffers[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}可以创建多重采样的纹理
1
2
3
4bool multisampled = samples > 1;
if (multisampled) {
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE);
}
显示第二个渲染目标代码
指定要附加两个颜色纹理缓冲区给帧缓冲
1
2
3
4
5
6FramebufferSpecification fbSpec;
fbSpec.Attachments = { FramebufferTextureFormat::RGBA8, FramebufferTextureFormat::RGBA8, FramebufferTextureFormat::Depth};
fbSpec.Width = 1280;
fbSpec.Height = 720;
m_Framebuffer = Framebuffer::Create(fbSpec);
// 附加的代码都在OpenGlFramebuffer中glsl编写第二个输出的颜色纹理
1
2
3
4
5
6
7
8
9
10
layout(location = 0) out vec4 color;
layout(location = 1) out vec4 color2;// 第二个渲染目标(颜色纹理)的颜色
void main() {
color = texture(u_Textures[int(v_TexIndex)], v_TexCoord * v_TilingFactor) * v_Color;
color2 = vec4(0.9, 0.2, 0.3, 1.0);;// 输出到第二个渲染目标(颜色纹理)中,红色
}EditorLayer根据下标获取第二个渲染目标的缓冲区ID,Imgui根据缓冲区ID呈现缓冲区的颜色纹理
1
2
3
4// imgui渲染帧缓冲中的东西。
// textureID是缓冲区ID
uint32_t textureID = m_Framebuffer->GetColorAttachmentRendererID(1);
ImGui::Image((void*)textureID, ImVec2(m_ViewportSize.x, m_ViewportSize.y), ImVec2(0, 1), ImVec2(1, 0));
六十五、鼠标拾取帧缓存数据
前言
此节目的
- 上一节已经显示了帧缓冲第二个颜色纹理缓冲的颜色
- 这节需要把帧缓冲第二个缓冲区改变类型,为有符号整形对应实体ID。
- 并且需要增加获取当前鼠标在viewport视口的相对位置,然后读取鼠标位置像素的**帧缓冲中第二个缓冲区(渲染目标)**的数据。
如何实现
获取鼠标在viewport视口的相对位置
鼠标的绝对位置是:当前位置距离整个屏幕**左上角(0,0)**的位置
鼠标的相对位置是:ImGui的API,得到viewport视口左上角的绝对位置,再鼠标绝对位置减去viewport窗口的左上角绝对位置即可
注意细节
由于ImGui的viewport视口左上角为00,而OpenGL的左下角才是00,所以读取缓冲区数据时候需要翻转y(用视口高度**-** 鼠标y相对位置即可)。
代码
OpenGLFramebuffer.cpp
1 | // 1.1纹理附加到帧缓冲 |
EditorLayer.cpp 获取鼠标在viewport视口的相对位置
1 | // 1.先获取Viewport视口左上角与viewport视口标题栏距离的偏移位置(0,24)- 必须放这,因为标题栏后就是视口的左上角 |
鼠标绝对位置减去viewport窗口的左上角绝对位置
1 | // 1.获取当前鼠标距离整个屏幕左上角(0,0)的位置 |
读取鼠标位置像素的**帧缓冲中第二个缓冲区(渲染目标)**的数据
1 | int OpenGLFramebuffer::ReadPixel(uint32_t attachmentIndex, int x, int y) |
第二个缓冲区(渲染目标)的数据在Glsl的片段着色器中设置
1 |
|
六十六、用固定值填充帧缓冲区
前言
前情提要
由上节已经可以读取鼠标位置的颜色缓冲区的值,但是当读取不是quad的范围的值是一个**奇怪的数字*
是因为cpp代码中使用了glClearColor(color.r, color.g, color.b, color.a);将缓冲区默认填上了颜色,这个颜色本来是float值,转换为int读取出来则是奇怪的数字
如何实现
使用新的OpenGL函数glClearTexImage,用特定值填充缓冲区
实现细节
- 由于这个glClearTexImage函数根据是设置为int值,还是float值需要指定不同参数
- 需要适当考虑扩展性,但当前只需要填充int值,所以可以先简单写死,后面有增加则再改。
代码
EditorLayer.cpp
1
2// 用-1填充帧缓冲的第二个颜色缓冲区
m_Framebuffer->ClearAttachment(1, -1);具体填充函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void OpenGLFramebuffer::ClearAttachment(uint32_t attachmentIndex, int value)
{
HE_CORE_ASSERT(attachmentIndex < m_ColorAttachments.size());
auto& spec = m_ColorAttachmentSpecifications[attachmentIndex];
glClearTexImage(m_ColorAttachments[attachmentIndex], 0,
Utils::HEngineFBTextureFormatToGL(spec.TextureFormat), GL_INT, &value);
}
static GLenum HEngineFBTextureFormatToGL(FramebufferTextureFormat format)
{
switch (format)
{
case FramebufferTextureFormat::RGBA8: return GL_RGBA8;
case FramebufferTextureFormat::RED_INTEGER: return GL_RED_INTEGER;
}
HE_CORE_ASSERT(false);
return 0;
}
六十七、获取鼠标所在的实体的ID
前言
前情提要
目前已经可以读取鼠标位置的颜色缓冲区的值,但是quad的范围读取的值是一个固定的50数字
需要实现读取quad实体内的像素在第二个缓冲区的值,会返回当前实体的ID
如何实现
在顶点缓冲布局中添加实体ID,这样每个顶点都有一个自己的EntityID值,再将这个ID值作为第二个缓冲区像素的颜色值,这样就可以成功在当前实体里的每个像素都有这个ID值
修改代码
Renderer2D.cpp 给顶点缓冲区布局添加实体ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18struct QuadVertex {
glm::vec3 Position;
glm::vec4 Color;
glm::vec2 TexCoord;
float TexIndex;
float TilingFactor;
// Editor-only;
int EntityID;
};
// 2.1设置顶点缓冲区布局
s_Data.QuadVertexBuffer->SetLayout({
{ ShaderDataType::Float3, "a_Position" },
{ ShaderDataType::Float4, "a_Color" },
{ ShaderDataType::Float2, "a_TexCoord" },
{ ShaderDataType::Float, "a_TexIndex" },
{ ShaderDataType::Float, "a_TilingFactor" },
{ ShaderDataType::Int, "a_EntityID" }
});Renderer2D.cpp 设置顶点属性的EntityID
1
2
3
4
5
6
7
8
9
10void Renderer2D::DrawQuad(const glm::mat4& transform, const glm::vec4& color, int entityID)
{
for (size_t i = 0; i < quadVertexCount; i++)
{
...
s_Data.QuadVertexBufferPtr->EntityID = entityID; //新增
s_Data.QuadVertexBufferPtr++;
}
...
}Texture.glsl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in vec2 a_TexCoord;
layout(location = 3) in float a_TexIndex;
layout(location = 4) in float a_TilingFactor;
layout(location = 5) in int a_EntityId; // 新增
uniform mat4 u_ViewProjection;
out vec4 v_Color;
out vec2 v_TexCoord;
out float v_TexIndex;
out float v_TilingFactor;
out flat int v_EntityId; // 新增
in float v_TilingFactor;
in flat int v_EntityId; // 新增
void main() {
color2 = v_EntityId; // 新增
}EditorLayer.cpp 获取当前选中实体信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void EditorLayer::OnUpdate(Timestep ts)
{
if (mouseX >= 0 && mouseY >= 0 && mouseX < (int)viewportSize.x && mouseY < (int)viewportSize.y)
{
int pixelData = m_Framebuffer->ReadPixel(1, mouseX, mouseY);
m_HoveredEntity = pixelData == -1 ? Entity() : Entity((entt::entity)pixelData, m_ActiveScene.get());
}
}
void EditorLayer::OnImGuiRender()
{
std::string name = "None";
if (m_HoveredEntity)
name = m_HoveredEntity.GetComponent<TagComponent>().Tag;
ImGui::Text("Hovered Entity: %s ", name.c_str());
}
六十八、点击选择实体
前言
前情提要
由上节已经可以读取当前鼠标位置所在实体围成的像素在缓冲区的实体ID值
此节实现鼠标点击实体,会出现gizmos,并且可以拖动什么的
实现过程中出现的Bug
- 拖动gizmo移动一个实体与另一个实体重叠时停下,且另一个实体在当前实体上面,再点击gizmo想移动原先实体,那么会获取在上面另一个实体的实体ID,即另一个实体会被选中,会切换gizmo。
- 显示了gizmo,但是若按下leftalt拖动旋转摄像机,则gizmo会消失
代码
1 | void EditorLayer::OnEvent(Event& e) |
六十九、Vulkan和新Shader系统
前言
当前项目以后Cherno打算支持vulkan,由于vulkan着色器代码也支持glsl语言,但是和Opengl的glsl标准不一样。
因为Vulkan中存在OpenGL的不存在的东西(反之亦然),所以着色器代码肯定不同。
vulkan和opengl的glsl对比-以Uniform为例
主要不同
在于,opengl支持uniform,vulkan不支持uniform,而是支持uniform缓冲区(push_constant、存储缓冲区)这比OpenGL的glsl更好。
opengl的uniform
1
uniform mat4 m_transform;
vulkan的uniform缓冲区
1
2
3layout(std140, binding=2) uniform Transform{
mat4 Transform;
}Opengl支持的uniform哪里不好
场景有很多个物体,这样一个物体调用一次drawcall(不是批处理模式),每个物体需要显示都需要上传摄像机的投影视图矩阵,那么10000个物体就有10000个uniform更新,但其实摄像机的投影视图矩阵在当前帧不变的,这样就会造成性能的下降。
vulkan哪里好
vulkan的uniform是在GPU开辟的一块缓冲区,每个物体需要摄像机的投影视图矩阵,只需将这块缓冲区放入投影矩阵的值,然后每个drawcall都去访问这块缓冲区,得到投影视图矩阵就行,性能更好。
此节目的
重新写shader系统,使其能支持vulkan和opengl两种glsl
如何实现
使用SPIR-V,作为中间表示语言,即可支持vulkan也支持opengl的glsl
介绍SPIR-V
介绍SPIR-V
vulkanApi要求以SPIR-V组件的形式提供着色器,而这个SPIR-V相当于“中间表示语言”
- 可以将写好的glsl、hlsl转换为SPIR-V
- 也可以将SPIR-V转换为glsl、hlsl。
使得可以完成只写一次shader,自动生成各种不同版本的shader语言。
SPIR-V什么工作方式:(可能我理解错了了)
将vulkan的glsl用SPIR-V编译成SPIR-V二进制文件,然后用SPIR-V的交叉编译这个二进制文件成hlsl、metal,以及兼容OpenGL的glsl。
实现过程中的重要功能
使用SPIR-V加入着色器缓存功能,不用每次都编译着色器,节省时间。
以上很有可能说的不正确,我有点迷糊,东拼西凑的写完以上内容,但大概意思是这样。
代码
写vulkan的glsl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in vec2 a_TexCoord;
layout(location = 3) in float a_TexIndex;
layout(location = 4) in float a_TilingFactor;
layout(location = 5) in int a_EntityID;
layout(std140, binding = 0) uniform Camera{ // std140是布局
mat4 u_ViewProjection;
};
struct VertexOutput {
vec4 Color;
vec2 TexCoord;
float TexIndex;
float TilingFactor;
};
layout(location = 0) out VertexOutput Output;
layout(location = 4) out flat int v_EntityID; // 4 是因为TilingFactor 是3
void main() {
Output.Color = a_Color;
Output.TexCoord = a_TexCoord;
Output.TexIndex = a_TexIndex;
Output.TilingFactor = a_TilingFactor;
v_EntityID = a_EntityID;
gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
}
layout(location = 0) out vec4 color;
layout(location = 1) out int color2;
struct VertexOutput {
vec4 Color;
vec2 TexCoord;
float TexIndex;
float TilingFactor;
};
layout(location = 0) in VertexOutput Input;
layout(location = 4) in flat int v_EntityID; // 4 是因为TilingFactor 是3
layout(binding = 0) uniform sampler2D u_Textures[32];
void main() {
color = texture(u_Textures[int(Input.TexIndex)], Input.TexCoord * Input.TilingFactor) * Input.Color;
color2 = v_EntityID;
}新建UniformBuffer类
1
2
3
4
5
6
7
8
9namespace HEngine {
class UniformBuffer
{
public:
virtual ~UniformBuffer() {}
virtual void SetData(const void* data, uint32_t size, uint32_t offset = 0) = 0;
static Ref<UniformBuffer> Create(uint32_t size, uint32_t binding);
};
}OpenGLUniformBuffer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25OpenGLUniformBuffer::OpenGLUniformBuffer(uint32_t size, uint32_t binding)
{
// 创建一个 OpenGL 缓冲对象,并存储在 m_RendererID
glCreateBuffers(1, &m_RendererID);
// 分配缓冲区内存,但不初始化数据,使用 GL_DYNAMIC_DRAW 作为提示(数据会频繁更新)
// TODO: 研究 GL_DYNAMIC_DRAW 是否最优,可能可以换成其他模式
glNamedBufferData(m_RendererID, size, nullptr, GL_DYNAMIC_DRAW);
// 绑定缓冲区到指定的 Uniform Buffer 绑定点
glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_RendererID);
}
OpenGLUniformBuffer::~OpenGLUniformBuffer()
{
// 删除 OpenGL 缓冲对象,释放 GPU 资源
glDeleteBuffers(1, &m_RendererID);
}
void OpenGLUniformBuffer::SetData(const void* data, uint32_t size, uint32_t offset)
{
// 更新缓冲区的部分数据,避免重新分配整个缓冲区,提高性能
glNamedBufferSubData(m_RendererID, offset, size, data);
}Renderer2D.cpp上传摄像机的投影视图矩阵给Uniform缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28struct Renderer2DData
{
.....
struct CameraData
{
glm::mat4 ViewProjection;
};
CameraData CameraBuffer;
Ref<UniformBuffer> CameraUniformBuffer;
};
void Renderer2D::Init()
{
.....
// 初始化CameraUniformBuffer实例,在构造函数中就将调用上面的glBindBufferBase函数
s_Data.CameraUniformBuffer = UniformBuffer::Create(sizeof(Renderer2DData::CameraData), 0);
}
void Renderer2D::BeginScene(const EditorCamera& camera)
{
HE_PROFILE_FUNCTION();
s_Data.CameraBuffer.ViewProjection = camera.GetViewProjection();
// 上传数据给缓冲区///
s_Data.CameraUniformBuffer->SetData(&s_Data.CameraBuffer, sizeof(Renderer2DData::CameraData));
StartBatch();
}
七十、 显示资源文件夹UI
前言
此节目的
为了实现quad有纹理,要实现像Unity那样拖动纹理的文件 到 实体的组件下就能生成纹理组件。
此节为完成此目的,需先完成显示本地assets文件夹下的文件夹和文件。
如何实现
- 用ImGUI渲染
- 由C++的fstream检索处理的文件和文件夹。
代码
新建ContentBrowserPanel.cpp
1 | static const std::filesystem::path s_AssetPath = "assets"; |
七十一、内容面板和ImGui拖放
前言
此节目的
为完成拖动材质赋予实体,需先完成拖动这个功能,因为项目已经有打开场景函数,所以此节完成拖动场景是否能打开场景。
代码
ContentBrowserPanel.cpp
1
2
3
4
5
6
7
8
9
10void ContentBrowserPanel::OnImGuiRender()
{
//实现拖拽功能,允许从内容浏览器拖动文件
if (ImGui::BeginDragDropSource())
{
const wchar_t* itemPath = relativePath.c_str();
ImGui::SetDragDropPayload("CONTENT_BROWSER_ITEM", itemPath, (wcslen(itemPath) + 1) * sizeof(wchar_t));
ImGui::EndDragDropSource();
}
}EditorLayer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void EditorLayer::OnImGuiRender(){
// 接收在此视口拖放过来的值
if (ImGui::BeginDragDropTarget())
{
// 因为接收内容可能为空,需要if判断 。 CONTENT_BROWSER_ITEM:拖动携带的内容
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_ITEM"))
{
const wchar_t* path = (const wchar_t*)payload->Data;
OpenScene(std::filesystem::path(g_AssetPath) / path);
}
ImGui::EndDragDropTarget();
}
}
void EditorLayer::OpenScene(const std::filesystem::path& path)
{
m_ActiveScene = CreateRef<Scene>();
m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
m_SceneHierarchyPanel.SetContext(m_ActiveScene);
SceneSerializer serializer(m_ActiveScene);
serializer.Deserialize(path.string());
}
七十二、拖拽添加纹理
前言
目的
完成拖动内容面板上的材质给实体,实体表面会显示这个材质
代码
SceneHierarchyPanel.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15DrawComponent<SpriteRendererComponent>("Sprite Renderer", entity, [](auto& component)
{
ImGui::Button("Texture", ImVec2(100.0f, 0.0f));
if (ImGui::BeginDragDropTarget())
{
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_ITEM"))
{
const wchar_t* path = (const wchar_t*)payload->Data;
std::filesystem::path texturePath = std::filesystem::path(g_AssetPath) / path;
component.Texture = Texture2D::Create(texturePath.string());
}
ImGui::EndDragDropTarget();
}
ImGui::DragFloat("Tiling Factor", &component.TilingFactor, 0.1f, 0.0f, 100.0f);
});Components.h
1
2
3
4
5
6struct SpriteRendererComponent
{
//新增
Ref<Texture2D> Texture;
float TilingFactor = 1.0f;
}Renderer2D.cpp 调用drawcall绘制
1
2
3
4
5
6
7
8
9void Renderer2D::DrawSprite(const glm::mat4& transform, SpriteRendererComponent& src, int entityID)
{
if (src.Texture) {
DrawQuad(transform, src.Texture, src.TilingFactor, src.Color, entityID);
}
else {
DrawQuad(transform, src.Color, entityID);
}
}
七十三、新增Play/Stop按钮
前言
此节目的
为了物理效果能可以运行,这节需要完成工具栏的UI,点击播放运行和停止物理效果(后几节做)。
代码
EditorLayer.cpp 设计工具栏UI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50void EditorLayer::UI_Toolbar()
{
// 设置 ImGui 样式:调整窗口的内边距和元素间距
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2));
ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(0, 0));
// 设置按钮颜色为透明(使按钮背景不可见)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
// 获取 ImGui 当前主题颜色
auto& colors = ImGui::GetStyle().Colors;
// 设置鼠标悬浮时的按钮颜色,透明度降低(更明显的悬浮效果)
const auto& buttonHovered = colors[ImGuiCol_ButtonHovered];
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(buttonHovered.x, buttonHovered.y, buttonHovered.z, 0.5f));
// 设置按钮按下时的颜色,透明度降低(更明显的按下效果)
const auto& buttonActive = colors[ImGuiCol_ButtonActive];
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(buttonActive.x, buttonActive.y, buttonActive.z, 0.5f));
// 创建无装饰、无滚动条的 ImGui 窗口(用于工具栏)
ImGui::Begin("##toolbar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// 计算按钮大小(按钮的高度 = 窗口高度 - 4)
float size = ImGui::GetWindowHeight() - 4.0f;
// 根据当前场景状态选择合适的图标:
// - 如果当前是编辑模式(Edit),显示“播放”按钮
// - 如果当前是播放模式(Play),显示“停止”按钮
Ref<Texture2D> icon = m_SceneState == SceneState::Edit ? m_IconPlay : m_IconStop;
// 设置按钮水平居中
ImGui::SetCursorPosX((ImGui::GetWindowContentRegionMax().x * 0.5f) - (size * 0.5f));
// 创建播放/停止按钮
if (ImGui::ImageButton((ImTextureID)icon->GetRendererID(), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1), 0))
{
// 如果当前是编辑模式,点击按钮后进入播放模式
if (m_SceneState == SceneState::Edit)
OnScenePlay();
// 如果当前是播放模式,点击按钮后进入编辑模式
else if (m_SceneState == SceneState::Play)
OnSceneStop();
}
// 恢复 ImGui 样式设置(避免影响其他 UI 元素)
ImGui::PopStyleVar(2); // 恢复窗口和元素间距
ImGui::PopStyleColor(3); // 恢复按钮颜色
ImGui::End(); // 结束 ImGui 窗口
}根据当前不同状态,Scene调用不同函数渲染静态场景、动态场景(物理效果)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void EditorLayer::OnUpdate(Timestep ts)
{
switch (m_SceneState)
{
case SceneState::Edit:
{
if (m_ViewportFocused)
m_CameraController.OnUpdate(ts);
m_EditorCamera.OnUpdate(ts);
m_ActiveScene->OnUpdateEditor(ts, m_EditorCamera);
break;
}
case SceneState::Play:
{
m_ActiveScene->OnUpdateRuntime(ts);
break;
}
}
}
七十四、2D物理引擎!
前言
此节目的
实现2D物理效果,选择使用Box2D库实现,具体使用可看官方文档
实现细节
- 实现物体效果需要rigidbody和box2dcollider两个组件
- 添加了rigidbody和box2dcollider两个组件,需要修改面板以及序列化代码
- 以后要实现:每次运行结束后可以重置物体的位置
有脚本的box2D物理运行顺序——有待搞清楚
Script-Physic-Render顺序
脚本影响pyhsic然后渲染,当前帧得到结果
Physic-Script-Render顺序
先Physic-脚本-渲染,则当前渲染的是上一帧的物理模拟计算的结果
代码
设置box2D为当前项目的submodule,并重新运行premake程序生成项目
1
git submodule add https://github.com/donghuiw/box2d.git
新增PhysicsManager类
PhysicsManage.h
namespace HEngine { class Scene; class Timestep; struct Rigidbody2DComponent; struct BoxCollider2DComponent; class PhysicsManager { public: void CreateWorld(); void DestoryWorld(); void AddRigibody(Scene* scene, entt::entity e); void AttachBoxshape(Scene* scene, entt::entity e); void DestoryBoxshape(Scene* scene, entt::entity e); void FixedUpdate(Timestep ts); void UpdateRigidbody(Scene* scnen, entt::entity e); public: static PhysicsManager& Get() { return m_instance; } private: b2WorldId m_WorldId = b2_nullWorldId; static PhysicsManager m_instance; }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
- PhysicsManage.cpp
```cpp
//创建物理世界,相互作用的物体、形状、关节和接触点的集合
void PhysicsManager::CreateWorld()
{
b2WorldDef worldDef = b2DefaultWorldDef();
worldDef.gravity = { 0.0f, -9.8f };
worldDef.restitutionThreshold = 0.5f;
m_WorldId = b2CreateWorld(&worldDef);
HE_CORE_ASSERT(b2World_IsValid(m_WorldId),"World id validation failed. ");
}
void PhysicsManager::AddRigibody(Scene* scene, entt::entity e)
{
Entity entity = { e, scene };
auto& transform = entity.GetComponent<TransformComponent>();
auto& rb2d = entity.GetComponent<Rigidbody2DComponent>();
b2BodyDef bodyDef = b2DefaultBodyDef();
bodyDef.type = GetBox2DBodyType(rb2d.Type);
bodyDef.position = { transform.Translation.x, transform.Translation.y };
bodyDef.rotation = b2MakeRot(transform.Rotation.z);
b2BodyId bodyId = b2CreateBody(m_WorldId, &bodyDef);
b2Body_SetFixedRotation(bodyId, rb2d.FixedRotation);
rb2d.RuntimeBodyId = bodyId;
HE_CORE_ASSERT(b2Body_IsValid(rb2d.RuntimeBodyId), "Body id validation failed.");
AttachBoxshape(scene, e);
}
//形状将碰撞几何体绑定到物体上,并添加密度、摩擦力和恢复力等材料属性
void PhysicsManager::AttachBoxshape(Scene* scene, entt::entity e)
{
Entity entity = { e, scene };
auto& transform = entity.GetComponent<TransformComponent>();
auto& bc2d = entity.GetComponent<BoxCollider2DComponent>();
auto& rb2d = entity.GetComponent<Rigidbody2DComponent>();
if (!b2Body_IsValid(rb2d.RuntimeBodyId)) return;
b2Polygon boxShape = b2MakeBox(bc2d.Size.x * transform.Scale.x, bc2d.Size.y * transform.Scale.y);
b2ShapeDef shapeDef = b2DefaultShapeDef();
shapeDef.friction = bc2d.Friction;
shapeDef.density = bc2d.Density;
shapeDef.restitution = bc2d.Restitution;
b2ShapeId shapeID = b2CreatePolygonShape(rb2d.RuntimeBodyId, &shapeDef, &boxShape);
bc2d.RuntimeShapeId = shapeID;
HE_CORE_ASSERT(b2Shape_IsValid(bc2d.RuntimeShapeId), "Shape id validation failed.");
}Components.h
struct Rigidbody2DComponent { enum class BodyType { Static = 0, Dynamic, Kinematic }; BodyType Type = BodyType::Static; bool FixedRotation = false; //Storage for runtime b2BodyId RuntimeBodyId = b2_nullBodyId; Rigidbody2DComponent() = default; Rigidbody2DComponent(const Rigidbody2DComponent& other) = default; Rigidbody2DComponent& operator=(const Rigidbody2DComponent&) = default; }; struct BoxCollider2DComponent { glm::vec2 Offset = { 0.0f, 0.0f }; glm::vec2 Size = { 0.5f, 0.5f }; //TODO(Yan): move into physics material in the future mybe float Density = 1.0f; float Friction = 0.5f; float Restitution = 0.0f; b2ShapeId RuntimeShapeId = b2_nullShapeId; //Storage for runtime void* RuntimeFixture = nullptr; BoxCollider2DComponent() = default; BoxCollider2DComponent(const BoxCollider2DComponent&) = default; };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- Scene.cpp
```cpp
void Scene::OnRuntimeStart()
{
PhysicsManager::Get().CreateWorld();
auto view = m_Registry.view<Rigidbody2DComponent>();
for (auto e : view)
{
PhysicsManager::Get().AddRigibody(this, e);
}
}
void Scene::OnRuntimeStop()
{
PhysicsManager::Get().DestoryWorld();
}在属性面板显示物理组件(省略包围盒组件代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20DrawComponent<Rigidbody2DComponent>("Rigidbody 2D", entity, [](auto& component)
{
const char* bodyTypeStrings[] = { "Static", "Dynamic", "Kinematic" };
const char* currentBodyTypeString = bodyTypeStrings[(int)component.Type];
if (ImGui::BeginCombo("Body Type", currentBodyTypeString)) {
for (int i = 0; i < 3; i++) {
bool isSelected = currentBodyTypeString == bodyTypeStrings[i];
if (ImGui::Selectable(bodyTypeStrings[i], isSelected)) {
currentBodyTypeString = bodyTypeStrings[i];
component.Type = (Rigidbody2DComponent::BodyType)i;
}
if (isSelected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::Checkbox("Fixed Rotation", &component.FixedRotation);
});场景yaml文件需保存和解析物理组件(省略包围盒组件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19if (entity.HasComponent<Rigidbody2DComponent>())
{
out << YAML::Key << "Rigidbody2DComponent";
out << YAML::BeginMap; // Rigidbody2DComponent
auto& rb2dComponent = entity.GetComponent<Rigidbody2DComponent>();
out << YAML::Key << "BodyType" << YAML::Value << RigidBody2DBodyTypeToString(rb2dComponent.Type);
out << YAML::Key << "FixedRotation" << YAML::Value << rb2dComponent.FixedRotation;
out << YAML::EndMap; // Rigidbody2DComponent
}
----------------------------------------------
auto rigidbody2DComponent = entity["Rigidbody2DComponent"];
if (rigidbody2DComponent)
{
auto& rb2d = deserializedEntity.AddComponent<Rigidbody2DComponent>();
rb2d.Type = RigidBody2DBodyTypeFromString(rigidbody2DComponent["BodyType"].as<std::string>());
rb2d.FixedRotation = rigidbody2DComponent["FixedRotation"].as<bool>();
}
EditorLayer.cpp
1 | void EditorLayer::OnScenePlay() |
七十五、UUID唯一标识
前言
- 为了点击运行场景,实体发生位置等变化复原而要实现的标识功能,定义UUID类,使用cpp的随机函数,随机ID
代码
定义好UUID类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40namespace HEngine
{
class UUID
{
public:
UUID();
UUID(uint64_t uuid);
UUID(const UUID&) = default;
operator uint64_t() const { return m_UUID; }
private:
uint64_t m_UUID;
};
}
namespace std {
template<>
struct hash<HEngine::UUID>
{
std::size_t operator()(const HEngine::UUID& uuid) const
{
return hash<uint64_t>()((uint64_t)uuid);
}
};
}
UUID.cpp-------------------------------------------
static std::random_device s_RandomDevice;
static std::mt19937_64 s_Engine(s_RandomDevice());
static std::uniform_int_distribution<uint64_t> s_UniformDistribution;
UUID::UUID()
: m_UUID(s_UniformDistribution(s_Engine))
{
}
UUID::UUID(uint64_t uuid)
: m_UUID(uuid)
{
}开始创建实体时候(CreateEntity)创建新的UUID,真正创建实体时(CreateEntityWithUUID)使用实参UUID
1
2
3
4
5
6
7
8
9
10
11
12
13
14Entity Scene::CreateEntity(const std::string& name)
{
return CreateEntityWithUUID(UUID(), name);
}
HEngine::Entity Scene::CreateEntityWithUUID(UUID uuid, const std::string& name)
{
Entity entity = { m_Registry.create(), this };
entity.AddComponent<IDComponent>(uuid);
entity.AddComponent<TransformComponent>();
auto& tag = entity.AddComponent<TagComponent>();
tag.Tag = name.empty() ? "Entity" : name;
return entity;
}序列化yaml-cpp文件时,读取实体的ID
1
2
3
4
5
6static void SerializeEntity(YAML::Emitter& out, Entity entity)
{
HE_CORE_ASSERT(entity.HasComponent<IDComponent>());
out << YAML::BeginMap; // Entity
out << YAML::Key << "Entity" << YAML::Value << entity.GetUUID();
}
七十六、开始、结束、复制场景
前言
此节目的
- 为实现点击运行,复制当前场景成为运行场景,点击结束销毁当前场景。
- 在当前场景复制实体
最重要的如何复制场景,复制场景需要复制当前场景的所有实体及其包含的组件,但是当前的entt库不包含复制组件的API,所以我们需要手动写复制实体。
代码
复制场景
Editorlayer.cpp 点击运行,执行Scene的Copy函数复制当前的场景,并设置面板的上下文为新场景
1
2
3
4
5
6
7
8
9void EditorLayer::OnScenePlay()
{
m_SceneState = SceneState::Play;
m_ActiveScene = Scene::Copy(m_EditorScene);
m_ActiveScene->OnRuntimeStart();
m_SceneHierarchyPanel.SetContext(m_ActiveScene);
}执行Scene的Copy函数复制当前的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48Ref<Scene> Scene::Copy(Ref<Scene> other)
{
// 创建一个新的 Scene 实例
Ref<Scene> newScene = CreateRef<Scene>();
// 复制视口宽度和高度
newScene->m_ViewportWidth = other->m_ViewportWidth;
newScene->m_ViewportHeight = other->m_ViewportHeight;
// 获取源场景的实体注册表和新场景的实体注册表
auto& srcSceneRegistry = other->m_Registry;
auto& dstSceneRegistry = newScene->m_Registry;
// 用于存储 UUID 和新创建的 entt::entity 之间的映射关系
std::unordered_map<UUID, entt::entity> enttMap;
// 获取源场景中所有具有 IDComponent 组件的实体视图
auto idView = srcSceneRegistry.view<IDComponent>();
// 逆序遍历所有实体(保证实体创建的顺序与原场景一致)
for (int i = idView.size() - 1; i >= 0; i--)
{
entt::entity e = *(idView.begin() + i);
// 获取实体的 UUID
UUID uuid = srcSceneRegistry.get<IDComponent>(e).ID;
// 获取实体的名称
const auto& name = srcSceneRegistry.get<TagComponent>(e).Tag;
// 在新场景中创建一个具有相同 UUID 和名称的新实体
Entity newEntity = newScene->CreateEntityWithUUID(uuid, name);
// 将新创建的实体存入映射表,供后续组件复制使用
enttMap[uuid] = (entt::entity)newEntity;
}
// 复制组件(排除 IDComponent 和 TagComponent,因为它们已在实体创建时设置)
CopyComponent<TransformComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);
CopyComponent<SpriteRendererComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);
CopyComponent<CameraComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);
CopyComponent<NativeScriptComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);
CopyComponent<Rigidbody2DComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);
CopyComponent<BoxCollider2DComponent>(dstSceneRegistry, srcSceneRegistry, enttMap);
// 返回复制后的新场景
return newScene;
}
复制实体
Editorlayer. cpp 按下快捷键ctrl+d,执行OnDuplicateEntity函数,复制当前选择的实体
1
2
3
4
5
6
7
8
9void EditorLayer::OnDuplicateEntity()
{
if (m_SceneState != SceneState::Edit)
return;
Entity selectedEntity = m_SceneHierarchyPanel.GetSelectedEntity();
if (selectedEntity)
m_EditorScene->DuplicateEntity(selectedEntity);
}Scene的DuplicateEntity函数执行具体操作
- 创建旧实体同名的新实体
- 复制组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32void Scene::DuplicateEntity(Entity entity)
{
std::string name = entity.GetName(); // 获取原实体的名称
Entity newEntity = CreateEntity(name); // 创建一个新的实体,名称与原实体相同
// 复制所有可能存在的组件
CopyComponentIfExists<TransformComponent>(newEntity, entity);
CopyComponentIfExists<SpriteRendererComponent>(newEntity, entity);
CopyComponentIfExists<CameraComponent>(newEntity, entity);
CopyComponentIfExists<NativeScriptComponent>(newEntity, entity);
CopyComponentIfExists<Rigidbody2DComponent>(newEntity, entity);
CopyComponentIfExists<BoxCollider2DComponent>(newEntity, entity);
}
template<typename Component>
static void CopyComponentIfExists(Entity dst, Entity src)
{
if (src.HasComponent<Component>()) // 检查原实体是否拥有该组件
dst.AddOrReplaceComponent<Component>(src.GetComponent<Component>()); // 复制组件到新实体
}
template<typename T, typename... Args>
T& AddOrReplaceComponent(Args&&... args)//接受按值传递的旧实体组件数据
{
/* emplace_or_replace<T>:
如果 m_EntityHandle 之前没有该组件,则添加组件。
如果 m_EntityHandle 已有该组件,则替换组件。 */
T& component = m_Scene->m_Registry.emplace_or_replace<T>(m_EntityHandle, std::forward<Args>(args)...);
m_Scene->OnComponentAdded<T>(*this, component);
return component;
}
七十七、成功绘制2D圆
前言
此节目的
给引擎添加渲染Circle图形
代码
circle的glsl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
layout(location = 0) in vec3 a_WorldPosition;
layout(location = 1) in vec3 a_LocalPosition;
layout(location = 2) in vec4 a_Color;
layout(location = 3) in float a_Thickness;
layout(location = 4) in float a_Fade;
layout(location = 5) in int a_EntityID;
layout(std140, binding = 0) uniform Camera
{
mat4 u_ViewProjection;
};
struct VertexOutput
{
vec3 LocalPosition;
vec4 Color;
float Thickness;
float Fade;
};
layout (location = 0) out VertexOutput Output;
layout (location = 4) out flat int v_EntityID;
void main()
{
Output.LocalPosition = a_LocalPosition;
Output.Color = a_Color;
Output.Thickness = a_Thickness;
Output.Fade = a_Fade;
v_EntityID = a_EntityID;
gl_Position = u_ViewProjection * vec4(a_WorldPosition, 1.0);
}
layout(location = 0) out vec4 o_Color;
layout(location = 1) out int o_EntityID;
struct VertexOutput
{
vec3 LocalPosition;
vec4 Color;
float Thickness;
float Fade;
};
layout (location = 0) in VertexOutput Input;
layout (location = 4) in flat int v_EntityID;
void main()
{
//Calculate distance and fill circle with white
float distance = 1.0 - length(Input.LocalPosition);
float circle = smoothstep(0.0, Input.Fade, distance);
circle *= smoothstep(Input.Thickness + Input.Fade, Input.Thickness, distance);
if(circle == 0.0f)
discard;
//Set ouput color
o_Color = Input.Color;
o_Color.a *= circle;
o_EntityID = v_EntityID;
}Renderer2D.cpp 批处理代码加上circle的顶点数组等信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74struct CircleVertex {
glm::vec3 WorldPosition;
glm::vec3 LocalPosition;
glm::vec4 Color;
float Thickness;
float Fade;
// Editor-only;
int EntityID;
};
struct Renderer2DData
{
Ref<VertexArray> CircleVertexArray;
Ref<VertexBuffer> CircleVertexBuffer;
Ref<Shader> CircleShader;
uint32_t CircleIndexCount = 0;
CircleVertex* CircleVertexBufferBase = nullptr;
CircleVertex* CircleVertexBufferPtr = nullptr;
}
void Renderer2D::Init()
{
//Circles
s_Data.CircleVertexArray = VertexArray::Create();
s_Data.CircleVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(CircleVertex));
s_Data.CircleVertexBuffer->SetLayout({
{ ShaderDataType::Float3, "a_WorldPosition" },
{ ShaderDataType::Float3, "a_LocalPosition" },
{ ShaderDataType::Float4, "a_Color" },
{ ShaderDataType::Float, "a_Thickness" },
{ ShaderDataType::Float, "a_Fade" },
{ ShaderDataType::Int, "a_EntityID" }
});
s_Data.CircleVertexArray->AddVertexBuffer(s_Data.CircleVertexBuffer);
s_Data.CircleVertexArray->SetIndexBuffer(quadIB); //Use quad IB
s_Data.CircleVertexBufferBase = new CircleVertex[s_Data.MaxVertices];
s_Data.CircleShader = Shader::Create("assets/shaders/Renderer2D_Circle.glsl");
}
void Renderer2D::StartBatch()
{
s_Data.CircleIndexCount = 0;
s_Data.CircleVertexBufferPtr = s_Data.CircleVertexBufferBase;
}
void Renderer2D::Flush()
{
if (s_Data.CircleIndexCount)
{
uint32_t dataSize = (uint32_t)((uint8_t*)s_Data.CircleVertexBufferPtr - (uint8_t*)s_Data.CircleVertexBufferBase);
s_Data.CircleVertexBuffer->SetData(s_Data.CircleVertexBufferBase, dataSize);
s_Data.CircleShader->Bind();
RenderCommand::DrawIndexed(s_Data.CircleVertexArray, s_Data.CircleIndexCount);
s_Data.Stats.DrawCalls++;
}
}
void Renderer2D::DrawCircle(const glm::mat4& transform, const glm::vec4& color, float thickness, float fade, int entityID)
{
for (size_t i = 0; i < 4; i++)
{
s_Data.CircleVertexBufferPtr->WorldPosition = transform * s_Data.QuadVertexPositions[i];
s_Data.CircleVertexBufferPtr->LocalPosition = s_Data.QuadVertexPositions[i] * 2.0f;
s_Data.CircleVertexBufferPtr->Color = color;
s_Data.CircleVertexBufferPtr->Thickness = thickness;
s_Data.CircleVertexBufferPtr->Fade = fade;
s_Data.CircleVertexBufferPtr->EntityID = entityID;
s_Data.CircleVertexBufferPtr++;
}
s_Data.CircleIndexCount += 6;
s_Data.Stats.QuadCount++;
}Components.h 添加Circle组件
1
2
3
4
5
6
7
8struct CircleRendererComponent {
glm::vec4 Color{ 1.0f, 1.0f, 1.0f, 1.0f };
float Thickness = 1.0f;
float Fade = 0.005f;
CircleRendererComponent() = default;
CircleRendererComponent(const CircleRendererComponent&) = default;
};Scene.cpp 扫描当前场景有Circle的组件,遍历然后调用draw绘制
1
2
3
4
5
6
7
8
9
10
11
12
13void Scene::OnUpdateEditor(Timestep ts, EditorCamera& camera)
{
//Draw circles
{
auto view = m_Registry.view<TransformComponent, CircleRendererComponent>();
for (auto entity : view)
{
auto [transform, circle] = view.get<TransformComponent, CircleRendererComponent>(entity);
Renderer2D::DrawCircle(transform.GetTransform(), circle.Color, circle.Thickness, circle.Fade, (int)entity);
}
}
}SceneSerializer.cpp 添加circle的序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21if (entity.HasComponent<CircleRendererComponent>())
{
out << YAML::Key << "CircleRendererComponent";
out << YAML::BeginMap; // CircleRendererComponent
auto& circleRendererComponent = entity.GetComponent<CircleRendererComponent>();
out << YAML::Key << "Color" << YAML::Value << circleRendererComponent.Color;
out << YAML::Key << "Thickness" << YAML::Value << circleRendererComponent.Thickness;
out << YAML::Key << "Fade" << YAML::Value << circleRendererComponent.Fade;
out << YAML::EndMap; // CircleRendererComponent
}
-----------------------------------------------------------
auto circleRendererComponent = entity["CircleRendererComponent"];
if (circleRendererComponent)
{
auto& crc = deserializedEntity.AddComponent<CircleRendererComponent>();
crc.Color = circleRendererComponent["Color"].as<glm::vec4>();
crc.Thickness = circleRendererComponent["Thickness"].as<float>();
crc.Fade = circleRendererComponent["Fade"].as<float>();
}SceneHierarchyPanel.cpp 组件添加Circle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void SceneHierarchyPanel::DrawComponents(Entity entity)
{
if (!m_SelectionContext.HasComponent<CircleRendererComponent>())
{
if (ImGui::MenuItem("Circle Renderer"))
{
m_SelectionContext.AddComponent<CircleRendererComponent>();
ImGui::CloseCurrentPopup();
}
}
DrawComponent<CircleRendererComponent>("Circle Renderer", entity, [](auto& component)
{
ImGui::ColorEdit4("Color", glm::value_ptr(component.Color));
ImGui::DragFloat("Thickness", &component.Thickness, 0.025f, 0.0f, 1.0f);
ImGui::DragFloat("Fade", &component.Fade, 0.00025f, 0.0f, 1.0f);
});
}
七十八、添加线段和矩形渲染
前言
此节目的
要给当前引擎添加能渲染线条、方框功能。渲染方框是在渲染线条的基础上实现的
代码
新增Renderer2D_Line.glsl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in int a_EntityID;
layout(std140, binding = 0) uniform Camera
{
mat4 u_ViewProjection;
};
struct VertexOutput
{
vec4 Color;
};
layout (location = 0) out VertexOutput Output;
layout (location = 1) out flat int v_EntityID;
void main()
{
Output.Color = a_Color;
v_EntityID = a_EntityID;
gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
}
layout(location = 0) out vec4 o_Color;
layout(location = 1) out int o_EntityID;
struct VertexOutput
{
vec4 Color;
};
layout (location = 0) in VertexOutput Input;
layout (location = 1) in flat int v_EntityID;
void main()
{
o_Color = Input.Color;
o_EntityID = v_EntityID;
}OpenGLRendererAPI.cpp 实际绘制Line的opengl代码
1
2
3
4
5
6
7
8
9void OpenGLRendererAPI::DrawLines(const Ref<VertexArray>& vertexArray, uint32_t vertexCount)
{
vertexArray->Bind();
glDrawArrays(GL_LINES, 0, vertexCount);
}
void OpenGLRendererAPI::SetLineWidth(float width)
{
glLineWidth(width);
}Renderer2D.cpp 批处理代码加上line的顶点数组等信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87struct LineVertex {
glm::vec3 Position;
glm::vec4 Color;
int EntityID;
};
Ref<VertexArray> LineVertexArray;
Ref<VertexBuffer> LineVertexBuffer;
Ref<Shader> LineShader;
uint32_t LineVertexCount = 0;// 只需要提供顶点数量
LineVertex* LineVertexBufferBase = nullptr;
LineVertex* LineVertexBufferPtr = nullptr;
// 0.在CPU开辟存储s_Data.MaxVertices个的LineVertex的内存
s_Data.LineVertexBufferBase = new LineVertex[s_Data.MaxVertices];
// 1.创建顶点数组
s_Data.LineVertexArray = VertexArray::Create();
// 2.创建顶点缓冲区
s_Data.LineVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(LineVertex));
// 2.1设置顶点缓冲区布局
s_Data.LineVertexBuffer->SetLayout({
{ShaderDataType::Float3, "a_Position"},
{ShaderDataType::Float4, "a_Color"},
{ShaderDataType::Int, "a_EntityID"}
});
// 1.1设置顶点数组使用的缓冲区,并且在这个缓冲区中设置布局
s_Data.LineVertexArray->AddVertexBuffer(s_Data.LineVertexBuffer);
// 3.索引缓冲-Line不需要索引缓冲区
s_Data.LineShader = Shader::Create("assets/shaders/Renderer2D_Line.glsl");
void Renderer2D::Flush(){
if (s_Data.LineVertexCount) {
// 计算当前绘制需要多少个顶点数据
uint32_t dataSize = (uint8_t*)s_Data.LineVertexBufferPtr - (uint8_t*)s_Data.LineVertexBufferBase;
// 截取部分CPU的顶点数据上传OpenGL
s_Data.LineVertexBuffer->SetData(s_Data.LineVertexBufferBase, dataSize);
s_Data.LineShader->Bind();
// 新增的:设置线条宽度
RenderCommand::SetLineWidth(s_Data.LineWidth);
// 调用绘画命令
RenderCommand::DrawLines(s_Data.LineVertexArray, s_Data.LineVertexCount);
s_Data.Stats.DrawCalls++;
}
}
void Renderer2D::DrawLine(const glm::vec3& p0, glm::vec3& p1, const glm::vec4& color, int entityID){
s_Data.LineVertexBufferPtr->Position = p0;
s_Data.LineVertexBufferPtr->Color = color;
s_Data.LineVertexBufferPtr->EntityID = entityID;
s_Data.LineVertexBufferPtr++;
s_Data.LineVertexBufferPtr->Position = p1;
s_Data.LineVertexBufferPtr->Color = color;
s_Data.LineVertexBufferPtr->EntityID = entityID;
s_Data.LineVertexBufferPtr++;
s_Data.LineVertexCount += 2;
}
// 根据一点中心位置确定4个点的位置绘制rect
void Renderer2D::DrawRect(const glm::vec3& position, const glm::vec2& size, const glm::vec4& color, int entityID){
// position是中心位置
glm::vec3 p0 = glm::vec3(position.x - size.x * 0.5f, position.y - size.y * 0.5f, position.z);// 左下角
glm::vec3 p1 = glm::vec3(position.x + size.x * 0.5f, position.y - size.y * 0.5f, position.z);// 右下角
glm::vec3 p2 = glm::vec3(position.x + size.x * 0.5f, position.y + size.y * 0.5f, position.z);// 右上角
glm::vec3 p3 = glm::vec3(position.x - size.x * 0.5f, position.y + size.y * 0.5f, position.z);// 左上角
DrawLine(p0, p1, color);
DrawLine(p1, p2, color);
DrawLine(p2, p3, color);
DrawLine(p3, p0, color);
}
// 根据实体的transform确定顶点位置再绘制
void Renderer2D::DrawRect(const glm::mat4& transform, const glm::vec4& color, int entityID){
glm::vec3 lineVertices[4];
for (size_t i = 0; i < 4; i++)
{
lineVertices[i] = transform * s_Data.QuadVertexPosition[i]; // quad的顶点位置正好是rect的顶点位置
}
DrawLine(lineVertices[0], lineVertices[1], color);
DrawLine(lineVertices[1], lineVertices[2], color);
DrawLine(lineVertices[2], lineVertices[3], color);
DrawLine(lineVertices[3], lineVertices[0], color);
}Scene.cpp
1
2
3
4
5
6
7void Scene::OnUpdateEditor(Timestep ts, EditorCamera& camera)
{
Renderer2D::BeginScene(camera);
Renderer2D::DrawLine(glm::vec3(2.0f), glm::vec3(5.0f), glm::vec4(1, 0, 1, 1));
Renderer2D::DrawRect(glm::vec3(0.0f), glm::vec3(1.0f), glm::vec4(1, 0, 1, 1));
Renderer2D::EndScene();
}
七十九、添加圆形组件和物理
前言
此节目的
添加圆形组件,设置圆形组件的属性面板,在编辑场景显示实体物理组件的包围盒
代码
Components.h 设置圆形包围盒组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// Circle包围盒
struct CircleCollider2DComponent {
glm::vec2 Offset = { 0.0f, 0.0f };
float Radius = 0.5f;
// TODO:移到物理材质
float Density = 1.0f; // 密度,0是静态的物理
float Friction = 0.5f; // 摩擦力
float Restitution = 0.0f; // 弹力,0不会弹跳,1无限弹跳
float RestitutionThreshold = 0.5f;// 复原速度阈值,超过这个速度的碰撞就会被恢复原状(会反弹)。
// 运行时候由于物理,每一帧的上述参数可能会变,所以保存为对象,但未使用
void* RuntimeFixture = nullptr;
CircleCollider2DComponent() = default;
CircleCollider2DComponent(const CircleCollider2DComponent&) = default;
};PhysicsManager.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void PhysicsManager::AttachCircleCollider(Scene* scene, entt::entity e)
{
Entity entity = { e, scene };
auto& transform = entity.GetComponent<TransformComponent>();
auto& cc2d = entity.GetComponent<CircleCollider2DComponent>();
auto& rb2d = entity.GetComponent<Rigidbody2DComponent>();
if (!b2Body_IsValid(rb2d.RuntimeBodyId)) return;
b2Circle circleShape;
circleShape.center = { cc2d.Offset.x, cc2d.Offset.y };
circleShape.radius = cc2d.Radius;
b2ShapeDef shapeDef = b2DefaultShapeDef();
shapeDef.friction = cc2d.Friction;
shapeDef.density = cc2d.Density;
shapeDef.restitution = cc2d.Restitution;
b2ShapeId shapeID = b2CreateCircleShape(rb2d.RuntimeBodyId, &shapeDef, &circleShape);
cc2d.RuntimeShapeId = shapeID;
HE_CORE_ASSERT(b2Shape_IsValid(cc2d.RuntimeShapeId), "Circle Shape id validation failed.");
}Scene.cpp 在属性面板显示圆形包围盒组件代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14if (!m_SelectionContext.HasComponent<CircleCollider2DComponent>())
{
if (ImGui::MenuItem("Circle Collider 2D"))
{
m_SelectionContext.AddComponent<CircleCollider2DComponent>();
ImGui::CloseCurrentPopup();
}
}
DrawComponent<CircleRendererComponent>("Circle Renderer", entity, [](auto& component)
{
ImGui::ColorEdit4("Color", glm::value_ptr(component.Color));
ImGui::DragFloat("Thickness", &component.Thickness, 0.025f, 0.0f, 1.0f);
ImGui::DragFloat("Fade", &component.Fade, 0.00025f, 0.0f, 1.0f);
});场景yaml文件需序列化保存和解析包围盒组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24if (entity.HasComponent<CircleCollider2DComponent>())
{
out << YAML::Key << "CircleCollider2DComponent";
out << YAML::BeginMap; // CircleCollider2DComponent
auto& cc2dComponent = entity.GetComponent<CircleCollider2DComponent>();
out << YAML::Key << "Offset" << YAML::Value << cc2dComponent.Offset;
out << YAML::Key << "Radius" << YAML::Value << cc2dComponent.Radius;
out << YAML::Key << "Density" << YAML::Value << cc2dComponent.Density;
out << YAML::Key << "Friction" << YAML::Value << cc2dComponent.Friction;
out << YAML::Key << "Restitution" << YAML::Value << cc2dComponent.Restitution;
out << YAML::EndMap; // CircleCollider2DComponent
}
----------------------------------------------------------------------
auto circleCollider2DComponent = entity["CircleCollider2DComponent"];
if (circleCollider2DComponent)
{
auto& cc2d = deserializedEntity.AddComponent<CircleCollider2DComponent>();
cc2d.Offset = boxCollider2DComponent["Offset"].as<glm::vec2>();
cc2d.Radius = boxCollider2DComponent["Radius"].as<float>();
cc2d.Density = boxCollider2DComponent["Density"].as<float>();
cc2d.Friction = boxCollider2DComponent["Friction"].as<float>();
cc2d.Restitution = boxCollider2DComponent["Restitution"].as<float>();
}
八十、 可视化物理包围盒
前言
此节目的
实现一个复选框点击后,场景可以渲染出图形相应的包围盒,使用上两节加的渲染Line和Rect来渲染盒状包围盒。,至于圆的包围盒,一样绘制圆,只不过控制厚度,从而实现圆环形状=圆包围盒
代码
1 | void EditorLayer::OnOverlayRender() |
八十一、添加模拟运行按钮
前言
此节目的
增加一个物理模拟运行模式,来运行给物体添加的物理效果,摄像机却使用编辑模式下的摄像机。
区分Play运行模式的物理效果
这个模式的摄像机会变成场景里的主摄像机,而不是当前的编辑相机
代码
Scene.cpp
void Scene::OnSimulationStart() { OnPhysics2DStart(); } void Scene::OnSimulationStop() { OnPhysics2DStop(); } void Scene::OnPhysics2DStart() { PhysicsManager::Get().CreateWorld(); auto view = m_Registry.view<Rigidbody2DComponent>(); for (auto e : view) { PhysicsManager::Get().AddRigibody(this, e); } } void Scene::OnPhysics2DStop() { PhysicsManager::Get().DestoryWorld(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- EditorLayer.cpp
```cpp
void EditorLayer::OnSceneSimulate()// 开始物理模拟模式
{
if (m_SceneState == SceneState::Play) {
OnSceneStop(); // 停止物理
}
m_SceneState = SceneState::Simulate;
m_ActiveScene = Scene::Copy(m_EditorScene);
m_ActiveScene->OnSimulationStart();
m_SceneHierarchyPanel.SetContext(m_ActiveScene);
}
八十二、Mono实现C#脚本
前言
此节目的
HEngine实现能cpp能调用C#的函数(C#语言嵌入cpp中),需要安装mono.Net库,并克隆git上的mono项目自己构建导出mono库。
即:需要.Net库和构建的mono库,用visual studio构建mono项目导出mono静态库,静态链接
Mono配置步骤:
1.安装mono.Net库,网址
2.克隆mono开源项目
克隆mono开源项目
1
git clone --recursive https://github.com/mono/mono
3.生成静态库
打开mono开源项目下的sln文件
打开后对libmono-static单个项目进行生成, 生成libmono-static-sgen.lib文件(函数的定义)
注意选择:debug 64和release 64 各自生成一次
Mono安装目录下include的文件夹有 libmono-static-sgen.lib文件中函数定义的头文件(函数声明)
4.移动生成的库文件与inlcude文件
1.将include/mono文件夹放入引擎下的vendor/mono/include下(函数声明)
2.拷贝生成的mono库文件到vender/mono下(函数定义)
5.移动Mono安装目录里的.Net库文件
到mono安装目录mono\lib\mono下,找到4.5版本的文件夹移动到HEngine-Editor\mono\lib\mono\4.5下
代码
新建项目HEngine-ScriptCore
添加Main.cs文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33using System;
namespace HEngine
{
public class Main
{
public float FloatVar { get; set; }
public Main()
{
Console.WriteLine("Main constructor!");
}
public void PrintMessage()
{
Console.WriteLine("Hello World from C#!");
}
public void PrintInt(int value)
{
Console.WriteLine($"C# says: {value}");
}
public void PrintInts(int value1, int value2)
{
Console.WriteLine($"C# says: {value1} and {value2}");
}
public void PrintCustomMessage(string message)
{
Console.WriteLine($"C# says: {message}");
}
}
}新建ScriptEngine类 写好初始化mono环境,加载C#项目生成的 dll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160namespace HEngine {
struct ScriptEngineData
{
//Mono 运行时的根域,所有的 AppDomain 都运行在它之上
MonoDomain* RootDomain = nullptr;
//用于 C# 脚本执行的独立 应用域
MonoDomain* AppDomain = nullptr;
//加载的 C# DLL(HEngine-ScriptCore.dll),包含用户的 C# 代码
MonoAssembly* CoreAssembly = nullptr;
};
static ScriptEngineData* s_Data = nullptr;
void ScriptEngine::Init()
{
s_Data = new ScriptEngineData();
InitMono();
}
void ScriptEngine::Shutdown()
{
ShutdownMono();
delete s_Data;
}
char* ReadBytes(const std::string& filepath, uint32_t* outSize)
{
std::ifstream stream(filepath, std::ios::binary | std::ios::ate);
if (!stream)
{
// Failed to open the file
return nullptr;
}
std::streampos end = stream.tellg();
stream.seekg(0, std::ios::beg);
uint32_t size = end - stream.tellg();
if (size == 0)
{
// File is empty
return nullptr;
}
char* buffer = new char[size];
stream.read((char*)buffer, size);
stream.close();
*outSize = size;
return buffer;
}
MonoAssembly* LoadCSharpAssembly(const std::string& assemblyPath)
{
uint32_t fileSize = 0;
char* fileData = ReadBytes(assemblyPath, &fileSize);
// 创建 MonoImage(DLL 映像),用于加载程序集。
MonoImageOpenStatus status;
MonoImage* image = mono_image_open_from_data_full(fileData, fileSize, 1, &status, 0);
if (status != MONO_IMAGE_OK)
{
const char* errorMessage = mono_image_strerror(status);
// Log some error message using the errorMessage data
return nullptr;
}
//加载程序集(DLL)到 Mono 运行时
MonoAssembly* assembly = mono_assembly_load_from_full(image, assemblyPath.c_str(), &status, 0);
mono_image_close(image);
// Don't forget to free the file data
delete[] fileData;
return assembly;
}
void PrintAssemblyTypes(MonoAssembly* assembly)
{
//获取assembly的image,然后获取类型定义表,读取所有 C# 类
MonoImage* image = mono_assembly_get_image(assembly);
const MonoTableInfo* typeDefinitionsTable = mono_image_get_table_info(image, MONO_TABLE_TYPEDEF);
int32_t numTypes = mono_table_info_get_rows(typeDefinitionsTable);
//遍历 C# 程序集中的所有类,并输出命名空间和类名。
for (int32_t i = 0; i < numTypes; i++)
{
uint32_t cols[MONO_TYPEDEF_SIZE];
mono_metadata_decode_row(typeDefinitionsTable, i, cols, MONO_TYPEDEF_SIZE);
const char* nameSpace = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAMESPACE]);
const char* name = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAME]);
HE_CORE_TRACE("{}.{}", nameSpace, name);
}
}
void ScriptEngine::InitMono()
{
mono_set_assemblies_path("mono/lib"); //加载.dll组件
//创建Mono运行时的顶级作用域。
MonoDomain* rootDomain = mono_jit_init("HEngineITRuntime");
HE_CORE_ASSERT(rootDomain);
s_Data->RootDomain = rootDomain;
//创建AppDomain(应用域),用于运行 C# 代码
s_Data->AppDomain = mono_domain_create_appdomain("HEngineScriptRuntime", nullptr);
mono_domain_set(s_Data->AppDomain, true);
//加载 C# DLL,这是写的 C# 代码
s_Data->CoreAssembly = LoadCSharpAssembly("Resources/Scripts/HEngine-ScriptCore.dll");
//打印 DLL 内所有类的名称。
PrintAssemblyTypes(s_Data->CoreAssembly);
MonoImage* assemblyImage = mono_assembly_get_image(s_Data->CoreAssembly);
MonoClass* monoClass = mono_class_from_name(assemblyImage, "HEngine", "Main");
//找到C#类HEngine.Main并在C#运行时创建实例(调用默认构造函数)。
MonoObject* instance = mono_object_new(s_Data->AppDomain, monoClass);
mono_runtime_object_init(instance);
//找到 PrintMessage()方法并调用它。
MonoMethod* printMessageFunc = mono_class_get_method_from_name(monoClass, "PrintMessage", 0);
mono_runtime_invoke(printMessageFunc, instance, nullptr, nullptr);
//找到PrintInt(int value)方法并传递参数5。
MonoMethod* printIntFunc = mono_class_get_method_from_name(monoClass, "PrintInt", 1);
int value = 5;
void* param = &value;
mono_runtime_invoke(printIntFunc, instance, ¶m, nullptr);
//找到PrintInts(int a, int b)方法并传递两个参数5,508。
MonoMethod* printIntsFunc = mono_class_get_method_from_name(monoClass, "PrintInts", 2);
int value2 = 508;
void* params[2] =
{
&value,
&value2
};
mono_runtime_invoke(printIntsFunc, instance, params, nullptr);
//创建C#字符串,调用PrintCustomMessage(string message)。
MonoString* monoString = mono_string_new(s_Data->AppDomain, "Hello World from C++!");
MonoMethod* printCustomMessageFunc = mono_class_get_method_from_name(monoClass, "PrintCustomMessage", 1);
void* stringParam = monoString;
mono_runtime_invoke(printCustomMessageFunc, instance, &stringParam, nullptr);
// HZ_CORE_ASSERT(false);
}
void ScriptEngine::ShutdownMono()
{
s_Data->AppDomain = nullptr;
s_Data->RootDomain = nullptr;
}
}
八十三、在C#调用C++内部函数
前言
此节目的
上一节实现了cpp调用c#函数
这一节要实现C#调用cpp内部函数,这样才可以算得上脚本
C#调用Cpp函数例子
- 无返回值、无参数
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace HEngine{
public class Main
{
public float FloatVar { get; set; }
public Main()
{
Console.WriteLine("Main constructor!");
CppFunction();// 调用cpp的函数
}
// 声明为内部调用:声明这个函数的定义在cpp内部被实现
[ ]
extern static void CppFunction();
}
}C++
1
2
3
4
5
6
7
8
9static void CppFunc() {
std::cout << "来自cpp内部函数" << std::endl;
}
void ScriptEngine::InitMono()
{
// 把C++的CppFunc绑定到c#的CppFunction
mono_add_internal_call("HEngine.Main::CppFunction", CppFunc);
//".后面是类 ::后面是函数"
}
- 带有参数
C#
1
2
3
4
5
6
7public Main()
{
NativeLog("Hello", 2025);
}
[ ]
extern static void NativeLog(string text, int parameter);C++
1
2
3
4
5
6
7
8static void NativeLog(MonoString* string, int parameter)
{
char* cStr = mono_string_to_utf8(string);
std::string str(cStr);
mono_free(cStr);
std::cout << str << "," << parameter << std::endl;
}
mono_add_internal_call("HEngine.Main::NativeLog", NativeLog);
- 带有结构体参数
C#
1
2
3
4
5
6
7
8public Main()
{
Vector3 vec1 = new Vector3(5, 2.5f, 1);
NativeLogVec3(ref vec1, out Vector3 vec2);
Console.WriteLine($"{vec2.X}, {vec2.Y}, {vec2.Z}");
}
extern static void NativeLogVec3(ref Vector3 vec, out Vector3 vec2);
[ ]cpp
1
2
3
4
5
6static void NativeLogVec3(glm::vec3* vec, glm::vec3* out)
{
std::cout << vec->x << "," << vec->y <<","<<vec->z << std::endl;
*out = glm::cross(*vec, glm::vec3(vec->x, vec->y, -vec->z));
}
mono_add_internal_call("HEngine.Main::NativeLogVec3", NativeLogVec3);注意Bug
由于结构体带有指针,需要申请内存
若在cpp函数内申请内存
1
2
3
4
5
6
7
8
9static glm::vec3* NativeLogVec3(glm::vec3* vec) {
std::cout <<"TestNativeLogVec3" << std::endl;
glm::vec3 result = glm::cross(*vec, glm::vec3(vec->x, vec->y, -vec->z));
// 因为result是局部变量,函数结束后会被销毁result的内存,所以返回的内存是空的
return &result;
// new出来的内存,是在cpp本地分配的,传给C#访问的地方也还是无效
return new glm::vec3(result); // 尝试new
}
封装抽象类
ScriptEngine.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 如果不引入头文件,必须外部声明,但这些都是在c文件定义的结构,所以需要extern"C"
extern "C" {
typedef struct _MonoClass MonoClass;
typedef struct _MonoObject MonoObject;
typedef struct _MonoMethod MonoMethod;
}
namespace HEngine {
class ScriptEngine
{
public:
static void Init(); // 初始化
static void Shutdown(); // 关闭
static void LoadAssembly(const std::filesystem::path& filepath); // 2.加载dll程序集
private:
static void InitMono(); // 1.初始化mono
static void ShutdownMono(); // 关闭mono
static MonoObject* InstantiateClass(MonoClass* monoClass); // 实例化Mono类为Mono实例对象
friend class ScriptClass;
};
class ScriptClass {
public:
ScriptClass() = default;
ScriptClass(const std::string& classNamespace, const std::string& className);// 3. 创建一个MonoClass类
MonoObject* Instantiate();// 4.创建一个由MonoClass类构成的mono对象并且初始化
MonoMethod* GetMethod(const std::string& name, int parameterCount);// 5.1 获取类的函数
MonoObject* InvokeMethod(MonoObject* instance, MonoMethod* method, void** params = nullptr);// 5.2 调用类的函数
private:
std::string m_ClassNamespace;
std::string m_ClassName;
MonoClass* m_MonoClass = nullptr;
};
}ScriptGlue
1
2
3
4
5
6
7
8
namespace HEngine {
class ScriptGlue
{
public:
static void RegisterFunctions();// 添加内部调用
};
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27namespace HEngine
{
static void NativeLog(MonoString* string, int parameter)
{
char* cStr = mono_string_to_utf8(string);
std::string str(cStr);
mono_free(cStr);
std::cout << str << ", " << parameter << std::endl;
}
static void NativeLog_Vector(glm::vec3* parameter, glm::vec3* outResult)
{
HE_CORE_WARN("Value: {0}", *parameter);
*outResult = glm::normalize(*parameter);
}
static float NativeLog_VectorDot(glm::vec3* parameter)
{
HE_CORE_WARN("Value: {0}", *parameter);
return glm::dot(*parameter, *parameter);
}
void ScriptGlue::RegisterFunctions()
{
HE_ADD_INTERNAL_CALL(NativeLog);
HE_ADD_INTERNAL_CALL(NativeLog_Vector);
HE_ADD_INTERNAL_CALL(NativeLog_VectorDot);
}
}
八十四、实现C#脚本控制实体
前言
此节目的
为实现C#脚本WSAD能控制实体的位置变化
代码
Entity.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36namespace HEngine
{
public class Entity
{
protected Entity() { ID = 0; }
internal Entity(ulong id)
{
ID = id;
}
public readonly ulong ID;
public Vector3 Translation
{
get
{
InternalCalls.TransformComponent_GetTranslation(ID, out Vector3 result);
return result;
}
set
{
InternalCalls.TransformComponent_SetTranslation(ID, ref value);
}
}
public bool HasComponent<T>() where T : Component, new()
{
Type componentType = typeof(T);
return InternalCalls.Entity_HasComponent(ID, componentType);
}
public T GetComponent<T>() where T : Component, new()
{
if (!HasComponent<T>())
return null;
T component = new T() { Entity = this };
return component;
}
}
}Player.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40using System;
using Hazel;
namespace Sandbox{
public class Player : Entity {
public Player(){
Console.WriteLine("Player()");
}
void OnCreate(){
Console.WriteLine($"Player.OnCreate() - {ID}");
}
void OnUpdate(float ts){
//Console.WriteLine($"Player.OnUpdate() - {ts}");
float speed = 1.0f;
Vector3 velocity = Vector3.Zero;
//
// 内部调用函数,事件是否触发//
if (Input.IsKeyDown(KeyCode.W)){
velocity.Y = 1.0f;
}
else if (Input.IsKeyDown(KeyCode.S)){
velocity.Y = -1.0f;
}
else if (Input.IsKeyDown(KeyCode.A)){
velocity.X = -1.0f;
}
else if (Input.IsKeyDown(KeyCode.D)){
velocity.X = 1.0f;
Console.WriteLine("press the D key");
}
velocity *= speed;
// Translation get访问器 是调用C++的内部函数 获取 实体的位置
Vector3 translation = Translation;
translation += velocity * ts;
// Translation set访问器 是调用C++的内部函数 设置 实体的位置
Translation = translation;
}
}
}C#声明调用C++内部函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public static class InternalCalls
{
[ ]
internal extern static bool Entity_HasComponent(ulong entityID, Type componentType);
[ ]
internal extern static void TransformComponent_GetTranslation(ulong entityID, out Vector3 translation);
[ ]
internal extern static void TransformComponent_SetTranslation(ulong entityID, ref Vector3 translation);
[ ]
internal extern static void Rigidbody2DComponent_ApplyLinearImpulse(ulong entityID, ref Vector2 impulse, ref Vector2 point, bool wake);
[ ]
internal extern static void Rigidbody2DComponent_ApplyLinearImpulseToCenter(ulong entityID, ref Vector2 impulse, bool wake);
[ ]
internal extern static bool Input_IsKeyDown(KeyCode keycode);
}对应的C++的内部函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23static void TransformComponent_GetTranslation(UUID entityID, glm::vec3* outTranslation)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
*outTranslation = entity.GetComponent<TransformComponent>().Translation;
}
static void TransformComponent_SetTranslation(UUID entityID, glm::vec3* translation)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
entity.GetComponent<TransformComponent>().Translation = *translation;
}
static bool Input_IsKeyDown(KeyCode keycode)
{
return Input::IsKeyPressed(keycode);
}
C++调用C#的函数(C++项目的代码)
找到dll里所有继承Entity的类,表明这是脚本类,得到对应的封装的Mono类
并用脚本map存储所有封装的mono类(用封装的Mono类实例化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38void ScriptEngine::LoadAssemblyClasses(MonoAssembly* assembly)
{
s_Data->EntityClasses.clear();
MonoImage* image = mono_assembly_get_image(assembly);
const MonoTableInfo* typeDefinitionsTable = mono_image_get_table_info(image, MONO_TABLE_TYPEDEF);
int32_t numTypes = mono_table_info_get_rows(typeDefinitionsTable);
// 1.加载Entity父类
MonoClass* entityClass = mono_class_from_name(image, "HEngine", "Entity");
for (int32_t i = 0; i < numTypes; i++)
{
uint32_t cols[MONO_TYPEDEF_SIZE];
mono_metadata_decode_row(typeDefinitionsTable, i, cols, MONO_TYPEDEF_SIZE);
const char* nameSpace = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAMESPACE]);
const char* name = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAME]);
std::string fullName;
if (strlen(nameSpace) != 0) {
fullName = fmt::format("{}.{}", nameSpace, name);
}
else {
fullName = name;
}
// 2.加载Dll中所有C#类
MonoClass* monoClass = mono_class_from_name(image, nameSpace, name);
if (monoClass == entityClass) {// entity父类不保存
continue;
}
// 3.判断当前类是否为Entity的子类
bool isEntity = mono_class_is_subclass_of(monoClass, entityClass, false); // 这个c#类是否为entity的子类
if (isEntity) {
// 存入封装的Mono类对象
// 3.1是就存入脚本map中
s_Data->EntityClasses[fullName] = CreateRef<ScriptClass>(nameSpace, name);
}
}
}在运行场景开始前时,循环遍历当前所有具有脚本组件的实体
1
2
3
4
5
6
7
8
9
10
11
12
13void Scene::OnRuntimeStart()
{
OnPhysics2DStart();
{// 脚本
ScriptEngine::OnRuntimeStart(this);
auto view = m_Registry.view<ScriptComponent>();
for (auto e : view) {
Entity entity = { e, this };
ScriptEngine::OnCreateEntity(entity);// 实例化实体拥有的C#脚本
}
}
}再调用C#类的OnCreate函数(初始化)
1
2
3
4
5
6
7
8
9void ScriptEngine::OnCreateEntity(Entity entity)
{
const auto& sc = entity.GetComponent<ScriptComponent>(); // 得到这个实体的组件
if (ScriptEngine::EntityClassExists(sc.ClassName)) { // 组件的脚本名称是否正确
Ref<ScriptInstance> instance = CreateRef<ScriptInstance>(s_Data->EntityClasses[sc.ClassName], entity);// 实例化类对象,并存储OnCreate、OnUpdate函数,调用父类Entity的构造函数,传入实体的UUID
s_Data->EntityInstances[entity.GetUUID()] = instance; // 运行脚本map存储这些ScriptInstance(类对象)
instance->InvokeOncreate(); // 调用C#的OnCreate函数
}
}存储OnCreate、OnUpdate函数,并调用C#父类Entity的构造函数传入当前实体的UUID给C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16ScriptInstance::ScriptInstance(Ref<ScriptClass> scriptClass, Entity entity)
:m_ScriptClass(scriptClass)
{
// 获取Sandbox Player类构成的MonoObject对象,相当于new Sandbox.Player()
m_Instance = scriptClass->Instantiate();
m_Constructor = s_Data->EntityClass.GetMethod(".ctor", 1);// 获取C#Entity类的构造函数
m_OnCreateMethod = scriptClass->GetMethod("OnCreate", 0);// 获取并存储Sandbox.Player类的函数
m_OnUpdateMethod = scriptClass->GetMethod("OnUpdate", 1);
// 调用C#Entity类的构造函数
{
UUID entityID = entity.GetUUID();
void* param = &entityID;
m_ScriptClass->InvokeMethod(m_Instance, m_Constructor, ¶m);// 第一个参数传入的是Entity子类(Player)构成的mono对象
}
}在运行场景的update函数,循环遍历当前所有具有脚本组件的实体
1
2
3
4
5
6
7
8
9
10
11
12void Scene::OnUpdateRuntime(Timestep ts)
{
// 脚本
{
ScriptEngine::OnRuntimeStart(this);
// 实例化实体中的C#脚本
auto view = m_Registry.view<ScriptComponent>();
for (auto e : view) {
Entity entity = { e, this };
ScriptEngine::OnUpdateEntity(entity, ts);
}
}根据UUID在运行脚本map找到这个封装的mono类对象,并调用C#类的OnUpdate函数
1
2
3
4
5
6
7
8
9void ScriptEngine::OnUpdateEntity(Entity entity, Timestep ts)
{
UUID entityUUID = entity.GetUUID(); // 得到这个实体的UUID
HE_CORE_ASSERT(s_Data->EntityInstances.find(entityUUID) != s_Data->EntityInstances.end());
// 根据UUID获取到ScriptInstance的指针
Ref<ScriptInstance> instance = s_Data->EntityInstances[entityUUID];
instance->InvokeOnUpdate((float)ts); // 调用C#的OnUpdate函数
}
八十五、C#获取实体的组件
前言
- 此节目的
- 由上节根据UUID获取实体的translation在根据wsad调整实体的位置
- 此节优化上节所做,实体的translation应属于实体的TransformComponent,所以此节需要在C#创建组件类
- 用UUID,C#组件类对应cpp的组件类。
代码
Component.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public abstract class Component{
public Entity Entity { get; set; }
}
public class TransformComponent : Component {
public Vector3 Translation{
get{
InternalCalls.TransformComponent_GetTranslation(Entity.ID, out Vector3 translation);
return translation;
}
set{
InternalCalls.TransformComponent_SetTranslation(Entity.ID, ref value);
}
}
}
public class Rigidbody2DComponent : Component{
public void ApplyLinearImpulse(Vector2 impulse, Vector2 worldPosition, bool wake){
InternalCalls.Rigidbody2DComponent_ApplyLinearImpulse(Entity.ID, ref impulse, ref worldPosition, wake); ;
}
public void ApplyLinearImpulse(Vector2 impulse, bool wake){
InternalCalls.Rigidbody2DComponent_ApplyLinearImpulseToCenter(Entity.ID, ref impulse, wake); ;
}
}ScriptGlue.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30template<typename... Component>
static void RegisterComponent()
{
([]()
{
std::string_view typeName = typeid(Component).name();
size_t pos = typeName.find_last_of(':');
std::string_view structName = typeName.substr(pos + 1);
std::string managedTypename = fmt::format("HEngine.{}", structName);
MonoType* managedType = mono_reflection_type_from_name(managedTypename.data(), ScriptEngine::GetCoreAssemblyImage());
if (!managedType)
{
HE_CORE_ERROR("Could not find component type {}", managedTypename);
return;
}
s_EntityHasComponentFuncs[managedType] = [](Entity entity) { return entity.HasComponent<Component>(); };
}(), ...);
}
template<typename... Component>
static void RegisterComponent(ComponentGroup<Component...>)
{
RegisterComponent<Component...>();
}
void ScriptGlue::RegisterComponents()
{
RegisterComponent(AllComponents{});
}Entity.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void OnCreate()
{
Console.WriteLine($"Player.OnCreate() - {ID}");
m_Transform = GetComponent<TransformComponent>();
m_Rigidbody = GetComponent<Rigidbody2DComponent>();
}
public T GetComponent<T>() where T : Component, new()
{
if (!HasComponent<T>())
{
return null;
}
T component = new T() { Entity = this };// 返回本地类实例对象
return component;
}
public bool HasComponent<T>() where T : Component, new()// new()是确保有空构造函数{
Type componentType = typeof(T);// 得到命名空间.类名名称,比如Sandbox.Player
return InternalCalls.Entity_HasComponent(ID, componentType);
}ScriptGlue.cpp
1
2
3
4
5
6
7
8
9
10
11static bool Entity_HasComponent(UUID entityID, MonoReflectionType* componentType)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
MonoType* managedType = mono_reflection_type_get_type(componentType);
HE_CORE_ASSERT(s_EntityHasComponentFuncs.find(managedType) != s_EntityHasComponentFuncs.end());
return s_EntityHasComponentFuncs.at(managedType)(entity);
}若实体存在对应组件,就返回C#本地组件类的实例化
1
2
3
4
5
6
7public T GetComponent<T>() where T : Component, new(){
if (!HasComponent<T>()){
return null;
}
T component = new T() { Entity = this };// 返回本地类实例对象
return component;
}C#在相关脚本类的OnUpdate函数对*“获取到的组件”*的属性修改或者函数调用
实际上都是根据传入UUID,调用C++内部函数来实现
1
2C
m_Rigidbody.ApplyLinearImpulse(velocity.XY, true);1
2
3
4
5[ ]
internal extern static void Rigidbody2DComponent_ApplyLinearImpulse(ulong entityID, ref Vector2 impulse, ref Vector2 point, bool wake);
[ ]
internal extern static void Rigidbody2DComponent_ApplyLinearImpulseToCenter(ulong entityID, ref Vector2 impulse, bool wake);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24cpp
static void Rigidbody2DComponent_ApplyLinearImpulse(UUID entityID, glm::vec2* impulse, glm::vec2* point, bool wake)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
auto& rb2d = entity.GetComponent<Rigidbody2DComponent>();
b2BodyId bodyid = rb2d.RuntimeBodyId;
b2Body_ApplyLinearImpulse(bodyid, b2Vec2{ impulse->x, impulse->y }, b2Vec2{ point->x, point->y }, wake);
}
static void Rigidbody2DComponent_ApplyLinearImpulseToCenter(UUID entityID, glm::vec2* impulse, bool wake)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
auto& rb2d = entity.GetComponent<Rigidbody2DComponent>();
b2BodyId bodyid = rb2d.RuntimeBodyId;
b2Body_ApplyLinearImpulseToCenter(bodyid, b2Vec2{ impulse->x, impulse->y }, wake);
}
八十六、实现面板操控C#变量
前言
此节目的
为完成在编辑器面板上可以显示C#脚本类的属性,对属性的值修改,C#脚本会热更新(mono4.5api支持)
关键代码
mono的API
1 | // 获取所有属性 |
代码思路
一个C#脚本对应一个ScriptClass类,ScriptClass类中有**map(fieldmap)**保存这个C#脚本的属性与值类型。
- 在加载dll后,读取游戏脚本库的类名,加载C#类得到封装的MonoClass对象
- 再根据MonoClass(反射)得到C#类的所有属性
- 循环属性,得到单个MonoClassField对象,根据MonoClassField(反射)得到C#属性的名称、访问权限、类型
- 根据权限决定map是否存储这个属性
ScriptEngine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52enum class ScriptFieldType
{
None = 0,
Float, Double,
Bool, Char, Byte, Short, Int, Long,
UByte, UShort, UInt, ULong,
Vector2, Vector3, Vector4,
Entity
};
// 属性名称对应的结构体
struct ScriptField {
ScriptFieldType Type;
std::string Name;
MonoClassField* ClassField;
};
class ScriptClass {
public:
const std::map<std::string, ScriptField>& GetFields() const { return m_Fields; }
private:
std::map<std::string, ScriptField> m_Fields;
friend class ScriptEngine;
};
------------------------------------------
void ScriptEngine::LoadAssemblyClasses()
{
.....
for (int32_t i = 0; i < numTypes; i++)
{
.....
MonoClass* monoClass = mono_class_from_name(s_Data->AppAssemblyImage, nameSpace, className);
.....
// 读取脚本类的属性
int fieldCount = mono_class_num_fields(monoClass);
HE_CORE_WARN("{} has {} fields:", className, fieldCount);
void* iterator = nullptr;
// 获取所有属性,并得到单个属性
while (MonoClassField* field = mono_class_get_fields(monoClass, &iterator))
{
const char* filedName = mono_field_get_name(field);// 获取单个属性的名称
uint32_t flags = mono_field_get_flags(field); // 获取单个属性的权限
if (flags & FIELD_ATTRIBUTE_PUBLIC) { //类型是Public的话
MonoType* type = mono_field_get_type(field); // 获取单个属性的类类型
ScriptFieldType fieldType = Utils::MonoTypeToScriptFieldType(type);
HE_CORE_TRACE(" {}({})", filedName,Utils::ScriptFieldTypeToStirng(fieldType));
// 用Map存储这个属性
scriptClass->m_Fields[filedName] = { fieldType, filedName, field };
}
}
}运行游戏时,在编辑面板可以用通过这fieldmap获取当前C#脚本的所有属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 实体的脚本组件 mutable去除常量属性
DrawComponent<ScriptComponent>("Script", entity, [entity](auto& component)mutable
{
Ref<ScriptInstance> scriptInstance = ScriptEngine::GetEntityScriptInstance(entity.GetUUID());
if (scriptInstance) {
// 读取map中的属性
const auto& fields = scriptInstance->GetScriptClass()->GetFields();
for (const auto& [name, field] : fields)// 获取保存的属性名称
{
if (field.Type == ScriptFieldType::Float) {
float data = scriptInstance->GetFieldValue<float>(name);// 下一步有函数定义:获取属性值
if (ImGui::DragFloat(name.c_str(), &data)) {
scriptInstance->SetFieldValue(name, data);// 下一步有函数定义:设置属性值
}
}
}
}得到了对应属性的名称,在可以用Mono的API来获取和设置这个属性的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37template<typename T>
T GetFieldValue(const std::string& name)
{
bool success = GetFieldValueInternal(name, s_FieldValueBuffer);
if (!success)
return T();
return *(T*)s_FieldValueBuffer;
}
bool ScriptInstance::GetFieldValueInternal(const std::string& name, void* buffer)
{
const auto& fields = m_ScriptClass->GetFields();
auto it = fields.find(name);
if (it == fields.end()) {
return nullptr;
}
const ScriptField& field = it->second;
// mono的API
mono_field_get_value(m_Instance, field.ClassField, buffer);
return true;
}
template<typename T>
void SetFieldValue(const std::string& name, T& value)
{
SetFieldValueInternal(name, &value);
}
bool ScriptInstance::SetFieldValueInternal(const std::string& name, void* value)
{
const auto& fields = m_ScriptClass->GetFields();
auto it = fields.find(name);
if (it == fields.end()) {
return false;
}
const ScriptField& field = it->second;
mono_field_set_value(m_Instance, field.ClassField, (void*)value);
return true;
}
八十七、添加C#脚本修改重载
前言
此节目的
实现在运行时按Ctrl+R重新加载核心程序集和应用程序集达到更新脚本部分更改的效果
ScriptEngine.cpp
1 | struct ScriptEngineData |
EditorLayer.cpp
1 | void EditorLayer::OnImGuiRender() |
八十八、脚本修改自动热重载
前言
此节目的
实现自动检测文件的更改,一旦更改自动热重载C#dll
ScriptEngine.cpp
1 | struct ScriptEngineData |
Application.cpp
1 | void Application::SubmitToMainThread(const std::function<void()>& function) |
八十九、添加渲染Text功能
前言
此节目的
添加Renderer2D_Text着色器,提供渲染文本功能
Renderer2D.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153struct TextVertex
{
glm::vec3 Position;
glm::vec4 Color;
glm::vec2 TexCoord;
// TODO: bg color for outline/bg
// Editor-only
int EntityID;
};
struct Renderer2DData
{
Ref<VertexArray> TextVertexArray;
Ref<VertexBuffer> TextVertexBuffer;
Ref<Shader> TextShader;
uint32_t TextIndexCount = 0;
TextVertex* TextVertexBufferBase = nullptr;
TextVertex* TextVertexBufferPtr = nullptr;
Ref<Texture2D> FontAtlasTexture;
glm::vec4 QuadVertexPositions[4]{};
}
void Renderer2D::Init()
{
// Text
s_Data.TextVertexArray = VertexArray::Create();
s_Data.TextVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(TextVertex));
s_Data.TextVertexBuffer->SetLayout({
{ ShaderDataType::Float3, "a_Position" },
{ ShaderDataType::Float4, "a_Color" },
{ ShaderDataType::Float2, "a_TexCoord" },
{ ShaderDataType::Int, "a_EntityID" }
});
s_Data.TextVertexArray->AddVertexBuffer(s_Data.TextVertexBuffer);
s_Data.TextVertexArray->SetIndexBuffer(quadIB);
s_Data.TextVertexBufferBase = new TextVertex[s_Data.MaxVertices];
s_Data.TextShader = Shader::Create("assets/shaders/Renderer2D_Text.glsl");
}
void Renderer2D::Flush()
{
if (s_Data.TextIndexCount)
{
uint32_t dataSize = (uint32_t)((uint8_t*)s_Data.TextVertexBufferPtr - (uint8_t*)s_Data.TextVertexBufferBase);
s_Data.TextVertexBuffer->SetData(s_Data.TextVertexBufferBase, dataSize);
auto buf = s_Data.TextVertexBufferBase;
s_Data.FontAtlasTexture->Bind(0);
s_Data.TextShader->Bind();
RenderCommand::DrawIndexed(s_Data.TextVertexArray, s_Data.TextIndexCount);
s_Data.Stats.DrawCalls++;
}
}
void Renderer2D::DrawString(const std::string& string, Ref<Font> font, const glm::mat4& transform, const glm::vec4& color)
{
const auto& fontGeometry = font->GetMSDFData()->FontGeometry;
const auto& metrics = fontGeometry.getMetrics();
Ref<Texture2D> fontAtlas = font->GetAtlasTexture();
s_Data.FontAtlasTexture = fontAtlas;
double x = 0.0;
double fsScale = 1.0 / (metrics.ascenderY - metrics.descenderY);
double y = 0.0;
float lineHeightOffset = 0.0f;
for (size_t i = 0; i < string.size(); i++)
{
char character = string[i];
if (character == '\r')
continue;
if (character == '\n')
{
x = 0;
y -= fsScale * metrics.lineHeight + lineHeightOffset;
continue;
}
auto glyph = fontGeometry.getGlyph(character);
if (!glyph)
glyph = fontGeometry.getGlyph('?');
if (!glyph)
return;
if (character == '\t')
glyph = fontGeometry.getGlyph(' ');
double al, ab, ar, at;
glyph->getQuadAtlasBounds(al, ab, ar, at);
glm::vec2 texCoordMin((float)al, (float)ab);
glm::vec2 texCoordMax((float)ar, (float)at);
double pl, pb, pr, pt;
glyph->getQuadPlaneBounds(pl, pb, pr, pt);
glm::vec2 quadMin((float)pl, (float)pb);
glm::vec2 quadMax((float)pr, (float)pt);
quadMin *= fsScale, quadMax *= fsScale;
quadMin += glm::vec2(x, y);
quadMax += glm::vec2(x, y);
float texelWidth = 1.0f / fontAtlas->GetWidth();
float texelHeight = 1.0f / fontAtlas->GetHeight();
texCoordMin *= glm::vec2(texelWidth, texelHeight);
texCoordMax *= glm::vec2(texelWidth, texelHeight);
// render here
s_Data.TextVertexBufferPtr->Position = transform * glm::vec4(quadMin, 0.0f, 1.0f);
s_Data.TextVertexBufferPtr->Color = color;
s_Data.TextVertexBufferPtr->TexCoord = texCoordMin;
s_Data.TextVertexBufferPtr->EntityID = 0; // TODO
s_Data.TextVertexBufferPtr++;
s_Data.TextVertexBufferPtr->Position = transform * glm::vec4(quadMin.x, quadMax.y, 0.0f, 1.0f);
s_Data.TextVertexBufferPtr->Color = color;
s_Data.TextVertexBufferPtr->TexCoord = { texCoordMin.x, texCoordMax.y };
s_Data.TextVertexBufferPtr->EntityID = 0; // TODO
s_Data.TextVertexBufferPtr++;
s_Data.TextVertexBufferPtr->Position = transform * glm::vec4(quadMax, 0.0f, 1.0f);
s_Data.TextVertexBufferPtr->Color = color;
s_Data.TextVertexBufferPtr->TexCoord = texCoordMax;
s_Data.TextVertexBufferPtr->EntityID = 0; // TODO
s_Data.TextVertexBufferPtr++;
s_Data.TextVertexBufferPtr->Position = transform * glm::vec4(quadMax.x, quadMin.y, 0.0f, 1.0f);
s_Data.TextVertexBufferPtr->Color = color;
s_Data.TextVertexBufferPtr->TexCoord = { texCoordMax.x, texCoordMin.y };
s_Data.TextVertexBufferPtr->EntityID = 0; // TODO
s_Data.TextVertexBufferPtr++;
s_Data.TextIndexCount += 6;
s_Data.Stats.QuadCount++;
if (i < string.size() - 1)
{
double advance = glyph->getAdvance();
char nextCharacter = string[i + 1];
fontGeometry.getAdvance(advance, character, nextCharacter);
float kerningOffset = 0.0f;
x += fsScale * advance + kerningOffset;
}
}
}Components.cpp
1
2
3
4
5
6
7
8struct TextComponent
{
std::string TextString;
Ref<Font> FontAsset = Font::GetDefault();
glm::vec4 Color{ 1.0f };
float Kerning = 0.0f;
float LineSpacing = 0.0f;
};Scene.cpp
1
Renderer2D::DrawString("DongHui", Font::GetDefault(), glm::mat4(1.0f), glm::vec4(1.0f));
九十、 脚本绑定Text相关
前言
此节目的
绑定Text组件相关函数到C#,以供脚本实现文本功能
ScriptEngine.cpp
1
2
3
4MonoString* ScriptEngine::CreateString(const char* string)
{
return mono_string_new(s_Data->AppDomain, string);
}ScriptGlue.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107namespace Utils
{
std::string MonoStringToString(MonoString* string)
{
char* cStr = mono_string_to_utf8(string);
std::string str(cStr);
mono_free(cStr);
return str;
}
}
static MonoString* TextComponent_GetText(UUID entityID)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
return ScriptEngine::CreateString(tc.TextString.c_str());
}
static void TextComponent_SetText(UUID entityID, MonoString* textString)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
tc.TextString = Utils::MonoStringToString(textString);
}
static void TextComponent_GetColor(UUID entityID, glm::vec4* color)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
*color = tc.Color;
}
static void TextComponent_SetColor(UUID entityID, glm::vec4* color)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
tc.Color = *color;
}
static float TextComponent_GetKerning(UUID entityID)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
return tc.Kerning;
}
static void TextComponent_SetKerning(UUID entityID, float kerning)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
tc.Kerning = kerning;
}
static float TextComponent_GetLineSpacing(UUID entityID)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
return tc.LineSpacing;
}
static void TextComponent_SetLineSpacing(UUID entityID, float lineSpacing)
{
Scene* scene = ScriptEngine::GetSceneContext();
HE_CORE_ASSERT(scene);
Entity entity = scene->GetEntityByUUID(entityID);
HE_CORE_ASSERT(entity);
HE_CORE_ASSERT(entity.HasComponent<TextComponent>());
auto& tc = entity.GetComponent<TextComponent>();
tc.LineSpacing = lineSpacing;
}Components.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public class TextComponent : Component
{
public string Text
{
get => InternalCalls.TextComponent_GetText(Entity.ID);
set => InternalCalls.TextComponent_SetText(Entity.ID, value);
}
public Vector4 Color
{
get
{
InternalCalls.TextComponent_GetColor(Entity.ID, out Vector4 color);
return color;
}
set
{
InternalCalls.TextComponent_SetColor(Entity.ID, ref value);
}
}
public float Kerning
{
get => InternalCalls.TextComponent_GetKerning(Entity.ID);
set => InternalCalls.TextComponent_SetKerning(Entity.ID, value);
}
public float LineSpacing
{
get => InternalCalls.TextComponent_GetLineSpacing(Entity.ID);
set => InternalCalls.TextComponent_SetLineSpacing(Entity.ID, value);
}
}