Unreal Engine 第三方插件的开发:演示一下如何使用 LiveLink 调用 OpenVR(作为第三方库),获取 VR Tracker 数据(外部输入),并控制场景物体的移动。

Live Link 是虚幻引擎中数据流传输通用接口插件。它的主要作用是从外部第三方提供的 DDC 工具,以及第三方提供的外设服务中实时获取数据流,用于开发环节或者最终产品的 Actor 驱动。

在 UE 4.27 之后(包括现在的 5.x),LiveLink 已经成为官方推荐的 UE 统一的外部数据源。

UnrealBuildTool

Unreal 为了构建 UE 项目,开发了一套构建工具,主要使用 C# 编写的。如果我们要开发 Unreal 的新 Program,调用 Unreal 的 API,那么就需要使用这套构建工具。

至于 UnrealBuildTool 的细节,就不展开了。Unreal 源码中附带了一个 BlankProgram(Engine\Source\Programs\BlankProgram),可以直接拷贝并改名。

不难发现,项目结构非常简单,主要是靠 .Build.cs.Target.cs 来说明构建信息。拷贝文件夹后,使用 GenerateProjectFiles.bat(Windows 下) 就可以将新的 Program 加入到了 Visual Studio 工程中。

.Build.cs 描述了如何构建模块,定义模块的依赖。

项目布局:

1
2
3
4
5
6
7
8
9
10
├─Private
├─Resources
│ └─Windows
└─ThirdParty
└─openvr
├─headers
└─lib
└─win64
MyLiveLinkProvider.Build.cs
MyLiveLinkProvider.Target.cs

程序的 Build.cs 大概是这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using UnrealBuildTool;
using System.IO;

public class MyLiveLinkProvider : ModuleRules
{
public MyLiveLinkProvider(ReadOnlyTargetRules Target) : base(Target)
{
PublicIncludePaths.Add("Runtime/Launch/Public");

PrivateIncludePaths.Add("Runtime/Launch/Private"); // For LaunchEngineLoop.cpp include

PrivateDependencyModuleNames.AddRange(new string[]
{
"ApplicationCore",
"Core",
"CoreUObject",
"Projects",
"LiveLinkInterface",
"LiveLinkMessageBusFramework",
"Messaging",
"UdpMessaging"
});

PrivateIncludePathModuleNames.AddRange(new string[]
{
"Launch"
});

PublicIncludePaths.AddRange(new string[]
{
Path.Combine(ModuleDirectory, "ThirdParty/openvr/headers")
});

if (Target.Platform == UnrealTargetPlatform.Win64)
{
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty/openvr/lib/win64", "openvr_api.lib"));
}

}
}

首先,UE4 模块中的代码时默认 不暴露 给外部模块的,所以需要导出给外部模块的代码,或者部分代码需要放到 Public 下面。同样,不想暴露的头文件可以放进 Private 文件夹。如果你私有包含一个模块 A,那么父模块调用你的模块时,是无法获取模块 A 的。如果你是公有包含模块 A,那么父模块调用你的模块的时候,是可以获取模块 A 的头文件的(但是也不会链接)。

  • PublicIncludePaths:公有头文件路径
    • PrivateIncludePaths:私有头文件路径
  • PrivateDependencyModuleNames:会进行链接
    • PrivateIncludePathModuleNames:只包含头文件,不会链接
  • PublicAdditionalLibraries:第三方附加库

命令行

Unreal 封装了一些有用的基础工具来加速开发,其中就包括命令行工具 FCommandLine,可以用来解析命令行参数。

1
2
3
4
5
6
7
FString BuildFromArgV
(
const TCHAR* Prefix,
int32 ArgC,
TCHAR* ArgV,
const TCHAR* Suffix
)

ArgC / ArgV 读取参数并组成字符串,可以附带前缀和后缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FMyLiveLinkProviderCoreInitArgs::FMyLiveLinkProviderCoreInitArgs(int32 ArgC, TCHAR* ArgV[])
{
FString CmdLine = FCommandLine::BuildFromArgV(nullptr, ArgC, ArgV, nullptr);
FCommandLine::Set(*CmdLine);

FString Value;

if (FParse::Value(*CmdLine, TEXT("-Framerate="), Value))
{
FParse::Value(*Value, TEXT("Numerator="), Framerate.Numerator);
FParse::Value(*Value, TEXT("Denominator="), Framerate.Denominator);
}

FParse::Value(*CmdLine, TEXT("-SourceName="), SourceName);

if (FParse::Value(*CmdLine, TEXT("-SubjectName="), Value))
{
Value.ParseIntoArray(SubjectNames, TEXT(";"), true);
}

if (FParse::Value(*CmdLine, TEXT("-SubjectBaseLocation="), Value))
{
SubjectBaseLocation.InitFromString(Value);
}

if (FParse::Value(*CmdLine, TEXT("-SubjectLocationScale="), Value))
{
SubjectLocationScale.InitFromString(Value);
}
}

字符串

  • TCHAR:对 charwchar_t 的封装
    • TCHAR* CharString = TEXT("Hello")
  • FName:不变的字符串,比如资源路径、文件类型
    • FName TestName = FName(TEXT("Hello"))
  • FText:静态字符串,负责处理文本的本地化,在 UI 中显示

Json

Build.cs 中添加 jsonJsonUtilities

1
2
3
4
5
6
PrivateDependencyModuleNames.AddRange(new string[]
{
//...
"Json",
"JsonUtilities"
});

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const FString JsonFilePath = FPaths::Combine(FPlatformProcess::BaseDir(), "data.json");

FString JsonString;
FFileHelper::LoadFileToString(JsonString, *JsonFilePath);

TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());
TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(JsonString);

if (FJsonSerializer::Deserialize(JsonReader, JsonObject) && JsonObject.IsValid())
{
TSharedPtr<FJsonObject> PersonObject = JsonObject->GetObjectField("Person");


TArray<TSharedPtr<FJsonValue>> objArray = PersonObject->GetArrayField("family");
}

日志

DEFINE_LOG_CATEGORY_STATIC 宏可以在源文件(一般就是 .ccp 文件)中定义日志分类。它定义的是 static 方法,所以只能在定义所在的文件里使用这个分类。

1
DEFINE_LOG_CATEGORY_STATIC(LogMyLiveLinkProviderCore, Log, All);

这样就定义了一个 LogMyLiveLinkProviderCore 的日志分类,只能在定义所在的文件里使用。

例如,

1
UE_LOG(LogMyLiveLinkProviderCore, Display, TEXT("%s Shutdown"), *InitArgs.SourceName);

如果查看代码,可以看到,DEFINE_LOG_CATEGORY_STATIC 实质上是 static struct

1
2
3
4
5
#define DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
static struct FLogCategory##CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \
{ \
FORCEINLINE FLogCategory##CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \
} CategoryName;

断言

程序运行过程中,经常需要检查条件是否满足,例如资源的初始化、值是否有效等等,就可以使用断言来检查。

1
2
3
4
5
checkf(InitArgs.Framerate.AsInterval() > 0, TEXT("IdealFramerate must be greater than zero!"));
checkf(!InitArgs.SourceName.IsEmpty(), TEXT("Source name cannot be empty!"));

int32 Result = GEngineLoop.PreInit(ArgC, ArgV, TEXT(" -Messaging"));
check(Result == 0);

优雅退出

1
FPlatformMisc::SetGracefulTerminationHandler();

小结

整理了一下开发需要用到的基础 API。