夜溅樱的茶馆 - 游戏开发
type: channel
link: http://srin.top/index.php/category/GameDev
description: 游戏开发相关博客
items: Unity 常用程序片段
items: Unity宏整理
items: Unity 录制视频
items: UE5 中用 Python 接口创建 Level Sequence 与设置 TriggerEvent
items: 一个由 BlueprintPure 函数引起的 “Bug”
items: UE5中 uDraper 插件无法编译 C++ 工程的修复
items: UE 中脚部 IK 使用总结
items: UE4技术总结——委托
items: FString与标准库string的转换
items: UE4 中的 Subsystem

Unity 常用程序片段
type: item
date: 2024-03-13T14:41:58+08:00
description: Unity 常用程序片段摘要此文主要记录在 Unity 开发过程中常用到的一些程序片段，因为不值得单独写一篇文章，因此当作杂项全放在此文中。不定时更新。时间相关计算时间戳这段代码适用于旧版本引擎，新版本引擎可以直接通过 System.DateTime.UtcNow.ToUniversalTime().ToUnixTimeMilliseconds() 获取时间戳。using System; namespace Utilities { public static class TimeUtils { public static long GetCurTimestamp() { var startTime = new DateTime(1970, 1, 1, 0, 0, 0); var timeStamp = Convert.ToInt64((DateTime.Now.ToUniversalTime() - startTime).TotalSeconds); return timeStamp; } } }参考unity c#获取时间戳

Unity宏整理
type: item
date: 2024-03-13T11:54:43+08:00
description: Unity宏整理整理一下常用的宏，免得老需要跑去文档查。这里只记录平时用得比较多的。宏定义 if 的语法非常简单，直接上例子：#if UNITY_EDITOR Debug.Log("Unity Editor"); #elif UNITY_IOS Debug.Log("Unity iOS"); #elif UNITY_ANDROID Debug.Log("Unity Android"); #else Debug.Log("Any other platform"); #endif和平时的 if 逻辑差不多，注意一下写法即可。常用编辑器环境限制相关宏UNITY_EDITOR此宏修饰的代码只有在编辑器环境下运行和编译，不分是哪个平台的编辑器环境。UNITY_EDITOR_WIN此宏修饰的代码只有在 Windows 平台下的编辑器环境才会运行/编译。UNITY_EDITOR_OSX此宏修饰的代码只有在 MacOS 平台下的编辑器环境下才会运行/编译。运行平台限制相关宏UNITY_STANDALONE_OSX此宏修饰的代码只有在打包目标平台、运行平台是 MacOS 平台情况下运行/编译。UNITY_STANDALONE_WIN此宏修饰的代码只有在打包目标平台、运行平台是 Windows 平台情况下运行/编译。UNITY_IOS注意不要打成UNITY_iOS。限制代码只在打包目标是 iOS 平台的时候编译，只在 iOS 平台运行。UNITY_IPHONE废弃的宏，取而代之的是 UNITY_IOS，效用和 UNITY_IOS 一样。UNITY_ANDROID限制代码只在打包目标是安卓平台的时候编译，只在安卓平台运行。UNITY_64只有在打包目标平台/运行平台是 64 位 的时候运行/编译。编辑器版本限制相关宏这类宏用于限制代码在某个版本的编辑器上才能运行，通常 Unity 版本格式是 X.Y.Z，对应的宏是 UNITY_X、UNITY_X_Y、UNITY_X_Y_Z，例如说：#if UNITY_2019 // do something here // 限制引擎版本为 2019，只要 X 是 2019 的版本都可以执行这个宏修饰的代码 #endif #if UNITY_2019_4 // do something here // 限制引擎版本为 2019.4，只要 X.Y 是 2019.4 的版本都可以执行整个宏修饰的代码 #endif #if UNITY_2019_4_14 // do something here // 限制引擎版本为 2019.4.14，只有 2019.4.14 版本的引擎可以执行 #endif其他版本的同理，此外还有限制比 X.Y.Z 版本新的宏 UNITY_X_Y_OR_NEWER，版本号的格式和前面限制版本的宏格式一致，只是后面多了个 _OR_NEWER。编译限制相关的宏ENABLE_MONO当使用的脚本后端是 Mono 时 Unity 会定义此宏。ENABLE_IL2CPP当使用的脚本后端时 IL2CPP 时 Unity 会定义此宏。NET_2_0编译脚本的目标 API 兼容性是 .NET 2.0 的时候 Unity 会定义此宏。NET_2_0_SUBSET编译脚本的目标 API 兼容性是 .NET 2.0 Subset 的时候 Unity 会定义此宏。NET_LEGACY编译脚本目标 API 兼容性是 .NET 2.0 或者 .NET 2.0 Subset 的时候 Unity 会定义此宏。NET_4_6目标 API 兼容性是 .Net 4.x 的时候会定义此宏。NET_STANDARD_2_0目标兼 API 容性是 .NET Standard 2.0 的时候会定义此宏。NET_STANDARD_2_1目标兼 API 容性是 .NET Standard 2.1 的时候会定义此宏。NET_STANDARD目标兼 API 容性是 .NET Standard 2.1 的时候会定义此宏。ENABLE_INPUT_SYSTEM在 Player Settings 中开启了 Input System 选项时会定义此宏。ENABLE_LEGACY_INPUT_MANAGER在 Player Settings 中开启了 Input Manager 选项时会定义此宏。如何自定义宏Player Settings 里面设置将自己想要添加的宏输入到上图中红框内即可。通过脚本设置PlayerSettings.SetScriptingDefineSymbols可以在 C# 脚本中调用此函数来设置自定义的宏，此函数定义是：public static void SetScriptingDefineSymbols(BuildTargetGroup …

