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...