演示一下基本从 LiveLink Program 到 Unreal 编辑器中进行控制对象的全流程。
注意,需要有源代码版本的 Unreal Engine,而不是从游戏 Launcher 中下载的 Unreal 版本。
起点
可以将 Engine\Source\Programs\BlankProgram
作为模板拷贝一份,然后重新命名(可以使用文本编辑器进行全局替换之类的),这里命名成 CircleLiveLinkProvider
,作为 Program 的起点。
使用 GenerateProjectFiles
刷新项目,这样新的 Program 就会出现在 UE 的工程中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include "CircleLiveLinkProvider.h" #include "RequiredProgramMainCPPInclude.h" DEFINE_LOG_CATEGORY_STATIC (LogCircleLiveLinkProvider, Log, All);IMPLEMENT_APPLICATION (CircleLiveLinkProvider, "CircleLiveLinkProvider" );INT32_MAIN_INT32_ARGC_TCHAR_ARGV (){ GEngineLoop.PreInit (ArgC, ArgV); UE_LOG (LogCircleLiveLinkProvider, Display, TEXT ("Hello World" )); FEngineLoop::AppExit (); return 0 ; }
编译一下,在 Engine\Binaries\Win64
(应该是对应平台下,我用的是 Windows,所以是在 Win64)文件夹下,会有对应编译好的可执行文件。
脱离引擎
如果想让程序独立引擎进行运行,需要使用和 Unreal 源码组织结构相同的目录层次结构。如果这时候你把生成的 .exe 拷贝出来运行,是会出现警告的,会提示没有游戏配置和引擎配置。
1 2 3 LogPaths: Warning: No paths for game localization data were specifed in the game configuration. LogInit: Warning: No paths for engine localization data were specifed in the engine configuration. LogCircleLiveLinkProvider: Display: Hello World
但是如果在 Engine\Binaries\Win64
文件夹下进行运行(也就是程序生成的目录),并不会出现这种问题。
这种裸 exe 其实是会有一些副作用的,比如我的电脑上,运行之后,会在 C:\Engine
中生成日志文件。
要想真正独立运行,我们需要把 .exe,放入到一个 伪装 的 Engine 下面。我们按照 Engine\Binaries\Win64
创建文件夹,并把引擎 Engine.和游戏配置拷贝出来。
1 2 3 4 5 6 7 8 9 10 11 CircleLiveLinkProvider └─Engine ├─Binaries │ └─Win64 │ CircleLiveLinkProvider.exe │ CircleLiveLinkProvider.pdb │ └─Config Base.ini BaseEngine.ini BaseGame.ini
这样这个 Program 就可以独立运行了。运行程序之后,会发现自动在 Engine
文件夹中生成了 Programs
和 Saved
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 └─Engine ├─Binaries │ └─Win64 │ CircleLiveLinkProvider.exe │ CircleLiveLinkProvider.pdb │ ├─Config │ Base.ini │ BaseEngine.ini │ BaseGame.ini │ ├─Programs │ └─CircleLiveLinkProvider │ └─Saved │ ├─Config │ │ ├─CrashReportClient │ │ │ └─UECC-Windows-69032E0743138D60D19DF9BAA8B91E3E │ │ │ CrashReportClient.ini │ │ │ │ │ └─WindowsEditor │ │ Engine.ini │ │ Game.ini │ │ │ └─Logs │ CircleLiveLinkProvider.log │ └─Saved └─Config └─WindowsEditor Manifest.ini
可以看到,日志就会出现在我们创建的文件夹中,而不会出现在系统默认(缺省)的执行路径中。
Build.cs
引入 LiveLink 所需的依赖,LiveLink 默认依赖 Udp,所以需要引入 Messaging
和 UdpMessaging
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 using UnrealBuildTool;public class CircleLiveLinkProvider : ModuleRules { public CircleLiveLinkProvider (ReadOnlyTargetRules Target ) : base (Target ) { PublicIncludePaths.Add("Runtime/Launch/Public" ); PrivateIncludePaths.Add("Runtime/Launch/Private" ); PrivateDependencyModuleNames.AddRange(new [] { "Core" , "CoreUObject" , "Projects" , "LiveLinkMessageBusFramework" , "LiveLinkInterface" , "Messaging" , "UdpMessaging" , }); } }
Target.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 using UnrealBuildTool;using System.Collections.Generic;[SupportedPlatforms(UnrealPlatformClass.All) ] public class CircleLiveLinkProviderTarget : TargetRules { public CircleLiveLinkProviderTarget (TargetInfo Target ) : base (Target ) { Type = TargetType.Program; IncludeOrderVersion = EngineIncludeOrderVersion.Latest; LinkType = TargetLinkType.Monolithic; LaunchModuleName = "CircleLiveLinkProvider" ; bBuildDeveloperTools = false ; bUseMallocProfiler = false ; bool bDebugOrDevelopment = Target.Configuration == UnrealTargetConfiguration.Debug || Target.Configuration == UnrealTargetConfiguration.Development; bBuildWithEditorOnlyData = Target.Platform.IsInGroup(UnrealPlatformGroup.Desktop) && bDebugOrDevelopment; bCompileAgainstEngine = false ; bCompileAgainstCoreUObject = true ; bCompileAgainstApplicationCore = false ; bCompileICU = false ; bIsBuildingConsoleApplication = true ; } }
LiveLink Demo 的实现
在源码文件夹下创建两个文件,LiveLinkCore.h
和 LiveLinkCore.cpp
,然后重新运行 GenerateProjectFiles
刷新项目的工程文件。
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 #pragma once #include "CoreMinimal.h" #include "Misc/FrameRate.h" struct ILiveLinkProvider ;struct FLiveLinkProviderCoreInitArgs { FLiveLinkProviderCoreInitArgs (int32 Argc, TCHAR* ArgV[]); FFrameRate Framerate = FFrameRate (60 , 1 ); FString SourceName{ TEXT ("CircleLiveLinkProvider" }); }; class CIRCLELIVELINKPROVIDER_API LiveLinkCore{ public : explicit LiveLinkCore (const FLiveLinkProviderCoreInitArgs& InitArgs) ; int32 Run () ; ~LiveLinkCore (); private : void StartProvider () ; void Tick (float DeltaTime) ; void StopProvider () const ; private : double FrameTime; FLiveLinkProviderCoreInitArgs InitArgs; TSharedPtr<ILiveLinkProvider> LiveLinkProvider; };
因为我们不想在这里就引入 LiveLink 的头文件,所以使用了前向声明 struct ILiveLinkProvider;
。
程序的大体结构设计就是 FLiveLinkProviderCoreInitArgs
负责解析命令行参数,然后将他注入到 LiveLinkCore
中,之后程序逻辑由 LiveLinkCore
负责。
命令行参数解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "LiveLinkCore.h" DEFINE_LOG_CATEGORY_STATIC (LogCircleLiveLinkProviderCore, Log, All);FLiveLinkProviderCoreInitArgs::FLiveLinkProviderCoreInitArgs (const int32 ArgC, TCHAR* ArgV[]) { const FString CmdLine = FCommandLine::BuildFromArgV (nullptr , ArgC, ArgV, nullptr ); FCommandLine::Set (*CmdLine); if (FString Value; 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); }
Framerate.Numerator
是分母,Framerate.Denominator
是分子,Framerate.Numerator
为 60,Framerate.Denominator
为 1,就是 60 帧 1s。
使用非常简单,在头文件中包含该头文件:
1 2 3 4 5 #pragma once #include "CoreMinimal.h" #include "LiveLinkCore.h"
1 2 3 4 5 6 7 8 9 10 11 12 INT32_MAIN_INT32_ARGC_TCHAR_ARGV (){ int32 Result = GEngineLoop.PreInit (ArgC, ArgV, TEXT (" -messaging" )); check (Result == 0 ); check (GConfig && GConfig->IsReadyForUse ()); FLiveLinkProviderCoreInitArgs LoopInitArgs (ArgC, ArgV) ; FEngineLoop::AppExit (); return Result; }
游戏内不会默认启用UDP消息传递。可以通过在打包好的游戏(不支持发布目标)内添加 -messaging 来启用它。文档
核心逻辑
构造函数和析构函数:
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 LiveLinkCore::LiveLinkCore (const FLiveLinkProviderCoreInitArgs& InitArgs): FrameTime (0.0 ), InitArgs (InitArgs) { } LiveLinkCore::~LiveLinkCore () { } void LiveLinkCore::StartProvider () { LiveLinkProvider = ILiveLinkProvider::CreateLiveLinkProvider (InitArgs.SourceName); FLiveLinkStaticDataStruct StaticData = FLiveLinkStaticDataStruct (FLiveLinkTransformStaticData::StaticStruct ()); FLiveLinkTransformStaticData& TransformStaticData = *StaticData.Cast <FLiveLinkTransformStaticData>(); TransformStaticData.PropertyNames.Add (TEXT ("Cosine" )); TransformStaticData.PropertyNames.Add (TEXT ("Sinine" )); LiveLinkProvider->UpdateSubjectStaticData (*InitArgs.SourceName, ULiveLinkTransformRole::StaticClass (), MoveTemp (StaticData)); } void LiveLinkCore::StopProvider () const { LiveLinkProvider->RemoveSubject (*InitArgs.SourceName); }
加载模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 INT32_MAIN_INT32_ARGC_TCHAR_ARGV (){ int32 Result = GEngineLoop.PreInit (ArgC, ArgV, TEXT (" -messaging" )); check (Result == 0 ); check (GConfig && GConfig->IsReadyForUse ()); ProcessNewlyLoadedUObjects (); FModuleManager::Get ().StartProcessingNewlyLoadedObjects (); FModuleManager::Get ().LoadModuleChecked (TEXT ("UdpMessaging" )); FPlatformMisc::SetGracefulTerminationHandler (); FLiveLinkProviderCoreInitArgs LoopInitArgs (ArgC, ArgV) ; LiveLinkCore (LoopInitArgs).Run (); FEngineLoop::AppPreExit (); FModuleManager::Get ().UnloadModulesAtShutdown (); FEngineLoop::AppExit (); return Result; }
主循环:
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 int32 LiveLinkCore::Run () { checkf (InitArgs.Framerate.AsInterval () > 0 , TEXT ("IdealFramerate must be greater than zero!" )); checkf (!InitArgs.SourceName.IsEmpty (), TEXT ("Source name cannot be empty!" )); double DeltaTime = 0.0 ; FrameTime = FPlatformTime::Seconds (); const float IdealFrameTime = InitArgs.Framerate.AsInterval (); StartProvider (); while (!IsEngineExitRequested ()) { Tick (DeltaTime); FTaskGraphInterface::Get ().ProcessThreadUntilIdle (ENamedThreads::GameThread); FTSTicker::GetCoreTicker ().Tick (DeltaTime); GFrameCounter++; IncrementalPurgeGarbage (true , FMath::Max <float >(0.002f , IdealFrameTime - (FPlatformTime::Seconds () - FrameTime))); FPlatformProcess::Sleep (FMath::Max <float >(0.0f , IdealFrameTime - (FPlatformTime::Seconds () - FrameTime))); const double CurrentTime = FPlatformTime::Seconds (); DeltaTime = CurrentTime - FrameTime; FrameTime = CurrentTime; } StopProvider (); UE_LOG (LogCircleLiveLinkProviderCore, Display, TEXT ("%s Shutdown" ), *InitArgs.SourceName); FTaskGraphInterface::Get ().ProcessThreadUntilIdle (ENamedThreads::GameThread); return 0 ; }
帧数据:
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 void LiveLinkCore::Tick (float DeltaTime) { FLiveLinkFrameDataStruct FrameDataStruct = FLiveLinkFrameDataStruct (FLiveLinkTransformFrameData::StaticStruct ()); FLiveLinkTransformFrameData& TransformFrameData = *FrameDataStruct.Cast <FLiveLinkTransformFrameData>(); const float Radians = FMath::DegreesToRadians <float >(GFrameCounter % 360 ); const float CosValue = FMath::Cos (Radians); const float SinValue = FMath::Sin (Radians); const int ScaleFactor = 200 ; TransformFrameData.Transform.SetLocation (FVector (ScaleFactor * CosValue, ScaleFactor * SinValue, ScaleFactor)); TransformFrameData.PropertyValues.Add (CosValue); TransformFrameData.PropertyValues.Add (SinValue); if (GFrameCounter % 100 == 0 ) { UE_LOG (LogCircleLiveLinkProviderCore, Display, TEXT ("(%d) - Cosine: %f Sine: %f" ), GFrameCounter, CosValue, SinValue); } TransformFrameData.WorldTime = FrameTime; const FTimecode EngineTimeCode = FTimecode (FrameTime, InitArgs.Framerate, true ); TransformFrameData.MetaData.SceneTime = FQualifiedFrameTime (EngineTimeCode, InitArgs.Framerate); LiveLinkProvider->UpdateSubjectFrameData (*InitArgs.SourceName, MoveTemp (FrameDataStruct)); }
最终效果
1 2 3 4 5 6 7 LogCircleLiveLinkProviderCore: Display: (0) - Cosine: 1.000000 Sine: 0.000000 LogCircleLiveLinkProviderCore: Display: (100) - Cosine: -0.173648 Sine: 0.984808 LogCircleLiveLinkProviderCore: Display: (200) - Cosine: -0.939693 Sine: -0.342020 LogCircleLiveLinkProviderCore: Display: (300) - Cosine: 0.500000 Sine: -0.866025 LogCore: Warning: *** INTERRUPTED *** : SHUTTING DOWN LogCore: Warning: *** INTERRUPTED *** : CTRL-C TO FORCE QUIT LogCircleLiveLinkProviderCore: Display: CircleLiveLinkProvider Shutdown
可以看到退出的时候并不是暴力退出,而是有一段优雅退出的过程。
游戏内使用
在游戏中勾选上 LiveLink 插件,重启编辑器
在编辑器内可以看到消息:
新建一个 Actor,添加一个 LiveLinkComponentController
,选择主题。可以看到编辑器里的 Cube 在做圆周运动了。
打包
要在打包后的游戏中使用 LiveLink,需要保存预设,并且在游戏启动的时候引入预设。
新建一个变量,设置为我们保存的预设:
启动的时候应用该预设,
项目设置中,设置为默认预设:
这样就可以打包,但在启动的时候需要加上 -messaging
。
小结
本文只是介绍一下基于 Unreal 的 Program 程序的开发,Unreal 某种意义上是一个平台,支持使用内部的 API 进行定制开发。当然,目前用的还是内置的数据结构,没有自定义数据结构,而且还有一点点关于如何从蓝图中获取和处理数据的部分没有涉及。