Unity 录制视频
type: item
date: 2023-08-09T11:20:00+08:00
description: Unity 录制视频概述最近偶尔需要录制视频给策划展示模型动画效果，所以总结一下 Unity 里面录制视频的方法。OBS 录屏法这个方法很简单，但是确实可以快速录制视频。而且此方法适用范围广，不仅限于录制Unity的视频，还可以用在直播推流等场景。坏处是相当于直接捕获显示器上的内容（至于是整个显示器显示的内容还是显示器部分区域的内容则是看你的设置来定），没法直接录制到目标分辨率。如果策划就是想要录制分辨率是 1920x1080 视频，那么没法直接在编辑器环境中直接录制出来。需要独立一个游戏运行窗口保证窗口分辨率在 1920x1080，或者打包然后让应用运行在 1920x1080 分辨率环境下，相对比较麻烦。内置 Recorder 录制法这个方法复杂一点，但是还是比较简单的。Unity 要求版本是 2018.4 以上。首先我们先从 Package Manager 中导入 Unity Recorder：如果 Package Manager 中没显示 Unity Recorder，可能需要勾选 Enable Preview Packages 之类的选项（理论上不会不显示，毕竟已经出了正式版）。导入之后就可以看到类似这样的窗口：如果没有这个录制操作窗口就需要在 Window 里面打开：普通录制直接手动点击打开的录制操作窗口中的“START RECORDING”按钮，或者红色的播放按钮就可以开始录制。只是录制简单的游戏画面内容的话基本够用了，如果还需要了解各个参数的作用可以参考官方文档。自动化使用 Unity Recorder 的好处在于能够自动化，例如批量读取动画数据文件，每个文件录制并保存为单独的视频文件/图片/动图等。但是官方文档没有特别详细说明如何在脚本中调用录制等功能，只是提供了一些例子脚本。这些脚本可以在 Package Manager 中导入：建议直接看 MultipleRecordingsExample.cs 脚本的内容，这个脚本里面就举了一个同时创建 3 种录制器的例子，分别将录制的内容输出为视频、图片、Unity 内置的 Anim。下面直接看代码，内容简单易懂：#if UNITY_EDITOR using System.IO; using UnityEditor; using UnityEditor.Recorder; using UnityEditor.Recorder.Input; namespace UnityEngine.Recorder.Examples { /// /// This example shows how to set up a recording session via script. /// To use this example, add the MultipleRecordingsExample component to a GameObject. /// /// Enter the Play Mode to start the recording. /// The recording automatically stops when you exit the Play Mode or when you disable the component. /// /// This script saves the recording outputs in [Project Folder]/SampleRecordings (except for the recorded animation, /// which is saved in Assets/SampleRecordings). /// public class MultipleRecordingsExample : MonoBehaviour { RecorderController m_RecorderController; void OnEnable() { var controllerSettings = ScriptableObject.CreateInstance<RecorderControllerSettings>(); m_RecorderController = new RecorderController(controllerSettings); var mediaOutputFolder = Path.Combine(Application.dataPath, "..", "SampleRecordings"); // animation output is an asset that must be created in Assets folder var animationOutputFolder = Pa…

