当前位置: 首页 > news >正文

网站开发系统论文域名被墙查询检测

网站开发系统论文,域名被墙查询检测,做网站如何安全 博客,北京海淀公司网站icp备案目录 Hazel 引擎学习笔记学习方法思考引擎结构创建工程程序入口点日志系统Premake\MD没有 cpp 文件的项目会出错include 到某个库就要包含这个库的路径#xff0c;注意头文件展开 事件系统 获取和利用派生类信息预编译头文件抽象窗口类和 GLFWgit submodule addpremake 脚本禁… 目录 Hazel 引擎学习笔记学习方法思考引擎结构创建工程程序入口点日志系统Premake\MD没有 cpp 文件的项目会出错include 到某个库就要包含这个库的路径注意头文件展开 事件系统 获取和利用派生类信息预编译头文件抽象窗口类和 GLFWgit submodule addpremake 脚本禁止一个类被实例化的方法纯虚析构函数protected constructor 它目前实现创建派生窗口类对象的方式make_unique 相比于 unique_ptrT(new T) 窗口事件层级 LayerLayer 的绘制顺序与接受事件的顺序相反LayerStackvector 的 emplace 和 insert 的区别使用for循环时需要提前设置迭代器 添加 Glad 图形库初始化 glad 的顺序 添加 ImGUI 库InGui 添加事件强制 premake 将路径视为 Windows 路径输入轮询 Input Polling键位 Key CodeGLM 数学库ImGui 拖拽和视窗 mGui Docking and Viewports为 ImGui 添加 dllexport 相关的宏定义在 Engine, ImGui, Sandbox 添加一些预编译头的 hack在 Engine 项目的宏定义的头文件中为 ImGui 添加 dllexport dllimport 相关的宏定义在 Engine 项目的宏定义的头文件中为 ImGui 添加 dllimport在 ImGui 项目中添加 dllexport 的宏定义使用 Module Definition File将 Engine 项目改为静态库 为 submodule 添加文件 lib 和 dll 之间的选择dll 导出的类中包含 stl 成员时出现的问题 修复其他一些 warningruntime libraryc17重复的编译内容CRT warning 渲染 Rendering 介绍Design Architecture如何设计 Graphics API Abstraction如何起步 Rendering: Render Context绘制三角形着色器 OpenGL Shaders渲染 API 抽象 Renderer API AbstractionCompile Time Or Runtime Vertex Buffer LayoutsOpenGL 和 DirectX 中的 Buffer Layout关于 initializer_list 顶点数组对象 Vertex Arrays Objectmake_shared 和抽象类连用无法构造对象const 限定符被丢弃 渲染流和提交 Render Flow And SubmissionRenderer Architecture 摄像机 CAMERAS顶点坐标计算时的归属分配问题Camera作为参数传给Renderer的BeginScene函数 正交相机 Orthographic Camera时间戳 TIMESTEPS and DELTA TIME三种 TimestampFixed TimestampVariable TimestampSemi-fixed Timestepspiral of death物理确定性物理模拟插值 变换 Transform材质 Material着色器抽象类 Shader Abstraction and Uniforms引用作用域和智能指针 Refs, Scopes and Smart Pointers为什么 Shader 不设置为 unique_ptr 而是设置为 shared_ptr 材质素材 TEXTURES混合 Blend着色器资源文件 Shader Asset Files着色器库 ShaderLibrary创建 2D 渲染器 How to Build a 2D Renderer渲染架构2D Renderer需要支持的内容关于BatchRenderer和Texture Atlas关于Scripting 相机控制器 Camera ControllersResizing可维护性 MaintenancePreparing for 2D Rendering2D Renderer Transforms and TexturesSingle Shader 2D RendererIntro to ProfilingVisual ProfilingInstrumentationImproving our 2D Rendering APIHow I Made a Game in an Hour Using HazelHazel 2020批渲染 BATCH RENDERINGBatch Rendering Textures使用 flat v_TexIndex 解决 z-fighting Drawing Rotated QuadsIndex Buffer 可以先绑定到 GL_ARRAY_BUFFER 设置缓冲再绑定到 GL_ELEMENT_ARRAY_BUFFER Renderer Stats and Batch ImprovementsTesting Hazels PerformanceLets Make Something in Hazel只能使用 int uniform 作为 array uniform 的 indexC类的成员变量初始化顺序 How Sprite Sheets/Texture Atlases WorkSubTextures - Creating a Sprite Sheet APICreating a Map of Tiles如何表示Tiles组成的地图 Next Steps DockspaceFramebuffersRender Pass Making a New C Project in HazelScene ViewportImGui 无边框使用 glTexImage2D 创建纹理附件方便更新 Code Review ImGui Layer Events虚析构函数在 Compile Time 决定 Input 的实现ImGui 的 IsWindowFocused Where to go next Code ReviewImGui::GetContentRegionAvail() 可能返回负值单独 resize ImGui 窗口时出现的闪烁不要把重要的最通用的宏定义放到头文件里VertexArray在跨平台的图形API里并不存在 Entity Component SystemIntro to EnTT (ECS)在引擎中包含整个 entt 路径的原因 Entities and Components空结构体 The ENTITY ClassAddComponent Camera SystemsScene CameraNative ScriptNative Script with virtual functionScene Hierarchy PanelProperties PanelCamera Component UIDrawing Component UITransform Component UIAdding/Removing Entities and Components UIMaking the Hazelnut Editor Look GoodSaving and Loading ScenesOpen/Save File DialogsTransformation GizmosEditor CameraMultiple Render Targets为鼠标点选准备 FBO清理 FBO 的颜色附件鼠标点选 Mouse PickingClicking to Select EntitiesSPIR-V and the New Shader SystemUniformBuffer SPIR-V and the New Shader SystemSPIR-VSPIR-V对各个平台的shader转换的流程图Vulkan SDKUniform 的写法在 Shader 中使用预编译头XXX 已在 XXX.obj 中重新定义代码中调用 shaderc 编译 glsl 到 spirvspirv 编译到 glsl从编译得到的 spirv 数据中获取 shader 信息在 OpenGL 加载 spirv 二进制数据其他问题 内容浏览器 Content Browser/Asset PanelContent Browser Panel - ImGui Drag Drop显示图标鼠标拖拽 Texture for EntitiesEverything You Need in a 2D Game EnginePLAY BUTTON顶点数组会被插值的 bug绘制顺序问题导致的 bug游戏模式窗口的 dock space 消失 2D PHYSICS显示文件 include 了哪个头文件Box2D Universally Unique Identifiers (UUID/GUID)Playing and Stopping Scenes (and Resetting)Rendering Circles in a Game EngineRendering Lines in a Game EngineCircle Physics CollidersVisualizing Physics CollidersReturn of the Game Engine SeriesPhysics Simulation ModeCommunity Issues/PRs and Merging Branches使用可变参数模板来完成重复性的工作Vulkan1.3GLFW 只调用 GLFW_PRESS RELEASE将从属性面板添加组件的函数替换成函数模板调整 include 的顺序摄像机接受事件的需要播放模式的条件类型的 size 为 0 时 asset在程序主循环中少使用一些函数鼠标选择穿过透明物体定义某一个类的 hash 模板的特化 添加 C#获取 Mono 文件链接到 Mono初始化 Mono创建一个 C# 类库测试程序集加载在 C 端获取 C# 的类实例化 C# 的类调用 C# 类的方法 Calling C from C#P InvokeInternal CallC#调用C的自定义struct为参数的静态函数C#调用C的重名重载函数Internal Call的特殊情况 Calling C from C#C# 到 C 的调用的抽象初始化时获取并存储 C# 中的继承于某一已知命名空间名类名的类的所有派生类的信息ScriptComponent 初始化时根据已知的 命名空间名类名 创建 MonoObjectScriptComponent 实例化 C# 的 Entity 的派生类获得 MonoObject 时创建的是派生类的 MoboObject还要调用 C# 的 Entity 基类的构造函数ScriptComponent 想要有多个 C# 类的实例怎么办 Scene OnUpdate 时获取所有包含 ScriptComponent 的 Entity调用 ScriptEngine::OnUpdateEntityScriptEngine::OnUpdateEntity 又调用 ScriptInstance 里存储的从 C# 获得的 OnUpdateComponent 将游戏 Assembly 和 核心 Assembly 分离C 获取 C# 类的属性将 C# 字段的值复制到运行时 Storing/copying C# field values to runtime序列化 ScriptComponent 的 fieldMapAdded FindEntityByName and Entity.As to retrieve script class instanceC# 程序集 Assembly 重新加载文件监视 fileWatch暂停和步进 Pausing and Stepping添加 C# debugging自定义 Buffer 和 ScopeBuffer游戏工程的抽象类 Project复制时的 bugClone submodule 时的错误error MSB8013: This project doesnt contain the Configuration and Platform combination of Debug|Win32字体AssetManager https://youtu.be/etdSXlVjXss Hazel 引擎学习笔记 学习方法思考 我感觉自己照抄视频中的脚本还是有点慢了 因为你不知道他什么时候加了什么东西或者自己照抄就很容易抄错 我觉得最好的方法就是自己快速过一遍他的视频知道他大概的思路是怎么样的然后自己再拉取那个 commit 的代码用 diff 方便看他具体修改了什么代码然后自己看看自己总结一下就差不多了 然后我还大部分参考了别的大佬的笔记https://blog.csdn.net/alexhu2010q/category_10165311.html 引擎结构 Entry point 启动点 Application layer Windows layer Input Event Renderer Render API abstract Debugging support Scripting language Memory system ECS Physics File IO, virtual file system(VFS) Build system build custom format of data offline 创建工程 创建 github 仓库 创建 Visual Studio 空项目 在 sln 文件的目录下 git clone 仓库 现在我们的解决方案中只有 Engine 一个项目 我们要做的是把引擎编译成一个库静态或者动态的然后应用程序链接这个库 在项目的属性页-配置管理器-活动解决方案平台-编辑 中删去 x86不去支持 32 位平台 配置管理器-项目上下文-平台-编辑 中删去 Win32 在 Engine 项目的属性页 配置 改为所有配置 在 Engine 项目的属性页-配置属性-常规-常规属性-配置类型 改为 dll 现在这个项目会构建成 dll 而不是 exe 在 Engine 项目的属性页-配置属性-常规-常规属性-输出目录 改为 $(SolutionDir)bin\$(Configuration)-$(Platform)\$(ProjectName)\ 中间目录改为 $(SolutionDir)bin-int\$(Configuration)-$(Platform)\$(ProjectName)\ 这就表示 bin-int 是一个可以随时删除的文件夹 在解决方案资源管理器中右键解决方案新建空项目命名为 Sandbox 对这个项目也是像之前一样删掉 32 位设置输出目录和中间目录但是编译成 exe 就不用改了 在解决方案资源管理器中对 Sandbox 项目右键设置为启动项目 退出这个项目用 vs code 打开 sln 文件将 sandbox 项目这行移动到 engine 项目的上面 这是为了方便第一次看这个项目的人他们习惯认为第一个项目是启动项目 在解决方案资源管理器中对 Sandbox 项目右键添加引用勾选 Engine 项目确认 这就链接到了 Engine 项目产生的 dll 我打开 Sandbox 项目的属性页 linker - command line 的时候没有看到 Hazel.lib这是为什么我确定我已经设置 Hazel 项目编译为 dll设置了 Sandbox 引用 Hazel 项目。 不管了继续往下看吧 生成 dll 的时候为什么会同时生成 lib 和 dll https://stackoverflow.com/questions/38602618/why-some-programs-require-both-lib-and-dll-to-work 这个回答说 lib 分为两种一种是静态库一种是导入库导入库是在 dll 编译时生成的只是包含 dll 所需要的符号 在两个项目下创建 src 文件夹 在 Engine 项目下面新建一个测试用的 Test.h #pragma oncenamespace MeowEngine {__declspec(dllexport) void Print();}Test.cpp #include Test.h#include stdio.hnamespace MeowEngine {void Print() {printf(Welcome to Meow Engine!);} }__declspec(dllexport) 的作用就是让编译器按照某种预定的方式(前面大致解释了这种方式的规则)来输出导出函数及变量的符号 然后在 Sandbox 里面创建 Application.cpp namespace MeowEngine {__declspec(dllimport) void Print();}void main() {MeowEngine::Print(); }这个时候构建 Sandbox 会运行报错因为找不到 dll 把 Engine 构建生成的 dll 复制粘贴到 Sandbox 的 exe 目录中就可以正常运行了 程序入口点 在 Engine 的 src 文件夹中新建一个 Engine 文件夹 创建 Application 类 #pragma oncenamespace MeowEngine {class __declspec(dllexport) Application{public:Application();virtual ~Application();void Run();}; }#include Application.hnamespace MeowEngine {Application::Application() {}Application::~Application() {}void Application::Run() {while (true) {}} }创建 Core.h #pragma once#ifdef ME_PLATFORM_WINDOWS#ifdef ME_BUILD_DLL#define ME_API __declspec(dllexport)#else#define ME_API __declspec(dllimport)#endif // ME_BUILD_DLL #else#error Meow Engine only support Windows! #endifME_PLATFORM_WINDOWS 表示对 Windows 构建 通过 ME_BUILD_DLL 我们实现了使用一个 ME_API 就能处理 dll 的导入导出 之前定义的 Application 类中 declspec 也可以替换成 ME_API 了 打开 Engine 属性页-配置属性-C/C±预处理器-预处理器定义添加 ME_PLATFORM_WINDOWS 用分号分隔 对 Sandbox 同样如此 对 Engine 再添加一个预处理器 ME_BUILD_DLL 表示这是用来构建 dll 的项目 把 Sandbox 的 Application.cpp 改名为 SandboxApp.cpp 与 Engine 中的 Application.cpp 区分开 然后如果要在 SandboxApp.cpp 中引用 Engine 中的文件的话就会需要比较复杂的引用 例如 #include ../../MeowEngine/src/MeowEngine/Core.h如果 Sandbox 中的某一个文件需要去引用很多 Engine 中的头文件就需要写很多这样的 include 代码比较难看 所以我们可以用新建一个头文件作为中介包括掉 Engine 中的一些头文件然后在 Sandbox 中只去包括那单独一个中介头文件 在 Engine 项目的 src 文件夹中创建一个头文件作为中介 MeowEngine.h #pragma once#include MeowEngine\Application.h在 Sandbox 的属性页-C/C±常规-附加包含目录 中添加 $(SolutionDir)MeowEngine\src; 也就是添加了 Engine 中的一个路径 这样我们在 Sandbox 项目中某一个文件要引用那个中介头文件的时候就可以不用写一长串路径了 例如 SandboxApp.cpp #include MeowEngine.h前面我们 Engine 项目中定义了 Application 类现在我们在 Sandbox 中继承它 #include MeowEngine.hclass Sandbox : public MeowEngine::Application { public:Sandbox() {}~Sandbox() {}};void main() {Sandbox* sandbox new Sandbox();sandbox-Run();delete sandbox; }我在想为什么要这么搞我觉得是因为 Application 是入口点所以不管是提供工具的 Engine 还是 Sandbox 都需要通过这个联系 还是得看之后写了什么吧 之后它把 main 函数的位置改了 他在 Engine 项目的 src/Engine 下面创建了一个 EntryPoint.h 把 main 放到了这里 #pragma once#ifdef ME_PLATFORM_WINDOWSextern MeowEngine::Application* MeowEngine::CreateApplication();void main(int args, char** argv) {auto app MeowEngine::CreateApplication();app-Run();delete app; }#endif // ME_PLATFORM_WINDOWS在 Application.h 中添加声明 CreateApplication #pragma once#include Core.hnamespace MeowEngine {class ME_API Application{public:Application();virtual ~Application();void Run();};// To be define in CLIENTApplication* CreateApplication(); }在 MeowEngine.h 中添加包含头文件 #pragma once// For use by CheapMeow applications#include MeowEngine\Application.h// ---Entry Point------------------------- #include MeowEngine\EntryPoint.h // --------------------------------------- 这样SandboxApp.cpp 就包含了 EntryPoint.h 原来在 SandboxApp.cpp 中的 main 就不需要了改成了对 CreateApplication 的实现 #include MeowEngine.hclass Sandbox : public MeowEngine::Application { public:Sandbox() {}~Sandbox() {}};MeowEngine::Application* MeowEngine::CreateApplication() {return new Sandbox(); }这样就是实现了在 Engine 里面声明 Application在 Engine 里面创建、调用 Application在 Sandbox 里面具体实现 我看别人说这样的好处是客户端只负责 new对象释放交给引擎处理 emmm毕竟我经验少一时半会也说不出这里怎么好了 日志系统 需要打印不同警告等级还需要格式化打印不同对象比如数数组对象 这里就是新建了一个 Log 类Log 类创建了两个 spdlog 静态对象 一般的使用方法例如获取静态对象-warn() 再创建一些宏把这些变成缩写 Log.h #include memory #include spdlog\spdlog.hnamespace MeowEngine {class Log{public:static void Init();static std::shared_ptrspdlog::logger GetCoreLogger() { return s_CoreLogger; }static std::shared_ptrspdlog::logger GetClientLogger() { return s_ClientLogger; }private:static std::shared_ptrspdlog::logger s_CoreLogger;static std::shared_ptrspdlog::logger s_ClientLogger;};}// Core log macros #define ME_CORE_TRACE(...) ::MeowEngine::Log::GetCoreLogger()-trace(__VA_ARGS__) #define ME_CORE_INFO(...) ::MeowEngine::Log::GetCoreLogger()-info(__VA_ARGS__) #define ME_CORE_WARN(...) ::MeowEngine::Log::GetCoreLogger()-warn(__VA_ARGS__) #define ME_CORE_ERROR(...) ::MeowEngine::Log::GetCoreLogger()-error(__VA_ARGS__) #define ME_CORE_CRITICAL(...) ::MeowEngine::Log::GetCoreLogger()-critical(__VA_ARGS__)// Client log macros #define ME_TRACE(...) ::MeowEngine::Log::GetClientLogger()-trace(__VA_ARGS__) #define ME_INFO(...) ::MeowEngine::Log::GetClientLogger()-info(__VA_ARGS__) #define ME_WARN(...) ::MeowEngine::Log::GetClientLogger()-warn(__VA_ARGS__) #define ME_ERROR(...) ::MeowEngine::Log::GetClientLogger()-error(__VA_ARGS__) #define ME_CRITICAL(...) ::MeowEngine::Log::GetClientLogger()-critical(__VA_ARGS__)Log.cpp #include Log.h#include spdlog/sinks/stdout_color_sinks.hnamespace MeowEngine {std::shared_ptrspdlog::logger Log::s_CoreLogger;std::shared_ptrspdlog::logger Log::s_ClientLogger;void Log::Init() {spdlog::set_pattern(%^[%T] %n: %v%$);s_CoreLogger spdlog::stdout_color_mt(MEOW ENGINE);s_CoreLogger-set_level(spdlog::level::trace);s_ClientLogger spdlog::stdout_color_mt(APP);s_ClientLogger-set_level(spdlog::level::trace);} }#pragma once#ifdef ME_PLATFORM_WINDOWSextern MeowEngine::Application* MeowEngine::CreateApplication();void main(int args, char** argv) {MeowEngine::Log::Init();MeowEngine::Log::GetCoreLogger()-warn(Welcome!);auto app MeowEngine::CreateApplication();app-Run();delete app; }#endif // ME_PLATFORM_WINDOWS 这里是声明写在头文件定义写在源文件 声明declare做的事情是告诉编译器有这么个 符号函数/变量 存在让编译器允许后面的代码使用这个符号。 定义define 做的事情是为这个符号函数/变量 分配内存空间/获取其地址让编译器知道去哪里找这个符号。 如果这里 cpp 不写 s_CoreLogger s_ClientLogger 的定义那么编译不会报错在 Visual Studio 点击变量名跳转也能跳转但是链接的时候会报错找不到符号 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK1120 1 个无法解析的外部命令 MeowEngine E:\MeowEngine\bin\Debug-x64\MeowEngine\MeowEngine.dll 1 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK2001 无法解析的外部符号 private: static class std::shared_ptrclass spdlog::logger MeowEngine::Log::s_CoreLogger (?s_CoreLoggerLogMeowEngine0V?$shared_ptrVloggerspdlogstdA) MeowEngine E:\MeowEngine\MeowEngine\Log.obj 1 Premake workspace 相当于一个解决方案 configurations 相当于解决方案的构建方式就是 DebugRelease 那种跟 VS 一样只是自己定义的一个任意名字的 enum用来区别自己选择的不同构建设定而已 platforms 的作用相当于另一种 configurations 只是方便之后区别不同情况 project 相当于定义了一个项目也就是解决方案中的一个项目比如 Engine 和 Sandbox define 是定义一个 preprocessor 预编译头 就是之前 ME_PLATFORM_WINDOWS 之类 kind 表示构建类型 language 表示构建语言 targetdir 表示构建位置 objdir 设置构建项目时应放置对象和其他中间文件的目录。 files 表示源文件包括 .h 和 .cpp include 表示附加的头文件目录 filter 对 configurations platforms 进行筛选在筛选之后的构建方式之下进行配置 还可以对 system 筛选这个 system 可以在工作区中定义不定义的话就自动识别 tokens 包括值令牌和命令令牌值令牌是 premake 提供的一些内建变量可以访问我们之前定义的比如 configurations platforms 之类的信息这样我们就可以方便使用这些信息来输出 具体有哪些内建变量可以看 wiki staticruntime On 的效果是 Sets to “MultiThreaded”这个我不知道是什么意思 之后的 https://blog.csdn.net/alexhu2010q/article/details/106942099 讲的还挺清楚的 之后还有一点是要在 Sandbox 中也包含 spdlog 的 include 路径 就很奇怪为什么呢因为 Sandbox 中包含了 MeowEngine.h 这个 MeowEngine,h 是我们之前用来连接到引擎的各个头文件的中介其中包含了引擎中的一些头文件其中包含了引擎的 Log 类的头文件Log 类包含了 spdlog那就相当于 Sandbox 中还是用到了 spdlog所以还要配置 然后这里如果代码没有报错但是链接始终不成功除了静态类成员需要定义这个坑还有一个可能性是有些需要导出导入的 dll 中的类没有附加 import export workspace MeowEnginearchitecture x64startproject Sandboxconfigurations{Debug,Release,Dist}outputdir %{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}project MeowEnginelocation MeowEnginekind SharedLiblanguage Ctargetdir (bin/ .. outputdir .. /%{prj.name})objdir (bin-int/ .. outputdir .. /%{prj.name})files{%{prj.name}/src/**.h,%{prj.name}/src/**.cpp}includedirs{%{prj.name}/vendor/spdlog/include}filter system:windowscppdialect C17staticruntime Onsystemversion latestdefines{ME_PLATFORM_WINDOWS,ME_BUILD_DLL}postbuildcommands{({COPY} %{cfg.buildtarget.relpath} ../bin/ .. outputdir .. /Sandbox)}filter configurations:Debugdefines ME_DEBUGsymbols Onfilter configurations:Releasedefines ME_RELEASEsymbols Onfilter configurations:Distdefines ME_DISTsymbols Onproject Sandboxlocation Sandboxkind ConsoleApplanguage Ctargetdir (bin/ .. outputdir .. /%{prj.name})objdir (bin-int/ .. outputdir .. /%{prj.name})files{%{prj.name}/src/**.h,%{prj.name}/src/**.cpp}includedirs{MeowEngine/vendor/spdlog/include,MeowEngine/src}links{MeowEngine}filter system:windowscppdialect C17staticruntime Onsystemversion latestdefines{ME_PLATFORM_WINDOWS}filter configurations:Debugdefines ME_DEBUGsymbols Onfilter configurations:Releasedefines ME_RELEASEsymbols Onfilter configurations:Distdefines ME_DISTsymbols On可以创建一个 bat 自动完成构建 call .\vendor\bin\premake\premake5.exe vs2019 pause\MD https://blog.csdn.net/alexhu2010q/article/details/107688039 如果dll和exe分别拥有自己的Heap可能导致同一块内存在堆A上创建又在堆B上释放 As this is a DLL, the problem might lie in different heaps used for allocation and deallocation (try to build the library statically and check if that will work).The problem is, that DLLs and templates do not agree together very well. In general, depending on the linkage of the MSVC runtime, it might be problem if the memory is allocated in the executable and deallocated in the DLL and vice versa (because they might have different heaps). And that can happen with templates very easily, for example: you push_back() to the vector inside the removeWhiteSpaces() in the DLL, so the vector memory is allocated inside the DLL. Then you use the output vector in the executable and once it gets out of scope, it is deallocated, but inside the executable whose heap doesn’t know anything about the heap it has been allocated from. Bang, you’re dead. 为了解决这个问题需要保证dll和exe享用同一块Heap。 那么需要设置 VS 中的 Runtime Library 选项 /MT Multi-threaded /MTd Multi-threaded Debug /MD Multi-threaded DLL /MDd Multi-threaded Debug DLL用带 DLL 的选项就可以了 没有 cpp 文件的项目会出错 如果一个 vs 项目没有 cpp 文件只有 h 文件那么它的属性页会缺失 C/C 这一项premake 中设置的 includedirs 也不会出现在这个项目中就会导致一些错误 我做这样一个项目一开始是为了测试某个头文件没想到有这样的错误 https://stackoverflow.com/questions/2309091/can-not-find-c-c-in-project-properties 我猜这是因为没有 cpp 文件的话就没有真正编译一个模块出来所以 vs 项目中才没有 C/C 这一项——因为这不算是 C/C 要编译的 include 到某个库就要包含这个库的路径注意头文件展开 最终我遇到了类似这样的问题 https://stackoverflow.com/questions/17715725/visual-studio-unable-to-find-header-file-during-compile-despite-include-director 我的问题跟他的差不多但是具体有所不同 我是在 A 项目中的附加包含目录中添加了某个库假设称为 foo.h的路径然后我在 A 项目的 a.h 中写了 // a.h#include foo.h // for example然后我在 B 项目中也添加了 a.h 的路径然后 include 了 a.h 然后 a.h 中就出现了报错说找不到 foo.h 这个时候我一直在想我在 A 项目中确实定义了 foo.h 的路径了啊怎么还会找不到呢然后就一直找解决方法没找到 之后我在 B 项目中把对 a.h 的 include 删掉问题就消失了这个时候我才反应过来是怎么回事 实际上我没有在 B 项目中定义 foo.h 的路径但是我又在 B 项目中变相地 include “foo.h”那么在 B 中去找 “foo.h” 就找不到了 所以解决方法就是用到这个库的项目都要加上这个库的路径 事件系统 获取和利用派生类信息 之后的 https://blog.csdn.net/alexhu2010q/article/details/106942099 讲的还挺清楚的 首先 premake 中 Engine 的 include 路径中添加一个 src方便引用 比如 Engine 这个文件夹放 Engine 的 vcxproj 然后底下有一个 src 文件那么我们的 src 文件夹中的文件包含东西的时候不希望在路径中还要写到 src project MeowEnginelocation MeowEnginekind SharedLiblanguage Ctargetdir (bin/ .. outputdir .. /%{prj.name})objdir (bin-int/ .. outputdir .. /%{prj.name})files{%{prj.name}/src/**.h,%{prj.name}/src/**.cpp}includedirs{%{prj.name}/vendor/spdlog/include,%{prj.name}/src}这个事件系统就是多态的一个很好的例子事件是多态的但是我却需要有一个东西能够处理所有类型的事件他就演示了怎么获得派生类的信息怎么利用获得的派生类的信息这些是一个简单的多态继承 virtual 函数所不会展示的 class EventDispatcher{templatetypename Tusing EventFn std::functionbool(T);public:// 传入事件的基类// 外部传入一个 EventFn 的同时还需要指定事件的派生类类型// 传入的派生类类型是传到模板的 T 中的// 也就是我们实现多态的方式是用基类指针存储那些会派生的数据// 然后要求外部调用的时候通过类模板传入类型 T// 拿到类型 T我们就可以获得派生类的相关信息Dispatcher 就是需要处理这个派生类的信息才能完成自己的工作// 例如验证自己存储的事件的派生类事件类型与传入的派生类事件类型是否相同用 enum 来判断// 自己存储的事件虽然是用基类指针来指向的但是它用虚函数来实现获得派生类事件类型所以即使是基类指针也能获取到派生类信息// 拿到类型 T还可以用这个 T 来将自己存储的基类指针指向的事件转换到派生类// 这样我们就可以调用传入的回调函数 EventFnEventDispatcher(Event event): m_Event(event){}templatetypename Tbool Dispatch(EventFnT func){if (m_Event.GetEventType() T::GetStaticType()){m_Event.m_Handled func(*(T*)m_Event);return true;}return false;}private:Event m_Event;};这里完成的是Event 自己有一个虚函数提供给子类继承用来返回函数的类型 这里返回的类型实际上是一个 enum 变量 而每一个函数对这个虚函数的继承都需要根据自己的情况来重写一遍 例如我新写了一个事件 A那么我先到 enum 定义中添加 A然后我再写继承函数会返回 A 这个 enum 那么我们用一个宏定义来实现继承虚函数返回自己的类型 enum 的这一步 dispatcher 是接受一个事件存下来然后应用阶段别人传入一个函数dispatcher 用这个传入的函数来处理自己保存好的事件 dispatcher 就根据泛型 T 来得到 enum通过和已经保存的事件的 enum 来比对enum 相同才能允许调用传入的函数 什么情况下只知道类型不知道实例就能调用这个类的函数静态函数所以 GetType GetName 这种都是静态函数 这样我们也就看到了静态函数的价值除了不需要对象的普通成员之外我们还可以用它来传递某个类型的信息 预编译头文件 我也遇到了一个问题就是在将是否使用预编译头从 否 改为 使用 的时候第一次生成 Engine 项目的时候会产生警告说有些 cpp 是不是没有包括预编译头文件哪怕是那些不需要使用到预编译头文件中的库的源文件 https://blog.csdn.net/alexhu2010q/article/details/107132670 但是我什么都不动再重新生成 Engine 项目就不会有这个警告了 真的是只有在切换之后的第一次生成才会出现的问题我猜与旧构建有关所以我在不使用预编译头的时候生成了 Engine 项目之后清理了项目然后切换到使用预编译头然后再生成这次就不管怎么样都生成不了了一直存在这个是否是少了 pch 的错误 所以确实是所有源文件都要加 pch 抽象窗口类和 GLFW git submodule add git submodule add 出错时怎么清理这个命令的痕迹 https://stackoverflow.com/questions/11887203/you-are-on-a-branch-yet-to-be-born-when-adding-git-submodule 如果是连接断开可能要设置代理看自己的 clash 的端口号 然后这个命令可以设置 git 的全局代理其中 7890 是我的 clash 的端口号 git config --global http.proxy http://127.0.0.1:7890premake 脚本 我一开始用的 油管主的 GLFW用它自带的 premake 脚本结果得到了链接错误 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK2019 无法解析的外部符号 __imp_realloc函数 defaultReallocate 中引用了该符号 MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(init.obj) 1 错误 LNK2019 无法解析的外部符号 __imp_strncpy函数 glfwWindowHintString 中引用了该符号 MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(window.obj) 1 错误 LNK2001 无法解析的外部符号 __imp_strncpy MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(input.obj) 1 错误 LNK2001 无法解析的外部符号 __imp_strncpy MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(monitor.obj) 1 错误 LNK2001 无法解析的外部符号 __imp_strncpy MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(win32_joystick.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(osmesa_context.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(monitor.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(vulkan.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(wgl_context.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(egl_context.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(window.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(context.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(input.obj) 1 错误 LNK2001 无法解析的外部符号 __imp__wassert MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(win32_thread.obj) 1 错误 LNK2019 无法解析的外部符号 __imp___stdio_common_vsscanf函数 _vsscanf_l 中引用了该符号 MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(context.obj) 1 错误 LNK2019 无法解析的外部符号 __imp_strspn函数 glfwUpdateGamepadMappings 中引用了该符号 MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(input.obj) 1 错误 LNK2019 无法解析的外部符号 __imp_wcscpy函数 createMonitor 中引用了该符号 MeowEngine E:\MeowEngine\MeowEngine\GLFW.lib(win32_monitor.obj) 1 错误 LNK1120 6 个无法解析的外部命令 MeowEngine E:\MeowEngine\bin\Debug-windows-x86_64\MeowEngine\MeowEngine.dll 1 在保证我代码没抄错之后我去比对了源代码发现它的 GLFW 的 premake 脚本不一样它使用了 /MT 构建 但是如果我全抄它的脚本确实/MT 了但是有新的链接错误 于是我就在我的 GLFW 自带的 premake 的脚本中稍微改了一下改成了 /MT然后就好了 project GLFWkind StaticLiblanguage Ctargetdir (bin/ .. outputdir .. /%{prj.name})objdir (bin-int/ .. outputdir .. /%{prj.name})files{include/GLFW/glfw3.h,include/GLFW/glfw3native.h,src/glfw_config.h,src/context.c,src/init.c,src/input.c,src/monitor.c,src/null_init.c,src/null_joystick.c,src/null_monitor.c,src/null_window.c,src/platform.c,src/vulkan.c,src/window.c,}filter system:linuxpic Onsystemversion latestfiles{src/x11_init.c,src/x11_monitor.c,src/x11_window.c,src/xkb_unicode.c,src/posix_module.c,src/posix_time.c,src/posix_thread.c,src/posix_module.c,src/glx_context.c,src/egl_context.c,src/osmesa_context.c,src/linux_joystick.c}defines{_GLFW_X11}filter system:macosxpic Onfiles{src/cocoa_init.m,src/cocoa_monitor.m,src/cocoa_window.m,src/cocoa_joystick.m,src/cocoa_time.c,src/nsgl_context.m,src/posix_thread.c,src/posix_module.c,src/osmesa_context.c,src/egl_context.c}defines{_GLFW_COCOA}filter system:windowssystemversion latestbuildoptions /MTfiles{src/win32_init.c,src/win32_joystick.c,src/win32_module.c,src/win32_monitor.c,src/win32_time.c,src/win32_thread.c,src/win32_window.c,src/wgl_context.c,src/egl_context.c,src/osmesa_context.c}defines { _GLFW_WIN32,_CRT_SECURE_NO_WARNINGS}filter configurations:Debugruntime Debugsymbols onfilter configurations:Releaseruntime Releaseoptimize speedfilter configurations:Distruntime Releaseoptimize speedsymbols off 如果还有问题那就可能是代码抄错了 例如我就抄错了导致 glfw 的初始化函数没运行结果之后 glfw 报错没有 tls我还在想是怎么回事hhhh 禁止一个类被实例化的方法 纯虚析构函数 https://blog.csdn.net/alexhu2010q/article/details/107132670 多态使用的时候如果子类中有属性开辟到堆区如果使用父类类型的指针来指向子类对象那么使用这个父类类型的指针来释放对象时只会调用父类的析构函数进行了函数地址早绑定 解决方法:将父类中的析构函数改为虚析构或者纯虚析构进行了函数地址晚绑定 虚析构和纯虚析构共性: 可以解决父类指针释放子类对象 都需要有具体的含函数实现 虚析构和纯虚析构的区别 如果是纯虚析构该类属于抽象类无法实例化对象纯虚析构的作用 一般纯虚函数和纯虚析构的区别 一般函数的纯虚函数不需要实现但是纯虚析构函数一定要在类外提供函数的实现。 因为某一个类一定要有能够被调用的析构函数…… protected constructor 对于抽象基类如果把其 constructor 设置为 protected那么该基类虽然不能被用户直接进行实例化但声明为 protected 能让该基类的子类被实例化 class Base1 { public:virtual ~Base1() {} protected:Base1() {} };class Base2 { public:virtual ~Base2() {}Base2() {} };int main() {Base1 *base1 new Base1(); //编译错误Base1的构造函数是protected不可以访问Base2 *base2 new Base2(); //编译成功 } 所以声明 protected constructor for base class就是保证基类的构造函数只能在其派生类中调用这种基类一般是 abstract 类但又不是接口类 它目前实现创建派生窗口类对象的方式 它目前实现创建派生窗口类对象的方式还是比较神奇的 因为 Window 现在包含纯虚函数是一个抽象类所以不能直接 new 同理也不能直接 make_unique 所以需要一个函数来创建一个 Window 类的派生类的对象 然后他又希望跨平台嘛所以才把窗口类做成多态的 但是他现在返回 Window 类的派生类的对象的方法是在 WIndow 类里面声明一个 static 函数 static Window* Create(const WindowProps props WindowProps());然后在类外定义死了这个类就是返回某个特定平台的 Window 派生类 Window* Window::Create(const WindowProps props){return new WindowsWindow(props);}这样的话就相当于这个 Create 只能派生一次了 make_unique 相比于 unique_ptr(new T) 虽然原来的 在 C 11 的时候作为函数参数的各个函数调用之间的调用顺序是不确定的 foo(unique_ptrT(new T), otherFunction()); // first case foo(make_uniqueT(), otherFunction()); // second case对于第一种写法各个函数调用的顺序可能是 // case 1 new T unique_ptrT(...) otherFunction()// case 2 new T otherFunction() unique_ptrT(...)// case 3 otherFunction() new T unique_ptrT(...)之中的一种 乍一看看不出什么但是当这个 otherFunction() 是一个可能会抛出异常的函数时。对于 case 2当 otherFunction() 抛出异常时foo(...) 函数会提前终止那么这里 new T 的地址无法返回后面的程序也就无法拿到这个地址来释放这个内存那么这样就会有一块内存无法释放 https://stackoverflow.com/questions/19472550/exception-safety-and-make-unique/19472607#19472607 C17 解决了这个顺序的问题现在每一个作为参数的函数调用都一定会完整的执行完才轮到下一个作为参数的函数调用 此外还有解决了一些其他情况下的调用问题例如 情况下 https://www.cppstories.com/2021/evaluation-order-cpp17/#does-it-mean-all-errors-are-fixed 但是这并不是说 foo(unique_ptrT(new T), otherFunction()); 这种形式一定不会产生内存泄漏对于这个情况C17 只是使得不会因为函数调用顺序不确定某函数抛出异常使得其他地方的 new 没有返回地址但这不是说这个形式本身没有其他内存泄露的可能性 例如 foo(unique_ptrT(new T), new int {10});这时如果 foo 函数内部或者 new T 会抛出异常导致 foo 函数提前终止原本应该在 foo 中释放的 new int 现在释放不了了那么还是会导致内存泄漏 但是这种问题的根本是在于直接就把一个 new 出来的匿名对象传到了函数中 而 cpp 的一个习惯是尽可能少地在程序级别上进行new和delete调用——最好是没有。任何需要动态内存的东西都应该隐藏在一个 RAII 对象中当它超出范围时释放内存。RAII 在构造函数中分配内存并在析构函数中释放内存这样当变量离开当前范围时内存就可以被释放。 注RAII 资源获取即初始化也就是说在构造函数中申请分配资源在析构函数中释放资源 窗口事件 他这个触发事件的流程分为若干个步骤 每一个引擎窗口有自定义的窗口数据结构体 每一个窗口派生类的对象有一个 glfw 窗口对象 GLFWwindow 成员 m_Window 与自定义的窗口数据结构体 WindowData 成员 m_Data 这使得整个应用可以有多个窗口每一个窗口有自己的状态 窗口绑定事件 m_Data 具有一个 std::functionvoid(Event) 成员 窗口派生类的对象在初始化时可以创建一个自定义的事件处理函数 OnEvent将这个自定义的事件处理函数用 std::bind 处理得到我们规定好的事件回调函数的格式 std::functionvoid(Event)绑定到 m_Data 中 也就是在初始化的时候给 m_Data 中的事件回调函数赋值 而事件回调函数具体的内容是接受一个事件根据这个事件初始化构造一个 EventDispatcher然后对 EventDispatcher 调用各种事件处理函数 我感觉这里 EventDispatcher 起到的就是一个 if 的作用因为其实 EventDispatcher 内部就是通过比对事件类型来判断是否执行传入的函数的 void Application::OnEvent(Event e) // 这里用 Event 的基类来接受各种事件的派生类的对象 {EventDispatcher dispatcher(e);// 以下列出各种事件的处理函数// 如果 e 是 WindowCloseEvent那么这里传入的函数会被调用dispatcher.DispatchWindowCloseEvent(BIND_EVENT_FN(OnWindowClose)); // 再列出其他情况的例 // 如果 e 是 WindowXXXEvent那么这里传入的函数会被调用// dispatcher.DispatchWindowXXXEvent(BIND_EVENT_FN(OnWindowXXX));HZ_CORE_TRACE({0}, e); // 对任意类型的事件打印事件信息 }GLFW 事件传递的起点 在每一个窗口派生类的对象的初始化函数中使用 glfwSetWindowUserPointer 将本对象包含的 glfw 窗口对象 GLFWwindow 成员 m_glfw_Window虽然源代码不是叫这个名字源代码写的也是 m_Window在这里写出来的话我感觉会混淆 与 m_Data 绑定起来 然后就是事件传递的起点glfw 提供了各种 glfw 事件的回调函数。在每一个窗口派生类的对象的初始化函数中都将 glfw 事件的回调函数绑定为一个匿名函数。在游戏运行时glfw 事件发生时调用这个绑定的匿名函数匿名函数的内容是通过 glfwGetWindowUserPointer 获取 m_Data然后创建引擎事件将这个引擎事件传入 m_Data调用 m_Data 的事件回调函数 这样就完成了通过 glfwSetWindowUserPointer 和 glfwGetWindowUserPointer将 glfw 窗口与自定义的窗口数据结构体一一绑定。而这样也就支持了多个 glfw 窗口能够有自己的事件绑定函数 glfwSetWindowUserPointer(m_Window, m_Data);// Set GLFW callbacks glfwSetWindowSizeCallback(m_Window, [](GLFWwindow* window, int width, int height) {WindowData data *(WindowData*)glfwGetWindowUserPointer(window);// custom logicWindowResizeEvent event(width, height);data.EventCallback(event); });层级 Layer 设计完Window和Event之后需要创建Layer类。Layer这个概念比较抽象具体在游戏里比如游戏画面可能是离摄像机最远的Layer然后依次可能会有UI Layer和Debug Layer。 游戏里的Layer应该具备最基本的两个功能可以在该Layer上渲染一些东西和接受外部的Event所以Layer类需要有以下内容 OnUpdate用于处理渲染的loop OnEvent用于处理事件 Init函数负责Layer的初始化 Exit函数 负责Layer的结束操作Layer 的绘制顺序与接受事件的顺序相反 在游戏里经常会有多个Layer当多个Layer存在时往往需要对上面一层的Layer(离摄像机最近的)进行处理比如我们在点击UI时并不想让角色随之进行动作所以这里设计了一个LayerStack用于按照到摄像机距离从远到近的存放Layer值得注意的是处理渲染时应该先画最远的Layer再画最近的Layer而处理事件时正好相反因为最上面一层的Layer才应该是接受event的对象二者的顺序正好是相反的。 LayerStack 实际实现 Layer 的功能是在 Application 中设置一个 LayerStack 它是在 Application 的 OnEvent 中调用各个层级的 OnEvent在 Application 的 OnUpdate 中调用各个层级的 OnUpdate。而 Application 的 OnEvent 是绑定到 Application 的 m_Winodw 中的 m_Data 这样其实就说明这个层级是对于某一个窗口而言的是一个窗口的内部有各种层级 这样也让我有点在意他通过 glfw 将 glfw 窗口和自定义的窗口数据结构体一一对应地绑定就是为了实现多窗口现在它在一个 Application 里把 Application 窗口的各个层级绑定到 Application 的窗口那就意味着 Application 只能有一个窗口了毕竟因为如果有多个窗口的话……好像也不是不行 但是我总觉得这个层级的事情应该是写在窗口里面的而不应该写在 Application 里…… vector 的 emplace 和 insert 的区别 insert函数和emplace函数的区别在于 emplace() 在插入元素时是在容器的指定位置直接构造元素而insert函数是先单独生成再将其复制或移动到容器中。因此在实际使用中推荐大家优先使用 emplace()。 使用for循环时需要提前设置迭代器 对于自定义类型的 vector使用for循环时需要提前设置迭代器 LayerStack::~LayerStack(){for (Layer* layer : m_Layers)delete layer;}所以在头文件时还需要声明头尾迭代器 std::vectorLayer*::iterator begin() { return m_Layers.begin(); }std::vectorLayer*::iterator end() { return m_Layers.end(); }private:std::vectorLayer* m_Layers;unsigned int m_LayerInsertIndex 0;添加 Glad 图形库 可以使用glew也可以使用glad库二者的在效率上好像没啥区别不过glad的库要更新一些所以这里用glad库具体步骤有 上网站https://glad.dav1d.de/上下载对应版本的header和src文件放在vendor文件夹下 网站上下载的glad库没有premake5文件所以按照glfw库的方式为其写一个与glfw库相同这里的glad库也是作为lib文件使用 把glad库的premake5文件相关内容整合到整个工程的premake5文件里 初始化 glad 的顺序 glfwMakeContextCurrent 之后再 gladLoadGLLoader 添加 ImGUI 库 由于我们用的是 glfw 库加上 OpenGL3 的版本所以要参考的两个 cpp 文件为imgui_impl_opengl3.cpp 和 imgui_impl_glfw.cpp 在Platform文件夹下创建OpenGL文件夹 把 imgui_impl_opengl3 的头文件和源文件放进去更名为 ImGuiOpenGLRenderer用来存放 ImGui调用 OpenGL 的代码。 而原本用到的 imgui_impl_glfw 相关内容就直接 Copy 和 Paste 到 ImGuiLayer 里。 可能需要修改一些头文件的包含 还有就是mgui\examples\example_glfw_opengl3\main.cpp 是用来创建一个示例 glfw 窗口的我们需要参考一些它对 imgui 的初始化和 Update 方法 初始化包含 ImGui::CreateContext(); ImGui_ImplOpenGL3_Init() 这两个函数 而渲染循环要看 main.cpp 中的渲染循环主要就是这些 ...// Start the Dear ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame();...// Rendering ImGui::Render();...ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());然后就把这些写到自己的 ImguiLayer 的 Update 中 计算 deltaTime 可以参考 ImGui_ImplGlfw_NewFrame(); 中写的它在 imgui\examples\imgui_impl_glfw.cpp 中定义 InGui 添加事件 参考已有的事件的写法我们需要在 ImGuiLayer 定义各个自定义的事件处理函数然后在 OnEvent 中用传入的事件来创建一个 EventDispatcher 实例然后将自己所有的事件处理函数都送到 EventDispatcher 中EventDispatcher 中只有事件类型和传入的事件处理函数的类型相同时才允许调用传入的函数……总之就是之前那一套 只是怎么自定义各个派生类事件的处理函数就需要知道一些怎么传递信息到 ImGui这还是需要看视频的 强制 premake 将路径视为 Windows 路径 之前 premake 中我们设置将构建结果复制到指定目录下 但是当工程中没有这个指定目录的时候premake 将不会把构建出来的 dll 复制过去导致第一次构建失败 而第一次构建失败的过程中在构建 Sandbox 的时候创建了这个目录 所以第二次构建 premake 可以找到这个目录可以构建成功 根据https://github.com/TheCherno/Hazel/pull/22 第一次构建premake 找不到这个目录或者说没有创建这个目录的原因是把第二个参数最后一个斜杠删掉了这就导致这个路径表示一个文件而不是表示一个路径 emmm我有点没懂主要是这样解释的话就无法解释为什么第二次构建会成功 但是总之他给出的解决方法是将第二个参数整体用双引号包起来表示这是一个 windows 路径 例如原先是 %{cfg.buildtarget.relpath}/XXX现在是 %{cfg.buildtarget.relpath}/“XXX” postbuildcommands {--({COPY} %{cfg.buildtarget.relpath} ../bin/ .. outputdir .. /Sandbox/)({COPY} %{cfg.buildtarget.relpath} \../bin/ .. outputdir .. /Sandbox/\) }输入轮询 Input Polling 需要知道当前的输入状态例如某一键是否被按下 如果对所有可能的输入状态设置 bool或者比特图……这样就太繁琐了 于是 glfw 提供了根据键的序号查询键的状态的函数 我们要做的就是封装 glfw 的查询函数封到自己的一个 Input 类中 这个类应该在任何地方都可以访问 又因为不同平台的输入设备不一样所以这个 Input 类需要是跨平台的也就是多态的 既要多态又要全局访问所以这个博客作者一开始希望的是 virtual 和 static 连用 https://blog.csdn.net/alexhu2010q/article/details/107688039 但是这样是没有意义的因为 virtual 的底层是依赖于对象里面存储的虚函数指针现在用 static 的话就是直接通过类名来调用函数没有对象了就没有虚函数指针了那就不能知道这个 virtual 函数实际上到底是指向哪个 static 函数了是基类的 static virtual 函数还是派生类的 static override 函数不知道了 所以正确的实现方法是在基类中创建一个静态单例。之前也做过在 Application 那里 单例对象设置成 privatestatic get 函数设置成 public这样可以防止外部更改这个单例对象 单例还要把类的拷贝构造函数和赋值运算符重载函数设置成 delete 键位 Key Code 我们是从 glfw 查询输入的但是 glfw 中的键位的 enum 值和各个平台自己设置的 key code 的 enum 值可能是不一样的 例如 glfw 中 #define HZ_KEY_TAB 258 但是 Windows 中 TAB 是 9 https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes 我们在引擎中肯定是要定义一套 Key Code 的这样纠结的地方就来了如果要跨平台例如我们现在不用 glfw 来轮询输入而是用 Win32 的 API 来轮询输入而如果这个时候我们还是用的 glfw 风格的 Key Code 传递给 Win32 的 API那么就会发生错误 最有效的解决方法就是我们知道我们要把引擎编译到什么平台上所以我们用预处理指令 #ifdef根据平台相关的预编译头来决定编译哪种 Key Code 这种在运行时的效率是最高的唯一一点不好是存储的游戏数据包含了键位映射数据可能无法跨平台共享或者这就需要你还开发一个跨平台共享数据的工具或者这也可以通过更详细的序列化措施来实现例如原来无法跨平台共享的是 enum 值那么现在我键位映射数据不存 enum 值而是存 string 值这样就可以跨平台了但是这总归是你需要 hard code 各个平台之间的转换的当然也不是说这样子不好…… 然后第二种方案就是在引擎中需要考虑跨平台 KeyCode 的时候从引擎输入到特定平台做一个转换函数例如 EngineKeyToGlfwKey(key)从特定平台输入引擎做一个转换函数例如 GlfwKeyToEngineKey(key) 第一种方案就太复杂了总归来说而且性能瓶颈也不在这里所以可以不用这样优化 然后如果我们将引擎的 Key Code 从某个平台 copy 过来那么在这个平台相关的代码中我们还可以方便一点就不用这两个转换了 这里的跨平台更多的说的是嗯跨平台的库的 API 例如其实我们引擎也是运行在 Windows但是其实我们没有用到 Windows 的库所以我们不会在 Windows 的应用上做键位交互我们一直是在和 glfw 窗口交互所以我们一直用 glfw 的 Key Code 作为引擎的 Key Code所以我们不用转换函数更不用转换到 Window 的 Key Code GLM 数学库 一个好的数学库除了跨平台还可以通过 simd 指令来提高效率 当然评论区也说 simd 不一定更好例如他需要内存对齐如果你忘了内存对齐他可能更慢之类的 如果要自己写 simd 指令就需要写汇编代码或者是 intrinsic 代码这些都是与编译器相关的 所以与其费心思学这些不如直接用现有的数学库例如 GLM ImGui 拖拽和视窗 mGui Docking and Viewports 游戏引擎比如Unity、UE4里的窗口都是可以拖拽(Docking)的这是编辑器最基本的功能为了不采用WPF、QT这些技术来完成拖拽功能可以直接用ImGui来完成而ImGui在其Dock分支正在开发这一功能还没合并到master上这意味着这个相关功能可能会随时更新 之前是链接到 ImGui然后直接把 ImGui 的示例代码 copy 一份然后放在自己的工程中 那是一个暂时性的操作现在我们对 ImGui 是 git submodule 的所以需要时刻与 ImGui 的更新保持同步所以我们应该以某种方式 include ImGui 的代码 而这里我们知道我们需要什么代码所以我们直接在工程中新建一个源文件然后包含我们需要的源文件 这些 ImGui 中的源文件就会被编译到工程中 其中有一个预编译头表示我们的 OpenGL 用 Glad 来加载 #include hzpch.h#define IMGUI_IMPL_OPENGL_LOADER_GLAD #include examples/imgui_impl_opengl3.cpp #include examples/imgui_impl_glfw.cpp然后我们就可以删掉我们之前写的暂时性的代码例如在 ImGuiLayer::OnAttach() 中的一些初始化的代码就可以去掉了因为这些在我们包含的 ImGui 代码中都有我们之前就是从这些抄过来的 还有一个变动是他把原先的 OnUpdate() 拆成了三个Begin(), OnImGuiRender(), End() Begin() 和 End() 就是原来的 OnUpdate() 中的头尾而 OnImGuiRender() 会调用一些 ImGui 的 API 为 ImGui 添加 dllexport 相关的宏定义 参考https://blog.csdn.net/alexhu2010q/article/details/107688039 拉取了 7c02b7863f1d70af88b7c447b223c9da4dc9e04a 这个 commit 的时候构建会报错 原因是我们在 E:\Hazel-master\Hazel\Sandbox\src\SandboxApp.cpp 中调用了 ImGui 的 API virtual void OnImGuiRender() override{ImGui::Begin(Test);ImGui::Text(Hello World);ImGui::End();}但是 ImGui 是链接到 Engine 项目的而 Engine 项目是编译成 dll 的所以 ImGui 是 Engine 项目编出的 dll 的一部分也就是说 ImGui 的类被 Engine 项目外部调用的时候也需要 dllexport dllimport 等操作 ImGui 中对他自己的类也有进行 IMGUI_API 的定义但是默认情况下这个 IMGUI_API 是没有值的所以我们可以利用它 在 Engine, ImGui, Sandbox 添加一些预编译头的 hack 直接在 Engine 和 ImGui 的项目属性中添加预编译头 IMGUI_API _declspec (dllexport)在 Sandbox 的项目属性中添加预编译头 IMGUI_API _declspec (dllimport) 这样可以 work证明我们之前的想法是对的但是我们需要一个非手动 hack 的解决方法 在 Engine 项目的宏定义的头文件中为 ImGui 添加 dllexport dllimport 相关的宏定义 例如在 Engine 项目的 Core.h 中添加这些定义 #ifdef HZ_PLATFORM_WINDOWS#ifdef HZ_BUILD_DLL#define HAZEL_API _declspec (dllexport)#define IMGUI_API _declspec (dllexport) // 添加对IMGUI_API的定义导出api#else #define HAZEL_API _declspec (dllimport)#define IMGUI_API _declspec (dllimport) // 添加对IMGUI_API的定义导入api#endif // HZ_BUILD_DLL #endif 但是这是头文件所以要对 ImGui 起作用的话就需要在 ImGui 中对包含 IMGUI_API 的文件的开头加上包含 Core.h 这样我们就修改了 submodule 的文件不可持续 在 Engine 项目的宏定义的头文件中为 ImGui 添加 dllimport在 ImGui 项目中添加 dllexport 的宏定义 在 Engine 项目的 Core.h 中添加这些定义 #ifdef HZ_PLATFORM_WINDOWS#ifdef HZ_BUILD_DLL#define HAZEL_API _declspec (dllexport)//#define IMGUI_API _declspec (dllexport) // 添加导出这一行不要了#else #define HAZEL_API _declspec (dllimport)#define IMGUI_API _declspec (dllimport) // 添加导入#endif // HZ_BUILD_DLL #endif 而在 ImGui 的 premake5.lua 中设置 defines {IMGUI_API__declspec(dllexport)} 这样就方便实现了目标 使用 Module Definition File 除了上面说的去定义宏的方法还可以使用第三种方法Module Definition File这种方法比较麻烦但是可以不改变submodule的内容其文件格式后缀为.def可以在该文件里列出所有需要exportdll的函数的签名如下图所示 Cherno给出的文件代码如下所示这里把ShowDemoWindow、End等四个函数进行了dllexport的操作不过这玩意儿很难写 将 Engine 项目改为静态库 当然还有一种方法是把 Engine 项目改为静态库这样就不会有那个问题了 为 submodule 添加文件 我们不能直接再 submodule 里面添加文件 但是我们又确实希望多加一些例如 premake5.lua 这个时候我们可以 fork 我们需要的 submodule然后我们对自己的 fork 时是有权限的我们就可以在自己的 fork 中添加文件然后再将自己的 fork 作为 submodule 这样我们可以在 fork 中更新原仓库也可以添加自己的文件了 lib 和 dll 之间的选择 引擎作为dll的优点 hotswapping code Easy to Link 引擎作为dll的缺点 没有static linking快因为Linker可以对static link的东西做优化比如inline操作 lib只会产生一个exe比dll方便 不用担心dll的版本与使用引擎的代码不匹配的问题 使用dll有一些因为使用template或者其他内容的警告很难处理比如说下面这个警告 // 因为使用了智能指针而没有把unique_ptr作为dll接口导出(dll boundary issues) warning C4251: Hazel::Application::m_Window: class std::unique_ptrHazel::Window,std::default_delete_Ty needs to have dll-interface to be used by clients of class Hazel::Application也可以维护两个版本一个dll版本一个lib版本但是工作量太大就算了。 其实一个Game Engine没有太大必要去做成hot swappable的Game Engine做出来的游戏很有必要支持热更但是游戏引擎本身就没必要了比如说Doom这个游戏他们就是把游戏的内容做成dll然后用Engine去启动这个dll作为游戏这样用户可以直接热更dll更新游戏但是引擎本身是不会更新的所以说具体使用Engine的时候Engine改动的频率不会很高所以最终还是决定把Hazel从dynamic library改为static library热更可以交给编写游戏程序的脚本语言来做而不一定非得用C支持热更 dll 导出的类中包含 stl 成员时出现的问题 https://stackoverflow.com/questions/4145605/stdvector-needs-to-have-dll-interface-to-be-used-by-clients-of-class-xt-war/6869033 https://stackoverflow.com/questions/32098001/stdunique-ptr-pimpl-in-dll-generates-c4251-with-visual-studio https://jeffpar.github.io/kbarchive/kb/168/Q168958/ 解决方法 如果 dll 没有必要把某个包含 stl 成员的类暴露出来那就不暴露出来不用 dllexport dllimport 如果确实这个包含 stl 成员的类的这个 stl 成员不会被客户端相对于编译成 dll 的项目而言的链接 dll 编译 exe 的那个项目所访问那么就可以禁用这个警告 如果确实客户端要用到这个包含 stl 成员的类那么可以考虑做一层包装之后再对这个包装类 dllexport dllimport、 如果包含 stl 成员的类中客户端只是用到某个成员那么可以不对整个类 dllexport dllimport 而是单独 dllexport dllimport 这个类中的某个成员 或者也可以使用 PImpl 技术但是如果 PImpl 是用 unique_ptr 来隐藏细节的话那么 dllexport 这个包含 unique_ptr 的类仍然会有问题 如果Application用不到这个数据那么没有必要把它暴露出来如果心里有数确实用不到的话可以禁用掉警告 修复其他一些 warning https://blog.csdn.net/alexhu2010q/article/details/111030313 改成了静态库之后会修复一部分 runtime library 改成静态库之后runtime library 还需要设置成 /MT c17 还有 -stdc11 这种错误 https://stackoverflow.com/questions/29473786/command-line-warning-d9002-ignoring-unknown-option-std-c11 在 premake 中指定 C17 就好了 cppdialect C17重复的编译内容 warning LNK4221: This object file does not define any previously undefined public symbols, so it will not be used by any link operation that consumes this library这个警告发生在当一个.obj文件要被Linker链接到一起的时候如果这个.obj里的东西其他的.obj都有那么这个.obj就会被忽略。 // a.cpp #include iostream// b.cpp #include iostream int func1() {return 0; } a 中的 b 中都有那么 a 会被忽略 解决方法就是删掉多余的 a 文件 CRT warning 对于某个单个文件想要忽略掉旧函数的安全提示可以用 #define _CRT_SECURE_NO_WARNINGS对于整个项目都想要忽略掉的话可以为项目定义这个预编译头 渲染 Rendering 介绍 Design Architecture 如何 Draw API Line举个例子不同的平台上渲染的方式都不同那么如何设计出那些通用的API方法也就是找到一个普适的由Hazel的Drawing API Line组成的程序根据平台的不同去使用对应的override的方法。举个例子Vulkan和OpenGL完全不一样在OpenGL里绘制一个三角形需要创建对应的Contex而Vulkan里面需要调用Command Queue、rendering devices等等但是二者肯定是有通用的地方的比如都需要上传对应的vertices数据、上传对应的顶点数据、上传一个shader、调用drawcall等等那么设计游戏引擎的时候能不能设计出来通用的API框架呢 如何设计 Graphics API Abstraction 下面给出了一个架构图右边的都是具体与各个platform绑定的API所以右边的API需要对每一个平台完成该平台对应的具体API的实现简单的说就是右边的东西都是与Platform相关的这些东西属于Render API Primitives而左边的渲染概念是所有平台通用的举个例子如果现在多了一个平台叫Toby平台那么右边所有的内容都需要加一个分支也就是增加对应的Toby平台的API的相关内容而左边的内容是完全不会改变的 关于渲染如何画出上面这条线也就是如何决定哪些类的平台无关的哪些类是平台通用的其实挺难的。即使做出了上面的这个划分实际执行起来也没有那么简单因为不同的平台使用的primitive(图右边的内容)可能也不是一样的比如在OpenGL和Vulkan实现Deffered Renderer在OpenGL上只需要创建一些frame buffers就可以了而Vulkan需要额外的内容比如pipelines、descriptive sets等两个平台上相同内容的执行逻辑本身就是不一样的 关于左边的内容这里再进一步解释一下 Scene Graph场景里物体的Hierarchy相当于Unity的Scene HierarchyUE4的World Outliner Sorting用于决定物体的渲染顺序可以用于透明颜色的Blending还可以把相同Material的物体sort到一起然后一起渲染 Culling决定哪些在Frustum里面比如Occlusion Culling MaterialMaterial其实就是Shader和Uniform Data的集合(或者再加一个Texture) LOD Animation CameraCamera can be tied to a framebuffer or a camera may be redering to a render target VFXVisual Effects比如粒子系统 postVFX后处理效果比如说颜色矫正实现眩晕、酒醉效果或Screen Space Occlusion等 还有些内容比如Render Command Queue这个Queue用来存储所有渲染的指令这样就可以开一个单独的线程用于执行这个QueueCommand Queue在Vulkan里是本身就有的而OpenGL就没有这个功能所以需要单独为OpenGL添加这一块的功能 如何起步 首先选择使用OpenGL来开始工作因为它是最简单和容易的图形库 然后需要build Render API这里就是使用OpenGL渲染出一个三角形即可这一步我以前做过没啥难度注意这里并不是一次性build所有的Render API 接着需要build Renderer这个Renderer可以绘制一个三角形 最后基于这个三角形的绘制我们可以绘制任何东西 Rendering: Render Context 开始搭建渲染引擎的第一件事就是创建对应的Render Context这个Context是与平台相关的不同的平台对应的Render Context也是不同的现阶段不会像之前设计EventSystem那样先搭建好大多数的代码框架而是会先从Render Context开始搭建 GLFW 的 context 支持 OpenGL 和 Vulkan但是当我们想要做 DirectX 和 Metal 的时候GLFW 就不够了 我们目前是在 Hazel\src\Platform\Windows\WindowsWindow.cpp 的 WindowsWindow::Init 中使用 glfw 提供的设置 context 的函数 glfwMakeContextCurrent 来完成 context 的设置为了支持包含 DirectX 和 Metal 的多平台这个就不能用了 同理gladLoadGLLoader glfwSwapBuffers 也是不能支持 DirectX 和 Metal 最后我们可能要把 glfwSwapBuffers 抽象成一个函数这个函数会完成 m_Context.SwapBuffers(); m_Context.GetSwapChain().Flush();之类的 现在要做的就是创建一个Context类经过反复考虑谁应该拥有Context后决定Context需要作为一个Static对象放到Window类里这样就是一个Window里绘制一个平台的渲染图像有的引擎可以在一个Window里实现左半边用DirectX绘制右半边用OpenGL绘制Hazel引擎暂时不打算支持这种功能。 其实具体的做法就是创建一个Context类然后把OpenGL的相关操作再封装一层 这里虽然是改成了 m_Context new OpenGLContext(m_Window); 然后把一些 glfw 的操作移动到了 OpenGLContext 中看上去好像没有什么方便但是总之这一结构意味着我们以后可以再创建一个 DirectXContext 然后使用统一的接口。多态嘛基类定义接口派生类实现OpenGLContext 和 DirectXContext 都继承 GraphicsContext这样的话我们在 GraphicsContext 定义一套统一的接口就可以方便实现跨平台的操作 绘制三角形 之前学过 OpenGL这里跳过 着色器 OpenGL Shaders 之前学过 OpenGL这里跳过 官方 Shader 示例 https://www.khronos.org/opengl/wiki/Shader_Compilation 里面还讲了分离式的着色程序例如第一个分离式的着色程序只有顶点着色器第二个分离式的着色程序只有几何和片元着色器然后我们创建一个着色管道将它们链接在一起然后可以使用这个着色管道 我觉得这样的方便之处在于以前我们要更换着色器的话是以着色器为单位进行更换的现在是以可分离的着色程序为单位进行更换的可能会更省力比如在上面的例子中如果我想更换顶点着色器的话我就不用管第二个可分离的着色程序了 渲染 API 抽象 Renderer API Abstraction Compile Time Or Runtime 关于游戏引擎Hazel它需要可以根据不同的平台使用不同的渲染接口比如DirectX、OpenGL、Metal或者Vulkan等。目前有两种做法。 第一种是在Compile Time决定Hazel引擎使用哪种渲染API具体是通过不同的宏来实现的比如USE_OPENGL_RENDERER这些宏然后根据这些宏的设定对引擎代码进行编译就只会编译OpenGL相关的渲染代码如果我想要OpenGL来实现绘制就使用对应的宏build OpenGL的代码如果想用Vulkan就用Vulkan对应的宏来build Vulkan的代码总之最后的build出来的引擎就只是支持一个平台的渲染API。 这样做的坏处时一次build只能用一个平台的渲染API而且每次切换渲染的API时都需要rebuild相关代码这对开发者来说是很不友好的比如说同样的技术实现的画面效果用DX或OpenGL应该是一样的如果不一样就说明出了什么问题如果开发者要对比两个平台的画面效果那么要反复切换宏然后rebuild这很麻烦。虽然可以把各个渲染平台对应的组件设置为各自的dll但仍然需要在Compile Time重新编译代码生成新的引擎build应该最终是exe文件。而好处就是引擎在runtime不必花时间去判断到底用哪个平台的渲染API所以runtime下效率会更快 第二种是在Runtime决定使用哪种渲染API既然是Runtime那么肯定是不能用宏了有人之前用if条件去为每一个渲染的API做一个条件判断这样做工程量很大也有点傻这里建议的做法是利用多态(虚函数)来做比如说有Shader类那么就有OpenGLShader和DirectXShader这样各平台的派生类这样在build时会编译所有可用平台的相关渲染api比如ios平台就会编译OpenGL和metal的渲染API。 Vertex Buffer Layouts OpenGL 和 DirectX 中的 Buffer Layout 在OpenGL里描述顶点缓存的布局与顶点着色器的关系不大而DX里只有绑定了顶点着色器后才可以描述Buffer Layout。 在DX里只有Vertex Buffer和Buffer Layout这种东西没有像OpenGL这样专门搞一个VAO来描述最终使用的顶点数据所以这里就按照DX的来每一个Vertex Buffer都有他自己对应的BufferLayout所以要定义一个BufferLayout的类 关于 initializer_list 方案 1 // Buffer.hclass BufferLayout { public:BufferLayout(const std::vectorBufferElement elements): m_Elements(elements){...} private:std::vectorBufferElement m_Elements; }// OpenGLBuffer.hclass OpenGLVertexBuffer : public VertexBuffer { public:virtual void SetLayout(const BufferLayout layout) override { m_Layout layout; }... }// Application.cpp// BufferLayout layout { // wrong std::vectorBufferElement layout { // list - vector constructor - vector{ ShaderDataType::Float3, a_Position },{ ShaderDataType::Float4, a_Color } };m_VertexBuffer-SetLayout(layout); // vector - vector copy constructor - vector member方案 2 // Buffer.hclass BufferLayout { public:BufferLayout(const std::initializer_listBufferElement elements): m_Elements(elements){...} private:std::vectorBufferElement m_Elements; }// OpenGLBuffer.hclass OpenGLVertexBuffer : public VertexBuffer { public:virtual void SetLayout(const BufferLayout layout) override { m_Layout layout; }... }// Application.cppBufferLayout layout { // list - vector constructor - vector member{ ShaderDataType::Float3, a_Position },{ ShaderDataType::Float4, a_Color } };m_VertexBuffer-SetLayout(layout);所以第一种方法多了一个 vector 的拷贝构造要想消去的话就要用 std::move // BufferLayout layout { // wrong std::vectorBufferElement vec { // list - vector constructor - vector{ ShaderDataType::Float3, a_Position },{ ShaderDataType::Float4, a_Color } };// m_VertexBuffer-SetLayout(layout); // vector - vector copy constructor - vector member BufferLayout layout(std::move(vec)); // no vector copy constructor关于 std::move 如何使用 https://stackoverflow.com/questions/3413470/what-is-stdmove-and-when-should-it-be-used-and-does-it-actually-move-anythi 顶点数组对象 Vertex Arrays Object OpenGL里的VAO其实本身不包含任何Buffer的数据它只是记录了Vertex Buffer和IndexBuffer的引用并且使用glVertexAttribPointer函数来决定VAO通过哪种方式来挖取 VBO中的数据。 这一节课的目的是创建Vertex Array类由于OpenGL有VAO这个东西而DX里完全没有这个概念但是前期的Hazel引擎是极大程度依赖OpenGL的所以目前是先创建VertexArray类至于Dx这种的里面可能会有对应VertexArray的API但里面的执行代码弄成空的就行了。 make_shared 和抽象类连用无法构造对象 来自这个人的问题 https://blog.csdn.net/alexhu2010q/article/details/111030313 make_shared make_unique 这些是在内部调用了传入的模板类的构造函数的所以不能用于抽象类因为抽象类包含纯虚函数无法构造对象 // Buffer.cppVertexBuffer* VertexBuffer::Create(float* vertices, uint32_t size)// Application.cppstd::shared_ptrVertexBuffer vertexBuffer; // vertexBuffer.reset(VertexBuffer::Create(vertices, sizeof(vertices))); // right // vertexBuffer std::make_sharedVertexBuffer(VertexBuffer::Create(vertices, sizeof(vertices))); // error: can not instantiate abstract class vertexBuffer std::shared_ptrVertexBuffer(VertexBuffer::Create(vertices, sizeof(vertices))); // rightconst 限定符被丢弃 报错的第一种情况 两个变量类型不匹配的时候编译器实际上生成了一个临时变量进行类型转换 对于底层 const不能通过引用来修改引用指向的变量而现在底层 const 引用一个 tmp 变量这是可以接受的因为虽然这个 tmp 与原来的 a 没有关系但是这个 tmp 不能被通过这个引用来更改那么其实指向这个 tmp 的引用和 a 的引用的效果是一样的所以编译器可以容忍 double a 3; const int b a; // right, because ref to const tmp var is meaningful// euqal todouble a 3; const int temp a; const int b temp;但是如果没有 const那么这里就相当于引用了一个临时变量而这个临时变量与 a 没有任何关系所以通过这个引用来修改值的时候不会影响到 a就失去了期望的效果所以编译器报错 double a 3; int b a; // wrong, because ref to non const tmp var is meaningless// euqal to double a 3; int temp a; int b temp;报错的第二种情况 class OpenGLVertexArray : public VertexArray{public:...// virtual const std::shared_ptrIndexBuffer GetIndexBuffer() const { return m_IndexBuffer; } // rightvirtual std::shared_ptrIndexBuffer GetIndexBuffer() const { return m_IndexBuffer; } // wrongprivate:...std::shared_ptrIndexBuffer m_IndexBuffer;};函数后加 const那么返回的对象的前面会加上一个 const但是函数返回值没有加 const那么就相当于忽略了 const 限定符导致报错 渲染流和提交 Render Flow And Submission 之前对 VerterBuffer、VertexArray、IndexBuffer进行了抽象化也就是说目前Application里不会有具体的OpenGL这种平台相关的代码还剩下一个DrawCall没有进行抽象化也就是里面的glDrawElements函数还有相关的glClear和glClearColor没有抽象化。 Renderer Architecture 前面做的抽象化比如VertexBuffer、VertexArray这些都是渲染要用到的相关概念的类抽象真正的跨平台 用于渲染的Renderer类还没有创建起来。 思考一下一个Renderer需要干什么 它需要Render一个Geometry。Render一个Geometry需要以下内容 一个Vertex Array包含了VertexBuffers和一个IndexBuffer 一个Shader 人物的视角即Camera系统本质上就是一个Projection和View矩阵 绘制物体的所在的世界坐标前面的VertexBuffer里记录的是局部坐标也就是Model(World)矩阵 Cube表面的材质属性wooden或者plastic金属度等相关属性这个也可以属于Shader的范畴 环境信息比如环境光照、比如Environment Map、Radiance Map 这些信息可以分为两类 环境相关的信息渲染不同的物体时环境信息也一般是相同的比如环境光照、人物的视角等 被渲染的物体相关的信息不同物体的相关信息很多是不同的比如VertexArray也可能部分属性相同比如材质这些相同的内容可以在批处理里进行处理从而优化性能 总结得到一个Renderer应该具有以下功能 设置环境相关的信息 接受被渲染的物体传入它对应的数据比如Vertex Array、引用的Material和Shader 渲染物体调用DrawCall 批处理为了优化性能把相同材质的物体一起渲染等 可以把Renderer每帧执行的任务分为四个步骤 BeginScene: 负责每帧渲染前的环境设置 Submit收集场景数据同时收集渲染命令提交渲染命令到队列里 EndScene对收集到的场景数据进行优化 Render按照渲染队列进行渲染 具体步骤如下 BeginScene 由于环境相关的信息是相同的所以在Renderer开始渲染的阶段需要先搭建相关环境为此设计了一个Begin Scene函数。Begin Scene阶段基本就是告诉Renderer我要开始渲染一个场景然后会设置其周围的环境比如环境光照、Camera。 Submit 这个阶段就可以渲染每一个Mesh了他们的Transform矩阵一般是不同的依次传给Renderer就可以了这里会把所有的渲染命令都commit到RenderCommandQueue里。 End Scene 应该是在这个阶段在收集完场景数据后做一些优化的操作比如 把使用相同的材质的物体合并到一起(Batch) 把在Frustum外部的物体Cull掉 根据位置进行排序 Render 在把所有的东西都commit到RenderCommandQueue里后所有的Scene相关的东西现在Renderer都处理好了也都拥有了该数据就可以开始渲染了。 整个过程的代码示例 // 在Render Loop里 while (m_Running) {// 这个ClearColor是游戏最底层的颜色一般不会出现在用户界面里可能用得比较少RenderCommand::SetClearColor();// 参数省略RenderCommand::Clear();RenderCommand::DrawIndexed();Renderer::BeginScene();// 用于设置Camera、Environment和lighting等Renderer::Submit();// 提交Mesh给RendererRenderer::EndScene();// 在多线程渲染里可能会在这个阶段用一个另外的线程执行Render::Flush操作需要结合Render Command QueueRenderer::Flush();... } Renderer 是对渲染部分的拆分就像上面所展示的那样 BeginScene() Submit() EndScene() 然后 Renderer 拆分的步骤里面调用的是 RenderCommandRenderCommand 里面有很多静态函数作为接口RenderCommand 还有一个指向 RendererAPI 的指针这是 impl 的思想把实现封装到第二个类用指针指向它就相当于用指针封装了实现 然后 RendererAPI 是抽象类具体实现看派生类依次实现跨平台的操作 摄像机 CAMERAS Camera除了与渲染相关还与玩家有着交互 比如User Input、比如玩家移动的时候Camera往往也需要移动所以说Camera既受GamePlay影响也会被Submit到Renderer做渲染工作这节课的主要目的是Planning。 Camera本身是一个虚拟的概念它的本质其实就是View和Projection矩阵的设置其属性有 相机的位置 相机的相关属性比如FOV比如Aspect Ratio MVP三个矩阵里M是与模型密切相关的但是不同模型在同一个相机下V和P矩阵是相同的所以说VP矩阵属于相机的属性 实际渲染时默认相机都是在世界坐标系原点朝向-z方向看的当调整相机属性时比如说Zoom In的时候相机的位置并没有变实际上是整个世界的物体在靠近相机即往Camera这边平移当我们向左移动相机的时候其实没有Camera这个概念实际上我们是把所有世界的物体向右移所以相机的transform变化矩阵与物体的transform变化矩阵正好是互逆的。也就是说我们可以通过记录相机的transformation矩阵然后取逆矩阵就可以得到对应的View矩阵了这里只需要Position和Rotation因为相机是没有缩放的。 顶点坐标计算时的归属分配问题 如下图所示是一个顶点进行计算到屏幕坐标系的过程 gl_Position project * view * model * vertPosproject * view 是属于相机的相机看到的所有物体的投影变换和视角变换都是一样的 model 是属于物体的每个物体在自己的局部坐标系下的变换是不一样的 Camera作为参数传给Renderer的BeginScene函数 具体在代码里的思路是在游戏的Game Loop里有一个BeginScene函数这个函数是Renderer的静态函数会去更新相机、灯光等设置所以这里的BeginScene函数里需要接受Camera类的对象作为参数不过这里的作为参数会有两种做法 Camera对象作为引用传入BeginScene函数传入的是引用 Camera对象作值传入BeginScene函数传入的是值 正常情况下我思考的肯定是第一种传Camera的方法但是这里却需要选择第二种原因就在于这里是多线程渲染因为在多线程渲染里会把函数都放在RenderCommandQueue里执行这个BeginScene也会放进去而在RenderCommandQueue里的函数如果存了相机的引用是一件比较危险的事情因为多线程代码里有了camera的引用camera在多线程渲染的时候就不保证是不变的如果渲染时主线程更改了Camera的相关信息比如Camera的Pos, 就会乱套注意传入const 也不行因为这只能保证不在RenderCommandQueue里去改Camera的信息并不代表主线程里不可以改变Camera的信息 太强了……我自己看视频的时候感觉油管主好像没有提到这个博客的作者就已经思考了这么多了 正交相机 Orthographic Camera 没什么好说的 时间戳 TIMESTEPS and DELTA TIME 原视频没什么好说的 三种 Timestamp 参考 https://gafferongames.com/post/fix_your_timestep/ Fixed Timestamp 固定 dt 等于某一值 double t 0.0;double dt 1.0 / 60.0;while ( !quit ){integrate( state, t, dt );render( state );t dt;}但是物理解算和渲染耗时不一定是 dt这样可能导致物理和渲染不同步 Variable Timestamp dt 可变根据上一帧实际所用时间而定 double t 0.0;double currentTime hires_time_in_seconds();while ( !quit ){double newTime hires_time_in_seconds();double frameTime newTime - currentTime;currentTime newTime;integrate( state, t, frameTime );t frameTime;render( state );}这样虽然确实会得到正确的 dt不会有不同步的问题但是实际游戏中每帧用时如果相差比较大的话dt 可能不稳定dt 不稳定可能超过某个值带来游戏设计者不期望的效果例如步长过大导致的物理解算不稳定 Semi-fixed Timestep 这里 dt 被限制在不大于给定的值例如不大于 1/60 如果大于了那么就多次模拟 dt 给定值 的物理解算 double t 0.0;double dt 1 / 60.0;double currentTime hires_time_in_seconds();while ( !quit ){double newTime hires_time_in_seconds();double frameTime newTime - currentTime;currentTime newTime;while ( frameTime 0.0 ){float deltaTime min( frameTime, dt );integrate( state, t, deltaTime );frameTime - deltaTime;t deltaTime;}render( state );}这样有一个问题可能陷入模拟的死亡螺旋 spiral of death 在 Semi-fixed Timestep 中如果你的单帧物理模拟时间大于 dt 阈值那么实际上下一次你的两帧之间的时间间隔会变长因为实际用时大于模拟 dt 的这个 dt 的时间。如果一直单帧物理模拟时间大于 dt那么两帧之间的时间间隔会单调增加无限增加最后达到不可忽视的地步 解决方法也很容易想到要么就确保你的单帧的物理解算时间确实小于模拟的 dt要么就是钳制模拟的次数模拟的次数减少了表现上就是物理效果变慢了 物理确定性 在需要网络同步的游戏中如果游戏逻辑是同步输入的那么我们希望客户端的物理是确定性的才能在各个客户端之间得到相同的物理表现 但是各个客户端之间的电脑配置是不一样的所以每一帧的渲染时间也是不一样的而之前的 Semi-fixed Timestep 的物理模拟的 dt 其实是和渲染速率相关的例如假设 dt 阈值是 1/60现在渲染 FPS 100那么物理模拟速率显然也是被动的成为 100 FPS 现在我们希望各个客户端之间的渲染速率可以不一样但是物理模拟速率一样所以我们需要一个新的方法 double t 0.0;const double dt 0.01;double currentTime hires_time_in_seconds();double accumulator 0.0;while ( !quit ){double newTime hires_time_in_seconds();double frameTime newTime - currentTime;currentTime newTime;accumulator frameTime;while ( accumulator dt ){integrate( state, t, dt );accumulator - dt;t dt;}render( state );}这里我们实现了物理模拟的 dt 为给定的值同时渲染速率是由硬件决定 这里也会有死亡螺旋问题 物理模拟插值 因为我们使用了固定 dt 的物理模拟所以当累加器中的值小于 dt 时不会执行一次完整的物理模拟 所以我们希望最后一次执行完物理模拟之后这个时候的物理状态为 curr累加器还剩下一个小于 dt 的值我们将他除以 dt转化为一个 0 到 1 的比率然后把 prev curr再模拟一步物理更新 curr然后在 curr 之间插值 double t 0.0;double dt 0.01;double currentTime hires_time_in_seconds();double accumulator 0.0;State previous;State current;while ( !quit ){double newTime time();double frameTime newTime - currentTime;if ( frameTime 0.25 )frameTime 0.25;currentTime newTime;accumulator frameTime;while ( accumulator dt ){previousState currentState;integrate( currentState, t, dt );t dt;accumulator - dt;}const double alpha accumulator / dt;State state currentState * alpha previousState * ( 1.0 - alpha );render( state );}这里两个渲染帧之间的物理模拟的次数做了限制 if ( frameTime 0.25 )frameTime 0.25;然后这个代码似乎是向后插值的这让我很迷惑我觉得应该向前插值啊 比如写成 double t 0.0;double dt 0.01;double currentTime hires_time_in_seconds();double accumulator 0.0;State previous;State current;while ( !quit ){double newTime time();double frameTime newTime - currentTime;if ( frameTime 0.25 )frameTime 0.25;currentTime newTime;accumulator frameTime;double alpha 0.0;while ( accumulator 0.0 ){previousState currentState;integrate( currentState, t, dt );t dt;accumulator - dt;if ( accumulator dt ){alpha accumulator / dt;accumulator 0.0;}}State state currentState * alpha previousState * ( 1.0 - alpha );render( state );}变换 Transform 添加一个 Transform 表示模型在世界中的位置 材质 Material Material 是包装了 shader 的绑定uniform 变量的提交等操作 着色器抽象类 Shader Abstraction and Uniforms 为了跨平台Shader 做成抽象类glfw 相关的放到 OpenGLShader 派生类中 引用作用域和智能指针 Refs, Scopes and Smart Pointers 做了一个缩写 // Core.h namespace Hazel {templatetypename Tusing Scope std::unique_ptrT;templatetypename Tusing Ref std::shared_ptrT;} 为什么 Shader 不设置为 unique_ptr 而是设置为 shared_ptr 我们现在是要对整个场景进行渲染 而现在我们是每一个层级之内有一些 ShaderVAO 要提交到渲染器 Shader VAO 那些都可能是体积挺大的都是资源文件所以我们需要用指针来指向 那好真实的场景中可能是有多个层级如果某一个层级被 pop 了失去了引用销毁了然后如果其中的 shader 和 VAO 等等是 unique_ptr就会被销毁 但是实际的渲染是延迟的就是说先要把场景中所有的资源先提交然后统一处理所以并不是提交了某个 shader 之后就立即渲染的是有一个延迟的如果在这个延迟的过程中销毁了 shader实际渲染的时候就找不到了 还有多线程的问题 shared_ptr是线程安全的它的引用计数的加和减操作都是原子级别的为了保证多线程会造成额外的消耗所以如果不是在多线程下使用的为了更高效未来还可能需要实现自己引擎的shared_ptr类无非不是线程安全的 根据编程经验绝大多数情况下可以使用shared_ptr而不是unique_ptr二者性能开销其实不大如果二者性能开销较大可能还不如用raw pointers 这里的Hazel::Ref也就是shared_ptr引用计数的部分可以视作一个非常粗略的AssetManager一旦资源的引用计数为0则自动销毁该资源 shared_ptr 的引用计数存放在一个 new 出来的 int 内存中shared_ptr 中有一个指针指向存放引用计数的内存多个指向相同对象的 shared_ptr 共享这个引用计数的内存shared_ptr 的指向引用计数的指针都指向这个内存因为是 new 出来的所以在堆上 传递智能指针的本质是在传递“所有权”ownership传递 shared_ptr 对象实例是传递一个可以共享的所有权传递 unique_ptr 对象是传递一个唯一的所有权 一般来说如果是传递 shared_ptr 对象如果是值传递那么会改变它的引用计数那么这种情况一般是想要延长这个对象的生命周期例如这里提到的渲染器的例子。如果是传递 shared_ptr 的引用这么做的原因可能是因为值传递 shared_ptr 涉及到线程安全所以可能花费较大但是传递引用那么不会增加它的引用计数那么就要小心确保拿到这个引用之后函数不会意外释放它导致引用计数错误所以我们一般用 const 避免意外释放 void doSomething(std::shared_ptrint o) {o nullptr; // usage wrong but pass compilation}void doSomething2(const std::shared_ptrint o) {o nullptr; // dont pass compilation}其他的可见 shared_ptr 的用法及事故 https://heleifz.github.io/14696398760857.html 用 shared_ptr不用 new 使用 weak_ptr 来打破循环引用 用 make_shared 来生成 shared_ptr 虽然他举的例子是 f(shared_ptrA(new A), shared_ptrB(new B)); 但是在 C17 中某一个函数参数完全计算之后才会轮到下一个参数计算所以 new A 之后一定是 shared_ptrAnew B 之后一定是 shared_ptrB虽然 new A 和 new B 不知道谁先谁后但是不会出现内存泄露了 用 enable_shared_from_this 来使一个类能获取自身的 shared_ptr 防止返回 this 野指针的写法就是说 return std::shared_ptrA(this); 这种写法算是构造了一个新的 shared_ptr 而不是从现有的 shared_ptr 中复制的 传递 shared_ptr 时要么值传递要么用 const 材质素材 TEXTURES Textures 并不只是单纯的颜色组合出来的一张图而已它还可以存储一些离线计算的结果还有法线贴图等比如动画里甚至可以用其存储skin矩阵 代码实现是 Texture 抽象类派生类跨平台用 stb_image.h 加载纹理没什么好说的 一般我们把资源文件的路径存放在一个 AssetManager 里面这样当硬盘上的资源文件更新的时候我们就可以根据这个路径将最新的资源文件更新到我们的游戏工程里面 但是目前我们还没有这个 AssetManager所以我们暂时把路径存放在资源类里面 混合 Blend 材质混合这些设置属于初始化的范围所以我们再在 renderer command 中添加一个初始化的内容他也是 impl 的最后调用 openglrenderAPI 中的 init其中放着比如设置混合模式的函数 着色器资源文件 Shader Asset Files 利用ifstream来读取文件 一般来说游戏引擎里的Shader都是在Editor下预先编译好的二进制文件然后再在Runtime对其进行组合和应用 着色器库 ShaderLibrary 这章也很简单其实就是把Shader的读取和存储位置都分配到ShaderLibrary类里ShaderLibrary本质就是个哈希mapkey是shader的名字value是shader的内容 创建 2D 渲染器 How to Build a 2D Renderer 不管是3D还是2D的游戏引擎都需要渲染2D的东西因为一个游戏里是必须有UI的。 渲染架构 目前引擎里Render的代码是这样的 // 把Camera里的VP矩阵信息传到Renderer的SceneData里 Hazel::Renderer::BeginScene(m_Camera); {glm::mat4 scale glm::scale(glm::mat4(1.0f), glm::vec3(0.1f));...flatColorShader-UploadUniformVec4(u_Color, m_FlatColor);// Submit里面会bind shader, 上传Vertex Array, 然后调用DrawCallHazel::Renderer::Submit(flatColorShader, m_QuadVertexArray, transform);...Hazel::Renderer::Submit(textureShader, m_QuadVertexArray, transform);... } Hazel::Renderer::EndScene();这里面的操作基本就是设置统一的SceneData后针对各个VertexArray也就是Mesh提交其Mesh数据然后添加对应的Draw的命令。 但这套操作对于绘制2D的内容而言不太符合有这么几个原因 2D的渲染过程中基本没有Mesh这个概念它不需要Vertex Array因为万物皆可用Quad来表示 2D的渲染也没啥Shader和Material的概念因为它就是一张图贴上去而已还要啥渲染感觉加点类似光照的后处理是不是就行了 那么如何设计相关的2D渲染呢由于3D渲染和2D渲染的相机不同这里直接可以分为两个Scene然后设置不同的Renderer即可设计两种Renderer分别负责2D和3D的内容大概是这样 Hazel::Renderer::BeginScene(m_Camera); {Hazel::Renderer::DrawCube(...);// 3D... } Hazel::Renderer::EndScene();Hazel::Renderer2D::BeginScene(m_OrthographicCamera); {Hazel::Renderer2D::DrawQuad(...);// 2D... } Hazel::Renderer2D::EndScene();2D Renderer需要支持的内容 2D的Renderer主要需要实现以下内容 2D Batch Render: 支持批处理的2D Renderer主要是合并多个Quad的Geometry Texture Atlas的支持 Sprite Animation系统 贴图Data压缩技术大概是只保留第一帧的全数据后面都只记录产生变化的像素的Delta值主要是为了支持高精度的贴图后面会细聊 UI系统主要是Layout系统还挺复杂的比如怎么布置UI、UI元素怎么随窗口变化而自动匹配、怎么对其Text、怎么支持不同分辨率的屏幕、Font文件的读取和使用(文字的SpriteSheet) 后处理系统为了做好2D游戏这个系统是必须的比如做2D的爆炸特效、实现HDR、粒子系统、blur、bloom的后处理效果、Color Grading用于矫正颜色 Scripting暂时不需要考虑 不需要考虑的 Dynamic Lighting 在性能上目标是实现每帧绘制10W个quad而且fps在60以上。 关于BatchRenderer和Texture Atlas 目前是做2D的部分那么先实现2D的quad的批处理即可至于每帧的贴图个数实际上游戏引擎里的需求一般每帧用到一两百张贴图就已经很多了。假设GPU上有32个贴图槽位假设其中的8个是用于其他需求的不是用于直接渲染的那么还剩24个槽位。那么120张贴图就要Flush Renderer五次也就是五次Draw Call。所以2D的渲染来说Texture Atlas或者说Sprite Sheet至关重要。就是在一张贴图上尽可能多的存储贴图内容。当然这种适合小的低像素的贴图才能进行组合如果是一个4K的贴图那么一般是不会把它合并到Texture Atlas里面的。 关于Scripting 当谈到Game Engine与User的Interaction部分时人们很容易想到ECS架构或者CGO(Composable GameObjects)。也就是说GameObject可以通过Component来组合而不是代码里面的通过继承来组合(比如多重继承)。比如一个Player是个Entity然后里面添加各种Component比如 Transform组件 Renderer组件 Script组件用于负责Interaction和自定义行为 但这里提到的是Scriting。有的是用lua作为脚本语言UE4里用蓝图作为可视化的脚本还提供了UFrosbite里也提供了类似蓝图的schematicsUnity是C#基本的游戏引擎都有这块部分。 相机控制器 Camera Controllers 把相机初始化相机移动和缩放相机的 view 和 proj 矩阵的更新放到了一个 Camera Controllers 里面 Resizing glfw 窗口 resize 的时候需要做 从 glfw 的 resize 事件唤起引擎自己设置的 resize 事件 或许在 glfw 的 resize 事件中还需要做的 重新设置 opengl 的 framebuffer 也不是所有 framebuffer 都需要调整例如阴影贴图可能就是一直就是 1024 * 1024 设置一个 framembuffer pool避免重复创建内存。 在引擎自己设置的 resize 事件中自定义逻辑 通知摄像机跟随变化 这里还涉及到一个东西就是相机如何根据窗口大小变化而变化比如当窗口变大时画面是变大还是会展示更多的内容 如果窗口改变的时候只调整Viewport那么窗口里绘制的东西会随着窗口变大而变大如果不想改变尺寸那么需要调整正交相机的投影矩阵动态调整 zoom 和 aspectRadio 正交相机中可能需要考虑一下用哪种方法而投影相机中一般都是调整 zoom 和 aspectRadio 给Application类添加一个bool标识窗口是否被缩小化了 缩小的时候窗口的width和height都会接收WindowResizedEvent变成0所以当缩小化时需要停止各个Layer的更新。 现在是这么写 bool OrthographicCameraController::OnWindowResized(WindowResizeEvent e){m_AspectRatio (float)e.GetWidth() / (float)e.GetHeight();m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);return false;}这样写的效果就是在 X 上拉伸物体不会缩放只是能看到的画面变多而在 Y 上拉伸物体会缩放 因为 m_AspectRatio 的定义是依赖于某一个轴的 同时这个 proj 矩阵也影响了世界中的一个单位是否是对应屏幕上的一个像素单位…… 可维护性 Maintenance 整理了一下文件 Preparing for 2D Rendering 将 2d 部分的 VAO 摄像机 渲染提交放到了一个 Sandbox2D 层中 Starting our 2D Renderer 为了避免跟原本的3D的Renderer混淆这里创建了个Renderer2D内容也比较简单里面全部都是静态函数之所以不做成成员函数是因为没有必要毕竟成员函数本质上也是静态函数无非静态函数的第一个参数变成了this指针而已。 这里可以开始设计Renderer2D类了这里设计的Renderer2D与原本的Renderer类的区别在于 2D渲染里没有什么Vertex Array和Mesh的概念万物皆可用带贴图的quad绘制所以这里只会有唯一的Mesh数据所以这里直接把quad的顶点数据作为静态数组存在了Renderer2D类里在其Init函数里被创建出来。 2D渲染里基本不需要用户在绘制的时候传入自定义的Shader所以Shader可以作为Renderer2D的静态数据 emmm我感觉 2d 不需要 shader 有点不对劲吧 2D Renderer Transforms and Textures Renderer2D 现在有两种 DrawQuad 函数一种是传入颜色一种是传入纹理的它们内部使用的 Shader 都是写死了的 在 Shader 基类中设置了统一的虚函数Uniform 的设置函数在 OpenGLShader 实现 现在就可以用 RefShader 的指针直接调用 Set Uniform 的函数而不用像之前那样笨笨的 dynamic_cast 到派生类再调用派生类的特定实现函数 在 OpenGLRendererAPI::Init() 中设置了深度测试 Single Shader 2D Renderer 之前说的那个Renderer2D 现在有两种 DrawQuad 函数一种是传入颜色一种是传入纹理的它们内部使用的 Shader 都是写死了的 这里是两个 Shader现在我们希望写成一个 Shader 其实就是一直写成采样 * main_color 的形式 对于传入颜色的 DrawQuad那么就把 shader 的纹理参数设置成 runtime 创建的一个贴图 WhiteTexture它的width和height均为1图片通道格式为RGBA或者RGB每个pixel的值都是(1,1,1,1)或(1,1,1)。 那么 Texture 中新增一个函数用于传入长和宽来创建一个 Texture 之前我们是传入纹理的地址在内部使用 stb_image.h 来读取纹理现在这个是没有现有的纹理直接创建空纹理 所以我们需要指定纹理的长宽和格式长宽是构造函数输入的格式是构造函数内部写死的比如纹理格式设置为 GL_RGBA8指定一个内部格式这样我们就知道一个通道有多少位方便我们在传入原始数据指针之前先计算好我们要准备什么样的数据 glTextureSubImage2D 用于从现有的数据指针也就是原始数据中创建纹理 对于传入纹理的 DrawQuad那么就把 shader 的颜色参数设置成白色 Intro to Profiling 他这个计时类的写法就很骚 它把开始计时写在构造函数里面结束计时写在析构函数里面构造函数中传入了回调函数计时结束时也就是析构时调用回调函数 创建一个宏定义包装创建类的语句其中回调函数是把计时结果压入自己的一个堆栈变量 这样当我们在一个作用域内调用这个宏定义的时候我们就是在创建计时器然后程序退出这个作用域的时候局部变量析构也就是计时器析构那么退出作用域的同时就停止计时了 因此我们就用一个宏定义就完成了计时的功能其他什么都不用操作了太妙了 你还可以自己用花括号做一个作用域出来 Visual Profiling 按照一定的格式把计时器记录的信息写成 json 然后谷歌浏览器有一个内置的功能读取这个 json 输出时序图 chrome://tracing 这样的意义应该就是我们也可以自己写 json 自己写读取 json 输出时序图的功能 或许 imgui 中就有这样的示例……或者有人写过…… 然后还有一些可以提到的就是如果两次计时比较相近的话返回的值可能会相同这样导致两个计时项分不开可能需要一些 dirty 的操作例如确保计时器不会返回与上一次相同的数…… 好吧……之后别人的更改也很简单把 high_resolution_clock 改成了 steady_clock Instrumentation 指定时间开始录制指定时间结束录制 Improving our 2D Rendering API 为 DrawQuad 函数添加了缩放比例和旋转角度这两个参数 shader 也对应地更改 emmmm在我看来这都是没有必要的也不是这么说只是我感觉现在缺少那种通用的 Draw 才导致居然连 uniform 参数的传递都要写到函数参数里面一般来说这应该是脚本来完成的把 How I Made a Game in an Hour Using Hazel Hazel 2020 Hazel决定使用lua作为脚本语言lua非常简单其实就是相当于几个C文件、5000多行代码而已这里没有选择C#作为脚本语言然后用Mono来跨平台是因为这样做工作量太大了。尽管C#是很好用的语言但基于跨平台的原因还是不选择它。不过如果只想在Win平台上发布游戏那么游戏引擎是可以考虑用C#的此时可以用C/CLI来负责C与C#的交互。 批渲染 BATCH RENDERING 一开始创建一个数据区大小为一次批处理最多绘制的长方形数量 * 单个长方形的 uniform 数据大小我们得到了这个数据区开头的指针设为 base 之后我们要画长方形的时候就往这个 base 指向的数据区里面填充一个长方形的数据例如 Position Color TexCoord 等 填充数据的时候我们有维护一个指向数据区末尾的指针 ptr还有维护要绘制的长方形的数量 然后等到 Render2D 在一个循环里面结束了我们让 ptr-base 就得到了数据区的大小我们把这个 base 和数据区的大小传入 openglopengl 就知道了 VBO 的大小 具体到 ptr - base 是怎么算的可以提一下我之前还没见过这种写法 uint32_t dataSize (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;这种写法就是把指针指向的对象的大小设置为了 8 个比特也就是一个字节也就是说现在指针与指针之间的间隔用一个比特来衡量那么 ptr - base 得到的数字的单位就是比特这样就能够满足 opengl 的要求 而我们在初始化的时候已经设置了 VAO 解释了各个属性我们还在初始化设置了一整个 EBOEBO 的大小是 一次批处理最多绘制的长方形数量 * 单个长方形的顶点数 这样我们之前已经设置好了 VAO也一次设置了好整个 EBO所以我们直接传入数据区的大小base 指针要绘制的长方形的数量就可以完成一次批渲染了 但是这样有一个问题就是……如果有些长方形不想渲染了怎么删掉……因为现在对数据区的处理是单调增的还没有删除相关的 具体思路 在Renderer2D的Init函数里创建动态可更新的VertexBuffer 在Renderer2D的Init函数里创建静态的IndexBuffer 修改Renderer2D的static SceneData数据把里面的VertexArray里的Vertex Buffer调整为1W个Quad大小的动态BufferIndex Buffer调整为1W个Quad大小的静态Buffer创建时俩Buffer里的数据都是uninitialized data 修改DrawQuad函数让其绘制时动态往Vertex Buffer里填充要绘制的顶点属性数据同时记录绘制Quad的个数目前只支持绘制FlatColorDrawQuad对应的FlatColor颜色会作为颜色的顶点属性存在Vertex Buffer里 在EndScene里根据记录绘制Quad的个数填充IndexBuffer里的数据然后调用DrawCall绘制这些Quads Batch Rendering Textures 基本思路是在提供的GPU槽位上绑定尽可能多的贴图然后让Vertex Attribute里包含使用的Texture的id。这个贴图槽位数即Texture slot limit取决于GPU。A desktop GPU至少会有32个贴图槽位而手机则至少有8个技术层面上向GPU驱动去查询GPU的最多贴图槽位这样是比较合理的。但是目前还是就写成最多32个槽位因为查询GPU相关参数这个功能还比较麻烦。 另外为了让使用相同的贴图的DrawQuad函数能合并使用同一张贴图需要设置一个数据结构用于记录已经用于绘制的贴图类似于mapkey为贴图资源的引用value为贴图绑定的槽位这样当绘制一个带Texture的Quad时它会去检查map如果有key就取得对应的贴图槽位存到顶点属性里合并到一个DrawCall内。当然这个map可能还不止32个key所以最多一次DrawCall是绘制32种贴图的Quad但对于2D的Renderer来说由于Texture Atlas存在这种超过32个贴图的情况很少见就先不考虑了。感觉用array代替map也行无非是把数组的id作为槽位就可以了。 注意这里的Texture的Key需要是一个unique identifier这里可以临时使用OpenGL的TextureID但是对于游戏引擎而言贴图是一种资源游戏引擎应该有自己的资源系统对于任何一种资源引擎都应该为其生成一个资源的Unique ID作为Asset Handle比如Unity把资源的ID存到了其.meta文件里。因为资源生成的Asset Handle不应该存在资源文件里第一点正常情况下即使资源文件被美术家修改了其Asset Handle也不应该变如果变了那原本引用这个素材的记录了这个素材的旧 id 的文件就会失去对这个素材的引用。同理Asset Handle 更不应该是运行时的东西比如 opengl 生成材质的 texture id。 具体思路如下 创建Texture数组数组大小为32数组id对应的是贴图槽位数组元素是Texture的ID会在BeginScene里被重置即数组元素全部为0然后记录一个s_Data.CurrentTextureSlotID在BeginScene被初始化为1因为0号槽位预定给了WhiteTexture使用用于绘制FlatColor 修改Shader文件用来同时适配32个Texture Uniform槽位 Shader 中会接受一个 float uniform 作为 TexIndex用 int 也可以就是要加 flat表明从顶点着色器到片元着色器不插值外部设置 VertexBuffer 的时候要多设置一个 TexIndex 同样这里也没有做从存储了这 32 个 Texture 的 map 中删除元素的逻辑 使用 flat v_TexIndex 解决 z-fighting https://github.com/TheCherno/Hazel/pull/362 可以使用 flat v_TexIndex 解决 z-fighting emmm说实话我从来没有想过要这么批处理纹理所以这个问题我没有想过……明明 v_TexIndex 只是用来 switch 的 Drawing Rotated Quads 现在 DrawQuad 的 API 变成我们已经有了静态的四边形的四个点的坐标现在我们输入了 pos, size, rotation就可以组成一个transform对我们的静态的坐标 transform就是得到了要求的点的坐标组成 VBO Index Buffer 可以先绑定到 GL_ARRAY_BUFFER 设置缓冲再绑定到 GL_ELEMENT_ARRAY_BUFFER 根据 commit 5e94d7da514829d69c22e93202319ade63f29d67 我们一般学的是Index Buffer 要绑定到 GL_ELEMENT_ARRAY_BUFFER 但是这个 commit 说绑定到 GL_ELEMENT_ARRAY_BUFFER 需要已经有 VAO 绑定这样的话就不能实现顺序无关了所以我们可以先绑定到 GL_ARRAY_BUFFER他是与 VAO 无关的 等到我们真正要用到这个 Index Buffer 的时候我们再绑定到 GL_ELEMENT_ARRAY_BUFFER Renderer Stats and Batch Improvements 添加Renderer的相关Statistics信息比如当前帧调用了几个DrawCall绘制了多少个Quad 改进Batch系统添加了一个 FlushAndReset在存储的顶点数据达到给定上限的时候可以将顶点数据 Flush 掉提交一次渲染然后重置顶点数据指针复用这一块内存 但是仍然没有删除某一个长方形的功能 用ImGui把Stats绘制出来 我感觉他这里 FlushAndReset() 中少了对 ResetStats() 的调用…… Debug 模式和 Release 模式差距还挺大的 Testing Hazel’s Performance Let’s Make Something in Hazel 只能使用 int uniform 作为 array uniform 的 index 根据 https://github.com/TheCherno/Hazel/pulls?q18abce3c28db4e8384ad21cf64b6d36bee003215 OpenGL 只能使用 int uniform 作为 array uniform 的 index C类的成员变量初始化顺序 构造函数的列表初始化中成员变量的初始化顺序不是列表中的顺序而是按照对应变量出现在C类头文件定义的顺序先后来初始化的 How Sprite Sheets/Texture Atlases Work 采样精灵表 SubTextures - Creating a Sprite Sheet API 为了方便处理SpriteSheet或者说Texture Atlas可以添加一个额外的SubTexture类它本质上就是一个Texture的Wrapper然后添加了额外的四个TexCoord坐标用于表示Texture对应部分区域的Texture从而起到SubTexture的作用。 SubTexture是一个笼统的概念虽然Texture是跨平台的(需要有OpenGLTexture等类)但它作为一个Wrapper不需要跨平台(不需要有OpenGLSubTexture等类) 在Renderer2D类里添加额外的DrawCall函数用于支持SubTexture 还需要注意一点目前的游戏引擎是没有合并Geometry的功能就目前的2D Renderer来说暂时是不需要的。因为2D Renderer里一般只会绘制看得见的东西不会像3D游戏里需要绘制到很多屏幕看不到的东西(Occlusion)。3D渲染才需要Geometry合并的功能比如说它会把只看得见的部分合并成一个Mesh然后把它绘制出来从而优化性能。 Creating a Map of Tiles 如何表示Tiles组成的地图每个Tile用哪个SubTexture 二维数组优先行遍历还是优先列遍历哪一种更Cache Friendly一些 C 中的数组按照行优先存储所以行优先遍历比较好 如何表示Tiles组成的地图 方法其实很多总之目的是为了表示出地图上用了哪些类型的Tile以及每个Tile的位置就目前所用的2D贴图而言它最小的图形应该是128128像素的所以19201080(16:9)的屏幕最多也就是15*8.4375个Tile而已。 这里介绍了两种 用字符串表示字符串里的不同字符用于代表不同类型的Tile 用很小的像素图表示场景一个Tile用一个像素表示这样地图看起来比较直观而且是用图片资源存储的比较方便迭代 Next Steps Dockspace 参照 imgui_demo.cpp 里的代码调用对应绘制 Dockspace 的代码。 找到的示例函数是 ShowExampleAppDockSpace它是在 ShowDemoWindow 里被直接调用的 ImGui::Image 用来在 ImGui 窗口中画图。为了支持跨平台他接受的纹理 id 是用一个指向纹理 id 的 void* 来实现 Framebuffers framebuffer 有一个变量 SwapChainTarget在 Vulkan 中 false 表示离屏渲染那么 false 要在 OpenGL 中表示离屏渲染就是 glBindFramebuffer(frameBufferIndex); SwapChainTarget true 表示渲染到屏幕就是 glBindFramebuffer(0); Render Pass 后续的引擎还会添加Render Pass的概念Render Pass在OpenGL里更像是一个抽象的概念但在Vulkan里却是一个实实在在存在的类它会有一个Framebuffer和一个target。像上面的RenderToScreen为false的Framebuffer其实就是一个渲染到屏幕上的Render Pass而已渲染时的代码大概是 renderer.BeginRenderPass();Making a New C Project in Hazel 之前在 ImGui 中用一个 framebuffer 的颜色附件作为纹理展示了出来 这就是一个 viewport 的原型那么这个 ImGui 层就是一个 editor 的原型 创建一个 Editor 工程把这个 ImGui 层的代码放进来方便归纳 Editor和 Engine 与 Sandbox 分开 Scene Viewport 在 ImGui 的 Render 函数中前面部分是抄的 ImGui 的例子后面我们用 ImGui::GetContentRegionAvail() 获得 ImGui 窗口的大小与存储的窗口大小相比如果不相同说明 ImGui 窗口的大小发生了变化那么我们通知 framebuffer 和 camera触发它们的 OnResize 函数 framebuffer 的 OnResize 函数需要把旧的framebuffer和纹理附件删掉重新创建framebuffer和纹理附件绑定 摄像机的 OnResize 函数是重新设置 AspectRatio 和 Proj 矩阵 ImGui 无边框 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ 0, 0 });使用 glTexImage2D 创建纹理附件方便更新 当然你也可以不重新创建 FBO 只是重新创建颜色附件和深度附件使用 glTexImage2D 创建颜色附件和深度附件就好了如果是 glTexStorage2D 创建的纹理这种纹理是不可变的 void OpenGLFramebuffer::Invalidate(){glCreateFramebuffers(1, m_RendererID);glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID);glCreateTextures(GL_TEXTURE_2D, 1, m_ColorAttachment);glBindTexture(GL_TEXTURE_2D, m_ColorAttachment);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Specification.Width, m_Specification.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_ColorAttachment, 0);glCreateTextures(GL_TEXTURE_2D, 1, m_DepthAttachment);glBindTexture(GL_TEXTURE_2D, m_DepthAttachment);glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, m_Specification.Width, m_Specification.Height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_DepthAttachment, 0);//glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, m_Specification.Width, m_Specification.Height);//glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, m_DepthAttachment, 0);HZ_CORE_ASSERT(glCheckFramebufferStatus(GL_FRAMEBUFFER) GL_FRAMEBUFFER_COMPLETE, Framebuffer is incomplete!);glBindFramebuffer(GL_FRAMEBUFFER, 0);}void OpenGLFramebuffer::Resize(uint32_t width, uint32_t height){m_Specification.Width width;m_Specification.Height height;if (m_RendererID ! -1){glBindTexture(GL_TEXTURE_2D, m_ColorAttachment);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Specification.Width, m_Specification.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);glBindTexture(GL_TEXTURE_2D, m_DepthAttachment);glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, m_Specification.Width, m_Specification.Height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, nullptr);//glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, m_Specification.Width, m_Specification.Height);}}写的时候我创建纹理用的是 GL_DEPTH_COMPONENT 的格式但是绑定的时候我还是用的 GL_DEPTH_STENCIL_ATTACHMENT 所以导致出现了 FBO 不完整的错误……我还一直都不知道为什么给我心态整的有点小崩hhh Code Review ImGui Layer Events 虚析构函数 使用基类指针指向派生类成员析构的时候需要基类定义了虚析构函数才能调用派生类的析构函数 在 Compile Time 决定 Input 的实现 因为我们已经知道了编译平台所以我们可以决定 Input 只在哪个平台上运行 所以我们的 Input 只需要对每一个平台一个 cpp 实现然后在编译的时候设置某个平台编译哪个 cpp 就好了 这就是多态的一种实现方法这就是这种方法的使用场合就是多态对于某一个平台是确定的那么直接在编译的时候确定就好了 另一种多态的实现方法是虚基类继承的方法这种适用于运行时切换多态中的某一种状态例如运行时需要切换 OpenGL 或者 DirectX 或者 Vulkan ImGui 的 IsWindowFocused 它用窗口是否为焦点来判断是否要接受事件m_ViewportFocused 或 m_ViewportHovered 为假的时候不接受事件m_ViewportFocused 都 m_ViewportHovered 为真的时候才接受事件 但是这个 m_ViewportFocused m_ViewportHovered 都是要 glfw 窗口被 focuse 的时候才开始判断的很合理 void EditorLayer::OnImGuiRender(){m_ViewportFocused ImGui::IsWindowFocused();m_ViewportHovered ImGui::IsWindowHovered();//Application::Get().GetImGuiLayer()-BlockEvents(!m_ViewportFocused || !m_ViewportHovered);Application::Get().GetImGuiLayer()-BlockEvents(!m_ViewportFocused);...然后这个emmm其实只是控制 ImGui 的输入事件然后其实这里写的摄像机的控制是用 glfw 的输入来控制的 所以要达成相同的目的还要这么写…… void EditorLayer::OnUpdate(Hazel::Timestep ts){HZ_PROFILE_FUNCTION();// Updateif (m_ViewportFocused m_ViewportHovered)m_CameraController.OnUpdate(ts);...Where to go next Code Review ImGui::GetContentRegionAvail() 可能返回负值 这是在最小化 glfw 窗口的时候出现的问题 不知道为啥我没出现这个问题 单独 resize ImGui 窗口时出现的闪烁 原来是这么写的 void EditorLayer::OnImGuiRender(){...ImVec2 viewportPanelSize ImGui::GetContentRegionAvail();if (m_ViewportSize ! *((glm::vec2*)viewportPanelSize)){m_Framebuffer-Resize((uint32_t)viewportPanelSize.x, (uint32_t)viewportPanelSize.y);m_ViewportSize { viewportPanelSize.x, viewportPanelSize.y };m_CameraController.OnResize(viewportPanelSize.x, viewportPanelSize.y);}...}把这个 imgui 窗口从 dockspace 中提取出来然后单独缩放的话就会有闪烁黑屏的现象 现在是这么写的可以修复黑屏 void EditorLayer::OnUpdate(Hazel::Timestep ts){// Resizeif (Hazel::FramebufferSpecification spec m_Framebuffer-GetSpecification();m_ViewportSize.x 0.0f m_ViewportSize.y 0.0f // zero sized framebuffer is invalid(spec.Width ! m_ViewportSize.x || spec.Height ! m_ViewportSize.y)){m_Framebuffer-Resize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);m_CameraController.OnResize(m_ViewportSize.x, m_ViewportSize.y);}...}void EditorLayer::OnImGuiRender(){ImVec2 viewportPanelSize ImGui::GetContentRegionAvail();m_ViewportSize { viewportPanelSize.x, viewportPanelSize.y };...}就很神奇他相当于是在 OnImGuiRender() 中获取到新的 size保存下来但是仍然使用旧的 size 的设置framebuffer相机参数来渲染然后在 OnUpdate() 中完成 resize 的处理framebuffer相机参数然后在下一次的 OnImGuiRender() 渲染上一次的 size 程序主循环是这样的 for (Layer* layer : m_LayerStack)layer-OnUpdate(timestep);m_ImGuiLayer-Begin(); for (Layer* layer : m_LayerStack)layer-OnImGuiRender(); m_ImGuiLayer-End();神奇的是如果把 resize 放到 OnImGuiRender() 中不管是放到获取 size 的前面还是后面效果都是一样的 所以我们可以知道起码这个问题resize 和获取 size 之间的顺序是无关的 但是我们又把 resize 放到 OnUpdate() 中就没有这个问题了所以是 resize 和 ImGui 的渲染之间有关系 马后炮只能分析到这里了 之后看别人的评论才知道是什么原因 https://github.com/TheCherno/Hazel/pull/268 我们相当于是 绑定了一个 framebuffer 渲染到这个 framebuffer然后解绑 resize 这个 framebuffer然后绑定这个时候 framebuffer 里面是空的 绘制 这个时候我们绘制的是一个空的 framebuffer所以会导致黑屏 这就很合理……即使我的 resize 是直接用 glTexImage2D 重新分配纹理附件完成的那重新分配也是分配了空的数据指针效果是一样的 所以说这或许给我长了记性黑屏闪烁可能是什么原因导致的 不要把重要的最通用的宏定义放到头文件里 它很难被所有cpp引用即使把它放到pch里它也只能保证被Hazel引擎内的代码使用对于引擎使用的第三方库文件不大可能会include这个Core.h文件。 头文件确实可以理论上可以被所有人引用但是还可能存在顺序问题不了解的头文件具体作用的人很容易就出错 更好的方法是通过premake5.lua把宏定义放到项目的Preprocessing对应的属性栏里如下图所示 VertexArray在跨平台的图形API里并不存在 除了OpenGL 其他的渲染API是根本没有VertexArray这个概念 所以VertexArray这个类要大改 因为它不是一个Render跨平台通用的概念 VertezArray其实是用来描述vertex buffer的其实DX和Vulkan的设计理念更好就是让vertex layout绑定到shader而OpenGL里vertex layout是存在vertex array里vertex arrray会绑定到context上跟shader没有绑定关系 Entity Component System 老生常谈为什么用 ECS Intro to EnTT (ECS) 介绍了一下 entt 的用法 在引擎中包含整个 entt 路径的原因 如果在引擎中不包含整个 entt 路径那么引擎使用到了 entt 中的某个函数的模板功能编译器就只会编译那些模板而在引擎是编译成 lib 提供给 Editor 和 Game 使用的Editor 和 Game 就没有办法使用到 entt 那些没有编译的模板 虽然油管主是这么讲的……但是我觉得把一个库的路径添加到附加包含目录的原因只是为了能够找到库吧…… 虽然我也知道模板是在编译的时候实例化的但是嗯所谓的”添加头文件路径是为了能够获得模板特性“这种说法就很怪 因为首先你要用 entt 的话那你就必须要 include entt 的文件那其实这里要不报错的话你要不就是用完整的相对路径要不就是用附加包含目录总之这和直接一直添加附加包含目录的作用是一样的…… 总之我很混乱 Entities and Components 空结构体 空的结构体在使用的时候会出现问题 比如这里是在 MeshComponent 没有成员的时候出现的问题 我一路看那个输出他最后我感觉是从一个 group 中 get是从一个 storage 中调用 get然后空结构体没有被编译所以 get 这个模板在实例化的时候就出错了 但是实际上我们不会使用一个空的结构体所以这个错误只是一个有趣的……小点 The ENTITY Class AddComponent entt 中的 entity 需要获取到它所在的 registry 才能调用 registry 的 get component 现在我们就创建一个 Entity 类每一个 Entity 存放一个 Scene 的指针在构造函数中初始化Scene 里面有唯一的一个 registry在 Entity 类创建函数里面通过指向 scene 的指针获取到 registry 再 get component 这样我们封装了 entt 的 component 相关的逻辑到 Entity 中对 Entity 直接调用 get component 就好了这样我们不需要知道 reg 然后这个 entity 需要知道 scene 用原始指针当然可以用智能指针也行智能指针的话不能用 shared ptr 因为我们不希望 entity 的存在会延续 scene 的生命周期所以我们应该用 weak ptr Camera Systems 之前他已经写过了一个 OrthographicCamera 现在不知道为什么又写了一个 Camera 类 他应该是为了 ECS 里面的 CameraComponent 我觉得这有点麻烦了说实话 Scene Camera 为 Camera 创建了一个派生类 SceneCamera 然后把 OrthographicCamera 中的一些东西搬到 SceneCamera 中 仍然觉得挺冗余 只是为了配合 CameraComponent 吧 Native Script 创建了一个 NativeScriptComponent其中有一些 std::function在主循环中会调用这些 function 还提供了一个 bind 函数对 function 成员赋值 把 bind 制作成模板类那么编译器在编译的时候根据输入的 T 的类型去找到 T 的对应的函数 NativeScriptComponent 在初始化的时候会调用 bind会指定模板类的类型 所以就相当于我只知道我有一个 NativeScriptComponent假设我不知道这个组件是怎么初始化的我就单单调用这个组件里面存好的 function 就好了 估计这就是他调用 Script 的想法……就是 Script 在初始化的时候从 Script 中取出函数放到 function 成员中 只是这里的例子有点怪就是 function 里面存的都是 C 里面写好的类的成员函数而不是脚本中的函数 应该只是为了演示这个思路才这么做的把 Native Script with virtual function 这个代码变得更奇怪了 NativeScriptComponent 中有一个函数模板 Bind组件存了两个函数指针一个是初始化一个是销毁Bind 中会接收到一个模板类型 T初始化函数中他会创建一个 T 类型的对象然后转换到 ScriptableEntity 类存为 instance销毁函数中删除 instance instance 是 ScriptableEntity 类这个类里面有虚函数 OnCreate OnUpdate 等提供给外部 所以相当于在场景初始化的时候为某个 entity 加上 NativeScriptComponent然后调用 NativeScriptComponent 的 Bind 函数其中要制定 ScriptableEntity 类的某个派生类 然后在主循环中假设我不知道是怎么初始化的我就去找有 NativeScriptComponent 的 Entity找到 NativeScriptComponent然后从 NativeScriptComponent 中获取 ScriptableEntity 类的 instance然后调用 ScriptableEntity 类的虚函数接口 emmmm感觉有点怪 感觉不像 Script Scene Hierarchy Panel 获取到所有 entity对每一个 entity 用 ImGui::TreeNodeEx 画一遍 因为我们在绘制 ImGui 的 TreeNode 的时候知道我们是对哪个 Entity 画的所以我们在画的时候就可以查询一下这个节点是不是被点击了如果被点击了那么就可以告知其他系统当前 UI 上选中的是哪个 Entity 他是用 ImGui::IsItemClicked() 来判断的就很神奇因为他这里查的时候没有指定查的是哪个节点嘛所以我觉得这种写法似乎跟 OpenGL 有点像都是相当于对状态机查比如说我在画这个节点的时候之后我的操作都默认是对这个节点进行的我不太懂没有学过 ImGui Properties Panel 类似略 Camera Component UI 类似略 Drawing Component UI 类似略 Transform Component UI 用矩阵存储Rotation数据是不准确的因为比如我有个绕Z轴旋转7000°的Rotation用矩阵去存储和运算就会存成0到360°范围的值所以这里用Yaml这种文本文件来存储GameObject的Transform(跟Unity一样) 修改Transform组件数据从mat4改成三个向量translation、rotation和scale它这里的旋转还是用欧拉角表示的还没用到四元数(因为目前的2DRenderer只会有绕Z轴的旋转不会有Gimbal Lock)。然后修改相应的使用代码和Inspector代码 这里画 Transform 的方式让我学到了一点 ImGui 画东西的方式……总之就是默认直接往下画如果要加新格式就声明然后就按照这个格式来这个格式结束之后就是按照默认的来堆……哎呀我也说不好但是总之是很简单的堆堆堆只是堆 UI 之前声明 UI 的堆的格式 Adding/Removing Entities and Components UI 类似 Making the Hazelnut Editor Look Good 设置了字体颜色样式 在 ImGui 各个按钮绘制之间设置了间隔 把 DrawComponent 中的 ImGui 的一些通用的绘制样式做成模板传入 lambda 来绘制每一个 Component 独特的部分 Saving and Loading Scenes 可以用JSON来存储场景但是这种格式在merge的时候没有Yaml格式好而且JSON文件很容易写漏掉{}。 yaml-cpp 的用法看一下代码里面是怎么写的就理解了 Open/Save File Dialogs 通过 Windows API出现出搜寻文件的创建可以加载指定路径的 .scene 文件将其反序列化得到 Scene 对象 保存文件同理 Transformation Gizmos ImGuizmo 的使用方法 传入 Manipulate 的是 view 和 proj 矩阵还有物体的 transform 通过 gizmos 操作之后的值会存到传入的 transform 对象中从这个对象中 decompose 出位移旋转缩放赋回物体 Editor Camera 把原本用作EditorCamera的OrthographicCamera和OrthographicCameraController重命名为EditorCamera和EditorCameraController类 并完善EditorCamera类专门负责Viewport的绘制EditorCamera不再是原本的Orthographic Camera而是支持两种模式而且默认为透视投影 添加快捷键让EditorCamera在Viewport里移动、旋转 神秘二次函数在图像论坛上获取的用于根据Viewport窗口大小调整合适的Pan Speed就是用中键来调整Viewport缩放 Multiple Render Targets 现在我们要实现一个鼠标点击场景能够获取到点击到的物体的功能 其实我觉得可以用射线来做但是应该是因为在复杂物体的情况下射线是不准确的吧 比如如果一个物体比较小很难用射线判断到相交或者被一个透明材质的物体挡住或者…… 然后第二个方法就是把 Entity ID 渲染到场景中跟渲染颜色是一样的只是没有 alpha test 和 alpha 混合这样…… 然后我们再输入鼠标在视口中的位置然后用这个视口位置来从贴图中查询值读取像素信息用 glReadPixel 既然我们 Entity ID 渲染到场景中跟渲染颜色是一样的利用深度的所以我们可以在一个 shader 里面同时完成这两件事 但是一般的 shader 的输出只有一个 frag color 这个时候就需要用到 MRT 技术其实就是一个 framebuffer 可以绑定多个颜色附件但只有一个深度附件。创建颜色附件的时候创建到 GL_COLOR_ATTACHMENT0 index然后 frag shader 中写 layout(location 0) out vec4 fragColor0; layout(location 1) out vec4 fragColor1; … 然后使用 glDrawBuffers 传入一个以 GL_COLOR_ATTACHMENT0 为基的数据告诉 opengl 哪一个 layout location index out 对应哪一个 GL_COLOR_ATTACHMENT0 index 然后他这里就是在创建 framebuffer 创建颜色附件和深度附件的时候额外多写了 MRT 的逻辑 然后他还添加了一个多重采样的设置 为鼠标点选准备 FBO 在创建纹理的时候提供一个 RED_INTEGER 的选项 在 frag shader 中设置 layout location 2 out 他这个计算鼠标在视口中的 0 到 1 的坐标的做法可能需要看一下 主要是某一个 imgui 的窗口的边界的确定就很神奇 调用的是 getcursorpos 得到的却是 viewposoffset调用的是 getwindowspos 得到的是 minbound 左上角是 0,0 右下角是 bound 对 y 取 bound.y-y 使得 y 轴反过来左下角是 0,0 右上角是 bound与 opengl 的视口坐标系的方向相符 之后在 commit 7d0ccc4077adaeea53b5ad23a78ffa0849319062 中把获取视口左下角坐标的函数改了改成了 GetWindowContentRegionMin(); 获取左下角GetWindowContentRegionMax(); 获取右上角 清理 FBO 的颜色附件 添加了一个清理 FBO 的颜色附件的函数 鼠标点选 Mouse Picking 如何在绘制物体的时候在Fragment Shader里知道物体的ID有两种直观思路 通过Vertex Shader传入 通过Uniform传入 对于现在的情况第二种方法是不行的因为我们现在是批处理了在一个 drawcall 里面绘制属于不同物体的面所以我们如果传 uniform 的话就会对于不同物体得到相同的一个 uniform 所以还是要通过顶点属性传入 而这个顶点属性的传入需要我们在代码中定义一个包含 entity id 的结构体但是实际上在游戏中我们不需要顶点属性包含 entity id我们只在编辑器中需要所以这个结构体中的 entity id 只在编辑器环境下存在 ok现在实现了绘制的时候要传入 entity id 到顶点属性配合之前做好的 MRTreadpixel鼠标点选就完成了 Clicking to Select Entities 我们在每帧可以获取到鼠标位置用 readpixel 获得 id 纹理上的 id保存到 hovered_entity 中 添加一个事件在鼠标点击时将 selected_entity 设置为 hovered_entity SPIR-V and the New Shader System 在引擎里只写一份Shader但是该Shader能跑在不同的平台上其实也就是引擎的Shader跨平台编译系统更具体来说目前是想让写的OpenGL的shader能够跑在vulkan上 实现Shader的缓存相当于编译出Shader在各个平台上的Binary文件然后利用类似glGetProgramBinary去直接上传Shader从而省去Runtime编译Shader的过程 具体分为以下步骤 介绍SPIR-V (可以把shader翻译为跨平台中间语言的编译器) 介绍github上的shaderc项目可以理解为封装了SPIR-V的github项目 介绍Vulkan SDK以及脚本一键安装Vulkan SDK环境 接入shaderc项目到Hazel里 修改现有的Shader文件以支持vulkan Tip: Vulkan里的depth范围是[-1 1]而OpenGL是[0, 1] 给调用的exe添加命令行参数 UniformBuffer uniform 本质上是告诉 GPU 在这次着色程序渲染的时候创建一个缓冲区 所以 uniform 可以视为 Shader 之外的一个但是配合 Shader 的东西 所以可以把 uniform 的设置抽象出来 这里就是创建了一个 UniformBuffer 基类 OpenGL 的派生类里面是 UBO 的内容 这么一看我才记起来 OpenGL 确实有 UBO怪不得他要提这个抽象 SPIR-V and the New Shader System SPIR-V SPIR-V是一个Shader的编译器它会把Shader编译为intermediate byte code。全称叫Standard Portable Intermediate Representation是一种跨平台的中间语言。 实际使用的时候比如我有一个vulkan用的shader语法是glsl那么怎么在OpenGL上用呢具体步骤如下 可以通过SPIR-V把它编译为Binary文件。基于这个文件通过SPIR-V的跨平台编译器返回一个string此时就是对应的OpenGL版本的shader(也是个文本文件)然后这个 string 当然可以直接通过OpenGL来编译但也可以基于这个文件通过SPIR-V的跨平台编译器获取OpenGL版本的shader编译得到的binary文件可以把它存到磁盘上用的时候通过glGetProgramBinary类似的代码读取Shader。 SPIR-V对各个平台的shader转换的流程图 https://www.khronos.org/spir/ GLSL-glslang-SPIR-V Tools Vulkan SDK 需要的 lib 可见仓库的 premake 的 lua 文件 可以直接下载 Vulkan SDK打开 installer.exe 下载的时候勾选 Debug 工具然后把 premake 的 lua 文件中的 Vulkan SDK Debug 路径改为环境变量的 Vulkan SDK 的路径因为油管主想要的是sdk 去环境变量找debug 相关的 lib 下载到自己的库但是现在我们直接下载都一起了所以一起到环境变量找就行了 Uniform 的写法 OpenGL 中 Uniform Struct 的写法 struct Transform{mat4 Transform; }uniform Transform u_RendererUniforms;但是 Vulkan 不会识别到 他说是不会识别到反射emmm这算反射吗不懂 Vulkan 的写法是 layout(push_constant) uniform Transform{mat4 Transform; }u_RendererUniforms;在 Shader 中使用预编译头 可能我们需要在 Shader 中使用预编译头来处理不同平台上的不同行为 例如 OpenGL 和 Vulkan 的深度范围是不一样的前者是 -1 到 1后者是 0 到 1 float value 0; #if OpenGLvalue value * 0.5 0.5 #endifXXX 已在 XXX.obj 中重新定义 一开始我拉取了 commit 之后构建出现了一堆“XXX 已在 XXX.obj 中重新定义”的错误 我看了一下输出应该是运行时库的错误 yaml-cpp.lib(stream.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MTd_StaticDebug”不匹配值“MDd_DynamicDebug”(EditorLayer.obj 中)于是我把 Editor 和 Engine 的运行时库都改成 Mtd 结果还是有错 2Hazel.lib(spirv_cross.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“MTd_StaticDebug”(EditorLayer.obj 中) 2Hazel.lib(spirv_cross.obj) : warning LNK4099: 未找到 PDB“”(使用“Hazel.lib(spirv_cross.obj)”或在“”中寻找)正在链接对象如同没有调试信息一样 2Hazel.lib(spirv_glsl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“MTd_StaticDebug”(EditorLayer.obj 中) 2Hazel.lib(spirv_glsl.obj) : warning LNK4099: 未找到 PDB“”(使用“Hazel.lib(spirv_glsl.obj)”或在“”中寻找)正在链接对象如同没有调试信息一样 2Hazel.lib(spirv_cfg.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“MTd_StaticDebug”(EditorLayer.obj 中) 2Hazel.lib(spirv_cfg.obj) : warning LNK4099: 未找到 PDB“”(使用“Hazel.lib(spirv_cfg.obj)”或在“”中寻找)正在链接对象如同没有调试信息一样 2Hazel.lib(spirv_cross_parsed_ir.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“MTd_StaticDebug”(EditorLayer.obj 中) 2Hazel.lib(spirv_cross_parsed_ir.obj) : warning LNK4099: 未找到 PDB“”(使用“Hazel.lib(spirv_cross_parsed_ir.obj)”或在“”中寻找)正在链接对象如同没有调试信息一样 2Hazel.lib(spirv_parser.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“MTd_StaticDebug”(EditorLayer.obj 中) 2Hazel.lib(spirv_parser.obj) : warning LNK4099: 未找到 PDB“”(使用“Hazel.lib(spirv_parser.obj)”或在“”中寻找)正在链接对象如同没有调试信息一样所以似乎是 vulkan 的 lib 要求我用 /MDd但是 yaml-cpp 要求我用 /MTd 之后我觉得嗯这个运行时库的设置不是用来链接别人的吗那么 yaml-cpp 没有链接到别人啊所以它的运行时库的设置应该跟着别人来吧 所以我就把 Editor Engine yaml-cpp 的运行时库的设置都改成了 /MDd 就好了 代码中调用 shaderc 编译 glsl 到 spirv 需要创建 shaderc 的 Compiler 和 CompileOptions 对象 shaderc::Compiler compiler; shaderc::CompileOptions options;options 中设置了要编译到 Vulkan 如果shader代码中用了宏定义那么这里还可以写 options.AddMacroDefinition(OpenGL); 编译的话就用到 compiler.CompileGlslToSpv shaderc::SpvCompilationResult module compiler.CompileGlslToSpv(source, Utils::GLShaderStageToShaderC(stage), m_FilePath.c_str(), options);存储也是根据 enum 值来存 shaderData[stage] std::vectoruint32_t(module.cbegin(), module.cend());同时用 ofstream 输出到文件 spirv 编译到 glsl 需要创建 shaderc 的 Compiler 和 CompileOptions 对象 shaderc::Compiler compiler; shaderc::CompileOptions options;options 中设置了要编译到 OpenGL 现在我们经过之间的步骤得到的是 vulkan 的 shader 的二进制文件现在我们要利用它得到 opengl 的 shader 的二进制文件 确实我们是用 glsl 文件得到的 vulkan 的 shader 的二进制文件那为什么不直接用 glsl 一步到位到 opengl 的 shader 的二进制文件……可能是因为油管主想演示这整个流程吧 这里一开始我还有点懵因为我因为 spirv 只是 vulkan 需要的着色器的二进制形式 现在我才明白原来 spirv 在 options 里面设置哪个平台得到的就是哪个平台的着色器的二进制数据 那么现在我们有 vulkan 的 shader 的二进制数据 m_VulkanSPIRV 我们想要 opengl 的 shader 的二进制数据 m_OpenGLSPIRV 但是 spirv 没有直接在两个平台的二进制数据之间转换的功能 所以我们需要用 spirv_cross 将 m_VulkanSPIRV 转换到 glsl 源码然后将得到的源码输入 spirv 生成 m_OpenGLSPIRV spirv_cross 的使用也很简单 spirv_cross::CompilerGLSL glslCompiler(spirv); m_OpenGLSourceCode[stage] glslCompiler.compile();之后将得到的源码输入 spirv 生成 m_OpenGLSPIRV 就是前面我们已经做过类似的了 从编译得到的 spirv 数据中获取 shader 信息 spirv_cross 除了把 spirv 文件转化成源码之外还有一个功能是可以获得源码的数据 将 spirv 的二进制文件输入 spirv_cross 之后编译成源码spirv_cross 就可以根据这个源码获得到 shader 的一些信息 这应该算反射把…… 比如 uniform buffers 有多少啊之类的 在 OpenGL 加载 spirv 二进制数据 直接用 glShaderBinary 和 glSpecializeShader 替换 glShaderSource 和 glCompileShader 其他问题 现在的逻辑是一旦有了二进制 shader 文件就直接读取了但是你不知道这个二进制文件是不是从当前源 shader 编译来的 就是说可能出现版本不一致的问题 果然还是需要 meta 内容浏览器 Content Browser/Asset Panel 层级显示 点击按钮修改路径 string Content Browser Panel - ImGui Drag Drop 显示图标 在这之前实现了图标显示 要考虑到图标的大小图标之间的间隔 因此需要获取到窗口的宽度除以单元格的宽度得到每一行能够显示的图标数量 单元格的宽度等于图标大小 图标之间的间隔 从之前的判断按钮是否点击 if (ImGui::Button(-)) 变为判断图片是否被点击 if (ImGui::IsItemHovered() ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) 其他的没啥了 显示图片按钮之前要先清理颜色不然虽然用的是透明图片但还是有默认底色 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::ImageButton((ImTextureID)icon-GetRendererID(), { thumbnailSize, thumbnailSize }, { 0, 1 }, { 1, 0 }); ImGui::PopStyleColor();鼠标拖拽 在显示 ImageButton 的时候调用 if (ImGui::BeginDragDropSource()) 这个时候我理解的是如果鼠标点击了就会触发 那么这个时候要设置 payload 给 imgui就是鼠标点击时的信息比如点击到的项目所代表的相对路径 然后在别的地方应该调用 if (ImGui::BeginDragDropTarget()) 这个时候是如果鼠标松开了就会触发 其中添加一个获得 payload 的逻辑 这个时候就获得了 content 的相对路径进而获得资源的完整路径可以打开了 这里想要鼠标拖拽到 viewport所以我们在显示 viewport 的 begin 和 end 之间加入这个接受 payload 的代码 Texture for Entities 经过之间鼠标拖拽到 viewport 的操作 现在我们希望鼠标拖拽到组件属性中的一个地方就能设置纹理 同样的写法只是在属性栏显示属性的时候加入接受 payload 的代码 Everything You Need in a 2D Game Engine 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 Rendering Scrpting: 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)的东西碰撞纹理CPU compute Editor相关的工具比如UNDO/REDO系统 UI相关比如Text Rendering可能需要使用signed distance field还有algnment、类似css之类的东西。UI Animation等 Memory Mapping可以用于帮助上传很大的贴图(比如500mb的贴图)到GPU 本地化 stripping 代码剥离 删除未使用或无法访问的代码 例如将 Editor 代码从发布的游戏中剥离 联网 音频 运行时在 Editor 中运行游戏 优化多线程GPU性能检测内存管理 PLAY BUTTON 顶点数组会被插值的 bug Shader里的TextureId应该用uniform以flat形式传输用顶点数组数据的形式会被interpolate 绘制顺序问题导致的 bug 在绘制透明物体的时候如果先绘制前面的透明物体那么前面的透明物体就会先写入 depth后面的透明物体的 depth 没有通过所以不会被绘制那么自然不会进入 alpha blend那么就会出现前面的透明物体完全挡住了后面的透明物体的 bug 游戏模式 Viewport窗口上面绘制了Toolbar一栏里面绘制了Play Button 代码的EditorLayer里存两种Scene对应的PlayMode的状态Editor和Play状态 Scene里的Update函数分为EditorUpdate和RuntimeUpdate函数 窗口的 dock space 消失 我不知道源码为什么会让 docking space 消失……我觉得是有什么函数调用的时候隐藏了然后写到了布局文件中 因为我删了他的一些代码之后就能显示那个小三角形了 然后我再恢复成源码小三角形也没有消失 这个 ImGui 的 docking space 分支果然还是有点问题 2D PHYSICS 显示文件 include 了哪个头文件 可以显示 cpp include 了哪些头文件 需要在Visual Studio里选中cpp文件然后右键点击属性在里面的C±Advanced-Show Includes改为Yes然后直接编译该cpp即可(Visual Studio里使用Ctrl F7可以单独编译一个cpp) Box2D 使用 Box2D 进行物理模拟 在游戏开始的时候遍历 RigidBodyComponent在物理世界中创建 body 如果该 entity 还有 Collider2DComponent就配置 b2FixtureDef 在程序主循环的时候调用物理世界的 update然后遍历包含 TransformComponent 和 RigidBodyComponent 的 entity从 RigidBodyComponent 中获取变换赋给 TransformComponent Universally Unique Identifiers (UUID/GUID) 创建了全局唯一 ID 组件 组件……也行把这或许不是需要展示出来的东西 或者说我觉得这更像是管理资源文件要用到的东西而不是游戏物体中需要的东西…… Playing and Stopping Scenes (and Resetting) 他是在切换到播放模式 OnScenePlay 的时候从 m_EditorScene 复制一份场景到 m_ActiveScene 结束播放模式的时候m_ActiveScene 重新赋为 m_EditorScene那么旧的 m_ActiveScene 指向的运行时内容就释放掉了 评论中有人说可以不用这么复制而是正常结束播放模式然后在结束播放模式的时候将场景文件重新反序列化回场景 emmmm我觉得这也可以但是万一在播放模式的时候用户又把场景文件删除了那岂不是永远得不到开始时的场景了 所以还是在内存中一直存着 m_EditorScene 比较合理 Rendering Circles in a Game Engine 添加了一个画圆的功能 配套的有shader批处理提交部分要存储批处理的 VAO VBO组件定义序列化与反序列化中的新增部分主循环中处理画圆组件的逻辑…… 我其实就觉得这些函数定义有点冗余…… Rendering Lines in a Game Engine 同上 Circle Physics Colliders 添加一个 Component 的流程……感觉很多都是重复的 感觉可以用反射生成代码来实现…… Visualizing Physics Colliders 在 OnUpdate 中加一个逻辑找所有的长方形的 collider绘制长方形找所有的圆形的 collider绘制圆形 emmm我在这里突然发现这个 EditorLayer 没有调用 Renderer2D 的 BeginScene 然后之后才发现在 Scene 里面是完整地调用了 BeginScene 和 EndScene 但是在 EditorLayer 的 OnUpdate 里面只有 EndScene 其实 EndScene 就是一次提交 所以就嗯我感觉这个设计有点反直觉 Return of the Game Engine Series Physics Simulation Mode 多做了一个物理模拟的模式 runtime 模式中会 update script模拟物理物理模拟模式中只会模拟物理 两个模式的不同就在于渲染2D时用的摄像机可能不同runtime 用的是 scene 中的相机物理模拟用的是 editor 中的相机 Community Issues/PRs and Merging Branches 使用可变参数模板来完成重复性的工作 他这个写法还挺有意思的 templatetypename... Component static void CopyComponent(entt::registry dst, entt::registry src, const std::unordered_mapUUID, entt::entity enttMap) {([](){auto view src.viewComponent();for (auto srcEntity : view){entt::entity dstEntity enttMap.at(src.getIDComponent(srcEntity).ID);auto srcComponent src.getComponent(srcEntity);dst.emplace_or_replaceComponent(dstEntity, srcComponent);}}(), ...); }这里用了一个逗号表达式来展开可变参数 那么逗号表达式的第一个表达式是一个生命周期仅存在于逗号表达式的 lambda 还有这种利用可变参数列表把所有类型包起来的写法我也是第一次见很优雅 templatetypename... Component struct ComponentGroup { };using AllComponents ComponentGroupTransformComponent, SpriteRendererComponent,CircleRendererComponent, CameraComponent, NativeScriptComponent,Rigidbody2DComponent, BoxCollider2DComponent, CircleCollider2DComponent;Vulkan1.3 Vulkan1.3 的下载Debug 库是和 SDK 一起下载的所以不用引擎自己下载的 终于不用我每次拉取的时候都手动把 Dependencies 里面的 VulkanSDK Debug 等等修改了 GLFW 只调用 GLFW_PRESS RELEASE GLFW 只调用 GLFW_PRESS RELEASE GL_REPEAT 是在回调函数中的…… 有点没懂 将从属性面板添加组件的函数替换成函数模板 调整 include 的顺序 将引擎内的头文件放在最前面 摄像机接受事件的需要播放模式的条件 Editor 在运行的时候有多个摄像机每个摄像机用于不同的模式摄像机接受事件需要处于特定模式 类型的 size 为 0 时 asset 我很好奇什么类型的 size 会为 0 我记得空结构的 size 至少为 1 才对 在程序主循环中少使用一些函数 https://github.com/TheCherno/Hazel/pull/539 他把 std::filesystem::relative 放到了点击事件中 这样就不用每一个循环中都使用一次这个函数 噢……涉及到 OnImGuiRender 的这种 Update 函数确实要小心 鼠标选择穿过透明物体 在 editor 渲染的 shader 中 discard 透明物体的像素就不会写入这样id framebuffer 也不会写入透明物体的 id 定义某一个类的 hash 模板的特化 stl 里面的 unordered_map 底层是用哈希实现的所以如果要用自定义类型作为 key需要对这个自定义类型实现哈希定义 首先在这个自定义类型的 cpp 文件里面声明一下 hash然后写一个特化hash 结构体里面有一个括号运算符重载这样hash 这个结构体就可以在 stl 中直接被使用 namespace std {template typename T struct hash;templatestruct hashHazel::UUID{std::size_t operator()(const Hazel::UUID uuid) const{return (uint64_t)uuid;}};}添加 C# 获取 Mono 文件 添加 C# 到游戏引擎中的教程 https://nilssondev.com/mono-guide/book/introduction.html JetBrain DotPeek 可以查看 .NET 的 DLL 中包含的代码 https://www.jetbrains.com/decompiler/ 下载 Mono https://www.mono-project.com/download/stable/#download-win 这里获得的是 Mono 要用到的 .NET的库文件 Mono 仓库 https://www.mono-project.com/download/stable/#download-win 在 Windows 上从源码构建 Mono 克隆 mono git checkout 到最近一次 Release 的 commit 打开克隆下来的 mono 源码中的 msvc 文件夹中的 sln 可以看到这个解决方案中有很多项目 https://nilssondev.com/mono-guide/book/introduction/building-mono.html 中并没有说我们要构建哪个项目 于是我们可以自己打开项目的属性页看常规属性-目标文件名打开编辑其中有完整构建名的预览 可以看到 libmono-static 的构建的目标文件是 libmono-static-sgen同理我们也可以这么看 libmono-dynamic 虽然 https://nilssondev.com/mono-guide/book/introduction/necessary-files.html 中说我们最需要的可能只是 mono-2.0-sgen.lib但是我们现在知道libmono-static 是构建成 liblibmono-dynamic 是构建成 dll这两个的区别仅此而已 所以如果我们要静态链接到 C#就用 libmono-static-sgen.lib 就好了 编译 libmono-static 项目 要选中 libmono-static 项目然后按 Ctrl Shift B 编译这样编译出来会在 msvc 中产生 build 文件夹里面放目标文件还会一起产生一个 include 文件夹里面放构建相关的头文件 单纯右键点击项目然后点编译不会产生这个 include 文件夹我也很奇怪为什么网上搜不到相关的描述…… Debug 和 Release 都构建一遍方便我们自己的 Debug 和 Release 项目构建 之后提到了编译 static 项目可能需要定义某些预编译头当然这和在 vs 中编译是一样的 于是我们可以去看看 libmono-static 项目中的预编译头……但是 libmono-static 项目是没有源文件的所以它不会被识别为一个 cpp 项目所以在项目属性中不会显示 cpp 相关的属性……emmm 所以这就很神奇我还挺想知道一个库没有源文件那么编译它的时候到底是在编译什么 在游戏引擎的项目的放第三方库的目录中新建一个文件夹 mono拷贝进去编译产生的 include 文件夹在里面创建一个 lib 文件夹拷贝进去编译产生的 build/sgen/x64/lib 中的内容应该包含 Debug 和 Release 链接到 Mono 更改 premake 文件添加 includeDir libraryDir 在需要使用到 Mono 的项目的 premake lua 文件中添加 link 初始化 Mono 点开 Mono 下载的库文件一般是下载到 C 盘我们在要用到 C# 的项目的 vxproj 根目录下创建 mono/lib把 C:\Program Files\Mono\lib\mono\4.5 这个文件夹拷贝进去 那么我们在初始化 Mono 的代码中可以写 mono_set_assemblies_path(mono/lib);相关的教程可见 https://nilssondev.com/mono-guide/book/first-steps/runtime-setup.html 然后是初始化 JIT这个指针是要存储的 MonoDomain* rootDomain mono_jit_init(MyScriptRuntime);if (rootDomain nullptr) {// Maybe log some error herereturn; }// Store the root domain pointer s_RootDomain rootDomain;C# 会先通过编译器转换为 MSIL JIT 在运行时按需将 MSIL 转换为机器语言。第一次用到某个方法的时候转换为机器语言存在内存的某个地方 stub存根是指针吗……之后再次调用这个方法的时候就直接取存根 托管执行的过程 https://learn.microsoft.com/en-us/dotnet/standard/managed-execution-process 然后是创建应用域……就不再死复制教程了 应用域 https://learn.microsoft.com/en-us/dotnet/framework/app-domains/application-domains 感觉像一个独立的沙盒 创建一个 C# 类库 我们的目标是创建一个 C# 的程序集它里面包含着游戏引擎相关的代码提供给 C# 的应用域使用 那么这个游戏引擎的 C# 程序集就是需要我们创建一个 C# 类库的项目编译出来 dll然后调用 LoadCSharpAssembly 这个函数的内容可以看教程 实际拉取的 Hazel 代码中我觉得可能要先编译这个类库然后再编译整个解决方案 也就是说 Hazel 依赖于 Hazel-ScriptCore 所以我觉得这位博主说的依赖关系可能是错的…… https://blog.csdn.net/alexhu2010q/article/details/126960468 反正我要是不这么做的话编译整个解决方案的时候编译 Engine 项目的时候还没有编辑 C# 类库所以还没有输出程序集 dll就会报错 感觉 premake 应该存在那种控制编译顺序的功能吧……一下子没找到之后再说吧 之后编译的时候会出现一些找不到 Windows 的库的问题我们需要链接到一些 Windows 的库 在 Dependencies.lua 中定义一些 Windows 的 lib 然后在引擎中 link 具体可以看仓库 ScriptEngine.cpp 定义的静态变量 static ScriptEngineData* s_Data 在 Visual Studio 的监视面板中会被识别为 Renderer2D.cpp静态变量 static Renderer2DData s_Data 这就很神奇应该是 Visual Studio 中的一个错误吧 我自己写的时候避免这个全局变量重名的情况就好了 实际上静态变量的作用域是编译模块这两个 cpp 不相关所以它们不是一个东西 测试程序集加载 https://nilssondev.com/mono-guide/book/first-steps/testing-assembly-loading.html 从程序集获取 MonoImage从 MonoImage 获取类型定义表 MonoTableInfo 整个表每一行代表一个类型列方向上存了类型有关的信息 嗯……就很神奇 我原本以为这个 col 是存数据的结果我一看 MONO_TYPEDEF_SIZE 居然是 enum那实际上 col 这个数组的大小只有六个……用一个 uint32_t 来存数据……很强 然后之后的 MONO_TYPEDEF_NAMESPACE 也是同一套 enum就很强 在 C 端获取 C# 的类实例化 C# 的类调用 C# 类的方法 教程已经很清楚了 https://nilssondev.com/mono-guide/book/first-steps/classes.html https://nilssondev.com/mono-guide/book/first-steps/methods.html Calling C from C# 前面的部分实现了在C调用C#里的任何内容包括调用Method和获取Property和Field等现在需要反过来实现在C#里调用C提供的API。其实有很多可选的做法 使用Platform Invoke (P/Invoke) 这种做法更适合C#工程去使用C的dll时使用我之前工作时就是用Unity去通过这种方式调用寻路导航插件的dll的 借助Mono的Internal Call C和C#的中间语言C/CLIEA的寒霜引擎就是用的C#作为编辑器C作为Runtime它们使用C/CLI进行交互不过这玩意儿是只支持Windows的 P Invoke https://mark-borg.github.io/blog/2017/interop/ cpp 中对函数声明 __declspec(dllexport) //----- ImageHandler.h ------- #pragma once#ifdef IMAGEHANDLER_EXPORTS # define IMAGEHANDLER_API __declspec(dllexport) #else # define IMAGEHANDLER_API __declspec(dllimport) #endifextern C IMAGEHANDLER_API float ValidateImageColourInterop(char** inputParams, int* numInputParams, char** outputParams, int* numOutputParams); extern C IMAGEHANDLER_API float ValidateImageResolutionInterop(char** inputParams, int* numInputParams, char** outputParams, int* numOutputParams);在 C# 中声明函数使用 DllImport 标签 using System; using System.Runtime.InteropServices;public class ColourValidator {[DllImport(ImageHandler, CallingConvention CallingConvention.Cdecl, CharSet CharSet.Ansi)]static extern int ValidateImageColourInterop(ref IntPtr inParams, ref int inParamsSize, ref IntPtr outParams, ref int outParamsSize);public bool Validate(){// ...int rc ValidateImageColourInterop(ref inputParamsRoot, ref inputParamsSize, ref outParamsRoot, ref outParamsSize);// ...} } Internal Call 目前的Scripting系统对应的.NET和Assembly的代码其实是Hazel的一部分所以并不适合用P/Invoke(而且Hazel引擎目前是用的static linking)。这里的Internal Call相当于告诉.NET Runtime我有一些Native Functions你可以调用。 cpp 里面的函数中的参数要设置成 C# 的类型定义了函数之后要使用 mono_add_internal_call 在 C# 注册 #define HZ_ADD_INTERNAL_CALL(Name) mono_add_internal_call(Hazel.InternalCalls:: #Name, Name)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){HZ_CORE_WARN(Value: {0}, *parameter);*outResult glm::normalize(*parameter);}static float NativeLog_VectorDot(glm::vec3* parameter){HZ_CORE_WARN(Value: {0}, *parameter);return glm::dot(*parameter, *parameter);}void ScriptGlue::RegisterFunctions(){HZ_ADD_INTERNAL_CALL(NativeLog);HZ_ADD_INTERNAL_CALL(NativeLog_Vector);HZ_ADD_INTERNAL_CALL(NativeLog_VectorDot);}C# 中声明函数使用 MethodImplAttribute 标签 [MethodImplAttribute(MethodImplOptions.InternalCall)] internal extern static void NativeLog(string text, int parameter);这么看两种在 C# 中调用 Cpp 的方法都挺简单 虽然Mono对string这个特殊变量做了个处理但对于函数的参数和返回值是其他struct和class的情况就比较复杂了在C里class和struct基本是一样的而C#里却完全不一样这里涉及到的点有 如何在C#进行值传入在C里也创建一份数据对象此时的传入值在C#里为struct在C里可以是struct或class各自的栈上各有一份数据 如何在C#进行引用传入即指针传入在C里输入的也是指针托管堆和非托管堆共享同一份数据 相同类在managed和unmanaged代码上存在时如何保证内存分布一致(涉及Marshalling 编组) 怎么传struct或class的数组、怎么传普通primitive的数组 返回类型也可以是值类型或指针类型这里应该和传参是一样的就不重复提了后面注意下语法就行了 总的来说C#与C交互处理传参和返回值时分两种情况 通过指针进行不存在值拷贝的过程C#的struct对应的是ref和out关键字C#的class直接传对象即可 通过Copy值类型进行 两种情况下只要接触到了struct和class都需要保证C和C#定义相同内存结构对于class会复杂一些后面再深入研究 C#调用C的自定义struct为参数的静态函数 由于C#里struct和class的区别这里具体分为两种如果是struct那么需要先定义两端的Struct写法为 // 对于POD类型, 只要它俩内存Layout相同即可 // C# struct Vector3 {float x;float y;float z; }// cpp struct float3 {float x,y,z; }static void LogFloat3(const float3* input) {LOG(input); }// method需要用ClassName::的形式 mono_add_internal_call(MyNamespace.Program::PrintVector3, LogFloat3);假设我要从C#这边传入一个Vector3再让C打印出来那么传入Vector3的引用就行了此时csharp这边传入的参数为ref A c这边传入的参数为A*代码如下所示 namespace MyNamespace {public class Program{public void PrintVector3(ref Vector3f input){PrintVector3(input);}[MethodImplAttribute(MethodImplOptions.InternalCall)]extern static void PrintString(ref Vector3f input);} }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);Calling C from C# C# 到 C 的调用的抽象 这里把 C# 到 C 的调用抽象成了 ScriptEngine ScriptClass ScriptInstance 三个部分 一开始看有点复杂之后还好 ScriptEngine 分为两个部分第一个部分是与 Mono 的配置有关的例如 InitMono, LoadAssembly, ShutdownMono把应用域啊assembly 啊存储到全局静态变量 s_Data第二部分是管理 C 内部的带有 C 的 ScriptComponent 类的 Entity 的 ScriptClass 类里面存了 nameSpace 和 name这样构造函数里面就调用 mono_class_from_name存储 MonoClass 所以可以说 ScriptClass 是用来存储 MonoClass 的还封装了 MonoClass 的接口例如 mono_class_get_method_from_name以及与存储了应用域的 ScriptEngine 配合实例化的接口 ScriptInstance 里面存了 MonoObject初始化参数包括 ScriptClass初始化时从 ScriptClass 获取并存储一些我们已知要从 C# 中获取的方法比如 .ctor, OnCreate, OnUpdate 等也存了调用 Method 的接口 这些安排都是和 Mono 接口相匹配的比如 mono_runtime_invoke 就需要 MonoObject MonoMethod那么 ScriptInstance 就刚好他存储了 MonoObject还在初始化的时候存储了 MonoMethod emmm 当然这个安排获取我也有点别的想法比如我觉得 MonoMethod 可以存在 ScriptClass 而不用每个 ScriptInstance 都存一套 Method…… 初始化时获取并存储 C# 中的继承于某一已知命名空间名类名的类的所有派生类的信息 ScriptEngine 与 Mono 的配置有关的部分中的 LoadAssembly 就是教程中的获取 Assembly dll 的路径然后加载。它还有一个初始化的步骤 LoadAssemblyClasses 是他需要知道 C# 中的类型于是跟教程中的 Test Assembly 一样我们可以从 assembly 获取 MonoImage再从 MonoImage 获取 MonoTableInfo再从 MonoTableInfo 的每一行获取 nameSpace 和 name有了这两个我们再用 mono_class_from_name 就能获取 MonoClass而我们已知我们的 C# 中的基类是 Hazel 命名空间基类名为 Entity所以我们同样也能获得基类的 MonoClass两个 MonoClass 可以进行比较可以判断是否是子类所以我们可以得到 C# 中定义的 Entity 的所有派生类所以我们可以存一个表 EntityClasseskey 是 C# 中 Entity 的派生类型的全称value 是我们用 nameSpace 和 name 构造的自定义的 ScriptClass 类 这样我们就完成了初始化时获取并存储 C# 中的继承于某一已知命名空间名类名的类的所有派生类的信息 ScriptComponent 初始化时根据已知的 命名空间名类名 创建 MonoObject 这个信息有什么用呢我们 C 的 Entity 的 ScriptComponent 在逻辑上需要创建 C# 的实例并且拥有它所以从 ScriptComponent 的角度来说它应该获取到一个 MonoClass 然后实例化它 ScriptComponent 是用户在编辑器里配置的用户这个时候应该只知道 C# 里面的类的类名所以我们约定用户往 ScriptComponent 里面写 命名空间名.类名因此我们就需要一个 string 命名空间名.类名 - MonoClass 的映射EntityClasses 就是做这件事 实际 Hazel 代码中ScriptComponent 获取到 EntityClasses 之后竟然是把他存到了一个 map EntityInstances 里面key 为 Entity 的 IDvalue 为 ScriptInstance ScriptComponent 实例化 C# 的 Entity 的派生类获得 MonoObject 时创建的是派生类的 MoboObject还要调用 C# 的 Entity 基类的构造函数 ScriptComponent 实例化 C# 的 Entity 的派生类获得 MonoObject 时创建的是派生类的 MoboObject还要调用 C# 的 Entity 基类的构造函数 之前我也一直没上代码但这个没上代码确实不太好说…… C# 中的游戏逻辑所在的类都是需要继承 Entity 的这样才能在初始化的时候被放入 EntityClasses这样 ScriptComponent 初始化的时候根据自己被写入的 string 才能从 EntityClasses 中找到对应的 ScriptClass 才能用 ScriptClass 实例化一个 MonoObject public class Player : Entity{...ScriptComponent 去 Mono 的反射信息中找的时候是用 ScriptComponent 里存的 Entity 的派生类的名字去找的的ScriptComponent 创建 MoboObject 的时候创建的 Entity 的派生类的 MonoObject void ScriptEngine::Init(){...// Retrieve and instantiate classs_Data-EntityClass ScriptClass(Hazel, Entity, true);...}ScriptInstance::ScriptInstance(RefScriptClass scriptClass, Entity entity): m_ScriptClass(scriptClass){m_Instance scriptClass-Instantiate(); // 创建的是派生类的 MonoObjectm_Constructor s_Data-EntityClass.GetMethod(.ctor, 1); // 但是还要调用一个基类的有参构造函数m_OnCreateMethod scriptClass-GetMethod(OnCreate, 0);m_OnUpdateMethod scriptClass-GetMethod(OnUpdate, 1);// Call Entity constructor{UUID entityID entity.GetUUID();void* param entityID;m_ScriptClass-InvokeMethod(m_Instance, m_Constructor, param);}}但是还要调用一个基类的有参构造函数就是我们在实例化一个 MonoClass 的时候我们用的是 mono_object_new这个时候我们是没有传入参数的所以这个函数是调用的这个派生类的无参构造函数 MonoObject* ScriptEngine::InstantiateClass(MonoClass* monoClass){MonoObject* instance mono_object_new(s_Data-AppDomain, monoClass);mono_runtime_object_init(instance);return instance;}调用的这个派生类的无参构造函数的之前根据继承原理调用的基类的是基类的无参构造函数 但是我们又需要保证调用 C# 的 Entity 基类里面的那个构造 ID 的函数C# 的类才能知道自己的 ID public class Entity{internal Entity(ulong id){ID id;}所以我们还需要调用一次 C# 的基类 Entity 的有参构造函数 这里也看出来我们在 C# 里面不能自己去 new 继承了 Entity 的派生类如果自己去 new自己就找不到 C 中对应的 Entity ScriptComponent 想要有多个 C# 类的实例怎么办 我还在想ScriptComponent 想要有多个 C# 类的实例怎么办……因为现在是不允许一个 Entity 有多个 ScriptComponent 嘛所以只能是 ScriptComponent 想要有多个 C# 类的实例 或许用 unordered_multimap 来做 id 到 ScriptInstance 的映射 Scene OnUpdate 时获取所有包含 ScriptComponent 的 Entity调用 ScriptEngine::OnUpdateEntityScriptEngine::OnUpdateEntity 又调用 ScriptInstance 里存储的从 C# 获得的 OnUpdate 这就没什么好说了 Component C# 中的 Entity 类的实例对应到 C 中 ScriptComponent 创建出的 MonoObjectC 里面有一个 UUID, ScriptInstance 的 map 注意这里不是直接存 UUID, MonoObject 而是存 UUID, ScriptInstance因为 ScriptInstance 是对 MonoObject 和他相关的接口的封装 但是现在如果我只是在 C 里面知道这个 map我在 C 端可以根据 UUID 取 ScriptInstance但是我在 C# 端却不能根据 MonoObject 取 UUID不能取 UUID 也就意味着不能取到 C 的 ScriptInstance 所以我们还需要在 C 调用 C# 的类的构造函数的时候传入 C 端的 Entity 的 UUID 具体就是C 端的 Entity 有 ScriptComponent这个 ScriptComponent 要实例化 C# 的类这个 ScriptComponent 自己知道自己属于哪个 Entity 也就知道自己的 UUID可以传参数给 C# 那现在 C# 中的 Entity 就知道自己的在 C 端分配的 UUID 了 那现在我们就可以用 internal call 在 C# 端调用 C 的函数C# 中的 Entity 传入 UUID获得自己对应到 C 的 Entity 这样我们就可以做很多事情了比如 HasComponent GetComponent 在 C 初始化的时候要 RegisterComponent其中输入了 C 中定义了一些 Component 的类型在函数中转化成 C# 中的命名空间.类名的 string 输入到 mono_reflection_type_from_name 获取 MonoType 在 C# 端我们会定义与 C 里面定义的 Component 的名字同名的C# 的 Component这样我们在 C 初始化 RegisterComponent 中用 mono_reflection_type_from_name 应该能获取到对应名字的 MonoType 其实这些 C# 的 Component 里面只是一些属性get set 都是 internal call调用 C 的提供的 get set 感觉这个 C# 的组件定义里面这些属性还有 C 中提供给 C# 的 internal call 都是可以代码生成的hhh 如果能获取到 MonoType说明 C# 端是成功定义了与 C 里面定义的 Component 的名字同名的 C# 的 Component那么我们可以再初始化一个 map MonoType, std::functionbool(Entity)因为我们在 C 里面是 类型 - 类型名 - C# 中的命名空间.类名 - mono_reflection_type_from_name - MonoType所以我们同时知道 MonoType 和 C 类名如果我们使用模板编程这里的 C 类型就是模板 T我们可以放入 [](Entity entity) { return entity.HasComponentT(); HasComponent 向 C 端传入 UUID 和 C# 的 Type 类型C# 的 Type 类型对应到 C 就是 MonoReflectionType C 中提供给 C# 的 internal call 的 HasComponent 可以通过 mono_reflection_type_get_type 获得 MonoType就是从这个 map 获得判断某个 Entity 是否具有某个组件的函数 C# 的 GetComponent 不是单纯的初始化因为我们现在 C# 的 Component 与 C 的 Component 是有对应关系的所以我们在 C# 中创建 C# 的 Component 类的时候我们需要首先从 C# 这里出发去问 C我自己对应到 C 的 Entity 中有没有这个 C 的对应的 Component有的话我才创建 C# 中的 Component 类 如果不做这个检查C# 直接就 new 的话那么 C# 中的组件其实也只是 get set 用 internal call 调用 C 而已调用了 CC 函数里面本来需要 C 的 Entity 具有特定的 C 的 Component现在没有的话就会出错 当然这里还有一个问题是嗯现在这个写法假设 C 的 Entity 中有这个 C 的对应的 Component那么我 C# 每次调用 GetComponent 都在 new 啊我感觉是不是应该第一次 new之后就返回之前 new 出来的对象…… public T GetComponentT() where T : Component, new() {if (!HasComponentT())return null;T component new T() { Entity this };return component; }将游戏 Assembly 和 核心 Assembly 分离 它刻意要把游戏的 Assembly 和 核心的 Assembly 分离做成两个类库就很强 这样的话就相当于游戏的 Assembly 是玩家写的核心的 Assembly 是引擎写好的不用动的 这样的话每次玩家更新脚本只用重新编译玩家写的 Assembly 就好了 C 获取 C# 类的属性 看代码就行了很简单 现在的获取属性还是需要游戏开始运行游戏开始运行时才会开始创建 ScriptInstancePanel 这里才能获取到 ScriptInstance才能 GetFieldValue 这是很显然的运行时C 这边才开始遍历 ScriptComponet 才开始创建继承了 Entity 的 C# 类的实例这个时候才有 C# 的对象才有对象里的字段 但是 Unity 那里是可以在编辑器模式下更改字段的默认值的所以我觉得那应该是从类得到的字段表然后存一个字段的默认值的 map在游戏开始时从这个 map 赋值 将 C# 字段的值复制到运行时 Storing/copying C# field values to runtime 很好就是我之前的想法 他设置了一个标志表示当前场景是否在运行如果是游戏在运行不是运行物理模拟那么就显示 ScriptInstance 的 get field如果拖动了那么就对 ScriptInstance SetValue如果游戏没有在运行那么如果拖动了 imgui 的 float那么就获取到这个 entity 对应的 field map 然后把值存到 field map 中 序列化 ScriptComponent 的 fieldMap 现在我们已经对每一个有 ScriptComponent 的 Entity 创建了一个 fieldMap用于在运行时把 fieldMap 中的值赋给 C# 类的实例 现在我们要序列化这个 fileMap 还是很简单的序列化某一个 ScriptComponent 的时候我们知道它存储的 C# 类的类名根据这个类名我们可以获得 MonoClass 根据 MonoClass 我们可以获得 Fields遍历 Fields 我们可以获得 name 和 fieldType 我们手写 fieldType 和 C type 的对应关系就可以从 field 中获取到 C 类型的值写入到 ymal 流 反序列化也是一样的 Added FindEntityByName and Entity.As to retrieve script class instance 现在我们要在一个 C# 的 Entity 的派生类的实例中获取另一个 Entity 的派生类的实例 我们暂时没有 CreateEntity 的方法我们现在可以做的是C# 根据一个 string name 到 C 中去找 Entity IDC 返回一个 Entity ID然后 C# 中根据这个 ID 创建一个 Entity 类的对象 public Entity FindEntityByName(string name) {ulong entityID InternalCalls.Entity_FindEntityByName(name);if (entityID 0)return null;return new Entity(entityID); }我们知道了 IDC 中有 UUID, ScriptInstance 的 mapScriptInstance 里存了 MonoObject 我们就可以从 C# 传 UUID 到 CC 去 map 里面找到 ScriptInstance获取 MonoObject 然后传到 C#C# 里面获取到 Object 之后拆箱成 Entity 的派生类 所以这里嗯新建一个 Entity 的话……感觉有点怪 就是说本来我们都知道 C# 世界中一个 Entity 的派生类对应 C 世界中的一个 ScriptInstance 中的一个 MonoObject 的 现在你要是允许这样创建一个 Entity 的话虽然你已经检查了 确保这个 Entity 可以对应到 C 中的某个 ScriptInstance 但这会不会造成多个 Entity 对应到 ScriptInstance public T AsT() where T : Entity, new() {object instance InternalCalls.GetScriptInstance(ID);return instance as T; }这也说明了在 C# 和 C 之间传对象的时候如果是要传 C# 的 class 的话那就不得不传 Object 了就不得不拆箱了 如果是要传原始类型的话不用编组如果是传自定义结构体的话就要编组了 C# 程序集 Assembly 重新加载 在 Unload 某个 AppDomain 之间先 mono_domain_set 当前的 RootDomain 为 false 此外没有了这个 commit 就是知道了这一点。他存下了 Core Assembly 和 Game Assembly 的位置然后提供了 imgui 的 menu 按钮和 imgui 检测按键去重新加载 Assembly Unload Assembly 的话就是 Unload 当前 AppDomain因为加载 Assembly 就是加载到当前 AppDomain 的 重新加载就是先 Unload 然后再 Load Core Assembly 和 Game Assembly 这个 commit 还没有添加文件检测 然后它也没有在 Reload 的时候更新 C 到 C# 的映射时要利用的反射信息例如 std::unordered_mapstd::string, RefScriptClass EntityClasses; std::unordered_mapUUID, RefScriptInstance EntityInstances; std::unordered_mapUUID, ScriptFieldMap EntityScriptFields;因为你完全可能 Reload 一个与旧的 Assembly 完全不同的 Assembly导致之前加载的所有的类都失效或者类不变但是类里面的字段全变了也有可能…… 这些都是可以做的…… 文件监视 fileWatch 使用了一个 filewatch 库来检测文件是否发生更改 https://github.com/ThomasMonkman/filewatch 因为这个 filewatch 是单独开一个线程的所以这就涉及到了文件监视线程和主线程的互斥问题 通过约定文件监视线程发送请求到 queue主线程从 queue 中拿请求的方式可以完成文件监视线程到主线程的通信 而主线程从 queue 中拿请求的时候文件监视线程不能发送请求所以这是两个互斥的操作 互斥可以用一个 std::scoped_lock 方便地实现对当前作用域上锁并且还是 RAII 的 // Application.h std::mutex m_MainThreadQueueMutex;// Application.cpp void Application::SubmitToMainThread(const std::functionvoid() function) {std::scoped_lockstd::mutex lock(m_MainThreadQueueMutex);m_MainThreadQueue.emplace_back(function); }void Application::ExecuteMainThreadQueue() {std::scoped_lockstd::mutex lock(m_MainThreadQueueMutex);for (auto func : m_MainThreadQueue)func();m_MainThreadQueue.clear(); }void Application::Run() {while (m_Running){...ExecuteMainThreadQueue();...} }暂停和步进 Pausing and Stepping 在内置的 OnUpdate 中设置条件判断 if (!m_IsPaused || m_StepFrames-- 0) 通过后才能 Update 这样如果是暂停时那么需要 m_StepFrames 为正才能 Update 如果点一次步进按钮会给 m_StepFrames 加 1就能达成点一次步进按钮世界步进一次的效果 添加 C# debugging 使用 mono_debug_open_image_from_memory 加载符号表 自定义 Buffer 和 ScopeBuffer 在加载文件的时候经常需要创建一个数据指针接收数据然后我们要记得销毁掉这个指针 所以我们可以自定义一个 RAII 风格的 ScopeBuffer 类它的生命周期是当前作用域离开了作用域就会被销毁 游戏工程的抽象类 Project 把游戏工程抽象出来 每一个游戏工程有自己的名称、启动场景、资源目录、C# 脚本目录 引擎的内容浏览器要依赖于当前工程创建因为只有知道了工程才能知道资源目录 复制时的 bug 复制 Entity 的时候会复制名字 如果复制的时候一直传引用的话那么最后得到的是 Clone submodule 时的错误 我在拉取到 MSDF 那个 commit 的时候 clone 不了 submodule E:\Hazel-master\Hazelgit submodule update --init --recursive fatal: No url found for submodule path Hazel/vendor/msdf-atlas-gen in .gitmodules查了一下 https://stackoverflow.com/questions/4185365/no-submodule-mapping-found-in-gitmodule-for-a-path-thats-not-a-submodule 我把 .gitmodule 中的路径中的 \\ 改成了 / 就好了 Windows真是神奇 error MSB8013: This project doesn’t contain the Configuration and Platform combination of Debug|Win32 看了这个我的 vcxproj 里面确实都是 x64这一点时可以信任 premake 的 https://stackoverflow.com/questions/33156131/x64-build-error-msb8013-this-project-doesnt-contain-the-configuration-and-pla 看了这个我的项目文件夹里面没有 Directory.Build.props 这个文件…… https://github.com/flutter/flutter/issues/129997 之后我重启电脑就好了……神奇的 Visual Studio 字体 用了 https://github.com/Chlumsky/msdf-atlas-gen 我根本看不懂…… AssetManager 说实话没太看懂……
http://www.dnsts.com.cn/news/227596.html

