0、前言

工作期间项目遇到一个小问题,在启动项目后,UE4找不到自定义的C++类型,导致某些蓝图炸了,看了一眼编辑器工具栏,没有编译按钮,故猜测项目以纯蓝图项目模式启动了,原因可能出在项目SVN上缺了一些东西,借此机会熟悉一下引擎模块加载过程。

1、如何判断是否为C++工程

很简单,看编辑器工具栏,也就是Play按钮那栏,有编译按钮即为C++项目。此时我的项目状态是,项目目录存在Source文件夹,结构没问题,生成结局方案和编译项目都没问题,IDE中可以以DEBUG模式启动,但断点均无法命中,IDE断点显示错误信息为没有加载相关代码。

已知纯蓝图项目没有编译按钮,那么这个按钮是否显示是由什么决定呢?

首先使用控件反射器获得编译按钮在C++中的位置(Slate控件),可以定位到SToolBarButtonBlock.cpp

...
SNew( SVerticalBox )

// Icon image
+ SVerticalBox::Slot()
.AutoHeight()
.HAlign( HAlign_Center )	// Center the icon horizontally, so that large labels don't stretch out the artwork
[
    IconWidget
]
+ SVerticalBox::Slot().AutoHeight()
.HAlign( HAlign_Center )
[
    SmallIconWidget
]

// Label text
+ SVerticalBox::Slot().AutoHeight()
.Padding(StyleSet->GetMargin(ISlateStyle::Join( StyleName, ".Label.Padding" )))
.HAlign( HAlign_Center )	// Center the label text horizontally
[
    SNew( STextBlock )
        .Visibility( LabelVisibility )
        .Text( ActualLabel )
        .TextStyle( StyleSet, ISlateStyle::Join( StyleName, ".Label" ) )	// Smaller font for tool tip labels
        .ShadowOffset( FVector2D::UnitVector )
]
];
...

是个通用的初始化函数,传入变量添加控件
一通操作后,会定位到一个叫LevelEditorToolBar.cpp的文件中(忘了怎么找到的了)

Section.AddDynamicEntry("CompilerAvailable", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection)
		{
			// Only show the compile options on machines with the solution (assuming they can build it)
			if (FSourceCodeNavigation::IsCompilerAvailable())
			{
				// Since we can always add new code to the project, only hide these buttons if we haven't done so yet
				InSection.AddEntry(FToolMenuEntry::InitToolBarButton(
					"CompileButton",
					FUIAction(
						FExecuteAction::CreateStatic(&FLevelEditorActionCallbacks::RecompileGameCode_Clicked),
						FCanExecuteAction::CreateStatic(&FLevelEditorActionCallbacks::Recompile_CanExecute),
						FIsActionChecked(),
						FIsActionButtonVisible::CreateStatic(FLevelEditorActionCallbacks::CanShowSourceCodeActions)),
					LOCTEXT("CompileMenuButton", "Compile"),
					FLevelEditorCommands::Get().RecompileGameCode->GetDescription(),
					FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Recompile")
				));}}));

你以为最关键的是FSourceCodeNavigation::IsCompilerAvailable()这个if?还真不是。往下看有一个

FIsActionButtonVisible::CreateStatic(FLevelEditorActionCallbacks::CanShowSourceCodeActions))  

这个才是控制这个按钮隐藏的关键(跟是否是C++项目有关系,但不多)

2、模块与C++的关系

众所周知,UE4是一款游戏,而开发者只是为UE4做Mod,C++类型丢失是因为没有加载相关模块。
深挖后发现,这个函数与模块管理器有关。判断是否加载过(任何)游戏模块。

return HotReloadSupport.IsAnyGameModuleLoaded();

注意,这里并非任何模块,而是任何GameModule,也就是自己写的C++代码,引擎模块和插件都不算。详见ModuleInterface.h最后一个函数的注释。

FModuleManager::QueryModules

这个函数可以获得引擎加载过的模块,bIsGameModule判断是否为GameModule,而这个变量来自于模块接口函数

IModuleInterface::IsGameModule()

// ModuleManager.h
class FDefaultGameModuleImpl
	: public FDefaultModuleImpl
{
	/**
	 * Returns true if this module hosts gameplay code
	 *
	 * @return True for "gameplay modules", or false for engine code modules, plug-ins, etc.
	 */
	virtual bool IsGameModule() const override
	{
		return true;
	}
};

也就是说,继承了FDefaultGameModuleImpl的模块属于GameModule。那么谁继承了他呢?就是ModuleManager.h这个文件后面有一大堆套娃宏

#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \
    extern "C" DLLEXPORT IModuleInterface* InitializeModule() \
    { \
        return new ModuleImplClass(); \
    } \
    extern "C" void IMPLEMENT_MODULE_##ModuleName() { } \
    PER_MODULE_BOILERPLATE \
    PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

#define IMPLEMENT_GAME_MODULE( ModuleImplClass, ModuleName ) \
	IMPLEMENT_MODULE( ModuleImplClass, ModuleName )

#define IMPLEMENT_PRIMARY_GAME_MODULE( ModuleImplClass, ModuleName, GameName ) \
    IMPLEMENT_TARGET_NAME_REGISTRATION() \
    IMPLEMENT_GAME_MODULE( ModuleImplClass, ModuleName )

是不是有点晕?
全局搜索IMPLEMENT_PRIMARY_GAME_MODULE这个宏后,惊喜的发现,他就定义在我们的项目代码中

// Source/项目名/项目名.cpp
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, 项目名, "项目名" );

3、模块注册与加载

OK,知道了这个宏用来向引擎注册游戏模块。
回到最初的问题,为什么引擎没有加载他?
回答这个问题,要先知道引擎为什么要加载他,或者为什么会加载他。
视角来到FModuleManager::LoadModuleWithFailureReason这边,这个函数是真正加载模块的地方。在这里做一下入侵,找到游戏模块的调用堆栈

if(InModuleName == FName(FString("MyProject")))
{
	volatile int x = 0;
	x++;// 在这里打个断点
}

// 启动时加载模块的函数
FProjectManager::LoadModulesForProject()
{
	...
	FModuleDescriptor::LoadModulesForPhase(LoadingPhase, 	CurrentProject->Modules, ModuleLoadFailures);
	...
}

可以看到,第二个参数就是这个项目包含的所有模块,其中就有我们在项目名.cpp中注册的模块。
那么这个名字是从哪读出来的呢?查找Modules引用可以定位到

FProjectDescriptor::Read()
{
	...
	if(!FModuleDescriptor::ReadArray(Object, TEXT("Modules"), Modules, OutFailReason))
	{
		return false;
	}
	...
}

这个Read是啥呢?看调用堆栈是来自

FProjectManager::LoadProjectFile( const FString& InProjectFile );

这个参数是我们项目里的uproject文件,那么文章开头的答案就有了。

4、结论

最终可以确定引擎没有加载C++代码是因为我没有将项目的uproject文件推到SVN上导致美术拉到的代码仅加载了蓝图。
so,直接将我的完整的uproject推上去,没有的话,加入下面的代码

"Description": "",
		"Modules": [
		{
			"Name": "MyProject",
			"Type": "Runtime",
			"LoadingPhase": "Default",
			"AdditionalDependencies": [
				"Engine"
			]
		}
	]

若将这段代码从一个C++工程中删除,就可以完美复现出开头的问题。

PS:美术是否需要安装VS环境仍需测试。

5、再深入一点

已知uproject文件记录了需要加载的模块的名字,那么模块的具体路径是哪来的呢?

To Be Continued...