|
发表于 2023-3-17 17:26:20
|
显示全部楼层
大家好,我是阿棍儿。关于LoadingScreen的相关文章不少,我研究了Lyra项目的CommonLoadingScreen插件之后,觉得有必要补充一些东西。
本篇基于UE5.1 虚幻的LoadingScreen提供了2个阶段(PreLoadScreen和LoadingScreen)和2种实现方式(UI和视频)的支持。如果掌握核心机制,自己去实现,也有一定的自由度。
LoadingScreen就是网上资料最多的那种,在关卡切换过程中显示的。PreLoadScreen是在引擎启动的过程种显示的。
PreLoadScreen
在loading screen显示之前,游戏窗口会显示黑屏,更早的时间点意味着更短的黑屏时间。
- 这种loading screen有3个自动播放时间点,每个时间点上只能用一种机制显示(UI/视频)
- 播放视频可以选择其中的2个时间点,不可多选,即如果在本时间点还在播前一个时间点的视频,则不会播本时间点的视频
- UI可以在3个时间点中选,可以多选,即UI方式会阻塞引擎启动,所以3个时间点的UI都可以完整显示
时间点参考系就是引擎启动时模块加载的LoadingPhase。在插件的*.plugin文件的结尾会有如下字段,其中的LoadingPhase就是引擎启动时加载模块的几个时间点。
{
...
"Modules": [
{
"Name": "模块名",
"Type"": "模块类型",
"LoadingPhase": "加载阶段"
}...
]
}PreLoadScreen类型定义如下
enum class EPreLoadScreenTypes : uint8
{
CustomSplashScreen,
EarlyStartupScreen,
EngineLoadingScreen
};
LoadingPhase和EPreLoadScreenTypes(粗体)的时序如下
EarliestPossible->PostConfigInit->PostSplashScreen
->CustomSplashScreen
->PreEarlyLoadingScreen
->EarlyStartupScreen
->PreLoadingScreen
->EngineLoadingScreen
->PreDefault->Default->PostDefault->PostEngineInit
如果把模块加载时间设置在PostSplashScreen阶段,3个时间点就都可以选用了。loading screen依赖movie player和slate render,二者在PostConfigInit和PostSplashScreen之间已经可以用了,也许也可以播出来,够勇的可以试试。
视频机制和UI机制的优先级关系
- CustomSplashScreen阶段
- EarlyStartupScreen阶段
- 视频机制优先,如果没有配置视频(例如没调用到GameMoviePlayer的SetupLoadingScreen函数进行配置),则尝试UI机制
- EngineLoadingScreen阶段
- 如果之前的视频没有结束,继续播放,不做处理
- 如果之前的视频已结束,UI机制优先
- 如果之前的视频已结束,UI机制有没配置,则尝试视频机制
播放视频
播放视频有2个时间点可选(EarlyStartupScreen和EngineLoadingScreen),只要在时间点之前调用到GameMoviePlayer的SetupLoadingScreen函数配置好即可自动播放。在这个时间点之前加载的插件就可以做到。
上一段话不准确!!!因为经实践,EarlyStartupScreen这个时间点还不具备播放视频的条件,解决方法见后文。
视频的配置方法
一般支持视频的loading screen插件都有类似的写法,例如以下这种。LoadingScreen里面的变量看情况配就行,就是一般的MoviePlayer配置方法。bAllowInEarlyStartup就是选择时间点的变量,true的话就是较早的时间点。
FLoadingScreenAttributes LoadingScreen;
// ...
LoadingScreen.bAllowInEarlyStartup = false;
// ...
GetMoviePlayer()->SetupLoadingScreen(LoadingScreen);
另外的配置方法就是在DefaultGame.ini里面进行配置,这个配置默认会在所有阶段尝试播放视频。具体该配什么可以看FDefaultGameMoviePlayer::SetupLoadingScreenFromIni里面怎么读ini文件的。下面给出一个例子,里面配置了2个视频,没有输入的话,两个视频都会播完,否则会提前结束这个阶段的loading screen。
- 视频路径是相对于项目目录/Content/Movies的
- 视频文件的名字不带后缀
[/Script/MoviePlayer.MoviePlayerSettings]
bWaitForMoviesToComplete=True
bMoviesAreSkippable=True
+StartupMovies=视频路径1(不带后缀)
+StartupMovies=视频路径2(不带后缀)如果是基于Lyra的CommonLoadingScreen插件来改造的话,用输入打断视频机制可能会失效。因为CommonLoadingScreen模块注册了FLoadingScreenInputPreProcessor对象,它会优先于UI吃掉输入。所以想看到输入中断视频的效果,要先排除这个干扰因素。
EarlyStartupScreen播不出来视频的问题
当我尝试使用ini播放loading screen时,视频并没有在EarlyStartupScreen阶段播放。调试发现是因为缺少MovieStreamer(解析视频文件的对象)。
不同的平台(Win/Apple...)需要不同的streamer来支持视频播放,streamer准备好之后,会进行注册,播放视频时会遍历尝试所有注册的streamer,直到找到能够成功播放的一个。
由于这个阶段太早了,没有可用的streamer在这个阶段被注册,所以无法播放视频。解决问题的方法就是手动加载所需要的streamer,可以把以下代码加到模块初始化的地方来提前加载需要的streamer。这里做的比较粗糙,可以考虑更优雅的写法来支持更多平台。
{
FModuleManager& ModuleManager = FModuleManager::Get();
FName StreamerName("WindowsMoviePlayer"); // 支持windows平台
if (!ModuleManager.IsModuleLoaded(StreamerName))
{
ModuleManager.LoadModuleChecked(StreamerName);
}
}
好多模块叫xxxStreamer,如何辨别可用的streamer模块?
我的方法是看哪个模块调用了FCoreDelegates::RegisterMovieStreamerDelegate的广播来注册streamer。
UI
播放视频的方式也可以播放UI,在FLoadingScreenAttributes里面给WidgetLoadingScreen赋值即可。参考FDefaultGameMoviePlayer::Initialize里面构造的Slate界面,视频上面就放了一层UI,不过这一节讨论的是另一种方式。
EPreLoadScreenTypes的3个时间点都可以显示UI,遗憾的是不能用UUserWidget,只能用Slate来实现。原因是UUserWidget需要UObject对象来作为Outer,而这3个时间点连GameInstance对象都没有。EngineLoadingScreen阶段也许有希望,因为可以使用UObject子类对象了,也许可以自己造一个Outer,够勇的可以试试。
上一段话不准确!!!因为CustomSplashScreen这个时间点会高概率在第3帧时锁死,我给官方发了个解决方法Fix hanged CustomSplashScreen #10173,可以参考。