UE5 中用 Python 接口创建 Level Sequence 与设置 TriggerEvent
type: item
date: 2022-10-19T20:51:00+08:00
description: UE5 中用 Python 接口创建 Level Sequence 与设置 TriggerEvent本文内容可能只能在 UE5 下有用，未在 UE4 环境下实验过。背景遇到了一个美术需求，需要批量读取一段动画，制作成 UE 中的 Level Sequence，然后给动画添加几个 Event Track。随后，需要在 Event Track 中添加 Trigger Event，设置插件 uDraper 布料的缓存数据路径。总之，最终效果如下：无视上图中红色的报错部分，这是因为我截图的时候没有打开对应的关卡，Sequence 找不到相关的引用。打码部分是动画名字，工程内容不太方便暴露所以打码至于为什么非得要用 Event Track 来设置路径，而不是在 Actor 的 Component 相关属性中直接设置路径，然后添加到 Sequence 中，只能说这是 uDraper 插件的问题，直接设置会弹出个弹窗说“路径缺少 xxxx 文件”（因为该路径只有缓存数据而没有布料相关的数据），但是如果在 Event Track 中通过 Event 帧调用函数Cache(缓存路径)设置就没这个问题，能够正常读取缓存文件。因此，就只能这么干了。可能有点绕，其实就是我需要在动画的第一帧调用 uDraper 提供的蓝图函数 Cache，并传入DirectoryPath类型的对象来指定布料缓存数据路径。另外，如果读者不太清楚或者没试过在 Level Sequence 中触发 Event，可以看看官方介绍文档，里面详细说明了如何在 Sequence 中添加 Event 帧，在指定的帧调用函数，从而实现在某个特定时刻执行某种行为、打开门、生成角色等功能。此文档中的操作流程和我们在代码中相关流程是一致的，因此后面我不会解释代码中为什么会出现某个步骤。Python 脚本直接进入正题，我们先看一眼完整的 Python 脚本：def main(): # 拿到场景编辑 subsystem level_editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) # 从关卡中获取 actor，注意获取的时候确认名字就叫这个，最好选中后用指令打印出来名字 # 因为在编辑器的场景预览中显示的名字不一定就是引擎设置的真名 actor = unreal.find_object(level_editor.get_current_level(), "guzhuang_C_1") camera_actor = unreal.find_object(level_editor.get_current_level(), "Camera_0") # 获取组件 cloth = unreal.find_object(actor, "cloth") # 这里 asset_list 是一个列表，里面全是 animation 的路径，类似于 /Game/Anim/dance.dance for i in asset_list: # 取个名字 cur_anim = i.split("/")[-1].split(".")[0] asset_tools = unreal.AssetToolsHelpers.get_asset_tools() # 传入参数创建新的 sequence，seq_path 是一个全局变量，配置了 sequence 的保存路径 lvl_seq = unreal.AssetTools.create_asset( asset_tools, asset_name=cur_anim, package_path=seq_path, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew()) # _fps 也是全局配置，保存了 sequence 的帧率 frame_rate = unreal.FrameRate(numerator=_fps, denominator=1) lvl_seq.set_display_rate(frame_rate) # 创建 actor binding actor_binding = lvl_seq.add_possessable(actor) # 添加场景中的 camera，其实可以在脚本中创建新的，但是我发现创建 camera 的话在脚本执行完后新建的 # Camera 会一直保留在场景中，所以最终还是选择直接用场景中现有的 camera cam_cut_track = lvl_seq.add_master_track( unreal.MovieSceneCameraCutTrack) cam_cu…

