【UE4源代码观察】尝试生成启动画面

目标

我想观察UE4的这个启动画面是怎么生成的:
在这里插入图片描述
如果可以的话,我想在我的空白工程中也尝试生成它。

观察

首先,我在主线程中打了若干断点,尝试定位生成这个小窗口的语句是哪一条,但是定位总不准确,这时我意识到生成这个窗口的语句可能不在主线程。
于是,我在启动画面出现时观察VS里显示的线程信息,根据名字找出了这个线程:
在这里插入图片描述
然后在WindowsPlatformSplash.cpp中找到了创建这个线程的语句:

GSplashScreenThread = CreateThread(NULL, 128 * 1024, (LPTHREAD_START_ROUTINE)StartSplashScreenThread, (LPVOID)NULL, STACK_SIZE_PARAM_IS_A_RESERVATION, &ThreadID);

UE4本身有多线程系统,不过CreateThread这个函数是windows原始的函数,看来若只是为了启动画面,暂时还不必要关注UE4自己的多线程系统了。

实践

【UE4源代码观察】观察Core模块中,我的空白工程中已经加入了core模块,我将用这个工程继续后续操作。完整代码见:GIT上的完整工程

1.新建项目

新建一个UE4Test的项目。同时,也复制Test1.Target.cs改名为UE4Test.Target.cs,当然其中名字也要对应更改。

2.新建一个Launch模块

我想和UE4源代码保持相同的结构,因此我也建立一个对应的Launch模块。但是,Launch模块中牵连了很多内容,所以这里只是一个“空”的模块。如LaunchWindows.cpp中只保留一个入口所需的最小内容:

#include"CoreMinimal.h"
#include"Windows/WindowsHWrapper.h"

extern int32 GuardedMain(const TCHAR* CmdLine, HINSTANCE hInInstance, HINSTANCE hPrevInstance, int32 nCmdShow);

int32 WINAPI WinMain(HINSTANCE hInInstance, HINSTANCE hPrevInstance, char*, int32 nCmdShow)
{
	int32 ErrorLevel = 0;
	const TCHAR* CmdLine = ::GetCommandLineW();
	ErrorLevel = GuardedMain(CmdLine, hInInstance, hPrevInstance, nCmdShow);
	return ErrorLevel;
}

之后,把UE4Test.Target.cs中的启动模块变成它

LaunchModuleName = "Launch";
3.入口函数

尝试生成,报错:

1>MSVCRT.lib(exe_main.obj) : error LNK2019: 无法解析的外部符号 main,该符号在函数 "int __cdecl __scrt_common_main_seh(void)" (?__scrt_common_main_seh@@YAHXZ) 中被引用

看起来想找 main 函数。但奇怪的是我们明明已经有WinMain这个入口函数了,为何不用它当入口呢?
我在UE4Test.Target.cs发现了这一句:

// UnrealHeaderTool is a console application, not a Windows app (sets entry point to main(), instead of WinMain())
bIsBuildingConsoleApplication = true;

看来bIsBuildingConsoleApplication这个变量为true时将告诉UBT:程序入口是main,否则就是WinMain。于是将这句去掉,保留默认值false。
(看注释会发现它提及的是UnrealHeaderTool 这个程序,然而实际上UE4Test.Target.cs是拷贝了Test1.Target.cs,而Test1.Target.cs是拷贝了BlankProgram.Target.cs,看来Epic的员工在写BlankProgram.Target.cs时是拷贝了UBT的啊(笑))

4.拷贝启动画面相关代码

启动画面(Splash)相关的代码WindowsPlatformSplash.cpp中,我准备将它相关的文件都拷贝过来,他们在 ApplicationCore这个模块中。
不过有点麻烦的是,它include了"WindowsPlatformApplicationMisc.h",只是为了调用一个GetAppIcon()函数,而这个h文件却随后又引出了其他许多文件。于是,我决定暂时清理掉"WindowsPlatformApplicationMisc.h"中除了GetAppIcon()以外的内容。
随后,在PreInitPreStartupScreen中调用:

FPlatformSplash::Show();
5.
1>D:/0_WorkSpace/ForGit/UEYaksueTest/Engine/Source/Runtime/Launch/Private/LaunchEngineLoop.cpp(5): fatal error C1083: 无法打开包括文件: “HAL/PlatformSplash.h”: No such file or directory

