UE5 中 LiveLink 的开发全流程教程
注意,需要有源代码版本的 Unreal Engine,而不是从游戏 Launcher 中下载的 Unreal 版本。
本文使用是 Unreal Engine 5.1 版本。关于一些基础 API 介绍,可以参考之前的一篇。
起点
可以将 Engine\Source\Programs\BlankProgram
作为模板拷贝一份,然后重新命名(可以使用文本编辑器进行全局替换之类的),这里命名成 CircleLiveLinkProvider
,作为 Program 的起点。
使用 GenerateProjectFiles
刷新项目,这样新的 Program 就会出现在 UE 的工程中。
// CircleLiveLinkProvider.cpp
#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 拷贝出来运行,是会出现警告的,会提示没有游戏配置和引擎配置。
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.和游戏配置拷贝出来。
CircleLiveLinkProvider
└─Engine
├─Binaries
│ └─Win64
│ CircleLiveLinkProvider.exe
│ CircleLiveLinkProvider.pdb
│
└─Config
Base.ini
BaseEngine.ini
BaseGame.ini
这样这个 Program 就可以独立运行了。运行程序之后,会发现自动在 Engine
文件夹中生成了 Programs
和 Saved
└─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
。
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class CircleLiveLinkProvider : ModuleRules
{
public CircleLiveLinkProvider(ReadOnlyTargetRules Target) : base(Target)
{
PublicIncludePaths.Add("Runtime/Launch/Public");
PrivateIncludePaths.Add("Runtime/Launch/Private"); // For LaunchEngineLoop.cpp include
PrivateDependencyModuleNames.AddRange(new[]
{
"Core",
"CoreUObject",
"Projects",
"LiveLinkMessageBusFramework",
"LiveLinkInterface",
"Messaging",
"UdpMessaging",
});
}
}
Target.cs
// Copyright Epic Games, Inc. All Rights Reserved.
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";
// Lean and mean
bBuildDeveloperTools = false;
// Never use malloc profiling in Unreal Header Tool. We set this because often UHT is compiled right before the engine
// automatically by Unreal Build Tool, but if bUseMallocProfiler is defined, UHT can operate incorrectly.
bUseMallocProfiler = false;
// Editor-only is enabled for desktop platforms to run unit tests that depend on editor-only data
// It's disabled in test and shipping configs to make profiling similar to the game
bool bDebugOrDevelopment = Target.Configuration == UnrealTargetConfiguration.Debug || Target.Configuration == UnrealTargetConfiguration.Development;
bBuildWithEditorOnlyData = Target.Platform.IsInGroup(UnrealPlatformGroup.Desktop) && bDebugOrDevelopment;
// Currently this app is not linking against the engine, so we'll compile out references from Core to the rest of the engine
bCompileAgainstEngine = false;
bCompileAgainstCoreUObject = true; // !! 注意这里
bCompileAgainstApplicationCore = false;
bCompileICU = false;
// UnrealHeaderTool is a console application, not a Windows app (sets entry point to main(), instead of WinMain())
bIsBuildingConsoleApplication = true;
}
}
LiveLink Demo 的实现
在源码文件夹下创建两个文件,LiveLinkCore.h
和 LiveLinkCore.cpp
,然后重新运行 GenerateProjectFiles
刷新项目的工程文件。
// LiveLinkCore.h
#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
负责。
命令行参数解析
// LiveLinkCore.cpp
#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。
使用非常简单,在头文件中包含该头文件:
// CircleLiveLinkProvider.h
#pragma once
#include "CoreMinimal.h"
#include "LiveLinkCore.h"
// ...
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 来启用它。文档
核心逻辑
构造函数和析构函数:
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);
}
加载模块:
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);
FEngineLoop::AppPreExit();
FModuleManager::Get().UnloadModulesAtShutdown();
FEngineLoop::AppExit();
return Result;
}
主循环:
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;
}
帧数据:
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));
}
最终效果
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 进行定制开发。当然,目前用的还是内置的数据结构,没有自定义数据结构,而且还有一点点关于如何从蓝图中获取和处理数据的部分没有涉及。