一个由 BlueprintPure 函数引起的 “Bug”
type: item
date: 2022-09-05T16:48:00+08:00
description: 一个由 BlueprintPure 函数引起的 “Bug”这篇文章会很短，其实主要是记录一下写出来的一个bug。背景背景主要是测试一个功能，用到了一个 BlueprintPure 函数：总体函数的逻辑大概是这样的：结果测试发现，运行效果如下：分析实际上这是因为 BlueprintPure 函数是每次在节点中访问一次就会执行一次。上面的程序逻辑中，我们执行了两次访问操作（一次是转换成字符串，一次是设置到名为 GuardBuying 的 UStruct 成员变量中），正是因为这两次访问操作，导致生成了两个不一样的随机数。所以只需要将逻辑改成：即只读取一次，并将得到的结果保存到变量中，后面要用的时候都访问该变量即可避免多次执行 BlueprintPure 类型的随机数生成函数，每次都获得一个新的随机数。

UE5中 uDraper 插件无法编译 C++ 工程的修复
type: item
date: 2022-08-19T15:52:00+08:00
description: UE5中 uDraper 插件无法编译 C++ 工程的修复uDraper是用来做布料模拟的插件。现在出现的问题是安装了 uDraper 之后无法编译 C++ 工程。经典报错就是：Expecting to find a type to be declared in a module rules named 'Vitrum' in UE5Rules, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null. This type must derive from the 'ModuleRules' type defined by Unreal Build Tool.解决方案第一步 更新引擎的插件首先找到 uDraper 的安装目录，在安装目录下找到插件目录：然后找到对应引擎编号的插件。这里假定使用的引擎是正式版 5.0 ：将这个文件夹下面的 Draper 文件夹复制，并粘贴到引擎的 Plugin 目录。注意在粘贴前删除掉引擎 Plugin 下原来的 Draper 文件夹：第二步 添加定制化的 UnrealBuildTool找到前面插件目录下的 Installation 文件夹：将以下三个文件复制到 [UE5 Root]\Engine\Binaries\DotNET\UnrealBuildTool：复制好之后，编辑 [UE5 Root]\Engine\Build\BatchFiles\Build.bat，并将文件中的 UnrealBuildTool.dll 替换成 UnrealBuildTool.DraperHook.dll：然后修改 [UE5 Root]\Engine\Config\BaseEngine.ini，将 UnrealBuildTool.exe 替换成 UnrealBuildTool.DraperHook.exe替换完成之后理论上就应该可以正常编译 C++ 工程了。

