本文用作查找表,方便快速定位问题所在。本次分析的版本是UE5.3
基础概念
modal window:模态窗,指那种只有先完成和此窗口交互后才能继续操作其他窗口的窗口。比如UE delete asset后弹出的确认窗口就是一个模态窗
调用堆栈
FEngineLoop::PreInitPreStartupScreen()
{
FSlateApplication::Create()
FSlateApplication::InitializeRenderer()
}
SlateApplication在FEngineLoop::PreInitPreStartupScreen()创建,PreInitPreStartupScreen被FEngineLoop::PreInit()调用,在引擎启动后,进入gamethread主循环之前。
FSlateApplication::InitializeRenderer()是RHI层类的初始化,仅仅是最低限度的准备类成员变量,还没有涉及实际的Graphic Api初始化
FEngineLoop::PreInitPostStartupScreen()
{
FPreLoadScreenManager::Initialize()
}
这一步是播放加载动画,不太重要。理论上从这里入手能修改打开游戏时弹出UE图标那个动画。
接下来就正式进入主循环。注意,以下内容都发生在gamethread的主循环中
FEngineLoop::Tick()
{
FSlateApplication::PollGameDeviceState() //设备轮询,让各种硬件平台的设备tick
FSlateApplication::FinishedInputThisFrame() //处理持续性的设备输入的结束,如长按按键松开的时候的事件
FDefaultGameMoviePlayer::Initialize() //movieplayer相关,平时应该用不到
FEngineLoop::ProcessLocalPlayerSlateOperations() //处理本地玩家对游戏内UI的操作。此步骤不涉及编辑器UI,纯粹游戏内逻辑
FSlateApplication::Tick(ESlateTickType::PlatformAndInput) //第一次tick
{
FSlateApplication::TickPlatform() //解决平台的输入,也会处理模态窗
}
FSlateApplication::Tick(ESlateTickType::TimeAndWidgets) //第二次tick
{
FSlateApplication::TickTime() //更新UI的时间戳
FSlateApplication::TickAndDrawWidgets()
{
//如果slate没有sleep
FSlateNotificationManager::Tick() //右下脚跳出的通知更新
FSlateApplication::DrawWindows()
{
FSlateApplication::DrawPrepass()
{
PrepassWindowAndChildren() //递归函数,递归draw children
SWidget::SlatePrepass() //递归记录所有slate所需的屏幕大小以及负责resize windows
}
FSlateApplication::DrawWindowAndChildren() //递归绘制所有窗口及子窗口
{
SWindow::PaintWindow()
FSlateInvalidationRoot::PaintInvalidationRoot()
if(UseSlowPath)
{
SWindow::PaintSlowPath()
SWidget::Paint() //递归绘制当前窗口下的所有widgets
SWidget::Tick() //如有必要,tick UI,会根据实际子类虚函数tick
SWidget::OnPaint() //按照实际的子类虚函数绘制UI几何体,在一步会调用children的
//SWidget::Paint(),是实际的递归发生点
}
else
{
FSlateInvalidationRoot::PaintFastPath()
)
}
FSlateRHIRenderer::DrawWindows() //实际的绘制指令,此前的代码本质是在gamethread准备绘制信息。由此进入render thread
} //DrawWindows
} //TickAndDrawWidgets
} //Tick
}
PollGameDeviceState和FinishedInputThisFrame用于输入设备tick,对各个不同的硬件平台进行了抽象。此处仅仅是让设备tick,还没有读入输入。
ProcessLocalPlayerSlateOperations用于处理游戏内部玩家和UI的交互。这里的游戏内就是指实际打包后的游戏,或者PIE的游戏界面。不涉及编辑器UI交互。
此操作后紧接了第一次FSlateApplication::Tick(),这次tick处理硬件平台的所有输入,解码输入的意思。
贴一段Tick的代码
void FSlateApplication::Tick(ESlateTickType TickType)
{
FScopeLock SlateTickAccess(&SlateTickCriticalSection);
{
float DeltaTime = GetDeltaTime();
if (EnumHasAnyFlags(TickType, ESlateTickType::PlatformAndInput))
{
TickPlatform(DeltaTime);
}
if (EnumHasAnyFlags(TickType, ESlateTickType::Time))
{
TickTime();
}
if (GSlateUseFixedDeltaTime)
{
DeltaTime = GetFixedDeltaTime();
}
if (EnumHasAnyFlags(TickType, ESlateTickType::Widgets))
{
TickAndDrawWidgets(DeltaTime);
}
}
}
很明显FSlateApplication::Tick会更具输入的不同执行不同类型的操作,第一次tick只执行了TickPlatform()。我们很快就会见到第二次Tick。
第二次Tick真正开始进行Slate更新及绘制的工作。两次Tick之前穿插了网络同步的工作,用多线程的方式使得网络同步和Slate绘制能同步进行。
FSlateApplication::TickAndDrawWidgets()
{
//如果slate没有sleep
FSlateNotificationManager::Tick() //右下脚跳出的通知更新
FSlateApplication::DrawWindows()
}
第二次Tick主要就是调用了TickAndDrawWidgets函数,该函数是UI绘制工作的入口。它首先调用FSlateNotificationManager::Tick(),这个函数就是右下角弹窗的tick。比如:
然后就是DrawWindows绘制Slate。
详细看DrawWindows
FSlateApplication::DrawWindows()
{
FSlateApplication::DrawPrepass()
{
PrepassWindowAndChildren() //递归函数,递归draw children
SWidget::SlatePrepass() //递归记录所有slate所需的屏幕大小以及负责resize windows
}
FSlateApplication::DrawWindowAndChildren() //递归绘制所有窗口及子窗口
{
SWindow::PaintWindow()
FSlateInvalidationRoot::PaintInvalidationRoot()
if(UseSlowPath)
{
SWindow::PaintSlowPath()
SWidget::Paint() //递归绘制当前窗口下的所有widgets
SWidget::Tick() //如有必要,tick UI,会根据实际子类虚函数tick
SWidget::OnPaint() //按照实际的子类虚函数绘制UI几何体,在一步会调用children的
//SWidget::Paint(),是实际的递归发生点
}
else
{
FSlateInvalidationRoot::PaintFastPath()
)
}
FSlateRHIRenderer::DrawWindows() //实际的绘制指令,此前的代码本质是在gamethread准备绘制信息。由此进入render thread
} //DrawWindows
DrawPrepass()主要功能是递归计算每个UI窗口所需要的参数,如窗口大小。SWidget就是所有UI组件的基类,如果要实现新的UI效果,一般就是继承SWidget或者SWidget的子类来找实现。接下来就是DrawWindowAndChildren(),因为调用层级太多,我上面就直接贴堆栈了。
在PaintInvalidationRoot中,绘制被分为SlowPath和FastPath。FastPath是一个有cache的path,不过编辑器模式下用的都是SlowPath。
SWidget::Tick()是UI组件的Tick函数,继承SWidget实现自定义UI类的时候需要实现这个虚函数。SWidget::OnPaint()则是一个纯虚函数,强制要求子类实现。如果新的SWidget有成员,且这些成员也需要绘制,那么需要在OnPaint中进行递归绘制,也就是在OnPaint中调用成员SWidget的SWidget::Paint()函数。
最后便是FSlateRHIRenderer::DrawWindows()
void FSlateRHIRenderer::DrawWindows_Private(FSlateDrawBuffer& WindowDrawBuffer)
{
···
ENQUEUE_RENDER_COMMAND(SlateDrawWindowsCommand)(
[Params, ViewInfo](FRHICommandListImmediate& RHICmdList)
{
Params.Renderer->DrawWindow_RenderThread(RHICmdList, *ViewInfo, *Params.WindowElementList, Params);
}
);
···
}
这个函数调用ENQUEUE_RENDER_COMMAND将实际的绘制任务发送倒Render Thread。是的,我们前面说了这么多,其实都是在Game thread准备需要渲染的UI任务。不过render thread也完全没有UI业务逻辑相关的工作了,仅仅只是读入Slate数据然后渲染。一般来说不太会直接修改render thread中的slate绘制,就算有,也和slate的关系不大了,而是属于渲染侧的优化,因此这里就不再详述了
最后的最后,是SlateApplication的关闭。
FEngineLoop::Exit()
{
FSlateApplication::Shutdown()
}