UE5 中 LiveLink 的开发全流程教程
创始人
2024-03-17 05:03:09

注意,需要有源代码版本的 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│└─ConfigBase.iniBaseEngine.iniBaseGame.ini

这样这个 Program 就可以独立运行了。运行程序之后,会发现自动在 Engine 文件夹中生成了 ProgramsSaved

└─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└─WindowsEditorManifest.ini

可以看到,日志就会出现在我们创建的文件夹中,而不会出现在系统默认(缺省)的执行路径中。

Build.cs

引入 LiveLink 所需的依赖,LiveLink 默认依赖 Udp,所以需要引入 MessagingUdpMessaging

// 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 includePrivateDependencyModuleNames.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 meanbBuildDeveloperTools = 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 gamebool 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 enginebCompileAgainstEngine = 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.hLiveLinkCore.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 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();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(0.002f, IdealFrameTime - (FPlatformTime::Seconds() - FrameTime)));FPlatformProcess::Sleep(FMath::Max(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();const float Radians = FMath::DegreesToRadians(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 进行定制开发。当然,目前用的还是内置的数据结构,没有自定义数据结构,而且还有一点点关于如何从蓝图中获取和处理数据的部分没有涉及。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...