UE 中脚部 IK 使用总结
type: item
date: 2022-07-13T16:49:00+08:00
description: UE 中脚部 IK 使用总结本文主要关于如何在 UE 中配置 Foot IK。其背后的算法、原理因为涉及内容较多，后面会单独另外写一篇。UE4 中 通过 Two Bone IK 进行 脚部 IK 的配置UE4 的动画蓝图中有多个专门给 IK 用的动画节点，我们比较常用的 IK 动画节点是 Two Bone IK。下面内容主要关于如何使用 Two Bone IK 动画节点来实现基本的脚部 IK。此处不包含后期4.26新引入的 Control Rig 内容（避免和后面 UE5 部分重复），只纯粹通过动画蓝图动画节点 Two Bone IK 实现。Two Bone IK 只适合比较简单的骨骼脚部骨骼结构（即只有脚踝、膝盖、大腿3个节点），不适合比较复杂的多个骨骼组成的情况（例如大腿-大腿part1-大腿part2-大腿part3-膝盖-膝盖part1-膝盖part2-膝盖part3-脚踝，这类构造）。如果遇到到需要在复杂脚部骨骼上使用的情况，那么就需要在 UE 中先自行创建虚拟骨骼，将结构简化成大腿-膝盖-脚踝的结构，然后每帧复制原模型骨骼的旋转值到虚拟骨骼，接着通过 Two Bone IK 对虚拟骨骼进行 IK 计算，计算好之后再把虚拟骨骼的位置复制回模型对应的骨骼上。这个过程会比较繁琐，而且容易写错。接下来我们看一个用 Two Bone IK 节点实现 IK 的例子。这个示例使用了官方第三人称 demo 来二次开发。运行 demo，可以看到原版 UE4 官方第三人称 demo 没有设置脚部 IK ：小白人在楼梯这类地方脚可能会悬空，而不是身体向下移动，让两只脚都踏在台阶上。接下来我们在动画蓝图中引入 Two Bone IK 节点并指定好参数即可配置好 Two Bone IK。我们先看看Two Bone IK 节点暴露出来可以配置的参数：IKBone: 就是要 IK 处理的目标骨骼，通常是骨链末端的骨骼，例如脚踝或者手腕之类。这里传入骨骼名字即可；Allow Stretching：允许骨骼被拉伸，这个选项如果勾选上，在 IK 目标点太远的时候，会允许骨骼被拉伸，从而保证目标点能够到达（有点类似橡筋人的效果）；Maintain Effector Rel Rot: 如果勾选了 Take Rotation from Effector Space 那么这里的设置会无效。主要用来保证被 IK 作用的末端骨骼能够保持在原来的局部旋转角度。通常不会勾选；Allow Twist: 是否允许关节调整位置（通常都勾上，不会改），不允许的话就需要修改 Twist Axis， 指定关节可以调整的轴。例如如果 Twist Axis 中只有 X 轴为 1 ，那么关节只可以在 X 轴（这个模型膝盖关节的X轴对应上下）上进行调整（UE中对应的是）。如果设置的 Twist Axis 有问题的话可能会出现以下效果：上图便是只允许在 Y 轴上调整，而膝盖关节的 Y 轴对应的是前后，因此关节只能够前后移动而不被左右移动，导致与脚踝脱节；Enable Debug Draw: debug 用，勾选上可以看到 Effector 位置和 Joint Target 设置的位置，主要调试用；Effector Loaction Space: Effector 使用的空间，例如是 Component Space 还是 World Space 等；Take Rotation from Effector Space: 如果勾选上此选型，那么末端骨骼的旋转值会从 Effector 取；Effector Target: Effector 的作用目标；Effector Location: Effector 的位置，其实就是末端骨骼要运动到的目标位置；Joint Target Location Space: Joint Target 使用的空间，例如是 Component Space 还是 World Space 等；Joint Target: Joint Target 作用目标，用来指定某个骨骼作为关节、控制 IK Bone 的父骨骼；Joint Target Location: Joint Target 倾向运动的目标位置，在膝盖往外拐或者内拐的时候修正膝盖朝向用；未修正：修正后：重点其实是在 Effector 和 Joint Target 的设置上。Two Bone IK 节点封装得比较好，只需要设定好参数，传入 Effector Target 目标位置就可以实现 脚部 IK。配置脚部 IK首先在动画蓝图中创建两个 Two Bone IK 动画节点，并分别对左右脚进行配置，同时对 Pelvis 骨骼进行位置调整，即对模型整体位置进行调整：其中 Two Bone IK 动画节点参数配置如下图所…

