我自己维护引擎的github地址在这里,里面加了不少注释,有需要的可以看看
参考视频链接在这里
很高兴的是,引擎的开发终于慢慢开始往深了走了,前几章的引擎UI搭建着实是有点折磨人,根据课程,接下来的引擎开发路线是:
不过由于我对某些章节比较感兴趣,所以实现模块的顺序可能跟Cherno课上的顺序略有不同
Scripting is designed to control the Engine.
之前在学Unity的时候,我就对这一块特别感兴趣,因为我在没有完全了解C#、Mono和C++之间的交互原理之前,去看Unity的源码是一件非常费劲的事情,所以把这几课的内容提前实现了。
参考:Mono Embedding for Game Engines
引擎打算跟Unity一样,使用C#作为脚本层,此时需要使用Mono作为C#与C++语言的衔接层,这里的Mono由于本身就很大,所以不作为Submodule了,而是直接下载对应的Binary放到引擎里,具体步骤有:
mscorlib.dll
Mono is an open source implementation of Microsoft’s .NET Framework.
Mono这个项目是微软官方开发的,最初是为了让.NET语言(比如C#)能在除了windows之外的系统上运行,不过现在的.NET Core已经是自动支持跨平台了,但这并不意味着Mono就没用了,它仍然为在.NET Runtime里使用C/C++的API提供了很好的支持,根据我在Unity论坛上看到的官方在2016年发布的声明来看,Unity近期内也没有舍弃Mono,来用.NET Core
的想法,相关言论如下:
We will not be using .NET Core to replace Mono. The open-source runtime is not ported to enough platforms for Unity, and it doesn’t have the embedding hooks that we need (Mono fulfills both of those requirements). Also, the class library profile in .NET Core doesn’t have a number of things that we support currently, and others it moves around, effectively breaking all previous Unity projects - so using it is not feasible.
We will likely support the .NET Standard though. Plans are still up in the air as we work through the technical issues. Please watch that experimental scripting previews forum for more details. We will continue to post there when we have new builds to drop with added functionality.
Mono其实有俩主要的版本:
这里选用的Mono版本是Classic Mono,原因如下:
assembly reloading
,这意味着我写C#代码,需要手动重新编译,那我开启游戏Editor在里面写脚本时,我肯定希望是Runtime重新load assembly的,不可能我每次改脚本都要重启引擎这里需要的库文件分为两类:.NET原生的库文件和Mono提供的库文件。
这里选择直接下载Mono的Github项目,然后选择一个可用的Tag版本,手动Build出来对应的库文件。Mono的官方Build文档看起来很复杂,但实际上好像没这么麻烦,直接clone该项目,它里面已经自带了VS用的solution文件了,在msvc文件夹下:
类似之前配置Vulkan SDK在项目里的方式,之前是调用Vulkan SDK.exe,然后把安装得到的include目录添加到Hazel Project的include路径,再把安装得到的Debug和Release下的库文件也添加到Hazel Project的depend路径即可。
这里的做法差不多,无非这里的header和库文件都是我亲手build出来的,直接从这个solution里build即可,会得到如下文件夹:
把里面的东西直接挪到HazelEditor/Mono/lib文件夹下即可,后面打算使用static link的方式,把mono link到Hazel里:
Mono需要的库文件有:
里面分为Debug和Release两个版本:
至于.NET提供的库文件,需要到网上去下载,这里选择直接安装Mono.exe,安装之后会存放对应的.NET库文件,如下图所示:
把这里的4.5文件夹下的内容,拷贝到Hazel的vendor下即可,我这里的存放路径为:
步骤也不麻烦:
先创建对应C#源码的路径,这个Test.cs作为样例cs文件:
再写premake5.lua文件即可,C#工程需要编译出一个dll:
project "Hazel-ScriptCore"location "%{prj.name}"kind "SharedLib"language "C#"dotnetframework "4.7.2"targetdir ("%{prj.name}/Build")objdir ("%{prj.name}/Intermediates")files {"%{prj.name}/Scripts/**.cs"}
这里先梳理一下各个项目之间的关系:
整个C++部分的引擎工程(即Hazel项目,包括其依赖的Mono部分),都是编译为一个Hazel.lib
的库文件的,HazelEditor工程会Link这个Hazel.lib
然后一起编译出一个HazelEditor.exe
文件,作为用户开发游戏的Editor软件(类似Unity2020.exe
),最后,C#部分的工程会build出来一个C#的dll,再把它跟HazelEditor.exe
文件放一起应该就可以了。
C++与C#之间交互的核心原理其实很简单,无论是C++调用C#,还是C#调用C++都是通过中间的Mono实现的,具体有:
参考:Mono Embedding for Game Engines
为了实现C++和C#通过Mono互相调用,需要额外写对接的代码。这里先保证能从C++端调用C#的代码,把前面编译得到的C# dll读取到C++的Mono项目里,同时在C++里通过Mono去操作C#这边dll里的内容,比如call method,读取Property和Field值等。
我创建了个Scripting类(这一部分代码参考前面提到的文档就行了):
#pragma once
#include
#include "mono/metadata/image.h"
#include "mono/jit/jit.h"namespace Hazel
{// 类似于Unity, C#这边的脚本层分为核心层和用户层两块// 核心层的代码(C#这边的源码)应该是和C++的代码会存在相互调用的情况的class Scripting{public:MonoAssembly* LoadCSharpAssembly(const std::string& assemblyPath);void PrintAssemblyTypes(MonoAssembly* assembly);// 根据C++这边输入的class name, 返回对应的MonoClass, 如果想在C++端创建C#上的对象, 需要借助此APIMonoClass* GetClassInAssembly(MonoAssembly* assembly, const char* namespaceName, const char* className);MonoObject* CreateInstance(MonoClass* p);// Mono gives us two ways of calling C# methods: mono_runtime_invoke and Unmanaged Method Thunks. // This Api will only cover mono_runtime_invoke// Using mono_runtime_invoke is slower compared to Unmanaged Method Thunks, but it's also safe and more flexible. // mono_runtime_invoke can invoke any method with any parameters, and from what I understand mono_runtime_invoke also does a lot more error checking and validation on the object you pass, as well as the parameters.// 在编译期不知道Method签名时, 适合用mono_runtime_invoke, 每秒高频率调用(10fps)的Method适合用Unmanaged Method Thunks, void CallMethod(MonoObject* instance, const char* methodName);// Field can be public or privateMonoClassField* GetFieldRef(MonoObject* instance, const char* fieldName);templateconst T& GetFieldValue(MonoObject* instance, MonoClassField* field){T value;mono_field_get_value(instance, field, &value);return value;}MonoProperty* GetPropertyRef(MonoObject* instance, const char* fieldName);templateconst T& GetPropertyValue(MonoObject* instance, MonoProperty* prop){T value;mono_property_get_value(instance, prop, &value);return value;}};
}
类实现如下:
#include "hzpch.h"
#include "Hazel/Utils/Utils.h"
#include "mono/metadata/assembly.h"
#include "Scripting.h"namespace Hazel
{static MonoDomain* s_CSharpDomain;// 读取一个C# dll到Mono里, 然后返回对应的Assembly指针MonoAssembly* Scripting::LoadCSharpAssembly(const std::string& assemblyPath){// InitMono部分// Let Mono know where the .NET libraries are located.mono_set_assemblies_path("../Hazel/vendor/Mono/DotNetLibs/4.5");MonoDomain* rootDomain = mono_jit_init("MyScriptRuntime");if (rootDomain == nullptr){// Maybe log some error herereturn nullptr;}// Create an App Domains_CSharpDomain = mono_domain_create_appdomain("MyAppDomain", nullptr);mono_domain_set(s_CSharpDomain, true);uint32_t fileSize = 0;// 用于直接读取C#的.dll文件, 把它读作bytes数组char* fileData = Utils::ReadBytes(assemblyPath, &fileSize);// NOTE: We can't use this image for anything other than loading the assembly because this image doesn't have a reference to the assemblyMonoImageOpenStatus status;// 把读取的dll传给Mono, 得到的assembly会存在Mono这边, 暂时不需要反射MonoImage* image = mono_image_open_from_data_full(fileData, fileSize, true, &status, false);if (status != MONO_IMAGE_OK){const char* errorMessage = mono_image_strerror(status);// Log some error message using the errorMessage datareturn nullptr;}// 从image里读取assembly指针MonoAssembly* assembly = mono_assembly_load_from_full(image, assemblyPath.c_str(), &status, 0);mono_image_close(image);// Don't forget to free the file datadelete[] fileData;return assembly;}// iterate through all the type definitions in our assemblyvoid Scripting::PrintAssemblyTypes(MonoAssembly* assembly){MonoImage* image = mono_assembly_get_image(assembly);// 从assembly的meta信息里读取meta data table, 这里读取的是Type对应的Table, 表里的每一行// 代表一个Typeconst MonoTableInfo* typeDefinitionsTable = mono_image_get_table_info(image, MONO_TABLE_TYPEDEF);int32_t numTypes = mono_table_info_get_rows(typeDefinitionsTable);// 遍历Table里的每行, 这里的numTypes最小为1, 因为C#的DLL和EXEs默认都会有一个Module类型的Type, 代表整个// assembly的modulefor (int32_t i = 1; i < numTypes; i++){// 每一行的每列元素记录了Type的相关信息, 比如namespace和type nameuint32_t cols[MONO_TYPEDEF_SIZE];mono_metadata_decode_row(typeDefinitionsTable, i, cols, MONO_TYPEDEF_SIZE);// 还可以获取field list和method list等const char* nameSpace = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAMESPACE]);const char* name = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAME]);printf("%s.%s\n", nameSpace, name);}}MonoClass* Scripting::GetClassInAssembly(MonoAssembly* assembly, const char* namespaceName, const char* className){MonoImage* image = mono_assembly_get_image(assembly);MonoClass* klass = mono_class_from_name(image, namespaceName, className);if (!klass)return nullptr;return klass;}MonoObject* Scripting::CreateInstance(MonoClass* p){if (!p) return nullptr;MonoObject* classInstance = mono_object_new(s_CSharpDomain, p);// Call the parameterless (default) constructormono_runtime_object_init(classInstance);return classInstance;}void Scripting::CallMethod(MonoObject* objectInstance, const char* methodName){// Get the MonoClass pointer from the instanceMonoClass* instanceClass = mono_object_get_class(objectInstance);// Get a reference to the method in the classMonoMethod* method = mono_class_get_method_from_name(instanceClass, methodName, 0);if (!method) return;// Call the C# method on the objectInstance instance, and get any potential exceptionsMonoObject* exception = nullptr;mono_runtime_invoke(method, objectInstance, nullptr, &exception);// TODO: Handle the exception}// 注意, MonoClassField本身不含Field数据, 里面存的是数据相对于object的offsetMonoClassField* Scripting::GetFieldRef(MonoObject* objInstance, const char* fieldName){MonoClass* testingClass = mono_object_get_class(objInstance);// Get a reference to the public field called "MyPublicFloatVar"return mono_class_get_field_from_name(testingClass, fieldName);}MonoProperty* Scripting::GetPropertyRef(MonoObject* objInstance, const char* propertyName){MonoClass* testingClass = mono_object_get_class(objInstance);// Get a reference to the public field called "MyPublicFloatVar"return mono_class_get_property_from_name(testingClass, propertyName);}
}
然后在代码里随便找个地方调用它来测试一下即可,我测过是OK的
前面的部分实现了在C++调用C#里的任何内容,包括调用Method和获取Property和Field等,现在需要反过来,实现在C#里调用C++提供的API。其实有很多可选的做法:
本章的内容如下:
P/Invoke is a technology that allows you to access structs, callbacks, and functions in unmanaged libraries from your managed code. Most of the P/Invoke API is contained in two namespaces: System and System.Runtime.InteropServices. Using these two namespaces give you the tools to describe how you want to communicate with the native component.
重点是:从managed code里获取unmanaged库里的structs、回调和functions,相关的managed的API都主要是在System
和 System.Runtime.InteropServices
命名空间下。
举个简单例子:
using System;
using System.Runtime.InteropServices;public class Program
{// Import user32.dll (containing the function we need) and define// the method corresponding to the native function.// 关键的DllImport Attribute, 它会让.NET Runtime去load对应的unmanaged dll(user32.dll)[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]// p/Invoke会定义与C++的函数签名完全相同的C# Method, private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);public static void Main(string[] args){// Invoke the function as a regular managed method.MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);}
}
这种写法的调用还是有overhead的,它比普通的调用C# method的开销要大
参考:Embedding Mono
参考:Mono Embedding for Game Engines
目前的Scripting系统,对应的.NET和Assembly的代码,其实是Hazel的一部分,所以并不适合用P/Invoke(而且Hazel引擎目前是用的static linking)。这里的Internal Call,相当于告诉.NET Runtime,我有一些Native Functions你可以调用。
之前都是利用mono_runtime_object_init
、mono_metadata_decode_row
等函数,通过mono_runtime或者meta机制,在C++里对C#进行操作的。C++这边没有metadata这种方便的东西,所以要通过这里的Internal Call,即mono提供的另一种机制,暴露接口给Mono。
主要是学习一下怎么写API,这里一共举了这么些例子:
核心就是把C++的static函数通过mono_add_internal_call
函数登记一下,然后就可以在C#这边调用了
C#调用C++的无参静态函数
举个简单的例子,有个无参的静态函数,C++端的写法如下:
static void PrintFuncForCSharp()
{LOG("PrintFuncForCSharp");
}// method需要用ClassName::的形式
mono_add_internal_call("MyNamespace.Program::Print", &PrintFuncForCSharp);
C#端的写法如下:
using System;
using System.Runtime.CompilerServices;namespace MyNamespace
{public class Program{public float MyPublicFloatVar = 5.0f;public void PrintFloatVar(){Console.WriteLine("MyPublicFloatVar = {0:F}", MyPublicFloatVar);Print();}[MethodImplAttribute(MethodImplOptions.InternalCall)]// 函数的名字其实是在C++这边就已经写死了的extern static void Print();}
}
C#调用C++的string为参数的静态函数
如果函数有签名,也是差不多的写法,需要注意的是函数参数为string时的情况,由于托管堆和非托管堆的string内存结构不同,所以从C#调用C++带string参数的函数时,C++这边对应函数的参数不是string,而是MonoString*
,举个例子,C++端的写法如下:
static void PrintStringFuncFromCSharp(MonoString* str)
{char* arr = mono_string_to_utf8(str);LOG(arr);// 释放内存mono_free(arr);
}mono_add_internal_call("MyNamespace.Program::PrintString", &PrintStringFuncFromCSharp);
C#端的写法如下:
using System;
using System.Runtime.CompilerServices;namespace MyNamespace
{public class Program{public void PrintFloatVar(){PrintString("PrintString");}[MethodImplAttribute(MethodImplOptions.InternalCall)]extern static void PrintString(string s);}
}
C#调用C++的自定义struc为参数的静态函数
由于C#里struct和class的区别,这里具体分为两种,如果是struct,那么写法为:
// csharp这边传入的参数为ref A ...// c++这边传入的参数为A*
C#调用C++的重名重载函数
鉴于mono_add_internal_call("MyNamespace.Program::Print", &PrintFuncForCSharp);
这种写法,C++这边应该是不支持函数重载的,但是C#这边是可以通过Wrapper来模拟函数重载的,所以C++这边只能用老的C语言的方式处理函数重载了,比如:
// C++ 端
static void Func(){};
static void FuncString(std::string){};// C#端
[MethodImplAttribute(MethodImplOptions.InternalCall)]
extern static void Func();[MethodImplAttribute(MethodImplOptions.InternalCall)]
extern static void FuncString(string);// 加个wrapper
public static void Function()
{Func();
}public static void Function(string s)
{FuncString(s);
}
如果想要在C++设计一个函数,这个函数返回一个指针或引用,此时再把这个函数暴露给C#,是不太好的,更好的方法是让这个函数返回值为void,原本返回的参数作为函数参数传入和传出(应该C#这边也能接受C++返回的指针,无非是要用unsafe的代码),写法大概是这样:
// C++
static void Func(glm::vec3* para, glm::vec3* outResult)// 原本想返回的*变成了参数
{...
}// C#
[MethodImplAttribute(MethodImplOptions.InternalCall)]
extern static void FuncString(ref Vector3 para, out Vector3 result);
而且这样写也不合理,应该是谁分配的内存,谁负责释放。
至此,C#与C++的交互的基础框架代码基本写好了,后面的课程Using C# Scripting with the Entity Component System是基于前面搭建好的关卡制作的,所以我就接着从前面跳过的Content Browser开始学习了。
虽然Content Browser很明显是Editor下用的东西,但并不意味着任何存在于Editor下的模块都没有在Runtime下出现的可能,毕竟它可能需要在Runtime为了Debug使用,Content Browser的主要功能是:
显然这个功能是不大可能需要进行Runtime Debug使用的(前面做过的Hierarchy窗口还有一些在Runtime使用可能,它可以帮忙看看Runtime下的场景Hierarchy)。所以会把Content Browser相关的代码写到HazelEditor工程下,目前的Hierarchy窗口则写在Hazel工程里。
这节课主要内容:
directory_iterator
遍历directories(见附录)OnImGuiRender
函数里绘制相关界面类声明很简单:
namespace Hazel
{class ContentBrowserPanel{public:const float HEIGHT = 24.0f;void Init();void OnImGuiRender();private:std::filesystem::path m_CurSelectedPath;std::filesystem::path m_LastSelectedPath;std::shared_ptr m_DirTex;std::shared_ptr m_FileTex;};
};
思路是文件夹和文件都用Button绘制,Button对应的背景图片是一个文件夹的图片,代码如下:
namespace Hazel
{void ContentBrowserPanel::Init(){m_DirTex = Texture2D::Create("Resources/Icons/DirectoryIcon.png");m_FileTex = Texture2D::Create("Resources/Icons/FileIcon.png");}void ContentBrowserPanel::OnImGuiRender(){ImGui::Begin("ContentBrowser");{std::filesystem::path p;if (m_CurSelectedPath.empty()){p = std::filesystem::current_path();m_CurSelectedPath = p;}elsep = std::filesystem::current_path() / (m_CurSelectedPath);// Combine Pathif (ImGui::Button("<-")){if (!m_LastSelectedPath.empty())m_CurSelectedPath = m_LastSelectedPath;}// 绘制项目根目录下的所有内容for (const std::filesystem::directory_entry& pp : std::filesystem::directory_iterator(p)){bool isDir = pp.is_directory();int frame_padding = -1; // -1 == uses default padding (style.FramePadding)ImVec2 size = ImVec2(HEIGHT, HEIGHT); // Size of the image we want to make visibleif (isDir)ImGui::Image((ImTextureID)m_DirTex->GetTextureId(), size, { 0, 0 }, { 1, 1 });elseImGui::Image((ImTextureID)m_FileTex->GetTextureId(), size, { 0, 0 }, { 1, 1 });ImGui::SameLine();if (ImGui::Button(pp.path().string().c_str())){if (isDir){m_LastSelectedPath = m_CurSelectedPath;m_CurSelectedPath = pp;}//LOG(m_CurSelectedPath);}}}ImGui::End();}
}
效果如下图所示:
做了以下事情:
ImGui::ImageButton
的默认背景颜色,属于ImGui的相关API写法(我用的ImGui::Image
绘制的icon,没有这个问题)代码如下:
// 1. 双击进入folder
// 不再直接判断Button是否点击了, 而是通过ImGui的MouseDoubleClick状态和是否hover来判断双击的
// 其实这里的ImGui::Button改成ImGui::Text也可以双击, 无非是没有hover时的高亮button效果了
ImGui::Button(pp.path().string().c_str());
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{if (isDir){m_LastSelectedPath = m_CurSelectedPath;m_CurSelectedPath = pp;}
}// 2. 通过这种写法, 让Button绘制时的默认颜色为0,0,0,0, 去掉alpha通道的影响
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::ImageButton((ImTextureID)icon->GetRendererID(), { thumbnailSize, thumbnailSize }, { 0, 1 }, { 1, 0 });
ImGui::PopStyleColor();
imgui拖拽相关的api比较复杂,所以这里单独分析一下。看了下Cherno写的代码,感觉imgui设计DragAndDrop的思路是这样的:
在imgui的render的循环里,在绘制了element后可以发出Drag的请求(此时的Drag是只针对前面绘制的element使用的),Drag时可以用byte数组的形式传入Data(也叫payload),imgui会把这个数据和drag的请求存起来,然后在别的窗口的render的代码里,可以查询Drop的状态,一旦判定了鼠标在该窗口释放,那么Drop的状态返回true,然后可以获取出对应的payload
相关代码如下:
// PS: imgui.h的698行提供了相关Drag和Drop的API// 发送数据和Drag请求的代码
void ContentBrowserPanel::OnImGuiRender()
{ImGui::Begin("ContentBrowser");{...// 绘制项目根目录下的所有内容for (const std::filesystem::directory_entry& pp : std::filesystem::directory_iterator(p)){...const auto& path = pp.path();// 不再直接判断Button是否点击了, 而是通过ImGui的MouseDoubleClick状态和是否hover来判断双击的// 其实这里的ImGui::Button改成ImGui::Text也可以双击, 无非是没有hover时的高亮button效果了ImGui::Button(path.string().c_str());if (path.extension() == ".scene"){// 拖拽时传入拖拽的item的pathif (ImGui::BeginDragDropSource()){const wchar_t* itemPath = path.c_str();int len = wcslen(itemPath) + 1;// Convert w_char array to char arr(deep copy)char* itemPathArr = new char[len];std::wcsrtombs(itemPathArr, &itemPath, len, nullptr);ImGui::SetDragDropPayload("CONTENT_BROWSER_ITEM", itemPathArr, (len) * sizeof(char));ImGui::EndDragDropSource();}}...}}ImGui::End();
}// 接受数据和Drop请求的代码
ImGui::Begin("Viewport");
{...// Viewport其实就是一张贴图ImGui::Image(m_ViewportFramebuffer->GetColorAttachmentTexture2DId(), size, { 0,1 }, { 1,0 });if (ImGui::BeginDragDropTarget()){if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_ITEM")){const char* path = (const char*)payload->Data;LOG(path);// 后续可以打开Scene}ImGui::EndDragDropTarget();}...
}
这里我只打印了path,具体打开Scene的操作也很简单,以后再加上
这节课的目的:
这里使用的是2D的Renderer,这种2D的Renderer,目前是只用贴图赋值上去就行了,未来2D的Renderer需不需要material的参与,后面再说(可能还得参考Unity或Unreal的做法)
具体做法挺简单的,主要是imgui的写法:
// 发出Drag事件
ImGui::Begin("ContentBrowser");
{...if (path.extension() == ".scene")...if (path.extension() == ".png" || path.extension() == ".jpg"){// 拖拽时传入拖拽的item的pathif (ImGui::BeginDragDropSource()){const wchar_t* itemPath = path.c_str();int len = wcslen(itemPath) + 1;// Convert w_char array to char arr(deep copy)char* itemPathArr = new char[len];std::wcsrtombs(itemPathArr, &itemPath, len, nullptr);ImGui::SetDragDropPayload("CONTENT_BROWSER_ITEM_IMAGE", itemPathArr, (len) * sizeof(char));ImGui::EndDragDropSource();}}...
}
ImGui::End();// 接受Drag事件
// Draw SpriteRendererComponent
if (go.HasComponent())
{DrawComponent("SpriteRenderer", go, [](SpriteRenderer& sr){ImGui::ColorEdit4("Color", glm::value_ptr(sr.GetTintColor()));// 贴图槽位其实是用Button绘制的, 这里并没有绘制出贴图的略缩图ImGui::Button("Texture", ImVec2(100.0f, 0.0f));if (ImGui::BeginDragDropTarget()){// 在Content Panel里做了相关文件拽出的代码, 这里只要做接受的代码即可if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_ITEM_IMAGE")){const char* path = (const char*)payload->Data;std::filesystem::path texturePath = path;sr.SetTexture(Texture2D::Create(texturePath.string()));}ImGui::EndDragDropTarget();}ImGui::DragFloat("Tiling Factor X", &sr.GetTilingFactor().x, 0.1f, 0.0f, 100.0f);ImGui::DragFloat("Tiling Factor Y", &sr.GetTilingFactor().y, 0.1f, 0.0f, 100.0f);});
}
最后再改一下SprieRenderer组件函数,支持绘制带Texture的Quad即可
很遗憾这个系列未来只会支持2D了,不过其实也是合理的,Cherno已经教给了我们很多东西了,在这个基础上是该自己去学习,再去搭建属于自己的3D引擎了
这节课梳理了下知识点,值得记下来的有:
目前的Hazel引擎,作为2D的游戏引擎,还缺少的功能有:
还有很多内容,就不一一列举了,后面慢慢加吧
先修复了一下上节课的bug,Shader里的TextureId应该用uniform以flat形式传输,用顶点数组数据的形式会被interpolate,这节课内容也挺简单的,内容不多:
核心就这点UI的代码:
// 思路是绘制一个小窗口, 然后拖到Dock里布局好, 此横向小窗口作为Toolbar, 中间绘制PlayButton
void EditorLayer::DrawUIToolbar()
{ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2));ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(0, 0));ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));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::Begin("##toolbar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);{float size = ImGui::GetWindowHeight() - 4.0f;std::shared_ptr icon = m_PlayMode == PlayMode::Edit ? m_IconPlay : m_IconStop;ImGui::SetCursorPosX((ImGui::GetWindowContentRegionMax().x * 0.5f) - (size * 0.5f));if (ImGui::ImageButton((ImTextureID)icon->GetTextureId(), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1), 0)){if (m_PlayMode == PlayMode::Edit)OnScenePlay();else if (m_PlayMode == PlayMode::Play)OnSceneStop();}ImGui::PopStyleVar(2);ImGui::PopStyleColor(3);}ImGui::End();
}
这是C++17的std::filesystem
提供的方便遍历目录的类,如下是例子,可以迅速遍历出来所有的子目录路径(返回的是相对的带斜杠的路径,而且不含子子文件的路径)
这里的auto类型为:
const std::filesystem::directory_entry
报错信息如下:
1>Hazel.lib(w32socket.obj) : error LNK2019: unresolved external symbol __imp_bind referenced in function mono_w32socket_bind
1>Hazel.lib(threadpool-io.obj) : error LNK2001: unresolved external symbol __imp_bind
可以看到这里的bind函数是找不到的,搜了一下win32 bind,发现它是属于Ws2_32.lib
下的API,然后我这么强行添加依赖,就可以了:
但为什么会这样呢?我自己build出来的static lib,应该是一个完整的内容,为啥还会让我额外依赖lib库呢。
首先,我分析了一下我Build这个static lib的过程,它的Build过程的依赖项目有:
在分析之前,需要介绍一些我在分析过程中额外学到的知识
如下图所示,这个选项:
参考:What is “Utility” Configuration type in Visual Studio
The utility project does not generate any predetermined output files, such as a .LIB, .DLL or .EXE. A utility project can be used as a container for files you can build without a link step
这种项目没有任何output文件,它可以用于:
感觉看这个说明,很难直接理解,我经过自己实践后发现,Utility类型的Project可以在build过程中创建新的头文件,相关LOG信息如下:
Rebuild started...
1>------ Rebuild All started: Project: build-init, Configuration: Release x64 ------
1>Setting up Mono configuration headers...
1>Successfully setup Mono configuration headers F:\GitRepositories\mono\msvc\..\config.h and F:\GitRepositories\mono\msvc\..\mono\mini\version.h from F:\GitRepositories\mono\msvc\..\winconfig.h.
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========
报错:The assembly mscorlib.dll was not found or could not be loaded.
在C# scripting这节课里,加载.NET库时,出了这个问题,很奇怪,咋都弄不对。
一开始是查了下源码,相关函数为:
/*** mono_set_assemblies_path:* \param path list of paths that contain directories where Mono will look for assemblies** Use this method to override the standard assembly lookup system and* override any assemblies coming from the GAC(Global Assembly Cache). This is the method* that supports the \c MONO_PATH variable.** Notice that \c MONO_PATH and this method are really a very bad idea as* it prevents the GAC from working and it prevents the standard* resolution mechanisms from working. Nonetheless, for some debugging* situations and bootstrapping setups, this is useful to have. */
// 这里的path可以是一堆用';'分隔的path的集合
void
mono_set_assemblies_path (const char* path)
{char **splitted, **dest;// 由于path可能含有多个路径, 路径之间用';'分隔, 所以这里把路径拆分为多个子路径, 最多有1000个子路径splitted = g_strsplit (path, G_SEARCHPATH_SEPARATOR_S, 1000);// 如果存在Mono_Path, 则把它置为null strif (assemblies_path)g_strfreev (assemblies_path);assemblies_path = dest = splitted;// 遍历每个输入的路径while (*splitted) {char *tmp = *splitted;// canonicalize: 规范化, 这个函数会规范输入的str, 应该会返回绝对路径if (*tmp)*dest++ = mono_path_canonicalize (tmp);g_free (tmp);splitted++;}*dest = *splitted;// 所以最终的路径都存在了assemblies_path里if (g_hasenv ("MONO_DEBUG"))return;splitted = assemblies_path;// 重新遍历每个输入的路径, 对于里面不合理的Dir, 打印出对应的警告while (*splitted) {// 如果输入路径不是已经存在的Dir, 则会打印path in MONO_PATH doesn't exist or has wrong permissions.if (**splitted && !g_file_test (*splitted, G_FILE_TEST_IS_DIR))g_warning ("'%s' in MONO_PATH doesn't exist or has wrong permissions.", *splitted);splitted++;}
}
然后怎么改路径都不对,最后发现自己复制粘贴进来的库的文件不对劲(不知道为啥,这也太奇怪了):
参考:The 4th argument in glDrawElements is WHAT?
事情是这样的,我同一个工程,两台电脑上,一台可以正常运行,另一台会在下面这行代码里报错:
// 报错时count为12
glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr);
关于这第四个参数,貌似以前是这么解释的:
indices: Specifies a byte offset (cast to a pointer type) into the buffer bound to GL_ELEMENT_ARRAY_BUFFER to start reading indices from.
而现在都是这么解释的:
indices: Specifies a pointer to the location where the indices are stored.
这两种解释都是正确的,取决于绘制的方式:
glDrawElements
的第四个参数传indices数组给GPU,来帮助绘制Using VBO:
For this case - see the definition of Index as “Specifies a byte offset (cast to a pointer type) into the buffer bound to GL_ELEMENT_ARRAY_BUFFER to start reading indices from”. This means that the data is already uploaded separately using glBufferData, and the index is used as an offset only. Everytime glDrawElements is called, the buffer is not uploaded, but only the offset can change if required. This makes it more efficient, especially where large number of vertices are involved.
参考Hazel - My Game Engine // Code Review6分12秒,这里借助AlDanial提供的cloc工具,快速查询了Hazel的src文件夹下的代码量级:
这里出现的数字都是行数,比如说,检测到了C++的cpp文件有56353行,里面有13315行是空白行,有3751行是注释行
参考:https://stackoverflow.com/questions/12124146/vc-fatal-error-lnk1168-cannot-open-filename-exe-for-writing
代码一直报错,意思是我没关掉应用cannot open filename.exe for writing
,但我又在任务管理器里找不到对应的exe。后来发现可以打开资源监视器,里面可以找到并杀掉对应的进程。
打开任务管理器的性能页面,左下角就可以打开Resources Monitor:
参考:2D PHYSICS! // Game Engine series
需要在Visual Studio里,选中cpp文件,然后右键点击属性,在里面的C+±>Advanced->Show Includes改为Yes,然后直接编译该cpp即可(Visual Studio里使用Ctrl + F7可以单独编译一个cpp),如下图所示:
结果如下图所示,看了下,这个选项也可以针对整个project进行设置:
注意,Output这里的Include的信息前面很多空格,这是为了方便显示嵌套include情况的,比如下面这个:`
1>Note: including file: F:\GitRepositories\Hazel\Hazel\Src\Hazel\ECS/Components/Transform.h
1>Note: including file: F:\GitRepositories\Hazel\Hazel\Src\Hazel\ECS/SceneSerializer.h
1>Note: including file: F:\GitRepositories\Hazel\Hazel\vendor\yaml-cpp\include\yaml-cpp/yaml.h
1>Note: including file: F:\GitRepositories\Hazel\Hazel\vendor\yaml-cpp\include\yaml-cpp/parser.h
1>Note: including file: F:\GitRepositories\Hazel\Hazel\vendor\yaml-cpp\include\yaml-cpp/dll.h
通过这个缩进,可以看出来,SceneSerializer.h引用了yaml.h,而yaml.h引用了parser.h。如果想要知道一个头文件在哪里被引用了,可以把这些Output粘贴到VS Code里,然后鼠标中键点在目标头文件的head位置,往上拖拽即可,直到找到多一个字母的路径,如下图所示: