目标
我想观察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
这在日后应该会有重定义的问题,到时候要把这里的定义去掉。