夜溅樱的茶馆 - UnrealEngine
type: channel
link: https://srin.top/index.php/category/UE
description: 用于保存虚幻引擎的使用经验与笔记。
items: 一个由 BlueprintPure 函数引起的 “Bug”
items: UE5中 uDraper 插件无法编译 C++ 工程的修复
items: UE 中脚部 IK 使用总结
items: UE4技术总结——委托
items: FString与标准库string的转换
items: UE4 中的 Subsystem
items: UE_LOG打印出FString
items: 关于使用 Sequence Recorder 无法录制面部 Morph Target 问题的解决方法

一个由 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…

UE_LOG打印出FString
type: item
date: 2022-06-15T10:13:48+08:00
description: UE_LOG打印出FStringFString FString2Print = TEXT("FSTRING TEST"); UE_LOG(LogTemp, Warning, TEXT("Printing FString: %s"), *FString2Print);只需要在FString前加上*即可用%s打印出来，其实就是转换为TCHAR数组，和C++里面的string.c_str()差不多

关于使用 Sequence Recorder 无法录制面部 Morph Target 问题的解决方法
type: item
date: 2022-06-09T17:37:00+08:00
description: 关于使用 Sequence Recorder 无法录制面部 Morph Target 问题的解决方法这里 Sequence Recorder 包含动画蓝图中默认的录制窗口：总体上来说有两种方法，一种是基于蓝图和代理骨骼，一种是通过C++代码实现。1. 蓝图加代理骨骼的实现主要是通过这个节点实现：选中节点，然后选中充当代理的骨骼（注意，最好是动画用不到的骨骼，不然大概率会出现奇怪的效果，如果没有这种可以自己在导入引擎后的 Skeleton 文件中创建虚拟骨骼）：上图中选中了 TONGLE 的 Translation X 作为数据来源，即骨骼的坐标 X 值作为数据来源。再设置下要设置数据的目标：这里设置的目标类型是名为 jawOpen 的 Morph Target。此外，目标也可以设置为另一个骨骼，以及 Material Parameter，这里不作展开。最后类似这样连接，就可以通过 TONGLE 骨骼的坐标 X 来控制 jawOpen 了：最左侧的 Transform (Modyfy) Bone 只是演示用，实际上工程中都是动画源。1.1. 优缺点优点：蓝图实现方便，能够快速验证；适用于只需要更新少量 Morph Target 的场合；缺点：Morph Target 一多起来（例如 ARKit 那种52个 Morph Target 的情况）就会很难管理，代码会很杂乱（这算是蓝图通病）；不适合协作修改（蓝图 Merge 起来还是不太方便）；可能需要创建大量的代理骨骼（一个骨骼最多只能传数据到9个 Morph Target）；2. 代码实现我比较推荐通过代码实现，总体步骤是创建插件，创建新的继承AnimNode_Base动画节点（或者其他继承AnimNode_Base的子类例如AnimNode_SkeletalControlBase），然后在动画节点的Evaluate_AnyThread（如果是其他AnimNode_Base的子类那么可能是EvaluateXXX_AnyThread，例如AnimNode_SkeletalControlBase的EvaluateComponentSpace_AnyThread）。下面是详细步骤。2.1. 创建插件这一步没什么好说的，直接通过引擎 Plugin 界面创建就行：插件名字等内容可以随意填，插件类型建议选Blank：2.2. 创建动画节点这里以AnimNode_Base作为基类为例子，实际上可以用别的动画节点类，可以根据业务需求选择。我们重点看AnimNode部分，除了AnimNode还需要创建AnimGraphNode，这里不详细说，因为创建默认的就可以了。假设我们创建的类为：USTRUCT(BlueprintInternalUseOnly) struct DATALINK_API FAnimNode_DLMoCap : public FAnimNode_Base { // 这个一定要有 GENERATED_USTRUCT_BODY() // ...这里先省略 }重点是重载FAnimNode_Base的这个函数：virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override; virtual void Update_AnyThread(const FAnimationUpdateContext & Context) override; virtual void Evaluate_AnyThread(FPoseContext& Output) override;其他函数基本上不怎么需要修改，而Initialize_AnyThread和Update_AnyThread主要用来初始化、加载数据，所以实际上重点只是Evaluate_AnyThread。在Evaluate_AnyThread函数中，最重要的部分则是：void FAnimNode_DLMoCap::Evaluate_AnyThread(FPoseContext& Output) { // 忽略这一行，主要是用来接受上游动画节点的动画数据，和我们要做的事情不太相关 SourcePose.Evaluate(Output); // 获取骨骼 const USkeleton* Skeleton = Output.AnimInstanceProxy->GetSkeleton(); // 从骨骼上获取UID，实际上相当于是根据名字获取动画曲线 Curve 的 UID // 这里的 jawOpen 最好是一个 FName 类型的变量，这样方便传入 Morph Target 名字，这里只是演示用所以直接输入了名字 const SmartName…