相关文章:

  • 网盘做网站空间企业名词解释
  • 网站建设 军报wordpress前端页面模板
  • 58招聘网站官网想学做宝宝食谱上什么网站
  • 网站启用cdn加速网站建设的工具是
  • app推广一手单吉安做网站优化
  • flash网站标题和网址网站页面图片布局如何设计
  • 自己建设个小网站要什么手续好网站的标准
  • 自助式网站制作织梦校园招生网站源码
  • 精品课程网站建设方案成都著名设计师
  • 西安有关做网站的公司html5基础知识
  • 网站建设报价单模板wordpress技术站主题
  • 无锡手机网站制作界面设计是什么
  • 做壁纸的专业网站永州市建设工程质量安全监督站官方网站
  • 张家港建网站的公司网站建设题库
  • 商城网站页面设计有了网站怎么写文章
  • 开展建设文明网站活动交通门户网站建设
  • 邢台做网站改版网站的优化承诺
  • 站长之家网页模板下载什么网站可以自己接工程做预算
  • 信阳网站建设哪家好广州力科网站建设公司
  • 小说网站怎么做权重杨凌规划建设局网站
  • vs2017网站开发中国光大国际建设工程公司网站
  • 外贸网站建设谷歌推广下面哪些不是网页制作工具
  • 用jsp源码做网站自己建网站要多少钱
  • 如何通过网站后台修改网站上海做得好的网站建设公司
  • 深圳企业网站制作设计方案昆明网站建设是什么意思
  • 什么是网站栏目标题制作网页app
  • 公共网站怎地做网站图片怎么做缓存
  • 淘宝网站建设类别长春净月潭建设投资集团网站
  • 建网站是怎么造成的学校的网站怎么做的好
  • dw超链接自己做的网站容桂做pc端网站