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宏整理整理一下常用的宏，免得老需要跑去文档查。这里只记录平时用得比较多的。宏定义 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 targetGroup, string defines); public static void SetScriptingDefineSymbols(BuildTargetGroup targetGroup, string[] defines);defines 要传入的是用分号隔开的宏，例如XXX;YYY，或者宏的文本数组。总结实际内容很简单，完整的 Unity 内置的宏可以到官方文档去查，这里就不多作介绍了。参考Unity 官方文档：Conditional compilationPlayerSettings.SetScriptingDefineSymbolsForGroup
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 = Path.Combine(Application.dataPath, "SampleRecordings"); // Video var videoRecorder = ScriptableObject.CreateInstance<MovieRecorderSettings>(); videoRecorder.name = "My Video Recorder"; videoRecorder.Enabled = true; // 设置格式，除了 MP4 还支持 WebM、MOV videoRecorder.OutputFormat = MovieRecorderSettings.VideoRecorderOutputFormat.MP4; // 比特率，主要影响录制出来的视频质量，如果想要好点可以设置为 Medium 或者 High videoRecorder.VideoBitRateMode = VideoBitrateMode.Low; // 设置游戏分辨率 videoRecorder.ImageInputSettings = new GameViewInputSettings { OutputWidth = 1920, OutputHeight = 1080 }; // 设置保留音频 videoRecorder.AudioInputSettings.PreserveAudio = true; // 设置输出的路径、文件名 videoRecorder.OutputFile = Path.Combine(mediaOutputFolder, "video_v") + DefaultWildcard.Take; // Animation // 这里是录制并输出 anim 格式的文件 var animationRecorder = ScriptableObject.CreateInstance<AnimationRecorderSettings>(); animationRecorder.name = "My Animation Recorder"; animationRecorder.Enabled = true; // 这里其实是创建了一个球体来录制，如果是希望录制指定对象，那么最好还是自己传要录制 // 的对象，例如通过 [SerializeField] 传 var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); // 指定录制对象是前面创建的球体，并且包括其子物体 animationRecorder.AnimationInputSettings = new AnimationInputSettings { gameObject = sphere, Recursive = true, }; // 添加录制的内容，这里是录制 Transform 组件的内容 animationRecorder.AnimationInputSettings.AddComponentToRecord(typeof(Transform)); animationRecorder.OutputFile = Path.Combine(animationOutputFolder, "anim_") + DefaultWildcard.GeneratePattern("GameObject") + "_v" + DefaultWildcard.Take; // Image Sequence // 这里创建录制照片用的 Recorder var imageRecorder = ScriptableObject.CreateInstance<ImageRecorderSettings>(); imageRecorder.name = "My Image Recorder"; imageRecorder.Enabled = true; // 输出 PNG 模式 imageRecorder.OutputFormat = ImageRecorderSettings.ImageRecorderOutputFormat.PNG; // 录制 Alpha 通道，我理解是如果需要录制透明背景的话这个设置时必须为 true imageRecorder.CaptureAlpha = true; // 这里是设置输出的路径、文件名 // DefaultWildcard.Take 相当于第几个 session // DefaultWildcard.Frame 当前 frame id，通常是四位数显示，前面用0补齐，例如0001 imageRecorder.OutputFile = Path.Combine(mediaOutputFolder, "_png", "image_v") + DefaultWildcard.Take + "." + DefaultWildcard.Frame; imageRecorder.imageInputSettings = new CameraInputSettings { Source = ImageSource.MainCamera, OutputWidth = 1920, OutputHeight = 1080, CaptureUI = true }; // Setup Recording // 传入前面创建的设置 controllerSettings.AddRecorderSettings(videoRecorder); controllerSettings.AddRecorderSettings(animationRecorder); controllerSettings.AddRecorderSettings(imageRecorder); // 变成受代码、手动操作控制，另外的模式分别是 SingleFrame、 // FrameInterval、TimeInterval，和面板里面的参数是对应的 // 根据自己需要来选即可 controllerSettings.SetRecordModeToManual(); // 录制时候游戏运行的目标帧率 controllerSettings.FrameRate = 60.0f; // 这个可以忽略，如果设置为 true 那么会打出更详细的 log RecorderOptions.VerboseMode = false; // 准备录制需要的内部数据，具体做啥可以先不理解 // 只需要记得在开始录制之前先执行这个指令 // 其实就是读取 Setting 中传入的一堆 RecorderSetting // 根据 Setting 创建对应的 Recorder Session m_RecorderController.PrepareRecording(); // 开始录制 m_RecorderController.StartRecording(); } void OnDisable() { // 停止录制 m_RecorderController.StopRecording(); } } } #endif脚本头部的注释已经写得很清楚了，只需要将这个脚本放到场景中的某个物体上就可以开始执行脚本内容进行录制。其他部分的代码我都加上了注释，我们自己写自动化脚本的时候对照着这个脚本抄就好。总结简单总结了一下 Unity Recorder 的使用方法，解析了官方例子 MultipleRecordingsExample.cs，平时如果遇到需要制作脚本录制视频的场合就可以对着官方例子写自己需要的录制逻辑。参考官方 Unity Recorder 文档
UE5 中用 Python 接口创建 Level Sequence 与设置 TriggerEvent
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_cut_section = cam_cut_track.add_section() # 添加 camera section cam_binding = lvl_seq.add_possessable(camera_actor) cam_binding_id = lvl_seq.make_binding_id( cam_binding, unreal.MovieSceneObjectBindingSpace.LOCAL) cam_cut_section.set_camera_binding_id(cam_binding_id) # 添加动画 loaded_asset = unreal.AnimSequence.cast( unreal.EditorAssetLibrary.load_asset(i)) frame_num = loaded_asset.get_editor_property( 'number_of_sampled_keys') frame_rate = loaded_asset.get_editor_property('target_frame_rate') # 设置 sequence 有内容的范围 lvl_seq.set_playback_start(0) lvl_seq.set_playback_end(frame_num) unreal.LevelSequenceEditorBlueprintLibrary.refresh_current_level_sequence() ### 添加动画 track ### anim_track = actor_binding.add_track( unreal.MovieSceneSkeletalAnimationTrack) # 添加动画 anim_section = anim_track.add_section() anim_section.set_range(0, frame_num) anim_section.params.animation = loaded_asset # 设置动画范围 cam_cut_section.set_range(0, frame_num) ### draper settings ### # cloth cloth_binding = lvl_seq.add_possessable(cloth) cloth_track = cloth_binding.add_track(unreal.MovieSceneEventTrack) cloth_section = cloth_track.add_event_trigger_section() cloth_section.set_range(0, frame_num) # 其实今天的重点在这里 cloth_channel = cloth_section.get_channels()[0] cloth_cache_binding = unreal.SequencerTools.create_quick_binding( lvl_seq, cloth, "Cache", False) cloth_new_event = unreal.SequencerTools.create_event(lvl_seq, cloth_section, cloth_cache_binding, ["(path=\"D:/UGit/XiaoLan_PC/content/XiaoLan/Effects/Character_Effects/BoWuGuan/ZhengChangShuoHua/cloth\")"]) cloth_channel.add_key(time=unreal.FrameNumber( 0), new_value=cloth_new_event) # 此处省略对 skirt 和 waist 的操作，因为和上面 cloth 的步骤一样 # 保存最终生成的 seq unreal.EditorAssetLibrary.save_loaded_asset(lvl_seq, False) unreal.log("=== INFO: Seq Creation is Completed ===")比较简短，但是我们还是来看看具体做了什么，以及涉及到的相关接口、类。查找场景中的 Actor首先 level_editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)，这里通过unreal.get_editor_susystem函数（相关文档）获取了 Subsystem unreal.LevelEditorSubsystem（相关文档）。这里其实就是获取了场景编辑器 Subsystem 后面方便我们通过这个 subsystem 对场景中的 Actor 进行访问甚至修改。顺带一提，其 Python 调用函数可以想象成在蓝图中调用函数，实际上确实也差不太多，都是通过反射实现的，所以蓝图能调用、访问 Python 都可以调用。在获得了 Level Editor Subsystem 之后，我们就可以调用 unreal.find_object 函数，在当前打开了的场景中寻找到我们需要绑定到 sequence 的 actor 。这里需要注意一下，find_object中传入的 actor 名字一定要确认是引擎标识的名字，而不是在 Level Editor 中看到的名字（例如我遇到过在场景中物品名称叫做guzhuang2，实际上引擎中记载的名字是guzhuang_C_1），最好在场景编辑器中通过 Python(REPL)，选中 actor 并执行：actor_system = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) actor = actor_system.get_selected_level_actors()[0] print(actor)确认 actor 的真实名称以及其是否保存在当前场景中（有些 actor 看起来是在当前场景中实际上可能是别的场景的 actor 的引用，可能是因为直接复制了别的场景的 actor 并粘贴到当前场景下），如果名字不对或者不是保存在当前场景中那么无法通过上面的unreal.find_object(level_editor.get_current_level(), "guzhuang_C_1")方法找到需要的 actor。除了寻找 actor，实际上find_object也可以用来获取 actor 身上挂载的组件，例如上方例程中就通过find_object来获取名为cloth的组件。在获取到我们需要的 actor 之后就可以开始 sequence 的创建了。创建新的 sequence其实就这部分：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)先获取到 UE 辅助创建 asset 的工具类，直接AssetTools.create_asset并传入对应参数就行，这里没什么坑也没什么可以说的。创建好之后要注意frame_rate这块，一定要确保 sequence 的帧率是你想要的帧率。最后通过 LevelSequence 类函数set_display_rate设置帧率即可。添加 Binding、Track、动画然后我们看回到一开始的 sequence 截图，对照着截图来看会更容易理解接下来要做的事情。可以看到首先 Sequence 中会有一个对某个 actor 的引用，actor 下面有一个组件的引用（如 cloth 组件的引用），组件引用下面还有一个 Track；或者 actor 的引用下面就是直接一个 Track（如 Animation Track）。因此对应的代码：# 创建 actor binding actor_binding = lvl_seq.add_possessable(actor) # ... 省略一部分无关代码 for i in asset_list: # 取个名字 cur_anim = i.split("/")[-1].split(".")[0] # 省略创建 sequence 的代码... # 添加动画 loaded_asset = unreal.AnimSequence.cast(unreal.EditorAssetLibrary.load_asset(i)) frame_num = loaded_asset.get_editor_property('number_of_sampled_keys') frame_rate = loaded_asset.get_editor_property('target_frame_rate') # 设置 sequence 有内容的范围 lvl_seq.set_playback_start(0) lvl_seq.set_playback_end(frame_num) unreal.LevelSequenceEditorBlueprintLibrary.refresh_current_level_sequence() ### 添加动画 track ### anim_track = actor_binding.add_track(unreal.MovieSceneSkeletalAnimationTrack) # 添加动画 anim_section = anim_track.add_section() anim_section.set_range(0, frame_num) anim_section.params.animation = loaded_asset还是比较简单明了的，通过unreal.EditorAssetLibrary.load_asset加载指定路径i的资产，并转换为AnimSequence格式，读取动画长度并创建 track，这里需要指定 track 的类型，例如如果是控制动画的 track，那么给 actor binding 创建 track 的时候就要指定是 unreal.MovieSceneSkeletalAnimationTrack。创建 track 后给 track 添加 section，最后内容、有效区间都是要在 section 上进行设定。对于本例子中 cloth 等组件过程也是类似的步骤：# ... cloth = unreal.find_object(actor, "cloth") ### draper settings ### for i in asset_list: # 取个名字，这里用动画名字，实际上可以用别的名字 cur_anim = i.split("/")[-1].split(".")[0] # 省略上面提到过的部分 ... # cloth cloth_binding = lvl_seq.add_possessable(cloth) cloth_track = cloth_binding.add_track(unreal.MovieSceneEventTrack) cloth_section = cloth_track.add_event_trigger_section() cloth_section.set_range(0, frame_num) # 其实今天的重点在这里 cloth_channel = cloth_section.get_channels()[0] cloth_cache_binding = unreal.SequencerTools.create_quick_binding( lvl_seq, cloth, "Cache", False) cloth_new_event = unreal.SequencerTools.create_event(lvl_seq, cloth_section, cloth_cache_binding, ["(path=\"D:/UGit/XiaoLan_PC/content/XiaoLan/Effects/Character_Effects/BoWuGuan/ZhengChangShuoHua/cloth\")"]) cloth_channel.add_key(time=unreal.FrameNumber( 0), new_value=cloth_new_event)前面解释过的添加绑定、创建 track、section 部分这里跳过。重点主要在创建 MovieSceneEvent 帧这里。这里主要通过 unreal.SequencerTools.create_quick_binding 创建新的 MovieSceneEvent ，并且将此 Event 与传入的成员函数（要能在蓝图中调用的函数，这里也是借助了反射来找到要绑定的函数）进行绑定。顺带一提，这里绑定的是cloth的成员函数Cache，实际上也可以是其他类的成员函数。例如说我们的 Actor 有一个用来更新状态的成员函数KillSelf，那么可以变成unreal.SequencerTools.create_quick_binding(lvl_seq, actor, "KillSelf", False)，这里的 actor 是指向 Actor 的指针。create_quick_binding 函数的用法、参数可以在官方文档查看。创建了函数绑定之后就可以通过 unreal.SequencerTools.create_event 创建 MovieSceneEvent 了。比较值得注意的是 Payload 传入参数部分 ["(path=\"D:/UGit/XiaoLan_PC/content/XiaoLan/Effects/Character_Effects/BoWuGuan/ZhengChangShuoHua/cloth\")"]。Cache 函数需要的参数是 unreal.DirectoryPath 类型的，而这里相当于通过传入 unreal.DirectoryPath 成员变量 path 的内容构造了一个 unreal.DirectoryPath 对象，并传入给 Cache 函数。创建完成 MovieSceneEvent 后通过 channel.add_key 添加到 Track 中。上面提到的 Payload 的部分最终会通过 ImportText 函数处理，转换成 unreal.DirectoryPath 对象（所以如果你参数输入格式不对的话会提示 ImportText xxxx 错误）。最终结果：点开这些刚刚创建的帧，就会打开蓝图看到这个帧调用的函数：上面步骤完成后，unreal.EditorAssetLibrary.save_loaded_asset(lvl_seq, False) 保存即可（第二个参数是只有内容被更改后才保存，这里不做这种判断，直接保存。其实 Python 调用的 API 都可以参考蓝图的文档）。参考官方文档：Sequencer 概述官方文档：Sequencer 中的 Python 脚本：这个还是挺有用的，必看官方文档：Scripting the Unreal Editor Using Python：必看，关于如何在编辑器中使用 UE官方例程，在Engine\Plugins\MovieScene\MovieRenderPipeline\Content\Python路径下、官方文档：unreal.MovieSceneEvent官方文档：unreal 模块的 PythonAPIUDN文档：UE4 Sequencer Python Cookbook：可能需要开发者账号，不过内容不多可以不看Python in Unreal Engine | Inside UnrealPythonSamples官方文档：Setting up Autocomplete for Editor Python Scripting
一个由 BlueprintPure 函数引起的 “Bug”这篇文章会很短，其实主要是记录一下写出来的一个bug。背景背景主要是测试一个功能，用到了一个 BlueprintPure 函数：总体函数的逻辑大概是这样的：结果测试发现，运行效果如下：分析实际上这是因为 BlueprintPure 函数是每次在节点中访问一次就会执行一次。上面的程序逻辑中，我们执行了两次访问操作（一次是转换成字符串，一次是设置到名为 GuardBuying 的 UStruct 成员变量中），正是因为这两次访问操作，导致生成了两个不一样的随机数。所以只需要将逻辑改成：即只读取一次，并将得到的结果保存到变量中，后面要用的时候都访问该变量即可避免多次执行 BlueprintPure 类型的随机数生成函数，每次都获得一个新的随机数。
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 使用总结本文主要关于如何在 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 动画节点参数配置如下图所示：对 Pelvis 骨骼的设置如下图所示：另外，之所以时 Pelvis 是因为 Pelvis 是除了 Root 骨骼之外的最顶层骨骼，相当于对整体骨骼进行一个偏移：我们先暂时忽略掉前面几个Offset变量在哪里更新的问题，等等再回到这部分。接下来我们去到小白人的角色蓝图，并制作下面的蓝图函数：这个函数主要用处就是根据给定的骨骼（虽然函数名字叫做 Get Socket Location，但是实际上在 UE 里面没区分这么严格，骨骼也可以是 Socket）和给定的检测距离 $d$，在给定骨骼的位置上方 $\frac{d}{2}$ 高度开始，向地板方向进行射线检测，检测总长度为给定的检测距离 $d$。最后通过射线检测到的碰撞位置（即射线与地面的交点）$\overrightarrow{r_{\mathrm{hit}}}$、射线末端位置 $\overrightarrow{r_{\mathrm{end}}}$、射线长度 $d$，计算出碰撞位置到射线末端的长度 $d_{2}$，由 $\frac{d}{2} - d_2$ 得出脚到地面的距离（其实是胶囊体底部到地面的距离，但是通常我们会保证模型默认姿势时脚就在胶囊体底部位置上，所以最终计算结果是一样的）。下图是站在地面的情况，此时末端到地面长度与射线长度的一半，即 $\frac{d}{2}$， 一致，所以不需要偏移：在一只脚在高处一只脚在低处的时候可以看到，较低处的脚会需要偏移（不偏移的话会浮在空中，就像前面没有添加IK时候的那样），偏移量就靠我们前面的函数计算得出：上图就是IK前的状态，我们计算得出偏移量之后对根节点往下偏移（否则左脚无法站在下面的地面上），同时将左脚上的 IK Effector 移动到较低的地面上。最终得到：其实上面缺了一个步骤，毕竟上面只是制作了计算偏移量的函数，还没有调用。因此我们需要在角色蓝图的 Tick 中进行调用：上图的流程：调用了我们先前定义的射线检测函数，计算出脚部偏移量，记录到变量中；比较左脚与右脚的偏移量，选择最小值作为 Pelvis 骨骼偏移量（选最小值也是因为前面实际上写成了$d_2 - \frac{d}{2}$，最后得到的是负数，所以需要比较一下选最小值，实际上就是选距离最大的）；随后，我们回到动画蓝图，我们将会在动画蓝图的 Update 函数中读取上面角色蓝图里面记录的变量：这里我们封装读取变量的部分成一个名为 UpdateOffset 的函数，其定义如下：这里要和HipOffset相减是我们动画蓝图里面会用HipOffset来调整Pelvis的高度，因此需要剪掉这部分避免重复偏移。最后动画蓝图运行的时候根据计算出的偏移量进行偏移：最终实现效果：可以看到已经没有出现前面的脚浮空的问题了。没有 IK 骨骼的处理没有 IK 骨骼的话得要手动创建虚拟骨骼，简化模型骨骼。动画蓝图流程编程：先复制原骨骼的位置、旋转角，并赋值给虚拟骨骼；对虚拟骨骼进行 Two Bone IK 处理，移动到目标位置；将虚拟骨骼更新后的目标位置、旋转值重新复制回去对应的骨骼上；其他部分，计算距离等步骤和上面一致。UE5 中的 IK 使用（Control Rig 实现）UE4.26 中引入了 Control Rig，但是实际上到 UE5.0 才算稳定下来。所以我将 Control Rig 设置 IK 的部分放在这里，并且是在 UE5 项目中实现。用 Contro Rig 实现的另一个原因是，UE 官方推荐脚部 IK 之类的动画优化都放在 Control Rig 中进行处理。这样动画蓝图可以专注动画切换逻辑处理，而不用把动画优化和切换混在一起导致动画逻辑混乱。Control Rig 简介官方介绍：Control Rig 是基于节点的操控系统，设计用于为操控者和动画师提供多种工具，通过对美术师友好的界面来创建灵活、动态和程序性的角色。与蓝图不同，Control Rig 使用称为 RigVM 的自有轻量级 VM 提供高效的姿势计算。用户可将称为 Rig Units 的节点与 Control Rig 图连接在一起，创造运行时间和动画 rig。总而言之，就是用来方便操作、控制动画的一套操控系统：MetaHuman 项目中就通过 Control Rig 实现了复杂的表情系统：此外，Control Rig 还可以用于在运行时优化动画，例如加入脚部、手部 IK 等功能。我们主要关注如何用 Control Rig 实现 IK 功能。UE5 官方 Demo 中的 IK下面主要解析一下官方 Demo 实现的 IK，这里的 Demo 是 UE5 内置的第三人称模版 Demo。首先我们看看第三人称模板中实现的脚部 IK 效果：可以看到看起来实现还是不错的。首先我们看看 Control Rig 的总览（可能看不太清楚，不过没有关系，我会在后面详细介绍每一步做了什么）：其实就分了5个步骤：首先判断是否进行 IK （因为有时候会关闭 IK，例如跳跃的时候），如果进行 IK 的话会在双脚（通常是脚踝处）进行射线检测（虽然实际调用的函数叫做 Sphere Trace，不过还是和射线检测差不多的东西），并记录下脚到地面的偏差值/距离；对脚部的当前 Z 偏差值（这是一个独立的变量，不是脚部的Z轴的值）进行插值，使之逼近目标偏差值；对 Pelvis 处（通常是臀部位置的骨骼，差不多在骨盆中心的位置）的当前 Z 偏差值也进行插值，使之逼近最低处脚部的目标偏差值，这是为了适配一只脚在高处一只脚在低处的情况，这样可以保证低处的脚能站在地面上而不会因为脚不够长而悬在空中；将2、3步计算出来的当前 Z 偏差值添加到 Pelvis 骨骼、双脚对应的 IK 骨骼的Z轴上；使用 Full Body IK节点，设定好 Effector 作用在双脚对应的骨骼上（如小白人模型的foot_l和foot_r），并将双脚IK骨骼的值复制给Effector，使之计算出双脚骨骼及父骨骼的位置，实现脚自适应高度，站在地面上的效果；接下来我们分步骤进行分析。第一步 检测脚到地面距离以及地面的倾斜角度先看总览：首先判断是否要进行 IK ，这里判断依据的变量ShouldDoIKTrace是暴露出去的，可以在动画蓝图外面控制。如果不进行 IK 的话会把双脚的 Z 方向 Offset 设为0，即不需要改变脚部高度。如果进行 IK，那么会调用 FootTrace 函数来获取脚到地面的距离，我们看下这个函数的实现。可以看到函数所做的事情：获取传入骨骼的 XYZ 坐标，并根据坐标、添加的偏移量设置射线参数。偏移主要是在 Y 轴上偏移了 5 个单位，即前进方向上偏移 5 个单位，从脚踝处移动到脚部中间；射线起始点的 Z 轴设定为脚踝 Z 轴值基础上，向上方偏移 30 个单位，而终点则设定为在脚踝 Z 轴下方 50 个单位，这样设置可以保证射线检测能检测到在脚上方、下方的障碍物；进行射线检测，得到射线与地面/障碍物发生碰撞的位置以及碰撞地方的法线值（这里法线值主要方便后面将脚部适配斜面，不过官方模板并没有适配倾斜角）；射线路径如下图所示：第二步 插值我们分开真正的目标值（第一步设置的）与当前脚部的偏移值，用Alpha Interpolate函数进行插值计算出脚部新的偏移值，后面再把计算出来的新偏移值设置到脚部上。这一步主要是平滑动画用，避免脚部位置突变。第三步 设置腰部偏移取测量出的两脚到地面的距离，取最大距离作为腰部的偏移值。这么做是为了能够让模型总体向下移动，从而保证位置较低的脚能够站在地面上：这里之所以用Less来进行判断，是因为我们前面FootTrace函数中返回的是射线与物品碰撞点的坐标，而我们直接把碰撞点世界坐标 Z 轴的值设置为偏移量，因此 Z 越小，就代表脚到地面越低，脚离地面的距离越可能大（因为实际上这里我们没有计算距离，所以只是可能）：如果不取距离最大的值来设置模型的高度偏移量，那么很可能因为脚不够长而导致脚必须通过拉伸才能够被放置在地面上。另外，上面的腰部指 Pelvis ，即模型的第二层骨骼：设置 Pelvis 骨骼的位置能够影响整个模型的位置。第四步 更新骨骼位置这一步更新双脚对应的 IK 骨骼的位置，以及 Pelvis 骨骼位置。这里只是简单的在世界空间中，Z 轴上添加了偏移量。之所以不直接更新双脚位置是因为如果需要调整双脚的位置，还需要顺带更新双脚父骨骼的位置、旋转值，即必须借助 IK 算法来进行更新。这里 IK 骨骼其实相当于一个临时变量，用来暂存更新后的位置数据。第五步 用 IK 算法设置脚部位置这一步读取上一步 IK 骨骼更新后的位置数据，并设置给双脚骨骼 Effector 目标位置，让 IK 算法依据目标位置反推父骨骼位置、角度，让双脚移动到目标位置上。这样就基本实现脚部 IK 了，可以看到角色双脚可以适配各类高低地形，不会出现浮空的现象：但是还有点瑕疵，在斜面上的时候脚会有一部分穿入地面，而且也不能够贴合地面：如果要修复这个问题，就需要利用第二步射线检测到的斜面倾斜角度，修正脚部角度，使得脚部贴合地面。最后，记得在动画蓝图加上 Control Rig 节点，让动画生效即可：（忽略掉 Control Rig 后面一系列节点，那部分是其他的优化，与脚部 IK 无关）。Control Rig 节点记得配置属性这里，指定好对应的 Control Rig Class：参考资料UE4中的TwoBoneIKUE4 脚部IK Foot Placement 的实现：UE4 中实现脚部 IK 部分的蓝图来自此文章，略有修改
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.BindUObject(this, &ADelegateListener::EnableLight)下面这张图列举了除了BindUObject之外还能够使用什么函数进行绑定，以及在什么情况下使用2。除了BindUObject之外还有别的绑定函数，这里直接借用官网过时的文档中的列表：大概如上，都非常简单，在使用的时候按照您要绑定的函数来选择对应的函数来绑定即可。这里简单补充几个官网文档没有提及的绑定：函数描述BindThreadSafeSP(SharedPtr, &FClass::Function)用一个弱指针TWeakPtr来绑定一个原生C++类成员函数，当指针SharedPtr指向的对象无效的时候不会执行绑定的回调函数BindWeakLambda(UObject, Lambda)绑定一个匿名函数，在传入的UObject有效，还没有被回收的时候都可以调用这个匿名函数。这个匿名函数中可以用this，但是其他关键词不一定能用BindUFunction(UObject, FName("FunctionName"))用来绑定一个UObject的UFUNCTION函数，原生的与动态的委托都可以用这个函数来绑定回调函数这里提几个注意事项5：注意BindRaw绑定的普通C++对象的成员函数，要特别注意执行的时候这个对象有没有被销毁。如果被销毁了那么触发委托执行绑定的函数会导致报错；注意BindLambda绑定的Lambda表达式捕获的外部变量，如果在触发委托的时候变量已经被销毁那么会导致报错（如引用捕获了一个局部变量）；BindWeakLambda、BindUObject、BindUFunction绑定时会弱引用一个UObject对象，需要预先IsBound()或者ExecuteIfBound来判断是否该对象还有效再执行委托，否则可能会报错；BindSP、BindThreadSafeSP绑定时会弱引用一个智能指针对象（UE4的智能指针），执行前需要先IsBound()或者ExecuteIfBound来判断该对象是不是还存在，否则可能会报错；如果单播委托对象被销毁，那么析构函数会自动调用UnBind进行解绑；2.1.1.c 执行委托执行单播委托需要调用的函数主要是Execute(您要传入的参数)，要注意的是，这个函数并不会检查您的绑定情况，因此如果委托未绑定，那么直接执行此函数会导致报错。因此往往推荐在调用Execute(传入参数)前先用IsBound()来检查是否已经进行了绑定。当然也可以直接调用ExecuteIfBound(传入参数)，这个函数等效于if(委托.IsBound())进行判断后再执行Execute(传入参数)。2.1.1.d PayLoad首先介绍下PayLoad的功能，PayLoad是委托绑定的时候传入的额外参数列表，保存在委托对象内。触发委托的时候PayLoad会跟着Execute(传入的参数)、ExecuteInBound(传入的参数)传入的参数之后填充到绑定函数的参数列表中，然后执行。举个例子：DECLARE_DELEGATE_OneParam(FLearnDelegate, float); static void LearningDelegate(float Bar) { UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f ==="), Bar); } static void LearningPayload(float Bar, FString Test) { UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f, %s ==="), Bar, *Test); } // 在GameInstance的初始化函数中或者其他地方 // 正常使用 FLearnDelegate DelegateObj1; DelegateObj1.BindStatic(LearningDelegate); DelegateObj1.ExecuteIfBound(23.0f); // PayLoad FLearnDelegate DelegateObj2; // 这里的“TEST”会在调用绑定函数的时候紧接着委托对戏那个传入的参数传入 DelegateObj2.BindStatic(LearningPayload, FString(TEXT("TEST!"))); // “TEST”会接在23.0f后面，所以最后是传入到Test参数中 DelegateObj2.ExecuteIfBound(23.0f);2.1.1.e 底层实现绑定函数指针相关代码在DelegateCombination.h以及Delegate.h中。首先我们需要有个大体的概念，其实本质上就是保存了一个函数指针，在执行的时候直接访问该函数指针对应的函数即可，如果是成员函数则比较特殊，需要同时知道成员函数所在的类，同时应该有一个弱指针指向该对象。接下来我们看具体实现。可以看到，实际上就是通过TDelegate这个类来实现的，所以实际上我们在定义委托的时候就是在调用TDelegate<ReturnType(一堆您传入的参数)>创建委托类型，并通过typedef重命名为您给定的名字，方便记忆与阅读。TDelegate在DelegateSignatureImpl.ini中实现。因为内容比较多，因此我们只看关键部分。首先我们看到他继承了TDelegateBase这个类：简单扫几眼，就会发现实际上用来保存指向函数的指针并不在TDelegate中，而应该是放在了父类，也就是TDelegateBase：读者可能发现了UserPolicy这个参数，这里实际上是4.26版本才新加入的内容6：之前的静态单播的基类是FDelegateBase，这个类没有变化，但是所有的public接口被改成了protected，无法直接使用了。这一点真是非常糟糕，哪有增加可扩展性的同时把接口都藏起来的，本来所有实现就都是写到头文件里的。最大的不同是接下来的地方，其实现不是通过直接对FDelegateBase的继承完成的，而是通过一个叫做FDefaultDelegateUserPolicy的结构体进行中转的。这个结构体中只定义了三个类型的别名，分别是FDelegateInstanceExtras、FDelegateExtras、FMulticastDelegateExtras。其中FDelegateExtras指向的就是FDelegateBase。静态单播的实现类TDelegateBase（原来叫TBaseDelegate，这诡异的命名）变成了模板类，该类继承于模板参数中的FDelegateExtras类型。说到这里我想应该已经明白了UE4这个改动的含义。这意味着我们可以通过自己定义一个FDefaultDelegateUserPolicy以外的其他结构体UserPolicy，并在其中定义上述三个类型，就可以釜底抽薪式地把写在底层的实现替换成我们自定义的实现，这无疑很大地增加了这个模块的可扩展性。简单的说就是FDelegateBase在经过抽象之后，允许用户单独创建一个UserPolicy结构体给TDelegateBase来自定义委托，当然如果没有传入自己定义的UserPolicy的话，那么会使用默认的FDefaultDelegateUserPolicy（这里用到了C++的模板偏特化特性7，能够在给定默认值的同时，能够让用户输入自己希望的值）：因此实际上此处的UserPolicy是FDefaultDelegateUserPolicy，那么我们简单看看FDefaultDelegateUserPolicy这一struct的内容：struct FDefaultDelegateUserPolicy { // 这里的using是别名指定 using FDelegateInstanceExtras = IDelegateInstance; // 注意下面这个，另外两个会在其他委托中用到，先不管 using FDelegateExtras = FDelegateBase; using FMulticastDelegateExtras = TMulticastDelegateBase<FDefaultDelegateUserPolicy>; };回到开始的TDelegate<InRetValType(ParamTypes...), UserPolicy> : public TDelegateBase<UserPolicy>，我们看看TDelegateBase的定义：所以实际上最终还是继承了FDefaultDelegateUserPolicy::FDelegateExtras，即FDelegateBase。我们继续追踪GetDelegateInstanceProtected()，继续看TDelegateBase，但是我们会发现，实际上TDelegateBase也没有保存指针，只是提供了一系列函数（如，是否已经绑定了函数的IsBound()等）：template <typename UserPolicy> class TDelegateBase : public UserPolicy::FDelegateExtras { template <typename> friend class TMulticastDelegateBase; // 用using指定别名 using Super = typename UserPolicy::FDelegateExtras; public: // 省略部分注释与宏判断 FName TryGetBoundFunctionName() const { // 注意这里，可以看出不是这里保存的函数指针 if (IDelegateInstance* Ptr = Super::GetDelegateInstanceProtected()) { // 实际上还是调用了委托对象提供的函数来实现具体的功能 return Ptr->TryGetBoundFunctionName(); } return NAME_None; } // 省略一系列函数 }可以看到，实际上即便是TDelegateBase，也是要通过Super::GetDelegateInstanceProtected()来获取委托对象，这个函数最终调用FDelegateBase类提供的GetDelegateInstanceProtected()来获取委托对象（注意using Super = typename UserPolicy::FDelegateExtras;，而在FDefaultDelegateUserPolicy中，using FDelegateExtras = FDelegateBase;），最终通过IDelegateInstance类的委托对象提供的函数来实现相关功能。因此我们还得要接着往下面看才能找到真正保存函数指针的地方。因此，我们看到FDelegateHandle：class FDelegateBase { template <typename> friend class TMulticastDelegateBase; template <typename> friend class TDelegateBase; protected: /** * Creates and initializes a new instance. * * @param InDelegateInstance The delegate instance to assign. */ explicit FDelegateBase() : DelegateSize(0) { } ~FDelegateBase() { // 可以看到实际上在被销毁的时候会自动调用函数取消绑定 Unbind(); } // 省略部分函数 // 这里是重点 /** * Gets the delegate instance. Not intended for use by user code. * * @return The delegate instance. * @see SetDelegateInstance */ FORCEINLINE IDelegateInstance* GetDelegateInstanceProtected() const { return DelegateSize ? (IDelegateInstance*)DelegateAllocator.GetAllocation() : nullptr; } // 省略函数 private: // 这个也是重点 FDelegateAllocatorType::ForElementType<FAlignedInlineDelegateType> DelegateAllocator; int32 DelegateSize; }上面提到，TDelegateBase最终调用的是FDelegateBase提供的GetDelegateInstanceProtected()，而这里我们可以看到，实际上是返回IDelegateInstance类型的数据（这里先忽略掉DelegateAllocator，只需要理解为一个工具类，用来分配内存，因为与委托不太相关所以先不详细说明，如果感兴趣可以看这篇文章8），因此最终函数指针理论上是包裹在IDelegateInstance中的。但是再想想，实际情况肯定没有这么简单，还记得我们前面说到的绑定函数吗？实际可能传入的函数指针类型非常多，例如可能传入一个在UObject对象中的成员函数，可能传入一个lambda函数等。所以实际上，会包裹在IDelegateInstance为基类的，根据各种传入函数指针类型进行适配的派生类中。例如，接着上面往下看，我们可以看到这类型的函数：/** * Static: 用来创建C++全局函数指针的委托 */ template <typename... VarTypes> UE_NODISCARD inline static TDelegate<RetValType(ParamTypes...), UserPolicy> CreateStatic(typename TIdentity<RetValType (*)(ParamTypes..., VarTypes...)>::Type InFunc, VarTypes... Vars) { TDelegate<RetValType(ParamTypes...), UserPolicy> Result; // 重点是下面这个，TBaseStaticDelegateInstance的基类就是IDelegateInstance TBaseStaticDelegateInstance<FuncType, UserPolicy, VarTypes...>::Create(Result, InFunc, Vars...); return Result; } /** * Static: 创建lambda函数的委托 */ template<typename FunctorType, typename... VarTypes> UE_NODISCARD inline static TDelegate<RetValType(ParamTypes...), UserPolicy> CreateLambda(FunctorType&& InFunctor, VarTypes... Vars) { TDelegate<RetValType(ParamTypes...), UserPolicy> Result; TBaseFunctorDelegateInstance<FuncType, UserPolicy, typename TRemoveReference<FunctorType>::Type, VarTypes...>::Create(Result, Forward<FunctorType>(InFunctor), Vars...); return Result; } // 还有更多，这里忽略简单看下TBaseStaticDelegateInstance：可以很轻松找到保存C++函数指针的变量（这个变量类型是UE4提供的专门用来保存C++函数指针的类型，网上资料很多9，这里就不进行介绍了）。同理，相似的，绑定UObject对象成员函数委托创建函数则有：最终执行的时候的形式就类似于这样：// 全剧函数 (*MyDelegate)(); // 对象成员函数 (MyObj->*FuncPtr)(); // 如果是在栈上 (StackObj.*Func1Ptr)();Payload的实现当然实际上UE4中会支持Payload，会先把一部分预先输入的参数拼接到调用委托的时候传入的参数后面去，形成一个参数列表，最后一起作为参数输入到绑定函数，但是原理差不多。以全局函数的执行为例：Payload实际上是一个TTuple：最终执行：因为Payload特性前面介绍过，所以这里不赘述。绑定但是只有创建是不行的，这时候的委托还没有绑定上要执行的函数。我们还是以绑定全局函数为例：/** * 绑定一个C++全局函数 */ template <typename... VarTypes> inline void BindStatic(typename TBaseStaticDelegateInstance<FuncType, UserPolicy, VarTypes...>::FFuncPtr InFunc, VarTypes... Vars) { *this = CreateStatic(InFunc, Vars...); }结合上面的CreateStatic的实现就可以明白了，因为CreateStatic返回的是右值，这里左侧的*this和=会调用到TDelegate的Move Assigment Operator： /** * Move assignment operator. * * @param OtherDelegate Delegate object to copy from */ inline TDelegate& operator=(TDelegate&& Other) { if (&Other != this) { // this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions DelegateInstanceInterfaceType* OtherInstance = Other.GetDelegateInstanceProtected(); if (OtherInstance != nullptr) { OtherInstance->CreateCopy(*this); } else { Unbind(); } } return *this; }最终将创建出来的TDelegate赋值给自身，从而实现绑定函数。绑定不同的函数指针对应不同的T<函数指针类型>DelegateInstance<...>::Create(...)，这里列举下，实际上看源代码也可以理解：创建函数对应的Delegate Instance创建函数CreateStatic()TBaseStaticDelegateInstance<...>::Create(...)CreateLambda()TBaseFunctorDelegateInstance<...>::Create(...)CreateWeakLambda()TWeakBaseFunctorDelegateInstance<...>::Create(...)CreateRaw()TBaseRawMethodDelegateInstance<...>::Create(...)CreateSP()TBaseSPMethodDelegateInstance<...>::Create(...)CreateThreadSafeSP()TBaseSPMethodDelegateInstance<...>::Create(...)CreateUFunction()TBaseUFunctionDelegateInstance<...>::Create(...)CreateUObject()TBaseUObjectMethodDelegateInstance<...>::Create(...)补充这里看起来没有介绍带参数、返回值的情况，因为实际上带参数、返回值的也是调用了FUNC_DECLARE_DELEGATE，调用的typedef都一样，只是传入到模板的参数数量不一样（借助了C++11中的可变模板参数实现）。最终还是：另外，这里的__VA_ARGS__实际上就是：比较容易理解，所以这里不作详细解释。2.1.1.f 总结总而言之，单播委托的使用流程如下图所示：graph TD; 开始 --> 使用宏定义委托类型 -->声明委托对象--> 绑定需要执行的函数指针到委托对象上-->|需要的时候|触发委托对象并执行指针指向的函数; 触发委托对象并执行指针指向的函数-->|不再需要绑定的函数|从委托对象中解绑函数-->|不再需要委托对象|销毁委托对象; 从委托对象中解绑函数-->|绑定新的函数|绑定需要执行的函数指针到委托对象上; 触发委托对象并执行指针指向的函数-->|指向的函数失效|报错;而委托的类层次结构我们可以总结为（其实我不是特别熟悉UML图，希望没有错）：classDiagram FDelegateBase <|-- TDelegateBase TDelegateBase <|-- TDelegate TDelegate ..|> FDefaultDelegateUserPolicy TDelegateBase ..|> FDefaultDelegateUserPolicy FDelegateBase *-- IDelegateInstance IDelegateInstance <|-- IBaseDelegateInstance IBaseDelegateInstance <|-- TCommonDelegateInstanceState TCommonDelegateInstanceState <|-- TBaseStaticDelegateInstance TCommonDelegateInstanceState <|-- TBaseFunctorDelegateInstance TCommonDelegateInstanceState <|-- TWeakBaseFunctorDelegateInstance TCommonDelegateInstanceState <|-- TBaseRawMethodDelegateInstance TCommonDelegateInstanceState <|-- TBaseSPMethodDelegateInstance TCommonDelegateInstanceState <|-- TBaseUFunctionDelegateInstance TCommonDelegateInstanceState <|-- TBaseUObjectMethodDelegateInstance TDelegate *-- TBaseStaticDelegateInstance TDelegate *-- TBaseFunctorDelegateInstance TDelegate *-- TWeakBaseFunctorDelegateInstance TDelegate *-- TBaseRawMethodDelegateInstance TDelegate *-- TBaseSPMethodDelegateInstance TDelegate *-- TBaseUFunctionDelegateInstance TDelegate *-- TBaseUObjectMethodDelegateInstance FDefaultDelegateUserPolicy : +FDelegateInstanceExtras(IDelegateInstance) FDefaultDelegateUserPolicy : +FDelegateExtras(FDelegateBase) FDefaultDelegateUserPolicy : +FMulticastDelegateExtras(TMulticastDelegateBase<FDefaultDelegateUserPolicy>) class TDelegate { +CreateStatic(...) +CreateLambda(...) +CreateWeakLambda(...) +CreateRaw(...) +CreateSP(...) +CreateThreadSafeSP(...) +CreateUFunction(...) +CreateUObject(...) +TDelegate(TDelegate&& Other) +TDelegate& operator=(TDelegate&& Other) +BindStatic(...) +BindLambda(...) +BindWeakLambda(...) +BindRaw(...) +BindSP(...) +BindThreadSafeSP(...) +BindUFunction(...) +BindUObject(...) } class TDelegateBase { -class TMulticastDelegateBase -Super(FDelegateBase) } class FDelegateBase { #FDelegateBase& operator=(FDelegateBase&& Other) #void Unbind() #IDelegateInstance* GetDelegateInstanceProtected() const -void* Allocate(int32 Size) -FDelegateAllocatorType::ForElementType<FAlignedInlineDelegateType> DelegateAllocator -int32 DelegateSize } class IDelegateInstance { <<interface>> +FName TryGetBoundFunctionName() +UObject* GetUObject() +const void* GetObjectForTimerManager() const +uint64 GetBoundProgramCounterForTimerManager() const +bool HasSameObject( const void* InUserObject ) const +bool IsCompactable( ) +bool IsSafeToExecute( ) +FDelegateHandle GetHandle() const } class TCommonDelegateInstanceState { #TTuple<VarTypes...> Payload #FDelegateHandle Handle +RetValType(InRetValType) }此外：单播委托支持PayLoad功能，但是动态类型的委托并不支持PayLoad；单播委托在执行之前必须要IsBound()检查是否已经绑定，否则会报错；单播委托允许绑定函数带有返回值；其实单播委托理解了后面的都不难理解了，因此后面的内容会没有单播委托这么详细（毕竟实现都相似的）。2.1.2 动态（单播）委托注意：这里讨论的是动态单播委托，动态多播委托后面会另外介绍动态其实是指能够被序列化，允许动态绑定，除此之外实际上和单播代理没有太大区别；动态委托也可以有返回值，但是只能有一个返回值；动态即能够被序列化，因此可以在蓝图中使用，也可以添加UPROPERTY；绑定的时候不需要函数指针，只需要函数名称字符串，但是只能够绑定UFUNCTION；动态委托运行是最慢的，所以如果没有必要的话就用别的委托；2.1.2.a 声明其实和单播委托的声明差不多：DECLARE_DYNAMIC_DELEGATE[_RetVal, ...]( DelegateName ); // 例如无参数，无返回值 DECLARE_DYNAMIC_DELEGATE(FNoRetNoParamDelegate); // 例如1个参数无返回值，最多支持到9个，注意和前面不同，给定参数类型的同时要加上类型名字，并且绑定参数和委托要保持一致 DECLARE_DYNAMIC_DELEGATE_OneParam(FOnAssetLoaded, class UObject*, Loaded); // 例如1个返回值一个参数 DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(UWidget*, FGenerateWidgetForObject, UObject*, Item); 2.1.2.b 绑定4.26的动态委托绑定函数只有一个BindUFunction，并提供UObject对象、函数名字即可。2.1.2.c 执行委托和单播委托类似：2.1.2.d 底层实现可以看到实际上依托TBaseDynamicDelegate来实现，而且宏定义声明一个动态委托就是声明了一个类继承TBaseDynamicDelegate。宏定义里面也另外定义了ExecuteIfBound与Execute函数，实际执行委托也是通过宏定义里面定义的这两个函数，同时依托UE4的反射、序列化机制实现的。TBaseDynamicDelegate内的实现不多，实际上还是得依靠TScriptDelegate：TScriptDelegate才是真正保存函数名字、绑定的对象的弱指针的地方：简单看下绑定部分，因为只能绑定UFUNCTION函数，所以只有一个绑定函数：执行则是依托一开始宏定义里面定义的Execute(传入参数)：实际执行的时候UE4会根据输入的函数名字找到对应的函数并执行，这个函数最终会被上面定义的Execute调用：2.1.2.e 总结因为比较简单，所以这里就先不花UML图来解析了。实际上声明一个动态委托类型就是创建了一个继承TBaseDynamicDelegate的类，并且类名为动态委托的名字；动态委托在执行时需要实时在类中按照给定的函数名字查找对应的函数，因此执行速度很慢，所以如果能用别的不是动态的委托代替就用别的委托10；动态委托能够被蓝图调用；动态委托能够序列化，因此可以被存储到硬盘上；绑定的时候不需要函数指针，只需要函数名称字符串，但是只能够绑定UFUNCTION；2.1.3 多播委托吐个槽，官方文档真的是一言难尽，只是multicast delegate这个词在中文页面上都有2种不同的翻译。更加关键的是，多播委托的官方文档居然还有低级错误，在《多播委托》页面最上面写明了“多播委托不能使用返回值”，下面给的声明多播委托示例就带了个返回值。多播委托能绑定多个函数指针，委托被执行的时候也会触发多个函数；多播委托执行的时候，执行绑定该委托的函数的顺序实际上是没有规定的（因此可能最后绑定的函数最先被执行）多播委托不允许有返回值。实际上底层是一个保存了所有绑定了这个委托的函数的FDelegateBase数组，执行委托的时候会遍历数组并调用绑定的函数2.1.3.a 声明DECLARE_MULTICAST_DELEGATE<参数数量>( DelegateName, ParamsTypes ); // 例如0个参数 DECLARE_MULTICAST_DELEGATE( DelegateName ); // 例如1个参数 DECLARE_MULTICAST_DELEGATE_OneParam( DelegateName, Param1Type );比较简单，和前面的委托都差不多。2.1.3.b 绑定先以绑定UObject对象的成员函数为例：UDelegatepTestClass* UObjMC = NewObject<UDelegatepTestClass>(this, UDelegatepTestClass::StaticClass()); // 先传入UObject，然后传入成员函数指针 CharacterDelegateMulticast7.AddUObject(UObjMC, &UDelegatepTestClass::DelegateProc1);其他的绑定方式差不太多，故在此不赘述。所有的绑定方式如下：函数名用途FDelegateHandle Add(const FDelegate& InNewDelegate)将某个函数委托添加到该多播委托的调用列表中FDelegateHandle AddStatic(...)添加原始C++指针全局函数委托FDelegateHandle AddLambda(...)添加匿名函数委托FDelegateHandle AddWeakLambda(...)添加弱引用对象的匿名函数委托，会对对象弱引用FDelegateHandle AddRaw(...)添加原始C++指针委托。原始指针不使用任何类型的引用，因此如果从委托下面删除了对象，则调用此函数可能不安全。调用Execute()时请小心！FDelegateHandle AddSP(...)添加基于共享指针的（快速、非线程安全）成员函数委托，共享指针委托保留对对象的弱引用FDelegateHandle AddThreadSafeSP(...)添加基于共享指针的成员函数委托（相对较慢，但是线程安全），会对对象弱引用FDelegateHandle AddUFunction(...)添加UFunction类型的成员函数，会对输入的对象弱引用FDelegateHandle AddUObject(...)添加UObject对象的成员函数，会对输入的对象弱引用2.1.3.c 执行委托.Broadcast()即可，即便在没有任何绑定的时候都可以用这个函数来触发委托执行。不过需要注意的是，绑定函数的执行顺序是未定义的，执行顺序很可能与绑定顺序不同（毕竟多播委托可能会多次添加、移除委托）。2.1.3.d 底层实现保存的绑定函数数组先看宏定义：接着往下看：可以看到实际上是TMulticastDelegate，看看它的定义：和单播委托一样，通过偏特化的方式保证UserPolicy在有默认值的同时能够让用户输入自己定义的UserPolicy。也是和单播委托一样，实际上保存指针的数组并不在TMulticastDelegate中，要在基类中查找，我们先看上一级的UserPolicy::FMulticastDelegateExtras，即TMulticastDelegateBase<FDefaultDelegateUserPolicy>：可以看到，实际上就是一个TDelegateBase数组（FMulticastInvocationListAllocatorType先不用管，主要是和内存分配有关，与我们关注的重点不太相关）。其实说到这里基本上可以和单播委托那边的分析结合起来看，但是首先，我们先接着看绑定的实现。绑定的实现首先我们看看常用的AddUObject是怎么实现的：template <typename UserClass, typename... VarTypes> inline FDelegateHandle AddUObject(UserClass* InUserObject, typename TMemFunPtrType<false, UserClass, void (ParamTypes..., VarTypes...)>::Type InFunc, VarTypes... Vars) { static_assert(!TIsConst<UserClass>::Value, "Attempting to bind a delegate with a const object pointer and non-const member function."); // 这里实际上调用了上面提到的FDelegate::CreateUObject，不理解的话看上面的内容即可 return Add(FDelegate::CreateUObject(InUserObject, InFunc, Vars...)); }可以看到实际上还是依靠了另一个函数Add，并且实际上用到了上面提到的单播委托的FDelegate::CreateUObject来创建一个委托对象。那么我们接着看看Add的实现：/** * Adds a delegate instance to this multicast delegate's invocation list. * * @param Delegate The delegate to add. */ FDelegateHandle Add(FDelegate&& InNewDelegate) { FDelegateHandle Result; if (Super::GetDelegateInstanceProtectedHelper(InNewDelegate)) { Result = Super::AddDelegateInstance(MoveTemp(InNewDelegate)); } return Result; }这里的Super其实是：即TMulticastDelegateBase<FDefaultDelegateUserPolicy>，因此最终会调用TMulticastDelegateBase的：而InvocationList的定义是：即用到上面定义的类型：所以实际上就是先创建一个单播委托，然后添加到了自己维护的TArray数组中。执行委托的实现/** * Broadcasts this delegate to all bound objects, except to those that may have expired. * * The constness of this method is a lie, but it allows for broadcasting from const functions. */ void Broadcast(ParamTypes... Params) const { bool NeedsCompaction = false; Super::LockInvocationList(); { const InvocationListType& LocalInvocationList = Super::GetInvocationList(); // call bound functions in reverse order, so we ignore any instances that may be added by callees for (int32 InvocationListIndex = LocalInvocationList.Num() - 1; InvocationListIndex >= 0; --InvocationListIndex) { // this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions const FDelegate& DelegateBase = (const FDelegate&)LocalInvocationList[InvocationListIndex]; IDelegateInstance* DelegateInstanceInterface = Super::GetDelegateInstanceProtectedHelper(DelegateBase); if (DelegateInstanceInterface == nullptr || !((DelegateInstanceInterfaceType*)DelegateInstanceInterface)->ExecuteIfSafe(Params...)) { NeedsCompaction = true; } } } Super::UnlockInvocationList(); if (NeedsCompaction) { const_cast<TMulticastDelegate*>(this)->CompactInvocationList(); } }可以看到实际上就是遍历一遍数组然后一个个调用ExecuteIfSafe(传入参数)。注意ExecuteIfSafe，如果委托无法被执行，那么就会返回false。而ExecuteIfSafe的实现随着不同类型的绑定函数而不同，例如如果绑定的是全局函数，实际上实现是：bool ExecuteIfSafe(ParamTypes... Params) const final { // Call the static function checkSlow(StaticFuncPtr != nullptr); (void)this->Payload.ApplyAfter(StaticFuncPtr, Params...); return true; }可以看到无论如何都会执行，但是如果是别的，例如绑定的是weaklambda，那么：bool ExecuteIfSafe(ParamTypes... Params) const final { if (ContextObject.IsValid()) { (void)this->Payload.ApplyAfter(Functor, Params...); return true; } return false; }会判断弱引用的对象是不是还有效，如果已经被销毁了就不会执行并且返回false。这样就可以保证无论何时调用Broadcast()都是安全的。2.1.3.e 总结实际上多播委托就是维护了一个由单播委托组成的数组，依托单播委托实现的；无论何时调用Broadcast()都是安全的。2.1.4 事件事件和多播委托相似（实际上就是多播，只是多了个friend class OwningType，用来辨别调用者是不是代理拥有者），功能都差不多，只是限定死了部分函数的权限：只有声明事件的类可以调用事件的Broadcast、IsBound和Clear函数。这就保证了只有事件的拥有者能够触发事件。事件绑定的函数也是不能够有返回值的。// 和组播类似 // 注意首个参数，用来指定事件拥有者 DECLARE_EVENT( OwningType, EventName ); // 1个参数 DECLARE_EVENT_OneParam( OwningType, EventName, Param1Type ); // 2个参数 DECLARE_EVENT_TwoParams( OwningType, EventName, Param1Type, Param2Type ); // 多个参数 DECLARE_EVENT_<Num>Params( OwningType, EventName, Param1Type, Param2Type, ...);事件和多播基本一致，而且因为后面的版本中事件类型会被移除，因此这里不进行详细说明。2.1.5 动态多播委托实际上上面已经详细说明了动态委托、多播委托，如果上面的内容理解了的话那么这里的内容也是很容易能够理解的了。2.1.5.a 声明// 动态多播不能有返回值，所以只列举有参数、无参数的例子 // 无参数 DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOpenViewDelegate_DynamicMulticast); // 1个参数，和前面不同的是要加上参数名字 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCharacterDelegate_DynamicMulticast, int, nCode); 2.1.5.b 绑定绑定函数使用场景Add添加一个函数委托AddUnique添加一个函数委托，但是只有在这个函数委托不存在维护的数组中的时候才添加（根据委托的签名是否已经存在数组中进行判断）AddDynamic用来绑定一个UObject类型的成员函数到委托中（这个接口实际上通过宏重定向到__Internal_AddDynamic）AddUniqueDynamic与上面的AddDynamic一样，但是会根据函数的签名确保不重复添加2.1.5.c 执行直接调用Broadcast(输入参数)即可，任何时候都可以调用这个函数，与多播委托一样。2.1.5.d 底层实现/** Declares a blueprint-accessible broadcast delegate that can bind to multiple native UFUNCTIONs simultaneously */ #define DECLARE_DYNAMIC_MULTICAST_DELEGATE( DelegateName ) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE) FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, , FUNC_CONCAT( *this ), void )可以看出实际上是调用了FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE:/** Declare user's dynamic multi-cast delegate, with wrapper proxy method for executing the delegate */ #define FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(TWeakPtr, DynamicMulticastDelegateClassName, ExecFunction, FuncParamList, FuncParamPassThru, ...) \ class DynamicMulticastDelegateClassName : public TBaseDynamicMulticastDelegate<TWeakPtr, __VA_ARGS__> \ { \ public: \ /** Default constructor */ \ DynamicMulticastDelegateClassName() \ { \ } \ \ /** Construction from an FMulticastScriptDelegate must be explicit. This is really only used by UObject system internals. */ \ explicit DynamicMulticastDelegateClassName( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) \ : TBaseDynamicMulticastDelegate<TWeakPtr, __VA_ARGS__>( InMulticastScriptDelegate ) \ { \ } \ \ /** Broadcasts this delegate to all bound objects, except to those that may have expired */ \ void Broadcast( FuncParamList ) const \ { \ ExecFunction( FuncParamPassThru ); \ } \ };可以看到，实际上和动态委托类似，会变成一个继承TBaseDynamicMulticastDelegate的类：TBaseDynamicMulticastDelegate提供了__Internal_AddDynamic的实现：/** * Binds a UObject instance and a UObject method address to this multi-cast delegate. * * @param InUserObject UObject instance * @param InMethodPtr Member function address pointer * @param InFunctionName Name of member function, without class name * * NOTE: Do not call this function directly. Instead, call AddDynamic() which is a macro proxy function that * automatically sets the function name string for the caller. */ template< class UserClass > void __Internal_AddDynamic( UserClass* InUserObject, typename FDelegate::template TMethodPtrResolver< UserClass >::FMethodPtr InMethodPtr, FName InFunctionName ) { check( InUserObject != nullptr && InMethodPtr != nullptr ); // NOTE: We're not actually storing the incoming method pointer or calling it. We simply require it for type-safety reasons. FDelegate NewDelegate; NewDelegate.__Internal_BindDynamic( InUserObject, InMethodPtr, InFunctionName ); this->Add( NewDelegate ); }最终调用的Add则由基类TMulticastScriptDelegate实现：而且最终保存的数组实际上也保存在TMulticastScriptDelegate中：可以看到，实际上就是一个数组，里面保存了一系列的动态委托。而Broadcast(传入参数)最终会调用到TMulticastScriptDelegate的：/** * Executes a multi-cast delegate by calling all functions on objects bound to the delegate. Always * safe to call, even if when no objects are bound, or if objects have expired. In general, you should * never call this function directly. Instead, call Broadcast() on a derived class. * * @param Params Parameter structure */ template <class UObjectTemplate> void ProcessMulticastDelegate(void* Parameters) const { if( InvocationList.Num() > 0 ) { // Create a copy of the invocation list, just in case the list is modified by one of the callbacks during the broadcast typedef TArray< TScriptDelegate<TWeakPtr>, TInlineAllocator< 4 > > FInlineInvocationList; FInlineInvocationList InvocationListCopy = FInlineInvocationList(InvocationList); // Invoke each bound function for( typename FInlineInvocationList::TConstIterator FunctionIt( InvocationListCopy ); FunctionIt; ++FunctionIt ) { if( FunctionIt->IsBound() ) { // Invoke this delegate! FunctionIt->template ProcessDelegate<UObjectTemplate>(Parameters); } else if ( FunctionIt->IsCompactable() ) { // Function couldn't be executed, so remove it. Note that because the original list could have been modified by one of the callbacks, we have to search for the function to remove here. RemoveInternal( *FunctionIt ); } } } }与多播委托类似，也是会在调用前先用FunctionIt->IsBound()进行判断，确保执行安全。当然，前面提到了动态委托运行速度很慢，所以您可以猜到动态多播会是本文中所有的委托中执行最慢的。参考注意：因为文章经过多次修改，因此实际上这里的顺序与文中提及的顺序不一致。LaTeX对引用顺序的处理就很好，所以后面我可能会考虑改用LaTeX来做这类笔记UE 4.26源代码 ↩官方文档：委托：严重过时的官方文档，请以最新源代码内容为准 ↩关于各类委托之间的不同点的讨论 ↩C++中实现委托：如果好奇在纯C++代码中如何实现委托，那么可以参考这篇文章 ↩全面理解UE4委托 ↩UE4：4.26版本对Delegate模块的改进 ↩C++ 模板，特化，与偏特化 ↩UE4-深入委托Delegate实现原理：这篇文章可以说是帮了大忙，不过本文部分内容实际上参考了这里的分析。但是文中有一部分内容已经对应不上4.26及以后的版本的源代码了。不过，还是非常值得一看，强烈推荐 ↩FFuncPtr官方文档：这个官方文档和往常一样，写得和没写一样，建议看别的 ↩【UE4笔记】各种Delegate委托的区别和应用 ↩
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在游戏开发过程中我们往往需要创建一系列的工具来辅助我们开发，例如 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 or overriding engine classes.下面简单翻译一下。UE4 会自动实例化你编写的 Subsystem，并且根据你的 Subsystem 类型（目前有 5 种类型）管理 Subsystem 的生命周期。Subsystem 能够暴露接口给蓝图和 Python 使用，不需要修改或者继承引擎的类（如 GameInstance）。目前 UE4 支持的 Subsystem 类型有以下 5 种：Engine 类：UEngineSubsystemEditor 类：UEditorSubsystemGameInstance 类：UGameInstanceSubsystemWorld 类：UWorldSubsystemLocalPlayer 类：ULocalPlayerSubsystem名称分别对应他们依存的 Outer 对象（称之为 Outer 是因为源代码里面指向这些对象的指针名为 Outer），以及他们对应的生命周期分别是：UEngine* GEngine（引擎启动期间存在，或者说游戏进程期间存在）;UEditorEngine* GEditor（编辑器启动期间存在）;UGameInstance* GameInstance（游戏运行期间存在）;UWorld* World（关卡运行期间存在，一个游戏可能会有多个关卡，另外要注意的是编辑器下看到的场景其实也是一个 World）;ULocalPlayer* LocalPlayer（本地玩家存在的时候存在，实际上通常和 GameInstance 生命周期差不多，但是可能有多个本地玩家，而且游戏进行过程中可以随时添加减少本地玩家，所以生命周期视情况、Outer 对象依附于哪个 LocalPlayer 而定）;他们其实之间没什么区别，默认的功能都较为相似，目前主要的区别在于不同类型的 Subsystem 生命周期不同。所有的 Subsystem 都直接或者间接继承了USubsystem类。我们用张图来大致展示一下各个系统的类的关系：图中的UDynamicSubsystem，前面没有提到，所以这里简单介绍下。我们可以看到图中主要是 EditorSubsystem 和 EngineSubsystem 继承了 DynamicSubsystem，这是因为这两类 Subsystem 主要是类似模块，能够随时加载和卸载。而 DynamicSubsystem 就能提供这种功能，让这类 Subsystem 只有在需要的时候加载进入编辑器或者游戏引擎中，不需要的时候就可以卸载掉。UDynamicSubsystem提供的额外功能只有加载和卸载功能。其他 3 类 Subsystem 会在对应的 Outer 对象生命周期内自动创建，Outer 对象生命周期结束的时候才会被自动销毁。这 3 类 Subsystem 相比于没有继承 DynamicSubsystem 的 Subsystem 少了加载和卸载的功能，其他方面没什么区别。编写自定义的 Subsystem 的时候只需要关注 Subsystem 用在什么场景和具备什么样的生命周期和需不需要动态加载卸载即可。看回到 USubsystem，可以注意到旁边的FSubsystemCollectionBase，这个类主要用来管理某一类型的 Subsystem，负责 Subsystem 的创建、销毁、查询（依靠查询功能，Subsystem 可以相互之间通信）。每个类型的 Subsystem 模块都会产生一个相对应的SubsystemCollection，例如GameInstanceSubsystemCollection。SubsystemCollection底层实际会包含一个TMap变量，用来保存每个特定类型USubsystem子类的实例（如UGameInstanceSubsystem子类的实例）。因为 TMap 会保证每个 Key 对应的 Value 唯一，而 Key 就是子类，所以在这个 Outer 对象对应的生命周期中只能创建出一个这个子类的对象。最终每个用户自定义的 Subsystem 子类生成对象都会是单例（例如用户编写了一个UMyGameInstanceSubsystem类，那么在 GameInstance 的生命周期中只能创建一个UMyGameInstanceSubsystem对象）。另外，FSubsystemCollectionBase继承了FGCObject，所以FSubsystemCollection内的对象会受到 UE4 的 GC 管理。UE4 的 GC 算法在这里不是重点，所以这里不详细说明。另另外，上图中的UMy*Subsystem都是代表用户自己创建的 Subsystem，用户编写自己的 Subsystem 的时候只需要继承特定类型的 Subsystem 即可，引擎会自动管理这些 Subsystem 的生命周期，保证和 Outer 对象的生命周期一致。大概了解了 Subsystem 的概念、构成之后，下面开始简单说明各个类型的 Subsystem 的生命周期、作用等。因为内容比较多所以我会把重点放在比较常用的 GameInstanceSubsystem 上，毕竟实际上功能差不太多，能够弄明白 GameInstance 就基本上可以弄明白其他的 Subsystem。如果有机会，其他的 Subsystem 后面我会详细说明它们的具体功能与实现。我们先从USubsystem类开始。USubsystem类首先我们从上述 5 种 Subsystem 共同的基类，USubsystem类说起。USubsystem也继承了UObject，因此和其他UObject一样，具有反射、元数据、序列化、被 UE4 自动 GC 等功能，可以和UObject一样添加各类UFUNCTION、UPROPERTY。这里不详细介绍UObject，如果感兴趣可以另外自己查询相关资料，或者有机会再另外总结，详细说明下。首先看看基类的定义：注意USubsystem被标记为了Abstract，抽象类，所以不要尝试去实例化它。接下来，我们看看USubsystem定义了什么接口。可以看出USubsystem这个抽象类本身是比较简单的，接口并不多，我们一个个介绍。ShouldCreateSubsystem用来控制是否创建 Subsystem，可以重写来自己控制什么时候创建 Subsystem，例如我们有部分 Subsystem 是只在客户端运行的， 不希望在服务端加载，那么就可以重写这个接口，保证我们写的 Subsystem 只在客户端被实例化。Initialize会在 Subsystem 实例化的时候调用，我们可以重写这个接口来初始化我们的 Subsystem。注意他的参数是FSubsystemCollectionBase& Collection，这使得 Subsystem 可以在实际上创建完成前获取到外部 Outer（这是一个UObject类型的指针，用来指向外部的对象，这个对象主要取决于 Subsystem 的类型，例如如果是 GameInstance 类型的那么就是指向 GameInstance），从而获取到其他的 Subsystem 对象。Deinitialize则是在 Subsystem 被销毁的时候执行，我们重写这个接口可以用来善后，例如释放掉 Subsystem 正在占用的资源。GetFunctionCallspace主要用来查看网络状态，他的默认实现是这样的：继续深入，可以看到 GEngine 下是这样描述的：该函数主要用于判断当前是不是在远端调用（即运行这段代码的时候是在服务端还是在客户端），Subsystem 重写之后可以用来做网络相关的功能。在私有变量中我们可以看到FSubsystemCollectionBase被声明为了友元类，这使得FSubsystemCollectionBase重的函数可以随意访问USubsystem中定义的函数与成员变量（另，FSubsystemCollectionBase继承了FGCObject，不然 F 开头的纯 C++类无法访问/管理 U 开头的 UE4 的类，如果感兴趣的话可以看一下相关的资料，这里不赘述)。FSubsystemCollectionBase这部分涉及到的代码太多，所以这里只是简单叙述下，不会说得太细（毕竟不是代码笔记）。后面有机会再详细解析。我们主要关注的地方是这个类怎么初始化我们的 Subsystem，怎么销毁的。初始化我们前面提到了 5 类 Outer 对象，这些对象实际上自己有一个变量FSubsystemCollection，专门用来保存 Subsystem，例如 GameInstance 中：又或者 World 中：（说句题外话，看一下旁边的行数就知道完全搞懂 UE4 是不现实的……World 这里的截图，3687 行还只是头文件的代码量)而这些 Outer 对象在初始化的时候会把自己传入到SubsystemCollection的Initialize中：而SubsystemCollection继承了SubsystemCollectionBase，如下图：因此最终执行的其实是SubsystemCollectionBase的Initialize：void FSubsystemCollectionBase::Initialize(UObject* NewOuter) { // 省略部分代码 if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass())) //如果是UDynamicSubsystem的子类 { // 省略初始化Dynamic类型Subsystem部分的代码 } else { //普通Subsystem对象的创建 TArray<UClass*> SubsystemClasses; GetDerivedClasses(BaseType, SubsystemClasses, true); //反射获得所有子类 for (UClass* SubsystemClass : SubsystemClasses) { AddAndInitializeSubsystem(SubsystemClass); //添加初始化Subsystem对象创建 } } }if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))将这部分代码分成了两部分，在这里面条件成立的情况是支持UDynamicSubsystem的Subsystem类型的初始化代码（Editor和Engine类型的Subsystem），其他的是较为简单的GameInstance、World、LocalPlayer类型的Subsystem。我们首先看比较简单的不是Dynamic的Subsystem部分，这里执行的操作实际上只有2步：通过反射获取BaseType（5种基本Subsystem类型）的子类；2. 全部每个单独进行AddAndInitializeSubsystembool FSubsystemCollectionBase::AddAndInitializeSubsystem(UClass* SubsystemClass){ //...省略一些判断语句 const USubsystem* CDO = SubsystemClass->GetDefaultObject<USubsystem>(); //从CDO调用ShouldCreateSubsystem来判断是否要创建 if (CDO->ShouldCreateSubsystem(Outer)) { //创建且添加到TMap里 USubsystem*& Subsystem = SubsystemMap.Add(SubsystemClass); //创建对象 Subsystem = NewObject<USubsystem>(Outer, SubsystemClass); //保存父指针 Subsystem->InternalOwningSubsystem = this; //调用Initialize Subsystem->Initialize(*this); return true; } }简单的说就是根据你重写的ShouldCreateSubsystem，以及依存的Outer对象，来创建Subsystem对象（并将Subsystem的持有者设定为输入的Outer对象），并且添加到SubsystemMap里面，最后调用用户重写的Initialize进行Subsystem的初始化。而且因为生成的实例保存的地方是TMap类型的SubsystemMap，所以最后可以保证每个Subsystem子类只生成一个实例，相当于实现了单例模式。下图是SubsystemMap的定义：Dynamic类型的Subsystem较为复杂点，这里只是简单介绍下大概的初始化过程。Dynamic类型的Subsystem的初始化首先看下DynamicSubsystem的声明：构造函数的实现：可以看到，实际上没有添加功能，只是相当于用来标记一个类别而已。实际动态加载卸载的功能还是通过FSubsystemCollectionBase实现。让我们回过头来看FSubsystemCollectionBase::Initiate：void FSubsystemCollectionBase::Initialize(UObject* NewOuter){ // 省略部分检查代码 if (SubsystemCollections.Num() == 0) // SubsystemCollections实际上是静态变量，这里通过内容数量判断是不是第一次创建 { // 初始化FSubsystemModuleWatcher，监听模块的加载与卸载用 FSubsystemModuleWatcher::InitializeModuleWatcher(); } // 省略 if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))// 如果是UDynamicSubsystem的子类 { // 注意这里的DynamicSystemModuleMap，实际上一部分官方自己写的Subsystem就在这里面 for (const TPair<FName, TArray<TSubclassOf<UDynamicSubsystem>>>& SubsystemClasses : DynamicSystemModuleMap) { for (const TSubclassOf<UDynamicSubsystem>& SubsystemClass : SubsystemClasses.Value) { if (SubsystemClass->IsChildOf(BaseType)) { AddAndInitializeSubsystem(SubsystemClass); } } } } else { //普通Subsystem对象的创建，省略 }这段代码比较简单，所以只是简单说一下做了什么。首先我们看到一开始就判断SubsystemCollections内容数量是不是为0，这个变量在头文件SubsystemCollection.h的定义如下：可以看到，是一个静态变量，所以实际上是在判断是不是第一次创建（因为引擎里有部分组件创建并添加进了这个变量之后就不会再移除，直到引擎关闭，所以可以这么干）。可以看到原代码中，第一次创建的时候就会调用FSubsystemModuleWatcher::InitializeModuleWatcher()来登记每个模块用到的所有DynamicSystem子类。随后会把DynamicSystemModuleMap中记录的DynamicSubsystem子类模版（原代码是TArray<TSubclassOf<UDynamicSubsystem>>）传入到函数AddAndInitializeSubsystem中正式开始初始化。因为AddAndInitializeSubsystem在上面非动态的Subsystem讲解中已经解释过了，就是简单地遍历并且初始化实例。所以这里着重看第一步，即FSubsystemModuleWatcher::InitializeModuleWatcher()的具体实现：void FSubsystemModuleWatcher::InitializeModuleWatcher(){ check(!ModulesChangedHandle.IsValid()); // 这里会获取所有UDynamicSubsystem的子类 TArray<UClass*> SubsystemClasses; GetDerivedClasses(UDynamicSubsystem::StaticClass(), SubsystemClasses, true); for (UClass* SubsystemClass : SubsystemClasses) { // 排除抽象类 if (!SubsystemClass->HasAllClassFlags(CLASS_Abstract)) { // 获取Subsystem对应的包 UPackage* const ClassPackage = SubsystemClass->GetOuterUPackage(); if (ClassPackage) { const FName ModuleName = FPackageName::GetShortFName(ClassPackage->GetFName()); if (FModuleManager::Get().IsModuleLoaded(ModuleName)) { // 初始化DynamicSubsystem并添加到静态变量DynamicSystemModuleMap，注意ModuleSubsystemClasses实际上是一个引用 TArray<TSubclassOf<UDynamicSubsystem>>& ModuleSubsystemClasses = FSubsystemCollectionBase::DynamicSystemModuleMap.FindOrAdd(ModuleName); ModuleSubsystemClasses.Add(SubsystemClass); } } } } // 添加监听事件，这里把函数OnModulesChanged与事件相关联了，这个事件是在模块加载和卸载的时候会被触发的 ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddStatic(&FSubsystemModuleWatcher::OnModulesChanged); }上面的DynamicSystemModuleMap（出现在了FSubsystemCollectionBase::Initialize和FSubsystemModuleWatcher::InitializeModuleWatcher中），是一个static类型变量：主要用来记录当前动态加载的Module和与其对应的所有UDynamicSubsystem类型，与FSubsystemModuleWatcher相关。总之这里只是简单的创建并按照模块来添加到DynamicSystemModuleMap中，后面加载和卸载模块的时候就要依赖DynamicSystemModuleMap来创建或者销毁模块对应的一系列DynamicSubsystem（不用担心多个模块重复用到了某个DynamicSubsystem子类而导致在销毁的时候删除某个其他模块仍要使用的子类对象。因为实际上会有GC系统来管理这些对象，只有所有模块都不会引用某个DynamicSubsystem对象，这个对象才会发生GC）。另外，注意这里是TArray<TSubclassOf<UDynamicSubsystem>>。这里是TArray的原因是我们的模块可能会依赖多个DynamicSubsystem子类，模块所有要用到的DynamicSubsystem子类模版类都会保存在TArray中。我们继续看下去，看看FSubsystemModuleWatcher::OnModulesChanged的实现：void FSubsystemModuleWatcher::OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange) { switch (ReasonForChange) { case EModuleChangeReason::ModuleLoaded: // 创建模块 AddClassesForModule(ModuleThatChanged); break; case EModuleChangeReason::ModuleUnloaded: // 销毁模块 RemoveClassesForModule(ModuleThatChanged); break; } }这个事件在每次加载或者卸载模块的时候都会触发，实际上就是依赖这个事件来实现对DynamicSystem子类的动态加载和卸载。接下来我们看看创建模块的具体实现：void FSubsystemModuleWatcher::AddClassesForModule(const FName& InModuleName) { // 找到模块对应的代码包 const UPackage* const ClassPackage = FindPackage(nullptr, *(FString("/Script/") + InModuleName.ToString())); TArray<TSubclassOf<UDynamicSubsystem>> SubsystemClasses; TArray<UObject*> PackageObjects; // 得到模块定义的所有对象 GetObjectsWithOuter(ClassPackage, PackageObjects, false); for (UObject* Object : PackageObjects) { // 尝试把包对象转成UClass类的对象 UClass* const CurrentClass = Cast<UClass>(Object); // 确保不是空指针，不是抽象类，是UDynamicSubsystem的子类 if (CurrentClass && !CurrentClass->HasAllClassFlags(CLASS_Abstract) && CurrentClass->IsChildOf(UDynamicSubsystem::StaticClass())) { SubsystemClasses.Add(CurrentClass); // 为这个类创建实例 FSubsystemCollectionBase::AddAllInstances(CurrentClass); } } // 如果其内部有定义Subsystem类，那么就登记 if (SubsystemClasses.Num() > 0) { // 登记到DynamicSystemModuleMap静态变量里面 FSubsystemCollectionBase::DynamicSystemModuleMap.Add(InModuleName, MoveTemp(SubsystemClasses)); } }AddClassesForModule的步骤可以总结为：获取模块定义的所有包对象将包对象转换为UClass类，判断是不是UDynamicSubsystem的子类，并且不是抽象类（是的，其实你可以继承UDynamicSubsystem并且声明为抽象类）第二步的判断通过，符合条件则开始用转换成UClass的UDynamicSubsystem类创造实例4. 把PackageObject中的所有DynamicSubsystem子类都创建好之后就会添加到静态变量FSubsystemCollectionBase::DynamicSystemModuleMap中（代码包中可能不只是定义/引用了一个DynamicSubsystem子类，所以存放的内容实际上是DynamicSubsystem子类数组）另外，创建实例的实现如下：void FSubsystemCollectionBase::AddAllInstances(UClass* SubsystemClass){ for (FSubsystemCollectionBase* SubsystemCollection : SubsystemCollections) { if (SubsystemClass->IsChildOf(SubsystemCollection->BaseType)) { // 前面解释过，用来创建对象 SubsystemCollection->AddAndInitializeSubsystem(SubsystemClass); } } }可以看到，最终创建实例的过程实际上就是和非动态的Subsystem（GameInstance、LocalPlayer、World）创建实例的过程是一样的。所以实际上是一开始启动的时候触发FSubsystemModuleWatcher::InitializeModuleWatcher，加载所有用到的UDynamicSubsystem子类，随后调用FSubsystemCollectionBase::AddAndInitializeSubsystem来Initialize所有FSubsystemCollectionBase::DynamicSystemModuleMap中的UDyanmicSubsystem子类，最后把生成的所有的UDynamicSubsystem子类实例添加到静态变量FSubsystemCollectionBase::SubsystemMap中。如果是动态加载那么会直接触发事件，调用FSubsystemCollectionBase::AddAllInstances，最后还是调用FSubsystemCollectionBase::AddAndInitializeSubsystem来生成实例。再提一嘴，FSubsystemCollectionBase::DynamicSystemModuleMap实际上是以模块划分，key就是模块名，value就是模块依赖的UDynamicSubsystem子类。单个模块可能需要用到多个UDynamicSubsystem，所以value是TArray类型的变量。后面加载或者释放某个模块的时候能够根据DynamicSystemModuleMap中的记录，知道该创建和销毁什么类型的实例。对于DynamicSubsystem来说实际上多了个FSubsystemModuleWatcher来管理，因此实际上我们可以把关系图更新为：销毁实际上每个 Outer 对象销毁的时候会调用SubsystemCollection.Deinitialize();，例如 GameInstance 的：Deinitialize代码如下：void FSubsystemCollectionBase::Deinitialize() { //...省略一些清除代码 for (auto Iter = SubsystemMap.CreateIterator(); Iter; ++Iter) //遍历Map { UClass* KeyClass = Iter.Key(); USubsystem* Subsystem = Iter.Value(); if (Subsystem->GetClass() == KeyClass) { Subsystem->Deinitialize(); //反初始化 Subsystem->InternalOwningSubsystem = nullptr; } } SubsystemMap.Empty(); Outer = nullptr; }可以看出，就是遍历然后逐个执行用户重写的Deinitialize。但是，此时 Subsystem 实际上还没有完全被 GC，看到上面的SubsystemMap.Empty()了吗？还记得 Subsystem 实际上是 UObject 吗？还记得我们提到过 FSubsystemCollectionBase 继承了 FGCObject，所以 F 开头的纯 C++类可以引用U 开头的 UE4 类型对象，从而能够让 UE4 的 GC 系统管理引用的对象吗？在 FSubsystemCollectionBase 中有以下代码：在SubsystemMap.Empty()后，因为保存的 Subsystem 不再被引用了，所以在下一帧 GC 系统介入的时候，会将原本保存在 Map 中的 Subsystem 对象判定为 PendingKill，并且开始 GC 销毁这些 Subsystem 对象（另外提一嘴，实际上 UE4 也是这么处理创建的 Widget 的，所以不建议手动销毁，直接不引用，让 GC 系统处理就好了）。Dynamic 类型 Subsystem 的销毁void FSubsystemModuleWatcher::RemoveClassesForModule(const FName& InModuleName) { TArray<TSubclassOf<UDynamicSubsystem>>* SubsystemClasses = FSubsystemCollectionBase::DynamicSystemModuleMap.Find(InModuleName); if (SubsystemClasses) { for (TSubclassOf<UDynamicSubsystem>& SubsystemClass : *SubsystemClasses) { // 销毁这个类的所有对象 FSubsystemCollectionBase::RemoveAllInstances(SubsystemClass); } // 移除登记 FSubsystemCollectionBase::DynamicSystemModuleMap.Remove(InModuleName); } }DyanamicSubsystem 在卸载和被销毁的时候都会触发事件OnModulesChanged，最终调用上面这个函数，比较简单所以不解释了。比较疑惑的可能就是FSubsystemCollectionBase::RemoveAllInstances函数，我们看看它的具体实现：void FSubsystemCollectionBase::RemoveAllInstances(UClass* SubsystemClass) { // 遍历属于该类型的实例 ForEachObjectOfClass(SubsystemClass, [](UObject* SubsystemObj) { USubsystem* Subsystem = CastChecked<USubsystem>(SubsystemObj); if (Subsystem->InternalOwningSubsystem) { // 释放掉Subsystem实例 Subsystem->InternalOwningSubsystem->RemoveAndDeinitializeSubsystem(Subsystem); } } }可以看到实际上还是调用FSubsystemCollectionBase::RemoveAndDeinitializeSubsystem来遍历删除 Subsystem 子类的实例：void FSubsystemCollectionBase::RemoveAndDeinitializeSubsystem(USubsystem* Subsystem) { check(Subsystem); USubsystem* SubsystemFound = SubsystemMap.FindAndRemoveChecked(Subsystem->GetClass()); check(Subsystem == SubsystemFound); Subsystem->Deinitialize(); Subsystem->InternalOwningSubsystem = nullptr; }可以看到，调用了用户重写的Deinitialize。说实话，前面基本上已经说完需要说的了，因为这些不同类型的 Subsystem 实际上只是定义了一些接口，自带的功能并不多，所以以下部分都会只是很简单的介绍下。Engine 类型的 SubsystemUE4 里面这种 Subsystem 的类名为“UEngineSubsystem”，这类 Subsystem 和引擎一起启动，在游戏进程启动开始的时候创建，进程结束销毁，运行期间一直是全局唯一，适用于开发引擎工具。Editor 类型的 Subsystem和编辑器一起启动，如果是 Runtime 的游戏的话那么不会启动，只会存在编辑器下，且全局唯一。在编辑器启动的时候开始创建，编辑器退出的时候销毁。GameInstance 类型的 Subsystem比较常用的 Subsystem。和游戏一起启动，游戏退出的时候销毁。只会在游戏 Runtime 或者 PIE（Play In Editor，在编辑器中启动的预览游戏场景）模式中存在。常常用于编写各类数据管理工具。例如我们有些时候希望能够有一个统一的界面管理系统，因为所有的 World 中都会用到 UI，而且有时候切换 World 也需要显示一个加载界面的 UI，因此不可能是 World 类型的 Subsystem。这时候我们往往会将相关的逻辑写在一个自己创建的 GameInstanceSubsystem 子类下，因为 GameInstance 类型的 Subsystem 能够在整个游戏进行期间存在（与 GameInstance 生命周期一致），独立于 World 的加载与切换。这类 Subsystem 只是多了一个获取 GameInstance 的函数。World 类型的 Subsystem和关卡 World 一起启动和销毁，数量可能大于 1（毕竟大多数游戏不止一个关卡）。生命周期和 GameMode 是一起的。不过要注意的地方是，UE4 编辑器里面预览的场景其实也是一个 World，所以实际上在预览场景里面可能也会创建 World 类型的 Subsystem，如果不想要你的 WorldSubsystem 在预览场景里面创建的话就要在ShouldCreateSubsystem里面做好判断。LocalPlayer 类型的 Subsystem和本地玩家一起创建和销毁，数量可能大于 1（例如本地分屏多玩家类型的游戏，在多个玩家的时候就会创建多个 LocalPlayer 的 Subsystem）。每个 LocalPlayer 会维护自己的 LocalPlayer 类型的 Subsystem，所以可能会有多个 ULocalPlayerSubsystem 子类实例，但是对于每个 LocalPlayer 来说都是单例。Subsystem 的使用Subsystem 的调用十分便利，因为官方已经包装好了相关的蓝图接口，所以在蓝图里面也可以调用 Subsystem 暴露出来的给蓝图调用函数（或者可以在 Subsystem 里面定义好BlueprintImplementableEvent，用 Subsystem 调用蓝图函数）。对应的 C++源码如下：在蓝图中的使用：而如果是在 C++中调用的话则是：//UMyEngineSubsystem获取 UMyEngineSubsystem* MySubsystem = GEngine->GetEngineSubsystem<UMyEngineSubsystem>(); //UMyEditorSubsystem的获取 UMyEditorSubsystem* MySubsystem = GEditor->GetEditorSubsystem<UMyEditorSubsystem>(); //UMyGameInstanceSubsystem的获取 UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(...); UMyGameInstanceSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameInstanceSubsystem>(); //UMyWorldSubsystem的获取 UWorld* World=MyActor->GetWorld(); //也都可以用其他方式获取World UMyWorldSubsystem* MySubsystem=World->GetSubsystem<UMyWorldSubsystem>(); //UMyLocalPlayerSubsystem的获取 ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(PlayerController->Player) UMyLocalPlayerSubsystem * MySubsystem = LocalPlayer->GetSubsystem<UMyLocalPlayerSubsystem>();注意如果使用EditorSubsystem的话就要在工程名.build.cs里面加上EditorSubsystem模块的饮用，因为这算是编辑器模块：// ... 省略部分内容 if (Target.bBuildEditor) { // 最重要的地方 PublicDependencyModuleNames.AddRange(new string[] { "EditorSubsystem" }); }参考1.官方Subsystem文档：建议直接看UE4源码，官方有部分地方不是特别详细（而且上面的信息不全），目前网上资料偏少，不如直接看源码2.《InsideUE4》GamePlay架构（十一）Subsystems：必看，很详细，能说的基本都说了3.[[英文直播]Programming Subsystems(真实字幕组)](https://www.bilibili.com/video/BV1t741187FZ)4.UE4.22 Subsystem分析：建议一读，写得不会涉及太多细节，但是该讲的都基本覆盖到了5.【UE4 C++】编程子系统 Subsystem7.UE4实验使用 FGCObject 引用UObject8.【UE4】TSubclassOf的使用