Lyra项目CommonLoadingScreen插件的CommonStartupLoadingScreen模块可以作为参考,它实现了在EngineLoadingScreen阶段显示一个Slate界面。
流程很简单,参考CommonStartupLoadingScreen的FCommonStartupLoadingScreenModule::StartupModule函数
- 先创建FCommonPreLoadScreen对象,并初始化它
- 将其注册给FPreLoadScreenManager
这个loading screen怎么显示,什么阶段显示,取决于FCommonPreLoadScreen的实现,我们可以参考其父类FPreLoadScreenBase来定制实现。
class PRELOADSCREEN_API FPreLoadScreenBase : public IPreLoadScreen
{
//...
virtual void Tick(float DeltaTime) override {} // 游戏线程tick
virtual void RenderTick(float DeltaTime) override {} // 渲染线程tick
// 每次Tick之前延迟的时间,这会导致第一帧也被延迟哦
virtual float GetAddedTickDelay() override { return 0.00f; }
// 初始化,可以在这里创建Slate控件
virtual void Init() override {}
// 选择时间点
//IMPORTANT: This changes a LOT of functionality and implementation details. EarlyStartupScreens happen before the engine is fully initialized and block engine initialization before they finish.
// this means they have to forgo even the most basic of engine features like UObject support, as they are displayed before those systems are initialized.
virtual EPreLoadScreenTypes GetPreLoadScreenType() const override { return EPreLoadScreenTypes::EngineLoadingScreen; }
// 是否完成,如果没完成,游戏线程会继续执行上面的tick,不会走后面的逻辑
virtual bool IsDone() const override;
//...
};
如果通过FPreLoadScreenManager给同一个时间点注册了多个loading screen,只有第一个会起作用。
不用UObject为UI加载图片
没法使用UObject,意味着不能走平时的资产加载的路子,我找到一个参考[1],而且打包也没问题。思路就是使用SlateStyle机制加载图片,亲测可用。
LoadingScreen
这个loading screen就是最常用的那种,主要用于关卡切换的过渡。基本机制就是利用以下3个委托,来确定播放和结束的时机,Pre的时候播,Post的时候停。
FCoreUObjectDelegates::FPreLoadMapDelegate FCoreUObjectDelegates::PreLoadMap;
FCoreUObjectDelegates::FPreLoadMapWithContextDelegate FCoreUObjectDelegates::PreLoadMapWithContext;
FCoreUObjectDelegates::FPostLoadMapDelegate FCoreUObjectDelegates::PostLoadMapWithWorld;
常见的作法就是用播放视频的方法播放视频或UI。可以把创建好的UI赋给FLoadingScreenAttributes::WidgetLoadingScreen来播放,比如,大侠刘茗:UE4[C++]如何启动真加载界面说得很清楚。
Lyra项目的CommonLoadingScreen模块给出了另一种方式,它没有使用MoviePlayer相关的东西,而是用UGameViewportClient::AddViewportWidgetContent函数直接切换了UI,这个做法看起来更加纯粹。
CommonLoadingScreen值得学习的一些细节
- 处理输入
- loading screen自然要阻挡输入,这里使用了FSlateApplication的InputPreProcessor机制,其输入处理是优先于UI的,可以保证输入不会漏到游戏里。
- ULoadingScreenManager::CheckForAnyNeedToShowLoadingScreen函数列出了绝大多数需要loading screen的情况
- ULoadingScreenManager::ShowLoadingScreen函数处理了与PreLoadScreen的互斥
- 其处理的不够完整,对我的实验形成了干扰,可以考虑改善一下
- ULoadingScreenManager::ChangePerformanceSettings处理了性能方面的问题
- 这方面对于非资深开发者来说比较头大,感谢官方给出了范本
- 调用FSlateApplication::Get().Tick()快速出图
参考
- ^【UE4】EarlyStartupScreenでアプリ起動後すぐに画像を表示する|株式会社ヒストリア https://historia.co.jp/archives/19862/
|
|