UE4技术总结——委托
type: item
date: 2022-06-15T10:17:00+08:00
description: UE4技术总结——委托在开始前先说明，这部分内容主要基于UE 4.26.2版本1编写，因此读者可能需要注意时效性。如果您对源代码感兴趣，可以直接阅读引擎的DelegateCombinations.h、Delegate.h以及相关代码。因为是一个非常基础，时不时会用到的功能，所以这里就不介绍使用场景了，直接进入正题。一、定义首先，官方定义如下2：委托 是一种泛型但类型安全的方式，可在C++对象上调用成员函数。可使用委托动态绑定到任意对象的成员函数，之后在该对象上调用函数，即使调用程序不知对象类型也可进行操作。复制委托对象很安全。你也可以利用值传递委托，但这样操作需要在堆上分配内存，因此通常并不推荐。请尽量通过引用传递委托。同时，根据官方文档，虚幻引擎支持3种类型的委托：单播委托多播委托事件动态委托之所以说是3种，是因为事件实际上在现在的版本中差不多就是多播委托（当然，实际上还是有些许不同的，主要是函数调用权限和多播不同，但是实际上也没有措施保证函数被不是拥有者的对象调用，因此读者只需要理解为多播委托即可）3。而且在UE的4.26.2版本源码中已经标明，事件类型的委托将会在后面更新的版本移除掉：因此，我们主要重点还是放在单播、多播、动态委托上，事件不会进行详细说明。同时，UE4中存在由基本委托组合起来的委托，但是在介绍组合的委托之前我们先看看这3种基本委托。接下来我们先简单看看该怎么用。顺带一提，这里我默认读者知道如何在C++中实现委托，如果您还不清楚，那么建议阅读文末参考中列出的的文章4（了解即可）。二、用法2.1 声明与调用委托UE4中的委托都通过宏定义来声明，随后就可以通过宏定义声明的委托来声明对应的委托变量，实际使用的时候会通过将函数绑定到委托变量来使用。2.1.1 单播委托单播委托只能绑定一个函数指针，执行委托的时候也只能触发一个函数；单播委托绑定的函数可以有返回值，这点和多播委托不同；2.1.1.a 声明// 无返回值函数的委托 // 无参数 DECLARE_DELEGATE(DelegateName); // 1个参数 DECLARE_DELEGATE_OneParam(DelegateName, Param1Type); // <Num>个参数，最多到9个 DECLARE_DELEGATE_<Num>Params(DelegateName, Param1Type, Param2Type, ...); // 有返回值 // 无参数 DECLARE_DELEGATE_RetVal(RetValType, DelegateName); // 1个参数 DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type); // 多个参数，最多到9个 DECLARE_DELEGATE_RetVal_<Num>Params(RetValType, DelegateName, Param1Type, Param2Type, ...);一个简单的声明单播委托的例子：// 直接用宏定义在顶部声明就可以了 DECLARE_DELEGATE(FLearningDelegate); class XXX_API ALearnDelegateActor : public AActor { GENERATED_BODY() public: // ... 省略 public: // 单播委托带有UPROPERTY宏，不能添加BlueprintAssignable标识符，动态多播才可以声明BlueprintAssignable FLearningDelegate FTestDelegate; }2.1.1.b 绑定在绑定函数之前我们先要声明委托和委托变量：// 单播无参数的委托，其他类型的单播委托如此类推 // 这行通常放在头文件的上方，类定义之外，毕竟是宏 DECLARE_DELEGATE(FSingleDelagateWithNoParam); // 用上面声明的委托声明委托变量 // 这里放在类定义中，作为一个属性进行定义 FSingleDelagateWithNoParam SingleDelagateWithNoParam;然后我们就可以绑定函数了，绑定函数的API有很多种，但是最常用的还是BindUObject，因此这里以BindUObject举例：// ADelegateListener::EnableLight的定义类似于void ADelegateListener::EnableLight()，没有参数，也没有返回值 // 这个绑定假设是在类里面绑定的，所以用了this，实际上可以是别的UObject SingleDelagateWithNoParam.Bi…

