注意,需要有源代码版本的 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 进行定制开发。当然,目前用的还是内置的数据结构,没有自定义数据结构,而且还有一点点关于如何从蓝图中获取和处理数据的部分没有涉及。