建设网站的费用如何入账,网站全新改版如何做,网站开发怎么学,什么站做咨询网站好我自己维护引擎的github地址在这里#xff0c;里面加了不少注释#xff0c;有需要的可以看看 参考视频链接在这里 很高兴的是#xff0c;引擎的开发终于慢慢开始往深了走了#xff0c;前几章的引擎UI搭建着实是有点折磨人#xff0c;根据课程#xff0c;接下来的引擎开发…我自己维护引擎的github地址在这里里面加了不少注释有需要的可以看看 参考视频链接在这里 很高兴的是引擎的开发终于慢慢开始往深了走了前几章的引擎UI搭建着实是有点折磨人根据课程接下来的引擎开发路线是
Content Browser又是UI。。。。简单的UUID系统PlayMode的开发点击按钮可以实现Editor下游戏的播放与暂停2D物理系统基础图元的渲染(目前只有Quad)C#脚本层与引擎C代码的交互这个我很感兴趣跟Unity交互的方式是类似的也是用Mono
不过由于我对某些章节比较感兴趣所以实现模块的顺序可能跟Cherno课上的顺序略有不同 C# Scripting 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放到引擎里具体步骤有
了解Mono获取Mono对应的Debug和Release版本的库文件还有对应的头文件获取Mono使用到的.NET的库文件比如mscorlib.dll在Hazel项目里添加对应库文件的配置修改Premake5.lua文件在Hazel里添加一个新的Project作为C#这边代码的ProjectC#工程会编译得到一个dll写C代码把前面编译得到的C# dll读取到C的Mono项目里 了解Mono 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其实有俩主要的版本
Classic Mono只支持最高到C#7.NET frameworkd 4.7.2的版本.NET Core Mono与跨平台.NET Core相匹配的Mono支持最新版的C#
这里选用的Mono版本是Classic Mono原因如下
Classic Mono更简单.NET Core Mono暂时不支持assembly reloading这意味着我写C#代码需要手动重新编译那我开启游戏Editor在里面写脚本时我肯定希望是Runtime重新load assembly的不可能我每次改脚本都要重启引擎 获取Mono对应的Debug和Release版本的库文件
这里需要的库文件分为两类.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需要的库文件有
eglib.liblibgcmonosgen.liblibmini-sgen.liblibmonoruntime-sgen.liblibmono-static-sgen.liblibmonoutils.libmono-2.0-sgen.libMonoPosixHelper.lib
里面分为Debug和Release两个版本 获取Mono使用到的.NET的库文件
至于.NET提供的库文件需要到网上去下载这里选择直接安装Mono.exe安装之后会存放对应的.NET库文件如下图所示 把这里的4.5文件夹下的内容拷贝到Hazel的vendor下即可我这里的存放路径为: 创建C#工程和Assembly
步骤也不麻烦
C#代码都放HazelEditor的Scripts文件夹下用premake5.lua文件创建C#工程
先创建对应C#源码的路径这个Test.cs作为样例cs文件
再写premake5.lua文件即可C#工程需要编译出一个dll:
project Hazel-ScriptCorelocation %{prj.name}kind SharedLiblanguage C#dotnetframework 4.7.2targetdir (%{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#还是C#调用C都是通过中间的Mono实现的具体有
C调用C#时由于C#的metadata的机制mono可以直接知道它有哪些method哪些类只要知道名字就可以直接从C这边通过Mono调用C#调用C时由于C没有直接类似的metadata机制所以C里需要选择性的暴露接口出来然后再在Mono这边登记过后C#这边才可以调用 Scripting类的创建
参考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 string
#include mono/metadata/image.h
#include mono/jit/jit.hnamespace 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 its 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);templateclass Tconst T GetFieldValue(MonoObject* instance, MonoClassField* field){T value;mono_field_get_value(instance, field, value);return value;}MonoProperty* GetPropertyRef(MonoObject* instance, const char* fieldName);templateclass Tconst 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.hnamespace 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 cant use this image for anything other than loading the assembly because this image doesnt 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);// Dont 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 MyPublicFloatVarreturn 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 MyPublicFloatVarreturn mono_class_get_property_from_name(testingClass, propertyName);}
}然后在代码里随便找个地方调用它来测试一下即可我测过是OK的 Calling C from C#
前面的部分实现了在C调用C#里的任何内容包括调用Method和获取Property和Field等现在需要反过来实现在C#里调用C提供的API。其实有很多可选的做法
使用Platform Invoke (P/Invoke) 这种做法更适合C#工程去使用C的dll时使用我之前工作时就是用Unity去通过这种方式调用寻路导航插件的dll的借助Mono的Internal CallC和C#的中间语言C/CLIEA的寒霜引擎就是用的C#作为编辑器C作为Runtime它们使用C/CLI进行交互不过这玩意儿是只支持Windows的
本章的内容如下
介绍P/Invoke学习如何借助Mono的Internal Call在C#里调用C的代码 关于P/Invoke 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的开销要大 Mono的Internal Call
参考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#调用C的无参静态函数C#调用C的带参静态函数参数为stringC#调用C的带参静态函数参数为C#这边自定义的struct(值类型)C#调用C的带参静态函数参数为C#这边自定义的class(引用类型)C#调用C的重名重载函数
核心就是把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);
}Internal Call的特殊情况
如果想要在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
虽然Content Browser很明显是Editor下用的东西但并不意味着任何存在于Editor下的模块都没有在Runtime下出现的可能毕竟它可能需要在Runtime为了Debug使用Content Browser的主要功能是
提供资源窗口方便把资源直接拖拽到Scene里
显然这个功能是不大可能需要进行Runtime Debug使用的(前面做过的Hierarchy窗口还有一些在Runtime使用可能它可以帮忙看看Runtime下的场景Hierarchy)。所以会把Content Browser相关的代码写到HazelEditor工程下目前的Hierarchy窗口则写在Hazel工程里。
这节课主要内容:
学会使用C的directory_iterator遍历directories(见附录)创建ContentBrowserPanel类也是单独占一个ImGui窗口类似于现有的HierarchyPanel类在其OnImGuiRender函数里绘制相关界面 创建ContentBrowserPanel类
类声明很简单
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_ptrTexture2D m_DirTex;std::shared_ptrTexture2D 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();}
}
效果如下图所示 Content Browser Panel - ImGui Drag Drop
做了以下事情
把单击进入folder改成双击鼠标进入folder属于ImGui的相关API写法通过Push和Pop StyleColor去掉ImGui::ImageButton的默认背景颜色属于ImGui的相关API写法(我用的ImGui::Image绘制的icon没有这个问题)实现Content Browser里的Drag和Drop我可以从里面拖拽Scene文件到Viewport里快速打开该Scene
代码如下
// 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的操作也很简单以后再加上 Textures for Entities!
这节课的目的
给SprietRenderer的Inspector界面添加一个Texture的slot然后绘制一些贴图属性拖拽ContenBrowser里的texture可以给SpriteRenderer赋上贴图
这里使用的是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.HasComponentSpriteRenderer())
{DrawComponentSpriteRenderer(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即可 Everything You Need in a 2D Game Engine (Hazel 2D) - Let’s Talk
很遗憾这个系列未来只会支持2D了不过其实也是合理的Cherno已经教给了我们很多东西了在这个基础上是该自己去学习再去搭建属于自己的3D引擎了
这节课梳理了下知识点值得记下来的有
2D游戏里经常用到Sprite Sheets因为GPU往往只能一次性绑定32个通道的Texture不过这种把多个贴图合为一个大贴图的方法不只是用于Sprite Sheets比如一个点光源、甚至多个点光源、周围六个方向的Shadow Map都可以合并存到一个贴图上。甚至2D游戏里的动画都是Sprite Sheets实现的
目前的Hazel引擎作为2D的游戏引擎还缺少的功能有
Animation系统用Sprite Sheets即可实现毕竟2D游戏的动画不会需要分辨率特别高的贴图Shader和材质系统2D游戏里由于万物都是贴图其实Material和Shader在2D游戏里并不是特别重要但少数情况还是会用到比如给角色周边添加彩色光照的buff效果后处理系统比如添加bloom、color grading等效果实现HDR RenderingScrpting: C#与C交互的脚本系统可视化编程系统Reflection系统这个系统可以帮助在Inspector上直接调整Property的值也可以实现Serialization(暂时不太懂为什么可以这么做)当在编辑器里更改数据的值时(比如从5变为6)C#相关的Assembly不需要重新编译即可改变内存里对应的值。通过反射也可以实现Assembly的加载、卸载和reload毕竟点击Play按钮进入PlayMode时是需要Reload C#的Assembly的2D的物理引擎Callbacks系统2D Particle System会使用到类似VFX graph(a node based editor used to define the flow of particles and how they react to things)的东西Editor相关的工具比如UNDO/REDO系统UI相关比如Text Rendering可能需要使用signed distance field还有algnment、类似css之类的东西。UI Animation等Memory Mapping可以用于帮助上传很大的贴图(比如500mb的贴图)到GPU
还有很多内容就不一一列举了后面慢慢加吧 PLAY BUTTON
先修复了一下上节课的bugShader里的TextureId应该用uniform以flat形式传输用顶点数组数据的形式会被interpolate这节课内容也挺简单的内容不多
Viewport窗口上面绘制了Toolbar一栏里面绘制了Play Button代码的EditorLayer里存两种Scene对应的PlayMode的状态Editor和Play状态Scene里的Update函数分为EditorUpdate和RuntimeUpdate函数(这个Cherno之前做过了我还没做用到的时候再做吧)
核心就这点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_ptrTexture2D 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();
}附录
使用directory_iterator遍历directories
这是C17的std::filesystem提供的方便遍历目录的类如下是例子可以迅速遍历出来所有的子目录路径(返回的是相对的带斜杠的路径而且不含子子文件的路径) 这里的auto类型为
const std::filesystem::directory_entry引入mono库时的报错
报错信息如下
1Hazel.lib(w32socket.obj) : error LNK2019: unresolved external symbol __imp_bind referenced in function mono_w32socket_bind
1Hazel.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过程的依赖项目有
build-external-btlsbuild-external-llvmbuild-initegliblibgcmonosgenlibminilibmonoruntimelibmonoutils
在分析之前需要介绍一些我在分析过程中额外学到的知识 Utility类型的Project
如下图所示这个选项 参考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文件它可以用于
导出一个MAKEFILE在里面自定义build rules使用该项目as a master project for your subprojects.Utility projects respect the list of specified outputs and checks to see if outputs are out of date.
感觉看这个说明很难直接理解我经过自己实践后发现Utility类型的Project可以在build过程中创建新的头文件相关LOG信息如下
Rebuild started...
1------ Rebuild All started: Project: build-init, Configuration: Release x64 ------
1Setting up Mono configuration headers...
1Successfully 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 mscorlib.dll找不到
报错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 doesnt exist or has wrong permissions.if (**splitted !g_file_test (*splitted, G_FILE_TEST_IS_DIR))g_warning (%s in MONO_PATH doesnt exist or has wrong permissions., *splitted);splitted;}
}然后怎么改路径都不对最后发现自己复制粘贴进来的库的文件不对劲(不知道为啥这也太奇怪了) 关于glDrawElements第四个参数的疑问
参考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. 这两种解释都是正确的取决于绘制的方式
如果没有使用VBO那么绘制的时候需要通过glDrawElements的第四个参数传indices数组给GPU来帮助绘制如果使用了VBO那么indices数组应该是借助EBO传给GPU的那么此时的indices就只需要传Specifies a byte offset (cast to a pointer type) into the buffer bound to GL_ELEMENT_ARRAY_BUFFER to start reading indices from 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. 使用cloc查看git仓库的代码量级
参考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 查看cpp文件include的所有头文件
参考2D PHYSICS! // Game Engine series 需要在Visual Studio里选中cpp文件然后右键点击属性在里面的C±Advanced-Show Includes改为Yes然后直接编译该cpp即可(Visual Studio里使用Ctrl F7可以单独编译一个cpp)如下图所示 结果如下图所示看了下这个选项也可以针对整个project进行设置 注意Output这里的Include的信息前面很多空格这是为了方便显示嵌套include情况的比如下面这个
1Note: including file: F:\GitRepositories\Hazel\Hazel\Src\Hazel\ECS/Components/Transform.h
1Note: including file: F:\GitRepositories\Hazel\Hazel\Src\Hazel\ECS/SceneSerializer.h
1Note: including file: F:\GitRepositories\Hazel\Hazel\vendor\yaml-cpp\include\yaml-cpp/yaml.h
1Note: including file: F:\GitRepositories\Hazel\Hazel\vendor\yaml-cpp\include\yaml-cpp/parser.h
1Note: 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位置往上拖拽即可直到找到多一个字母的路径如下图所示