cpp文件找不到ApplicationCore中的h文件,需要在Launch.Build.cs中添加:

 PrivateIncludePaths.Add("Runtime/ApplicationCore/Public");
6.
1>  [1/3] Module.Launch.cpp
1>D:\0_WorkSpace\ForGit\UEYaksueTest\Engine\Source\Runtime\ApplicationCore\Public\GenericPlatform/GenericPlatformSplash.h(40): error C2079: “FGenericPlatformSplash”使用未定义的 struct“APPLICATIONCORE_API”

上面的错误需要在Launch.Build.cs中添加对ApplicationCore模块的依赖:

PrivateDependencyModuleNames.AddRange(new string[] {
                "Core",
                "ApplicationCore",
            });
7.看不到启动画面

此时可以生成成功,但是并不能看到启动画面,只有控制台提示程序退出:
在这里插入图片描述
使用调试模式启动时,会直接触发一处断点:
在这里插入图片描述
这个断点是UE4自己设计的,如果宏里的第一个表达式为真,则触发:

// Conditional logging (fatal errors only).
#define UE_CLOG(Condition, CategoryName, Verbosity, Format, ...) \
{ \
	static_assert(TIsArrayOrRefOfType<decltype(Format), TCHAR>::Value, "Formatting string must be a TCHAR array."); \
	if (ELogVerbosity::Verbosity == ELogVerbosity::Fatal) \
	{ \
		if (Condition) \
		{ \
			LowLevelFatalErrorHandler(UE_LOG_SOURCE_FILE(__FILE__), __LINE__, Format, ##__VA_ARGS__); \
			_DebugBreakAndPromptForRemote(); \
			FDebug::ProcessFatalError(); \
			UE_LOG_EXPAND_IS_FATAL(Verbosity, CA_ASSUME(false);, PREPROCESSOR_NOTHING) \
		} \
	} \
}

可见我的bIsInitialized是在这里是假。
我查到了有个函数会将其设为真:

bool FCommandLine::Set(const TCHAR* NewCommandLine)
{
	if (!bIsInitialized)
	{
		FCString::Strncpy(OriginalCmdLine, NewCommandLine, UE_ARRAY_COUNT(OriginalCmdLine));
		FCString::Strncpy(LoggingOriginalCmdLine, NewCommandLine, UE_ARRAY_COUNT(LoggingOriginalCmdLine));
	}

	FCString::Strncpy( CmdLine, NewCommandLine, UE_ARRAY_COUNT(CmdLine) );
	FCString::Strncpy(LoggingCmdLine, NewCommandLine, UE_ARRAY_COUNT(LoggingCmdLine));
	// If configured as part of the build, strip out any unapproved args
	WhitelistCommandLines();

	bIsInitialized = true;

而这个函数在PreInitPreStartupScreen中被调用,于是加上它。

8.启动画面资源

启动画面的线程仍旧没有创建,调试发现:FWindowsPlatformSplash::Show()中的FGenericPlatformSplash::GetSplashPath返回了false。
这个函数是找到启动画面资源的函数,这时我意识到启动画面的资源还没有拷贝过来,于是拷贝它,他在:
\Engine\Content\Splash路径中

9.给主线程加上循环

启动画面的线程创建了,但是很快会退出,我想这是因为我的主线程里没有循环,于是我在主线程中加上了循环:

#if PLATFORM_WINDOWS
int32 GuardedMain(const TCHAR* CmdLine, HINSTANCE hInInstance, HINSTANCE hPrevInstance, int32 nCmdShow)
#else
int32 GuardedMain(const TCHAR* CmdLine)
#endif
{
	int32 ErrorLevel = EnginePreInit(CmdLine);

	while (true)//暂时加一个死循环
	{
		EngineTick();
	}

	return ErrorLevel;
}

最终成功:
在这里插入图片描述

其他

实践过程中有一处错误:

1>Launch.cpp.obj : error LNK2001: 无法解析的外部符号 "class FEngineLoop GEngineLoop" (?GEngineLoop@@3VFEngineLoop@@A)

这个GEngineLoop在LaunchEngineLoop.h中只是一个extern:

/** Global engine loop object. This is needed so wxWindows can access it. */
extern FEngineLoop GEngineLoop;

真正的GEngineLoop在其他地方有定义,然而我并没有摸清在哪,于是只能在Lanuch.cpp中手动加上了它的定义:

FEngineLoop GEngineLoop;//yaksuetest

这在日后应该会有重定义的问题,到时候要把这里的定义去掉。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值