FString与标准库string的转换
type: item
date: 2022-06-15T10:14:01+08:00
description: FString与标准库string的转换FString Tmp = TEXT("String For Test"); // FString 转 std::string std::string test_str(TCHAR_TO_UTF8(*Tmp)); // std::string 转 FString FString Tmp2(test_str.c_str());

UE4 中的 Subsystem
type: item
date: 2022-06-15T10:14:00+08:00
description: UE4 中的 Subsystem在游戏开发过程中我们往往需要创建一系列的工具来辅助我们开发，例如 UI 管理工具，各类导表工具。在 UE4.22 之前我们只能够自己编写单例，并且自己管理生命周期。或者直接将管理游戏的工具编写进 GameInstance 中。但是随着代码量的增加，GameInstance 将会变得难以维护。在 4.22 版本发布了之后，我们可以直接将工具写在 Subsystem 中，让引擎帮我们自动管理工具类的生命周期，不再需要自己维护工具的生命周期或者修改引擎的类（如 GameInstance）。在 Subsystem 出现之前的黑暗时代我们往往需要一个全局的，生命周期是在整个游戏进行的过程中一直存在的单例，而如果你想要在 UE4 里面实现一个单例，那么你需要用到以下代码：UCLASS() class HELLO_API UMyScoreManager : public UObject { GENERATED_BODY() public: // 一些公用的函数或者Property UPROPERTY(EditAnywhere, BlueprintReadWrite) float Score; static UMyScoreManager* instance; UFUNCTION(BlueprintPure,DisplayName="MyScoreManager") static UMyScoreManager* Instance() { if (instance == nullptr) { instance=NewObject<UMyScoreManager>(); instance->AddToRoot(); } return instance; } UFUNCTION(BlueprintCallable) void AddScore(float delta); }; // 还得要记得在.cpp头部给一个初始值 UMyScoreManager* UMyScoreManager::instance = nullptr这就对新人很不友好了（又一个不让新人碰 C++只让写 Lua 的原因），UE4 的实现比较难看懂，而且容易出错。例如很多人会忘记加上instance->AddToRoot();，如果不记得加上，那么刚刚生成的对象可能会被 GC 掉，调用的时候会导致崩溃。而且用这种方式创建的单例会在 Editor 模式下继续存在，所以运行预览和停止预览之后并不会销毁，下一次预览的时候里面的数据可能还是上一次运行的数据。如果想要处理这个问题，就需要自己手动加上Initialize()和Deinitialize()函数，手动调用，自己管理生命周期。或者是另一种方法，直接把单例写进UGameInstance的子类里面，然后在UGameInstance的Init和Shutdown里面进行创建和销毁。但是即便是这样也需要手动为每一个单例类写一遍，很容易出错，也不容易维护。总而言之，不管是什么样的实现方法，UE4 客户端开发都得要自己管理好自己写的单例类的生命周期，心智负担极大。所以官方推出了 Subsystem，并自己用在了 UE4 的部分组件的开发中（如 VaRest，官方用 Subsystem 制作了 REST API 插件），方便引擎开发、客户端开发人员对引擎或者游戏做扩展、插件，同时不用自己操心生命周期的问题。Subsystem 时代为什么使用 Subsystem用 Subsystem 的好处：不需要自己管理生命周期，引擎自动帮你管理，而且保证和指定的类型（目前只有 5 种）生命周期一致；官方提供蓝图接口，能够很方便地在蓝图调用 Subsystem；与UObject类一样，可以定义UFUNCTION和UPROPERTY；容易使用，只需继承需要的 Subsystem 类型就能够正常使用，维护成本低；更模块化，而且可以迁移某个 Subsystem 到其他游戏项目使用；所以为了代码更加方便维护与移植，还是使用 Subsystem 编写需要用到的工具比较好。Subsystem 简介传统美德，先附上官方的介绍：Subsystems in Unreal Engine 4 (UE4) are automatically instanced classes with managed lifetimes. These classes provide easy to use extension points, where the programmers can get Blueprint and Python exposure right away while avoiding the complexity of modifying o…