GIST NOTES 注意 :使用GIST管理的笔记,在国内网络可能会无法显示。
可以通过执行cook的Commandlet来重新生成AssetRegistry以及ushaderbytecode:
1 UE4Editor-cmd.exe PROJECT_NAME.uproject -run=cook -targetplatform=WindowsNoEditor -Iterate -UnVersioned -Compressed
执行完毕之后Saved/Cooked
下的AssetRegistry.bin
/Metadate目录
/Content/ShaderArchive-*.ushaderbytecode
以及Ending/GlobalShaderCache*.bin
等文件都是生成之后最新的了,可以在之后通过HotPatcher来打包他们了。
IPA包最大为4GB
自动化Editor的Crash上报 因为UE的Crash是拉起一个CrashReportClient程序来执行的,所以如果我们需要在程序出现Crash时上报log信息,可以在CrashReportClientMainWindows
中做这部分逻辑。
监听资源创建 在Editor中创建资源,并没有直接保存到磁盘上,所以要监听OnInMemoryAssetCreated
:
1 2 3 FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>( TEXT("AssetRegistry" )); AssetRegistryModule.Get().OnInMemoryAssetCreated().AddRaw(this , &FTestEditorModule::OnInMemoryAssetCreated);
回调过来的就是一个UObject*
,实际上是一个UBlueprint*
:
1 2 3 4 5 6 7 8 void FTestEditorModule::OnInMemoryAssetCreated (UObject* Object) { if (nullptr == Object) return ; UBlueprint* Blueprint = Cast<UBlueprint>(Object); if (nullptr == Blueprint) return ; }
可以实现监听uasset创建事件,对该uasset执行一些操作(如默认添加接口等)。
拿到UBlueprint
后就可以通过FBlueprintEditorUtils
等辅助类来实现对蓝图资源的操作了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 bool FTestEditor::AddInterface (UBlueprint* Blueprint) { if (nullptr == Blueprint) return false ; UClass* Class = Blueprint ? *Blueprint->GeneratedClass : Blueprint ? Blueprint->GetClass() : NULL ; if (nullptr == Class) return false ; if (!Class->IsChildOf<UUserWidget>()) return false ; static UClass* InterfaceClass = UUnLuaInterface::StaticClass(); UFunction* Func = FBlueprintEditorUtils::GetInterfaceFunction(Blueprint, FName("GetModuleName" )); if (nullptr == Func) { FBlueprintEditorUtils::ImplementNewInterface(Blueprint, InterfaceClass->GetFName()); } Func = FBlueprintEditorUtils::GetInterfaceFunction(Blueprint, FName("GetModuleName" )); if (nullptr == Func) return false ; auto ImplementedInterfaces = Blueprint->ImplementedInterfaces; if (ImplementedInterfaces.Num() <= 0 ) return false ; auto InterfacesDesc = ImplementedInterfaces[0 ]; auto Graphs = InterfacesDesc.Graphs; if (Graphs.Num() <= 0 ) return false ; auto Graph = Graphs[0 ]; if (nullptr == Graph) return false ; auto Nodes = Graph->Nodes; if (Nodes.Num() <= 0 ) return false ; auto Node = Nodes[1 ]; if (nullptr == Node) return false ; auto Pins = Node->Pins; if (Pins.Num() <= 0 ) return false ; auto Pin = Pins[1 ]; if (nullptr == Pin) return false ; FString moduleName; UnLuaExtensionUtils::GetLuaModuleName(Class->GetName(), Class->GetPathName(), moduleName); Pin->DefaultValue = moduleName; return true ; }
Build lighting from commandlet 在命令行构建光照,可以使用ResavePackages
这个commandlet:
1 UE4Editor-cmd.exe "E:\UE4Project.uproject" -run=resavepackages -buildlighting -quality=Preview -allowcommandletrendering -map=MapName
打包时的错误,可以通过这些错误码的描述来排查原因,该enum定义在Programs/AutomationTool/AutomationUtils/AutomationException.cs
。
Programs/AutomationTool/AutomationUtils/AutomationException.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 namespace AutomationTool { public enum ExitCode { Error_UATNotFound = -1 , Success = 0 , Error_Unknown = 1 , Error_Arguments = 2 , Error_UnknownCommand = 3 , Error_SDKNotFound = 10 , Error_ProvisionNotFound = 11 , Error_CertificateNotFound = 12 , Error_ProvisionAndCertificateNotFound = 13 , Error_InfoPListNotFound = 14 , Error_KeyNotFoundInPList = 15 , Error_ProvisionExpired = 16 , Error_CertificateExpired = 17 , Error_CertificateProvisionMismatch = 18 , Error_CodeUnsupported = 19 , Error_PluginsUnsupported = 20 , Error_UnknownCookFailure = 25 , Error_UnknownDeployFailure = 26 , Error_UnknownBuildFailure = 27 , Error_UnknownPackageFailure = 28 , Error_UnknownLaunchFailure = 29 , Error_StageMissingFile = 30 , Error_FailedToCreateIPA = 31 , Error_FailedToCodeSign = 32 , Error_DeviceBackupFailed = 33 , Error_AppUninstallFailed = 34 , Error_AppInstallFailed = 35 , Error_AppNotFound = 36 , Error_StubNotSignedCorrectly = 37 , Error_IPAMissingInfoPList = 38 , Error_DeleteFile = 39 , Error_DeleteDirectory = 40 , Error_CreateDirectory = 41 , Error_CopyFile = 42 , Error_OnlyOneObbFileSupported = 50 , Error_FailureGettingPackageInfo = 51 , Error_OnlyOneTargetConfigurationSupported = 52 , Error_ObbNotFound = 53 , Error_AndroidBuildToolsPathNotFound = 54 , Error_NoApkSuitableForArchitecture = 55 , Error_FilesInstallFailed = 56 , Error_RemoteCertificatesNotFound = 57 , Error_LauncherFailed = 100 , Error_UATLaunchFailure = 101 , Error_FailedToDeleteStagingDirectory = 102 , Error_MissingExecutable = 103 , Error_DeviceNotSetupForDevelopment = 150 , Error_DeviceOSNewerThanSDK = 151 , Error_TestFailure = 152 , Error_SymbolizedSONotFound = 153 , Error_LicenseNotAccepted = 154 , Error_AndroidOBBError = 155 , }; }
使用AssetRegistry检测资源是否存在 在Editor/UnrealEd/Private/FileHelpers.cpp
中提供了一个实现,优先通过AssetRegistry
来查找,查找不到则退回到从磁盘查找:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 namespace FileHelperPackageUtil{ bool DoesPackageExist (UPackage* Package, FString* OutFilename = nullptr ) { IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry(); if (!AssetRegistry.IsLoadingAssets() || !GIsEditor) { TArray<FAssetData> Data; FAssetRegistryModule::GetRegistry().GetAssetsByPackageName(Package->GetFName(), Data, true ); if (Data.Num() > 0 && OutFilename) { *OutFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), Package->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension()); } return Data.Num() > 0 ; } return FPackageName::DoesPackageExist(Package->GetName(), nullptr , OutFilename); } }
分析打包的资源 工程中有很多的资源其实并没有打到包中去,当需要分析包体中资源大小时,可以通过Asset Audit
工具来实现,通过Window
-Developer Tools
-AssetAudit
打开。
可以看到资源路径、大小、位于哪些Chunk中等一系列的信息,便于排查资源大小和Chunk中的资源冗余。
首先,需要说明Editor
里的资源大小和最终打到包内的大小是不一样的,在右上角会列出已经打包的平台(Saved/Cooked)下的平台。
Asset Audit
需要读取DevelopmentAssetRegistry.bin
文件来得到某个平台的资源信息的,它在以下路径中:
1 Client\Saved\Cooked\WindowsNoEditor\FGame\Metadata\DevelopmentAssetRegistry.bin
这个文件记录着某个平台执行完Cook之后资源的大小信息,注意Cook之后的资源如Texture2D等设置的压缩均以执行,但是打包成pak也会执行压缩,这里列出来的大小是没有经过打包pak压缩的Cook资源之后的原始大小。
可以在打包时自动提取Cooked目录下的Metadata目录,在AssetAudit窗口的右上角选择Custom,选择DevelopmentAssetRegistry.bin文件即可。
UE4中ES3.1的75根骨骼限制 之前提到过在ES2.0上使用单个材质蒙皮的骨骼不能超过75根,在ES3之后就没有这个限制了,但是UE里目前还有这个限制。
Warning: SkeletalMesh SK_m0146b0003, is not supported for current feature level (ES3_1) and will not be rendered. NumBones 78 (supported 75), NumBoneInfluences: 4
Runtime/RHI/Public/RHIDefinitions.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 inline int32 GetFeatureLevelMaxNumberOfBones (const FStaticFeatureLevel FeatureLevel) { switch (FeatureLevel) { case ERHIFeatureLevel::ES3_1: return 75 ; case ERHIFeatureLevel::SM5: return 65536 ; default : checkf(0 , TEXT("Unknown FeatureLevel %d" ), (int32)FeatureLevel); } return 0 ; }
看了下相关的代码,UE在下面这次提交中修改了ES3.1原本256到75,commit里提到的是修复了Mobile Preview的Crash:
Limit ES3.1 to 75 bones like ES2. All ES3.1 feature level platforms use UB for Bones. Project has to set Compat.MAX_GPUSKIN_BONES=75 to support SkelMeshes with more than 75 bones for ES3.1 and ES2.
该代码在4.21 Preview 4中提交:Unreal Engine 4.21 Preview
在answers上也有一些相关的问题:
UE4热更注意事项
不能引用基础包中不存在的插件中的资源
包含shaderbytecode(share material shaderbytecode)
包含AssetRegistry(新加/删除/修改资源引用)
避免一个目录既包含uasset又包含non-asset的情况
upluginmanifest 项目打包时会根据项目启用的插件生成一个PROJECT_NAME.upluginmanifest
文件,其中记录了每个启用的插件的uplugin
的路径和内容信息,该文件也会打包到pak中。
Mount Point为:../../../PROJECT_NAME/Plugins/PROJECT_NAME.upluginmanifest
在Editor下运行时不会读取这个文件,通过扫描引擎和项目以及Mods
目录下的Plugin目录来查找插件的,相关的逻辑在Runtime/Projects/Private/PluginManager.cpp
的ReadAllPlugins
函数中。
1 2 3 4 5 6 #if !WITH_EDITOR if (Project != nullptr ) { FindPluginManifestsInDirectory(*FPaths::ProjectPluginsDir(), ManifestFileNames); } #endif
在非Editor下通过加载upluginmanifest文件来确定当前工程中有哪些插件的(upluginmanifest文件可以有多个,只要放在../../../PROJECT_NAME/Plugins
目录下即可),如果一个插件在基础包中不存在,但是热更时新建了一个Content Only插件打包资源,需要把该插件添加至upluginmanifest中并且也需要把该插件的uplugin打包至pak中。
添加非Content路径的Non-Asset目录到基础包 在Project Settings
-Packaging
-Additional Non-Asset Directories to Package
可以添加相对路Content下的目录,但是不能够直接选Content之外的目录。
但是,其实这里是可以填相对路径的,如添加[PROJECT_DIR]/Source/Script
目录:
1 2 [/Script/UnrealEd.ProjectPackagingSettings] +DirectoriesToAlwaysStageAsUFS=(Path="../Source/Script")
在打包时能够正确地处理这个相对路径的,Mount Point也正常:
1 "D:\UnrealProjects\Client\Source\Script\UnLua.lua" "../../../FGame/Source/Script/UnLua.lua"
使用这种相对路径可以实现把位于项目Content之外的Non-Asset目录添加到基础包中。
实现分析:[/Script/UnrealEd.ProjectPackagingSettings]
的DirectoriesToAlwaysStageAsUFS
值是在Programs/AutomationTool/Scripts/CopyBuildToStagingDirectory.Automation.cs
中被读取的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 private static void StageAdditionalDirectoriesFromConfig (DeploymentContext SC, DirectoryReference ProjectContentRoot, StagedDirectoryReference StageContentRoot, ConfigHierarchy PlatformGameConfig, bool bUFS, string ConfigKeyName ) { List<string > ExtraDirs; if (PlatformGameConfig.GetArray("/Script/UnrealEd.ProjectPackagingSettings" , ConfigKeyName, out ExtraDirs)) { foreach (var PathStr in ExtraDirs) { string RelativePath = null ; var PathParts = PathStr.Split('"' ); if (PathParts.Length == 3 ) { RelativePath = PathParts[1 ]; } else if (PathParts.Length == 1 ) { RelativePath = PathParts[0 ]; } if (RelativePath != null ) { DirectoryReference InputDir = DirectoryReference.Combine(ProjectContentRoot, RelativePath); StagedDirectoryReference OutputDir = StagedDirectoryReference.Combine(StageContentRoot, RelativePath); if (bUFS) { List<FileReference> Files = SC.FindFilesToStage(InputDir, StageFilesSearch.AllDirectories); Files.RemoveAll(x => x.HasExtension(".uasset" ) || x.HasExtension(".umap" ) || (SC.DedicatedServer && x.HasExtension(".mp4" ))); SC.StageFiles(StagedFileType.UFS, InputDir, Files, OutputDir); } else { SC.StageFiles(StagedFileType.NonUFS, InputDir, StageFilesSearch.AllDirectories, OutputDir); } } } } } public static void CreateStagingManifest (ProjectParams Params, DeploymentContext SC ) { if (PlatformGameConfig != null ) { StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, true , "DirectoriesToAlwaysStageAsUFS" ); StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, false , "DirectoriesToAlwaysStageAsNonUFS" ); if (SC.DedicatedServer) { StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, true , "DirectoriesToAlwaysStageAsUFSServer" ); StageAdditionalDirectoriesFromConfig(SC, ProjectContentRoot, StageContentRoot, PlatformGameConfig, false , "DirectoriesToAlwaysStageAsNonUFSServer" ); } } }
在Directory.Combine
里正确地处理了我们所指定的相对于Content的../Source/Script
路径。
FDataTime::UtcNow 注意FDataTime::UtcNow在同一帧的不同时机获取的到是不一样的,因为它底层调用的是GetSystemTime
(Windows)。
uexp和ubulk的作用
Rather than one large asset, these allow us to write an asset’s bulk data (.ubulk) and exports (uexp) out into separate files. This system improves perf in certain circumstances where file read contiguity is lost due to the large size of assets. This feature avoids this by enabling the reader to skip over an asset’s bulk data when seeking to the next file in a series without having to actually have serialized and seeked past that data (since it’s in separate file).
为了优化性能把资源的信息和数据进行拆分,在进行资源信息的索引时不用访问真正的数据,提高了查找性能和内存消耗。
资源调试命令 可以在Console中使用以下命令:
会列出当前的资源加载信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Class Count NumKB MaxKB ResExcKB ResExcDedSysKB ResExcShrSysKB ResExcDedVidKB ResExcShrVidKB ResExcUnkKB Class 4331 10240.85 12969.37 0.00 0.00 0.00 0.00 0.00 0.00 FontFace 9 9498.02 9498.02 9495.54 9495.54 0.00 0.00 0.00 0.00 MetaData 732 8441.93 8441.93 0.00 0.00 0.00 0.00 0.00 0.00 ScriptStruct 2165 3923.20 5156.56 0.00 0.00 0.00 0.00 0.00 0.00 SkeletalMesh 1 3988.86 3989.04 1974.06 30.55 0.00 0.00 0.00 1943.52 Function 7928 2012.63 2457.73 0.00 0.00 0.00 0.00 0.00 0.00 Package 732 1316.65 1453.87 0.00 0.00 0.00 0.00 0.00 0.00 Enum 1275 312.38 760.76 0.00 0.00 0.00 0.00 0.00 0.00 DeviceProfile 85 409.36 661.37 0.00 0.00 0.00 0.00 0.00 0.00 Material 103 418.41 467.83 4722.42 4722.42 0.00 0.00 0.00 0.00 ToolMenu 36 193.83 466.16 0.00 0.00 0.00 0.00 0.00 0.00 DelegateFunction 652 165.51 185.93 0.00 0.00 0.00 0.00 0.00 0.00 Texture2D 146 111.05 111.05 56192.00 0.00 0.00 56192.00 0.00 0.00 MaterialExpressionMultiply 194 66.84 98.67 0.00 0.00 0.00 0.00 0.00 0.00 StaticMesh 29 77.02 98.41 2055.31 23.56 0.00 0.00 0.00 2031.75 MaterialExpressionTextureSample 61 44.87 79.66 0.00 0.00 0.00 0.00 0.00 0.00 MaterialExpressionCustom 68 59.53 70.68 0.00 0.00 0.00 0.00 0.00 0.00 MaterialInstanceDynamic 32 53.74 65.76 16.45 16.45 0.00 0.00 0.00 0.00 GameNetworkMgr 1 64.43 64.43 0.00 0.00 0.00 0.00 0.00 0.00 BodySetup 41 42.91 62.30 1158.44 1158.44 0.00 0.00 0.00 0.00 MaterialExpressionConstant 153 36.16 61.26 0.00 0.00 0.00 0.00 0.00 0.00
还有下列相关的命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Mem FromReport obj list -alphasort rhi.DumpMemory LogOutStatLevels ListSpawnedActors DumpParticleMem ConfigMem r.DumpRenderTargetPoolMemory ListTextures -alphasort ListSounds -alphasort ListParticleSystems -alphasort obj list class=SoundWave -alphasort obj list class=SkeletalMesh -alphasort obj list class=StaticMesh -alphasort obj list class=Level -alphasort
也可以使用memreport
来进行详细的分析:
会在Saved/Profiling/MemReports
下创建.memreport
文件。
IOS基础包拆分 在前面提到了UE为Android提供了打包到obb中的文件过滤规则:
1 2 3 [/Script/AndroidRuntimeSettings.AndroidRuntimeSettings] +ObbFilters=-pakchunk1-*
但是UE并没有为IOS提供相应的操作,默认情况下会把IOS的所有的pak文件都打包至IPA中。
为了统一Android和IOS的基础包规则,我自己实现了IOS上类似Android那种指定过滤规则的功能,做个简单的介绍。
我使用的是Mac远程打包,流程是在Mac上编译代码生成IPA,拉回Win,在Win上进行Cook,生成Pak文件,最后把原始IPA解包,再添加Pak等文件组合成最终IPA。
我的需求是,自定义指定过滤规则,可以把某些文件忽略,不打包到IPA中。那么这一步的操作其实就位于把IPA解包再打包的流程里,经过翻阅UE的代码,发现这个操作是通过iPhonePackager
这个独立程序来实现的,那么就需要对这个程序的代码进行改造了。
经过调试分析,发现真正实现重新打包IPA的操作是在以下函数中执行的:
Programs/IOS/iPhonePackager/CookTime.cs 1 2 3 4 /** * Using the stub IPA previously compiled on the Mac, create a new IPA with assets */ static public void RepackageIPAFromStub();
该函数位于iPhonePackager
-CookTime
类中。
1 2 3 4 5 6 7 8 9 10 11 12 static public void RepackageIPAFromStub ( ) { string SourceDir = Path.GetFullPath(ZipSourceDir); string [] PayloadFiles = Directory.GetFiles(SourceDir, "*.*" , Config.bIterate ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories); foreach (string Filename in PayloadFiles) { } }
需要做的操作就是介入这个过程,把PayloadFiles
中的文件列表通过我们自定义的规则来执行过滤。
从流程上分为以下几个步骤:
从项目中读取Filter的配置
创建出真正的过滤器
在RepackageIPAFromStub
遍历文件的流程里使用过滤器进行检测是否需要被打入ipa
只需要几十行代码就可以实现,首先需要添加一个IniReader的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 using Tools.DotNETCommon;using System.Runtime.InteropServices;using Ini;namespace Ini { public class IniReader { private string path; [DllImport("kernel32" ) ] private static extern int GetPrivateProfileString (string section, string key, string def, StringBuilder retVal, int size, string filePath ) ; public IniReader (string INIPath ) { path = INIPath; } public string ReadValue (string Section, string Key ) { StringBuilder ReaderBuffer = new StringBuilder(255 ); int ret = GetPrivateProfileString(Section, Key, "" , ReaderBuffer, 255 , this .path); return ReaderBuffer.ToString(); } } }
然后在RepackageIPAFromStub
函数中创建过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FileFilter IpaPakFileFilter = new FileFilter(FileFilterType.Include); { string ProjectDir = Directory.GetParent(Path.GetFullPath(Config.ProjectFile)).FullName; string EngineIni = Path.Combine(ProjectDir,"Config" ,"DefaultEngine.ini" ); IniReader EngineIniReader = new IniReader(EngineIni); Program.Log("RawPakFilterRules {0}" , RawPakFilterRules); string [] PakRules = RawPakFilterRules.Split(',' ); List<string > PakFilters = new List<string >(PakRules); if (PakFilters != null ) { IpaPakFileFilter.AddRules(PakFilters); } }
这里从项目的Config/DefaultEngine.ini
的[/Script/IOSRuntimeSettings.IOSRuntimeSettings]项读取IPAFilters
的值,规则与Android相同,但是要把规则都写在一行,多个规则以逗号分隔。
1 2 [/Script/IOSRuntimeSettings.IOSRuntimeSettings] IPAFilters =-*.pak,pakchunk0-*
最终,还需要在RepackageIPAFromStub
遍历Payload
文件的循环中进行检测是否匹配我们指定的过滤规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static public void RepackageIPAFromStub ( ) { string SourceDir = Path.GetFullPath(ZipSourceDir); string [] PayloadFiles = Directory.GetFiles(SourceDir, "*.*" , Config.bIterate ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories); foreach (string Filename in PayloadFiles) { if (!IpaPakFileFilter.Matches(Filename)) { Program.Log("IpaPakFileFilter not match file {0}" , Filename); continue ; } } }
这样再执行打包IOS,就会按照指定的过滤规则来添加文件了,实现了与Android上一致的行为。
打包过程中的Log如下(上文代码已注释):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Saving IPA ... ProjectDir path C:\BuildAgent\workspace\PackageWindows\Client EngineIni path C:\BuildAgent\workspace\PackageWindows\Client\Config\DefaultEngine.ini RawPakFilterRules -*.pak,pakchunk0-* PakRules -*.pak PakRules pakchunk0-* IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Assets.car IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Info.plist IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\LaunchScreenIOS.png IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Manifest_DebugFiles_IOS.txt IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Manifest_NonUFSFiles_IOS.txt IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\mute.caf IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\ue4commandline.txt IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\movies\logo.mp4 IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\movies\sparkmore.mp4 IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\paks\pakchunk0-ios.pak IpaPakFileFilter not match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\paks\pakchunk1-ios.pak IpaPakFileFilter not match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\cookeddata\fgame\content\paks\pakchunk2-ios.pak IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\Engine\Content\SlateDebug\Fonts\LastResort.ttf IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GCloudVoice.bundle\files\config.json IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GCloudVoice.bundle\files\libwxvoiceembed.bin IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GCloudVoice.bundle\files\mute_detection.aiff IpaPakFileFilter match file C:\BuildAgent\workspace\PackageWindows\Client\Saved\StagedBuilds\IOS\GRobotResource.bundle\config.json ...
可以看到,过滤规则已经生效了。
Android基础包拆分 在打包时,不想要把所有的资源都打包到apk中,所以可以在打包时进行拆分,只把必要资源打包到apk中,首先需要把基础包中的资源进行pak拆分,可以把通过Project Settings
-Asset Manager
中进行设置或者通过创建PrimaryAssetLable
资源进行标记。
目标是:
把基础包的打包资源拆分到多个Pak中
只把必要的pak文件打包到apk里
其余的pak在运行时进行下载
第一步都可以通过项目设置进行控制,第二部的条件就是要实现一个过滤规则,不过UE已经提供了这个机制,可以指定过滤掉哪些文件,只需要添加配置即可。
1 2 3 4 5 [/Script/AndroidRuntimeSettings.AndroidRuntimeSettings] +ObbFilters=-pakchunk1-* +ObbFilters=-pakchunk2-* +ObbFilters=-pakchunk3-*
ObbFilters的规则以-
开头就是排除规则,会把基础包中的chunk1-3
的pak给过滤掉,可以用于后续的下载流程。
也可以指定Exclute
和Include
规则组合来用:
1 2 +ObbFilters=-*.pak +ObbFilters=pakchunk0-*
第一步忽略掉所有的pak文件,然后把pakchunk0-*.pak
显式添加至obb中。
UMG OnTouchStarted不触发 注意不要使用UButton来作为触发控件来接收OnTouchStarted的事件,使用Board或者Image都可以,但是UButton不行,估计是UButton拦截了事件。
监听资源操作事件 可以通过IAssetRegistry
获取到下列事件的delegate并监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetAddedEvent, FAssetAddedEvent); virtual FAssetAddedEvent& OnAssetAdded () override { return AssetAddedEvent; }DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetRemovedEvent, FAssetRemovedEvent); virtual FAssetRemovedEvent& OnAssetRemoved () override { return AssetRemovedEvent; }DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetRenamedEvent, FAssetRenamedEvent); virtual FAssetRenamedEvent& OnAssetRenamed () override { return AssetRenamedEvent; }DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FAssetUpdatedEvent, FAssetUpdatedEvent ); virtual FAssetUpdatedEvent& OnAssetUpdated () override { return AssetUpdatedEvent; }DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FInMemoryAssetCreatedEvent, FInMemoryAssetCreatedEvent ); virtual FInMemoryAssetCreatedEvent& OnInMemoryAssetCreated () override { return InMemoryAssetCreatedEvent; }DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FInMemoryAssetDeletedEvent, FInMemoryAssetDeletedEvent ); virtual FInMemoryAssetDeletedEvent& OnInMemoryAssetDeleted () override { return InMemoryAssetDeletedEvent; }DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FFilesLoadedEvent, FFilesLoadedEvent ); virtual FFilesLoadedEvent& OnFilesLoaded () override { return FileLoadedEvent; }DECLARE_DERIVED_EVENT( UAssetRegistryImpl, IAssetRegistry::FFileLoadProgressUpdatedEvent, FFileLoadProgressUpdatedEvent ); virtual FFileLoadProgressUpdatedEvent& OnFileLoadProgressUpdated () override { return FileLoadProgressUpdatedEvent; }
Android not found uproject UE中有一个BUG,在4.25.1引擎版本中可以复现,步骤如下:
安装apk,第一次启动游戏
打开UE的沙盒数据目录UE4Game/PROJECTNAME
,在这个目录下创建Content/Paks
目录
重新启动游戏
Log中也有Project file not found: ../../../FGame/FGame.uproject
提示。
在Android上自动挂载的Pak文件可以放到Saved/Paks
下,有时间具体分析一下这个问题。
提取chunk的paklist文件 在开启Generate Chunks
之后,如果项目中有添加PrimaryAssetLable
资源,会生成对应的Chunk文件。 生成的paklist文件所在目录为:
1 2 3 4 5 6 # 源码版 Engine\Programs\AutomationTool\Saved\Logs # 安装版 C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.26 # 安装版 BuildCookRun C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.26\BuildCookRun
paklist相关的文件列表如下:
1 2 3 4 5 6 PakList_pakchunk0-WindowsNoEditor.txt PakList_pakchunk1-WindowsNoEditor.txt PakList_pakchunk2-WindowsNoEditor.txt PrePak_WindowsNoEditor_NonUFSFiles.txt PrePak_WindowsNoEditor_NonUFSFilesDebug.txt PrePak_WindowsNoEditor_UFSFiles.txt
需要重点关注的是PakList_pakchunk*.txt
和PrePak*_UFSFiles.txt
这几个文件,NonUFSFiles.txt
中的文件不会被打包到Pak中。PakList_pakchunk*.txt
中是每一个chunk中包含的文件,并且是以绝对路径 Mount路径
的方式来组织的,PrePak*_UFSFiles.txt
是当前打包的版本中所有包含的文件,但其中的路径不是绝对路径 Mount路径
。
在项目设置中添加的NoUFS
文件夹都会默认打包到chunk0
的pak中。
在Windows上可以使用以下命令来自动拷贝:
1 echo f|xcopy /y/i/s/e "%AppData%\Unreal Engine\AutomationTool\Logs\E+UnrealEngine+Launcher+UE_4.25\PakList_*.txt" "E:\ClientVersion\0.0.1.0"
IOS远程构建最大文件不能超过2G Win上远程构建出IOS包的流程是代码和bundle都上传到Mac上编译,生成不包含资源的IPA,拉回本地执行资源Cook生成Pak后,把代码的IPA和资源的Pak合并成真正的IPA文件。 但是这样有个问题,UE里的实现是把所有要合并的文件读到内存中再合并打包的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 static public void RepackageIPAFromStub ( ) { { string SourceDir = Path.GetFullPath(ZipSourceDir); string [] PayloadFiles = Directory.GetFiles(SourceDir, "*.*" , Config.bIterate ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories); foreach (string Filename in PayloadFiles) { string AbsoluteFilename = Path.GetFullPath(Filename); string RelativeFilename = AbsoluteFilename.Substring(SourceDir.Length + 1 ).Replace('\\' , '/' ); string ZipAbsolutePath = String.Format("Payload/{0}{1}.app/{2}" , Config.GetTargetName(), Program.Architecture, RelativeFilename); byte [] FileContents = File.ReadAllBytes(AbsoluteFilename); if (FileContents.Length == 0 ) { FileContents = new byte [1 ]; FileContents[0 ] = 0 ; } FileSystem.WriteAllBytes(RelativeFilename, FileContents); if ((FileContents.Length >= 1024 * 1024 ) || (Config.bVerbose)) { FilesBeingModifiedToPrintOut.Add(ZipAbsolutePath); } } } }
可以看到是把Payload的每个文件读到byte[]
里的,这就有了一个限制,在C#中,数组的长度最大是int32.MaxValue
,意味着byte[]
不能存储超过2G的文件,不然就会触发异常。 查询了MSDN的文档,发现设置gcAllowVeryLargeObjects 也不会改变单个维度的数组的大小。
所以这个问题只能从其他方面入手了,让UE进行资源打包的时候把Pak的文件拆分,让每个文件都小于2GB即可,可以通过UE里的Chunk机制进行拆分。
Android运行时请求权限 ActivityCompact 里面有个 requestPermission方法,可以用来处理这种情况。
DS产生corefile 在Shipping的时候DS Crash可以通过启动参数-core
来指定可以生成core文件。
指定SkeletalMesh的LOD级别 可以直接对设置ForcedLodModel的值(LOD0需要设置1,实际的LOD级别就是N-1,值为0则是自动):
也可以对USkinnedMeshComponent
实例调用SetForcedLOD
函数:
Runtime/Engine/Classes/Components/SkinnedMeshComponent.h 1 2 3 4 int32 USkinnedMeshComponent::GetForcedLOD () const void USkinnedMeshComponent::SetForcedLOD (int32 InNewForcedLOD)
可以用在背包中显示3D模型的场景,避免使用比较低的LOD级别。
HotPatcher的自动化导出Release脚本 组合命令使用HotPatcher的Commandlet,实现Release信息的自动化导出:
HotRelease.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import os import sys import argparse parser = argparse.ArgumentParser(description="used for build engine or project" ) hot_release = parser.add_argument_group('HotRelease' ) hot_release.add_argument('--enginebin' ,help='UE4Editor-cmd.exe binary path' ) hot_release.add_argument('--projectdir' ,help='project root directory' ) hot_release.add_argument('--projectname' ,help='project name,match projectname.uproject' ) hot_release.add_argument('--versiondir' ,help='client version file dir' ) hot_release.add_argument('--versionid' ,help='current release version id' ) hot_release.add_argument('--outdir' ,help='export release result save dir' ) platform_list=[ "WindowsNoEditor" , "Android_ASTC" , "IOS" , ] def getPlatformPakListName (PlatformName,ProjectName ): return "PakList_%s-%s.txt" % (ProjectName,PlatformName) def getProjectFullPath (ProjectDir,ProjectName ): return "%s\\%s.uproject" % (ProjectDir,ProjectName) def get_platform_paklist (clientversion_path,versionid,project_name ): result_dict = {} if os.path.exists(clientversion_path): version_paklist_path = os.path.normpath(os.path.abspath(os.path.join(clientversion_path, versionid))) print("versionid: %s" % versionid) for platform in platform_list: platform_paklist_path = os.path.join(version_paklist_path, getPlatformPakListName(platform,project_name)) if os.path.exists(platform_paklist_path): result_dict[platform] = platform_paklist_path print("platform:%s paklist:%s" % (platform,result_dict[platform])) return result_dict def ExportRelease (versionid,engine_bin_path,project_dir,project_name,platform_paklist_dict,savepath ): AddPlatformPakListCmd = "-AddPlatformPakList=" for key,value in platform_paklist_dict.items(): AddPlatformPakListCmd = "%s%s+%s," % (AddPlatformPakListCmd,key,value) print(AddPlatformPakListCmd) commands_tuple = [ engine_bin_path, getProjectFullPath(project_dir,project_name), "-run=HotRelease" , "-versionid=%s" % (versionid), "-byPakList=true" , AddPlatformPakListCmd, "-savepath.path=%s" % (savepath), ] final_cmd = "" for param in commands_tuple: final_cmd = "%s %s" % (final_cmd,param) print(final_cmd) os.system(final_cmd) def GetArgByName (ParserArgs,ArgName ): ArgsPairs = ParserArgs.__dict__ for key,value in ArgsPairs.items(): if key == ArgName: return value def printSelectorHelp (): print("Args is invalid!" ) def main (): ParserArgs = parser.parse_args() engine_bin_path = GetArgByName(ParserArgs,"enginebin" ) project_dir = GetArgByName(ParserArgs,"projectdir" ) project_name = GetArgByName(ParserArgs,"projectname" ) version_id = GetArgByName(ParserArgs,"versionid" ) clientversion_path = GetArgByName(ParserArgs,"versiondir" ) outdir = GetArgByName(ParserArgs,"outdir" ) if engine_bin_path and project_dir and project_name and version_id and clientversion_path and outdir: ExportRelease( version_id, engine_bin_path, project_dir, project_name, get_platform_paklist(clientversion_path,version_id,project_name), outdir ) else : printSelectorHelp() if __name__ == "__main__" : main()
Texture的压缩 之前的笔记中,提到过可以在Project Settings
-Cooker
-Texture
-ASTC Compression vs Size
可以设置默认的资源质量和大小的级别:
1 2 3 4 5 0=12x12 1=10x10 2=8x8 3=6x6 4=4x4
在Texture的资源编辑中也可以针对某个Texture单独设置:
Lowest->Hightest对应着0-4
的值,使用Default则使用项目设置中的配置。
并且,设置Compression Settings
的类型也会对资源压缩的类型有差别,Default则是项目设置中的参数,如果设置成NormalMap的类型会是ASTC_4x4
的。
listtextures 控制台命令listtextures
可以列出已被加载过的Texture的信息。
Unreal Insights实时分析Android 上一节提到了使用SessionFrontEnd
实时分析Android的方法,在实际的测试当中发现不太稳定,会造成游戏的Crash,UE在新的引擎版本中也提供了新的性能分析工具Unreal Insights,可以更方便和直观地进行Profile。
同样也需要端口映射,需要把PC的1980端口映射到设备上:
1 adb reverse tcp:1980 tcp:1980
然后需要给Android设备添加启动命令:
1 ../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device" -iterative -tracehost=127.0.0.1 -Trace=CPU
在PC上开启Unreal Insights,在手机上启动游戏,即可实时捕获:
Unreal Insights也可以实时捕获PIE的数据,需要在Editor启动时添加-trace
参数:
1 UE4Editor.exe PROJECT_NAME.uproject -trace=counters,cpu,frame,bookmark,gpu
在启动游戏后在Unreal Insights里通过New Connection
监听127.0.0.1即可。
SessionFrontEnd实时Profile Android 有几个条件:
需要USB连接PC和手机
需要安装adb
首先需要映射端口,因为SessionFrontEnd
是通过监听端口的方式来与游戏内通信的,手机和PC并不在同一个网段,所以需要以adb的形式把PC的监听端口转发给手机的端口。
SessionFrontEnd的监听端口可以通过对UE4Editor.exe
的端口分析获取:
1 2 3 4 5 6 7 8 9 C:\Users\lipengzha>netstat -ano | findstr "231096" TCP 0.0.0.0:1985 0.0.0.0:0 LISTENING 231096 TCP 0.0.0.0:3961 0.0.0.0:0 LISTENING 231096 TCP 0.0.0.0:3963 0.0.0.0:0 LISTENING 231096 TCP 127.0.0.1:4014 127.0.0.1:12639 ESTABLISHED 231096 TCP 127.0.0.1:4199 127.0.0.1:12639 ESTABLISHED 231096 UDP 0.0.0.0:6666 *:* 231096 UDP 0.0.0.0:24024 *:* 231096 UDP 0.0.0.0:58101 *:* 231096
需要把PC的1985端口映射到Android的1985端口,这样手机上APP启动时,连接0.0.0.0
的1985
端口就可以连接到PC上的端口。
通过adb命令来执行:
1 adb reverse tcp:1985 tcp:1985
然后需要给手机上App指定启动参数:
1 ../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device"
把这些文本保存为UE4Commandline.txt
文件,放到项目的数据目录下即可,具体路径为:
1 /sdcard/UE4Game/PROJECT_NAME/
之后直接启动App,在PC上的SessionFrontEnd
中就可以看到设备的数据了。
C++中获取引擎的版本信息 可以通过FEngineVersion
来获取:
Runtime/Core/Public/Misc/EngineVersion.h 1 2 static const FEngineVersion& Current () ;
判断对象是否有效的方式与区别 在UE中的UObject对象实例传递和存储都是以UObject*
的指针来存储的,因为指针只是一块内存的地址,而这块内存是否是有效的对象是不清楚的。所以需要有不同检测方式来检测,一般情况下有以下几种状态:
指针为NULL/nullptr
指针非NULL,对象被GC标记为PaddingKill
一块无效的内存地址
如果指针地址是一个无效的内存地址,那么不能通过它来调用任何获取/修改到任何数据成员的函数的。如果对无效的内存地址调用IsPaddingKill的,会Crash,所以要从更底层的角度来检测。
这三种状态可以通过以下几种检测方式来判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 bool IsValidLowLevel () const ; bool IsValidLowLevelFast (bool bRecursive = true ) const ; FORCEINLINE bool IsValid (const UObject *Test) { return Test && !Test->IsPendingKill(); }
M1 Mac的UE4.26兼容性报告
UMG CanvasPanel合批 在Project Settings
-Engine
-Slate Settings
中开启Explicit Canvas Child ZOrder
可以开启Canvas的合并。
蓝图节点右上角标识的含义
OptimizeCode代码优化
注意:如果关闭代码优化 导致构造对象Crash(或者new走不到对象的构造函数),需要检查项目和插件的代码中是否有重名的类,会导致一些奇怪的问题。 在build.cs中可以通过OptimizeCode
来控制是否执行代码优化:
1 OptimizeCode = CodeOptimization.InShippingBuildsOnly;
有以下几个可选值:
1 2 3 4 5 6 7 8 public enum CodeOptimization { Never, InNonDebugBuilds, InShippingBuildsOnly, Always, Default, }
当开启优化时,对翻译单元的编译指令参数:
并且会使用优化版本的Engine/SharedPCH.Engine.h
。
当关闭优化时的编译指令为:
会使用非优化版本的Engine/SharedPCH.Engine.NonOptimized.h
.
ASTC Compression Quality By Size 在项目的DefaultEngine.ini中的[/Script/UnrealEd.CookerSettings]
中通过DefaultASTCQualityBySize
设置(0-4):
1 2 3 [/Script/UnrealEd.CookerSettings] DefaultASTCQualityBySize =2 DefaultASTCQualityBySpeed =3
0-4分别对应以下压缩级别:
1 2 3 4 5 0=12x12 1=10x10 2=8x8 3=6x6 4=4x4
引擎修改语言的配置存储 在UE引擎中的的编辑器偏好设置 中修改区域和语言 ,会被存储在以下文件中:
1 C:\Users\lipengzha\AppData\Local\UnrealEngine\4.25\Saved\Config\Windows\EditorSettings.ini
其值如下:
1 2 3 4 [Internationalization] Language =zh-Hans Culture= Locale =zh-Hans
如果是英文的,则是en
.
中文赋值给FString 首先,把文件编码修改为UTF8,然后使用以下方式:
1 FString str = UTF8_TO_TCHAR("中文" );
注意对UTF8编码的中文不要使用TEXT,因为文件编码UTF8已经是把中文字符串编码成了UTF8的方式,所以可以直接使用""
来包裹中文字符,如果此时使用TEXT
作为宽字符存储UTF8的编码,在UTF8_TO_TCHAR
中会出现错误。
IOS相对到绝对路径转换 在接入的一些库中,需要传递文件的绝对路径,可以通过下面的方式进行转换:
1 IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*InRelatePath);;
它是定义在IFileManager
接口中的一个虚函数,应该在各个平台的PlatformFile中均有自己的实现,但是在Android中依然是相对路径的,不知道UE是不是忘了实现了。
IOS:Runtime/Core/Public/IOS/IOSPlatformFile.h
Android上相对路径转换成绝对路径的方式在之前的笔记中有写。
Android获取包名 通过UPL在Java里添加以下代码:
1 2 3 4 5 public String AndroidThunkJava_GetPackageName () { Context context = getApplicationContext(); return context.getPackageName(); }
在C++里通过JNI调用即可:
1 2 3 4 5 6 7 8 9 10 11 12 FString UFlibAppHelper::GetAppPackageName () { FString result; #if PLATFORM_ANDROID if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) { jmethodID GetInstalledPakPathMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetPackageName" , "()Ljava/lang/String;" , false ); result = FJavaHelper::FStringFromLocalRef(Env, (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis,GetInstalledPakPathMethodID)); } #endif return result; }
Android获取外部存储路径 获取App的沙盒路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 FString FAndroidGCloudPlatformMisc::getExternalStorageDirectory () { FString result; if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) { jobject JniEnvContext; { jclass activityThreadClass = Env->FindClass("android/app/ActivityThread" ); jmethodID currentActivityThread = FJavaWrapper::FindStaticMethod(Env, activityThreadClass, "currentActivityThread" , "()Landroid/app/ActivityThread;" , false ); jobject at = Env->CallStaticObjectMethod(activityThreadClass, currentActivityThread); jmethodID getApplication = FJavaWrapper::FindMethod(Env, activityThreadClass, "getApplication" , "()Landroid/app/Application;" , false ); JniEnvContext = FJavaWrapper::CallObjectMethod(Env, at, getApplication); } jmethodID getExternalFilesDir = Env->GetMethodID(Env->GetObjectClass(JniEnvContext), "getExternalFilesDir" , "(Ljava/lang/String;)Ljava/io/File;" ); jobject ExternalFileDir = Env->CallObjectMethod(JniEnvContext, getExternalFilesDir,nullptr ); jmethodID getFilePath = Env->GetMethodID(Env->FindClass("java/io/File" ), "getPath" , "()Ljava/lang/String;" ); jstring pathString = (jstring)Env->CallObjectMethod(ExternalFileDir, getFilePath, nullptr ); const char *nativePathString = Env->GetStringUTFChars(pathString, 0 ); result = ANSI_TO_TCHAR(nativePathString); } return result; }
Android相对路径转绝对路径 有些需求需要把FPaths::ProjectDir()
等路径转换为移动设备上的绝对路径,可以参考Core/Private/Android/AndroidPlatformFile.cpp#L126 中的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const FString &GetFileBasePath () { static FString BasePath = GFilePathBase + FString(FILEBASE_DIRECTORY) + FApp::GetProjectName() + FString("/" ); return BasePath; } FString AndroidRelativeToAbsolutePath (bool bUseInternalBasePath, FString RelPath) { if (RelPath.StartsWith(TEXT("../" ), ESearchCase::CaseSensitive)) { do { RelPath.RightChopInline(3 , false ); } while (RelPath.StartsWith(TEXT("../" ), ESearchCase::CaseSensitive)); return (bUseInternalBasePath ? GInternalFilePath : GetFileBasePath()) / RelPath; } return RelPath; }
删除AndroidManifest.xml中的项 因为UE默认会给AndroidManifest.xml
添加项,如果其中的项我们想要手动控制,直接添加的话会产生错误,提示已经存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 UATHelper: Packaging (Android (ASTC)): > Task :app:processDebugManifest FAILED UATHelper: Packaging (Android (ASTC)): UATHelper: Packaging (Android (ASTC)): Z:\app\src\main\AndroidManifest.xml:47 :5 -106 Error: UATHelper: Packaging (Android (ASTC)): Element meta-data#com.epicgames.ue4.GameActivity.bUseExternalFilesDir at AndroidManifest.xml:47:5-106 duplicated with element declared at AndroidManifest.xml:27:5-107 UATHelper: Packaging (Android (ASTC)): Z:\app\src\main\AndroidManifest.xml Error: UATHelper: Packaging (Android (ASTC)): Validation failed, exiting UATHelper: Packaging (Android (ASTC)): UATHelper: Packaging (Android (ASTC)): FAILURE: Build failed with an exception. UATHelper: Packaging (Android (ASTC)): UATHelper: Packaging (Android (ASTC)): * What went wrong: UATHelper: Packaging (Android (ASTC)): Execution failed for task ':app:processDebugManifest'. UATHelper: Packaging (Android (ASTC)): > Manifest merger failed with multiple errors, see logs UATHelper: Packaging (Android (ASTC)): UATHelper: Packaging (Android (ASTC)): See http: UATHelper: Packaging (Android (ASTC)): UATHelper: Packaging (Android (ASTC)): * Try: UATHelper: Packaging (Android (ASTC)): Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. UATHelper: Packaging (Android (ASTC)): UATHelper: Packaging (Android (ASTC)): * Get more help at https: UATHelper: Packaging (Android (ASTC)): UATHelper: Packaging (Android (ASTC)): BUILD FAILED in 10 s UATHelper: Packaging (Android (ASTC)): 189 actionable tasks: 1 executed, 188 up-to-date UATHelper: Packaging (Android (ASTC)): ERROR: cmd.exe failed with args /c "C:\Users\lipengzha\Documents\Unreal Projects\GCloudExample\Intermediate\Android\armv7\gradle\rungradle.bat" :app:assembleDebug PackagingResults: Error: cmd.exe failed with args /c "C:\Users\lipengzha\Documents\Unreal Projects\GCloudExample\Intermediate\Android\armv7\gradle\rungradle.bat" :app:assembleDebug UATHelper: Packaging (Android (ASTC)): Took 13.3060694 s to run UnrealBuildTool.exe, ExitCode=6 UATHelper: Packaging (Android (ASTC)): UnrealBuildTool failed. See log for more details. (C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.25 \UBT-.txt) UATHelper: Packaging (Android (ASTC)): AutomationTool exiting with ExitCode=6 (6 ) UATHelper: Packaging (Android (ASTC)): BUILD FAILED PackagingResults: Error: Unknown Error
如果想要修改或者删除UE默认生成的AndroidManifest.xml
中的项,可以通过先删除再添加的方式。
以删除以下项为例:
1 <meta-data android:name="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" android:value="false" />
在UPL的androidManifestUpdates
中编写以下代码:
1 2 3 4 5 6 7 8 9 10 11 <androidManifestUpdates > <loopElements tag ="meta-data" > <setStringFromAttribute result ="ApplicationSectionName" tag ="$" name ="android:name" /> <setBoolIsEqual result ="bUseExternalFilesDir" arg1 ="$S(ApplicationSectionName)" arg2 ="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" /> <if condition ="bUseExternalFilesDir" > <true > <removeElement tag ="$" /> </true > </if > </loopElements > </androidManifestUpdates >
就是去遍历AndroidManfest.xml
中已经存在meta-data
中,android:name
为com.epicgames.ue4.GameActivity.bUseExternalFilesDir
的项给删除。
强引用UClass 如果在UPROPERTY中通过TSubclassOf
引用了一个UClass,会导致该UClass的BP的CDO无法被释放:
1 2 URPOPERTY() TSubclassOf<class UClassName > StrongClassRef ;
可以使用软引用的方式来解决:
1 2 URPOPERTY() TSoftClassPtr<class UClassName > StrongClassRef ;
TSoftClassPtr is a templatized wrapper around FSoftObjectPtr that works like a TSubclassOf, it can be used in UProperties for blueprint subclasses
UE4编译代码的真正命令参数 在之前的文章Build flow of the Unreal Engine4 project 中有提到,UE编译模块的时候会执行到ExecuteActions
中。 那么UE真正编译每个翻译单元的编译器参数是什么呢? 在UBT调用ExecuteAction之前,会完成所有编译参数的拼接和预处理。
1 static void ExecuteAction (ManagedProcessGroup ProcessGroup, BuildAction Action, List CompletedActions, AutoResetEvent CompletedEvent)
在Win上是通过调用cl-filter.exe
来执行的,而如何把代码和编译参数喂给编译器呢? UE是通过生成了一个.cpp.obj.response
来记录当前编译单元的信息的,包含编译器参数和包含目录/输出等等。
该文件在生成位置在模块的Intermediate目录下:
1 Intermediate\Build\Win64\UE4Editor\Development\HotPatcherRuntime\FlibPakReader.cpp.obj.response
文件内容太长,可以下载该文件查看:FlibPakReader.cpp.obj.response
可以使用手动调用cl.exe
的方式来执行测试:
1 2 cl.exe @CPP_OBJ_RESPONSE_PATH //showIncludes // "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.27.29110\bin\HostX64\x64\cl.exe" @"C:\BuildAgent\workspace\PackageWindows\Client\Plugins\UnLua\Intermediate\Build\Win64\UE4\Development\LuaProtobuf\Module.LuaProtobuf.cpp.obj.response" /showIncludes
build.cs添加宏定义的值 在UE中使用build.cs
添加宏定义:
1 2 PrivateDefinitions.Add("TEST_MACRO_HAS_VALUE=1" ); PublicDefinitions.Add("TEST_MACRO_NOT_VALUE" );
可以指定值,也可以不指定宏的值,但是UE生成时会给没有值的宏为1
:
1 #define TEST_MACRO_NOT_VALUE 1
可以在UE生成的Deginitions.MODULENAME.h
中查看,位于以下位置:
1 Intermediate\Build\PLATFIRM_NAME\UE4\Development\MODULE_NAME\Definitions.MODULE_NAME.h
编译引擎的命令 与BuildGraph的方式不同,直接在VS中点UE4编译所使用的命令行:
1 Engine/Build/BatchFiles/Build.bat -Target="UE4Editor Win64 Development" -Target="ShaderCompileWorker Win64 Development -Quiet" -WaitMutex -FromMsBuild
编辑器检测Actor移动 可以通过重写Editor
中的PostEditMove
函数:
1 virtual void PostEditMove (bool bFinished) override ;
Actor Tick in Editor 在Actor的构造函数中开启Tick:
1 PrimaryActorTick.bCanEverTick = true ;
但是这个只能设置Runtime的Actor的Tick,当想要让Editor下的Actor也能执行Tick,则需要重写Actor的ShouldTickIfViewportsOnly
函数:
1 2 3 4 5 6 7 8 virtual bool ShouldTickIfViewportsOnly () const override ;bool ARecastDetourTestingActor::ShouldTickIfViewportsOnly () const { return true ; }
在AActor
的类中,默认返回false.
UE4 ERROR: Missing object file 在编译时遇到以下错误:
1 2 3 ERROR: Missing object file C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine\Engine\Plugins\Runtime\Database\SQLiteCore\Intermediate\Build\Android\UE4\Developmen t\SQLiteCore\codec.ca7.o listed in C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine\Engine\Plugins\Runtime\Database\SQLiteCore\Intermediate\Build\Android\UE4\Develo pment\SQLiteCore\SQLiteCore.precompiled
在引擎中添加了代码,并且编译了Android的的平台支持(通过Make Installed Win64
),但是在编译IOS时出现这样的报错。 经过排查发现,这个错误时找不到codec.ca7.o
文件导致的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public override List<FileItem> Compile (ReadOnlyTargetRules Target, UEToolChain ToolChain, CppCompileEnvironment BinaryCompileEnvironment, FileReference SingleFileToCompile, ISourceFileWorkingSet WorkingSet, IActionGraphBuilder Graph ) { List<FileItem> LinkInputFiles = base .Compile(Target, ToolChain, BinaryCompileEnvironment, SingleFileToCompile, WorkingSet, Graph); CppCompileEnvironment ModuleCompileEnvironment = CreateModuleCompileEnvironment(Target, BinaryCompileEnvironment); if (Rules.bUsePrecompiled && Target.LinkType == TargetLinkType.Monolithic) { if (!FileReference.Exists(PrecompiledManifestLocation)) { throw new BuildException("Missing precompiled manifest for '{0}'. This module was most likely not flagged for being included in a precompiled build - set 'PrecompileForTargets = PrecompileTargetsType.Any;' in {0}.build.cs to override." , Name); } PrecompiledManifest Manifest = PrecompiledManifest.Read(PrecompiledManifestLocation); foreach (FileReference OutputFile in Manifest.OutputFiles) { FileItem ObjectFile = FileItem.GetItemByFileReference(OutputFile); if (!ObjectFile.Exists) { throw new BuildException("Missing object file {0} listed in {1}" , OutputFile, PrecompiledManifestLocation); } LinkInputFiles.Add(ObjectFile); } return LinkInputFiles; } }
去插件的Intermediate
中查看了一下,确实没有这个文件,估计是拷贝造成的问题。
分隔String为数组 可以使用FString的ParserIntoArray
函数:
1 2 TArray<FString> BreakedPoints; UFlibAppHelper::GetSourceVersion().ParseIntoArray(BreakedPoints,TEXT("." ));
UE4.25中ShaderPatch问题 在4.25引擎版本中调用FShaderCodeLibrary::CreatePatchLibrary
来创建ShaderCode Patch会触发check抛异常:
这是因为FEditorShaderCodeArchive
的构造函数中调用了ShaderHashTable的Initialize,并给了默认值0x1000
:
1 2 3 4 5 6 7 8 9 10 FEditorShaderCodeArchive(FName InFormat) : FormatName(InFormat) , Format(nullptr ) { Format = GetTargetPlatformManagerRef().FindShaderFormat(InFormat); check(Format); SerializedShaders.ShaderHashTable.Initialize(0x10000 ); SerializedShaders.ShaderMapHashTable.Initialize(0x10000 ); }
导致在后续的流程中(FSerializedShaderArchive::Serialize
)调用Initialize
的时候check失败了(因为HaseSize已经有值了,并不是0,对其再调用Initialize就触发了check):
查了下FEditorShaderCodeArchive
构造函数中调用Initialize
的代码是在4.25之后的引擎版本才有的,所以影响到的之后4.25+的版本。 代码对比:
解决方案:把FSerializedShaderArchive::Serialize
中ShaderMapHashTable
的Initialize
和ShaderHashTable
的Initialize
在Editor下注释掉,因为FEditorShaderCodeArchive
的代码只在Editor下有效,并且是只在生成ShaderPatch时有用。
这就造成了以下几个问题:
FEditorShaderCodeArchive
的构造只有Eidotor并且ShaderPatch是才有用,也就意味着这里写的ShaderMapHashTable
的Initialize
和ShaderHashTable
的Initialize
只有在创建ShaderPatch时才会执行
在打基础包时执行Cook会编译shader,但是不会执行FEditorShaderCodeArchive的构造,ShaderMapHashTable
的Initialize
和ShaderHashTable
的Initialize
也就不会执行,就需要在使用的地方来调用它们的初始化
这也是UE中没有管理好这两个状态的地方:在FEditorShaderCodeArchive
和FSerializedShaderArchive::Serialize
中都做了Initialize的操作,在打基础包时造成了ShaderMapHashTable
和ShaderHashTable
的Initialize
已经被FEditorShaderCodeArchive
初始化的情况下又被FSerializedShaderArchive::Serialize
执行了一遍,导致Crash,但是我们又不能粗暴地把任何一处的初始化操作去掉,只能通过检测ShaderMapHashTable
和ShaderHashTable
的Initialize
是否已经被执行,来选择性的跳过。
阅读代码可以知道ShaderMapHashTable
和ShaderHashTable
的Initialize
只应该执行一次,并且初始化之后HashSize和IndexSize应该具有非0值:
Runtime/Core/Public/Containers/HashTable.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FORCEINLINE void FHashTable::Initialize (uint32 InHashSize, uint32 InIndexSize) { check(HashSize == 0u ); check(IndexSize == 0u ); HashSize = InHashSize; IndexSize = InIndexSize; check(HashSize <= 0x10000 ); check(FMath::IsPowerOfTwo(HashSize)); if (IndexSize) { HashMask = (uint16)(HashSize - 1 ); Hash = new uint32[HashSize]; NextIndex = new uint32[IndexSize]; FMemory::Memset(Hash, 0xff , HashSize * 4 ); } }
在Initialize
时会检测当前的HashSize
和IndexSize
是否为0,并在之后进行赋值。所以,我们只要获取FHashTable
的HashSize
和IndexSize
检测它们是否为0即可判断当前的HashTable
对象是否已经被Initialize
过,但是,UE里的FHashTable
里这两个成员都是protected
的,只能修改引擎来实现了:
添加获取FHashTable
的HashSize
和IndexSize
属性的成员函数:
1 2 3 4 5 6 7 8 class FHashTable { public : FORCEINLINE uint32 GetHashSize () const {return HashSize;}; FORCEINLINE uint32 GetIndexSize () const {return IndexSize;}; };
然后在FSerializedShaderArchive::Serialize
进行检测,如果已被初始化则跳过Initialize
逻辑:
Runtime/RenderCore/Private/ShaderCodeArchive.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 void FSerializedShaderArchive::Serialize (FArchive& Ar) { Ar << ShaderMapHashes; Ar << ShaderHashes; Ar << ShaderMapEntries; Ar << ShaderEntries; Ar << PreloadEntries; Ar << ShaderIndices; check(ShaderHashes.Num() == ShaderEntries.Num()); check(ShaderMapHashes.Num() == ShaderMapEntries.Num()); if (Ar.IsLoading()) { auto ShaderHashInitialized = [](const FHashTable& HashTable)->bool { return HashTable.GetHashSize() || HashTable.GetIndexSize(); }; { const uint32 HashSize = FMath::Min<uint32>(0x10000 , 1u << FMath::CeilLogTwo(ShaderMapHashes.Num())); if (!ShaderHashInitialized(ShaderMapHashTable)) { ShaderMapHashTable.Initialize(HashSize, ShaderMapHashes.Num()); } for (int32 Index = 0 ; Index < ShaderMapHashes.Num(); ++Index) { const uint32 Key = GetTypeHash(ShaderMapHashes[Index]); ShaderMapHashTable.Add(Key, Index); } } { const uint32 HashSize = FMath::Min<uint32>(0x10000 , 1u << FMath::CeilLogTwo(ShaderHashes.Num())); if (!ShaderHashInitialized(ShaderHashTable)) { ShaderHashTable.Initialize(HashSize, ShaderHashes.Num()); } for (int32 Index = 0 ; Index < ShaderHashes.Num(); ++Index) { const uint32 Key = GetTypeHash(ShaderHashes[Index]); ShaderHashTable.Add(Key, Index); } } } }
这样可以统一ShaderPatch和Runtime的HashTable的Initialize流程。
而且,需要注意的是:生成出来的ShaderPatch的ushaderbytecode
文件是与基础包内的文件名一致的,所以不能使用引擎启动时的默认挂载(会导致基础包内的ushaderbytecode文件无法被加载,从而crash)。
应该在挂载之后自己处理ShaderPatch的ushaderbytecode文件的加载,使用以下函数加载:
1 2 3 4 bool UFlibPatchParserHelper::LoadShaderbytecode (const FString& LibraryName, const FString& LibraryDir) { return FShaderCodeLibrary::OpenLibrary(LibraryName, LibraryDir); }
注意 :ShaderPatch的更新不直接支持Patch的迭代,如:1.0 Metadata + 1.1的ShaderPatch,并不能生成1.2的ShaderPatch,必须要基于1.1的完整Metadata才可以,即每次Patch必须要基于上一次完整的Metadate数据(Project和Global的ushaderbytecode文件),在工程管理上每次打包都需要把完整的Metadata收集起来。
Android项目中所有的UPL 可以在项目路径下Intermediate/Android/ActiveUPL.xml
,里面列出了当前项目中所有的UPL文件路径:
1 2 3 4 5 6 7 Plugins\Online\Android\OnlineSubsystemGooglePlay\Source\OnlineSubsystemGooglePlay_UPL.xml Plugins\Runtime\AndroidPermission\Source\AndroidPermission\AndroidPermission_APL.xml Plugins\Runtime\GoogleCloudMessaging\Source\GoogleCloudMessaging\GoogleCloudMessaging_UPL.xml Plugins\Runtime\GooglePAD\Source\GooglePAD\GooglePAD_APL.xml Plugins\Runtime\Oculus\OculusVR\Source\OculusHMD\OculusMobile_APL.xml Source\Runtime\Online\Voice\AndroidVoiceImpl_UPL.xml Source\ThirdParty\GoogleGameSDK\GoogleGameSDK_APL.xml
Win和Mac出IOS包的区别 UE支持以远程构建的方式来出IOS包,Mac上只编译代码,Cook和编译shader等操作在Win上执行,生成的ios包使用ushaderbytecode,而在Mac上打包IOS则会使用Matellib。
有时间来分析一下IOS在使用matallib和ushaderbytecode时效率上有没有区别。
UE4新地图的Package问题 发现UE中的一个问题:
创建一个新的地图
获取这个新地图的UPackage
,得到的是Engine/Maps/Templates/Template_Default
但是在第二次启动的时候就正常了,怀疑是新建资源的时候没有更新AssetRegistry,有时间具体分析一下原因。
C++加载BP的Enum 在蓝图中新建的枚举资源,在C++中访问:
1 2 3 4 5 6 7 8 9 10 FString UFlibAppHelper::GetEnumNameByValue (TSoftObjectPtr<UUserDefinedEnum> EnumPath, int32 value) { FString result; UUserDefinedEnum* Enumer = LoadObject<UUserDefinedEnum>(nullptr , *EnumPath.ToString()); if (Enumer) { result = Enumer->GetDisplayNameTextByValue(value).ToString(); } return result; }
效果:
DS设置超时时间 修改DefaultEngine.ini:
1 2 [/Script/OnlineSubsystemUtils.IpNetDriver] ConnectionTimeout =10.0
DDC的资料
路径过长导致远程构建失败 当引擎的路径过长时,远程构建会出现以下错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ********** BUILD COMMAND STARTED ********** Running: C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine\Engine\Binaries\DotNET\UnrealBuildTool.exe FGame IOS Development -Project=C:\BuildAgent\workspace\PackageW indows\Client\FGame.uproject C:\BuildAgent\workspace\PackageWindows\Client\FGame.uproject -NoUBTMakefiles -remoteini="C:\BuildAgent\workspace\PackageWindows\Client" -skipdeploy -Manifest=C:\BuildAgent\workspace\PackageWindows\Client\Intermediate\Build\Manifest.xml -NoHotReload -log="C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+B uildAgent+workspace+PackageWindows+InstalledEngine+Engine\BuildCookRun\UBT-FGame-IOS-Development.txt" WARNING: Running from a path with a long directory name ("C:\BuildAgent\workspace\PackageWindows\InstalledEngine\Engine" = 61 characters). Root paths shorter than 50 character s are recommended to avoid exceeding maximum path lengths on Windows. [Remote] Using remote server 'xx.xx.xx.xx' on port 2222 (user 'buildmachine') [Remote] Using private key at C:\BuildAgent\workspace\PackageWindows\Client\Build\NotForLicensees\SSHKeys\xx.xx.xx.xx\buildmachine\RemoteToolChainPrivate.key ERROR: Unable to determine home directory for remote user. SSH output: Host key verification failed. Took 0.8051806s to run UnrealBuildTool.exe, ExitCode=6 UnrealBuildTool failed. See log for more details. (C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+BuildAgent+workspace+PackageWindows+InstalledEngine+Eng ine\BuildCookRun\UBT-FGame-IOS-Development.txt) AutomationTool exiting with ExitCode=6 (6) Took 1.6488741s to run AutomationTool.exe, ExitCode=6 AutomationTool exiting with ExitCode=1 (Error_Unknown) BUILD FAILED
这里有一个Running from a path with a long directory name 的警告,然后跟着一个SSH Key的验证错误。 造成这个错误的原因就是因为警告中的内容,而非SSHKey的问题,因为Win默认的路径长度不能长于260,所以当引擎根目录位于较深的目录中时,可能会导致引擎的路径超过限制,导致后续的失败。
解决方案自然就是两个办法:
减少引擎的路径深度
修改系统的最长路径支持
Win10现在支持了长路径支持,开启即可:Win10 开启长路径支持
Mount Point的作用 在Mount Pak的时候,有一个参数可以指定MountPoint:
1 2 3 4 5 6 7 bool Mount (const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath = NULL , bool bLoadIndex = true ) ;
那么它是干什么的呢? 首先从Mount函数开始:
1 2 3 4 if (InPath != NULL ){ Pak->SetMountPoint(InPath); }
如果在调用Mount时传递了InPath
,则通过加载Pak的FPakFile实例调用SetMountPoint
,把InPath设置给它。 其实在FPakFile中,MountPath是有默认值的(从Pak文件中读取),在FPakFile的构造函数中调用了Initialize(Reader, bLoadIndex);
,Initialize中又调用了LoadIndex
,在LoadIndex
中从Pak中读取Pak的Mount Point的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void FPakFile::LoadIndex (FArchive* Reader) { if (CachedTotalSize < (Info.IndexOffset + Info.IndexSize)) { UE_LOG(LogPakFile, Fatal, TEXT("Corrupted index offset in pak file." )); } else { if (Info.Version >= FPakInfo::PakFile_Version_FrozenIndex && Info.bIndexIsFrozen) { SCOPED_BOOT_TIMING("PakFile_LoadFrozen" ); Reader->Seek(Info.IndexOffset); int32 FrozenSize = Info.IndexSize; void * DataMemory = FMemory::Malloc(FrozenSize); Reader->Serialize(DataMemory, FrozenSize); Data = TUniquePtr<FPakFileData>((FPakFileData*)DataMemory); NumEntries = Data->Files.Num(); MountPoint = Data->MountPoint; } } }
简单的可以理解为:如果Mount时不传递Mount Point就会从Pak文件中读取,如果有传入就设置为传入的值(Pak文件中的MountPoint是Pak中所有文件的公共路径)。
那么,给Pak设置MountPoint的作用是什么呢? 真实目的是,检测要加载的文件是否存在于当前Pak中!因为Pak的Mount Point的默认含义是当前Pak中所有文件的公共路径,所以只需要检测要读取的文件是否以这个路径开头,就可以首先排除掉基础路径不对的文件(基础路径都不对,意味着这个文件在Pak中也不存在)。
具体逻辑可以看这个函数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static bool FindFileInPakFiles (TArray<FPakListEntry>& Paks,const TCHAR* Filename,FPakFile** OutPakFile,FPakEntry* OutEntry = nullptr ) { FString StandardFilename (Filename) ; FPaths::MakeStandardFilename(StandardFilename); int32 DeletedReadOrder = -1 ; for (int32 PakIndex = 0 ; PakIndex < Paks.Num(); PakIndex++) { int32 PakReadOrder = Paks[PakIndex].ReadOrder; if (DeletedReadOrder != -1 && DeletedReadOrder > PakReadOrder) { UE_LOG( LogPakFile, Verbose, TEXT("Delete Record: Accepted a delete record for %s" ), Filename ); return false ; } FPakFile::EFindResult FindResult = Paks[PakIndex].PakFile->Find(*StandardFilename, OutEntry); if (FindResult == FPakFile::EFindResult::Found ) { if (OutPakFile != NULL ) { *OutPakFile = Paks[PakIndex].PakFile; } UE_CLOG( DeletedReadOrder != -1 , LogPakFile, Verbose, TEXT("Delete Record: Ignored delete record for %s - found it in %s instead (asset was moved between chunks)" ), Filename, *Paks[PakIndex].PakFile->GetFilename() ); return true ; } else if (FindResult == FPakFile::EFindResult::FoundDeleted ) { DeletedReadOrder = PakReadOrder; UE_LOG( LogPakFile, Verbose, TEXT("Delete Record: Found a delete record for %s in %s" ), Filename, *Paks[PakIndex].PakFile->GetFilename() ); } } UE_CLOG( DeletedReadOrder != -1 , LogPakFile, Warning, TEXT("Delete Record: No lower priority pak files looking for %s. (maybe not downloaded?)" ), Filename ); return false ; }
当我们从Pak中读取文件时,通过对游戏中所有Mount的Pak调用Find
函数,而FPakFile::Find
的函数就实现了上述我说的逻辑:
1 2 3 4 5 6 7 8 9 10 11 FPakFile::EFindResult FPakFile::Find (const FString& Filename, FPakEntry* OutEntry) const { QUICK_SCOPE_CYCLE_COUNTER(PakFileFind); if (Filename.StartsWith(MountPoint)) { FString Path (FPaths::GetPath(Filename)) ; } }
所以,MountPoint的作用就是在从Pak中查找文件时,首先判断文件的路径是否与Pak中所有文件的基础路径 相匹配(StartWith),如果不存在也就不会进入后续的流程了。
引擎的Splash过程
通过在这里断点可以比较直观地分析,引擎初始化到什么阶段做了什么事情。
UE4.25.1的Assembly路径错误 在UE4.25.1中,引擎生成的Engine/Intermediate/Build/BuildRules/UE4Rules.dll
等文件具有路径错误。
具体的原因是在UnrealBuildTool/System/RulesCompiler.cs
的CreateEngineOrEnterpriseRulesAssembly
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private static RulesAssembly CreateEngineOrEnterpriseRulesAssembly (RulesScope Scope, List<DirectoryReference> RootDirectories, string AssemblyPrefix, IReadOnlyList<PluginInfo> Plugins, bool bReadOnly, bool bSkipCompile, RulesAssembly Parent ) { DirectoryReference AssemblyDir = RootDirectories[0 ]; if (UnrealBuildTool.IsFileInstalled(FileReference.Combine(AssemblyDir, AssemblyPrefix))) { DirectoryReference UserDir = Utils.GetUserSettingDirectory(); if (UserDir != null ) { ReadOnlyBuildVersion Version = ReadOnlyBuildVersion.Current; AssemblyDir = DirectoryReference.Combine(UserDir, "UnrealEngine" , String.Format("{0}.{1}" , Version.MajorVersion, Version.MinorVersion)); } } FileReference EngineAssemblyFileName = FileReference.Combine(AssemblyDir, "Intermediate" , "Build" , "BuildRules" , AssemblyPrefix + "Rules" + FrameworkAssemblyExtension); RulesAssembly EngineAssembly = new RulesAssembly(Scope, RootDirectories, Plugins, ModuleFileToContext, new List<FileReference>(), EngineAssemblyFileName, bContainsEngineModules: true , DefaultBuildSettings: BuildSettingsVersion.Latest, bReadOnly: bReadOnly, bSkipCompile: bSkipCompile, Parent: Parent); }
上面的代码中AssemblyDir
是引擎目录,AssemblyPrefix
是UE4
,拼接起来能够通过UnrealBuildTool.IsFileInstalled
的检测。 但是,在if的代码块中,获取了用户目录,在IOS中就是:
1 2 3 4 C:\Users\lipengzha\AppData\Local\UnrealEngine\4.25 /Users/buildmachine/Library/Application Support/Epic
拼接起来就是上面这两个USER_DIR
的UnrealEngine/4.25/
,在下面读取Assembly
的流程中就会使用这个路径。
在使用Win的时候,其实没有问题,因为就算把UE4Rules.dll
写入到用户目录下,在Win上同样是可以访问到的。但是 ,在使用Win远程构建IOS的时候就会出现问题。 在远程构建时,会使用Rsync把引擎的文件同步到Mac上再执行编译,其中就包括Engine/Intermediate/Build/BuildRuls/
下的所有文件,因为4.25.1中的代码会把Build/BuildRuls/UE4Rules.dll
等生成到Win的用户目录下,所以远程构建,RSync就不能正确地把BuildRuls
下的文件上传到Mac上,故而引起打包错误:
1 2 ERROR: Precompiled rules assembly '/Users/buildmachine/Library/Application Support/Epic/UnrealEngine/4.25/Intermediate/Build/BuildRules/UE4Rules.dl l' does not exist.
可以看到,在Mac上也是从Mac的用户目录查找的,因为压根Mac上就没有这俩文件,所以就会产生这个错误。 解决这个问题的办法,就是修改UnrealBuildTool/System/RulesCompiler.cs
的CreateEngineOrEnterpriseRulesAssembly
函数,把BuildRules
相关的文件写入到Engine/Intermediate/Build/BuildRules
中,在UE4.25.2中已经修复了这个错误。 在4.25.2 Hotfix released 中列出了Fixed! UE-94140 Fix assembly location for remote toolchain ,其实就是直接修改了CreateEngineOrEnterpriseRulesAssembly
函数:
1 2 3 4 5 6 7 8 9 private static RulesAssembly CreateEngineOrEnterpriseRulesAssembly (RulesScope Scope, List<DirectoryReference> RootDirectories, string AssemblyPrefix, IReadOnlyList<PluginInfo> Plugins, bool bReadOnly, bool bSkipCompile, RulesAssembly Parent ) { FileReference EngineAssemblyFileName = FileReference.Combine(RootDirectories[0 ], "Intermediate" , "Build" , "BuildRules" , AssemblyPrefix + "Rules" + FrameworkAssemblyExtension); RulesAssembly EngineAssembly = new RulesAssembly(Scope, RootDirectories[0 ], Plugins, ModuleFileToContext, new List<FileReference>(), EngineAssemblyFileName, bContainsEngineModules: true , DefaultBuildSettings: BuildSettingsVersion.Latest, bReadOnly: bReadOnly, bSkipCompile: bSkipCompile, Parent: Parent); }
可以直接在github中查看:UnrealBuildTool/System/RulesCompiler.cs#L442
跨Level选择Actor 在场景中,美术和游戏逻辑是区分开的,所以有时候需要程序关卡去操控美术关卡的对象,但是UE的不同关卡其实是不同的资源,属于不同的Pacakge,是不能直接跨关卡来选择对象实例的。 选择时会有以下错误:
1 LogProperty: Warning: Illegal TEXT reference to a private object in external package (StaticMeshActor /Game/Test/Map/Level_Sub2.Level_Sub2:PersistentLevel.Cube_2) from referencer (BP_AActor_C /Game/Test/Map/Level_Sub1.Level_Sub1:PersistentLevel.BP_AActor_2). Import failed...
这是因为在PropertyBaseObject.cpp
的FObjectPropertyBase::ImportText_Internal
中对Object属性是否可以跨关卡做了检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 UObject* FObjectPropertyBase::FindImportedObject ( const FProperty* Property, UObject* OwnerObject, UClass* ObjectClass, UClass* RequiredMetaClass, const TCHAR* Text, uint32 PortFlags, FUObjectSerializeContext* InSerializeContext , bool bAllowAnyPackage ) { if (Result && !Result->HasAnyFlags(RF_Public) && OwnerObject && Result->GetOutermost() != OwnerObject->GetOutermost()) { const FObjectPropertyBase* ObjectProperty = CastField<const FObjectPropertyBase>(Property); if ( !ObjectProperty || !ObjectProperty->AllowCrossLevel()) { UE_LOG(LogProperty, Warning, TEXT("Illegal TEXT reference to a private object in external package (%s) from referencer (%s). Import failed..." ), *Result->GetFullName(), *OwnerObject->GetFullName()); Result = nullptr ; } } }
其中AllowCrossLevel
有两个继承类有覆写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool FObjectPropertyBase::AllowCrossLevel () const { return false ; } bool FLazyObjectProperty::AllowCrossLevel () const { return true ; } bool FSoftObjectProperty::AllowCrossLevel () const { return true ; }
所以,不能够直接通过创建FObjectPropertyBase
这种硬引用方式的属性从SubLevel1选择SubLevel2中的Actor。 那么如何解决这么问题呢?,上面已经列出了两个可以跨平台选择的属性,分别是FLazyObjectProperty
和FSoftObjectProperty
,那么以FSoftObjectProperty
为例,可以通过TSoftObjectPtr
来实现:
1 TSoftObjectPtr<AActor> Actor;
TSoftObjectPtr
获取到的其实是SubLevel2中的资源的路径:
1 /Game/Test/Map/Level_Sub2.Level_Sub2:PersistentLevel.Cube_2
在运行时访问需要使用以下操作来获取:
上面蓝图中节点Load Asset Blocking
是UKismetSystemLibrary
中的函数:
1 2 3 4 5 UObject* UKismetSystemLibrary::LoadAsset_Blocking (TSoftObjectPtr<UObject> Asset) { return Asset.LoadSynchronous(); }
看来UE加载资源时,并没有区分真正的物理资源和场景中的实例,统一使用资源的路径来加载,这一点做的非常爽,可以把另一个关卡中的Actor当作资源来读取,并且获取的还就是运行时的那个实例,非常Nice。
添加外部库的注意事项 在添加外部的代码库时,需要关注以下几个问题:
纯代码的库,要测试是否具有平台相关的写法,需要同时支持Win/Android/IOS/Mac四个平台
对于Android的so要同时支持arm64/armv7,打包时so文件的拷贝需要使用UPL执行
ios的.a
要同时具有bitcode
和非bitcode
版本,不然在shipping时如果开启了bitcode,链接不支持bitcode的库会有链接错误的问题。
检测当前构建是否支持bitcode的流程:
1 2 3 4 5 6 7 8 if (Target.IOSPlatform.bShipForBitcode){ } { }
在Target.cs
中可以直接通过IOSPlatform
获取当前构建是否支持bitcode,在其他的Module中,可以通过target.cs
中的Target.IOSPlatform
获取。
Outer
ShareMaterialShaderCode 在打包时可以在Project Settings
-Packaging
中设置Share Material Shader Code
和Shadred Material Native Libraries
来减少包体的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 UPROPERTY(config, EditAnywhere, Category=Packaging) bool bShareMaterialShaderCode;UPROPERTY(config, EditAnywhere, Category=Packaging, meta = (EditCondition = "bShareMaterialShaderCode" , ConfigRestartRequired = true )) bool bSharedMaterialNativeLibraries;
开启了之后打出的包中会生成下列文件:
1 2 ShaderArchive-Blank425-PCD3D_SM5.ushaderbytecode ShaderCode-Global-PCD3D_SM5.ushaderbytecode
但是,如果开启之后如果后续的Cook资源Shader发生了变动,而基础包内还是旧的ShaderBytecode信息,会导致材质丢失。
有两个办法:
后续的打包时可以把Shaderbytecode文件打包在pak中,挂载时加载;
Cook热更资源时把Shaderbytecode打包在资源内;
获取ContentBrowser中选择的资源 1 2 3 4 5 6 7 8 #include "IContentBrowserSingleton.h" TArray<FAssetData> FHotPatcherEditorModule::GetSelectedAssetsInBrowserContent () { FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser" )); TArray<FAssetData> AssetsData; ContentBrowserModule.Get().GetSelectedAssets(AssetsData); return AssetsData; }
命令行太长无法适应调试记录 1 无法执行“C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\VC\Tools\MSVC\14.16.27023\bin\HostX64\x64\c1xx.dll”: 命令行太长,无法适应调试记录
在报错的模块的build.cs中添加下列属性即可:
1 bLegacyPublicIncludePaths = false ;
添加资源右键菜单按钮 在ContentBrowser选择资源时的右键菜单按钮。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void FHotPatcherEditorModule::AddAssetContentMenu () { if (!UToolMenus::IsToolMenuUIEnabled()) { return ; } FToolMenuOwnerScoped MenuOwner ("CookUtilities" ) ; UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("ContentBrowser.AssetContextMenu" ); FToolMenuSection& Section = Menu->AddSection("AssetContextCookUtilities" , LOCTEXT("CookUtilitiesMenuHeading" , "CookUtilities" ));; Section.AddDynamicEntry("SoundWaveAsset" , FNewToolMenuSectionDelegate::CreateLambda([this ](FToolMenuSection& InSection) { const TAttribute<FText> Label = LOCTEXT("CookUtilities_CookAsset" , "Cook Assets" ); const TAttribute<FText> ToolTip = LOCTEXT("CookUtilities_CookAssetsTooltip" , "Cook Assets" ); const FSlateIcon Icon = FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.SoundSimple" ); const FToolMenuExecuteAction UIAction = FToolMenuExecuteAction::CreateRaw(this ,&FHotPatcherEditorModule::OnCookAssets); InSection.AddMenuEntry("CookUtilities_CookAssets" , Label, ToolTip, Icon, UIAction); })); }
可以在绑定的函数中对资源进行操作。
GenerateProjectFiles指定VS版本 GenerateProjectFiles.bat
最终也是调用到UnrealBuildTool.exe
,可以通过-2015
/-2017
来指定VS2015和VS2017引擎版本。
有一些Module用到了平台相关的内容,在另一个平台会编译不过,所以需要在uplugin
中给Module添加模块白名单,只有在其中的平台上才会进行编译。
1 2 3 4 5 6 7 8 9 10 11 "Modules": [ { "Name" : "OculusHMD" , "Type" : "Runtime" , "LoadingPhase" : "PostConfigInit" , "WhitelistPlatforms" : [ "Win64" , "Win32" , "Android" ] },
也可以设置平台的黑名单,使用BlacklistPlatforms
。
IDetailCustomization 使用IDetailCustomization
的方式可以给UE的属性面板添加特殊的东西,比如按钮。
以给F的结构的Detail添加按钮的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #pragma once #include "IDetailCustomization.h" class FReleaseSettingsDetails : public IDetailCustomization{ public : static TSharedRef<IDetailCustomization> MakeInstance () ; virtual void CustomizeDetails (IDetailLayoutBuilder& DetailBuilder) override ; }; ```` .cpp: ```cpp #include "CreatePatch/ReleaseSettingsDetails.h" #include "CreatePatch/FExportReleaseSettings.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "DetailWidgetRow.h" #include "Widgets/Input/SButton.h" #define LOCTEXT_NAMESPACE "ReleaseSettingsDetails" TSharedRef<IDetailCustomization> FReleaseSettingsDetails::MakeInstance () { return MakeShareable(new FReleaseSettingsDetails()); } void FReleaseSettingsDetails::CustomizeDetails (IDetailLayoutBuilder& DetailBuilder) { TArray< TSharedPtr<FStructOnScope> > StructBeingCustomized; DetailBuilder.GetStructsBeingCustomized(StructBeingCustomized); check(StructBeingCustomized.Num() == 1 ); FExportReleaseSettings* ReleaseSettingsIns = (FExportReleaseSettings*)StructBeingCustomized[0 ].Get()->GetStructMemory(); IDetailCategoryBuilder& VersionCategory = DetailBuilder.EditCategory("Version" ,FText::GetEmpty(),ECategoryPriority::Default); VersionCategory.SetShowAdvanced(true ); VersionCategory.AddCustomRow(LOCTEXT("ImportPakLists" , "Import Pak Lists" ),true ) .ValueContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(0 ) .AutoWidth() [ SNew(SButton) .Text(LOCTEXT("Import" , "Import" )) .ToolTipText(LOCTEXT("ImportPakLists_Tooltip" , "Import Pak Lists" )) .IsEnabled_Lambda([this ,ReleaseSettingsIns]()->bool { return ReleaseSettingsIns->IsByPakList(); }) .OnClicked_Lambda([this , ReleaseSettingsIns]() { if (ReleaseSettingsIns) { ReleaseSettingsIns->ImportPakLists(); } return (FReply::Handled()); }) ] + SHorizontalBox::Slot() .Padding(5 ,0 ,0 ,0 ) .AutoWidth() [ SNew(SButton) .Text(LOCTEXT("Clear" , "Clear" )) .ToolTipText(LOCTEXT("ClearPakLists_Tooltip" , "Clear Pak Lists" )) .IsEnabled_Lambda([this ,ReleaseSettingsIns]()->bool { return ReleaseSettingsIns->IsByPakList(); }) .OnClicked_Lambda([this , ReleaseSettingsIns]() { if (ReleaseSettingsIns) { ReleaseSettingsIns->ClearImportedPakList(); } return (FReply::Handled()); }) ] ]; } #undef LOCTEXT_NAMESPACE
这里我们只是定义了一个IDetailCustomization
的类,其中的CustomizeDetails
是对FExportReleaseSettings
添加的细节。
该类在创建DetailView
时使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void SHotPatcherExportRelease::CreateExportFilterListView () { FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor" ); FDetailsViewArgs DetailsViewArgs; { DetailsViewArgs.bAllowSearch = true ; DetailsViewArgs.bHideSelectionTip = true ; DetailsViewArgs.bLockable = false ; DetailsViewArgs.bSearchInitialKeyFocus = true ; DetailsViewArgs.bUpdatesFromSelection = false ; DetailsViewArgs.NotifyHook = nullptr ; DetailsViewArgs.bShowOptions = true ; DetailsViewArgs.bShowModifiedPropertiesOption = false ; DetailsViewArgs.bShowScrollBar = false ; DetailsViewArgs.bShowOptions = true ; DetailsViewArgs.bUpdatesFromSelection= true ; } FStructureDetailsViewArgs StructureViewArgs; { StructureViewArgs.bShowObjects = true ; StructureViewArgs.bShowAssets = true ; StructureViewArgs.bShowClasses = true ; StructureViewArgs.bShowInterfaces = true ; } SettingsView = EditModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, nullptr ); FStructOnScope* Struct = new FStructOnScope(FExportReleaseSettings::StaticStruct(), (uint8*)ExportReleaseSettings.Get()); SettingsView->GetOnFinishedChangingPropertiesDelegate().AddRaw(ExportReleaseSettings.Get(),&FExportReleaseSettings::OnFinishedChangingProperties); SettingsView->GetDetailsView()->RegisterInstancedCustomPropertyLayout(FExportReleaseSettings::StaticStruct(),FOnGetDetailCustomizationInstance::CreateStatic(&FReleaseSettingsDetails::MakeInstance)); SettingsView->SetStructureData(MakeShareable(Struct)); }
使用RegisterInstancedCustomPropertyLayout
把所写的FReleaseSettingsDetails
实例注册到DetailView
中。注意调用时机要在SetStructureData
之前。 然后就可以看到添加的两个按钮了:
提取PakList文件 打包时生成的PakList*.txt文件存放位置为:
1 Engine\Programs\AutomationTool\Saved\Logs
PakList的命名规则为PakList_PROJECTNAME_PLATFORM.txt
,如:
1 2 3 PakList_Blank425-WindowsNoEditor.txt PakList_Blank425-Android_ASTC.txt PakList_blank425-ios.txt
但是UE在打包下一次时会把上一次生成的PakList*.txt
文件给清理掉。所以如果要提取某个平台的PakList需要在打包完当前平台之后立即提取,不然打包下个平台就把之前的删掉了。
绑定DetailsView的属性变化事件 在编辑器中创建DetailsView
时,如果使用继承自UObject的对象,可以重载PostEditChangeProperty
来实现属性变化的监听,但是如果使用F
的结构,则不能直接在类的函数中监听,需要通过绑定IStructureDetailsView
的属性变动代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 TSharedPtr<IStructureDetailsView> SettingsView; FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor" ); FDetailsViewArgs DetailsViewArgs; { DetailsViewArgs.bAllowSearch = true ; DetailsViewArgs.bHideSelectionTip = true ; DetailsViewArgs.bLockable = false ; DetailsViewArgs.bSearchInitialKeyFocus = true ; DetailsViewArgs.bUpdatesFromSelection = false ; DetailsViewArgs.NotifyHook = nullptr ; DetailsViewArgs.bShowOptions = true ; DetailsViewArgs.bShowModifiedPropertiesOption = false ; DetailsViewArgs.bShowScrollBar = false ; DetailsViewArgs.bShowOptions = true ; DetailsViewArgs.bUpdatesFromSelection= true ; } FStructureDetailsViewArgs StructureViewArgs; { StructureViewArgs.bShowObjects = true ; StructureViewArgs.bShowAssets = true ; StructureViewArgs.bShowClasses = true ; StructureViewArgs.bShowInterfaces = true ; } SettingsView = EditModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, nullptr ); FStructOnScope* Struct = new FStructOnScope(FExportReleaseSettings::StaticStruct(), (uint8*)ExportReleaseSettings.Get()); SettingsView->GetOnFinishedChangingPropertiesDelegate().AddRaw(ExportReleaseSettings.Get(),&FExportReleaseSettings::OnFinishedChangingProperties); SettingsView->SetStructureData(MakeShareable(Struct));
关键是这行代码:
1 SettingsView->GetOnFinishedChangingPropertiesDelegate().AddRaw(ExportReleaseSettings.Get(),&FExportReleaseSettings::OnFinishedChangingProperties);
把当前DetailsView的属性变动事件绑定到OnFinishedChangingProperties
上。
从指定Pak加载文件 具体可以看FPakPlatformFile::OpenRead
的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 IFileHandle* FPakPlatformFile::OpenRead (const TCHAR* Filename, bool bAllowWrite) { IFileHandle* Result = NULL ; FPakFile* PakFile = NULL ; FPakEntry FileEntry; if (FindFileInPakFiles(Filename, &PakFile, &FileEntry)) { #if PAK_TRACKER TrackPak(Filename, &FileEntry); #endif Result = CreatePakFileHandle(Filename, PakFile, &FileEntry); if (Result) { FCoreDelegates::OnFileOpenedForReadFromPakFile.Broadcast(*PakFile->GetFilename(), Filename); } } else { if (IsNonPakFilenameAllowed(Filename)) { Result = LowerLevel->OpenRead(Filename, bAllowWrite); } } return Result; }
先通过文件名拿到传入文件在所有Mounted的pak中最大Order的Pak文件,然后使用CreatePakFileHandle
从Pak文件中读取文件。FPakFile
描述的是Pak文件,FPakEntry
描述的是Pak中文件的信息,比如大小、偏移等。通过FPakFile
和FPakEntry
可以从指定的Pak中读取指定的文件。
WITH_EDITOR包裹反射属性的问题 有时只想要一些属性在编辑器下存在,打包时不需要,按照常规的思路,需要对这些属性使用WITH_EDITOR
包裹:
1 2 3 4 #if WITH_EDITOR UPROPERTY() int32 ival; #endif
这个代码在Editor的Configuration下没有问题,但是一旦编译非Editor就会产生如下错误:
1 ERROR: Build/Win64/FGame/Inc/FGame/NetActor.gen.cpp(97): error C2039: 'ival': is not a member of 'ANetActor'
那么,既然我们明明已经用WITH_EDITOR
包裹了ival
的属性,为什么在编译非Editor的时候UHT还会为这个属性生成反射代码呢? 这个问题涉及到了以下几个概念:
gen.cpp中是UHT为反射标记的类和属性生成的反射信息
UHT的生成流程在调用编译器之前
UE构建系统的流程我之前做过分析:Build flow of the Unreal Engine4 project
因为C++的宏是在调用编译器后预处理阶段做的事情,在执行UHT时,压根不会检测宏条件,所以上面的代码,UHT依然会为ival
生成反射信息到gen.cpp
中,而UHT执行完毕之后进入编译阶段WITH_EDITOR
会参与预处理,ival
因此在类定义中不存在,但是UHT已经为它生成了反射代码,会通过获取成员函数指针的方式访问到它,进而产生了上述的编译错误。
所以这是UE反射代码生成先于预处理造成的问题,在写代码时是比较反直觉的。但是这个问题也并非不能解决,UE提供了WITH_EDITORONLY_DATA
宏来专门处理这个问题,一个宏解决不了,就引入一个新的。
但是为什么WITH_EDITOR
不可以,而WITH_EDITORONLY_DATA
就可以呢?因为UHT在生成反射代码时为WITH_EDITORONLY_DATA
做了特殊检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 void FNativeClassHeaderGenerator::ExportProperties (FOutputDevice& Out, UStruct* Struct, int32 TextIndent) { FProperty* Previous = NULL ; FProperty* PreviousNonEditorOnly = NULL ; FProperty* LastInSuper = NULL ; UStruct* InheritanceSuper = Struct->GetInheritanceSuper(); UStruct* CurrentSuper = InheritanceSuper; while (LastInSuper == NULL && CurrentSuper) { for ( TFieldIterator<FProperty> It(CurrentSuper,EFieldIteratorFlags::ExcludeSuper); It; ++It ) { FProperty* Current = *It; if ( It.GetStruct() == CurrentSuper && Current->ElementSize ) { LastInSuper = Current; } } CurrentSuper = CurrentSuper->GetSuperStruct(); } FMacroBlockEmitter WithEditorOnlyData (Out, TEXT("WITH_EDITORONLY_DATA" )) ; for ( TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::ExcludeSuper); It; ++It ) { FProperty* Current = *It; if (It.GetStruct() == Struct) { WithEditorOnlyData(Current->IsEditorOnlyProperty()); { FUHTStringBuilder JustPropertyDecl; const FString* Dim = GArrayDimensions.Find(Current); Current->ExportCppDeclaration( JustPropertyDecl, EExportedDeclaration::Member, Dim ? **Dim : NULL ); ApplyAlternatePropertyExportText(*It, JustPropertyDecl, EExportingState::TypeEraseDelegates); Out.Logf(TEXT("%s%s;\r\n" ), FCString::Tab(TextIndent + 1 ), *JustPropertyDecl); } LastInSuper = NULL ; Previous = Current; if (!Current->IsEditorOnlyProperty()) { PreviousNonEditorOnly = Current; } } } }
看下FMacroBlockEmitter
的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 struct FMacroBlockEmitter { explicit FMacroBlockEmitter (FOutputDevice& InOutput, const TCHAR* InMacro) : Output (InOutput) , bEmittedIf(false) , Macro(InMacro) { } ~FMacroBlockEmitter() { if (bEmittedIf) { Output.Logf(TEXT("#endif // %s\r\n" ), Macro); } } void operator () (bool bInBlock) { if (!bEmittedIf && bInBlock) { Output.Logf(TEXT("#if %s\r\n" ), Macro); bEmittedIf = true ; } else if (bEmittedIf && !bInBlock) { Output.Logf(TEXT("#endif // %s\r\n" ), Macro); bEmittedIf = false ; } } FMacroBlockEmitter(const FMacroBlockEmitter&) = delete ; FMacroBlockEmitter& operator =(const FMacroBlockEmitter&) = delete ; private : FOutputDevice& Output; bool bEmittedIf; const TCHAR* Macro; };
当生成代码时会为使用WITH_EDITORONLY_DATA
包裹的属性在gen.cpp
中添加WITH_EDITORONLY_DATA
宏(有点套娃的感觉),使gen.cpp
在非EDITOR下编译时也不会把这部分反射代码参与真正的编译,从而解决了上面的问题。
Android获取已安装App的Apk路径 有个需求,需要在运行时获取到,App的Apk路径,查了一下UE里没有现成的接口,只能用JNI调用从Java那边想办法了。 通过在Android Developer上查找,发现ApplicationInfo 中具有sourceDir
属性,记录着APK的路径。 而可以通过PackageManager
调用getApplicationInfo 可以获取指定包名的ApplicationInfo。
那么就好说了,UPL里java代码走起:
1 2 3 4 5 6 7 8 9 10 11 12 public String AndroidThunkJava_GetInstalledApkPath () { Context context = getApplicationContext(); PackageManager packageManager = context.getPackageManager(); ApplicationInfo appInfo; try { appInfo = packageManager.getApplicationInfo(context.getPackageName(),PackageManager.GET_META_DATA); return appInfo.sourceDir; }catch (PackageManager.NameNotFoundException e){ return "invalid" ; } }
然后在UE里使用JNI调用:
1 2 3 4 5 if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()){ jmethodID GetInstalledPakPathMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetInstalledApkPath" , "()Ljava/lang/String;" , false ); FString ResultApkPath = FJavaHelperEx::FStringFromLocalRef(Env, (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis,GetInstalledPakPathMethodID)); }
其中FJavaHelperEx::FStringFromLocalRef
是我封装的从jstring到FString的转换函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 namespace FJavaHelperEx{ FString FStringFromParam (JNIEnv* Env, jstring JavaString) { if (!Env || !JavaString || Env->IsSameObject(JavaString, NULL )) { return {}; } const auto chars = Env->GetStringUTFChars(JavaString, 0 ); FString ReturnString (UTF8_TO_TCHAR(chars)) ; Env->ReleaseStringUTFChars(JavaString, chars); return ReturnString; } FString FStringFromLocalRef (JNIEnv* Env, jstring JavaString) { FString ReturnString = FStringFromParam(Env, JavaString); if (Env && JavaString) { Env->DeleteLocalRef(JavaString); } return ReturnString; } }
获取的结果:
Cook资源 可以看CookOnTheFlyServer.cpp中的代码:
Editor/UnrealEd/Private/CookOnTheFlyServer.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 uint32 UCookOnTheFlyServer::FullLoadAndSave (uint32& CookedPackageCount) { if (bCookPackage) { FString PlatFilename = Filename.Replace(TEXT("[Platform]" ), *Target->PlatformName()); UE_CLOG(GCookProgressDisplay & (int32)ECookProgressDisplayMode::PackageNames, LogCook, Display, TEXT("Cooking %s -> %s" ), *Package->GetName(), *PlatFilename); bool bSwap = (!Target->IsLittleEndian()) ^ (!PLATFORM_LITTLE_ENDIAN); if (!Target->HasEditorOnlyData()) { Package->SetPackageFlags(PKG_FilterEditorOnly); } else { Package->ClearPackageFlags(PKG_FilterEditorOnly); } GIsCookerLoadingPackage = true ; FSavePackageResultStruct SaveResult = GEditor->Save(Package, World, FlagsToCook, *PlatFilename, GError, NULL , bSwap, false , SaveFlags, Target, FDateTime::MinValue(), false ); GIsCookerLoadingPackage = false ; if (SaveResult == ESavePackageResult::Success && UAssetManager::IsValid()) { if (!UAssetManager::Get().VerifyCanCookPackage(Package->GetFName())) { SaveResult = ESavePackageResult::Error; } } const bool bSucceededSavePackage = (SaveResult == ESavePackageResult::Success || SaveResult == ESavePackageResult::GenerateStub || SaveResult == ESavePackageResult::ReplaceCompletely); if (bSucceededSavePackage) { FAssetRegistryGenerator* Generator = PlatformManager->GetPlatformData(Target)->RegistryGenerator.Get(); UpdateAssetRegistryPackageData(Generator, Package->GetFName(), SaveResult); FPlatformAtomics::InterlockedIncrement(&ParallelSavedPackages); } if (SaveResult != ESavePackageResult::ReferencedOnlyByEditorOnlyData) { SavePackageSuccessPerPlatform[PlatformIndex] = true ; } else { SavePackageSuccessPerPlatform[PlatformIndex] = false ; } } }
Compile对Instanced的替换调用栈 在这个函数中会收集到当前资源被修改后,依赖它的资源,存储在Dependencies
数组中。
得到Dependencies
之后,会在FBlueprintCompileReinstancer::UpdateBytecodeReferences
中使用:
对资源点击Compile的调用栈 会执行到FKismetEditorUtilities::CompileBlueprint
:
Editor/UnrealEd/Private/Kismet2/Kismet2.cpp 1 2 3 4 5 6 void FKismetEditorUtilities::CompileBlueprint (UBlueprint* BlueprintObj, EBlueprintCompileOptions CompileFlags, FCompilerResultsLog* pResults) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC() FBlueprintCompilationManager::CompileSynchronously(FBPCompileRequest(BlueprintObj, CompileFlags, pResults)); }
UMG的子控件引用热更问题 UMG的UserWidget如果UI_A添加了另一个UserWidget UI_B,它们并不是先创建了UI_A,再去创建加载UI_B的资源并创建,UMG的子控件是以Instanced的方式创建的,相当于UI_A中存储的只是当时UI_B的一份示例,并不涉及资源的直接引用。这样会导致在热更时,如果我们只修改了UI_B,此时并没有造成UI_A资源变动,Cook和打包时如果只把UI_B打包,其实对UI_A是没有效果的,并不会有相应的变动。 这是UE Asset和Instanced没有区分的问题,资源并没有修改,但是实际上却对它造成了变化。解决的办法只能在修改了子控件后把使用Instanced方式引用的父控件一起Cook打包,才能有正确的效果。
注意环形引用导致的宏未定义错误 在UE中发现编译报错宏的未定义错误,但是发现头文件已经被包含了,照常是不会出现问题的,如果出现这个问题,检查下代码中是否有头文件的环形引用,解决之后即可。 猜测的原因是环形引用导致预处理爆栈没有包含到真正的宏定义头文件,从而产生了编译错误。
监听Slate的输入事件 UE提供了注册Listener的方法,通过FSlateApplication::Get()
进行注册:
1 2 InputProcessor = MakeShared<FTranslationPickerInputProcessor>(this ); FSlateApplication::Get().RegisterInputPreProcessor(InputProcessor, 0 );
RegisterInputPreProcessor
接收的第一个参数是TSharedPtr<class IInputProcessor>
,它是一个接口类型,第二个参数是插入Listener的位置,默认是插入到尾部。
IInputProcessor
提供的接口:
Runtime/Slate/Public/Framework/Application/IInputProcessor.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class SLATE_API IInputProcessor { public : IInputProcessor(){}; virtual ~IInputProcessor(){} virtual void Tick (const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) = 0 ; virtual bool HandleKeyDownEvent (FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) { return false ; } virtual bool HandleKeyUpEvent (FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) { return false ; } virtual bool HandleAnalogInputEvent (FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) { return false ; } virtual bool HandleMouseMoveEvent (FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false ; } virtual bool HandleMouseButtonDownEvent ( FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false ; } virtual bool HandleMouseButtonUpEvent ( FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false ; } virtual bool HandleMouseButtonDoubleClickEvent (FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false ; } virtual bool HandleMouseWheelOrGestureEvent (FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) { return false ; } virtual bool HandleMotionDetectedEvent (FSlateApplication& SlateApp, const FMotionEvent& MotionEvent) { return false ; }; };
我们可以通过继承它来实现自己的监听需求。
修改LaunchScreen视频比例 当使用Project Settings
-Project
-Movies
中来为游戏启动时播放视频时,默认情况下是锁定视频的长宽比的,在全面屏流行的现在,长宽比为2.x的比比皆是,锁定视频比例会导致两侧有黑边,所以希望视频能够拉伸来适应屏幕的大小。 需要修改引擎的代码:
Runtime/MoviePlayer/Private/DefaultGameMoviePlayer.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 FVector2D FDefaultGameMoviePlayer::GetMovieSize () const { const FVector2D ScreenSize = MainWindow.Pin()->GetClientSizeInScreen(); return ScreenSize; }
把FDefaultGameMoviePlayer::GetMovieSize()
修改为上面的代码,其实就是把从视频获取长宽的代码去掉,强制使用窗口的大小。
Rider传递命令行参数 在VS中有UnrealVS
可以方便地给工程传递命令行参数,但是Rider中要复杂一点。 要选择Run
-Edit Configurations
,在弹出窗口的左侧选择要修改的工程,右侧的Program Arguments
则是传递给程序的参数:
为了方便编辑,可以把Edit Configurations
添加到Toolbar中,在Toolbar上点击右键,点击Customize Menus and Toolbars
,在弹出的Menus and Toolbars
窗口中,把Edit Configurations
添加至Toolbar Run Actions
中即可。
IOS自动化导入Certificate和Provision 传统的打包ios时,需要手动在Project Settings
-Platforms
-IOS
中选择打包要使用的Certificate
和Provision
,当需要切换打包Configuration的时候就需要打开编辑器重新选择一遍(因为Development和Shipping用到的证书不同),很麻烦,我是一个非常讨厌做重复操作的人,所以研究了一下解决了这个问题。 也是得益于UE本身提供了代码中导入Certificate
和Provision
的方式(之前我还写了自动化把证书导入到系统中,其实不用了)。 在前面的笔记中提到过UE的Target也提供了平台相关的Target对象:平台相关Target ,要实现本文提到的需求就要通过控制IOSPlatform
来实现。
IOSPlatform
提供了以下几个属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [CommandLine("-ImportProvision=" ) ] public string ImportProvision = null ;[CommandLine("-ImportCertificate=" ) ] public string ImportCertificate = null ;[CommandLine("-ImportCertificatePassword=" ) ] public string ImportCertificatePassword = null ;
通过操作它们的值来实现自动化导入证书和Provision,我写了一段使用代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class FGameTarget : TargetRules { public FGameTarget ( TargetInfo Target ) : base (Target ) { Type = TargetType.Game; DefaultBuildSettings = BuildSettingsVersion.V1; ExtraModuleNames.AddRange( new string [] { "FGame" } ); bDisableDebugInfo = true ; if (Target.Platform == UnrealTargetPlatform.IOS) { DirectoryReference ProjectDir = ProjectFile.Directory; IOSPlatform.bGeneratedSYM = true ; string PackageConfiguration = "" ; switch (Target.Configuration) { case UnrealTargetConfiguration.Debug: case UnrealTargetConfiguration.Development: case UnrealTargetConfiguration.Test: { PackageConfiguration = "Development" ; IOSPlatform.bForDistribution = false ; break ; }; case UnrealTargetConfiguration.Shipping: { PackageConfiguration = "Distibution" ; IOSPlatform.bForDistribution = true ; break ; }; } string cerPath = Path.Combine(ProjectDir.FullName, "Source/ThirdParty/iOS/" ,PackageConfiguration,"XXXXXX_IOS.p12" ); string proversionPath = Path.Combine(ProjectDir.FullName, "Source/ThirdParty/iOS/" ,PackageConfiguration,"com.tencent.xxxx.xx_SignProvision.mobileprovision" ); string cerPassword = "password" ; Console.WriteLine("Import Certificate:" +cerPath); Console.WriteLine("Import Provision:" +proversionPath); if (File.Exists(cerPath) && File.Exists(proversionPath)) { Console.WriteLine("Import Certificate & Provision set to IOSPlatform" ); IOSPlatform.ImportCertificate = cerPath; IOSPlatform.ImportProvision = proversionPath; IOSPlatform.ImportCertificatePassword = cerPassword; } } } }
在打包IOS时就会自动使用所指定的证书了,并且会在Shipping时自动化启用Distribution
,这样就可以避免要事先把证书和provision
导入到系统中。
Mac打包iOS的codesign错误 在Mac上直接打包iOS时遇到以下错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 2020-09-27 19:23:48:778 : /usr/bin/codesign --force --sign 2C74981D1576F95021XXXXXXXXXXA7ECBD8A81A0 --entitlements /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Intermediate/ProjectFilesIOS/build/FGame.build/Development-iphoneos/FGame.build/FGame.app.xcent --timestamp=none /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Binaries/IOS/Payload/FGame.app 2020-09-27 19:23:59:896 : /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Binaries/IOS/Payload/FGame.app: errSecInternalComponent 2020-09-27 19:23:59:896 : Command /usr/bin/codesign failed with exit code 1 2020-09-27 19:23:59:896 : 2020-09-27 19:23:59:899 : ** BUILD FAILED ** 2020-09-27 19:23:59:899 : 2020-09-27 19:23:59:899 : The following build commands failed: 2020-09-27 19:23:59:899 : CodeSign /Users/buildmachine/Documents/BuildWorkspace/workspace/PackageClient/Client/Binaries/IOS/Payload/FGame.app 2020-09-27 19:23:59:900 : (1 failure) 2020-09-27 19:23:59:915 : Took 15.181822s to run env, ExitCode=65 2020-09-27 19:23:59:920 : ERROR: CodeSign Failed 2020-09-27 19:23:59:920 : (see /Users/buildmachine/Library/Logs/Unreal Engine/LocalBuildLogs/BuildCookRun/Log.txt for full exception trace) 2020-09-27 19:23:59:922 : AutomationTool exiting with ExitCode=32 (Error_FailedToCodeSign) 2020-09-27 19:23:59:962 : Took 568.832115s to run mono, ExitCode=32 2020-09-27 19:23:59:974 : AutomationTool exiting with ExitCode=1 (Error_Unknown) 2020-09-27 19:24:00:010 : RunUAT ERROR: AutomationTool was unable to run successfully.
可以看到是执行codesign的时候遇到了错误导致打包失败的。
这是因为打包时会访问钥匙串,需要输入密码授权,如果弹窗之后没有授权就会导致codesign执行失败。Stack overflow上有相同的问题:Xcode Command /usr/bin/codesign failed with exit code 1 : errSecInternalComponent
解决方案有三种:
在打包时的弹窗中输入密码解锁钥匙串
在打包之前的解锁钥匙串
在弹窗中输入密码后选择始终允许codesign访问钥匙串
解锁钥匙串使用以下终端命令:
1 $ security unlock-keychain login.keychain
VirtualCamera 可以使用UE4+支持ARKit的设备来实现通过获取iOS设备的设备位置信息来控制游戏中的相机,从而实现类似虚拟制片的相机追踪效果。官方文档:VirtualCameraPlugin
首先,支持ARKit的设备在Apple的网站上有列出:Augmented Reality - Apple ,以下设备支持:
ARKit 3.0 is only supported on devices with iOS 13, A12/A12X Bionic chips (or later), the Apple Neural Engine (ANE), and a TrueDepth Camera, such as the iPhone XS, iPhone XS Max, iPhone XR, and the 11-inch and 12.9-inch 2018 iPad Pros.
刚好我的iPad Air3在支持之列。而且目前预览版的UE4.26,支持了ARKit3.5
需要在UE项目中启用三个插件:
操作方法为:打开地图,编辑地图所使用的GameMode为VirtualCameraGameMode
,在iPad上打开Unreal Remote 2 输入PC的IP地址,连接成功后在UE编辑器内Play,游戏画面就会传递到iPad,iPad的设备位置和旋转就会回传到UE里控制编辑器内的相机。
这是UE默认提供的方案,但是我后面想要iPad可以与Oculus Quest结合起来,其实问题的关键点在于需要获取到ARKit的设备数据,然后通过网络与Oculus Quest通信。 目前的思路是:
使用UE访问ARKit的设备位置数据在局域网内同步
获取Oculus的设备位置数据
想办法统一坐标系
两边都拿到基于地面高度为基准的高度信息是没问题的,但是如何把ARKit设备的XY和Oculus的结合结合起来是个要思考的问题。
升级至AndroidX资料 看操作方式也是使用UPL来介入打包过程,先记录下。
远程构建在4.26的问题 之前的不少笔记中都写到了使用远程构建的方式出iOS的ipa(详见UE4开发笔记:Mac/iOS篇#配置远程构建 ),但是在4.26发现了一个问题,会导致代码的编译和Cook的不一致。 再来复习一遍远程构建的流程:
把本机的引擎和工程代码上传至Mac
在Mac上执行编译
编译完毕之后在Mac上生成ipa包(但不包含Cook资源)
把生成的ipa包拉回本地,解包,Cook美术资源,再合并为ipa
这其中有个关键的点是:代码的编译和Cook是分别在Mac和Win上执行的,这意味着执行这两个操作的引擎分别是Mac版引擎和Win版引擎。
这个问题就在于,目前4.26的一些代码中加入了PLATFORM_IOS || PLATFORM_MAC
的宏判断,如果完整的打包过程都是在Mac上执行的,就不会出现问题,因为代码编译和Cook都是调用Mac版引擎的,但是在远程构建时就会出现问题了。会导致在Mac上编译工程代码时PLATFORM_IOS || PLATFORM_MAC
这个检查会通过,而在Win上Cook去编译Shader时,因为使用的是Win版引擎,会导致这个宏检查是false,就会导致Cook和代码之间的版本差异,会有Crash。 具体错误如下:
1 2 3 4 5 Sep 19 16:48:01 lipengzha-iPhone FGame[1895] <Notice>: [UE4] [2020.09.19-08.48.01:821][ 0]LogPakFile: New pak file ../../../FGame/Content/Paks/fgame-ios.pak added to pak precacher. Sep 19 16:48:01 lipengzha-iPhone FGame[1895] <Notice>: [UE4] Assertion failed: Shader->Bindings.StructureLayoutHash == ParameterStructMetadata->GetLayoutHash() [File:/Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/BuildEngine/Engine/Engine/Source/Runtime/Engine/Private/ShaderCompiler/ShaderCompiler.cpp] [Line: 4308] Seams shader FPixelProjectedReflectionMobile_ReflectionPassPS's parameter structure has changed without recompilation of the shader Sep 19 16:48:01 lipengzha-iPhone FGame[1895] <Notice>: [UE4] [2020.09.19-08.48.01:834][ 0]Assertion failed: Shader->Bindings.StructureLayoutHash == ParameterStructMetadata->GetLayoutHash() [File:/Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/BuildEngine/Engine/Engine/Source/Runtime/Engine/Private/ShaderCompiler/ShaderCompiler.cpp] [Line: 4308] Seams shader FPixelProjectedReflectionMobile_ReflectionPassPS's parameter structure has changed without recompilation of the shader
导致这个问题的代码:Renderer/Private/PostProcess/PostProcessPixelProjectedReflectionMobile.h#L15 ,这个宏在Win和Mac两个引擎是不同的值。 不过目前4.26还没有出正式版本,观望正式版会不会修正。
Log 在UE中,经常需要在C++代码中打印日志,UE也提供了方法来创建出可以通过UE_LOG打印日志的宏。 首先,先来看一下UE_LOG是什么,它是个宏,使用的方法:
1 UE_LOG(LogTemp,Log,TEXT("" ));
它被定义在Core/Public/Logging/LogMacros.h :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #define UE_LOG(CategoryName, Verbosity, Format, ...) \ { \ static_assert (TIsArrayOrRefOfType<decltype (Format), TCHAR>::Value, "Formatting string must be a TCHAR array." ); \ static_assert ((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) < ELogVerbosity::NumVerbosity && ELogVerbosity::Verbosity > 0 , "Verbosity must be constant and in range." ); \ CA_CONSTANT_IF((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY && (ELogVerbosity::Warning & ELogVerbosity::VerbosityMask) <= FLogCategory##CategoryName::CompileTimeVerbosity) \ { \ UE_LOG_EXPAND_IS_FATAL(Verbosity, PREPROCESSOR_NOTHING, if (!CategoryName.IsSuppressed(ELogVerbosity::Verbosity))) \ { \ auto UE_LOG_noinline_lambda = [](const auto & LCategoryName, const auto & LFormat, const auto &... UE_LOG_Args) FORCENOINLINE \ { \ TRACE_LOG_MESSAGE(LCategoryName, Verbosity, LFormat, UE_LOG_Args...) \ UE_LOG_EXPAND_IS_FATAL(Verbosity, \ { \ FMsg::Logf_Internal(UE_LOG_SOURCE_FILE(__FILE__), __LINE__, LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \ _DebugBreakAndPromptForRemote(); \ FDebug::ProcessFatalError(); \ }, \ { \ FMsg::Logf_Internal(nullptr , 0 , LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \ } \ ) \ }; \ UE_LOG_noinline_lambda(CategoryName, Format, ##__VA_ARGS__); \ UE_LOG_EXPAND_IS_FATAL(Verbosity, CA_ASSUME(false );, PREPROCESSOR_NOTHING) \ } \ } \ }
可以看到,使用UE_LOG时传入的第一个参数,是一个对象,后面的参数则是一个枚举值以及输出的Formater,以及更多的参数(用于匹配Formater中的占位符)。
UE提供了几种方法来创建Log的Category:
1 2 3 DECLARE_LOG_CATEGORY_EXTERN(LogCategoryName,All,All); DECLARE_LOG_CATEGORY_CLASS(LogCategoryName2,All,All); DECLARE_LOG_CATEGORY_EXTERN_HELPER(LogCategoryName3,All,All);
挨个来看一下它们的定义,其实他们都是定义了一个类,并需要创建出一个对象,可以用来传递给UE_LOG
的第一个参数,而后两个参数则都是ELogVerbosity
的枚举值,用于给当前的LogCategory指定运行时和编译时的日志等级。 看一下这个枚举的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 namespace ELogVerbosity{ enum Type : uint8 { NoLogging = 0 , Fatal, Error, Warning, Display, Log, Verbose, VeryVerbose, All = VeryVerbose, NumVerbosity, VerbosityMask = 0xf , SetColor = 0x40 , BreakOnLog = 0x80 }; }
可以根据自己的需求来指定不同的日志等级。
DECLARE_LOG_CATEGORY_EXTERN 1 2 3 4 5 6 7 8 9 10 11 #define DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \ extern struct FLogCategory ##CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \{ \ FORCEINLINE FLogCategory##CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \ } CategoryName;
如果有以下声明:
1 DECLARE_LOG_CATEGORY_EXTERN(LogCategoryName,All,All);
宏展开之后就为:
1 2 3 4 extern struct FLogCategoryLogCategoryName : public FLogCategory<ELogVerbosity::All, ELogVerbosity::All>{ FORCEINLINE FLogCategoryLogCategoryName () : FLogCategory (TEXT("LogCategoryName" )) {} } LogCategoryName;
其实就是继承自FLogCategory
的一个类定义,并且声明 了一个LogCategoryName
的对象。 注意,这里只是声明,还需要定义,不然在编译时会有未定义错误,所以就需要在cpp里写代码进行定义,UE也提供了一个宏:
1 DEFINE_LOG_CATEGORY(LogCategoryName);
它的定义就很简单了,只是定义一个对象而已:
1 2 3 4 5 #define DEFINE_LOG_CATEGORY(CategoryName) FLogCategory##CategoryName CategoryName;
对象经过定义之后就可以在UE_LOG
使用了。 这种分离声明和定义的方式可以用在暴露给外部使用的情况,别的文件或者模块只需要包含具有声明的头文件即可。
DECLARE_LOG_CATEGORY_CLASS DECLARE_LOG_CATEGORY_CLASS
宏的实现就比DECLARE_LOG_CATEGORY_EXTERN
多做了一些操作,它定义了一个结构并创建出一个static对象 ,不需要自己再使用DEFINE_LOG_CATEGRORY
进行定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \ static struct FLogCategory ##CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \{ \ FORCEINLINE FLogCategory##CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \ } CategoryName; #define DECLARE_LOG_CATEGORY_CLASS(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \ DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity)
它的声明会展开为:
1 2 3 4 static struct FLogCategoryLogCategoryName : public FLogCategory<ELogVerbosity::All, ELogVerbosity::All>{ FORCEINLINE FLogCategoryLogCategoryName () : FLogCategory (TEXT("LogCategoryName" )) {} } LogCategoryName;
可以看到,和DECLARE_LOG_CATEGORY_EXTERN
的区别在于:
去掉了extern
修饰符
增加了static修饰符,定义对象
使用这个宏的用途一般直接写在.cpp文件中,只供当前的翻译单元使用。
DECLARE_LOG_CATEGORY_EXTERN_HELPER DECLARE_LOG_CATEGORY_EXTERN_HELPER
这个宏只是DECLARE_LOG_CATEGORY_EXTERN
的封装,并没有自己做什么特别的事情,和DECLARE_LOG_CATEGORY_EXTERN
的用法完全一致。
1 2 3 #define DECLARE_LOG_CATEGORY_EXTERN_HELPER(A,B,C) DECLARE_LOG_CATEGORY_EXTERN(A,B,C)
后记 DECLARE_LOG_CATEGORY_EXTERN
也可以通过XXXX_API
的方式修饰并导出符号,使其可以在外部模块中使用。
JNI调用接收ActivityResult 有时需要通过startActivityForResult
来创建Intent
来执行一些操作,如打开摄像头、打开相册选择图片等。
但是Android做这些操作的时候不是阻塞在当前的函数中的,所以不能直接在调用的函数里接收这些数据。而通过startActivityForResult
执行的Action的结果都会调用到Activity的onActivityResult
中。
1 2 3 @Override protected void onActivityResult (int requestCode, int resultCode, Intent data) {}
UE在UPL中提供了往OnActivityResult追加Java代码的用法:
1 2 <gameActivityOnActivityResultAdditions > </gameActivityOnActivityResultAdditions >
使用这种方式添加的Java代码会追加到OnActivityResult
函数的末尾,但是这种方式有一个问题,那就是执行了自己追加到OnActivityResult
的代码之后,还要处理接收到的结果,并且传递到UE端来,有点麻烦。
经过翻阅代码,发现UE提供了Java端的OnActivityResult
的多播代理事件,这样就可以直接在UE里用C++来监听OnActivityResult
的事件,自己做处理。
1 2 3 4 5 6 DECLARE_MULTICAST_DELEGATE_SixParams(FOnActivityResult, JNIEnv *, jobject, jobject, jint, jint, jobject); static FOnActivityResult OnActivityResultDelegate;
在UE侧就可以通过绑定这个多播代理来监听Java端的OnActivityResult
调用,可以在其中做分别的处理。
它由AndroidJNI.cpp
中的Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult
函数从Java那边调用过来,调用机制在上个笔记中有记录。
Java调C++ 有些需求和实现需要从Java调到C++这边,可以通过下面这种方式: 首先,在GameActivity中新建一个native
的java函数声明(不需要定义):
1 public native void nativeOnActivityResult (GameActivity activity, int requestCode, int resultCode, Intent data) ;
然后在C++端按照下面的规则定义一个C++函数:
1 2 3 4 JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult (JNIEnv* jenv, jobject thiz, jobject activity, jint requestCode, jint resultCode, jobject data) { FJavaWrapper::OnActivityResultDelegate.Broadcast(jenv, thiz, activity, requestCode, resultCode, data); }
可以看到函数名字的规则为:
函数前需要加JNI_METHOD
修饰,它是一个宏__attribute__ ((visibility ("default"))) extern "C"
函数名需要以Java_
开头,并且后面跟上com_epicgames_ue4_GameActivity_
,标识是定义在GameActivity
中的
然后再跟上java中的函数名
接受参数的规则:
第一个参数是Java的Env
第二个是java里的this
后面的参数以此是从java里传递参数
AndroidP的全面屏适配 在UE4打包的时候,会给项目生成GameActivity.java文件,里面的OnCreate具有适配全面屏的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void onCreate (Bundle savedInstanceState) { if (UseDisplayCutout) { WindowManager.LayoutParams params = getWindow().getAttributes(); params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; getWindow().setAttributes(params); } }
Android P和之后的系统不支持,所以就要自己写Jni调用来强制让P和之后系统版本支持。 在UE4.23+以后的引擎版本,支持了通过UPL往OnCreate函数添加代码,就可以直接把代码插入到GameActivity.java了:
1 2 3 4 5 6 7 8 9 10 <gameActivityOnCreateFinalAdditions> <insert> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParams lp = this .getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; this .getWindow().setAttributes(lp); } </insert> </gameActivityOnCreateFinalAdditions>
在UE4.23之前不可以给OnCreate
添加代码,只能自己写个JNI调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void AndroidThunkJava_SetFullScreenDisplayForP () { final GameActivity Activity = this ; runOnUiThread(new Runnable() { private GameActivity InActivity = Activity; public void run () { WindowManager windowManager = InActivity.getWindowManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParams lp = InActivity.getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; InActivity.getWindow().setAttributes(lp); Log.debug( "call AndroidThunkJava_SetFullScreenDisplayForP" ); } } }); }
将其在UPL中通过<gameActivityClassAdditions>
添加到GameActivity.java中,在游戏启动时通过JNI调用即可。
注:
layoutInDisplayCutoutMode
的可选项为:
外部资料
远程编译Shader的Key查找bug 在Project Settings
-Platforms
-IOS
中开启Enable Remote Shader Compile
后,如果填入的构建机地址具有指定端口,在查找SSHkey时会有问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 bool IsRemoteBuildingConfigured (const FShaderCompilerEnvironment* InEnvironment) { GRemoteBuildServerSSHKey = "" ; if (InEnvironment != nullptr && InEnvironment->RemoteServerData.Contains(TEXT("SSHPrivateKeyOverridePath" ))) { GRemoteBuildServerSSHKey = InEnvironment->RemoteServerData[TEXT("SSHPrivateKeyOverridePath" )]; } if (GRemoteBuildServerSSHKey.Len() == 0 ) { GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings" ), TEXT("SSHPrivateKeyOverridePath" ), GRemoteBuildServerSSHKey, GEngineIni); GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings" ), TEXT("SSHPrivateKeyOverridePath" ), GRemoteBuildServerSSHKey, GEngineIni); if (GRemoteBuildServerSSHKey.Len() == 0 ) { if (!FParse::Value(FCommandLine::Get(), TEXT("serverkey" ), GRemoteBuildServerSSHKey) && GRemoteBuildServerSSHKey.Len() == 0 ) { if (GRemoteBuildServerSSHKey.Len() == 0 ) { FString Path = FPlatformMisc::GetEnvironmentVariable(TEXT("APPDATA" )); GRemoteBuildServerSSHKey = FString::Printf(TEXT("%s\\Unreal Engine\\UnrealBuildTool\\SSHKeys\\%s\\%s\\RemoteToolChainPrivate.key" ), *Path, *GRemoteBuildServerHost, *GRemoteBuildServerUser); } } } } }
可以看到这里查找的Key路径时直接通过GRemoteBuildServerHost
拼接的,但是如果在配置中指定了端口,那么GRemoteBuildServerHost
的值为这种格式xxx.xx.xx.xx:1234
,但是Win上目录名不能带:
,就会导致Key查找失败。
还有在同文件的ExecRemoteProcess
函数中,没有针对具有指定端口的情况做处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool ExecRemoteProcess (const TCHAR* Command, const TCHAR* Params, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr) {#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING return FPlatformProcess::ExecProcess(Command, Params, OutReturnCode, OutStdOut, OutStdErr); #else if (GRemoteBuildServerHost.IsEmpty()) { return false ; } FString CmdLine = FString(TEXT("-i \"" )) + GRemoteBuildServerSSHKey + TEXT("\" \"" ) + GRemoteBuildServerUser + '@' + GRemoteBuildServerHost + TEXT("\" " ) + Command + TEXT(" " ) + (Params != nullptr ? Params : TEXT("" )); return ExecProcess(*GSSHPath, *CmdLine, OutReturnCode, OutStdOut, OutStdErr); #endif }
需要做一些处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bool ExecRemoteProcess (const TCHAR* Command, const TCHAR* Params, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr) {#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING return FPlatformProcess::ExecProcess(Command, Params, OutReturnCode, OutStdOut, OutStdErr); #else if (GRemoteBuildServerHost.IsEmpty()) { return false ; } FString RemoteBuildServerIP = GRemoteBuildServerHost; FString RemoteBuildServerPort = TEXT("22" ); if (GRemoteBuildServerHost.Contains(TEXT(":" ))) { GRemoteBuildServerHost.Split(TEXT(":" ),&RemoteBuildServerIP,&RemoteBuildServerPort); } FString CmdLine = FString(TEXT("-i \"" )) + GRemoteBuildServerSSHKey + TEXT("\" \"" ) + GRemoteBuildServerUser + '@' + RemoteBuildServerIP + TEXT("\" " ) TEXT("-p " ) + RemoteBuildServerPort +TEXT(" " )+ Command + TEXT(" " ) + (Params != nullptr ? Params : TEXT("" )); return ExecProcess(*GSSHPath, *CmdLine, OutReturnCode, OutStdOut, OutStdErr); #endif }
平台相关Target 在项目的Target.cs中定义着项目的TargetRules,但是也不是所有的平台都可以通用全部的参数,每个平台都有自己特定的属性,所以UE的TargetRules定义中中还包含各个平台的Target:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 [ConfigSubObject ] public AndroidTargetRules AndroidPlatform = new AndroidTargetRules();[ConfigSubObject ] public IOSTargetRules IOSPlatform = new IOSTargetRules();[ConfigSubObject ] public LuminTargetRules LuminPlatform = new LuminTargetRules();[ConfigSubObject ] public LinuxTargetRules LinuxPlatform = new LinuxTargetRules();[ConfigSubObject ] public MacTargetRules MacPlatform = new MacTargetRules();[ConfigSubObject ] public PS4TargetRules PS4Platform = new PS4TargetRules();[ConfigSubObject ] public SwitchTargetRules SwitchPlatform = new SwitchTargetRules();[ConfigSubObject ] public WindowsTargetRules WindowsPlatform; [ConfigSubObject ] public XboxOneTargetRules XboxOnePlatform = new XboxOneTargetRules();[ConfigSubObject ] public HoloLensTargetRules HoloLensPlatform;
当需要对某个平台进行特殊控制时,可以在TargetRules中访问特定平台的对象。
如在IOS平台生成dSYM:
1 2 3 4 if (Target.Platform == UnrealTargetPlatform.IOS){ IOSPlatform.bGeneratedSYM = true ; }
TargetRule获取项目路径 使用ProjectFile
:
1 Console.WriteLine("ProjectDir:" + ProjectFile.Directory);
UObject获取资源路径 可以使用FStringAssetReference
:
1 2 FStringAssetReference ObjectPath (Object) ;FString AssetPackagePath = ObjectPath.ToString();
获取的路径格式为:
1 /Game/StarterContent/Materials/M_Basic_Floor.M_Basic_Floor
如果想要获得类似编辑器Copy Reference
的信息,则可以使用FAssetData
:
1 2 FAssetData AssetData (Object) ;FString ReferenceInfo = AssetData.GetExportTextName();
获取的格式为:
1 Material'/Game/StarterContent/Materials/M_Basic_Floor.M_Basic_Floor'
也能够根据UObject获取到它的资源类型。
如果用FAssetData
获取C++类会得到下面的这样的信息(在运行时动态创建的对象):
1 Actor'/Game/StarterContent/Maps/UEDPIE_0_Minimal_Default.Minimal_Default:PersistentLevel.Actor_0'
打包iOS导出dSYM 像Bugly之类的crash上报平台都需要上传符号表才能看到具体的堆栈信息,而iOS上的符号和调试信息都是在dSYM
文件中的。 UE提供了dSYM的生成选项,在Project Settings
-Platforms
-iOS
-Build
:
Generate dSYM file for code debugging and profiling:只开启这个会在Binaries/IOS
下生成PROJECT_NAME.dSYM
文件
Generate dSYM bundle for third party crash tools:依赖上面的选项,如果开启会在Binaries/IOS
下生成PROJECT_NAME.dSYM.zip
,并且不会再生成PROJECT_NAME.dSYM
文件。
但是,在使用源码版打包iOS项目的时候生成的dSYM
特别大,超过2G,而同样的工程用Luncher引擎打包就只有100+M,而且bugly之类的上传还有大小限制。
经过对比之后发现,大小的差距主要是在_DWARF_debug_*
这些上(左侧为Launcher版,右侧为DebugGame源码版):
本来以为是源码版会把所有参与编译的代码都导出到dSYM文件中,但是经过翻阅引擎代码发现,其实TargetRules中有控制调试信息的选项,就是来控制产生这些_DWARF_debug_*
的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [CommandLine("-NoDebugInfo" ) ] [XmlConfigFile(Category = "BuildConfiguration" ) ] public bool bDisableDebugInfo = false ;[XmlConfigFile(Category = "BuildConfiguration" ) ] public bool bDisableDebugInfoForGeneratedCode = false ;[XmlConfigFile(Category = "BuildConfiguration" ) ] public bool bOmitPCDebugInfoInDevelopment = false ;
在项目的Target.cs
中控制这些变量即可,如bDisableDebugInfo=true
,在源码版引擎中也不会生很大的_DWARF_debug
文件了(左侧为Lunch版引擎,右侧为DebugGame源码版控制bDisableDebugInfo=true
):
而且,还发现UE在打包IOS平台的时候处理有问题,本来以为打Shipping生成的dSYM会没有调试信息了,但是测试发现还是非常大。
经过分析后发现,在UE的构建系统中是通过两个值来控制是否创建调试信息的:
1 2 3 4 5 6 GlobalCompileEnvironment.bCreateDebugInfo = !Target.bDisableDebugInfo && ShouldCreateDebugInfo(Target); GlobalLinkEnvironment.bCreateDebugInfo = GlobalCompileEnvironment.bCreateDebugInfo;
可以看到是通过Target.bDisableDebugInfo
和ShouldCreateDebugInfo(Target)
两个值来控制的,而ShouldCreateDebugInfo
函数则是每个平台的有自己的重写实现。
如在Windows上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public override bool ShouldCreateDebugInfo (ReadOnlyTargetRules Target ) { switch (Target.Configuration) { case UnrealTargetConfiguration.Development: case UnrealTargetConfiguration.Shipping: case UnrealTargetConfiguration.Test: return !Target.bOmitPCDebugInfoInDevelopment; case UnrealTargetConfiguration.DebugGame: case UnrealTargetConfiguration.Debug: default : return true ; }; }
但是,在IOS和Mac上完全没有判断!
1 2 3 4 5 6 public override bool ShouldCreateDebugInfo (ReadOnlyTargetRules Target ) { return true ; }
这导致只能自己用bDisableDebugInfo=true
来控制,太坑爹了。翻了代码才发现bOmitPCDebugInfoInDevelopment
这个选项只在PC上有效:BuildConfigProperties.INT.udn#L85 。
坑点 根据上面的介绍,可以通过控制bDisableDebugInfo=true
来生成体积较小的dSYM
,但这样有牵扯出来一个坑的问题:使用远程构建时dSYM
无法通过UE的构建系统传回本地。 查了下代码,没有什么比较方便的办法来控制从远程拷贝的文件,目前我使用pscp
从远程拉取dSYM文件回本地。
编译引擎支持Android和iOS 把源码版引擎导出为安装版引擎时可以使用BuildGraph(Win+Android+iOS):
1 Engine/Build/BatchFiles/RunUAT.bat BuildGraph -Script=InstalledEngineBuild.xml -Target="Make Installed Build Win64" -set :WithDDC=false -set :WithWin32=false -std:HostPlatformEditorOnly=true -std:HostPlatformOnly=true -set :WithAndroid=true -set :WithIOS=true -set :WithLumin=false -set :WithLuminMac=false -set :WithTVOS=false -set :WithLinux=false -set :WithLinuxAArch64=false -set :WithHoloLens=false -set :EmbedSrcSrvInfo=false -set :WithFullDebugInfo=false -set :GameConfigurations=Development -set :VS2019=true -set :InstalledDir=D:\EngineBin -compile
但是编译iOS需要一台Mac,类似项目的远程构建,引擎构建iOS支持时也需要。 在之前的笔记中写到,项目的远程构建iOS时可以在DefaultEngine.ini
中加上远程机器的地址和SSHKey:
1 2 3 4 [/Script/IOSRuntimeSettings.IOSRuntimeSettings] RemoteServerName =xxx.xx.xx.xxxRSyncUsername =machinenameSSHPrivateKeyOverridePath =D:\XXXX\RemoteToolChainPrivate.key
在构建引擎时需要把它们写到Engine\Config\BaseEngine.ini
中,然后再使用上面的命令构建即可。
Android设置宽高比 在Project Settings
-Platforms
-Android
-Maximum support aspect ratio
的值,默认是2.1,但是在全面屏的情况下会有黑边。
它控制的值是AndroidManifest.xml
中的值:
1 <meta-data android:name ="android.max_aspect" android:value ="2.1" />
我目前设置的值是2.5.
注:Enable FullScreen Immersive on KitKat and above devices
控制的是进入游戏时是否隐藏虚拟按键。
UPARAM 可以在UFUNCTION的函数中给指定的参数来指定它的UPARAM,可以用来控制函数的参数属性。之前是用来指定Ref(UPARAM(Ref)
),其实也可以使用UPARAM(meta=())
来指定meta
属性。
如在蓝图实现使用枚举bitmask:
1 2 3 4 5 6 7 8 UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category="Audiokinetic|Actor" , meta=(AdvancedDisplay="2" , AutoCreateRefTerm = "PostEventCallback,ExternalSources" )) static int32 PostEvent ( class UAkAudioEvent* AkEvent, class AActor* Actor, UPARAM(meta = (Bitmask, BitmaskEnum = EAkCallbackType)) int32 CallbackMask, const FOnAkPostEventCallback& PostEventCallback, const TArray<FAkExternalSourceInfo>& ExternalSources, bool bStopWhenAttachedToDestroyed = false , FString EventName = FString("" )) ;
指定返回值的名字什么的都不在话下:
1 static UPARAM (DisplayName = "Bundle" ) FOSCBundle& AddMessageToBundle (const FOSCMessage& Message, UPARAM(ref) FOSCBundle& Bundle) ;
可以在UPARAM 看到引擎代码中的各种用法。
无边框模式 可以开启Project Settings
-Description
-Settings
-Use Boardless Window
:
这个变量在UGameEngine::CreateGameWindow 中用到,用来控制引擎启动创建窗口时,窗口的Style.
在不重新打包的情况下可以将下面配置写到Saved/Config/WindowsNoEditor/Game.ini
中:
1 2 [/Script/EngineSettings.GeneralProjectSettings] bUseBorderlessWindow =true
重启游戏就是无边框模式了。
命令行导入iOS证书和Provision 在部署构建机的时候,需要给每台构建机都导入iOS的provision
和p12
证书,如果每个都要打开一遍编辑器,纯粹是重复劳动,翻了下代码,UE编辑器中导入证书是通过调用IPhonePackager.exe
来实现的,而且是以命令行的方式执行,这样就好办了,按照它的参数自己实现脚本即可。 具体看引擎中的相关代码:IOSTargetSettingsCustomization.cpp#L1312
Certificate 导入证书的命令为:
1 Engine\Binaries\DotNET\IOS\IPhonePackager.exe Install Engine -project "D:\Client\FGame.uproject" -certificate "D:\IOS_DEVELOPMENT.p12" -bundlename "com.tencent.xxxx.xx"
证书是导入到系统中的,可以在certmgr.msc
中查看导入的证书:
注意 :因为iPhonePackager.exe在导入证书时会让弹框输入证书的密码:
这导致不能用在自动化流程里,但是我怎么可能会老老实实每台电脑都输一次密码呢,看了一下iPhonePackager的代码,找到了弹窗让输入密码的地方Programs/IOS/iPhonePackager/ToolsHub.cs#L263 ,我给它加了个从命令行参数读取密码的选项,如果有该参数就不会弹框:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 string CertificatePassword = "" ;string [] arguments = Environment.GetCommandLineArgs();for (int index = 0 ;index<arguments.Length;++index){ if (arguments[index] == "-cerpassword" && index != (arguments.Length-1 )) { CertificatePassword = arguments[index + 1 ]; Console.WriteLine("Usage -cerpasseord argument" ); } } X509Certificate2 Cert = null ; try { Cert = new X509Certificate2(CertificateFilename, CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); } catch (System.Security.Cryptography.CryptographicException ex){ if (CertificatePassword.Length > 0 || PasswordDialog.RequestPassword(out CertificatePassword)) { Cert = new X509Certificate2(CertificateFilename, CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); } else { throw ex; } }
把上面的代码替换掉Programs/IOS/iPhonePackager/ToolsHub.cs#L263 这里的部分,重新编译iPhonePackager即可。
Provision 导入Provision
的命令为:
1 Engine\Binaries\DotNET\IOS\IPhonePackager.exe Install Engine -project "D:\Client\FGame.uproject" -provision "D:\com.tencent.xxxx.xx_Development_SignProvision.mobileprovision" -bundlename "com.tencent.xxxx.xx"
Provision文件导入后会放在用户目录下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public static string ProvisionDirectory{ get { if (Environment.OSVersion.Platform == PlatformID.MacOSX || Environment.OSVersion.Platform == PlatformID.Unix) { return Environment.GetEnvironmentVariable("HOME" ) + "/Library/MobileDevice/Provisioning Profiles/" ; } else { return Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData) + "/Apple Computer/MobileDevice/Provisioning Profiles/" ; } } }
Win上就为:C:\Users\USER_NAME\AppData\Local\Apple Computer\MobileDevice\Provisioning Profiles
.
C#获取命令行参数 需要using system;
.
1 2 3 4 5 6 string [] arguments = Environment.GetCommandLineArgs();for (int index = 0 ;index<arguments.Length;++index){ }
打包时给不同的平台添加资源 有时我们需要在打包时给不同的平台添加不同目录下的外部资源(如WWise),UE提供了Project Settings
-Packaging
-Additional Non-Asset Directories to Package
,找了一下,并没有发现单独给某个平台指定添加路径的地方。但是 ,Additional Non-Asset Directories to Package
本身是支持给不同平台添加不同目录的,只要在Additional Non-Asset Directories to Package
添加的目录下根据不同的平台创建不同的目录。 如:
1 2 3 4 5 6 7 D:\EmptyProject\Content\WwiseAudio>tree /a +---Android | \---English(US) +---iOS | \---English(US) \---Windows \---English(US)
在项目的Content\WwiseAudio
下,有Android
/iOS
/Windows
等目录,在Additional Non-Asset Directories to Package
中添加的是WwiseAudio
目录,打包时只会把对应平台的目录给打包进去,但是没有看到UE的文档里哪有写。
监听资源保存的事件 1 2 3 4 5 6 7 8 void PackageSaved (const FString& PacStr,UObject* PackageSaved) { UE_LOG(LogTemp,Log,TEXT("Package %s Saved." ),*PacStr); } void FEmptyProjectModule::StartupModule () { UPackage::PackageSavedEvent.AddStatic(&PackageSaved); }
UStruct的json序列化 因为UStruct在UE内是具有反射的,所以不用自己去解析编码就可以实现序列化和反序列化,UE中提供了一个辅助模块:JsonUtilities
,里面具有FJsonObjectConverter
类,定义了一系列的操作。 我简单封装了一下,对Ustrut的序列化和反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 template <typename TStructType>static bool TSerializeStructAsJsonObject (const TStructType& InStruct,TSharedPtr<FJsonObject>& OutJsonObject) { if (!OutJsonObject.IsValid()) { OutJsonObject = MakeShareable(new FJsonObject); } bool bStatus = FJsonObjectConverter::UStructToJsonObject(TStructType::StaticStruct(),&InStruct,OutJsonObject.ToSharedRef(),0 ,0 ); return bStatus; } template <typename TStructType>static bool TDeserializeJsonObjectAsStruct (const TSharedPtr<FJsonObject>& OutJsonObject,TStructType& InStruct) { bool bStatus = false ; if (OutJsonObject.IsValid()) { bStatus = FJsonObjectConverter::JsonObjectToUStruct(OutJsonObject.ToSharedRef(),TStructType::StaticStruct(),&InStruct,0 ,0 ); } return bStatus; } template <typename TStructType>static bool TSerializeStructAsJsonString (const TStructType& InStruct,FString& OutJsonString) { bool bRunStatus = false ; { TSharedPtr<FJsonObject> JsonObject; if (TSerializeStructAsJsonObject<TStructType>(InStruct,JsonObject) && JsonObject.IsValid()) { auto JsonWriter = TJsonWriterFactory<>::Create(&OutJsonString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter); bRunStatus = true ; } } return bRunStatus; } template <typename TStructType>static bool TDeserializeJsonStringAsStruct (const FString& InJsonString,TStructType& OutStruct) { bool bRunStatus = false ; TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(InJsonString); TSharedPtr<FJsonObject> DeserializeJsonObject; if (FJsonSerializer::Deserialize(JsonReader, DeserializeJsonObject)) { bRunStatus = TDeserializeJsonObjectAsStruct<TStructType>(DeserializeJsonObject,OutStruct); } return bRunStatus; }
Redirector Redirector
是标记被移动资源的引用关系的,在移动具有引用的资源时会产生。 如/Game/GameMap
引用到了一个UI资源/Game/UMG_Main
,当移动/Game/UMG_Main
到/Game/TEST/UMG_Main
时,会在/Game/UMG_Main
的磁盘路径下创建出一个Redirector,用与告诉引用到该UI的资源,它的真实路径已经发生变化了,所以叫Redirector。 在项目中需要尽量避免Redirector的存在。
检查引擎是否运行在Commandlet UE中有检测的方法,定义在CoreGlobals.h 的IsRunningCommandlet
:
1 2 3 4 5 6 7 8 9 10 11 FORCEINLINE bool IsRunningCommandlet () {#if WITH_ENGINE return PRIVATE_GIsRunningCommandlet; #else return false ; #endif }
FSoftObjectPath限定类型 默认FSoftObjectPath
可以指定任何继承自UObject的资源类型,但有时候只想要指定某些类型,UE提供了限定类型的功能,要使用UPROPERTY:
1 2 UPROPERTY(config, EditAnywhere, Category=DefaultMaps, meta=(AllowedClasses="World" )) FSoftObjectPath EditorStartupMap;
在meta
中使用AllowedClasses
即可,AllowedClassess
的值是实际类型去掉前缀U
,如USoundClass
要使用SoundClass
。
AllowedClasses
可以指定多个,使用逗号分隔,使当前FSoftObjectPath可以指定多个限定类型的资源。而且还可以使用ExactClass
来控制是否严格限定类型(是否允许继承层次中类型中的资源,如UDataTable
和UCompositeDataTable
),如果不指定,默认是严格限定类型的,如果ExactClass=true
则可以使用继承层次中类型的资源。
编辑器viewport显示文字 UKismetSystemLibrary::PrintString
是运行时可以输出到viewport,在编辑器下无效果,想要实现编辑器下的文本输出可以通过FCanvas
的DrawItem
来实现:
1 2 3 FCanvasTextItem TextItem (FVector2D(100 , 200 ), LOCTEXT("OutOfTextureMemory" , "RAN OUT OF TEXTURE MEMORY, EXPECT CORRUPTION AND GPU HANGS!" ), GEngine->GetMediumFont(), FLinearColor::Red) ;TextItem.EnableShadow(FLinearColor::Black); Canvas->DrawItem(TextItem);
如stat fps
等,都是通过FCanvas
来实现的,如DrawMapWarnings :
以及RenderStatFPS :
FCanvas
可以通过FViewport::GetDebugCanvas()
来获得。
开启多核心编译 UBT会从下面三个文件中读取配置:
1 2 3 * Engine/Saved/UnrealBuildTool/BuildConfiguration.xml * *User Folder/AppData*/Roaming/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml * *My Documents*/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
我们只需要修改其中的任意一个就可以。
默认具有以下内容:
1 2 3 <?xml version="1.0" encoding="utf-8" ?> <Configuration xmlns ="https://www.unrealengine.com/BuildConfiguration" > </Configuration >
更多的BuildConfigutation的参数可以看引擎文档Configuring Unreal Build System 。在文档中ProcessorCountMultiplier
元素的数字就是允许使用的处理器数,但是 ,UE的文档和实际的代码有出入,按照上面的文档,设置ProcessorCountMultiplier
是在BuildConfigutation
下的,但是这么写会与错误:
1 2 3 4 5 6 <?xml version="1.0" encoding="utf-8" ?> <Configuration xmlns ="https://www.unrealengine.com/BuildConfiguration" > <BuildConfiguration > <ProcessorCountMultiplier > 7</ProcessorCountMultiplier > </BuildConfiguration > </Configuration >
错误信息:
1 BuildConfiguration.xml(4): [] 元素 命名空间“https://www.unrealengine.com/BuildConfiguration”中的“BuildConfiguration”。 的子元素 命名空间“https://www.unrealengine.com/BuildConfiguration”中的“ProcessorCountMultiplier”。 无效。应为可能元素的列表: 命名空间“https://www.unrealengine.com/BuildConfiguration”中的“bPGOProfile, bAllowHybridExecutor, DMUCSDistProp, bAllowXGE, bGeneratedSYMFile, bAllowSNDBS, bIgnoreOutdatedImportLibraries, bUseFastSemanticsRenderContexts, bDisableDebugInfo, bParseTimingInfoForTracing, CppStandard, bPrintDebugInfo, bUseSharedPCHs, bDisableDebugInfoForGeneratedCode, bForcePrecompiledHeaderForGameModules, bUseShippingPhysXLibraries, bUseAdaptiveUnityBuild, bXGENoWatchdogThread, MinGameModuleSourceFilesForUnityBuild, bAdaptiveUnityDisablesOptimizations, bCheckLicenseViolations, bAllowParallelExecutor, bOmitFramePointers, bUseCheckedPhysXLibraries, bSupportEditAndContinue, bAllowASLRInShipping, bStripSymbols, bAllowDistcc, bVerboseDistccOutput, bUseInlining, bAllowDistccLocalFallback, bAdaptiveUnityEnablesEditAndContinue, bEnableMemorySanitizer, bCheckSystemHeadersForModification, bUseFastPDBLinking, bAdaptiveUnityDisablesProjectPCHForProjectPrivate, BaseLogFileName, bUsePerFileIntellisense, bCreateMapFile, bUsePCHFiles, DMUCSCoordinat...。
查阅代码之后发现,这些配置选项要去看BuildConfigProperties.INT.udn 里的参数,ProcessorCountMultiplier
是在ParallelExecutor
下的,所以改成以下配置:
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="utf-8" ?> <Configuration xmlns ="https://www.unrealengine.com/BuildConfiguration" > <ParallelExecutor > <ProcessorCountMultiplier > 7</ProcessorCountMultiplier > <MaxProcessorCount > 7</MaxProcessorCount > <bStopCompilationAfterErrors > true</bStopCompilationAfterErrors > </ParallelExecutor > </Configuration >
目前我使用的完整配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?xml version="1.0" encoding="utf-8" ?> <Configuration xmlns ="https://www.unrealengine.com/BuildConfiguration" > <BuildConfiguration > <ProcessorCountMultiplier > 7</ProcessorCountMultiplier > <MaxParallelActions > 7</MaxParallelActions > <bAllowParallelExecutor > true</bAllowParallelExecutor > </BuildConfiguration > <SNDBS > <ProcessorCountMultiplier > 4</ProcessorCountMultiplier > <MaxProcessorCount > 4</MaxProcessorCount > </SNDBS > <ParallelExecutor > <ProcessorCountMultiplier > 7</ProcessorCountMultiplier > <MaxProcessorCount > 7</MaxProcessorCount > <bStopCompilationAfterErrors > true</bStopCompilationAfterErrors > </ParallelExecutor > </Configuration >
相关连接:
URL Encode/Decode UE提供了相关的API:
1 2 static FString FGenericPlatformHttp::UrlDecode (const FString & EncodedString) static FString FGenericPlatformHttp::UrlEncode (const FString & UnencodedString)
执行结果:
查看APK的签名信息 可以使用keytool.exe
(在JDK的bin中),使用以下参数:
1 keytool.exe" -list -printcert -jarfile FGame-armv7.apk
会打印出apk的签名色所有者和发布者,以及证书的有效时间、证书指纹等等。
ASTC设置压缩率 在Project Settings
-Engine
-Cooker
中有选项:
Android重启App 当游戏更新完毕之后有时候需要重启App才可以生效,在UE中可以使用UPL写入以下java代码:
1 2 3 4 5 6 7 8 9 10 public void AndroidThunkJava_AndroidAPI_RestartApplication ( ) { Context context = getApplicationContext(); PackageManager pm = context.getPackageManager(); Intent intent = pm.getLaunchIntentForPackage(context.getPackageName()); int delayTime = 500 ; AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); PendingIntent restartIntent = PendingIntent.getActivity(context, 0 , intent, PendingIntent.FLAG_CANCEL_CURRENT); alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + delayTime, restartIntent); System.exit(0 ); }
在需要重启的时候通过jni调用来触发:
1 2 3 4 5 6 7 8 9 void RestartApplication () {#if PLATFORM_ANDROID if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true )) { FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis, AndroidThunkJava_AndroidAPI_RestartApplication); } #endif }
打包Windows以窗口模式启动 在项目的Config
下新建DefaultGameUserSettings.ini
文件,填入以下内容:
1 2 3 4 5 6 7 8 9 10 11 [/Script/Engine.GameUserSettings] bUseVSync =False ResolutionSizeX =1920 ResolutionSizeY =1080 LastUserConfirmedResolutionSizeX =1920 LastUserConfirmedResolutionSizeY =1080 WindowPosX =-1 WindowPosY =-1 FullscreenMode =2 LastConfirmedFullscreenMode =2 Version =5
打包之后就会以窗口模式启动了,分辨率可以自己修改。
指定地图Cook及打包 在DefaultEditor.ini
中添加以下项:
1 2 3 4 5 6 7 [AllMaps] +Map=/Game/Assets/Scene/Map/Fb/v3/8r/xzzn/Fb_ThePoolOfTribute +Map=/Game/Assets/Scene/Map/LightSpeed/LightSpeed [AlwaysCookMaps] +Map=/Game/Assets/Scene/Map/Fb/v3/8r/xzzn/Fb_ThePoolOfTribute +Map=/Game/Assets/Scene/Map/LightSpeed/LightSpeed
Windows通过bat存储环境变量 设置用户环境变量:
设置系统环境变量:
1 setx ENV_NAME env_value /m
C++访问Collision Chanel ECollisionChanel
是定义在EngineTypes.h 中的枚举类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 UENUM(BlueprintType) enum ECollisionChannel{ ECC_WorldStatic UMETA(DisplayName="WorldStatic"), ECC_WorldDynamic UMETA(DisplayName="WorldDynamic"), ECC_Pawn UMETA(DisplayName="Pawn"), ECC_Visibility UMETA(DisplayName="Visibility" , TraceQuery="1"), ECC_Camera UMETA(DisplayName="Camera" , TraceQuery="1"), ECC_PhysicsBody UMETA(DisplayName="PhysicsBody"), ECC_Vehicle UMETA(DisplayName="Vehicle"), ECC_Destructible UMETA(DisplayName="Destructible"), ECC_EngineTraceChannel1 UMETA(Hidden), ECC_EngineTraceChannel2 UMETA(Hidden), ECC_EngineTraceChannel3 UMETA(Hidden), ECC_EngineTraceChannel4 UMETA(Hidden), ECC_EngineTraceChannel5 UMETA(Hidden), ECC_EngineTraceChannel6 UMETA(Hidden), ECC_GameTraceChannel1 UMETA(Hidden), ECC_GameTraceChannel2 UMETA(Hidden), ECC_GameTraceChannel3 UMETA(Hidden), ECC_GameTraceChannel4 UMETA(Hidden), ECC_GameTraceChannel5 UMETA(Hidden), ECC_GameTraceChannel6 UMETA(Hidden), ECC_GameTraceChannel7 UMETA(Hidden), ECC_GameTraceChannel8 UMETA(Hidden), ECC_GameTraceChannel9 UMETA(Hidden), ECC_GameTraceChannel10 UMETA(Hidden), ECC_GameTraceChannel11 UMETA(Hidden), ECC_GameTraceChannel12 UMETA(Hidden), ECC_GameTraceChannel13 UMETA(Hidden), ECC_GameTraceChannel14 UMETA(Hidden), ECC_GameTraceChannel15 UMETA(Hidden), ECC_GameTraceChannel16 UMETA(Hidden), ECC_GameTraceChannel17 UMETA(Hidden), ECC_GameTraceChannel18 UMETA(Hidden), ECC_OverlapAll_Deprecated UMETA(Hidden), ECC_MAX, };
但是我们在项目设置中添加是可以取任意的名字的,而且创建的Chanel的数量有上限(18个),这是因为ECollisionChanel
是预先定义了18个供游戏创建的枚举值,假如我们创建了一个名字为AAA
的Chanel,会在当前项目的Config/DefaultEngine.ini
的[/Script/Engine.CollisionProfile]
中创建以下项:
1 +DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Overlap,bTraceType=False,bStaticObject=False,Name="AAA")
在这里跟枚举值做了绑定,在C++代码中进行设置的时候就需要指定ECC_GameTraceChannel1
的枚举值。
Todo:可以写一个方便从名字获取ECollisionChanel枚举值的库。
Actor的延迟Spawn
1 2 3 4 5 6 7 8 FTransform SpawnTransform (Rotation, Origin) ;auto MyDeferredActor = Cast<ADeferredActor>(UGameplayStatics::BeginDeferredActorSpawnFromClass(this , DeferredActorClass, SpawnTransform));if (MyDeferredActor != nullptr ){ MyDeferredActor->Init(ShootDir); UGameplayStatics::FinishSpawningActor(MyDeferredActor, SpawnTransform); }
编译引擎出现以下错误:
1 2 3 4 ERROR: Unable to find installation of PDBCOPY.EXE, which is required to strip symbols. This tool is included as part of the 'Windows Debugging Tools' component of the Windows 10 SDK (https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk). while executing task <Strip Platform="Win64" BaseDir="C:\BuildAgent\workspace\FGameEngine\FEngine" Files="#UE4Editor Win64 Unstripped" OutputDir="C:\BuildAgent\workspace\FGameEngine\FEngine\Engine\Saved" Tag="#UE4Editor Win64 Stripped" /> at Engine\Build\InstalledEngineBuild.xml(183) (see C:\BuildAgent\workspace\FGameEngine\FEngine\Engine\Programs\AutomationTool\Saved\Logs\Log.txt for full exception trace)
这是因为没有安装Windows Debugging Tools,在这里 下载Win10SDK安装程序,在其中选择安装Windows Debug Tools安装即可。
BuildGraph构建引擎 可以使用下列命令: 构建引擎的工具集:
1 Engine\Build\BatchFiles\RunUAT.bat BuildGraph -Script=Engine\Build\InstalledEngineBuild.xml -Target="Build Tools Win64" -set :HostPlatformOnly=true -set :WithWin64=true -set :WithWin32=false -set :WithDDC=false -clean
构建引擎:
1 2 Engine/Build/BatchFiles/RunUAT.bat BuildGraph -Script=Engine\Build\InstalledEngineBuild.xml -target=\"Make Installed Build Win64\" -set :WithDDC=false -set :WithWin64=true -set :WithWin32=false -set :WithAndroid=false -set :WithIOS=false -set :WithTVOS=false -set :WithLinux=false -set :WithLinuxAArch64=false -set :WithLumin=false -set :WithHoloLens=false
Game module could not be loaded 在引入其他模块的代码时,有时候会具有下面这样的情况:
Log中的提示:
1 2 3 4 5 [2020.07.18-01.58.50:729][ 0]LogWindows: Failed to load 'E:/UnrealProjects/Examples/HotPatcherExample/Binaries/Win64/UE4Editor-HotPatcherExample.dll' (GetLastError=1114) [2020.07.18-01.58.50:729][ 0]LogModuleManager: Warning: ModuleManager: Unable to load module 'E:/UnrealProjects/Examples/HotPatcherExample/Binaries/Win64/UE4Editor-HotPatcherExample.dll' because the file couldn't be loaded by the OS. [2020.07.18-02.01.38:988][ 0]LogWindowsTextInputMethodSystem: Display: IME system now activated using TSF (微软拼音). [2020.07.18-02.01.38:991][ 0]Message dialog closed, result: Ok, title: Message, text: The game module 'HotPatcherExample' could not be loaded. There may be an operating system error or the module may not be properly set up. [2020.07.18-02.01.38:991][ 0]LogCore: Engine exit requested (reason: EngineExit() was called)
这个问题应该是项目中(或者插件中)加载DLL失败的问题,一般情况下是DLL文件存在问题或者DLL中逻辑的错误造成。
经过排查后发现我触发这个问题的方式为在使用UnLua注册lua_Reg
函数时漏掉了置空最后一个元素:
1 2 3 4 5 6 7 8 9 10 static const luaL_Reg AMyActorLib[]={ {"ReceiveBytes" ,ReceiveBytes}, }; BEGIN_EXPORT_REFLECTED_CLASS(AMyActor) ADD_LIB(AMyActorLib) END_EXPORT_CLASS(AMyActor) IMPLEMENT_EXPORTED_CLASS(AMyActor)
因为AMyActorLib
这个数组最后一个元素没有置空,在UnLuaEx.inl
中的AddLib函数中:
1 2 3 4 5 6 7 8 9 10 11 12 template <bool bIsReflected>void TExportedClassBase<bIsReflected>::AddLib(const luaL_Reg *InLib){ if (InLib) { while (InLib->name && InLib->func) { GlueFunctions.Add(new FGlueFunction(ANSI_TO_TCHAR(InLib->name), InLib->func)); ++InLib; } } }
如果lua_Reg
数组的最后一个元素不为空就会导致这里出发UB行为(死循环),导致当前模块加载失败,也就触发了前面模块加载失败的问题。
蓝图编辑器中节点属性的修改 在编辑器模式下修改节点的值:
执行的函数是UEdGraphSchema_K2::TrySetDefaultValue
(Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp ),是通过Schema
来调用的。简单来说就是通过当前的节点,去修改节点上的Pin的值,不管原始类型是什么,FString/int/float还是枚举,都可以通过这个方法设置。
UMG资料文档
文章:
Enum反射 UHT为Enum生成的代码 在UE中,当我们声明一个枚举类型时可以像UClass一样地形式为其添加UENUM
标记,指导UHT为其生成反射代码:
1 2 3 4 5 6 7 UENUM(BlueprintType) enum class ETypeName : uint8{ None, Int, Float };
经过UHT之后就变成了:
1 2 3 4 5 6 7 8 #define FOREACH_ENUM_ETYPENAME(op) \ op(ETypeName::None) \ op(ETypeName::Int) \ op(ETypeName::Float) enum class ETypeName : uint8;template <> TOPDOWNEXAMPLE_API UEnum* StaticEnum<ETypeName>();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 static UEnum* ETypeName_StaticEnum () { static UEnum* Singleton = nullptr ; if (!Singleton) { Singleton = GetStaticEnum(Z_Construct_UEnum_TopdownExample_ETypeName, Z_Construct_UPackage__Script_TopdownExample(), TEXT("ETypeName" )); } return Singleton; } template <> TOPDOWNEXAMPLE_API UEnum* StaticEnum<ETypeName>() { return ETypeName_StaticEnum(); } static FCompiledInDeferEnum Z_CompiledInDeferEnum_UEnum_ETypeName (ETypeName_StaticEnum, TEXT("/Script/TopdownExample" ), TEXT("ETypeName" ), false , nullptr , nullptr ) ; uint32 Get_Z_Construct_UEnum_TopdownExample_ETypeName_Hash () { return 2221805252U ; } UEnum* Z_Construct_UEnum_TopdownExample_ETypeName () {#if WITH_HOT_RELOAD UPackage* Outer = Z_Construct_UPackage__Script_TopdownExample(); static UEnum* ReturnEnum = FindExistingEnumIfHotReloadOrDynamic(Outer, TEXT("ETypeName" ), 0 , Get_Z_Construct_UEnum_TopdownExample_ETypeName_Hash(), false ); #else static UEnum* ReturnEnum = nullptr ; #endif if (!ReturnEnum) { static const UE4CodeGen_Private::FEnumeratorParam Enumerators[] = { { "ETypeName::None" , (int64)ETypeName::None }, { "ETypeName::Int" , (int64)ETypeName::Int }, { "ETypeName::Float" , (int64)ETypeName::Float }, }; #if WITH_METADATA const UE4CodeGen_Private::FMetaDataPairParam Enum_MetaDataParams[] = { { "BlueprintType" , "true" }, { "ModuleRelativePath" , "MyK2Node.h" }, }; #endif static const UE4CodeGen_Private::FEnumParams EnumParams = { (UObject*(*)())Z_Construct_UPackage__Script_TopdownExample, nullptr , "ETypeName" , "ETypeName" , Enumerators, ARRAY_COUNT(Enumerators), RF_Public|RF_Transient|RF_MarkAsNative, UE4CodeGen_Private::EDynamicType::NotDynamic, (uint8)UEnum::ECppForm::EnumClass, METADATA_PARAMS(Enum_MetaDataParams, ARRAY_COUNT(Enum_MetaDataParams)) }; UE4CodeGen_Private::ConstructUEnum(ReturnEnum, EnumParams); } return ReturnEnum; }
UEnum的构造思路和UClass差不多,通过UHT生成Enum的反射代码,记录枚举类型的名字、枚举值的名字、元数据等等,通过UE4CodeGen_Private::ConstructUEnum
把这些反射数据构造出UEnum。
同样也是通过延迟注册 的方式把UEnum构造出来。
运行时访问UEnum 如果要获取一个UENMU
的UEnum*可以通过StaticEnum<ETypeName>()
或者通过UEnum* const MethodEnum = FindObjectChecked<UEnum>(ANY_PACKAGE, TEXT("ETypeName"), true);
来拿。
在UE4.22+的版本中可以使用下列方法:
1 const UEnum* TypeEnum = StaticEnum<EnumType>();
在4.21及之前的版本就要麻烦一点:
1 const UEnum* TypeEnum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EnumType" ), true );
注意上面的TEXT("EnumType")
其中要填想要获取的枚举类型名字。
根据枚举名字获取枚举值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // get enum value by name { FString EnumTypeName = TEXT("ETargetPlatform"); FString EnumName = FString::Printf(TEXT("%s::%s"),*EnumTypeName,TEXT("Int")) UEnum* ETargetPlatformEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true); int32 EnumIndex = ETargetPlatformEnum->GetIndexByName(FName(*EnumName)); if (EnumIndex != INDEX_NONE) { UE_LOG(LogTemp, Log, TEXT("FOUND ENUM INDEX SUCCESS")); int32 EnumValue = ETargetPlatformEnum->GetValueByIndex(EnumIndex); ETargetPlatform CurrentEnum = (ETargetPlatform)EnumValue; } }
如果也想再封装一层模板类,让枚举名字也可以自动获取,则需要用得到C++的RTTI特性:
1 2 3 4 5 6 7 8 9 10 template <typename T>static std ::string GetCPPTypeName () { std ::string result; std ::string type_name = typeid (T).name(); std ::for_each(type_name.begin(),type_name.end(),[&result](const char & character){if (!std ::isdigit (character)) result.push_back(character);}); return result; }
枚举值与字符串的互相转换 有些需要序列化枚举值的需要,虽然我们可以通过FindObject<UEnum>
传入枚举名字拿到UEnum*
,再通过GetNameByValue
拿到名字,但是这样需要针对每个枚举都要单独写,我写了模板函数来做这个事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <typeinfo> #include <cctype> #include <algorithm> template <typename T>static std ::string GetCPPTypeName () { std ::string result; std ::string type_name = typeid (T).name(); std ::for_each(type_name.begin(),type_name.end(),[&result](const char & character){if (!std ::isdigit (character)) result.push_back(character);}); return result; } template <typename ENUM_TYPE>static FString GetEnumNameByValue (ENUM_TYPE InEnumValue, bool bFullName = false ) { FString result; { FString TypeName; FString ValueName; #if ENGINE_MINOR_VERSION > 21 UEnum* FoundEnum = StaticEnum<ENUM_TYPE>(); #else FString EnumTypeName = ANSI_TO_TCHAR(GetCPPTypeName<ENUM_TYPE>().c_str()); UEnum* FoundEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true ); #endif if (FoundEnum) { result = FoundEnum->GetNameByValue((int64)InEnumValue).ToString(); result.Split(TEXT("::" ), &TypeName, &ValueName, ESearchCase::CaseSensitive, ESearchDir::FromEnd); if (!bFullName) { result = ValueName; } } } return result; }
以及从字符串获取枚举值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 template <typename ENUM_TYPE>static bool GetEnumValueByName (const FString& InEnumValueName, ENUM_TYPE& OutEnumValue) { bool bStatus = false ; #if ENGINE_MINOR_VERSION >22 UEnum* FoundEnum = StaticEnum<ENUM_TYPE>(); FString EnumTypeName = FoundEnum->CppType; #else FString EnumTypeName = *GetCPPTypeName<ENUM_TYPE>(); UEnum* FoundEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true ); #endif if (FoundEnum) { FString EnumValueFullName = EnumTypeName + TEXT("::" ) + InEnumValueName; int32 EnumIndex = FoundEnum->GetIndexByName(FName(*EnumValueFullName)); if (EnumIndex != INDEX_NONE) { int32 EnumValue = FoundEnum->GetValueByIndex(EnumIndex); ENUM_TYPE ResultEnumValue = (ENUM_TYPE)EnumValue; OutEnumValue = ResultEnumValue; bStatus = false ; } } return bStatus; }
Struct反射 前面讲到了Class/function/property的反射,UE还支持结构体的反射,其实从C++的标准语义来说并没有区分“结构”和“类”,关键字struct
和class
的区别只在于默认的访问权限。
在UE里面,支持反射的结构提只能使用struct
,并且不能包含任何UFUNCTION的函数,命名必须以F开头。
1 2 3 4 5 6 7 8 9 10 USTRUCT(BlueprintType) struct FTestStruct { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere) int32 ival; UPROPERTY(EditAnywhere) UTexture2D* Texture; };
UE的标记语法和Class的类似,不过UHT为Struct生成的代码要简单许多,因为没有UFUNCTION也没有继承UObject。
GENERATED_USTRUCT_BODY
这个标记UHT会展开生成一个真正的C++宏(在genreated.h中):
1 2 3 4 5 6 #define HotPatcherExample_Source_HotPatcherExample_TestStruct_h_11_GENERATED_BODY \ friend struct Z_Construct_UScriptStruct_FTestStruct_Statics ; \ HOTPATCHEREXAMPLE_API static class UScriptStruct* StaticStruct () ; template <> HOTPATCHEREXAMPLE_API UScriptStruct* StaticStruct<struct FTestStruct >();
把UHT生成的用于记录当前struct反射数据的结构Z_Construct_UScriptStruct_FTestStruct_Statis
声明为当前struct类的友元,可以让它访问到自己的私有成员。而且还声明了当前Struct的static成员函数StaticStruct
和全局模板函数StaticStruct<FTestStruct>
。
对Struct生成的反射数;
据的结构与class的类似,只有Property的反射信息。
gen.cpp
中有以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class UScriptStruct* FTestStruct::StaticStruct () { static class UScriptStruct * Singleton = NULL ; if (!Singleton) { extern HOTPATCHEREXAMPLE_API uint32 Get_Z_Construct_UScriptStruct_FTestStruct_Hash () ; Singleton = GetStaticStruct(Z_Construct_UScriptStruct_FTestStruct, Z_Construct_UPackage__Script_HotPatcherExample(), TEXT("TestStruct" ), sizeof (FTestStruct), Get_Z_Construct_UScriptStruct_FTestStruct_Hash()); } return Singleton; } template <> HOTPATCHEREXAMPLE_API UScriptStruct* StaticStruct<FTestStruct>(){ return FTestStruct::StaticStruct(); } static FCompiledInDeferStruct Z_CompiledInDeferStruct_UScriptStruct_FTestStruct (FTestStruct::StaticStruct, TEXT("/Script/HotPatcherExample" ), TEXT("TestStruct" ), false , nullptr , nullptr ) ;static struct FScriptStruct_HotPatcherExample_StaticRegisterNativesFTestStruct { FScriptStruct_HotPatcherExample_StaticRegisterNativesFTestStruct() { UScriptStruct::DeferCppStructOps(FName(TEXT("TestStruct" )),new UScriptStruct::TCppStructOps<FTestStruct>); } } ScriptStruct_HotPatcherExample_StaticRegisterNativesFTestStruct; UScriptStruct* Z_Construct_UScriptStruct_FTestStruct () {#if WITH_HOT_RELOAD extern uint32 Get_Z_Construct_UScriptStruct_FTestStruct_Hash () ; UPackage* Outer = Z_Construct_UPackage__Script_HotPatcherExample(); static UScriptStruct* ReturnStruct = FindExistingStructIfHotReloadOrDynamic(Outer, TEXT("TestStruct" ), sizeof (FTestStruct), Get_Z_Construct_UScriptStruct_FTestStruct_Hash(), false ); #else static UScriptStruct* ReturnStruct = nullptr ; #endif if (!ReturnStruct) { UE4CodeGen_Private::ConstructUScriptStruct(ReturnStruct, Z_Construct_UScriptStruct_FTestStruct_Statics::ReturnStructParams); } return ReturnStruct; } uint32 Get_Z_Construct_UScriptStruct_FTestStruct_Hash () { return 4266809061U ; }
上面的代码包含了从UHT生成的反射信息中构造出UStructSctruct
以及延迟注册的方法,和Class的方式类似。
还包含生成的结构Z_Construct_UScriptStruct_STRUCTNAME_Statics
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 struct Z_Construct_UScriptStruct_FTestStruct_Statics { #if WITH_METADATA static const UE4CodeGen_Private::FMetaDataPairParam Struct_MetaDataParams[]; #endif static void * NewStructOps () ; #if WITH_METADATA static const UE4CodeGen_Private::FMetaDataPairParam NewProp_Texture_MetaData[]; #endif static const UE4CodeGen_Private::FObjectPropertyParams NewProp_Texture; #if WITH_METADATA static const UE4CodeGen_Private::FMetaDataPairParam NewProp_ival_MetaData[]; #endif static const UE4CodeGen_Private::FIntPropertyParams NewProp_ival; static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[]; static const UE4CodeGen_Private::FStructParams ReturnStructParams; }; #if WITH_METADATA const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FTestStruct_Statics::Struct_MetaDataParams[] = { { "BlueprintType" , "true" }, { "ModuleRelativePath" , "TestStruct.h" }, }; #endif void * Z_Construct_UScriptStruct_FTestStruct_Statics::NewStructOps () { return (UScriptStruct::ICppStructOps*)new UScriptStruct::TCppStructOps<FTestStruct>(); } #if WITH_METADATA const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture_MetaData[] = { { "Category" , "TestStruct" }, { "ModuleRelativePath" , "TestStruct.h" }, }; #endif const UE4CodeGen_Private::FObjectPropertyParams Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture = { "Texture" , nullptr , (EPropertyFlags)0x0010000000000001 , UE4CodeGen_Private::EPropertyGenFlags::Object, RF_Public|RF_Transient|RF_MarkAsNative, 1 , STRUCT_OFFSET(FTestStruct, Texture), Z_Construct_UClass_UTexture2D_NoRegister, METADATA_PARAMS(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture_MetaData, UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture_MetaData)) }; #if WITH_METADATA const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival_MetaData[] = { { "Category" , "TestStruct" }, { "ModuleRelativePath" , "TestStruct.h" }, }; #endif const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival = { "ival" , nullptr , (EPropertyFlags)0x0010000000000001 , UE4CodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, 1 , STRUCT_OFFSET(FTestStruct, ival), METADATA_PARAMS(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival_MetaData, UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival_MetaData)) }; const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UScriptStruct_FTestStruct_Statics::PropPointers[] = { (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_Texture, (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UScriptStruct_FTestStruct_Statics::NewProp_ival, }; const UE4CodeGen_Private::FStructParams Z_Construct_UScriptStruct_FTestStruct_Statics::ReturnStructParams = { (UObject* (*)())Z_Construct_UPackage__Script_HotPatcherExample, nullptr , &NewStructOps, "TestStruct" , sizeof (FTestStruct), alignof (FTestStruct), Z_Construct_UScriptStruct_FTestStruct_Statics::PropPointers, UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::PropPointers), RF_Public|RF_Transient|RF_MarkAsNative, EStructFlags(0x00000001 ), METADATA_PARAMS(Z_Construct_UScriptStruct_FTestStruct_Statics::Struct_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FTestStruct_Statics::Struct_MetaDataParams)) };
可以看到Struct的每个属性也是通过FPropertyParamsBase
来存储的,与Class一致。
区别在于,Struct使用UE4CodeGen_Private::FStructParams
来存储当前结构的反射信息,其声明如下(CoreUObject/Public/UObject/UObjectGlobals.h ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct FStructParams { UObject* (*OuterFunc)(); UScriptStruct* (*SuperFunc)(); void * (*StructOpsFunc)(); const char * NameUTF8; SIZE_T SizeOf; SIZE_T AlignOf; const FPropertyParamsBase* const * PropertyArray; int32 NumProperties; EObjectFlags ObjectFlags; uint32 StructFlags; #if WITH_METADATA const FMetaDataPairParam* MetaDataArray; int32 NumMetaData; #endif };
这个结构中比较特殊的一点是StructOpsFunc
是一个函数指针,用来管理C++结构的构造和析构,使用的也是placement-new的方式,TCppStructOps<>
模板定义在CoreUObject/Public/UObject/Class.h 。
类反射 UHT为类产生的反射信息 当在UE中新建一个类并继承自UObject
时,可以在类声明的上一行添加UCLASS
标记,当执行编译的时候UBT会调用UHT来根据标记来生成C++代码(不过非UCLASS的类也可以用宏来生成反射信息)。
UHT为类生成的代码为:
为所有的UFUNCTION的函数创建FName,命名规则为NAME_CLASSNAME_FUNCTIONNAME
,如NAME_AMyActor_TestFunc
BlueprintNativeEvent和BlueprintImplementEvent创建同名函数实现,并通过ProcessEvent
转发调用
为所有加了UFUNCTION的函数生成Thunk函数,为当前类的static函数,原型为static void execFUNCNAME( UObject* Context, FFrame& Stack, RESULT_DECL)
;
创建当前类的StaticRegisterNatives*
函数,并把上一步提到的exec
这样的thunk函数通过Name-execFunc指针
的形式通过FNativeFunctionRegistrar::RegisterFunctions
注册到UClass;
创建出Z_Construct_UClass_CLASSNAME_NoRegister
函数,返回值是CLASSNAME::StaticClass()
创建出Z_Construct_UClass_CLASSNAME_Statics
类(GENERATED_BODY等宏会把该类添加为我们创建类的友元,使其可以访问私有成员,用于获取成员指针)
Z_Construct_UClass_CLASSNAME_Statics
类的结构为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 UCLASS(BlueprintType) class XXXX_API AMyActor :public AActor{ GENERATED_BODY() UPROPERTY() int32 ival; UFUNCTION() int32 GetIval () ; UFUNCTION() void TESTFUNC () ; }; struct Z_Construct_UClass_AMyActor_Statics { static UObject* (*const DependentSingletons[])(); static const FClassFunctionLinkInfo FuncInfo[]; #if WITH_METADATA static const UE4CodeGen_Private::FMetaDataPairParam NewProp_ival_MetaData[]; #endif static const UE4CodeGen_Private::FIntPropertyParams NewProp_ival; static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[]; static const UE4CodeGen_Private::FImplementedInterfaceParams InterfaceParams[]; static const FCppClassTypeInfoStatic StaticCppClassTypeInfo; static const UE4CodeGen_Private::FClassParams ClassParams; }; UObject* (*const Z_Construct_UClass_AMyActor_Statics::DependentSingletons[])() = { (UObject* (*)())Z_Construct_UClass_AActor, (UObject* (*)())Z_Construct_UPackage__Script_MicroEnd_423, }; const FClassFunctionLinkInfo Z_Construct_UClass_AMyActor_Statics::FuncInfo[] = { { &Z_Construct_UFunction_AMyActor_GetIval, "GetIval" }, { &Z_Construct_UFunction_AMyActor_TESTFUNC, "TESTFUNC" }, };
该类中的成员为:
static UObject* (*const DependentSingletons[])();
记录当前类基类的Z_Construct_UClass_BASECLASSNAME
函数指针,用它可以构造出基类的UClass,还记录了当前类属于哪个Package的函数指针Z_Construct_UPackage__Script_MODULENAME
。
Z_Construct_UPackage__Script_MODULENAME
函数是定义在MODULE_NAME.init.gen.cpp
里。
static const UE4CodeGen_Private::FMetaDataPairParam Class_MetaDataParams[];
用于记录UCLASS的元数据,如BlueprintType标记
反射属性的F*PropertyParams
以及其Metadata,均为static成员
static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];
,数组,用于存储当前类所有的反射属性的信息(是个指针数组,用于存储5.3中的static成员的地址)
static const UE4CodeGen_Private::FImplementedInterfaceParams InterfaceParams[];
,数组,用于存储当前类所有的接口信息
1 2 3 const UE4CodeGen_Private::FImplementedInterfaceParams Z_Construct_UClass_AMyActor_Statics::InterfaceParams[] = { { Z_Construct_UClass_UUnLuaInterface_NoRegister, (int32)VTABLE_OFFSET(AMyActor, IUnLuaInterface), false }, };
static const FCppClassTypeInfoStatic StaticCppClassTypeInfo;
用于类型萃取,记录当前类是否是抽象类。
1 2 3 const FCppClassTypeInfoStatic Z_Construct_UClass_AMyActor_Statics::StaticCppClassTypeInfo = { TCppClassTypeTraits<AMyActor>::IsAbstract, };
static const UE4CodeGen_Private::FClassParams ClassParams;
构造出UClass需要的所有反射数据,统一记录上面所有生成的反射信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const UE4CodeGen_Private::FClassParams Z_Construct_UClass_AMyActor_Statics::ClassParams = { &AMyActor::StaticClass, nullptr , &StaticCppClassTypeInfo, DependentSingletons, FuncInfo, Z_Construct_UClass_AMyActor_Statics::PropPointers, InterfaceParams, ARRAY_COUNT(DependentSingletons), ARRAY_COUNT(FuncInfo), ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::PropPointers), ARRAY_COUNT(InterfaceParams), 0x009000A0 u, METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::Class_MetaDataParams, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::Class_MetaDataParams)) };
全局函数Z_Construct_UClass_AMyActor
通过ClassParams
构造出真正的UClass对象。
使用IMPLEMENT_CLASS 注册当前类到GetDeferredClassRegistration() ,如果在WITH_HOT_RELOAD
为true的情况下也会注册到GetDeferRegisterClassMap() 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #define IMPLEMENT_CLASS(TClass, TClassCrc) \ static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof (TClass), TClassCrc); \ UClass* TClass::GetPrivateStaticClass() \ { \ static UClass* PrivateStaticClass = NULL ; \ if (!PrivateStaticClass) \ { \ \ GetPrivateStaticClassBody( \ StaticPackage(), \ (TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0 ), \ PrivateStaticClass, \ StaticRegisterNatives##TClass, \ sizeof (TClass), \ alignof (TClass), \ (EClassFlags)TClass::StaticClassFlags, \ TClass::StaticClassCastFlags(), \ TClass::StaticConfigName(), \ (UClass::ClassConstructorType)InternalConstructor<TClass>, \ (UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \ &TClass::AddReferencedObjects, \ &TClass::Super::StaticClass, \ &TClass::WithinClass::StaticClass \ ); \ } \ return PrivateStaticClass; \ }
如AMyActor
的IMPLEMENT_CLASS(AMyActor,3240835608)
经过预处理之后为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor (TEXT("AMyActor" ), sizeof (AMyActor), 3240835608 ) ;UClass * AMyActor::GetPrivateStaticClass () { static UClass * PrivateStaticClass = NULL ; if (!PrivateStaticClass) { GetPrivateStaticClassBody( StaticPackage(), (TCHAR*)TEXT("AMyActor" ) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0 ), PrivateStaticClass, StaticRegisterNativesAMyActor, sizeof (AMyActor), alignof (AMyActor), (EClassFlags)AMyActor::StaticClassFlags, AMyActor::StaticClassCastFlags(), AMyActor::StaticConfigName(), (UClass::ClassConstructorType)InternalConstructor<AMyActor>, (UClass::ClassVTableHelperCtorCallerType) InternalVTableHelperCtorCaller<AMyActor>, &AMyActor::AddReferencedObjects, &AMyActor::Super::StaticClass, &AMyActor::WithinClass::StaticClass ); } return PrivateStaticClass; };
其中TClassCompiledInDefer<TClass>
这个模板类的构造函数中通过调用UClassCompiledInDefer 将当前反射类的注册到GetDeferredClassRegistration() ,它得到的是一个类型为FFieldCompiledInInfo*
的数组,用于记录引擎中所有反射类的信息,用于在CoreUObjectModule启动时将UHT生成的这些反射信息在ProcessNewlyLoadedUObjects 函数中通过UClassRegisterAllCompiledInClasses 将所有反射类的UClass构造出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void UClassRegisterAllCompiledInClasses () {#if WITH_HOT_RELOAD TArray<UClass*> AddedClasses; #endif SCOPED_BOOT_TIMING("UClassRegisterAllCompiledInClasses" ); TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration(); for (const FFieldCompiledInInfo* Class : DeferredClassRegistration) { UClass* RegisteredClass = Class->Register(); #if WITH_HOT_RELOAD if (GIsHotReload && Class->OldClass == nullptr ) { AddedClasses.Add(RegisteredClass); } #endif } DeferredClassRegistration.Empty(); #if WITH_HOT_RELOAD if (AddedClasses.Num() > 0 ) { FCoreUObjectDelegates::RegisterHotReloadAddedClassesDelegate.Broadcast(AddedClasses); } #endif }
而TClassCompiledInDefer<AMyActor>
的Register
函数就是调用AMyActor::StaticClass
的,然后StaticClass中调用GetPrivateStaticClass
,其中有一个static对象,就是当前类的UClass,所以它只会构造依次,使用UXXXX::StaticClass
都是直接获得。
注意:UClass的构造是跟着模块的启动创建的,所以之后当引擎启动到一个模块的时候它的UClass才被创建出来)。
非UCLASS的反射 有些继承自UObject的类是没有加UCLASS标记的,所以也不会包含gen.cpp
和generated.h
文件,但是UE也提供了非UCLASS的反射方法,类似于UTextureBuffer 这个类。
在类内时添加DECLARE_CASTED_CLASS_INTRINSIC_WITH_API 宏用于手动添加,实现类似UHT生成GENERATED_BODY
宏的操作:
1 2 3 4 5 6 class UTextBuffer : public UObject , public FOutputDevice { DECLARE_CASTED_CLASS_INTRINSIC_WITH_API(UTextBuffer, UObject, 0 , TEXT("/Script/CoreUObject" ), CASTCLASS_None, COREUOBJECT_API) }
DECLARE_CASTED_CLASS_INTRINSIC_WITH_API 可以处理类似generated.h
的行为,但是gen.cpp
里创建出static TClassCompiledInDefer<CLASS_NAME>
的代码还没有,UE提供了另一个宏:
1 IMPLEMENT_CORE_INTRINSIC_CLASS(UTextBuffer, UObject, { });
虽然和gen.cpp
里通过UHT生成的代码不同,但是统一使用TClassCompiledInDefer<TClass>
和FCompiledInDefer
来注册到引擎中。 这样就实现了可以不使用UCLASS标记也可以为继承自UObject的类生成反射信息。
UClass的构造思路 前面讲了这么多都是在分析UE创建UClass的代码,我想从UE的实现思路上分析一下设计过程。
首先UHT通过分析代码创建出gen.cpp和generated.h中间记录着当前类的反射信息、类本身的反射信息、类中函数的反射信息、类数据成员的反射信息。
当前类的反射信息(类、成员函数、数据成员)等被统一存储在一个名为Z_Construct_UClass_CLASSNAME_Statics
的结构中;
该结构通过IMPLEMENT_CLASS
生成的代码将当前类添加到GetDeferredClassRegistration() 中。因为全局作用域static对象的构造时机 是先于主函数的第一条语句的,所以当进入引擎逻辑的时候,引擎内置的模块中类的TClassCompiledInDefer<>
都已经被创建完毕,在编辑器模式下, 因为不同的模块都是编译为DLL的,所以在加载模块的时候它们的static对象才会被创建。
1 2 static TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor (TEXT("AMyActor" ), sizeof (AMyActor), 3240835608 ) ;
与上一步同样的手法,把类生成的反射信息通过FCompiledInDefer
收集到GetDeferredCompiledInRegistration()
1 static FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor (Z_Construct_UClass_AMyActor, &AMyActor::StaticClass, TEXT("/Script/MicroEnd_423" ), TEXT("AMyActor" ), false , nullptr , nullptr , nullptr ) ;
引擎如何使用生成的反射信息 在UHT生成反射的代码之后,引擎会根据这些代码生成UClass、UStruct、UEnum、UFunction和UProperty等。
它们都是在ProcessNewlyLoadedUObjects
中被执行的,注意 该函数会进来很多次,当每一个模块被加载的时候都会走一遍,因为在Obj.cpp 的InitUObject 函数中,把函数ProcessNewlyLoadedUObjects 添加到了FModuleManager::Get().OnProcessLoadedObjectsCallback()
中:
1 2 3 #if !USE_PER_MODULE_UOBJECT_BOOTSTRAP FModuleManager::Get().OnProcessLoadedObjectsCallback().AddStatic(ProcessNewlyLoadedUObjects); #endif
之所以 要这么做,是因为UE的Module中都会有很多的反射类,但引擎一启动并不是所有的类在同一时刻都被加载了,因为模块有不同的加载时机,所以引擎中对于UClass的构造也不是一个一次性过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void ProcessNewlyLoadedUObjects () { LLM_SCOPE(ELLMTag::UObject); DECLARE_SCOPE_CYCLE_COUNTER(TEXT("ProcessNewlyLoadedUObjects" ), STAT_ProcessNewlyLoadedUObjects, STATGROUP_ObjectVerbose); UClassRegisterAllCompiledInClasses(); const TArray<UClass* (*)()>& DeferredCompiledInRegistration = GetDeferredCompiledInRegistration(); const TArray<FPendingStructRegistrant>& DeferredCompiledInStructRegistration = GetDeferredCompiledInStructRegistration(); const TArray<FPendingEnumRegistrant>& DeferredCompiledInEnumRegistration = GetDeferredCompiledInEnumRegistration(); bool bNewUObjects = false ; while ( GFirstPendingRegistrant || DeferredCompiledInRegistration.Num() || DeferredCompiledInStructRegistration.Num() || DeferredCompiledInEnumRegistration.Num() ) { bNewUObjects = true ; UObjectProcessRegistrants(); UObjectLoadAllCompiledInStructs(); UObjectLoadAllCompiledInDefaultProperties(); } #if WITH_HOT_RELOAD UClassReplaceHotReloadClasses(); #endif if (bNewUObjects && !GIsInitialLoad) { UClass::AssembleReferenceTokenStreams(); } }
UClass构造的调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void UClassRegisterAllCompiledInClasses () {#if WITH_HOT_RELOAD TArray<UClass*> AddedClasses; #endif TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration(); for (const FFieldCompiledInInfo* Class : DeferredClassRegistration) { UClass* RegisteredClass = Class->Register(); #if WITH_HOT_RELOAD if (GIsHotReload && Class->OldClass == nullptr ) { AddedClasses.Add(RegisteredClass); } #endif } DeferredClassRegistration.Empty(); #if WITH_HOT_RELOAD if (AddedClasses.Num() > 0 ) { FCoreUObjectDelegates::RegisterHotReloadAddedClassesDelegate.Broadcast(AddedClasses); } #endif }
可以看到,在UClassRegisterAllCompiledInClasses
只是去调用了每个反射类的StaticClass函数(Class->Register()
内部是对类型的StaticClass
的转发调用),在开启WITH_HOT_RELOAD
的情况下也会把新的UClass给代理调用传递出去,然后把当前的数组置空。
之所以要置空,就是因为前面说的,UE的UClass构造是一个模块一个模块来执行的,当一个模块执行完毕之后就把当前模块注册到GetDeferredClassRegistration()
里的元素置空,等着下个模块启动的时候(加载DLL时它们的static成员会构造然后注册到里面),再执行LoadModuleWithFailureReason
就是又一遍循环。
在模块启动的时候会执行LoadModuleWithFailureReason
里面调用了这个Delegate,所以每一个模块启动的时候都会执行ProcessNewlyLoadedUObjects
,把自己当前模块中的UClass/UStruct/UEnum都构造出来。
函数反射 UHT生成的反射信息 在UE的代码中加了UFUNCTION()
修饰后UHT就会为该函数生成反射代码。
每一个支持反射的函数UHT都会给它生成一个类和一个函数: 如在AMyActor
这个类下有一个ReflexFunc
的函数:
1 2 3 4 5 UFUNCTION() bool ReflexFunc (int32 InIval, UObject* InObj) { return false ; }
UHT会生成这样命名规则的一个类和函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 struct Z_Construct_UFunction_AMyActor_ReflexFunc_Statics { struct MyActor_eventReflexFunc_Parms { int32 InIval; UObject* InObj; bool ReturnValue; }; static void NewProp_ReturnValue_SetBit (void * Obj) ; static const UE4CodeGen_Private::FBoolPropertyParams NewProp_ReturnValue; static const UE4CodeGen_Private::FObjectPropertyParams NewProp_InObj; static const UE4CodeGen_Private::FIntPropertyParams NewProp_InIval; static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[]; #if WITH_METADATA static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[]; #endif static const UE4CodeGen_Private::FFunctionParams FuncParams; }; void Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue_SetBit (void * Obj) { ((MyActor_eventReflexFunc_Parms*)Obj)->ReturnValue = 1 ; } const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue = { "ReturnValue" , nullptr , (EPropertyFlags)0x0010000000000580 , UE4CodeGen_Private::EPropertyGenFlags::Bool | UE4CodeGen_Private::EPropertyGenFlags::NativeBool, RF_Public|RF_Transient|RF_MarkAsNative, 1 , sizeof (bool ), sizeof (MyActor_eventReflexFunc_Parms), &Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue_SetBit, METADATA_PARAMS(nullptr , 0 ) }; const UE4CodeGen_Private::FObjectPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InObj = { "InObj" , nullptr , (EPropertyFlags)0x0010000000000080 , UE4CodeGen_Private::EPropertyGenFlags::Object, RF_Public|RF_Transient|RF_MarkAsNative, 1 , STRUCT_OFFSET(MyActor_eventReflexFunc_Parms, InObj), Z_Construct_UClass_UObject_NoRegister, METADATA_PARAMS(nullptr , 0 ) }; const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InIval = { "InIval" , nullptr , (EPropertyFlags)0x0010000000000080 , UE4CodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, 1 , STRUCT_OFFSET(MyActor_eventReflexFunc_Parms, InIval), METADATA_PARAMS(nullptr , 0 ) }; const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers[] = { (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_ReturnValue, (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InObj, (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::NewProp_InIval, }; #if WITH_METADATA const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams[] = { { "ModuleRelativePath" , "MyActor.h" }, }; #endif const UE4CodeGen_Private::FFunctionParams Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::FuncParams = { (UObject*(*)())Z_Construct_UClass_AMyActor, nullptr , "ReflexFunc" , nullptr , nullptr , sizeof (MyActor_eventReflexFunc_Parms), Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers, UE_ARRAY_COUNT(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::PropPointers), RF_Public|RF_Transient|RF_MarkAsNative, (EFunctionFlags)0x00020401 , 0 , 0 , METADATA_PARAMS(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::Function_MetaDataParams)) }; UFunction* Z_Construct_UFunction_AMyActor_ReflexFunc () { static UFunction* ReturnFunction = nullptr ; if (!ReturnFunction) { UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AMyActor_ReflexFunc_Statics::FuncParams); } return ReturnFunction; }
定义的Z_Construct_UFunction_AMyActor_ReflexFunc_Statics
类中包含了以下信息:
存储函数的参数、返回值的结构体(POD),注意该结构的声明顺序是按照函数参数的顺序+最后一个成员是函数返回值的方式排列。
函数参数、返回值的F*PropertyParams
,用来给函数的每个参数以及返回值生成反射信息,用于构造出UProperty,static成员;
成员static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];
,数组,用于记录该函数的参数和返回值的类型为FPropertyParamsBase
的static数据成员的地址。
成员static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[];
,用于记录函数的元数据。如所属文件、Category、注释等等。
成员static const UE4CodeGen_Private::FFunctionParams FuncParams;
用于记录当前函数的名字、Flag、参数的F*PropertyParams
、参数数量,参数的结构大小等等,用于通过它来创建出UFunction*
。
至于生成的SetBit
d的函数的作用,在上面属性反射的部分已经讲到了。
UE4CodeGen_Private::FFunctionParams
结构声明为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct FFunctionParams { UObject* (*OuterFunc)(); UFunction* (*SuperFunc)(); const char * NameUTF8; const char * OwningClassName; const char * DelegateName; SIZE_T StructureSize; const FPropertyParamsBase* const * PropertyArray; int32 NumProperties; EObjectFlags ObjectFlags; EFunctionFlags FunctionFlags; uint16 RPCId; uint16 RPCResponseId; #if WITH_METADATA const FMetaDataPairParam* MetaDataArray; int32 NumMetaData; #endif };
UHT生成的Z_Construct_UFunction_AMyActor_ReflexFunc
函数做了以下事情:
1 2 3 4 5 6 7 8 9 UFunction* Z_Construct_UFunction_AMyActor_ReflexFunc () { static UFunction* ReturnFunction = nullptr ; if (!ReturnFunction) { UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AMyActor_Add_Statics::FuncParams); } return ReturnFunction; }
根据定义的Z_Construct_UFunction_AMyActor_ReflexFunc_Statics
结构中的FuncParams
成员来创建出真正的UFunction
对象。
最后,Z_Construct_UFunction_AMyActor_ReflexFunc
这个函数会被注册到当前类反射数据的FuncInfo
中。
Thunk函数 UE会为标记为UFUNCTION
的Native函数生成对应的Thunk函数(BlueprintImplementableEvent
的函数不会)。如下列函数:
1 2 3 4 5 UFUNCTION() bool ReflexFunc (int32 InIval, UObject* InObj) { return false ; }
生成的Thunk函数形式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 DECLARE_FUNCTION(execReflexFunc); DEFINE_FUNCTION(AMyActor::execReflexFunc) { P_GET_PROPERTY(FIntProperty,Z_Param_InIval); P_GET_OBJECT(UObject,Z_Param_InObj); P_FINISH; P_NATIVE_BEGIN; *(bool *)Z_Param__Result=P_THIS->ReflexFunc(Z_Param_InIval,Z_Param_InObj); P_NATIVE_END; }
DECLARE_FUNCTION
/DEFINE_FUNCTION
这两个宏是定义在CoreUObject/Public/UObject/ObjectMacros.h 中的:
1 2 3 4 5 #define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL ) #define DEFINE_FUNCTION(func) void func( UObject* Context, FFrame& Stack, RESULT_DECL )
展开这两个宏:
1 2 3 4 5 6 7 8 9 10 11 12 static void AMyActor::execReflexFunc ( UObject* Context, FFrame& Stack, RESULT_DECL ) ;void AMyActor::execReflexFunc ( UObject* Context, FFrame& Stack, RESULT_DECL ) { P_GET_PROPERTY(FIntProperty,Z_Param_InIval); P_GET_OBJECT(UObject,Z_Param_InObj); P_FINISH; P_NATIVE_BEGIN; *(bool *)Z_Param__Result=P_THIS->ReflexFunc(Z_Param_InIval,Z_Param_InObj); P_NATIVE_END; }
可以看到,UHT为每个反射函数生成的都是一个参数一致的static成员函数,接收通用的参数,就可以用来处理所有的函数调用。
Thunk函数中用到的这些宏:
RESULT_DECL(CoreUObject/Public/UObject/Script.h ):
1 2 3 4 5 #define RESULT_PARAM Z_Param__Result #define RESULT_DECL void*const RESULT_PARAM
其他的形如P_GET_PROPERTY
之类的宏,都是定义在CoreUObject/Public/UObject/ScriptMacros.h 文件中的,作用就是从栈上操作参数(因为Thunk函数是通用的参数,所以要从通用的参数中获取到每个函数具体的参数,UE提供这些宏来做这些事情)。
这些Thunk函数通过UHT生成的StaticRegisterNatives*
函数注册到UClass中(在GetPrivateStaticClass
把该函数指针传递了进去):
1 2 3 4 5 6 7 8 9 void AMyActor::StaticRegisterNativesAMyActor () { UClass* Class = AMyActor::StaticClass(); static const FNameNativePtrPair Funcs[] = { { "BPNativeEvent" , &AMyActor::execBPNativeEvent }, { "ReflexFunc" , &AMyActor::execReflexFunc }, }; FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, UE_ARRAY_COUNT(Funcs)); }
UE不会为BlueprintImplementableEvent
生成Thunk函数,但是会为它生成函数的反射信息,所以也可以通过反射的信息来调用BlueprintImplementableEvent
的函数。
因为`BlueprintImplementatableEventd的函数是C++提供原型不提供实现,让蓝图来进行覆写的,所以它不使用Thunk的形式调用(应该执行字节码的方式,这个暂时还没看到,有时间再来分析)。
Custom Thunk 前面讲到当给函数加了UFUNCTION
标记时UHT会给我们生成对应的Thunk函数,但是有些情况下需要我们自己来写Thunk的函数,如UKismetArrayLibrary 中的对Array进行操作的函数,或者UDataTableFunctionLibrary 中的GetDataTableRowFromName
函数。
UE提供了让我们自己实现Thunk函数的方法,在UFUNCTION
中添加CustomThunk
标记:
1 2 UFUNCTION(CustomThunk) bool ReflexFunc (int32 InIval, UObject* InObj)
这样UHT就不会为这个函数生成出它的Thunk函数,这种情况下就需要自己提供了。自己写的方式和UHT生成的代码一样,可以使用DECLARE_FUNCTION
或者DEFINE_FUNCTION
(手动写按照Thunk函数的签名规则也是没问题的)
1 2 3 4 DECLARE_FUNCTION(execReflexFunc) { }
运行时访问反射函数 1 2 3 4 5 6 7 8 9 10 11 12 for (TFieldIterator<UFunction> It(InActor->GetClass()); It; ++It){ UFunction* FuncProperty = *It; if (FuncProperty->GetName() == TEXT("GetIval" )) { struct CallParam { int32 ival; }CallParamIns; InActor->ProcessEvent(FuncProperty, &CallParamIns); } }
通过ProcessEvent来调用,第二个参数传递进去参数和返回值的结构。
每个被反射的函数UHT都会给它生成一个参数的结构体 ,其排列的顺序为:函数参数依次排列,最后一个成员为返回值。如:
1 2 UFUNCTION() int32 Add (int32 R, int32 L) ;
UHT为其生成的参数结构为:
1 2 3 4 5 6 struct MyActor_eventAdd_Parms { int32 R; int32 L; int32 ReturnValue; };
在通过UFunction*来调用函数时,需要把这个布局的结构传递作为传递给函数的参数以及接收返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for (TFieldIterator<UFunction> It(InActor->GetClass()); It; ++It){ UFunction* Property = *It; if (Property->GetName() == TEXT("Add" )) { struct AddFuncCallParam { int32 R; int32 L; int32 RetValue; }CallParamIns; CallParamIns.R = 123 ; CallParamIns.L = 456 ; InActor->ProcessEvent(Property, &CallParamIns); UE_LOG(LogTemp, Log, TEXT("UFunction:%s value:%d" ), *Property->GetName(), CallParamIns.RetValue); } }
可以通过UFunction
拿到当前函数的参数的结构的大小UFunction::ParmsSize
,在运行时动态访问的话可以通过这个结构大小分配出一块内存,然后用UProperty对这块内存进行访问,因为通过UProperty访问成员其实本质上也是通过该成员在类内的偏移来做的(对数据成员获取成员指针得到的是一个相对于对象基址的偏移值)。
坑点 注意:通过UE的UFunction调用并不能正确地处理引用类型,如:
1 2 UFUNCTION() int32& Add (int32 R, int32& L) ;
这个函数生成的反射代码和非引用的一摸一样(对L参数生成的UProperty的Flag会多一个CPF_OutParm
,返回值的UProperty还具有CPF_ReturnParm
)。 这会造成通过UFunction*
调用传递的参数和想要获取的返回值都只是一份拷贝(因为本来调用时的参数传递到ProcessEvent之前都会被赋值到UHT创建出来的参数结构),不能再后续的流程中对得到的结果进行赋值。
而且 ,通过遍历UFunction得到的参数和返回值UProperty
,其中的Offset值是相对于UHT生成的参数结构。
在获取蓝图或者C++中具有多个返回值UFunction的时候,因为UE的具有多个返回值的机制是通过传递进来引用实现的,所以不能够只是通过检测UProperty是否具有CPF_ReturnValue
来检测,因为包含该flag的UProperty只有一个,还需要检测CPF_OutParam
来判断是都是通过引用方式传递的“返回值”。
属性反射 当在UCLASS类中给一个属性添加UPROPERTY
标记时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 UCLASS() class MICROEND_423_API AMyActor : public AActor{ GENERATED_BODY() protected : virtual void BeginPlay () override ; public : virtual void Tick (float DeltaTime) override ; UPROPERTY(EditAnywhere) int32 ival; };
会生成反射代码,生成反射代码的代码是在UHT中的Programs/UnrealHeaderTool/Private/CodeGenerator.cpp :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #if WITH_METADATA const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = { { "Category" , "MyActor" }, { "ModuleRelativePath" , "Public/MyActor.h" }, }; #endif const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = { "ival" , nullptr , (EPropertyFlags) 0x0010000000000001 , UE4CodeGen_Private::EPropertyGenFlags::Int, RF_Public | RF_Transient | RF_MarkAsNative, 1 , STRUCT_OFFSET(AMyActor, ival), METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData)) };
首先const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival
是创建出一个结构,用于记录当前属性的信息,比如变量名、相对于对象起始地址的偏移。
注意UE4CodeGen_Private::FIntPropertyParams
这样的类型都是定义在UObject/UObjectGlobals.h 中的,对于基础类型的属性(如int8/int32/float/array/map)等使用的都是FGenericPropertyParams
。
F*PropertyParams 在UObject/UObjectGlobals.h 文件中,引擎里定义了很多的F*PropertyParams
,但是他们的数据结构基本相同(但是他们并没有继承关系),都是POD的类型,每个数据依次排列,而且不同类型的Params尽量都保持了一致的顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct FGenericPropertyParams // : FPropertyParamsBaseWithOffset{ const char * NameUTF8; const char * RepNotifyFuncUTF8; EPropertyFlags PropertyFlags; EPropertyGenFlags Flags; EObjectFlags ObjectFlags; int32 ArrayDim; int32 Offset; #if WITH_METADATA const FMetaDataPairParam* Z_Construct_UClass_; int32 NumMetaData; #endif };
一个一个来分析它的参数。
NameUTF8 NameUTF8是属性的UTF8的名字,在运行时可以通过UProperty
的GetNameCPP
来获取。
RepNotifyFuncUTF8 RepNotifyFuncUTF8是在当前属性绑定的修改之后的函数名字。
如果属性是这样声明:
1 2 3 4 UPROPERTY(EditAnywhere,ReplicatedUsing=OnRep_Replicatedival) int32 ival; UFUNCTION() virtual void OnRep_Replicatedival () {}
这样生成的反射代码就为:
1 2 3 4 5 6 7 8 9 10 const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = { "ival" , "OnRep_Replicatedival" , (EPropertyFlags)0x0010000100000021 , UE4CodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, 1 , STRUCT_OFFSET(AMyActor, ival), METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData,ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData)) };
因为OnRep_Replicatedival
也是UFUNCTION的函数,所以可以通过反射来访问。
PropertyFlags PropertyFlags
是一个类型为EPropertyFlags 的枚举,枚举值按照位排列,根据在UPROPERTY中的标记来按照位或来记录当前属性包含的标记信息。 在UnrealHeaderTool/Private/CodeGenerator.cpp 中生成代码时会根据当前属性的flag和CPF_ComputedFlags 做位运算:
1 2 3 4 5 6 7 8 9 10 EPropertyFlags PropFlags = Prop->PropertyFlags & ~CPF_ComputedFlags;
在运行时可以通过UProperty
的HasAnyPropertyFlags
函数来检测是否具有特定的flag。
一般情况下,可以通过UProperty
来获取到该参数的标记属性,比如当通过UFunction
获取函数的参数时,可以区分哪个UProperty是输入参数、哪个是返回值。
Flags 第四个参数Flags
是类型为UE4CodeGen_Private::EPropertyGenFlags
是一个枚举,定义在UObject/UObjectGlobals.h 中,标记了当前Property的类型。
ObjectFlags ObjectFlags是EObjectFlags
类型的枚举,其枚举值也是按照位来划分的。定义在CoreUObject/Public/UObject/ObjectMacros.h
UHT生成这部分代码在Programs/UnrealHeaderTool/Private/CodeGenerator.cpp 中:
1 const TCHAR* FPropertyObjectFlags = FClass::IsOwnedByDynamicType(Prop) ? TEXT("RF_Public|RF_Transient" ) : TEXT("RF_Public|RF_Transient|RF_MarkAsNative" );
通过FClass::IsOwnedByDynamicType
函数来检测是否为PROPERTY添加RF_MarkAsNative
的flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 template <typename T>static bool IsDynamic (const T* Field) { return Field->HasMetaData(NAME_ReplaceConverted); } bool FClass::IsOwnedByDynamicType (const UField* Field) { for (const UField* OuterField = Cast<const UField>(Field->GetOuter()); OuterField; OuterField = Cast<const UField>(OuterField->GetOuter())) { if (IsDynamic(OuterField)) { return true ; } } return false ; } bool FClass::IsOwnedByDynamicType (const FField* Field) { for (FFieldVariant Owner = Field->GetOwnerVariant(); Owner.IsValid(); Owner = Owner.GetOwnerVariant()) { if (Owner.IsUObject()) { return IsOwnedByDynamicType(Cast<const UField>(Owner.ToUObject())); } else if (IsDynamic(Owner.ToField())) { return true ; } } return false ; }
通过调用UField
的HasMetaData
检测是否具有TEXT("ReplaceConverted")
的元数据(该元数据就是后面要讲到的Metadata)。
ArrayDim ArrayDim用于记录当前属性的元素数量,当只是声明一个单个对象时,如:
1 2 3 4 5 6 7 8 9 10 11 12 UPROPERTY() float fval; UPROPERTY(EditAnywhere) FString StrVal = TEXT("123456" ); UPROPERTY(EditAnywhere) TSubclassOf<UObject> ClassVal; UPROPERTY(EditAnywhere) UTexture2D* Texture2D; UPROPERTY() FResultDyDlg ResultDlg; UPROPERTY() UMySceneComponent* SceneComp;
这些属性所有的ArrayDim
均为1.
但是当使用C++原生数组时:
1 2 UPROPERTY() int32 iArray[12 ];
它的ArrayDim就为:
1 CPP_ARRAY_DIM(iArray, AMyActor)
CPP_ARRAY_DIM 这个宏定义在CoreUObject/Public/UObject/UnrealType.h :
1 2 3 #define CPP_ARRAY_DIM(ArrayName, ClassName) \ (sizeof (((ClassName*)0 )->ArrayName) / sizeof (((ClassName*)0 )->ArrayName[0 ]))
就是用来计算数组内的元素数量的,普通的非数组属性其值为1,可以当作是元素数量为1的数组。
Offset STRUCT_OFFSET
宏的作用为得到数据成员相对于类起始地址的偏移,通过获取数据成员指针 得到,类型为size_t
。
然后当前类中的反射属性都会被添加到PropPointers
中,也是UHT生成的代码:
1 2 3 const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_AMyActor_Statics::PropPointers[] = { (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_AMyActor_Statics::NewProp_ival, };
在运行时可以通过UProperty
得到指定的对象值:
1 2 3 4 5 6 7 8 9 for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It) { UProperty* Property = *It; if (Property->GetNameCPP() == FString("ival" )) { int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor); UE_LOG(LogTemp, Log, TEXT("Property:%s value:%d" ), *Property->GetNameCPP(),i32); } }
其中UProperty
中的ContainerPtrToValuePtr
系列函数都会转发到ContainerVoidPtrToValuePtrInternal
:
1 2 3 4 5 6 7 8 9 10 11 FORCEINLINE void * ContainerVoidPtrToValuePtrInternal (void * ContainerPtr, int32 ArrayIndex) const { check(ArrayIndex < ArrayDim); check(ContainerPtr); if (0 ) { check(!Cast<UClass>(GetOuter())); } return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex; }
其实就是拿到UObject的指针,然后偏移到指定位置(第二个参数用在对象是数组的情况,用来访问指定下标的元素,默认情况下访问下标为0)。
它们的类型分别为FMetaDataPairParam 和int32
,用来记录当前反射属性的元数据:比如属性的Category
、注释、所属的文件、ToolTip
信息等等,比如在C++函数上添加的注释能够在编辑器蓝图中看到注释的信息,都是靠解析这些元数据来实现的。
1 2 3 4 5 6 7 8 #if WITH_METADATA struct FMetaDataPairParam { const char * NameUTF8; const char * ValueUTF8; }; #endif
这两个参数通过METADATA_PARAMS
包裹,用于处理WITH_MATEDATA
的不同情况:
1 2 3 4 5 6 #if WITH_METADATA #define METADATA_PARAMS(x, y) x, y, #else #define METADATA_PARAMS(x, y) #endif
把UHT生成的代码宏展开为:
1 2 3 4 5 6 7 8 #if WITH_METADATA const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = { { "Category" , "MyActor" }, { "ModuleRelativePath" , "Public/MyActor.h" } }; #endif METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))
就是把UE4CodeGen_Private::FMetaDataPairParam
这个类型的数组和数组元素个数传递给F*PropertyParams
,实现在WITH_METADATA
的情况下处理是否具有Metadata的情况。
运行时的Property 引擎中通过UE4CodeGen_Private::ConstructFProperty
来创建出真正Runtime使用的UProperty
(4.25之后是FProperty
),定义在UObject/UObjectGlobals.cpp 。
FBoolPropertyParams特例 当一个反射的数据是bool类型时,引擎产生的反射信息中有一个比较有意思的特例,FBoolPropertyParams
,可以看一下它的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct FBoolPropertyParams // : FPropertyParamsBase{ const char * NameUTF8; const char * RepNotifyFuncUTF8; EPropertyFlags PropertyFlags; EPropertyGenFlags Flags; EObjectFlags ObjectFlags; int32 ArrayDim; uint32 ElementSize; SIZE_T SizeOfOuter; void (*SetBitFunc)(void * Obj); #if WITH_METADATA const FMetaDataPairParam* MetaDataArray; int32 NumMetaData; #endif };
它具有一个SetBitFunc
的函数指针。看一下生成的反射代码:
1 2 3 4 5 6 7 8 9 10 UPROPERTY() bool bEnabled; void Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit (void * Obj) { ((AMyActor*)Obj)->bEnabled = 1 ; } const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled = { "bEnabled" , nullptr , (EPropertyFlags)0x0010000000000000 , UE4CodeGen_Private::EPropertyGenFlags::Bool | UE4CodeGen_Private::EPropertyGenFlags::NativeBool, RF_Public|RF_Transient|RF_MarkAsNative, 1 , sizeof (bool ), sizeof (AMyActor), &Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit, METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData)) };
注意 :其中的关键是,UHT给该属性生成了一个SetBit
的函数,why?其他类型的属性都可以通过STRUCT_OFFSET
来获取该成员的类内偏移,为什么bool就不行了呢?
这是因为C++有位域(bit-field
)这个概念,一个bool可能只占1bit,而不是1byte,但这不是真正的原因。
真正的原因是,C++标准规定了不能对位域进行取地址操作!前面已经提到了STRUCT_OFFSET
实际上是获取到数据成员的指针,得到的是类内偏移,但是因为C++的不能对位域取地址的规定,STRUCT_OFFSET
无法用在位域的成员上的。
[IOS/IEC 14882:2014 §9.6]The address-of operator & shall not be applied to a bit-field, so there are no pointers to bit-fields.
那么这又是因为什么呢?因为系统编址的最小单位是字节而不是位,所以没办法取到1字节零几位的地址。也就决定了不能对位域的数据成员取地址。
UE内其实大量用到了bool使用位域的方式来声明(如果不使用位域,bool类型的空间浪费率达到87.5% :)),所以UE就生成了一个函数来为以位域方式声明的成员设置值。
但是! UE不支持直接对加了UPROPERTY的bool使用位域:
1 2 UPROPERTY() bool bEnabled:1 ;
编译时会有下列错误:
LogCompile: Error: bool bitfields are not supported.
要写成下列方式:
1 2 UPROPERTY() uint8 bEnabled:1 ;
使用这种方式和使用bool bEnabled;
方式生成的反射代码一模一样,所以,UE之所以会生成一个函数来设置bool的值,是因为既要支持原生bool,也要支持位域。
通过UProperty获取值 如果我知道某个类的对象内有一个属性名字,那么怎么能够得到它的值呢?这个可以基于UE的属性反射来实现:
首先通过TFieldIterator
可以遍历该对象的UProperty:
1 2 3 4 for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It){ UProperty* Property = *It; }
然后可以根据得到的Property来判读名字:
1 if (Property->GetNameCPP() == FString("ival" ))
检测是指定名字的Property后可以通过UProperty
上的ContainerPtrToValuePtr
函数来获取对象内该属性的指针:
1 int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor)
前面讲到过,UPropery里存储Offdet值就是当前属性相对于对象起始地址的偏移。而ContainerPtrToValuePtr函数所做的就是得到当前对象偏移Offset的地址然后做了类型转换。
Property的Flag 通过上面的分析,可以看到UPROPERTY
添加的标记,如EditAnywhere
等,会给指定的Property生成FLAG存储在F*PropertyParams
结构的第三个参数中,是位描述的。
可选值为EPropertyFlags
枚举值:
Runtime/CoreUObject/Public/UObject/ObjectMacros.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 enum EPropertyFlags : uint64{ CPF_None = 0 , CPF_Edit = 0x0000000000000001 , CPF_ConstParm = 0x0000000000000002 , CPF_BlueprintVisible = 0x0000000000000004 , CPF_ExportObject = 0x0000000000000008 , CPF_BlueprintReadOnly = 0x0000000000000010 , CPF_Net = 0x0000000000000020 , CPF_EditFixedSize = 0x0000000000000040 , CPF_Parm = 0x0000000000000080 , CPF_OutParm = 0x0000000000000100 , CPF_ZeroConstructor = 0x0000000000000200 , CPF_ReturnParm = 0x0000000000000400 , CPF_DisableEditOnTemplate = 0x0000000000000800 , CPF_Transient = 0x0000000000002000 , CPF_Config = 0x0000000000004000 , CPF_DisableEditOnInstance = 0x0000000000010000 , CPF_EditConst = 0x0000000000020000 , CPF_GlobalConfig = 0x0000000000040000 , CPF_InstancedReference = 0x0000000000080000 , CPF_DuplicateTransient = 0x0000000000200000 , CPF_SubobjectReference = 0x0000000000400000 , CPF_SaveGame = 0x0000000001000000 , CPF_NoClear = 0x0000000002000000 , CPF_ReferenceParm = 0x0000000008000000 , CPF_BlueprintAssignable = 0x0000000010000000 , CPF_Deprecated = 0x0000000020000000 , CPF_IsPlainOldData = 0x0000000040000000 , CPF_RepSkip = 0x0000000080000000 , CPF_RepNotify = 0x0000000100000000 , CPF_Interp = 0x0000000200000000 , CPF_NonTransactional = 0x0000000400000000 , CPF_EditorOnly = 0x0000000800000000 , CPF_NoDestructor = 0x0000001000000000 , CPF_AutoWeak = 0x0000004000000000 , CPF_ContainsInstancedReference = 0x0000008000000000 , CPF_AssetRegistrySearchable = 0x0000010000000000 , CPF_SimpleDisplay = 0x0000020000000000 , CPF_AdvancedDisplay = 0x0000040000000000 , CPF_Protected = 0x0000080000000000 , CPF_BlueprintCallable = 0x0000100000000000 , CPF_BlueprintAuthorityOnly = 0x0000200000000000 , CPF_TextExportTransient = 0x0000400000000000 , CPF_NonPIEDuplicateTransient = 0x0000800000000000 , CPF_ExposeOnSpawn = 0x0001000000000000 , CPF_PersistentInstance = 0x0002000000000000 , CPF_UObjectWrapper = 0x0004000000000000 , CPF_HasGetValueTypeHash = 0x0008000000000000 , CPF_NativeAccessSpecifierPublic = 0x0010000000000000 , CPF_NativeAccessSpecifierProtected = 0x0020000000000000 , CPF_NativeAccessSpecifierPrivate = 0x0040000000000000 , CPF_SkipSerialization = 0x0080000000000000 , };
StaticClass和GetClass的区别 StaticClass
是继承自UObject类的static函数,GetClass
是UObjectBase
的成员函数。
UObjectBase
的GetClass
获取到的UClass就是在NewObject时传递进来的UClass.(代码在UObject\UObjectGlobal.cpp
中)
用途不一样,StaticClass
是在获取具体类型的UClass,而GetClass
是获取到当前对象的真实UClass。
UObject的FObjectInitializer构造函数的调用
注意:只有GENERATER_UCLASS_BODY才可以实现FObjectInitializer
的构造函数。
在继承自UObject的类中,都可以自己写一个接收const FObjectInitializer&
参数的构造函数,在创建对象时会被调用:
1 UMyObject::UMyObject(const FObjectInitializer& Initializer){}
在类中的GENERATED_UCLASS_BODY
中默认声明这样一个构造函数。并且,在UHT生成的代码中通过宏还定义了一个函数:
1 DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyObject)
这个宏经过展开之后就是这样的:
1 static void __DefaultConstructor(const FObjectInitializer& X) { new ((EInternal*)X.GetObj())UMyObject(X); }
为当前的对象类型UMyObject
定义了一个__DefaultConstructor
函数,作用是在当前FObjectInitializer
传入的参数的Object上调用FObjectInitializer
的构造函数。
注意 :如果是GENERATED_BODY
则不会声明这个构造函数,使用的就是另一个宏:
1 DEFINE_DEFAULT_CONSTRUCTOR_CALL(UMyObject)
展开之后是这样的:
1 static void __DefaultConstructor(const FObjectInitializer& X) { new ((EInternal*)X.GetObj())UMyObject; }
是通过GANERATED_BODY
和GENERATED_UCLASS_BODY
来定义了两种__DefaultConstructor
的实现,一种是调用FObjectInitializer
的构造函数,另一种是调用类的默认构造函数。
所以,默认情况下FObjectInitializer的构造函数和UObject的默认构造函数在调用时只会走一个。
那么它是如何被调用到的呢?
在NewObject
中调用的StaticConstructObject_Internal
中有以下代码:
1 2 3 4 5 6 7 8 9 bool bRecycledSubobject = false ; Result = StaticAllocateObject(InClass, InOuter, InName, InFlags, InternalSetFlags, bCanRecycleSubobjects, &bRecycledSubobject); check(Result != NULL ); if (!bRecycledSubobject){ STAT(FScopeCycleCounterUObject ConstructorScope(InClass, GET_STATID(STAT_ConstructObject))); (*InClass->ClassConstructor)( FObjectInitializer(Result, InTemplate, bCopyTransientsFromClassDefaults, true , InInstanceGraph) ); }
通过传入进来的UClass对象获取其中的ClassConstructor
函数指针,并构造出一个FObjectInitializer
作为参数传递。
在之前的笔记(StaticClass是如何定义的 )中,写到了,通过GetPrivateStaticClass
获取到当前UObject类的UClass实例会实例化出一个当前类的InternalConstructor
函数(注意这个模板参数T是UObject的类):
1 2 3 4 5 6 7 8 template <class T >void InternalConstructor ( const FObjectInitializer & X ){ T::__DefaultConstructor(X); }
就将其转发到了上面讲到的__DefaultConstructor
函数上,然后它里面又转发到了所传入FObjectInitializer
对象上的const FObjectInitializer&
构造函数上(或者默认构造函数上)。
创建对象并调用const FObjectInitializer&
构造函数的调用流程为:
通过调用StaticAllocateObject
根据传入的UClass创建出对象
通过传入的UClass对象内的ClassConstructor函数指针调用所创建UObject类的InternalConstructor
函数
UMyObject::InternalConstructor
会转发到UMyObject::__DefaultConstructor
UMyObject::__DefaultConstructor
会从接收到的FObjectInitializer
对象上获取到通过StaticAllocateObject
创建的对象,然后通过placement-new
的方式,在这块内存上调用UMyObject
类的const FObjectInitializer&
构造函数。
通过以上的流程就实现了NewObject
时会自动调用到它的FObjectInitializer
构造函数。
注意:在CDO的构造上有点区别,CDO的构造是通过UClass::GetDefaultObject
中实现上述流程的。
StaticClass是如何定义的 在UE中可以对UObject的类执行UXXX::StaticClass
方法来获取到它的UClass对象。
但是它是如何定义的?首先要看我们声明对象的MyActor.generated.h
中(以AMyActor类为例):
1 template <> MICROEND_423_API UClass* StaticClass<class AMyActor >();
为AMyActor类特化了StaticClass
的版本。再去MyActor.gen.cpp
中找以下它的实现:
1 2 3 4 5 IMPLEMENT_CLASS(AMyActor, 31943282 ); template <> MICROEND_423_API UClass* StaticClass<AMyActor>(){ return AMyActor::StaticClass(); }
从这里看也就只是转发调用而已,但是关键点隐藏在其他地方。 首先AMyActor::StaticClass
类的定义是在AMyActor.generated.h
的DECLARE_CLASS
宏中(该宏定义在ObjectMacros.h#L1524 ),返回的是GetPrivateStaticClass
的调用。
而GetPrivateStaticClass
则在AMyAcotr.gen.cpp
中的IMPLEMENT_CLASS 实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define IMPLEMENT_CLASS(TClass, TClassCrc) \ static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof (TClass), TClassCrc); \ UClass* TClass::GetPrivateStaticClass() \ { \ static UClass* PrivateStaticClass = NULL ; \ if (!PrivateStaticClass) \ { \ \ GetPrivateStaticClassBody( \ StaticPackage(), \ (TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0 ), \ PrivateStaticClass, \ StaticRegisterNatives##TClass, \ sizeof (TClass), \ alignof (TClass), \ (EClassFlags)TClass::StaticClassFlags, \ TClass::StaticClassCastFlags(), \ TClass::StaticConfigName(), \ (UClass::ClassConstructorType)InternalConstructor<TClass>, \ (UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \ &TClass::AddReferencedObjects, \ &TClass::Super::StaticClass, \ &TClass::WithinClass::StaticClass \ ); \ } \ return PrivateStaticClass; \ }
可以看到GetPrivateStaticClass
其实就是通过这些元数据构造出UClass的。
如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 IMPLEMENT_CLASS(AMyActor, 3240835608 ); static TClassCompiledInDefer < AMyActor > AutoInitializeAMyActor(TEXT("AMyActor" ), sizeof (AMyActor), 3240835608 );UClass * AMyActor::GetPrivateStaticClass () { static UClass * PrivateStaticClass = NULL ; if (!PrivateStaticClass) { GetPrivateStaticClassBody( StaticPackage(), (TCHAR*)TEXT("AMyActor" ) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0 ), PrivateStaticClass, StaticRegisterNativesAMyActor, sizeof (AMyActor), alignof (AMyActor), (EClassFlags) AMyActor::StaticClassFlags, AMyActor::StaticClassCastFlags(), AMyActor::StaticConfigName(), (UClass::ClassConstructorType) InternalConstructor<AMyActor>, (UClass::ClassVTableHelperCtorCallerType) InternalVTableHelperCtorCaller<AMyActor>, &AMyActor::AddReferencedObjects, &AMyActor::Super::StaticClass, &AMyActor::WithinClass::StaticClass ); } return PrivateStaticClass; };
UClass中的函数指针 上面代码中比较关键的点为:
1 2 (UClass::ClassConstructorType)InternalConstructor<TClass>, \ (UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \
这两行是模板实例化出了两个函数并转换成函数指针传递给GetPrivateStaticClassBody
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <class T >void InternalConstructor ( const FObjectInitializer & X ){ T::__DefaultConstructor(X); } template <class T >UObject * InternalVTableHelperCtorCaller (FVTableHelper & Helper ){ return T::__VTableCtorCaller(Helper); }
就是对__DefaultConstructor
这样函数的的转发调用。
UClass::ClassConstructorType
与UClass::ClassVTableHelperCtorCallerType
这两个typedef
为:
1 2 typedef void (*ClassConstructorType) (const FObjectInitializer&) ;typedef UObject* (*ClassVTableHelperCtorCallerType) (FVTableHelper& Helper);
GetPrivateStaticClassBody 其中的GetPrivateStaticClassBody
函数是定义在Runtime\CoreUObject\Private\UObject\Class.cpp 中的。
原型为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void GetPrivateStaticClassBody ( const TCHAR* PackageName, const TCHAR* Name, UClass*& ReturnClass, void (*RegisterNativeFunc)(), uint32 InSize, uint32 InAlignment, EClassFlags InClassFlags, EClassCastFlags InClassCastFlags, const TCHAR* InConfigName, UClass::ClassConstructorType InClassConstructor, UClass::ClassVTableHelperCtorCallerType InClassVTableHelperCtorCaller, UClass::ClassAddReferencedObjectsType InClassAddReferencedObjects, UClass::StaticClassFunctionType InSuperClassFn, UClass::StaticClassFunctionType InWithinClassFn, bool bIsDynamic ) ;
其中第四个参数是传入注册Native函数的函数指针,该函数在MyActor.gen.cpp
中生成,也可以通过在UFUNCTION中添加CustomThunk
函数来自己实现,UnLua的覆写C++函数就是基于替换thunk函数做的。
GetPrivateStaticClassBody
中通过(UClass*)GUObjectAllocator.AllocateUObject
来分配出UClass的内存,因为所有的UClass结构都一致。
1 2 3 4 5 6 7 8 9 10 void AMyActor::StaticRegisterNativesAMyActor () { UClass* Class = AMyActor::StaticClass(); static const FNameNativePtrPair Funcs[] = { { "ReceiveBytes" , &AMyActor::execReceiveBytes }, { "TESTFUNC" , &AMyActor::execTESTFUNC }, }; FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs)); }
其实就是把Native的函数通过AddNativeFunction
添加到UClass中:
1 2 3 4 5 6 7 8 void FNativeFunctionRegistrar::RegisterFunctions (class UClass* Class, const FNameNativePtrPair* InArray, int32 NumFunctions) { for (; NumFunctions; ++InArray, --NumFunctions) { Class->AddNativeFunction(UTF8_TO_TCHAR(InArray->NameUTF8), InArray->Pointer); } }
获取UObject的资源路径 可以通过FSoftObjectPath
传入UObject来获得:
1 2 3 4 5 FString GetObjectResource (UObject* Obj) { FSoftObjectPath SoftRef (Obj) ; return SoftRef.ToString(); }
注意 :直接ToString
获取到的路径是PackagePath的,形如/Game/XXXX.XXXX
这种形式,可以通过GetLongPackageName
得到去掉.XXXX
的字符串。
BP to CPP 在Project Settings
-Packaging
-Blueprint
下添加想要转换的蓝图资源:
设置之后执行打包就会在项目的下列路径中产生对应的.h/.cpp
以及生成generated.h/gen.cpp
:
1 Intermediate\Plugins\NativizedAssets\Windows\Game\Intermediate\Build\Win64\UE4\Inc\NativizedAssets
监听窗口关闭 可以通过监听FSlateFontServices
里的OnSlateWindowDestroyed
:
1 2 3 4 5 6 7 DECLARE_MULTICAST_DELEGATE_OneParam(FOnSlateWindowDestroyed, void *); FOnSlateWindowDestroyed& OnSlateWindowDestroyed () { return OnSlateWindowDestroyedDelegate; }
监听方法:
1 FSlateApplication::Get().GetRenderer()->OnSlateWindowDestroyed().AddRaw(this , &FSceneViewport::OnWindowBackBufferResourceDestroyed);
UnLua的EXPORT_PRIMITIVE_TYPE UnLua里使用EXPORT_PRIMITIVE_TYPE
宏来导出内置类型:
1 EXPORT_PRIMITIVE_TYPE(uint64, TPrimitiveTypeWrapper<uint64>, uint64)
宏展开之后为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 template < > struct UnLua : :TType < TPrimitiveTypeWrapper < uint64 > , false > { static const char * GetName () { return "uint64" ; } }; struct FExporteduint64Helper { typedef TPrimitiveTypeWrapper < uint64 > ClassType; static FExporteduint64Helper StaticInstance; UnLua::TExportedClass < false , TPrimitiveTypeWrapper < uint64 > , uint64 > * ExportedClass; ~FExporteduint64Helper() { delete ExportedClass; } FExporteduint64Helper(): ExportedClass(nullptr ) { UnLua::TExportedClass < false , TPrimitiveTypeWrapper < uint64 > , uint64 > * Class = (UnLua::TExportedClass < false , TPrimitiveTypeWrapper < uint64 > , uint64 > * ) UnLua::FindExportedClass("uint64" ); if (!Class) { ExportedClass = new UnLua::TExportedClass < false , TPrimitiveTypeWrapper < uint64 > , uint64 > ("uint64" , nullptr ); UnLua::ExportClass((UnLua::IExportedClass * ) ExportedClass); Class = ExportedClass; } Class - > AddProperty("Value" , & ClassType::Value); } }; FExporteduint64Helper FExporteduint64Helper::StaticInstance; static struct FTypeInterfaceuint64 { FTypeInterfaceuint64() { UnLua::AddTypeInterface("uint64" , UnLua::GetTypeInterface < uint64 > ()); } }TypeInterfaceuint64;
4.25 MountPak没有材质 在项目打包时在Project Settgins
-Packaging
中开启了Share Material shader code
时,后续的热更pak打包,如果没有同步把ushaderbytecode
打包进去并自己加载,会产生下列材质丢失的问题:
经过调试后发现,是因为4.25在mount pak之后不会加载新的Pak中的shaderbytecode
,找到了问题,解决办法就手到擒来了,找到引擎中加载shaderbytecode
的代码自己调用一遍即可。
在项目打包时默认会生成两个shaderbytecode
文件:
1 2 ShaderArchive-Global-PCD3D_SM5.ushaderbytecode ShaderArchive-HotPatcherExample-PCD3D_SM5.ushaderbytecode
并且它们存在于pak中Mount point的路径均为:
1 ../../../PROJECT_NAME/Content/
而且根据shaderbytecode
文件路径的组成规则:
1 2 3 4 5 6 7 8 static FString ShaderExtension = TEXT(".ushaderbytecode" );static FString StableExtension = TEXT(".scl.csv" );static FString PipelineExtension = TEXT(".ushaderpipelines" );static FString GetCodeArchiveFilename (const FString& BaseDir, const FString& LibraryName, FName Platform) { return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-" ), *LibraryName) + Platform.ToString() + ShaderExtension; }
所以,只需要传递基础路径和LibraryName即可(OpenLibrary中通过调用GetCodeArchiveFilename
来获取要加载的文件)。
即要重新加载global和项目的shaderbytecode,在mount成功之后执行下面两行代码即可:
1 2 FShaderCodeLibrary::OpenLibrary("Global" , FPaths::ProjectContentDir()); FShaderCodeLibrary::OpenLibrary(FApp::GetProjectName(), FPaths::ProjectContentDir());
通过笔记最前面的一段话,可以总结出两个解决方案:
开启了Share Material shader code的情况下,需要把shaderbytecode打包,并自己在Mount时加载;
打Pak时Cook资源不要开启Share Material shader code
,这样会把资源的shader都打包在资源内部,从而避免需要单独加载shader的问题;
Android上Arrow组件的crash 在游戏中把一个Actor上的Arrow
组件设置为visible,打包Android上运行会Crash:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 06-17 15:23:51.976 26991 27112 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 27112 (RenderThread 2), pid 26991 (MainThread-UE4) 06-17 15:23:52.350 27129 27129 F DEBUG : #00 pc 062fa090 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FArrowSceneProxy::GetDynamicMeshElements(TArray<FSceneView const*, FDefaultAllocator> const&, FSceneViewFamily const&, unsigned int, FMeshElementCollector&) const+824) 06-17 15:23:52.350 27129 27129 F DEBUG : #01 pc 058010f4 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN14FSceneRenderer25GatherDynamicMeshElementsER6TArrayI9FViewInfo17FDefaultAllocatorEPK6FSceneRK16FSceneViewFamilyR25FGlobalDynamicIndexBufferR26FGlobalDynamicVertexBufferR24FGlobalDynamicReadBufferRKS0_Ih18TMemStackAllocatorILj0EEESL_SL_R21FMeshElementCollector+2456) 06-17 15:23:52.350 27129 27129 F DEBUG : #02 pc 0580f8a8 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN14FSceneRenderer21ComputeViewVisibilityER24FRHICommandListImmediateN22FExclusiveDepthStencil4TypeER6TArrayI13FViewCommands16TInlineAllocatorILj4E17FDefaultAllocatorEER25FGlobalDynamicIndexBufferR26FGlobalDynamicVertexBufferR24FGlobalDynamicReadBuffer+39716) 06-17 15:23:52.350 27129 27129 F DEBUG : #03 pc 054ffb30 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FMobileSceneRenderer::InitViews(FRHICommandListImmediate&)+1076) 06-17 15:23:52.350 27129 27129 F DEBUG : #04 pc 05500a98 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FMobileSceneRenderer::Render(FRHICommandListImmediate&)+1228) 06-17 15:23:52.350 27129 27129 F DEBUG : #05 pc 057fa970 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZZN15FRendererModule24BeginRenderingViewFamilyEP7FCanvasP16FSceneViewFamilyENK4$_85clER24FRHICommandListImmediate+2316) 06-17 15:23:52.350 27129 27129 F DEBUG : #06 pc 057fc944 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (_ZN10TGraphTaskI31TEnqueueUniqueRenderCommandTypeIZN15FRendererModule24BeginRenderingViewFamilyEP7FCanvasP16FSceneViewFamilyE21FDrawSceneCommandNameZNS1_24BeginRenderingViewFamilyES3_S5_E4$_85EE11ExecuteTaskER6TArrayIP14FBaseGraphTask17FDefaultAllocatorEN13ENamedThreads4TypeE+712) 06-17 15:23:52.350 27129 27129 F DEBUG : #07 pc 040bfa04 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FNamedTaskThread::ProcessTasksNamedThread(int, bool)+2876) 06-17 15:23:52.350 27129 27129 F DEBUG : #08 pc 040be518 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FNamedTaskThread::ProcessTasksUntilQuit(int)+108) 06-17 15:23:52.350 27129 27129 F DEBUG : #09 pc 0518eaec /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (RenderingThreadMain(FEvent*)+436) 06-17 15:23:52.350 27129 27129 F DEBUG : #10 pc 051d93d8 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRenderingThread::Run()+20) 06-17 15:23:52.350 27129 27129 F DEBUG : #11 pc 04142c8c /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRunnableThreadPThread::Run()+164) 06-17 15:23:52.350 27129 27129 F DEBUG : #12 pc 040b9ef0 /data/app/com.imzlp.GWorld-tDCKTLEN7M7ZaRP1btpWdA==/lib/arm/libUE4.so (FRunnableThreadPThread::_ThreadProc(void*)+80) 06-17 15:23:52.430 26991 27061 D UE4 : Used memory: 361838 06-17 15:27:43.270 13042 13231 I MtpDatabase: Mediaprovider didn't delete /storage/emulated/0/UE4Game/GWorld/GWorld/Saved/Paks/1.45_Android_ETC2_001_P.pak 06-17 15:27:45.781 13042 13231 D MtpServer: path: /storage/emulated/0/UE4Game/GWorld/GWorld/Saved/Paks/1.45_Android_ETC2_001_P.pak parent: 68 storageID: 00010001
有时间在来具体分析。
target/build.cs输出Log 可以使用C#里的以下代码:
1 2 3 using System;System.Console.WriteLine("12346" );
TargetRules的BuildSettingsVersion
PS:UE4.24之后才有。
可以在Target.cs
中指定:
1 DefaultBuildSettings = BuildSettingsVersion.V2;
BuildSettingsVersion 可以指定构建时使用的默认设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public enum BuildSettingsVersion{ V1, V2, Latest }
UE4.23及之前的引擎版本是V1
,用来控制当项目升级引擎版本时使用之前引擎的构建设置,用于解决项目升级之后会有大量错误的问题。
注意:因为4.25之后的是V2,默认bLegacyPublicIncludePaths=false
,这个会导致如果模块中相对于Public
的代码路径,如Public/Core/CoreCode.h
,如果没有添加Core目录到PublicIncludePaths
中,在工程的其他地方不指定相对路径,直接用CoreCode.h
,在V1的版本里是可以编译过的,但是在V2中就会有编译错误。
从TargetRules获取引擎版本 TargetRules
中具有**Version (ReadOnlyBuildVersion)**成员,它是BuildVersion
的类型,定义在Programs\UnrealBuildTool\System\BuildVersion.cs 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 [Serializable ] public class BuildVersion { public int MajorVersion; public int MinorVersion; public int PatchVersion; public int Changelist; public int CompatibleChangelist; public bool IsLicenseeVersion; public bool IsPromotedBuild; public string BranchName; public string BuildId; public string BuildVersionString; }
可以在Target.cs
或者Build.cs
里通过Target.Version
来访问引擎版本,可以根据不同的引擎版本来使用不同的库。
从TargetRules获取Configuration 从ReadOnlyTargetRules
接收到的Target,可以从其中获取Configuration
成员,用于检测打包的BuildConfiguration
:
1 2 3 4 if (Target.Configuration == UnrealTargetConfiguration.Shipping){ }
枚举值为Development
/Debug
/DebugGame
/Shipping
/Test
等。
IOS CrashLog分析
GC
UE4使用标记 -清扫 式的GC方式,它是一种经典的垃圾回收方式。一次垃圾回收分为两个阶段。第一阶段从一个根集合出发,遍历所有可达对象,遍历完成后就能标记出可达对象和不可达对象了,这个阶段会在一帧内完成。第二阶段会渐进式的清理这些不可达对象,因为不可达的对象将永远不能被访问到,所以可以分帧清理它们,避免一下子清理很多UObject,比如map卸载时,发生明显的卡顿。
UObject之间的引用关系需要用强指针引用加UPROPERTY标记完成。
UPROPERTY标记通过UHT之后会生成UProperty对象,UProperty对象可以控制对属性的访问。也通过UProperty对象保存引用关系。
如果想要给没有添加UPROPERTY标记的对象添加引用可以通过重写UObject
的虚函数AddReferencedObjects
,比如AActor中的OwnedComponents
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 TSet<UActorComponent*> OwnedComponents; void AActor::AddReferencedObjects (UObject* InThis, FReferenceCollector& Collector) { AActor* This = CastChecked<AActor>(InThis); Collector.AddReferencedObjects(This->OwnedComponents); #if WITH_EDITOR if (This->CurrentTransactionAnnotation.IsValid()) { This->CurrentTransactionAnnotation->AddReferencedObjects(Collector); } #endif Super::AddReferencedObjects(InThis, Collector); }
SDetailsView监听属性变化 可以通过监听基类SDetailsViewBase
中的OnFinishedChangingPropertiesDelegate
代理来实现。
接收参数是FPropertyChangedEvent
1 DECLARE_MULTICAST_DELEGATE_OneParam(FOnFinishedChangingProperties, const FPropertyChangedEvent&);
UObject serializer的调用栈
有时间再来分析具体内容。
FName/FString/FText的区别 FName FName的字符串一般用在为资源命名或者访问资源时(比如命名的骨骼)需要使用。 它使用一个轻型系统使用字符串,特定字符串会被重复使用,在数据表中也就只存储一次。
只在内存中存储一次
不区分大小写
不能被修改
查找和访问速度比较快
内部有一个HASH值 FName在Edior中占12个字节,打包8字节,FName的XXXX_12
这样的字符串会被分成string part和number part,估计是为了想不为每个拼接的结果都在NamePool中创建一份吧。
FString FSting比较类似于标准库的std::string
,区分大小写,可修改,每份实例都是单独的内存。
FText FText是用于本地化的类,所有需要展示的文本都需要使用FText,它提供了以下功能:
创建本地化文本
格式化文本
从数字生成文本
从日期或时间生成文本
转换文本(大小写转换等)
文档
Plugin添加其他Plugin的模块 如果插件A要引用插件B中的模块,那么就需要在插件A的uplugin文件中添加对插件B的依赖:
1 2 3 4 5 6 "Plugins": [ { "Name" : "B" , "Enabled" : true } ]
然后就可以在插件A中添加插件B中的模块了。
Build Configurations
Build Status
Describle
Debug
引擎和游戏符号都以debug方式编译,需要源码版引擎
DebugGame
优化引擎代码,只可以调试Game符号
Development
默认的配置,在DebugGame的模式上进行优化,只可以调试游戏符号
Shipping
不包含调试符号、Console、stats、profiling工具,用于发行版本
Test
与Shipping相同,但是要会包含Console、stats、profiling等工具
如果使用从EpicLauncher
安装的引擎,打包Debug时会提示:
1 Targets cannot be built in the Debug configuration with this engine distribution.
这个报错是在UBT中产生的,具体代码在UnrealBuildTool\Configuration\UEBuildTarget.cs
GlobalShaderCache的加载 在Runtime/Engine/Private/ShaderCompiler/ShaderCompiler.cpp 中有获取GlobalShaderCache*.bin
的方法:
1 2 3 4 static FString GetGlobalShaderCacheFilename (EShaderPlatform Platform) { return FString(TEXT("Engine" )) / TEXT("GlobalShaderCache-" ) + LegacyShaderPlatformToShaderFormat(Platform).ToString() + TEXT(".bin" ); }
在同文件中定义的CompileGlobalShaderMap
函数中被读取。 调用栈:
完整流程有时间再来分析。
ushaderbytecode的加载 在Runtime/RenderCore/Private/ShaderCodeLibrary.cpp 文件中,可以获取到shaderbytecode相关的文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static uint32 GShaderCodeArchiveVersion = 2 ;static uint32 GShaderPipelineArchiveVersion = 1 ;static FString ShaderExtension = TEXT(".ushaderbytecode" );static FString StableExtension = TEXT(".scl.csv" );static FString PipelineExtension = TEXT(".ushaderpipelines" );static FString GetCodeArchiveFilename (const FString& BaseDir, const FString& LibraryName, FName Platform) { return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-" ), *LibraryName) + Platform.ToString() + ShaderExtension; } static FString GetStableInfoArchiveFilename (const FString& BaseDir, const FString& LibraryName, FName Platform) { return BaseDir / FString::Printf(TEXT("ShaderStableInfo-%s-" ), *LibraryName) + Platform.ToString() + StableExtension; } static FString GetPipelinesArchiveFilename (const FString& BaseDir, const FString& LibraryName, FName Platform) { return BaseDir / FString::Printf(TEXT("ShaderArchive-%s-" ), *LibraryName) + Platform.ToString() + PipelineExtension; } static FString GetShaderCodeFilename (const FString& BaseDir, const FString& LibraryName, FName Platform) { return BaseDir / FString::Printf(TEXT("ShaderCode-%s-" ), *LibraryName) + Platform.ToString() + ShaderExtension; } static FString GetShaderDebugFolder (const FString& BaseDir, const FString& LibraryName, FName Platform) { return BaseDir / FString::Printf(TEXT("ShaderDebug-%s-" ), *LibraryName) + Platform.ToString(); }
然后在同文件的FShaderLibraryInstance::Create
来加载。
完整流程有时间再来分析。
默认打包到pak里的资源 UE在打包的时候会把工程下的Content
中的资源进行依赖分析然后打包,但是经过对比之后发现,引擎中还会添加额外的没有引用到的资源,经过分析代码发现引擎的ini中有指定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 [Engine.StartupPackages] bSerializeStartupPackagesFromMemory =true bFullyCompressStartupPackages =false +Package=/Engine/EngineMaterials/BlinkingCaret +Package=/Engine/EngineMaterials/DefaultBokeh +Package=/Engine/EngineMaterials/DefaultBloomKernel +Package=/Engine/EngineMaterials/DefaultDeferredDecalMaterial +Package=/Engine/EngineMaterials/DefaultDiffuse +Package=/Engine/EngineMaterials/DefaultLightFunctionMaterial +Package=/Engine/EngineMaterials/WorldGridMaterial +Package=/Engine/EngineMaterials/DefaultMaterial +Package=/Engine/EngineMaterials/DefaultNormal +Package=/Engine/EngineMaterials/DefaultPhysicalMaterial +Package=/Engine/EngineMaterials/DefaultVirtualTextureMaterial +Package=/Engine/EngineMaterials/DefaultWhiteGrid +Package=/Engine/EngineMaterials/EditorBrushMaterial +Package=/Engine/EngineMaterials/EmissiveMeshMaterial +Package=/Engine/EngineMaterials/Good64x64TilingNoiseHighFreq +Package=/Engine/EngineMaterials/Grid +Package=/Engine/EngineMaterials/Grid_N +Package=/Engine/EngineMaterials/LandscapeHolePhysicalMaterial +Package=/Engine/EngineMaterials/MiniFont +Package=/Engine/EngineMaterials/PaperDiffuse +Package=/Engine/EngineMaterials/PaperNormal +Package=/Engine/EngineMaterials/PhysMat_Rubber +Package=/Engine/EngineMaterials/PreintegratedSkinBRDF +Package=/Engine/EngineMaterials/RemoveSurfaceMaterial +Package=/Engine/EngineMaterials/WeightMapPlaceholderTexture +Package=/Engine/EngineDebugMaterials/BoneWeightMaterial +Package=/Engine/EngineDebugMaterials/DebugMeshMaterial +Package=/Engine/EngineDebugMaterials/GeomMaterial +Package=/Engine/EngineDebugMaterials/HeatmapGradient +Package=/Engine/EngineDebugMaterials/LevelColorationLitMaterial +Package=/Engine/EngineDebugMaterials/LevelColorationUnlitMaterial +Package=/Engine/EngineDebugMaterials/MAT_LevelColorationLitLightmapUV +Package=/Engine/EngineDebugMaterials/ShadedLevelColorationLitMaterial +Package=/Engine/EngineDebugMaterials/ShadedLevelColorationUnlitMateri +Package=/Engine/EngineDebugMaterials/TangentColorMap +Package=/Engine/EngineDebugMaterials/VertexColorMaterial +Package=/Engine/EngineDebugMaterials/VertexColorViewMode_AlphaAsColor +Package=/Engine/EngineDebugMaterials/VertexColorViewMode_BlueOnly +Package=/Engine/EngineDebugMaterials/VertexColorViewMode_ColorOnly +Package=/Engine/EngineDebugMaterials/VertexColorViewMode_GreenOnly +Package=/Engine/EngineDebugMaterials/VertexColorViewMode_RedOnly +Package=/Engine/EngineDebugMaterials/WireframeMaterial +Package=/Engine/EngineSounds/WhiteNoise +Package=/Engine/EngineFonts/SmallFont +Package=/Engine/EngineFonts/TinyFont +Package=/Engine/EngineFonts/Roboto +Package=/Engine/EngineFonts/RobotoTiny +Package=/Engine/EngineMaterials/DefaultTextMaterialTranslucent +Package=/Engine/EngineFonts/RobotoDistanceField
就算是工程中没有任何资源,也会默认把这些资源给打包进来。
创建Commandlet 在一个Editor的Module下创建下列文件和代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #pragma once #include "Commandlets/Commandlet.h" #include "HotPatcherPatcherCommandlet.generated.h" UCLASS() class UHotPatcherPatcherCommandlet :public UCommandlet{ GENERATED_BODY() public : virtual int32 Main (const FString& Params) override ; }; #include "HotPatcherCookerCommandlet.h" int32 UHotPatcherCookerCommandlet::Main (const FString& Params) { UE_LOG(LogTemp, Log, TEXT("UHotPatcherCookerCommandlet::Main" )); return 0 ; }
然后在启动的时候就可以使用下列参数来运行Commandlet,并且可以给它传递参数:
1 UE4Editor.exe PROJECT_NAME.uproject -run=HotPatcherCooker -aaa="D:\\AAA.json" -test1
UnLua如何实现函数覆写
概括来说:UnLua绑定了UE创建对象的事件,当创建CDO时会调用到UnLua的NotifyUObjectCreated
,在其中拿到了该对象的UClass,对该对象的UClass中的UFUNCTION通过SetNativeFunc
修改为CallLua
函数,这样就实现了覆写UFUNCTION。
下面来具体分析一下实现。UnLua实现覆写完整的调用栈:
替换Thunk函数 在UnLua的FLuaContext的initialize函数中,将GLuaCxt注册到了GUObjectArray
中:
1 2 3 4 5 6 if (!bAddUObjectNotify){ GUObjectArray.AddUObjectCreateListener(GLuaCxt); GUObjectArray.AddUObjectDeleteListener(GLuaCxt); }
而FLuaContext
继承自FUObjectArray::FUObjectCreateListener
和FUObjectArray::FUObjectDeleteListener
,所以当UE的对象系统创建对象的时候会把调用到FLuaContext的NotifyUObjectCreated
与NotifyUObjectDeleted
。
当创建一个UObject的时候会在FObjectArray
的AllocateUObjectIndex
中对多有注册过的CreateListener
调用NotifyUObjectDeleted
函数。
而UnLua实现覆写UFUNCTION的逻辑就是写在NotifyUObjectCreated
中的TryBindLua
调用中,栈如下: 一个一个来说他们的作用:
FLuaContext::TryBindUnlua 1 2 bool FLuaContext::TryToBindLua (UObjectBaseUtility *Object) ;
主要作用是:如果创建的对象继承了UUnLuaInterface
,具有GetModuleName
函数,则通过传进来的UObject获取到它的UCclass,然后再通过UClass得到GetModuleName
函数的UFunction
,并通过CDO对象调用该UFunction,得到该CLass绑定的Lua模块名。
若没有静态绑定,则检查是否具有动态绑定。
UUnLuaManager::Bind 该函数定义在UnLua/pRIVATE/UnLuaManager.cpp
文件中。
在TryBindUnlua
中得到了当前创建对象的UClass
和绑定的模块名,传递到了Bind函数中,它主要做了几件事情:
注册Class到lua
require对应的lua模块
调用UnLuaManager::BindInternal
函数
为当前对象创建一个lua端对象并push上一个Initialize
函数并调用
BindInternal 其中的关键函数为UnLuaManager::BindInternal
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 bool UUnLuaManager::BindInternal (UObjectBaseUtility *Object, UClass *Class, const FString &InModuleName, bool bNewCreated) { if (!Object || !Class) { return false ; } lua_State *L = *GLuaCxt; TStringConversion<TStringConvert<TCHAR, ANSICHAR>> ModuleName(*InModuleName); if (!bNewCreated) { if (!BindSurvivalObject(L, Object, Class, ModuleName.Get())) { return false ; } FString *ModuleNamePtr = ModuleNames.Find(Class); if (ModuleNamePtr) { return true ; } } ModuleNames.Add(Class, InModuleName); Classes.Add(InModuleName, Class); #if UE_BUILD_DEBUG TSet<FName> *LuaFunctionsPtr = ModuleFunctions.Find(InModuleName); check(!LuaFunctionsPtr); TMap<FName, UFunction*> *UEFunctionsPtr = OverridableFunctions.Find(Class); check(!UEFunctionsPtr); #endif TSet<FName> &LuaFunctions = ModuleFunctions.Add(InModuleName); GetFunctionList(L, ModuleName.Get(), LuaFunctions); TMap<FName, UFunction*> &UEFunctions = OverridableFunctions.Add(Class); GetOverridableFunctions(Class, UEFunctions); OverrideFunctions(LuaFunctions, UEFunctions, Class, bNewCreated); return ConditionalUpdateClass(Class, LuaFunctions, UEFunctions); }
这个函数接受到的参数是创建出来的UObject,以及它的UClass,还有对应的Lua的模块名。
把对象的UClass与Lua的模块名对应添加到ModuleNames
和Classes
中
从Lua端通过L获取所指定模块名中的所有函数
从UClass获取所有的BlueprintEvent、RepNotifyFunc函数
对两边获取的结果调用UUnLuaManager::OverrideFunctions
执行替换
UUnLuaManager::OverrideFunctions 对从Lua端获取的函数使用名字在当前类的UFunction中查找,依次对其调用UUnLuaManager::OverrideFunction
.
UUnLuaManager::OverrideFunction
判断传入的UFunction是不是属于传入的Outer UClasss
判断是否允许调用被覆写的函数
调用AddFunction
函数
UUnLuaManager::AddFunction
如果函数为FUNC_Native
则将FLuaInvoker::execCallLua
和所覆写的函数名通过AddNativeFunction
添加至UClass
将UFunction
内的函数指针替换为(FNativeFuncPtr)&FLuaInvoker::execCallLua
如果开启了允许调用被覆写的函数,则把替换NativeFunc之前的UFunction对象存到GReflectionRegistry
中
Call lua 首先,需要说的一点是,当使用UEC++写的带有UFUNCTION
并具有BlueprintNativeEvent
或者BlueprintImplementableEvent
标记的函数,UHT会给生成对应名字的函数:
1 2 3 4 5 6 UFUNCTION(BlueprintNativeEvent,BlueprintCallable) bool TESTFUNC () ; bool TESTFUNC_Implementation () ; UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "BeginPlay" )) bool TESTImplEvent (AActor* InActor,int32 InIval) ;
UHT生成的函数和传递的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #define MicroEnd_423_Source_MicroEnd_423_Public_MyActor_h_13_EVENT_PARMS \ struct MyActor_eventReceiveBytes_Parms \ { \ TArray<uint8> InData; \ }; \ struct MyActor_eventTESTFUNC_Parms \ { \ bool ReturnValue; \ \ \ MyActor_eventTESTFUNC_Parms() \ : ReturnValue(false ) \ { \ } \ }; \ struct MyActor_eventTESTImplEvent_Parms \ { \ AActor* InActor; \ int32 InIval; \ bool ReturnValue; \ \ \ MyActor_eventTESTImplEvent_Parms() \ : ReturnValue(false ) \ { \ } \ }; static FName NAME_AMyActor_TESTFUNC = FName(TEXT("TESTFUNC" ));bool AMyActor::TESTFUNC () { MyActor_eventTESTFUNC_Parms Parms; ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTFUNC),&Parms); return !!Parms.ReturnValue; } static FName NAME_AMyActor_TESTImplEvent = FName(TEXT("TESTImplEvent" ));bool AMyActor::TESTImplEvent (AActor* InActor, int32 InIval) { MyActor_eventTESTImplEvent_Parms Parms; Parms.InActor=InActor; Parms.InIval=InIval; ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTImplEvent),&Parms); return !!Parms.ReturnValue; }
可以看到,UHT帮我们定义了同名函数,并将其转发给ProcessEvent
。
注意:这里通过FindFunctionChecked
方法是调用的UObject::FindFunctionChecked
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 UFunction* UObject::FindFunction ( FName InName ) const { return GetClass()->FindFunctionByName(InName); } UFunction* UObject::FindFunctionChecked ( FName InName ) const { UFunction* Result = FindFunction(InName); if (Result == NULL ) { UE_LOG(LogScriptCore, Fatal, TEXT("Failed to find function %s in %s" ), *InName.ToString(), *GetFullName()); } return Result; }
可以看到,这里传递给ProcessEvent
的UFunction*
就是从当前对象的UClass中得到的。
经过前面分分析可以知道,UnLua实现的函数覆写,就是把UClass中的UFunction中的原生thunk函数指针替换为FLuaInvoker::execCallLua
,而且当一个对象的BlueprintNativeEvent
和BlueprintImplementableEvent
函数被调用的时候会调用到ProcessEvent
并传入对应的UFunction*
,在ProcessEvent
中又调Invork
(调用其中的原生指针),也就是实现调用到了unlua中替换绑定的FLuaInvoker::execCallLua
,在这个函数中再转发给调用lua端的函数,从而实现了覆写函数的目的。
引擎对Android版本的支持 在之前的笔记里:Android SDK版本与Android的版本 列出了Android系统版本和API Leve版本之间的对照表。
但是UE不同的引擎版本对Android的系统支持也是不一样的,在Project Setting
-Android
中的Minimum SDK Version
中可以设置最小的SDK版本,也就是UE打包Android所支持的最低系统版本。
在UE4.25中,最低可以设置Level为19,即Android4.4,在4.25之前的引擎版本最低支持Level 9,也就是Android 2.3。
这部分的代码可以在Runtime/Android/AndroidRuntimeSettings/Classes/AndroidRuntimeSettings.h 中查看,并对比不同引擎版本的区别。
引擎对AndroidNDK的要求 UE在打包Android的时候会要求系统中具有NDK环境,但是不同的引擎版本对NDK的版本要求也不一样。
当使用不支持的NDK版本时,打包会有如下错误:
1 2 UATHelper: Packaging (Android (ETC2)): ERROR: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended) PackagingResults: Error: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended)
提示当前系统中的NDK版本不支持,并会显示支持的版本。
UE打包时对NDK版本的检测是在UBT中执行的,具体文件为UnrealBuildTool/Platform/Android/AndroidToolChain.cs
。
其中定义了当前引擎版本支持的NDK的最低和最高版本:
1 2 3 4 readonly int MinimumNDKToolchain = 210100 ;readonly int MaximumNDKToolchain = 230100 ;readonly int RecommendedNDKToolchain = 210200 ;
可以在Github上比较方便地查看不同引擎版本要求的NDK版本:UE_425_AndroidToolChain.cs
从TargetRules获取引擎版本 之前写到过在C++代码里,UE提供了几个宏可以获取引擎版本(UE版本号的宏定义 ),那么怎么在build.cs里检测引擎版本?
在UE4.19版本之前从UBT获取引擎版本比较麻烦:
1 2 3 4 5 6 7 BuildVersion Version; if (BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out Version)){ System.Console.WriteLine(Version.MajorVersion); System.Console.WriteLine(Version.MinorVersion); System.Console.WriteLine(Version.PatchVersion); }
在UE4.19及以后的引擎版本,可以通过ReadOnlyTargetRules.Version
来获得,它是ReadOnlyBuildVersion
类型,包裹了一个BuildVersion
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 namespace UnrealBuildTool { [Serializable ] public class BuildVersion { public int MajorVersion; public int MinorVersion; public int PatchVersion; public int Changelist; public int CompatibleChangelist; public bool IsLicenseeVersion; public bool IsPromotedBuild; public string BranchName; public string BuildId; public string BuildVersionString; } }
其中的MajorVersion
/MinorVersion
/PatchVersion
分别对应X.XX.X。
FPaths中Dir函数的对应路径 FPaths提供了很多EngineDir
等之类的函数,我在unlua里导出了这些符号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 print(fmt("EngineDir: {}" ,UE4.FPaths.EngineDir())) print(fmt("EngineUserDir: {}" ,UE4.FPaths.EngineUserDir())) print(fmt("EngineContentDir: {}" ,UE4.FPaths.EngineContentDir())) print(fmt("EngineConfigDir: {}" ,UE4.FPaths.EngineConfigDir())) print(fmt("EngineSavedDir: {}" ,UE4.FPaths.EngineSavedDir())) print(fmt("EnginePluginsDir: {}" ,UE4.FPaths.EnginePluginsDir())) print(fmt("RootDir: {}" ,UE4.FPaths.RootDir())) print(fmt("ProjectDir: {}" ,UE4.FPaths.ProjectDir())) print(fmt("ProjectUserDir: {}" ,UE4.FPaths.ProjectUserDir())) print(fmt("ProjectContentDir: {}" ,UE4.FPaths.ProjectContentDir())) print(fmt("ProjectConfigDir: {}" ,UE4.FPaths.ProjectConfigDir())) print(fmt("ProjectSavedDir: {}" ,UE4.FPaths.ProjectSavedDir())) print(fmt("ProjectIntermediateDir: {}" ,UE4.FPaths.ProjectIntermediateDir())) print(fmt("ProjectPluginsDir: {}" ,UE4.FPaths.ProjectPluginsDir())) print(fmt("ProjectLogDir: {}" ,UE4.FPaths.ProjectLogDir()))
他们对应的具体路径为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 EngineDir: ../../../Engine/ EngineUserDir: : /Users/imzlp/AppData/Local/UnrealEngine/4.22/ EngineContentDir: ../../../Engine/Content/ EngineConfigDir: ../../../Engine/Config/ EngineSavedDir: : /Users/imzlp/AppData/Local/UnrealEngine/4.22/Saved/ EnginePluginsDir: ../../../Engine/Plugins/ RootDir: : /Program Files/Epic Games/UE_4.22/ ProjectDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/ ProjectUserDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/ ProjectContentDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Content/ ProjectConfigDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Config/ ProjectSavedDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Saved/ ProjectIntermediateDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Intermediate/ ProjectPluginsDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Plugins/ ProjectLogDir: ../../../../../../Users/imzlp/Documents/UnrealProjectSSD/GWorldClient/Saved/Logs/
这些相对路径都是相对于引擎的exe 的路径的:
通过Commandline替换加载的ini 如项目下的DefaultEngine.ini
/DefaultGame.ini
等。 去掉Default
和ini
后缀之后是它们的baseName,可以通过下列命令行来替换:
1 2 3 4 -EngineINI=REPLACE_INI_FILE_PAT.ini -GameINI=REPLACE_INI_FILE_PAT.ini
具体实现是在FConfigCacheIni::GetDestIniFilename
中做的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 FString FConfigCacheIni::GetDestIniFilename (const TCHAR* BaseIniName, const TCHAR* PlatformName, const TCHAR* GeneratedConfigDir) { FString CommandLineSwitch = FString::Printf(TEXT("%sINI=" ), BaseIniName); FString IniFilename; if (FParse::Value(FCommandLine::Get(), *CommandLineSwitch, IniFilename) == false ) { FString Name (PlatformName ? PlatformName : ANSI_TO_TCHAR(FPlatformProperties::PlatformName())) ; if (FCString::Stristr(BaseIniName, GeneratedConfigDir) != nullptr ) { IniFilename = BaseIniName; } else { IniFilename = FString::Printf(TEXT("%s%s/%s.ini" ), GeneratedConfigDir, *Name, BaseIniName); } } FPaths::MakeStandardFilename(IniFilename); return IniFilename; }
获取当前平台信息 可以使用FPlatformProperties
来获取当前程序的平台信息。 同样是使用UE的跨平台库写法,FGenericPlatformProperties
是定义在Core/Public/GenericPlatform/GenericPlatformProperties.h
中的。
例:可以使用FPlatformProperties::PlatformName()
运行时来获取当前平台的名字。
而FPlatformProperties
的typedef
是定义在Core/Public/HAL/PlatformProperties.h
中。
在UE4.22之前,UE的跨平台库的实现方式都是创建一个泛型平台类:
1 2 3 4 struct FGenericPlatformUtils { static void GenericMethod () {} };
然后每个平台实现:
1 2 3 4 5 6 7 8 struct FWindowsPlatformUtils :public FGenericPlatformUtils{ static void GenericMethod () { } }; typedef FWindowsPlatformUtils FPlatformUtils;
在UE4.22之前,需要使用下面这种方法:
1 2 3 4 5 6 7 8 9 10 #if PLATFORM_ANDROID #include "Android/AndroidPlatformUtils.h" #elif PLATFORM_IOS #include "IOS/IOSPlatformUtils.h" #elif PLATFORM_WINDOWS #include "Windows/WindowsPlatformUtils.h" #elif PLATFORM_MAC #include "Mac/MacPlatformUtils.h" #endif
需要手动判断每个平台再进行包含,也是比较麻烦的,在4.23之后,UE引入了一个宏:COMPILED_PLATFORM_HEADER
,可以把上面的包含简化为下面的代码:
1 #include COMPILED_PLATFORM_HEADER(PlatformUtils.h)
它是定义在Runtime/Core/Public/HAL/PreprocessorHelpers.h
下的宏:
1 2 3 4 5 6 7 #if PLATFORM_IS_EXTENSION #define COMPILED_PLATFORM_HEADER(Suffix) PREPROCESSOR_TO_STRING(PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix)) #else #define COMPILED_PLATFORM_HEADER(Suffix) PREPROCESSOR_TO_STRING(PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME/PLATFORM_HEADER_NAME, Suffix)) #endif
注释已经比较说明作用了。而且它还有兄弟宏:
1 2 3 4 5 6 7 #if PLATFORM_IS_EXTENSION #define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix)) #else #define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PLATFORM_HEADER_NAME/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix)) #endif
命名有规律是多么重要的一件事…
遍历UCLASS或USTRUCT的反射成员 可以通过TFieldIterator
来遍历:
1 2 3 4 for (TFieldIterator<UProperty> PropertyIt(ProxyClass); PropertyIt; ++PropertyIt){ }
注意:4.25之后没有UProperty,变成了FProperty.
控制打包时ini的拷贝 在DeploymentContext.cs
中的DeploymentContext
函数中,有以下两行代码:
1 2 3 ReadConfigFileList(GameConfig, "Staging" , "WhitelistConfigFiles" , WhitelistConfigFiles); ReadConfigFileList(GameConfig, "Staging" , "BlacklistConfigFiles" , BlacklistConfigFiles);
这两个数组会在CopyBuildToStageingDirectory.Automation.cs
中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static Nullable<bool > ShouldStageConfigFile (DeploymentContext SC, DirectoryReference ConfigDir, FileReference ConfigFile ) { StagedFileReference StagedConfigFile = SC.GetStagedFileLocation(ConfigFile); if (SC.WhitelistConfigFiles.Contains(StagedConfigFile)) { return true ; } if (SC.BlacklistConfigFiles.Contains(StagedConfigFile)) { return false ; } }
用途就是指定哪些config会添加到包体中。 用法如下(写到DefaultGame.ini
中):
1 2 [Staging] +BlacklistConfigFiles=GWorldClient/Config/DefaultGameExtensionSettings.ini
CharCast的坑 CharCast
是定义在StringConv.h
的模板函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename To, typename From>FORCEINLINE To CharCast (From Ch) { To Result; FPlatformString::Convert(&Result, 1 , &Ch, 1 , (To)UNICODE_BOGUS_CHAR_CODEPOINT); return Result; }
就是对FPlatformString::Convert
的转发调用。
PS:UNICODE_BOGUS_CHAR_CODEPOINT
宏定义为'?'
。
FPlatformString::Convert
有两个版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 template <typename SourceEncoding, typename DestEncoding>static FORCEINLINE typename TEnableIf< TAreEncodingsCompatible<SourceEncoding, DestEncoding>::Value, DestEncoding* >::Type Convert (DestEncoding* Dest, int32 DestSize, const SourceEncoding* Src, int32 SrcSize, DestEncoding BogusChar = (DestEncoding)'?' ) { if (DestSize < SrcSize) return nullptr ; return (DestEncoding*)Memcpy(Dest, Src, SrcSize * sizeof (SourceEncoding)) + SrcSize; } template <typename SourceEncoding, typename DestEncoding>static typename TEnableIf< !TAreEncodingsCompatible<SourceEncoding, DestEncoding>::Value && TIsFixedWidthEncoding<SourceEncoding>::Value, DestEncoding* >::Type Convert (DestEncoding* Dest, int32 DestSize, const SourceEncoding* Src, int32 SrcSize, DestEncoding BogusChar = (DestEncoding)'?' ) { const int32 Size = DestSize <= SrcSize ? DestSize : SrcSize; bool bInvalidChars = false ; for (int I = 0 ; I < Size; ++I) { SourceEncoding SrcCh = Src[I]; Dest[I] = (DestEncoding)SrcCh; bInvalidChars |= !CanConvertChar<DestEncoding>(SrcCh); } if (bInvalidChars) { for (int I = 0 ; I < Size; ++I) { if (!CanConvertChar<DestEncoding>(Src[I])) { Dest[I] = BogusChar; } } LogBogusChars<DestEncoding>(Src, Size); } return DestSize < SrcSize ? nullptr : Dest + Size; }
其中关键的是第二个实现, 通过判断CanConvertChar
来检测是否能够转换字符,如果不能转换就把转换结果设置为BogusChar
,默认也就是?
,这也是把不同编码的数据转换为FString有些会显示一堆?
的原因。
1 2 3 4 5 6 7 8 9 10 11 template <typename DestEncoding, typename SourceEncoding>static bool CanConvertChar (SourceEncoding Ch) { return IsValidChar(Ch) && (SourceEncoding)(DestEncoding)Ch == Ch && IsValidChar((DestEncoding)Ch); }
所以:类似LoadFileToString
去读文件如果编码不支持,那么读出来的数据和原始文件里是不一样的。
bUsesSlate 在UE的TargetRules
中有一项属性bUsesSlate
,可以用来控制是否启用Slate,UE文档里的描述如下:
Whether the project uses visual Slate UI (as opposed to the low level windowing/messaging, which is always available).
但是我想知道是否启用对于项目打出的包有什么区别。经过测试发现,以移动端为例,bUsesSlate
的值并不会影响libUE4.so
的大小。
有影响的地方只在于 打包时的pak大小,这一点可以从两次分别打包的PakList*.txt
中得知,经过对比发现若bUsesSlate=false
,则在打包时不会把Engine\Content\Slate
下的图片资源打包。我把两个版本的PakList*.txt
都放在这里 ,有兴趣的可以看都是有哪些资源没有被打包。
下面这幅图是两个分别开启bUsesSlate
的图(左侧false
右侧true
),可以看到只有main.obb.png
的大小不一样。
可以看到默认情况下main.obb.png
减小了大概6-7M,APK的大小也减小的差不多。
Unreal Plugin Language 在UE中为移动端添加第三方模块或者修改配置文件时经常会用到AdditionalPropertiesForReceipt
,里面创建ReceiptProperty
传入的xml
文件就是UE的Unreal Plugin Language
脚本。
ReceiptProperty
的平台名称在IOS和Android上是固定的,分别是IOSPlugin
和AndroidPlugin
,不可以指定其他的名字(详见代码UEDeployIOS.cs#L1153 和UEDeployAndroid.cs#L4303 )。
1 AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin" , Path.Combine(ThirdPartyPath, "Android/PlatformUtils_UPL_Android.xml" )));
打包时Paklist文件的生成 UE打出Pak时,需要一个txt的参数传入,里面记录着要打到pak里的文件信息,直接使用UE的打包改文件会存储在:
1 C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\D+UnrealEngine+Epic+UE_4.23\PakList_microend_423-ios.txt
类似的路径下。
这个文件生成的地方为:
1 D:\UnrealEngine\Epic\UE_4.24 \Engine\Source\Programs\AutomationTool\BuildGraph\Tasks\PakFileTask.cs
在它的Execute
函数里,有通过外部传入的PakFileTaskParameters
的参数来把文件写入。
ShaderStableInfo*.scl.csv 在Cook的时候会在Cooked/PLATFORM/PROJECT_NAME/Metadata/PipelineCaches
下生成类似下面这样的文件:
1 2 3 4 ShaderStableInfo-Global-PCD3D_SM4.scl.csv ShaderStableInfo-Global-PCD3D_SM5.scl.csv ShaderStableInfo-GWorld-PCD3D_SM4.scl.csv ShaderStableInfo-GWorld-PCD3D_SM5.scl.csv
里面记录了FStableShaderKeyAndValue
结构的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct RENDERCORE_API FStableShaderKeyAndValue { FCompactFullName ClassNameAndObjectPath; FName ShaderType; FName ShaderClass; FName MaterialDomain; FName FeatureLevel; FName QualityLevel; FName TargetFrequency; FName TargetPlatform; FName VFType; FName PermutationId; FSHAHash PipelineHash; uint32 KeyHash; FSHAHash OutputHash; FStableShaderKeyAndValue() : KeyHash(0 ) { } }
作用有时间再来分析。
PE的DLL为什么需要导入库? 在ELF中,共享库所有的全局函数和变量在默认情况下都可以被其他模块使用,也就是说ELF默认导出所有的全局符号。但是在DLL中不同,PE环境下需要显式地告诉编译器我们需要导出的符号,否则编译器就默认所有符号都不导出。
在MSVC中可以使用__declspec(dllexport)
以及__declspec(dllimport)
来分别表示导出本DLL的符号以及从别的DLL中导入符号。除了上面两个属性关键字还可以定义def
文件来声明导入导出符号,def文件时连接器的链接脚本文件,可以当作链接器的输入文件,用于控制链接过程。
在我之前的一篇文章(动态链接库的使用:加载和链接 )中写到过DLL导入库的创建和使用,但是为什么DLL需要导入库而so不需要呢?前面已经回答,因为ELF是默认全导出的,PE是默认不导出的,但是我想知道原因是什么。
其实在有了上面的两个属性关键字之后不使用导入库也可以实现符号的导入和导出。
当某个PE文件被加载时。Windows加载器的其中一个任务就是把所有需要导入的函数地址确定并且将导入表中的元素调整到正确的地址,以实现动态链接的过程,导入表中有IAT,其中的每个元素对应一个被导入的符号。
编译器无法知道一个符号是从外部导入的还是本模块中定义的,所以编译器是直接产生调用指令
在__declspec
出现之前,微软提供的方法就是使用导入库,在这种情况下,对于导入函数的调用并不区分是导入函数还是导出函数,它统一地产生直接调用的指令,但是链接器在链接时会将导入函数的目标地址导向一小段桩代码(stub),由这个桩代码再将控制权交给IAT中的真正目标。
所以导入库的作用就是将编译器产生的调用命令转发到导入表的IAT中目标地址。
UCLASS的config 如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 UCLASS(config=Engine, defaultconfig) class PAPER2D_API UPaperRuntimeSettings : public UObject{ GENERATED_UCLASS_BODY() UPROPERTY(EditAnywhere, config, Category=Experimental) bool bEnableSpriteAtlasGroups; UPROPERTY(EditAnywhere, config, Category=Experimental, meta=(ConfigRestartRequired=true )) bool bEnableTerrainSplineEditing; UPROPERTY(EditAnywhere, config, Category=Settings) bool bResizeSpriteDataToMatchTextures; };
这个类是个config的类,可以从ini中读取配置,关键的地方就是UCLASS(Config=)
的东西,一般情况下是Engine
/Game
/Editor
,它们的ini文件都是Default*.ini
,如上面这个类,如果想要自己在ini中来指定它们这些参数的值,则需要写到项目的Config/DefaultEngine.ini
中:
1 2 [/Script/Paper2D.PaperRuntimeSettings] bEnableSpriteAtlasGroups = true
其中ini的Section
为该配置类的PackagePath
。
操作剪贴板Clipboard 有些需求是要能够访问到用户的粘贴板,来进行复制、和粘贴的功能。
在UE中访问粘贴板的方法如下:
1 2 3 4 5 6 FString PasteString; FPlatformApplicationMisc::ClipboardPaste(PasteString); FPlatformApplicationMisc::ClipboardCopy(TEXT("123465" ));
注意:FPlatformApplicationMisc
是定义在ApplicationCore
下的,使用时要包含该模块。
在场景中Copy/Paste的实现 在UE的场景编辑器中对一个选中的Actor进行Ctrl+C
时把拷贝的内容粘贴到一个文本编辑器里可以看到类似以下的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Begin Map Begin Level Begin Actor Class=/Script/Engine.Pawn Name=Pawn_1 Archetype=/Script/Engine.Pawn'/Script/Engine.Default__Pawn' Begin Object Class=/Script/Engine.SceneComponent Name="DefaultSceneRoot" End Object Begin Object Name="DefaultSceneRoot" RelativeLocation=(X=600.000000,Y=280.000000,Z=150.000000) bVisualizeComponent=True CreationMethod=Instance End Object RootComponent=SceneComponent'"DefaultSceneRoot"' ActorLabel="Pawn" InstanceComponents(0)=SceneComponent'"DefaultSceneRoot"' End Actor End Level Begin Surface End Surface End Map
它记录了当前拷贝的Actor
的类,位置、以及与默认对象(CDO)不一致的属性。 拷贝上面的文本,在UE的场景编辑器里粘贴,会在场景里创建出来一个一摸一样的对象。
Copy 在场景编辑器中执行Ctrl+C
会把文本拷贝到粘贴板的实现为UEditorEngine::CopySelectedActorsToClipboard
函数,其定义在EditorServer.cpp
中:
1 2 3 4 5 6 7 8 9 10 void CopySelectedActorsToClipboard ( UWorld* InWorld, const bool bShouldCut, const bool bIsMove = false , bool bWarnAboutReferences = true ) ;
调用栈为:
之后又会调用UUnrealEngine::edactCopySelected
函数(EditorActor.cpp
),在edactCopySelected
中通过构造出一个FExportObjectInnerContext
的对象收集到所选择的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class FSelectedActorExportObjectInnerContext : public FExportObjectInnerContext{ public : FSelectedActorExportObjectInnerContext() : FExportObjectInnerContext(false ) { for (UObject* InnerObj : TObjectRange<UObject>(RF_ClassDefaultObject, true , EInternalObjectFlags::PendingKill)) { UObject* OuterObj = InnerObj->GetOuter(); bool bIsChildOfSelectedActor = false ; UObject* TestParent = OuterObj; while (TestParent) { AActor* TestParentAsActor = Cast<AActor>(TestParent); if (TestParentAsActor && TestParentAsActor->IsSelected()) { bIsChildOfSelectedActor = true ; break ; } TestParent = TestParent->GetOuter(); } if (bIsChildOfSelectedActor) { InnerList* Inners = ObjectToInnerMap.Find(OuterObj); if (Inners) { Inners->Add( InnerObj ); } else { InnerList& InnersForOuterObject = ObjectToInnerMap.Add(OuterObj, InnerList()); InnersForOuterObject.Add(InnerObj); } } } } };
再通过UExporter::ExportToOutputDevice
进行序列化操作,就得到了该对象序列化之后的字符串。
Paste 把文本拷贝之后在场景中粘贴会创建出Actor的核心实现为UEditorEngine::PasteSelectedActorFromClipboard
函数,其定义在EditorServer.cpp
中:
1 2 3 4 5 6 7 8 void PasteSelectedActorsFromClipboard ( UWorld* InWorld, const FText& TransDescription, const EPasteTo PasteTo ) ;
调用栈为:
检测字符数组是UTF8还是GBK编码 基于上个笔记的需求,所以要能够区分一个字符数组是使用UTF8还是GBK编码的。
GBK gbk 的第一字节是高位为 1 的,第 2 字节可能高位为 0 。这种情况一定是 gbk ,因为 UTF8 对 >127 的编码一定每个字节高位为 1 。
UTF8 UTF8 是兼容 ascii 的,所以 0~127 就和 ascii 完全一致了。
UTF8的中文文字一定编码成三个字节:
汉字以及汉字标点(包括日文汉字等),在 UTF8 中一定被编码成:1110**** 10****** 10******
。
如上个笔记中的鸡
字,其UTF8的编码为11101001 1011100 10100001
,符合上面的规则。
其他 相关资料:
工具:
UTF8编码的字符数组转FString 从网络收过来的数据流是以字节形式接收的,但是对于不同使用UTF8
或者GBK
编码的字符来说,他们是由多个字节组成的,如鸡
这个汉字的UTF8编码为0xE9B8A1
:
1 2 char Chicken[] = { (char )0xE9 ,(char )0xB8 ,(char )0xA1 ,`\0 `};FString ChickenChnese (UTF8_TO_TCHAR(Name)) ;
因为Chicken
这个数据有四个字节,前三个字节是鸡
这个汉字的UTF8编码,最后一个字节是\0
表示结束符。
之前想的是把这个数组再表示为UTF8的字符,但是这里混淆了一个概念:这个数组本身就是UTF8编码的信息了,所以应该是把它从UTF8转换为TCHAR
表示的字符,要使用UE的UTF8_TO_CHAR
。
因为UTF8兼容ASCII编码,所以可以混用:
1 2 3 ANSICHAR TestArray[] = { 'a' ,'b' ,'c' , (char )0xE9 ,(char )0xB8 ,(char )0xA1 ,'d' ,'e' ,'1' ,'\0' }; FString TestStr (UTF8_TO_TCHAR(TestArray)) ;
编辑器SpawnActor 可以使用UEditorEngine
中的SpawnActor
函数。
1 2 AActor* UEditorEngine::AddActor (ULevel* InLevel, UClass* Class, const FTransform& Transform, bool bSilent, EObjectFlags InObjectFlags)
使用这个方法Spawn出来的会自动选中。
虚拟制片的流程 最近想业余研究一下虚拟制片的工作流程,准备研究一下弄个个人版的方案玩玩,收集一些资料。
硬件要求:
Valve的定位基站(HTC Vive)
Vive Tracker一个
相机+视频采集卡/网络摄像头
绿幕
软件要求:
Unreal Engine
SteamVR
OBS
额外注意事项:
拍摄时应该要和引擎内的帧率同步(高级点的相机)
拍摄时人物不应离绿幕太近会有反光的问题
EditCondition支持表达式 在UPROPERTY里可以对一个属性被设置的条件,比如某个bool开启时才允许编辑:
1 2 3 4 URPOPERTY() bool EnableInput;UPROPERTY(meta=(EditCondition="EnableInput" )) EInputMode InputMode;
也可以对其使用取反操作:
1 2 3 4 URPOPERTY() bool EnableInput;UPROPERTY(meta=(EditCondition="!EnableInput" )) EInputMode InputMode;
UE的文档介绍里说EditContion
是支持表达式的:
The EditCondition meta tag is no longer limited to a single boolean property. It is now evaluated using a full-fledged expression parser, meaning you can include a full C++ expression.
获取资源依赖关系 最近有个需求要获取UE里资源的引用关系,类似UE的Reference Viewer
的操作,既然知道了Reference Viewer中有想要的那么就去它的模块里面翻代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 UEdGraphNode_Reference* UEdGraph_ReferenceViewer::RecursivelyConstructNodes (bool bReferencers, UEdGraphNode_Reference* RootNode, const TArray<FAssetIdentifier>& Identifiers, const FIntPoint& NodeLoc, const TMap<FAssetIdentifier, int32>& NodeSizes, const TMap<FName, FAssetData>& PackagesToAssetDataMap, const TSet<FName>& AllowedPackageNames, int32 CurrentDepth, TSet<FAssetIdentifier>& VisitedNames) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry" ); TArray<FAssetIdentifier> ReferenceNames; TArray<FAssetIdentifier> HardReferenceNames; if ( bReferencers ) { for (const FAssetIdentifier& AssetId : Identifiers) { AssetRegistryModule.Get().GetReferencers(AssetId, HardReferenceNames, GetReferenceSearchFlags(true )); AssetRegistryModule.Get().GetReferencers(AssetId, ReferenceNames, GetReferenceSearchFlags(false )); } } else { for (const FAssetIdentifier& AssetId : Identifiers) { AssetRegistryModule.Get().GetDependencies(AssetId, HardReferenceNames, GetReferenceSearchFlags(true )); AssetRegistryModule.Get().GetDependencies(AssetId, ReferenceNames, GetReferenceSearchFlags(false )); } } }
通过FAssetRegistryModule
模块去拿就可以了,FAssetIdentifier
中只需要有PackageName
即可,这个PackageName是LongPackageName
,不是PackagePath
。
地图的存储和加载 存储栈:
加载栈:
DataTable 要创建一个可以用于创建DataTable
的结构需要继承于FTableRowBase
,如果要在编辑器中可编辑,该结构中的UPROPERTY不要包含Category
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #pragma once #include "CoreMinimal.h" #include "Engine/DataTable.h" #include "EnemyProperty.generated.h" USTRUCT(BlueprintType) struct FRoleProperty : public FTableRowBase{ GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 AttackValue; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Defense; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 HP; };
FCommandLine过滤模式 FCommandLine
是UE封装的启动参数的管理类,在Launch
模块下的FEngineLoop::PreInit
中被初始化(FCommandLine::Set
)为程序启动的CmdLine
。FCommandLine
支持Append
和Parser
这是比较常用的功能,但是今天要说的是另外一个:CommandLine的白名单和黑名单模式。 考虑这样的需求:在游戏开发阶段,有很多参数可以在启动时配置,方便测试,但是在发行时需要把启动时从命令行读取配置的功能给去掉,强制使用我们设置的默认参数。
OVERRIDE_COMMANDLINE_WHITELIST 怎么才是最简单的办法?其实这一点根本不需要自己去处理这部分的内容,因为FCommandLine
支持白名单模式。 开启的方法为在target.cs
中增加WANTS_COMMANDLINE_WHITELIST
宏:
1 GlobalDefinitions.Add("WANTS_COMMANDLINE_WHITELIST=1" );
如果只开启这个,则默认情况下不允许接收任何外部传入的参数,但具有默认的参数-fullscreen /windowed
。 当然,我们可以自己指定这个WHITLIST
,那么就是在target.cs
中使用OVERRIDE_COMMANDLINE_WHITELIST
宏:
1 GlobalDefinitions.Add("OVERRIDE_COMMANDLINE_WHITELIST=\"-arg1 -arg2 -arg3 -arg4\"" );
这样就只有我们指定的这些参数才可以在运行时接收,防止玩家恶意传递参数导致游戏出错。 这部分的代码是写在Misc/CommandLine.cpp 中的。
Example :
1 2 3 GlobalDefinitions.Add("WANTS_COMMANDLINE_WHITELIST=1" ); GlobalDefinitions.Add("OVERRIDE_COMMANDLINE_WHITELIST=\"-e -c\"" );
这里只指定了-e
/-c
两个参数,如果程序在启动时被指定了其他的参数,如:
1 D:/UnrealEngine/EngineSource/UE_4.21_Source/Engine/Binaries/Win64/UE4Launcher.exe -e -c -d
在FCommandLine::Get()
只能得到-e
和-c
,-d
和exe路径都被丢弃了,只能用在指定开关,不能用来传递值。
FILTER_COMMANDLINE_LOGGING 还可以在target.cs
中指定FILTER_COMMANDLINE_LOGGING
来控制对FCommandLine::LoggingCmdLine
的过滤:
1 GlobalDefinitions.Add("FILTER_COMMANDLINE_LOGGING=\"-arg1 -arg2 -arg3 -arg4\"" );
它会在程序从命令行中接收的参数中过滤所指定的参数,类似于参数的黑名单。
Example :
1 2 GlobalDefinitions.Add("WANTS_COMMANDLINE_WHITELIST=1" ); GlobalDefinitions.Add("FILTER_COMMANDLINE_LOGGING=\"-e -c\"" );
在运行时传入参数:
1 D:/UnrealEngine/EngineSource/UE_4.21 _Source/Engine/Binaries/Win64/UE4Launcher.exe -e -c -d
得到的是:
1 -D:/UnrealEngine/EngineSource/UE_4.21 _Source/Engine/Binaries/Win64/UE4Launcher.exe -d
-e
和-c
被过滤掉了。
PIE的坑 UE在PIE中运行是和Standalone模式是不同的,在PIE运行时可以通过监听FEditorDelegates
下的PreBeginPIE
/BeginPIE
以及PrePIEEnded
/EndPIE
等代理来检测PIE模式下的游戏运行和退出。但是 ,PIE的PrePIEEnded
和EndPIE
都是先于GameInstance
的Shutdown
函数的,在一些情况下,如果PIEEnd中做了一些清理操作而在GameInstance的Shutdown函数中有使用的话会有问题。 解决办法为绑定FGameDelegates
的EndPlayMapDelegate
:
1 FGameDelegates::Get().GetEndPlayMapDelegate().AddRaw(GLuaCxt, &FLuaContext::OnEndPlayMap);
UPARAM(ref) UE中使用TArray<bool>&
是作为返回值的,在蓝图中就是在节点右侧。
1 2 UFUNCTION(BlueprintCallable) static void ModifySomeArray (TArray<bool > &BooleanArray) ;
如果想要传入已有的对象,则需要UPARAM(ref)
:
1 2 UFUNCTION(BlueprintCallable) static void ModifySomeArray (UPARAM(ref) TArray<bool > &BooleanArray) ;
the file couldn’t be loaded by the OS. 启动引擎时如果具有类似的Crash信息:
1 ModuleManager: Unable to load module 'G:/UE_4.22/Engine/Binaries/Win64/UE4Editor-MeshUtilities.dll' because the file couldn't be loaded by the OS.
把引擎的DDC 删掉之后重启引擎即可。DDC 的目录:
1 2 C:\Program Files\Epic Games\UE_4.22\Engine\DerivedDataCache C:\Users\imzlp\AppData\Local\UnrealEngine\4.22\DerivedDataCache
把上面两个目录都删掉。 如果启动项目时提示的是工程中的模块,则把工程和插件下的Binaries
和Intermediate
都删了重新编译生成。
PURE_VIRTUAL macro 在UE的代码中看到一些虚函数使用UE的PURE_VIRTUAL
宏来指定,类似下面这种代码:
1 virtual void GameInitialize () PURE_VIRTUAL (IINetGameInstance::GameInitialize,) ;
看起来有点奇怪,看一下它的代码:
1 2 3 4 5 #if CHECK_PUREVIRTUALS #define PURE_VIRTUAL(func,extra) =0; #else #define PURE_VIRTUAL(func,extra) { LowLevelFatalError(TEXT("Pure virtual not implemented (%s)" ), TEXT(#func)); extra } #endif
相当于给了纯虚函数一个默认实现,在错误调用时能够看到信息。
使用tcping检测端口是否开放 可以使用tcping 这个工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ tcping 127.0.0.1 10086 Probing 127.0.0.1:10086/tcp - Port is open - time=10.775ms Probing 127.0.0.1:10086/tcp - Port is open - time=0.428ms Probing 127.0.0.1:10086/tcp - Port is open - time=0.344ms Probing 127.0.0.1:10086/tcp - Port is open - time=0.317ms Ping statistics for 127.0.0.1:10086 4 probes sent. 4 successful, 0 failed. (0.00% fail) Approximate trip times in milli-seconds: Minimum = 0.317ms, Maximum = 10.775ms, Average = 2.966ms Probing 127.0.0.1:10086/tcp - No response - time=2002.595ms Probing 127.0.0.1:10086/tcp - No response - time=2000.168ms Probing 127.0.0.1:10086/tcp - No response - time=2000.202ms Probing 127.0.0.1:10086/tcp - No response - time=2001.308ms Ping statistics for 127.0.0.1:10086 4 probes sent. 0 successful, 4 failed. (100.00% fail) Was unable to connect, cannot provide trip statistics
成员模板函数特化不要放在class scope 如下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Template { public : template <typename T> Template& operator >>(T& Value) { return *this ; } template <> Template& operator >><bool >(bool & Value) { return *this ; } };
声明了模板函数operator>>
,而且添加了一个bool的特化,这个代码在Windows下编译没有问题,但是在打包Android时会产生错误:
1 error: explicit specialization of 'operator>>' in class scope
说时显式特化写在了类作用域内,解决办法是把特化的版本放到类之外:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Template { public : template <typename T> Template& operator >>(T& Value) { return *this ; } }; template <>Template& Template::operator >><bool >(bool & Value) { return *this ; }
Runtime模块包含Editor模块的错误 如果打包时有下列错误:
1 2 UnrealBuildTool.Main: ERROR: Missing precompiled manifest for 'EditorWidgets'. This module was most likely not flagged for being included in a precompiled build - set 'PrecompileForTargets = PrecompileTargetsType.Any;' in EditorWidgets.build.cs to override. UnrealBuildTool.Main: BuildException: Missing precompiled manifest for 'EditorWidgets'. This module was most likely not flagged for being included in a precompiled build - set 'PrecompileForTargets = PrecompileTargetsType.Any;' in EditorWidgets.build.cs to override.
我这里的错误提示是EditorWidgets
是个预编译模块。 这个错误的原因是在Runtime
的模块中添加了UnrealEd
模块,因为它是属于Editor的,打包时不会把Editor的模块打包进来,所以就会有现在错误。
注意:一定不要在Runtime的模块中包含Editor或者Developer的模块,这个是Epic的EULA限制,如果需要用到Editor或者Developer的东西,则自己在插件或者工程下新建一个Editor
或者Developer
才行。
获取蓝图添加的所有接口 拿到UBlueprint
之后可以通过获取ImplementedInterfaces
来访问:
1 2 3 4 5 6 7 8 for (const auto & InterfaceItem : Blueprint->ImplementedInterfaces){ if (InterfaceItem.Interface.Get()->IsChildOf(UUnLuaInterface::StaticClass())) { bImplUnLuaInterface = true ; break ; } }
UnrealFrontEnd DeviceLog 之前在PC上看移动端的Log的方式是是用Logcat,但是发现UE其实提供了工具,就是UnrealFrontLog中的DeviceLog
: 最下面的那一行也可以执行控制台命令,很方便。
PS:我测试了在PC上连接Android设备没有问题,但是连上IOS不显示,在Mac上连接IOS没有问题,不过不显示Log,但可以执行命令。
获取某个类的所有子类 查找所有继承自某个UClass的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include "UObject/UObjectIterator.h" TArray<UClass*> UNetGameInstance::GetAllSubsystem () { TArray<UClass*> result; for (TObjectIterator<UClass> It; It; ++It) { if (It->IsChildOf(UNetSubsystemBase::StaticClass()) && !It->HasAnyClassFlags(CLASS_Abstract)) { result.Add(*It); } } return result; }
4.22.3打包IOS Crash的问题 在下列环境下:
macOS Mojave 10.14.5
XCode 11.2.1(11B500)
UE4.22.3
iPhone 7 IOS13.3.1
在这个环境下打包出来的IOS在运行时会Crash,但是换到4.23.1就没有这个问题。 Crash的Log:GWorld 2020-3-12 6-58-PM.crash
IOS贴图非2次幂的显示问题 默认情况下在iOS里使用非2次幂大小的贴图会有下列问题: 提示的是See Power of Two Settings in Texture Editor 。 这是因为使用到的贴图的大小不是2的次幂。
解决办法: 在编辑器中打开Texture,将Texture
-Power Of Two Mode
改成Pad to power of two
,这样会填充贴图为2次幂大小。
如果不想要使用Power Of Two Mode
也可以修改Compression
-Compression Settings
为USerInterface2D
,但是非2次幂的贴图大小会有性能问题(在iPhone6(A7处理器)中只能设置为填充才可以)。
ES2.0最多75根骨骼的限制 因为移动平台缺少32位的索引支持,所以最多支持65k个顶点和75根骨骼。 但是可以通过拆分骨骼模型的材质来实现,每个材质支持75根,这是单次drawcall的限制,分成不同的批次就可以了。
PS:不能用uniform了,换其他方式,比如VTF,也可以实现超过75根骨骼。
在UE中如果在ES2.0中(目前UE的ES3.0也是75)想要更高的骨骼数量,需要拆分模型使用的材质:
Warning: SkeletalMesh SK_m0146b0003, is not supported for current feature level (ES3_1) and will not be rendered. NumBones 78 (supported 75), NumBoneInfluences: 4
材质和模型插槽的关系:
使用HTTP请求下载文件的坑和技巧 使用HTTP可以请求下载文件,response的结果就是文件的内容。 在下载一个文件之前可以先使用HEAD
请求来只获取头,可以从Content-Length
头获取到文件的大小。
1 2 3 4 5 6 7 TSharedRef<IHttpRequest> HttpHeadRequest = FHttpModule::Get().CreateRequest(); HttpHeadRequest->OnHeaderReceived().BindUObject(this , &UDownloadProxy::OnRequestHeadHeaderReceived); HttpHeadRequest->OnProcessRequestComplete().BindUObject(this , &UDownloadProxy::OnRequestHeadComplete); HttpHeadRequest->SetURL(InternalDownloadFileInfo.URL); HttpHeadRequest->SetVerb(TEXT("HEAD" )); HttpHeadRequest->ProcessRequest();
在UE中需要通过监听HTTP请求的OnHeaderReceived
派发来获得想要的头数据:
1 2 3 4 5 6 7 void UDownloadProxy::OnRequestHeadHeaderReceived (FHttpRequestPtr RequestPtr, const FString& InHeaderName, const FString& InNewHeaderValue) { if (InHeaderName.Equals(TEXT("Content-Length" ))) { InternalDownloadFileInfo.Size = UKismetStringLibrary::Conv_StringToInt(InNewHeaderValue); } }
之后就可以用Get
方法来请求文件了:
1 2 3 4 5 6 7 8 9 TSharedRef<IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest(); HttpRequest->OnRequestProgress().BindUObject(this , &UDownloadProxy::OnDownloadProcess, bIsSlice?EDownloadType::Slice:EDownloadType::Start); HttpRequest->OnProcessRequestComplete().BindUObject(this , &UDownloadProxy::OnDownloadComplete); HttpRequest->SetURL(InternalDownloadFileInfo.URL); HttpRequest->SetVerb(TEXT("GET" )); RangeArgs = TEXT("bytes=0-" )+FString::FromInt(FileTotalByte); HttpRequest->SetHeader(TEXT("Range" ), RangeArgs); HttpRequest->ProcessRequest();
其中Range头的格式为:Byte=0-
指请求整个文件的大小,Byte=0-99
则是请求前100byte,注意请求的范围不要超过文件大小 ,不然会有400错误。 通过控制HTTP请求的Range
头,我们可以指定下载文件的任意部分,可以实现暂停继续/分片下载。
在UE中使用HTTP请求一个大文件的时候,如果该请求没有结束就去拿response的结果一定要注意一个问题:那就是Response的Content数据Payload
是一个TArray
动态数组,当Content的内容不断地被写入,会导致容器的Reserve
也就是内存重新分配,获取该数组的内存地址是非常危险的。
所以建议在HTTP请求时先对response的Content的Payload
进行Reserve
使其能够容纳足够数量的数据,缺点就是会一次性占用整个文件的内存。 解决内存占用的办法就是通过Http
请求的Range
来实现分片下载(也就是把一个大文件分成数个小块,一块一块地下载),从而降低内存占用,
当下载文件后,通常还有进行文件校验的操作,等文件下载完之后再执行校验(如MD5计算)时间会很长,所以要解决校验的时间问题,想过开一个线程去计算,但是开线程只解决了不阻塞主线程,不会加速MD5的计算过程,后来想到MD5
是摘要计算,进而联想到可不可以边下边进行MD5计算,根据没有全新的轮子定理 (我瞎掰的),我查到了OpenSSL中的MD5实现支持使用MD5_Update
来增量计算,所以这个问题就迎刃而解了,具体看我前面的笔记MD5的分片校验 。
基于上面这些内容,可以实现一个简陋的下载器功能了,可作为游戏中的下载组件,虽然看似简单,但是设计一个合理的结构和没有bug的版本还是要花点功夫的。 我把上面介绍的内容写成了一个插件:
HTTP的分片请求在服务端的Log:
资料文档:
MD5的分片校验 有一个需求:对下载的文件执行MD5运算。但是当文件比较大的时候如果等文件下载完再执行校验耗时会很长。
所以我想有没有办法边下边进行MD5的计算,因为知道MD5是基于摘要的,所以觉得边下边校验的方法应该可行。我查了一下相关的实现,找到OpenSSL
中有相关的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #ifndef HEADER_MD5_H #define HEADER_MD5_H #include <openssl/e_os2.h> #include <stddef.h> #ifdef __cplusplus extern "C" {#endif #ifdef OPENSSL_NO_MD5 #error MD5 is disabled. #endif #if defined(__LP32__) #define MD5_LONG unsigned long #elif defined(OPENSSL_SYS_CRAY) || defined(__ILP64__) #define MD5_LONG unsigned long #define MD5_LONG_LOG2 3 #else #define MD5_LONG unsigned int #endif #define MD5_CBLOCK 64 #define MD5_LBLOCK (MD5_CBLOCK/4) #define MD5_DIGEST_LENGTH 16 typedef struct MD5state_st { MD5_LONG A,B,C,D; MD5_LONG Nl,Nh; MD5_LONG data[MD5_LBLOCK]; unsigned int num; } MD5_CTX; #ifdef OPENSSL_FIPS int private_MD5_Init (MD5_CTX *c) ;#endif int MD5_Init (MD5_CTX *c) ;int MD5_Update (MD5_CTX *c, const void *data, size_t len) ;int MD5_Final (unsigned char *md, MD5_CTX *c) ;unsigned char *MD5 (const unsigned char *d, size_t n, unsigned char *md) ;void MD5_Transform (MD5_CTX *c, const unsigned char *b) ;#ifdef __cplusplus } #endif #endif
其中提供的MD5计算可以分开操作的有三个函数:
1 2 3 int MD5_Init (MD5_CTX *c) ;int MD5_Update (MD5_CTX *c, const void *data, size_t len) ;int MD5_Final (unsigned char *md, MD5_CTX *c) ;
其中的MD5_Update
就是我们需要的函数。
所以使用的伪代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 MD5_CTX Md5CTX; void Request () { MD5_Init(&Md5CTX); } void TickRequestProgress (char * InData,uint32 InLength) { MD5_Update(&Md5CTX,InData,InLength); } void RequestCompleted () { unsigned char digest[16 ] = { 0 }; MD5_Final(digest, &Md5CTX); char md5string[33 ]; for (int i = 0 ; i < 16 ; ++i) std ::sprintf (&md5string[i * 2 ], "%02x" , (unsigned int )digest[i]); pritf("MD5:%s" ,md5string); }
这样当文件下载完,MD5计算就完成了。
注:在UE4中(~4.24)提供的OpenSSL在Win下只支持到VS2015,可以自己把这个限制给去掉(VS2015的链接库在VS2017中使用也没有问题)。
当我们使用FPaths::ProjectDir()
的是否考虑过它是怎么实现在不同的平台上为不同的路径的呢? 首先看一下FPaths::ProjectDir()
的代码:
1 2 3 4 5 6 FString FPaths::ProjectDir () { return FString(FPlatformMisc::ProjectDir()); }
可以看到它是FPlatformMisc
的一层转发,继续深入代码去找FPlatformMisc::ProjectDir
的实现,如果用VS的Go to definition
可以看到一堆的文件里都有FPlatformMisc
的定义:
FPlatformMisc
只是一个类型定义(typedef),通过平台宏来判断,当编译为不同的平台是会把FPlatformMisc
通过typedef
为目标平台的类。 进行平台判断的代码在Runtime/Core/Public/HAL/PlatformMisc.h
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #pragma once #include "CoreTypes.h" #include "GenericPlatform/GenericPlatformMisc.h" #if PLATFORM_WINDOWS #include "Windows/WindowsPlatformMisc.h" #elif PLATFORM_PS4 #include "PS4/PS4Misc.h" #elif PLATFORM_XBOXONE #include "XboxOne/XboxOneMisc.h" #elif PLATFORM_MAC #include "Mac/MacPlatformMisc.h" #elif PLATFORM_IOS #include "IOS/IOSPlatformMisc.h" #elif PLATFORM_LUMIN #include "Lumin/LuminPlatformMisc.h" #elif PLATFORM_ANDROID #include "Android/AndroidMisc.h" #elif PLATFORM_HTML5 #include "HTML5/HTML5PlatformMisc.h" #elif PLATFORM_QUAIL #include "Quail/QuailPlatformMisc.h" #elif PLATFORM_LINUX #include "Linux/LinuxPlatformMisc.h" #elif PLATFORM_SWITCH #include "Switch/SwitchPlatformMisc.h" #endif
其中的每一个平台的*PlatformMisc.h
文件中都有FPlatformMisc
的类型定义(typedef
),各个平台代码文件都是存放在Runtime/Core/Public
下,每个平台有自己的目录。 而且,所有的*PlatformMisc
类都继承自一个FGenericPlatformMisc
的类,作为通用平台的接口,如WindowsPlatformMisc
:
1 2 3 4 5 struct CORE_API FWindowsPlatformMisc : public FGenericPlatformMisc{}
FGenericPlatformMisc
中声明了我们常用的ProjectDir
函数,供各个平台来独立实现,这样在通过FPlatformMisc
来调用的时候就是所编译的目标平台的实现,这是UE实现跨平台代码的思路。
为APK添加外部存储独写权限 在Project Settings
-Platform
-Android
-Advanced APK Packaging
-Extra Permissions
下添加:
1 2 android.permission.WRITE EXTERNAL STORAGE android.permission.READ_EXTERNAL_STORAGE
Android写入文件 当调用FFileHelper::SaveArrayToFile
时:
1 FFileHelper::SaveArrayToFile(TArrayView<const uint8>(data, delta), *path, &IFileManager::Get(), EFileWrite::FILEWRITE_Append));
在该函数内部会创建一个FArchive
的对象来管理当前文件,其内部具有一个IFileHandle
的对象Handle
,在Android平台上是FFileHandleAndroid
。
在FArchive
中写入文件调用的是Serialize
,它又会调用Handle
的Write
:
1 2 3 4 bool FArchiveFileWriterGeneric::WriteLowLevel ( const uint8* Src, int64 CountToWrite ) { return Handle->Write( Src, CountToWrite ); }
Android的Write的实现为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 virtual bool Write (const uint8* Source, int64 BytesToWrite) override { CheckValid(); if (nullptr != File->Asset) { return false ; } bool bSuccess = true ; while (BytesToWrite) { check(BytesToWrite >= 0 ); int64 ThisSize = FMath::Min<int64>(READWRITE_SIZE, BytesToWrite); check(Source); if (__pwrite(File->Handle, Source, ThisSize, CurrentOffset) != ThisSize) { bSuccess = false ; break ; } CurrentOffset += ThisSize; Source += ThisSize; BytesToWrite -= ThisSize; } Length = FMath::Max(Length, CurrentOffset); return bSuccess; }
可以看到是每次1M往文件里存的。
Android写入文件错误码对照
根据 error code number 查找 error string.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 #ifndef __BIONIC_ERRDEF #error "__BIONIC_ERRDEF must be defined before including this file" #endif __BIONIC_ERRDEF( 0 , 0 , "Success" ) __BIONIC_ERRDEF( EPERM , 1 , "Operation not permitted" ) __BIONIC_ERRDEF( ENOENT , 2 , "No such file or directory" ) __BIONIC_ERRDEF( ESRCH , 3 , "No such process" ) __BIONIC_ERRDEF( EINTR , 4 , "Interrupted system call" ) __BIONIC_ERRDEF( EIO , 5 , "I/O error" ) __BIONIC_ERRDEF( ENXIO , 6 , "No such device or address" ) __BIONIC_ERRDEF( E2BIG , 7 , "Argument list too long" ) __BIONIC_ERRDEF( ENOEXEC , 8 , "Exec format error" ) __BIONIC_ERRDEF( EBADF , 9 , "Bad file descriptor" ) __BIONIC_ERRDEF( ECHILD , 10 , "No child processes" ) __BIONIC_ERRDEF( EAGAIN , 11 , "Try again" ) __BIONIC_ERRDEF( ENOMEM , 12 , "Out of memory" ) __BIONIC_ERRDEF( EACCES , 13 , "Permission denied" ) __BIONIC_ERRDEF( EFAULT , 14 , "Bad address" ) __BIONIC_ERRDEF( ENOTBLK , 15 , "Block device required" ) __BIONIC_ERRDEF( EBUSY , 16 , "Device or resource busy" ) __BIONIC_ERRDEF( EEXIST , 17 , "File exists" ) __BIONIC_ERRDEF( EXDEV , 18 , "Cross-device link" ) __BIONIC_ERRDEF( ENODEV , 19 , "No such device" ) __BIONIC_ERRDEF( ENOTDIR , 20 , "Not a directory" ) __BIONIC_ERRDEF( EISDIR , 21 , "Is a directory" ) __BIONIC_ERRDEF( EINVAL , 22 , "Invalid argument" ) __BIONIC_ERRDEF( ENFILE , 23 , "File table overflow" ) __BIONIC_ERRDEF( EMFILE , 24 , "Too many open files" ) __BIONIC_ERRDEF( ENOTTY , 25 , "Not a typewriter" ) __BIONIC_ERRDEF( ETXTBSY , 26 , "Text file busy" ) __BIONIC_ERRDEF( EFBIG , 27 , "File too large" ) __BIONIC_ERRDEF( ENOSPC , 28 , "No space left on device" ) __BIONIC_ERRDEF( ESPIPE , 29 , "Illegal seek" ) __BIONIC_ERRDEF( EROFS , 30 , "Read-only file system" ) __BIONIC_ERRDEF( EMLINK , 31 , "Too many links" ) __BIONIC_ERRDEF( EPIPE , 32 , "Broken pipe" ) __BIONIC_ERRDEF( EDOM , 33 , "Math argument out of domain of func" ) __BIONIC_ERRDEF( ERANGE , 34 , "Math result not representable" ) __BIONIC_ERRDEF( EDEADLK , 35 , "Resource deadlock would occur" ) __BIONIC_ERRDEF( ENAMETOOLONG , 36 , "File name too long" ) __BIONIC_ERRDEF( ENOLCK , 37 , "No record locks available" ) __BIONIC_ERRDEF( ENOSYS , 38 , "Function not implemented" ) __BIONIC_ERRDEF( ENOTEMPTY , 39 , "Directory not empty" ) __BIONIC_ERRDEF( ELOOP , 40 , "Too many symbolic links encountered" ) __BIONIC_ERRDEF( ENOMSG , 42 , "No message of desired type" ) __BIONIC_ERRDEF( EIDRM , 43 , "Identifier removed" ) __BIONIC_ERRDEF( ECHRNG , 44 , "Channel number out of range" ) __BIONIC_ERRDEF( EL2NSYNC , 45 , "Level 2 not synchronized" ) __BIONIC_ERRDEF( EL3HLT , 46 , "Level 3 halted" ) __BIONIC_ERRDEF( EL3RST , 47 , "Level 3 reset" ) __BIONIC_ERRDEF( ELNRNG , 48 , "Link number out of range" ) __BIONIC_ERRDEF( EUNATCH , 49 , "Protocol driver not attached" ) __BIONIC_ERRDEF( ENOCSI , 50 , "No CSI structure available" ) __BIONIC_ERRDEF( EL2HLT , 51 , "Level 2 halted" ) __BIONIC_ERRDEF( EBADE , 52 , "Invalid exchange" ) __BIONIC_ERRDEF( EBADR , 53 , "Invalid request descriptor" ) __BIONIC_ERRDEF( EXFULL , 54 , "Exchange full" ) __BIONIC_ERRDEF( ENOANO , 55 , "No anode" ) __BIONIC_ERRDEF( EBADRQC , 56 , "Invalid request code" ) __BIONIC_ERRDEF( EBADSLT , 57 , "Invalid slot" ) __BIONIC_ERRDEF( EBFONT , 59 , "Bad font file format" ) __BIONIC_ERRDEF( ENOSTR , 60 , "Device not a stream" ) __BIONIC_ERRDEF( ENODATA , 61 , "No data available" ) __BIONIC_ERRDEF( ETIME , 62 , "Timer expired" ) __BIONIC_ERRDEF( ENOSR , 63 , "Out of streams resources" ) __BIONIC_ERRDEF( ENONET , 64 , "Machine is not on the network" ) __BIONIC_ERRDEF( ENOPKG , 65 , "Package not installed" ) __BIONIC_ERRDEF( EREMOTE , 66 , "Object is remote" ) __BIONIC_ERRDEF( ENOLINK , 67 , "Link has been severed" ) __BIONIC_ERRDEF( EADV , 68 , "Advertise error" ) __BIONIC_ERRDEF( ESRMNT , 69 , "Srmount error" ) __BIONIC_ERRDEF( ECOMM , 70 , "Communication error on send" ) __BIONIC_ERRDEF( EPROTO , 71 , "Protocol error" ) __BIONIC_ERRDEF( EMULTIHOP , 72 , "Multihop attempted" ) __BIONIC_ERRDEF( EDOTDOT , 73 , "RFS specific error" ) __BIONIC_ERRDEF( EBADMSG , 74 , "Not a data message" ) __BIONIC_ERRDEF( EOVERFLOW , 75 , "Value too large for defined data type" ) __BIONIC_ERRDEF( ENOTUNIQ , 76 , "Name not unique on network" ) __BIONIC_ERRDEF( EBADFD , 77 , "File descriptor in bad state" ) __BIONIC_ERRDEF( EREMCHG , 78 , "Remote address changed" ) __BIONIC_ERRDEF( ELIBACC , 79 , "Can not access a needed shared library" ) __BIONIC_ERRDEF( ELIBBAD , 80 , "Accessing a corrupted shared library" ) __BIONIC_ERRDEF( ELIBSCN , 81 , ".lib section in a.out corrupted" ) __BIONIC_ERRDEF( ELIBMAX , 82 , "Attempting to link in too many shared libraries" ) __BIONIC_ERRDEF( ELIBEXEC , 83 , "Cannot exec a shared library directly" ) __BIONIC_ERRDEF( EILSEQ , 84 , "Illegal byte sequence" ) __BIONIC_ERRDEF( ERESTART , 85 , "Interrupted system call should be restarted" ) __BIONIC_ERRDEF( ESTRPIPE , 86 , "Streams pipe error" ) __BIONIC_ERRDEF( EUSERS , 87 , "Too many users" ) __BIONIC_ERRDEF( ENOTSOCK , 88 , "Socket operation on non-socket" ) __BIONIC_ERRDEF( EDESTADDRREQ , 89 , "Destination address required" ) __BIONIC_ERRDEF( EMSGSIZE , 90 , "Message too long" ) __BIONIC_ERRDEF( EPROTOTYPE , 91 , "Protocol wrong type for socket" ) __BIONIC_ERRDEF( ENOPROTOOPT , 92 , "Protocol not available" ) __BIONIC_ERRDEF( EPROTONOSUPPORT, 93 , "Protocol not supported" ) __BIONIC_ERRDEF( ESOCKTNOSUPPORT, 94 , "Socket type not supported" ) __BIONIC_ERRDEF( EOPNOTSUPP , 95 , "Operation not supported on transport endpoint" ) __BIONIC_ERRDEF( EPFNOSUPPORT , 96 , "Protocol family not supported" ) __BIONIC_ERRDEF( EAFNOSUPPORT , 97 , "Address family not supported by protocol" ) __BIONIC_ERRDEF( EADDRINUSE , 98 , "Address already in use" ) __BIONIC_ERRDEF( EADDRNOTAVAIL , 99 , "Cannot assign requested address" ) __BIONIC_ERRDEF( ENETDOWN , 100 , "Network is down" ) __BIONIC_ERRDEF( ENETUNREACH , 101 , "Network is unreachable" ) __BIONIC_ERRDEF( ENETRESET , 102 , "Network dropped connection because of reset" ) __BIONIC_ERRDEF( ECONNABORTED , 103 , "Software caused connection abort" ) __BIONIC_ERRDEF( ECONNRESET , 104 , "Connection reset by peer" ) __BIONIC_ERRDEF( ENOBUFS , 105 , "No buffer space available" ) __BIONIC_ERRDEF( EISCONN , 106 , "Transport endpoint is already connected" ) __BIONIC_ERRDEF( ENOTCONN , 107 , "Transport endpoint is not connected" ) __BIONIC_ERRDEF( ESHUTDOWN , 108 , "Cannot send after transport endpoint shutdown" ) __BIONIC_ERRDEF( ETOOMANYREFS , 109 , "Too many references: cannot splice" ) __BIONIC_ERRDEF( ETIMEDOUT , 110 , "Connection timed out" ) __BIONIC_ERRDEF( ECONNREFUSED , 111 , "Connection refused" ) __BIONIC_ERRDEF( EHOSTDOWN , 112 , "Host is down" ) __BIONIC_ERRDEF( EHOSTUNREACH , 113 , "No route to host" ) __BIONIC_ERRDEF( EALREADY , 114 , "Operation already in progress" ) __BIONIC_ERRDEF( EINPROGRESS , 115 , "Operation now in progress" ) __BIONIC_ERRDEF( ESTALE , 116 , "Stale NFS file handle" ) __BIONIC_ERRDEF( EUCLEAN , 117 , "Structure needs cleaning" ) __BIONIC_ERRDEF( ENOTNAM , 118 , "Not a XENIX named type file" ) __BIONIC_ERRDEF( ENAVAIL , 119 , "No XENIX semaphores available" ) __BIONIC_ERRDEF( EISNAM , 120 , "Is a named type file" ) __BIONIC_ERRDEF( EREMOTEIO , 121 , "Remote I/O error" ) __BIONIC_ERRDEF( EDQUOT , 122 , "Quota exceeded" ) __BIONIC_ERRDEF( ENOMEDIUM , 123 , "No medium found" ) __BIONIC_ERRDEF( EMEDIUMTYPE , 124 , "Wrong medium type" ) __BIONIC_ERRDEF( ECANCELED , 125 , "Operation Canceled" ) __BIONIC_ERRDEF( ENOKEY , 126 , "Required key not available" ) __BIONIC_ERRDEF( EKEYEXPIRED , 127 , "Key has expired" ) __BIONIC_ERRDEF( EKEYREVOKED , 128 , "Key has been revoked" ) __BIONIC_ERRDEF( EKEYREJECTED , 129 , "Key was rejected by service" ) __BIONIC_ERRDEF( EOWNERDEAD , 130 , "Owner died" ) __BIONIC_ERRDEF( ENOTRECOVERABLE, 131 , "State not recoverable" ) #undef __BIONIC_ERRDEF
Lua:从内存加载module 从内存执行:
1 2 3 4 5 6 int FLuaPanda::OpenLuaPanda (lua_State* L) { luaL_dostring(L, (const char *)LuaPanda_lua_data); return 1 ; }
添加到PRELOAD中:
1 2 3 4 luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE); lua_pushcfunction(L, &FLuaPanda::OpenLuaPanda); lua_setfield(L, -2 , "LuaPanda" ); lua_pop(L, 1 );
直接添加到LOADED中:
1 luaL_requiref(L, "LuaPanda" , &FLuaPanda::OpenLuaPanda,1 );
gituhb快捷代码搜索 可以把ue的官方仓库的搜索创建为Chrome的自定义搜索引擎:
1 https://github.com/EpicGames/UnrealEngine/search?q=%s&unscoped_q=%s
这样在chrome的地址栏就可以通过uesource
来触发搜索。
控制写入文件FLAG FFileHelper::SaveStringToFile
的最后一个参数可以传入一个Flag,用来控制文件的写入规则:
1 2 3 4 5 6 7 8 static bool SaveStringToFile( const FString & String, const TCHAR * Filename, EEncodingOptions EncodingOptions, IFileManager * FileManager, uint32 WriteFlags )
其WriteFlags
可用值为:
1 2 3 4 5 6 7 8 9 10 enum EFileWrite { FILEWRITE_None = 0x00, FILEWRITE_NoFail = 0x01, FILEWRITE_NoReplaceExisting = 0x02, FILEWRITE_EvenIfReadOnly = 0x04, FILEWRITE_Append = 0x08, FILEWRITE_AllowRead = 0x10, FILEWRITE_Silent = 0x20 };
它们被定义在Engine/Source/Runtime/Core/Public/HAL/FileManager.h ,因为它们的值是支持位运算的,所以它们的使用方法为:
1 2 3 4 5 6 #include "FileHelper.h" #include "Paths.h" FString FilePath = FPaths::ConvertRelativePathToFull(FPaths::GameSavedDir()) + TEXT("/MessageLog.txt" ); FString FileContent = TEXT("This is a line of text to put in the file.\n" ); FFileHelper::SaveStringToFile(FileContent, *FilePath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), EFileWrite::FILEWRITE_Append | EFileWrite::FILEWRITE_AllowRead | EFileWrite::FILEWRITE_EvenIfReadOnly);
Assertion failed: Class->Children == 0 这是UBT里产生的错误,原因是项目内有两个同名的类。
1 2 3 4 5 6 7 8 1> Running UnrealHeaderTool "C:\Users\Administrator\Documents\Unreal Projects\GWorldSlg\GWorld.uproject" "C:\Users\Administrator\Documents\Unreal Projects\GWorldSlg\Intermediate\Build\Win64\GWorldEditor\Development\GWorldEditor.uhtmanifest" -LogCmds="loginit warning, logexit warning, logdatabase error" -Unattended -WarningsAsErrors -installed 1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: === Critical error: === 1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: 1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: Assertion failed: Class->Children == 0 [File:D:\Build\++UE4\Sync\Engine\Source\Programs\UnrealHeaderTool\Private\HeaderParser.cpp] [Line: 5758] 1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: 1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: 1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error: 1>C:/Users/Administrator/Documents/Unreal Projects/GWorldSlg/Source/GWorld/Public/Modules/CoreEntity/Instance/TouchControlledPawnTrace.h(20) : LogWindows: Error:
打包时添加外部文件 在Project Settings
-Project
-Packaging
-Addtional Non-Asset Directories to Package
:
注意:添加的目录必须要位于项目的Content下。
如Mobile422/Content/Script/
目录下的文件,在pak中的mount point为:
1 2 3 4 LogPakFile: Display: "Mobile422/Content/Script/.vscode/launch.json" offset: 80875520, size: 1173 bytes, sha1: 47DE6617E94EE86597148CCD53FC76E1E1A3EE22, compression: None. LogPakFile: Display: "Mobile422/Content/Script/Cube_Blueprint_C.lua" offset: 80877568, size: 888 bytes, sha1: 00F529AE4206E38D322E2983478AFBC2999A036E, compression: None. LogPakFile: Display: "Mobile422/Content/Script/UnLua.lua" offset: 80879616, size: 1977 bytes, sha1: 4015051A6663684CA3FBE1D60003CA62CD27A8AD, compression: None. LogPakFile: Display: "Mobile422/Content/Script/UnLuaPerformanceTestProxy.lua" offset: 80881664, size: 6087 bytes, sha1: 6662D86BEF54610414C00E43CF2C0F514DDF7434, compression: None.
C++关键字绝对不要作为变量名 集成了一个库,其中有下列代码:
1 2 3 4 5 6 7 8 9 #define BLOCKSIZE 64 static inline void xor_key(uint8_t key[BLOCKSIZE], uint32_t xor ) { int i; for (i=0 ;i<BLOCKSIZE;i+=sizeof (uint32_t )) { uint32_t * k = (uint32_t *)&key[i]; *k ^= xor ; } }
编译时候发现有这样的报错:
1 2 3 4 5 6 7 8 9 10 11 inlineblock.cpp:9 :42 : error: blocks support disabled - compile with -fblocks or pick a deployment target that supports them xor_key(uint8_t key[BLOCKSIZE], uint32_t xor ) { ^ inlineblock.cpp:9 :45 : error: block pointer to non-function type is invalid xor_key(uint8_t key[BLOCKSIZE], uint32_t xor ) { ^ inlineblock.cpp:13 :12 : error: type name requires a specifier or qualifier *k ^= xor ; ^ inlineblock.cpp:13 :12 : error: expected expression 4 errors generated.
这个错误的原因就是函数参数xor
是^
的关键字 。
Android NDK UE4支持r14b
-r18b
的Android NDK,但是我在UE4.22.3中设置r18b
被引擎识别为r18c
:
1 2 3 4 5 6 7 8 9 10 UATHelper: Packaging (Android (ETC2)): Using 'git status' to determine working set for adaptive non-unity build (C:\Users\imzlp\Documents\Unreal Projects\GWorldClient). UATHelper: Packaging (Android (ETC2)): ERROR: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended) PackagingResults: Error: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended) UATHelper: Packaging (Android (ETC2)): Took 7.4575476s to run UnrealBuildTool.exe, ExitCode=5 UATHelper: Packaging (Android (ETC2)): ERROR: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt) UATHelper: Packaging (Android (ETC2)): (see C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\Log.txt for full exception trace) PackagingResults: Error: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt) UATHelper: Packaging (Android (ETC2)): AutomationTool exiting with ExitCode=5 (5) UATHelper: Packaging (Android (ETC2)): BUILD FAILED PackagingResults: Error: Unknown Error
之所以要换NDK的版本是因为不同的NDK版本所包含的编译器对C++11标准支持度不同。
NDK
clang version
r14b
clang 3.8.275480 (based on LLVM 3.8.275480)
r17c
clang version 6.0.2
r18b
clang version 7.0.2
r20b
clang version 8.0.7
将BindAction暴露给蓝图 UInputComponent
函数中的BindAction
是个模板函数,可以在代码中使用,但是在蓝图中就很不方便了。
本来想着直接裹一个函数库的实现将BindAction暴露给蓝图,但是在BindAction需要传入的函数代理那里在蓝图里传递很不方便,看了一下BindAction的代码,写了下面这个函数,可以通过传递函数名来绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 UCLASS() class GWORLD_API UFlibInputEventHelper : public UBlueprintFunctionLibrary{ GENERATED_BODY() public : UFUNCTION(BlueprintCallable,Category = "GWorld|FLib|InputHelper" ,meta=(AutoCreateRefTerm="InActionName,InFuncName" )) static void BindInputAction (UInputComponent* InInputComp, const FName& InActionName, EInputEvent InKeyEvent, UObject* InCallObject, const FName& InFuncName) ; }; void UFlibInputEventHelper::BindInputAction (UInputComponent* InInputComp, const FName& InActionName, EInputEvent InKeyEvent, UObject* InCallObject, const FName& InFuncName) { FInputActionBinding AB (InActionName, InKeyEvent) ; AB.ActionDelegate.BindDelegate(InCallObject, InFuncName); InInputComp->AddActionBinding(MoveTemp(AB)); }
APK包中OBB文件 在选择把data文件打包APK之后,把打包出的APK解包,是可以看到obb文件的,在assets
文件夹下有main.obb.webp
,其就是Saved/StagedBuilds/
目录下的PROJECT_NAME.obb
文件,HASH值都是一样的。
其实OBB文件中存储的就是我们的pak文件以及在项目设置中添加的启动视频的mp4文件,使用7z之类的压缩软件可以打开未加密的obb查看,可以看到它的目录结构就是PROJECT_NAME/Content/Paks/PROJECTNAME-Android_ETC2.pak
这样的形式。
1 2 3 4 5 6 7 8 9 10 \---FGame \---Content +---Movies | logo.mp4 | SparkMore.mp4 | \---Paks pakchunk0-Android_ASTC.pak pakchunk1-Android_ASTC.pak pakchunk2-Android_ASTC.pak
移动端的启动参数 UE4打出来的包可以使用XXXX.exe -Params
等方式来传递给游戏参数,但是移动平台(IOS/Android)打包出来的怎么传递参数呢?
Android启动参数 看了一下引擎里的代码,在Launch
模块下Launch\Private\Android\LaunchAndroid.cpp
中有InitCommandLine
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 static void InitCommandLine () { static const uint32 CMD_LINE_MAX = 16384u ; FCommandLine::Set(TEXT("" )); AAssetManager* AssetMgr = AndroidThunkCpp_GetAssetManager(); AAsset* asset = AAssetManager_open(AssetMgr, TCHAR_TO_UTF8(TEXT("UE4CommandLine.txt" )), AASSET_MODE_BUFFER); if (nullptr != asset) { const void * FileContents = AAsset_getBuffer(asset); int32 FileLength = AAsset_getLength(asset); char CommandLine[CMD_LINE_MAX]; FileLength = (FileLength < CMD_LINE_MAX - 1 ) ? FileLength : CMD_LINE_MAX - 1 ; memcpy (CommandLine, FileContents, FileLength); CommandLine[FileLength] = '\0' ; AAsset_close(asset); while (*CommandLine && isspace (CommandLine[strlen (CommandLine) - 1 ])) { CommandLine[strlen (CommandLine) - 1 ] = 0 ; } FCommandLine::Append(UTF8_TO_TCHAR(CommandLine)); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("APK Commandline: %s" ), FCommandLine::Get()); } FString CommandLineFilePath = GFilePathBase + FString("/UE4Game/" ) + (!FApp::IsProjectNameEmpty() ? FApp::GetProjectName() : FPlatformProcess::ExecutableName()) + FString("/UE4CommandLine.txt" ); FILE* CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r" ); if (CommandLineFile == NULL ) { CommandLineFilePath = CommandLineFilePath.Replace(TEXT("UE4CommandLine.txt" ), TEXT("ue4commandline.txt" )); CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r" ); } if (CommandLineFile) { char CommandLine[CMD_LINE_MAX]; fgets(CommandLine, ARRAY_COUNT(CommandLine) - 1 , CommandLineFile); fclose(CommandLineFile); while (*CommandLine && isspace (CommandLine[strlen (CommandLine) - 1 ])) { CommandLine[strlen (CommandLine) - 1 ] = 0 ; } FCommandLine::Set(TEXT("" )); FCommandLine::Append(UTF8_TO_TCHAR(CommandLine)); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Override Commandline: %s" ), FCommandLine::Get()); } #if !UE_BUILD_SHIPPING if (FString* ConfigRulesCmdLineAppend = FAndroidMisc::GetConfigRulesVariable(TEXT("cmdline" ))) { FCommandLine::Append(**ConfigRulesCmdLineAppend); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ConfigRules appended: %s" ), **ConfigRulesCmdLineAppend); } #endif }
简单来说就是在UE4Game/ProjectName/ue4commandline.txt
中把启动参数写到里面,引擎启动的时候会从这个文件去读,然后添加到FCommandLine中。
IOS启动参数 与Android的做法相同,IOS的参数传递是在main
函数中调用FIOSCommandLineHelper::InitCommandArgs(FString());
,不过路径和Android不一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static void InitCommandArgs (FString AdditionalCommandArgs) { FCommandLine::Set(TEXT("" )); FString CommandLineFilePath = FString([[NSBundle mainBundle] bundlePath]) + TEXT("/ue4commandline.txt" ); FILE* CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r" ); if (CommandLineFile) { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Found ue4commandline.txt file" ) LINE_TERMINATOR); char CommandLine[CMD_LINE_MAX] = {0 }; char * DataExists = fgets(CommandLine, ARRAY_COUNT(CommandLine) - 1 , CommandLineFile); if (DataExists) { while (*CommandLine && isspace (CommandLine[strlen (CommandLine) - 1 ])) { CommandLine[strlen (CommandLine) - 1 ] = 0 ; } FCommandLine::Append(UTF8_TO_TCHAR(CommandLine)); } } else { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("No ue4commandline.txt [%s] found" ) LINE_TERMINATOR, *CommandLineFilePath); } if (!AdditionalCommandArgs.IsEmpty() && !FChar::IsWhitespace(AdditionalCommandArgs[0 ])) { FCommandLine::Append(TEXT(" " )); } FCommandLine::Append(*AdditionalCommandArgs); FCommandLine::Append(*GSavedCommandLine); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Combined iOS Commandline: %s" ) LINE_TERMINATOR, FCommandLine::Get()); }
关键就是[[NSBundle mainBundle] bundlePath]
这一句,它获取的是App的包路径,所以把UE4CommandLine.txt放到包路径下就可以了。
UE编译环境的VS安装配置 保存为.vsconfig
然后使用Visual Studio Installer导入配置安装即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 { "version" : "1.0" , "components" : [ "Microsoft.VisualStudio.Workload.NativeDesktop" , "Microsoft.VisualStudio.Workload.Python" , "Microsoft.VisualStudio.Workload.Node" , "Microsoft.VisualStudio.Workload.NativeGame" , "Microsoft.VisualStudio.Workload.NativeCrossPlat" , "microsoft.visualstudio.component.debugger.justintime" , "microsoft.net.component.4.6.2.sdk" , "microsoft.net.component.4.6.2.targetingpack" , "microsoft.net.component.4.7.sdk" , "microsoft.net.component.4.7.targetingpack" , "microsoft.net.component.4.7.1.sdk" , "microsoft.net.component.4.7.1.targetingpack" , "microsoft.net.component.4.7.2.sdk" , "microsoft.net.component.4.7.2.targetingpack" , "microsoft.visualstudio.component.vc.diagnostictools" , "microsoft.visualstudio.component.vc.cmake.project" , "microsoft.visualstudio.component.vc.atl" , "microsoft.visualstudio.component.vc.testadapterforboosttest" , "microsoft.visualstudio.component.vc.testadapterforgoogletest" , "microsoft.visualstudio.component.winxp" , "microsoft.visualstudio.component.vc.cli.support" , "microsoft.visualstudio.component.vc.modules.x86.x64" , "component.incredibuild" , "microsoft.component.netfx.core.runtime" , "microsoft.component.cookiecuttertools" , "microsoft.component.pythontools.web" , "microsoft.visualstudio.component.classdesigner" , "microsoft.net.component.3.5.developertools" , "component.unreal.android" , "component.linux.cmake" , "microsoft.component.helpviewer" , "microsoft.visualstudio.component.vc.clangc2" , "microsoft.visualstudio.component.vc.tools.14.14" ] }
下载地址:vs_installer.vsconfig
扫描资源引用时需注意Redirector 当我们在UE的资源管理器中进行rename
/move
等操作时,会产生一个与更名之前名字一样的Redirector
:
在使用UAssetManager::GetAssets
进行资源的扫描时也会查询到这些redirector
,但是他们不是真正的资源,在处理时需要过滤掉它们。FAssetData
中有一个成员函数IsRedirector
可以用来判断扫描到的FAssetData
是不是重定向器。
但是良好的项目规范是每进行delete
/rename
/move
之后都手动在编辑器中执行Fix up Redirector In Folder
,就会清理掉这些Redirector了,保持项目的干净。
Cook执行代码 UE中执行Cook 的代码位于UnrealEd
模块下,源码位于:
1 Editor/UnrealEd/Private/Commandlets/CookCommandlet.cpp
其中有int32 UCookCommandlet::Main(const FString& CmdLineParams)
是起始逻辑。
No world was found for object 在写代码的时候在UObject里调用了一些需要传递WorldContentObject
的函数,但是却把UObject的this
传递了进去,因为这个东西不在场景中,无法通过它获取的World,所以会产生下列警告:
LogScript: Warning: Script Msg: No world was found for object (/Engine/Transient.SubsysTouchControllerTrace_1) passed in to UEngine::GetWorldFromContextObject().
解决办法就是传递一个能够获取到World
的对象进去。
移动设备的渲染预览 ToolBar
-Settings
-PreviewRenderingLevel
-Android ES3.1
/Android ES2
/IOS
:
Android SDK版本与Android的版本 可以在Google的开发者站点看到:Android SDK Platform Build.VERSION_CODES的含义:Build.VERSION_CODES
AndroidVersion
SDK Version
Build.VERSION_CODES
Android 11
(API level 30)
R
Android 10
(API level 29)
Q
Android 9
(API level 28)
P
Android 8.1
(API level27)
O_MR1
Android 8.0
(API level 26)
O
Android 7.1
(API level 25)
N_MR1
Android 7.0
(API level 24)
N
Android 6.0
(API level 23)
M
Android 5.1
(API level 22)
LOLLIPOP_MR1
Android 5.0
(API level 21)
LOLLIPOP
Android 4.4W
(API level 20)
KITKAT_WATCH
Android 4.4
(API level 19)
KITKAT
Android 4.3
(API level 18)
JELLY_BEAN_MR2
Android 4.2
(API level 17)
JELLY_BEAN_MR1
Android 4.1
(API level 16)
JELLY_BEAN
Android 4.0.3
(API level15)
ICE_CREAM_SANDWICH_MR1
Android 4.0
(API level 14)
ICE_CREAM_SANDWICH
Android 3.2
(API level 13)
HONEYCOMB_MR2
Android 3.1
(API level 12)
HONEYCOMB_MR1
Android 3.0
(API level 11)
HONEYCOMB
Android 2.3.3
(API level 10)
GINGERBREAD_MR1
Android 2.3
(API level 9)
GINGERBREAD
UE4对Android的最低支持是SDK9,也就是Android2.3。
Android项目设置
EnableGradleInsteadOfAnt
:使用Gradle替代Ant用来编译和生成APK。
EnableFullScreenImmersiveOnKitKatAndAboveDevices
:全屏模式下隐藏虚拟按键;
EnableImprovedVirtualKeyboard
:启用虚拟键盘;
在构造函数中用SetupAttachmen替代AttachToComponent 在构造函数中使用AttachToComponent
在打包时会有这样的错误:
Error: AttachToComponent when called from a constructor is only setting up attachment and will always be treated as KeepRelative. Consider calling SetupAttachment directly instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: begin: stack for UAT UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: === Handled ensure: === UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: Ensure condition failed: AttachmentRules.LocationRule == EAttachmentRule::KeepRelative && AttachmentRules.RotationRule == EAttachmentRule::KeepRelative && AttachmentRules.ScaleRule == EAttachmentRule::KeepRelative [File:D:\Build\++UE4\Sync\Engine\Source\Runtime\Engine\Privat e\Components\SceneComponent.cpp] [Line: 1786 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: AttachToComponent when called from a constructor is only setting up attachment and will always be treated as KeepRelative. Consider calling SetupAttachment directly instead. UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: Stack: UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe36fccd89 UE4Editor-Engine.dll!DispatchCheckVerify<bool ,<lambda_2fa4c8014e6e2d59bee8e8ac7e5934f3> >() [d:\build\++ue4\sync\engine\source\runtime\core\public \misc\assertionmacros.h:161 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe35e04fa1 UE4Editor-Engine.dll!USceneComponent::AttachToComponent() [d:\build\++ue4\sync\engine\source\runtime\engine\private \components\scenecomponent.cpp:1786 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe215db1b3 UE4Editor-GWorld-0001. dll!ABasePlayerPawn::ABasePlayerPawn() [c:\users\imzlp\documents\unreal projects\gworldclient\source\gworld\private \modules\coreentity\instance\baseplayerpawn.cpp:21 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe215fdf57 UE4Editor-GWorld-0001. dll!InternalConstructor<ABasePlayerPawn>() [c:\program files\epic games\ue_4.22 \engine\source\runtime\coreuobject\public \uobject\class.h:2841 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe382a610f UE4Editor-CoreUObject.dll!UClass::CreateDefaultObject() [d:\build\++ue4\sync\engine\source\runtime\coreuobject\private \uobject\class.cpp:3076 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe384e1056 UE4Editor-CoreUObject.dll!UObjectLoadAllCompiledInDefaultProperties() [d:\build\++ue4\sync\engine\source\runtime\coreuobject\private \uobject\uobjectbase.cpp:793 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe384cedef UE4Editor-CoreUObject.dll!ProcessNewlyLoadedUObjects() [d:\build\++ue4\sync\engine\source\runtime\coreuobject\private \uobject\uobjectbase.cpp:869 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe382aa727 UE4Editor-CoreUObject.dll!TBaseStaticDelegateInstance<void __cdecl(void )>::ExecuteIfSafe() [d:\build\++ue4\sync\engine\source\runtime\core\public \delegates\delegateinstancesimpl.h:813 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe38874a2b UE4Editor-Core.dll!TBaseMulticastDelegate<void >::Broadcast() [d:\build\++ue4\sync\engine\source\runtime\core\public \delegates\delegatesignatureimpl.inl:977 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe38a45cb5 UE4Editor-Core.dll!FModuleManager::LoadModuleWithFailureReason() [d:\build\++ue4\sync\engine\source\runtime\core\private \modules\modulemanager.cpp:530 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe5fb58d67 UE4Editor-Projects.dll!FModuleDescriptor::LoadModulesForPhase() [d:\build\++ue4\sync\engine\source\runtime\projects\private \moduledescriptor.cpp:596 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe5fb58ff7 UE4Editor-Projects.dll!FProjectManager::LoadModulesForProject() [d:\build\++ue4\sync\engine\source\runtime\projects\private \projectmanager.cpp:63 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dabd260 UE4Editor-Cmd.exe!FEngineLoop::PreInit() [d:\build\++ue4\sync\engine\source\runtime\launch\private \launchengineloop.cpp:2425 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dab5377 UE4Editor-Cmd.exe!GuardedMain() [d:\build\++ue4\sync\engine\source\runtime\launch\private \launch.cpp:129 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dab55ca UE4Editor-Cmd.exe!GuardedMainWrapper() [d:\build\++ue4\sync\engine\source\runtime\launch\private \windows\launchwindows.cpp:145 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dac316c UE4Editor-Cmd.exe!WinMain() [d:\build\++ue4\sync\engine\source\runtime\launch\private \windows\launchwindows.cpp:275 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ff65dac4cb6 UE4Editor-Cmd.exe!__scrt_common_main_seh() [d:\agent\_work\3 \s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288 ] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe7b247974 KERNEL32.DLL!UnknownFunction [] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: [Callstack] 0x00007ffe7c62a271 ntdll.dll!UnknownFunction [] UATHelper: Packaging (Windows (64 -bit)): LogInit: Display: LogOutputDevice: Error: end: stack for UAT
解决的办法就是提示中的那样,用SetupAttachment
替换AttachToComponent
。
在其他的线程运行程序并获取程序输出 在写避编辑器功能的时候经常会启动外部的程序来执行任务,如果要求程序执行完成才走其他逻辑则会阻塞,这样的体验很不好,所以一般是开一个额外的线程来执行程序启动。废话不多说直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 #pragma once #include "FThreadUtils.hpp" #include "CoreMinimal.h" #include "GenericPlatform/GenericPlatformProcess.h" DECLARE_MULTICAST_DELEGATE_OneParam(FOutputMsgDelegate, const FString&); DECLARE_MULTICAST_DELEGATE(FProcStatusDelegate); class FProcWorkerThread : public FThread{ public : explicit FProcWorkerThread (const TCHAR *InThreadName,const FString& InProgramPath,const FString& InParams) : FThread(InThreadName, []() {}), mProgramPath(InProgramPath), mPragramParams(InParams) {} virtual uint32 Run () override { if (FPaths::FileExists(mProgramPath)) { FPlatformProcess::CreatePipe(mReadPipe, mWritePipe); mProcessHandle = FPlatformProcess::CreateProc(*mProgramPath, *mPragramParams, false , true , true , &mProcessID, 0 , NULL , mWritePipe,mReadPipe); if (mProcessHandle.IsValid() && FPlatformProcess::IsApplicationRunning(mProcessID)) { ProcBeginDelegate.Broadcast(); } FString Line; while (mProcessHandle.IsValid() && FPlatformProcess::IsApplicationRunning(mProcessID)) { FPlatformProcess::Sleep(0.0f ); FString NewLine = FPlatformProcess::ReadPipe(mReadPipe); if (NewLine.Len() > 0 ) { Line += NewLine;