UE5 中 LiveLink 的开发全流程教程

news2024/11/14 13:25:49

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/60477.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虚拟机搭载Linux · VMware + Ubuntu 部署 路线参考(20.04.5)

提前回家&#xff0c;要部署OS的实验环境。感谢广源同学给予的帮助和支持~ 电脑文件系统进行了整理&#xff0c;重型文件大部分转移到移动硬盘上。 &#xff08;解压了好久然后我找到镜像源了呜呜没发过来&#xff09; 一、VMware 16 安装 VMware虚拟机安装Linux教程(超详细)…

详解 Spring Boot 项目中的日志文件

目录 1. 日志的作用 2. 自定义日志打印 2.1 日志的基本格式 2.2 得到日志对象 2.3 使用日志对象提供的方法&#xff0c; 打印自定义的日志内容 2.4 日志框架的说明 3. 日志的持久化 3.1 配置日志文件的文件名 3.2 配置日志文件的保存路径 3.3 持久化日志的特性 4. 日…

Java集合(Collection List Set Map)

文章目录Collection接口和常用方法Collection接口遍历元素方式1 -使用Iterator(迭代器)Collection接口遍历对象方式2-for循环增强List接口和常用方法List[ArrayList, LinkedList, Vector]的三种遍历方式ArrayList的注意事项ArrayList的底层操作机制源码分析Vector和ArrayList的…

【已解决】vue后台页面跳转无法正常显示

今天写后端&#xff0c;发现一个问题&#xff0c;我的其他页面之间都可以正常跳转显示&#xff0c;但是我的其中一个页面&#xff08;简称U页面&#xff09;&#xff0c;我跳转到U页面时还可以显示&#xff0c;但之后点击其他页面就无法正常显示了&#xff08;能跳转不能显示&a…

chapter6——流水线的艺术

目录1.影响最大时钟频率的因素2.流水线3.DLX指令集的实现4.流水线对吞吐率的影响5.流水线原理6.流水线冒险结构冒险数据冒险控制冒险其他冒险对高速ASIC日益增长的需求使得越来越需要增加电路每个时钟周期的计算吞吐率。可以通过流水线提高ASIC在这方面的性能&#xff0c;但是也…

首个大规模图文多模态数据集LAION-400M介绍

前言 openAI的图文多模态模型CLIP证明了图文多模态在多个领域都具有着巨大潜力&#xff0c;随之而来掀起了一股图文对比学习的风潮。 就在前几天&#xff08;2022年12月&#xff09;&#xff0c;连Kaiming都入手这一领域&#xff0c;将MAE的思路与CLIP的思路结合&#xff0c;…

MySQL常用高级语句

SQL高级语句 DISTINCT 不显示重复的内容 语法&#xff1a;SELECT DISTINCT “字段” FROM “表名”; select distinct name from lk1;SELECT 显示表格中一个或数个栏位的所有资料 语法&#xff1a;SELECT “字段” FROM “表名”; select * from lk1; #查看表格详细信息…

微机原理与汇编语言—理论知识复习

书上重点内容 本篇博客整理老师课上强调的重点理论知识&#xff0c;以便复习备考&#xff0c;如有错误欢迎指正。 这门课主要讲CPU芯片与其他芯片&#xff08;内存芯片和I/O接口芯片&#xff09;之间交互。 一条指令的执行过程&#xff1a;取指&#xff08;从主存取到CPU寄…

最优化理论笔记及期末复习(《数值最优化》——高立)

目录一、预备知识二、无约束最优化方法的基本结构三、凸集和凸函数四、负梯度方法和Newton型方法五、共轭梯度法六、约束最优化问题的最优性理论七、罚函数方法八、期末复习8.1 知识点复习8.2 习题复习8.3 大实验代码8.3.1实验内容8.3.2实验目的8.3.3算法描述8.3.4程序中的参数…

[附源码]计算机毕业设计基于Springboot的中点游戏分享网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【C++】STL:String

&#x1f431;作者&#xff1a;傻响 &#x1f431;专栏&#xff1a;《C/C - STL》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; ​ 目录 STD - String标准库 字符串类介绍 字符串类构造函数 No.1 string() ; No.2 string(const char…

实操1 : Jupyter Notebook 如何更换主题+全部主题展示+深色主题下如何设置可视化图表

文章目录(一) 如何更换主题(二) 全部主题展示(三) 深色主题下设置可视化图表(一) 如何更换主题 1.打开 Jupyter Notebook, 新建一个Python文件 在文件中输入下方命令开始安装主题 pip install --upgrade jupyterthemes -i https://pipy.douban.com/simple2.win R 打开 cmd 命…

效率工具之Arthas

Arthas 阿里巴巴开源的Java诊断工具&#xff1b;追踪方法执行链、反编译、监控JVM状态 在线安装 使用 1. trace 跟踪调用链 解决痛点&#xff1a;定位问题根据日志推理分析&#xff0c;方法出入参不可见&#xff0c;分支判断太多情况下 定位很慢&#xff0c;分析出可能有问…

[附源码]JAVA毕业设计科院垃圾分类系统(系统+LW)

[附源码]JAVA毕业设计科院垃圾分类系统&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术…

JVM学习-- JVM调优

一、选择垃圾收集器 垃圾收集器和内存大小有关 一般情况&#xff0c; serialserial old 适用几十兆内存 pspo 适用几百兆~几个G parNewCMS 可以用到20G G1 可以用到上百G ZGC 可以 4T~16T 1. 常见垃圾收集器组合参数设定 -XX:UseConc(urrent)MarkSweepGC ParNew CM…

【web实战-业务逻辑】评论点赞逻辑

目录 点赞逻辑一&#xff1a; 第一步&#xff1a;找关键 第二步&#xff1a;猜测逻辑 第三步&#xff1a;结论 第四步&#xff1a;归类 点赞逻辑二&#xff1a; 第一步&#xff1a;找关键 第二步&#xff1a;猜测逻辑 第三步&#xff1a;结论 第四步&#xff1a;归纳…

Framework 学习之旅:Service 启动过程

前言 Service 的启动过程将分为两个部分&#xff0c;分别是ContextImpl到ActivityManageService调用过程和ActivityThread启动Service过程。 ContextImpl到ActivityManageService调用过程 一般启动服务操作在Activity中调用startService方法&#xff0c;从Activity的startSe…

[附源码]计算机毕业设计springboot智慧园区运营管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

360T7路由器进行WiFi无线中继教程

360T7路由器进行WiFi中继教程1. 概述2. 360T7路由器进行WiFi中继实现教程2.1 登录路由器管理界面2.2 选择上网方式2.3 搜索WiFi2.4 连接WiFi2.5 点击确认2.6 在主页面查看网络1. 概述 中继路由系统由一组中继路由器组成&#xff0c;为不能交换路由信息的路由域提供中继路由。该…

关于小程序session_key漏洞问题的解决2022-12-01

业务背景&#xff1a;开发了小程序&#xff0c;使用了一段时间以后&#xff0c;小程序提示系统漏洞session_key的问题&#xff0c;在网上找了好多的博客&#xff0c;感觉好多写的没那么清晰&#xff0c;更偏重于理论&#xff0c;导致自己走了很多的弯路&#xff0c;为了更方便快…