Unreal Engine 第三方插件的开发:演示一下如何使用 LiveLink 调用 OpenVR(作为第三方库),获取 VR Tracker 数据(外部输入),并控制场景物体的移动。
LiveLink
Live Link 是虚幻引擎中数据流传输通用接口插件。它的主要作用是从外部第三方提供的 DDC 工具,以及第三方提供的外设服务中实时获取数据流,用于开发环节或者最终产品的 Actor 驱动。
在 UE 4.27 之后(包括现在的 5.x),LiveLink 已经成为官方推荐的 UE 统一的外部数据源。
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" ); 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
:对 char
和 wchar_t
的封装
TCHAR* CharString = TEXT("Hello")
FName
:不变的字符串,比如资源路径、文件类型
FName TestName = FName(TEXT("Hello"))
FText
:静态字符串,负责处理文本的本地化,在 UI 中显示
Json
在 Build.cs
中添加 json
和 JsonUtilities
:
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。