UE Slate系统源码分析

本文用作查找表,方便快速定位问题所在。本次分析的版本是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()
}
  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值