夜溅樱的茶馆 - 2022年6月
type: channel
link: https://srin.top/index.php/2022/06/
description: Virtus et Honor
items: UE4技术总结——委托
items: FString与标准库string的转换
items: UE4 中的 Subsystem
items: UE_LOG打印出FString
items: 关于使用 Sequence Recorder 无法录制面部 Morph Target 问题的解决方法

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…