一些篇幅短小待深挖的知识随笔罗列在这里。


UE4:蓝图编辑器中节点属性的修改

在编辑器模式下修改节点的值:

执行的函数是UEdGraphSchema_K2::TrySetDefaultValue(Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp),是通过Schema来调用的。简单来说就是通过当前的节点,去修改节点上的Pin的值,不管原始类型是什么,FString/int/float还是枚举,都可以通过这个方法设置。

UE4:类反射

UHT为类产生的反射信息

当在UE中新建一个类并继承自UObject时,可以在类声明的上一行添加UCLASS标记,当执行编译的时候UBT会调用UHT来根据标记来生成C++代码(不过非UCLASS的类也可以用宏来生成反射信息)。

UHT为类生成的代码为:

  1. 为所有的UFUNCTION的函数创建FName,命名规则为NAME_CLASSNAME_FUNCTIONNAME,如NAME_AMyActor_TestFunc
  2. BlueprintNativeEvent和BlueprintImplementEvent创建同名函数实现,并通过ProcessEvent转发调用
  3. 为所有加了UFUNCTION的函数生成Thunk函数,为当前类的static函数,原型为static void execFUNCNAME( UObject* Context, FFrame& Stack, RESULT_DECL)
  4. 创建当前类的StaticRegisterNatives*函数,并把上一步提到的exec这样的thunk函数通过Name-execFunc指针的形式通过FNativeFunctionRegistrar::RegisterFunctions注册到UClass;
  5. 创建出Z_Construct_UClass_CLASSNAME_NoRegister函数,返回值是CLASSNAME::StaticClass()
  6. 创建出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
// AMyActor.h
UCLASS(BlueprintType)
class XXXX_API AMyActor:public AActor
{
GENERATED_BODY()
// ...

UPROPERTY()
int32 ival;
UFUNCTION()
int32 GetIval();
UFUNCTION()
void TESTFUNC();
};

// generated code in AMyActor.gen.cpp
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" }, // 3480851337
{ &Z_Construct_UFunction_AMyActor_TESTFUNC, "TESTFUNC" }, // 2984899165
};

该类中的成员为:

  • 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),
0x009000A0u,
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::Class_MetaDataParams, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::Class_MetaDataParams))
};
  1. 全局函数Z_Construct_UClass_AMyActor通过ClassParams构造出真正的UClass对象。
  2. 使用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
// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
UClass* TClass::GetPrivateStaticClass() \
{ \
static UClass* PrivateStaticClass = NULL; \
if (!PrivateStaticClass) \
{ \
/* this could be handled with templates, but we want it external to avoid code bloat */ \
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; \
}

AMyActorIMPLEMENT_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
/** Register all loaded classes */
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.cppgenerated.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的实现思路上分析一下设计过程。

  1. 首先UHT通过分析代码创建出gen.cpp和generated.h中间记录着当前类的反射信息、类本身的反射信息、类中函数的反射信息、类数据成员的反射信息。
  2. 当前类的反射信息(类、成员函数、数据成员)等被统一存储在一个名为Z_Construct_UClass_CLASSNAME_Statics的结构中;
  3. 该结构通过IMPLEMENT_CLASS生成的代码将当前类添加到GetDeferredClassRegistration()中。因为全局作用域static对象的构造时机是先于主函数的第一条语句的,所以当进入引擎逻辑的时候,引擎内置的模块中类的TClassCompiledInDefer<>都已经被创建完毕,在编辑器模式下, 因为不同的模块都是编译为DLL的,所以在加载模块的时候它们的static对象才会被创建。
1
2
// gen.cpp
static TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor(TEXT("AMyActor"), sizeof(AMyActor), 3240835608);
  1. 与上一步同样的手法,把类生成的反射信息通过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.cppInitUObject函数中,把函数ProcessNewlyLoadedUObjects添加到了FModuleManager::Get().OnProcessLoadedObjectsCallback()中:

1
2
3
#if !USE_PER_MODULE_UOBJECT_BOOTSTRAP // otherwise this is already done
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
// CoreUObject/Private/UObject/UObjectBase.cpp
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
// CoreUObject/Private/UObject/UObjectBase.cpp
/** Register all loaded classes */
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都构造出来。

UE4:函数反射

UHT生成的反射信息

在UE的代码中加了UFUNCTION()修饰后UHT就会为该函数生成反射代码。

每一个支持反射的函数UHT都会给它生成一个类和一个函数:
如在AMyActor这个类下有一个Add的函数,UHT会生成这样命名规则的一个类和函数:

1
2
struct Z_Construct_UFunction_AMyActor_Add_Statics;
UFunction* Z_Construct_UFunction_AMyActor_Add();

定义的这个类中包含了以下信息:

  1. 存储函数的参数、返回值的结构体(POD)
  2. 函数参数、返回值的F*PropertyParams,用于给它们生成UProperty,static成员
  3. 成员static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];,数组,用于记录参数和返回值的FPropertyParamsBase数据。
  4. 成员static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[];,用于记录函数的元数据。如所属文件、Category、注释等等。
  5. 成员static const UE4CodeGen_Private::FFunctionParams FuncParams;用于记录当前函数的名字、Flag、参数的F*PropertyParams、参数数量,参数的结构大小等等,用于通过它来创建出UFunction*

定义的函数做了以下事情:

1
2
3
4
5
6
7
8
9
UFunction* Z_Construct_UFunction_AMyActor_Add()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AMyActor_Add_Statics::FuncParams);
}
return ReturnFunction;
}

根据定义的Z_Construct_UFunction_AMyActor_Add_Statics结构中的FuncParams成员来创建出真正的UFunction对象。

最后,Z_Construct_UFunction_AMyActor_Add这个函数会被注册到当前类反射数据的FuncInfo中。

运行时访问反射函数

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);
}
}

坑点

注意:通过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生成的参数结构。

UE4:属性反射

当在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:
// Called when the game starts or when spawned
virtual void BeginPlay() override;

public:
// Called every frame
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
// UObject/UObjectGlobals.h
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的名字,在运行时可以通过UPropertyGetNameCPP来获取。

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
/*
// All the properties that should never be loaded or saved
#define CPF_ComputedFlags (CPF_IsPlainOldData | CPF_NoDestructor | CPF_ZeroConstructor | CPF_HasGetValueTypeHash)

0x0000000040000000 CPF_IsPlainOldData
0x0000001000000000 CPF_NoDestructor
0x0000000000000200 CPF_ZeroConstructor
0x0008000000000000 CPF_HasGetValueTypeHash
*/
EPropertyFlags PropFlags = Prop->PropertyFlags & ~CPF_ComputedFlags;

在运行时可以通过UPropertyHasAnyPropertyFlags函数来检测是否具有特定的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
// Programs/UnrealHeaderTool/Public/ParserClass.h

/** Helper function that checks if the field is a dynamic type (can be constructed post-startup) */
template <typename T>
static bool IsDynamic(const T* Field)
{
return Field->HasMetaData(NAME_ReplaceConverted);
}

// Programs/UnrealHeaderTool/Private/ParserClass.cpp
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;
}

通过调用UFieldHasMetaData检测是否具有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
/** helper to calculate an array's dimensions **/
#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)
{
// in the future, these checks will be tested if the property is NOT relative to a UClass
check(!Cast<UClass>(GetOuter())); // Check we are _not_ calling this on a direct child property of a UClass, you should pass in a UObject* in that case
}
return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}

其实就是拿到UObject的指针,然后偏移到指定位置(第二个参数用在对象是数组的情况,用来访问指定下标的元素,默认情况下访问下标为0)。

Z_Construct_UClass_和NumMetaData

它们的类型分别为FMetaDataPairParamint32,用来记录当前反射属性的元数据:比如属性的Category、注释、所属的文件、ToolTip信息等等,比如在C++函数上添加的注释能够在编辑器蓝图中看到注释的信息,都是靠解析这些元数据来实现的。

1
2
3
4
5
6
7
8
// CoreUObject/Public/UObject/UObjectGlobals.h
#if WITH_METADATA
struct FMetaDataPairParam
{
const char* NameUTF8;
const char* ValueUTF8;
};
#endif

这两个参数通过METADATA_PARAMS包裹,用于处理WITH_MATEDATA的不同情况:

1
2
3
4
5
6
// METADATA_PARAMS(x, y) expands to x, y, if WITH_METADATA is set, otherwise expands to nothing
#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
// source code 
UPROPERTY()
bool bEnabled;

// gen.cpp
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的地址然后做了类型转换。

UE4:StaticClass和GetClass的区别

StaticClass是继承自UObject类的static函数,GetClassUObjectBase的成员函数。

UObjectBaseGetClass获取到的UClass就是在NewObject时传递进来的UClass.(代码在UObject\UObjectGlobal.cpp中)

用途不一样,StaticClass是在获取具体类型的UClass,而GetClass是获取到当前对象的真实UClass。

UE4: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_BODYGENERATED_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);
// Don't call the constructor on recycled subobjects, they haven't been destroyed.
if (!bRecycledSubobject)
{
STAT(FScopeCycleCounterUObject ConstructorScope(InClass, GET_STATID(STAT_ConstructObject)));
(*InClass->ClassConstructor)( FObjectInitializer(Result, InTemplate, bCopyTransientsFromClassDefaults, true, InInstanceGraph) );
}

通过传入进来的UClass对象获取其中的ClassConstructor函数指针,并构造出一个FObjectInitializer作为参数传递。

在之前的笔记(UE4:StaticClass是如何定义的)中,写到了,通过GetPrivateStaticClass获取到当前UObject类的UClass实例会实例化出一个当前类的InternalConstructor函数(注意这个模板参数T是UObject的类):

1
2
3
4
5
6
7
8
// class.h

// Helper template to call the default constructor for a class
template<class T>
void InternalConstructor( const FObjectInitializer& X )
{
T::__DefaultConstructor(X);
}

就将其转发到了上面讲到的__DefaultConstructor函数上,然后它里面又转发到了所传入FObjectInitializer对象上的const FObjectInitializer&构造函数上(或者默认构造函数上)。

创建对象并调用const FObjectInitializer&构造函数的调用流程为:

  1. 通过调用StaticAllocateObject根据传入的UClass创建出对象
  2. 通过传入的UClass对象内的ClassConstructor函数指针调用所创建UObject类的InternalConstructor函数
  3. UMyObject::InternalConstructor会转发到UMyObject::__DefaultConstructor
  4. UMyObject::__DefaultConstructor会从接收到的FObjectInitializer对象上获取到通过StaticAllocateObject创建的对象,然后通过placement-new的方式,在这块内存上调用UMyObject类的const FObjectInitializer&构造函数。

通过以上的流程就实现了NewObject时会自动调用到它的FObjectInitializer构造函数。

注意:在CDO的构造上有点区别,CDO的构造是通过UClass::GetDefaultObject中实现上述流程的。

UE4: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.hDECLARE_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
// Runtime/CoreUObject/Public/UObject/ObjectMacros.h
// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
UClass* TClass::GetPrivateStaticClass() \
{ \
static UClass* PrivateStaticClass = NULL; \
if (!PrivateStaticClass) \
{ \
/* this could be handled with templates, but we want it external to avoid code bloat */ \
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
// class.h

// Helper template to call the default constructor for a class
template<class T>
void InternalConstructor( const FObjectInitializer& X )
{
T::__DefaultConstructor(X);
}

// Helper template to call the vtable ctor caller for a class
template<class T>
UObject* InternalVTableHelperCtorCaller(FVTableHelper& Helper)
{
return T::__VTableCtorCaller(Helper);
}

就是对__DefaultConstructor这样函数的的转发调用。

UClass::ClassConstructorTypeUClass::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 /*= false*/
);

其中第四个参数是传入注册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
// MyActor.gen.cpp
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
// Runtime\CoreUObject\Private\UObject\Class.cpp 
void FNativeFunctionRegistrar::RegisterFunctions(class UClass* Class, const FNameNativePtrPair* InArray, int32 NumFunctions)
{
for (; NumFunctions; ++InArray, --NumFunctions)
{
Class->AddNativeFunction(UTF8_TO_TCHAR(InArray->NameUTF8), InArray->Pointer);
}
}

UE4:获取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的字符串。

内存对齐的作用

之前写过一篇文章来介绍C++里内存对齐的规则:结构体成员内存对齐问题

但是,为什么呢?

首先,内存中数据的排列方式被称为内存布局。结构中不同的排列方式,占用的内存不同,也会简介影响CP访问内存的效率(无论是否对齐,CPU都可以正常处理,但又效率问题)。为了权衡空间占用情况和访问效率,引入了内存对齐规则。

CPU在单位时间内能处理的一组二进制数称为,这组二进制数的位数称为字长。如果是32位CPU,其字长位32位也就是4字节。一般来说,字长越大,计算机处理信息的速度就越快。

以32位CPU为例,CPU每次只能从内存中读取4个字节的数据,所以每次只能对4的倍数的地址进行读取。

假设现在有一个4字节整数类型数据,首地址并不是4的倍数,假定位0x3,则该类型存储在地址为0x3~0x7的内存空间中。因此CPU如果像读取该数据,则需要分别在0x10x5处进行两次读取,而且还需要对读取到的数据进行处理才能得到该整数,如图:

CPU的处理速度比从内存中读取数据的速度快很多,所以减少CPU对内存空间的访问是提供程序性能的关键。因此采取内存对齐策略是提高程序性能的关键,因为是32为CPU,所以只需要按4字节对齐,上面的例子可以变为CPU只需要读取一次:

因为对齐的是字节,所以内存对齐也叫字节对齐。内存对齐在C++中是编译器处理的,一般不用人为指定,但是需要了解内存对齐的规则,这样有助于写出既节省内存又性能最佳的程序。

参考资料:

  • 《Rust编程之道》4.1.3 P101.
  • 《深入理解计算机系统第三版》3.9.3 P189

NPM:指定Packages的版本

  • ~version "Approximately equivalent to version" See npm semver - Tilde Ranges & semver (7)
  • ^version "Compatible with version" See npm semver - Caret Ranges & semver (7)
  • version Must match version exactly
  • >version Must be greater than version
  • >=version etc
  • <version
  • <=version
  • 1.2.x 1.2.0, 1.2.1, etc., but not 1.3.0
  • http://sometarballurl (this may be the URL of a tarball which will be downloaded and installed locally
  • * Matches any version
  • latest Obtains latest release

What's the difference between tilde(~) and caret(^) in package.json?

UE4: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

UE4:监听窗口关闭

可以通过监听FSlateFontServices里的OnSlateWindowDestroyed:

1
2
3
4
5
6
7
/**
* Called on the game thread right before the slate window handle is destroyed.
* This gives users a chance to release any viewport specific resources they may have active when the window is destroyed
* @param Pointer to the API specific backbuffer type
*/
DECLARE_MULTICAST_DELEGATE_OneParam(FOnSlateWindowDestroyed, void*);
FOnSlateWindowDestroyed& OnSlateWindowDestroyed() { return OnSlateWindowDestroyedDelegate; }

监听方法:

1
FSlateApplication::Get().GetRenderer()->OnSlateWindowDestroyed().AddRaw(this, &FSceneViewport::OnWindowBackBufferResourceDestroyed);

UE4: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;

UE4:4.25 MountPak没有材质

经过调试后发现,是因为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());

UE4: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

有时间在来具体分析。

UE4:target/build.cs输出Log

可以使用C#里的以下代码:

1
2
3
using System;

System.Console.WriteLine("12346");

UE4: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
/// <summary>
/// Determines which version of the engine to take default build settings from. This allows for backwards compatibility as new options are enabled by default.
/// </summary>
public enum BuildSettingsVersion
{
/// <summary>
/// Legacy default build settings for 4.23 and earlier.
/// </summary>
V1,

/// <summary>
/// New defaults for 4.24: ModuleRules.PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs, ModuleRules.bLegacyPublicIncludePaths = false.
/// </summary>
V2,

// *** When adding new entries here, be sure to update GameProjectUtils::GetDefaultBuildSettingsVersion() to ensure that new projects are created correctly. ***

/// <summary>
/// Always use the defaults for the current engine version. Note that this may cause compatibility issues when upgrading.
/// </summary>
Latest
}

UE4.23及之前的引擎版本是V1,用来控制当项目升级引擎版本时使用之前引擎的构建设置,用于解决项目升级之后会有大量错误的问题。

UE4:从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
{
/// <summary>
/// The major engine version (4 for UE4)
/// </summary>
public int MajorVersion;

/// <summary>
/// The minor engine version
/// </summary>
public int MinorVersion;

/// <summary>
/// The hotfix/patch version
/// </summary>
public int PatchVersion;

/// <summary>
/// The changelist that the engine is being built from
/// </summary>
public int Changelist;

/// <summary>
/// The changelist that the engine maintains compatibility with
/// </summary>
public int CompatibleChangelist;

/// <summary>
/// Whether the changelist numbers are a licensee changelist
/// </summary>
public bool IsLicenseeVersion;

/// <summary>
/// Whether the current build is a promoted build, that is, built strictly from a clean sync of the given changelist
/// </summary>
public bool IsPromotedBuild;

/// <summary>
/// Name of the current branch, with '/' characters escaped as '+'
/// </summary>
public string BranchName;

/// <summary>
/// The current build id. This will be generated automatically whenever engine binaries change if not set in the default Engine/Build/Build.version.
/// </summary>
public string BuildId;

/// <summary>
/// The build version string
/// </summary>
public string BuildVersionString;
// ...
}

可以在Target.cs或者Build.cs里通过Target.Version来访问引擎版本,可以根据不同的引擎版本来使用不同的库。

UE4:从TargetRules获取Configuration

ReadOnlyTargetRules接收到的Target,可以从其中获取Configuration成员,用于检测打包的BuildConfiguration

1
2
3
4
if (Target.Configuration == UnrealTargetConfiguration.Shipping)
{
// ...
}

枚举值为Development/Debug/DebugGame/Shipping/Test等。

a/lib格式

.a.lib是静态链接库格式,它们是ar压缩的.o.obj文件,并且内部有txt文件来记录每个目标文件中的符号信息。

在IOS和MacOS上的txt中的符号名都以_开头。

IOS CrashLog分析

C++:rvalue和lvalue

rvalue(左值)是指对象的一条表达式,左值的字面意思是能再赋值运算符左侧的东西。但其实不是所有的左值都能用在赋值运算符的左侧,左值也有可能指示某个常量。

待补充。

UE4: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
// Actor.h
TSet<UActorComponent*> OwnedComponents;

// Actor.cpp
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);
}

UE4:SDetailsView监听属性变化

可以通过监听基类SDetailsViewBase中的OnFinishedChangingPropertiesDelegate代理来实现。

接收参数是FPropertyChangedEvent

1
DECLARE_MULTICAST_DELEGATE_OneParam(FOnFinishedChangingProperties, const FPropertyChangedEvent&);

UE4:UObject serializer的调用栈

有时间再来分析具体内容。

UE4:FName/FString/FText的区别

FName

FName的字符串一般用在为资源命名或者访问资源时(比如命名的骨骼)需要使用。
它使用一个轻型系统使用字符串,特定字符串会被重复使用,在数据表中也就只存储一次。

  1. 只在内存中存储一次
  2. 不区分大小写
  3. 不能被修改
  4. 查找和访问速度比较快
  5. 内部有一个HASH值
    FName在Edior中占12个字节,打包8字节,FName的XXXX_12这样的字符串会被分成string part和number part,估计是为了想不为每个拼接的结果都在NamePool中创建一份吧。

FString

FSting比较类似于标准库的std::string,区分大小写,可修改,每份实例都是单独的内存。

FText

FText是用于本地化的类,所有需要展示的文本都需要使用FText,它提供了以下功能:

  1. 创建本地化文本
  2. 格式化文本
  3. 从数字生成文本
  4. 从日期或时间生成文本
  5. 转换文本(大小写转换等)

文档

C++:move和forward的区别

std::movestd::forward均是定义在<utility>中的函数。

functiondescrible
x2=forward(x)x2是一个右值,x不能是左值;不抛出异常
x2=move(x)x2是一个右值;不抛出异常
x2=move_if_noecept(x)若x2可移动,x2=move(x);否则x2=x;不抛出异常

std::move进行简单的右值转换:

1
2
3
4
5
template<typename T>
remove_reference<T>&& move(T&& t)noexcept
{
return static_cast<remove_reference<T>&&>(t);
}

其实move应该命名为rvalue才对,它没有移动任何东西,而是从实参生成一个rvalue,从而所指向的对象可以移动。

我们用move告知编译器,此对象在上下文中不再被使用,因此其值可以被移动,留下一个空对象。最简单的就是swap的实现。

std::forward从右值生成一个右值:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
T&& forward(remove_reference<T>& t)noexcept
{
return static_cast<T&&>(t);
}
template<typename T>
T&& forward(remove_reference<T>&& t)noexcept
{
static_asset(!is_lvalue_reference<T>,"forward of value");
return static_cast<T&&>(t);
}

这对forward函数总是会一直提供,两者间的选择是通过重载解析实现的。任何左值都会由第一个版本处理,任何右值都会有第二个版本处理。

1
2
3
int i=7;
forward(i); // 调用第一个版本
forward(7); // 调用第二个版本

第二个版本的断言是为了防止用显示模板实参和一个左值调用第二个版本。

forward的典型用法是将一个实参完美转发到另一个函数。

当系统用移动操作窃取一个对象的表示形式时,使用move;当希望转发一个对象时,使用forward。因此forward总是安全的,而move标记x将被销毁,因此使用时要小心。调用std::move(x)之后x唯一的用法就是析构或者赋值目的。

C++:rvalue和lvalue的重载规则

实现non-const lvalue版本

如果一个类只实现了:

1
A(A&){}

则该类只能被lvalue调用,但不能被rvalue调用。如下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A{

public:
A(){printf("A();\n");}
A(A& In){printf("A(A& In);\n");}
~A(){printf("~A();\n");}
};
int main()
{
A tmpA;
A tmpB = std::move(tmpA);

}

会有下列错误:

1
2
3
4
5
6
7
8
9
10
C:\Users\visionsmile\Desktop\cpp\rvalue.cpp:31:4: error: no matching constructor for initialization of 'A'
A tmpB = std::move(tmpA);
^ ~~~~~~~~~~~~~~~
C:\Users\visionsmile\Desktop\cpp\rvalue.cpp:15:2: note: candidate constructor not viable: expects an l-value for 1st argument
A(A& In)
^
C:\Users\visionsmile\Desktop\cpp\rvalue.cpp:12:2: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
A(){
^
1 error generated.

实现const lvalue版本

如果实现了const版本:

1
A(const A& In){printf("A(const A& In);\n");}

则既可以被rvalue也可以被lvalue调用。

只实现rvalue版本

如果类中只有rvalue的函数版本:

1
A(A&& rIn){printf("A(A&& rIn);\n");}

则只能被rvalue调用,不能被lvalue调用。

1
2
3
4
5
6
7
8
9
10
11
class A{
public:
A(){printf("A();\n");}
A(A&& rIn){printf("A(A&& In);\n");}
~A(){printf("~A();\n");}
};
int main()
{
A tmpA;
A tmpB = tmpA;
}

或有下列编译错误:

1
2
3
4
5
6
7
C:\Users\visionsmile\Desktop\cpp\rvalue.cpp:31:4: error: call to implicitly-deleted copy constructor of 'A'
A tmpB = tmpA;
^ ~~~~
C:\Users\visionsmile\Desktop\cpp\rvalue.cpp:19:2: note: copy constructor is implicitly deleted because 'A' has a user-declared move constructor
A(A&& rIn)
^
1 error generated.

既有rvalue也有lvalue版本

如果既提供了rvalue也提供了lvalue版本,则可以区分为rvalue服务和为lvalue服务的能力

1
2
3
4
5
6
7
8
9
10
11
12
class A{
public:
A(){printf("A();\n");}
A(const A& In){printf("A(const A& In);\n");}
A(A&& rIn){printf("A(A&& In);\n");}
~A(){printf("~A();\n");}
};
int main()
{
A tmpA;
A tmpB = tmpA;
}

结语

如果类未提供move语义,只提供常规的copy构造函数和copy assignment操作符,rvalue引用可以调用他们。

因此std::move意味着:调用move语义,否则调用cop语义。

UE4:Plugin添加其他Plugin的模块

如果插件A要引用插件B中的模块,那么就需要在插件A的uplugin文件中添加对插件B的依赖:

1
2
3
4
5
6
"Plugins": [
{
"Name": "B",
"Enabled": true
}
]

然后就可以在插件A中添加插件B中的模块了。

UE4:Build Configurations

Build StatusDescrible
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

Install 7z on Linux

1
2
3
4
5
6
7
8
$ sudo apt-get update
$ sudo apt-get install p7zip-full
# 压缩
$ 7z a data.7z data.txt
# 显示7z文件中的文件列表
$ 7z l data.7z
# 解压
$ 7z e data.7z

UE4: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函数中被读取。
调用栈:

完整流程有时间再来分析。

UE4: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来加载。

完整流程有时间再来分析。

UE4:默认打包到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\Config\BaseEngine.ini
[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/DefaultPostProcessMaterial
+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

; Console platforms will remove EngineDebugMaterials from their StartupPackages
+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

; only needed for TextRender feature (3d Text in world)
+Package=/Engine/EngineMaterials/DefaultTextMaterialTranslucent
+Package=/Engine/EngineFonts/RobotoDistanceField

就算是工程中没有任何资源,也会默认把这些资源给打包进来。

UE4:创建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
// .h
#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;
};
// .cpp
#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

UE4: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
// LuaContext.cpp
if (!bAddUObjectNotify)
{
GUObjectArray.AddUObjectCreateListener(GLuaCxt); // add listener for creating UObject
GUObjectArray.AddUObjectDeleteListener(GLuaCxt); // add listener for deleting UObject
}

FLuaContext继承自FUObjectArray::FUObjectCreateListenerFUObjectArray::FUObjectDeleteListener,所以当UE的对象系统创建对象的时候会把调用到FLuaContext的NotifyUObjectCreatedNotifyUObjectDeleted

当创建一个UObject的时候会在FObjectArrayAllocateUObjectIndex中对多有注册过的CreateListener调用NotifyUObjectDeleted函数。

而UnLua实现覆写UFUNCTION的逻辑就是写在NotifyUObjectCreated中的TryBindLua调用中,栈如下:

一个一个来说他们的作用:

FLuaContext::TryBindUnlua

1
2
// Try to bind Lua module for a UObject
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函数中,它主要做了几件事情:

  1. 注册Class到lua
  2. require对应的lua模块
  3. 调用UnLuaManager::BindInternal函数
  4. 为当前对象创建一个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
/**
* Bind a Lua module for a UObject
*/
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())) // try to bind Lua module for survival UObject again...
{
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); // get all functions defined in the Lua module
TMap<FName, UFunction*> &UEFunctions = OverridableFunctions.Add(Class);
GetOverridableFunctions(Class, UEFunctions); // get all overridable UFunctions

OverrideFunctions(LuaFunctions, UEFunctions, Class, bNewCreated); // try to override UFunctions

return ConditionalUpdateClass(Class, LuaFunctions, UEFunctions);
}

这个函数接受到的参数是创建出来的UObject,以及它的UClass,还有对应的Lua的模块名。

  1. 把对象的UClass与Lua的模块名对应添加到ModuleNamesClasses
  2. 从Lua端通过L获取所指定模块名中的所有函数
  3. 从UClass获取所有的BlueprintEvent、RepNotifyFunc函数
  4. 对两边获取的结果调用UUnLuaManager::OverrideFunctions执行替换

UUnLuaManager::OverrideFunctions

对从Lua端获取的函数使用名字在当前类的UFunction中查找,依次对其调用UUnLuaManager::OverrideFunction.

UUnLuaManager::OverrideFunction

  1. 判断传入的UFunction是不是属于传入的Outer UClasss
  2. 判断是否允许调用被覆写的函数
  3. 调用AddFunction函数

UUnLuaManager::AddFunction

  1. 如果函数为FUNC_Native则将FLuaInvoker::execCallLua和所覆写的函数名通过AddNativeFunction添加至UClass
  2. UFunction内的函数指针替换为(FNativeFuncPtr)&FLuaInvoker::execCallLua
  3. 如果开启了允许调用被覆写的函数,则把替换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
// generated.h
#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; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTFUNC_Parms() \
: ReturnValue(false) \
{ \
} \
}; \
struct MyActor_eventTESTImplEvent_Parms \
{ \
AActor* InActor; \
int32 InIval; \
bool ReturnValue; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTImplEvent_Parms() \
: ReturnValue(false) \
{ \
} \
};

// gen.cpp
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;
}

可以看到,这里传递给ProcessEventUFunction*就是从当前对象的UClass中得到的。

经过前面分分析可以知道,UnLua实现的函数覆写,就是把UClass中的UFunction中的原生thunk函数指针替换为FLuaInvoker::execCallLua,而且当一个对象的BlueprintNativeEventBlueprintImplementableEvent函数被调用的时候会调用到ProcessEvent并传入对应的UFunction*,在ProcessEvent中又调Invork(调用其中的原生指针),也就是实现调用到了unlua中替换绑定的FLuaInvoker::execCallLua,在这个函数中再转发给调用lua端的函数,从而实现了覆写函数的目的。

UE4:引擎对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中查看,并对比不同引擎版本的区别。

UE4:引擎对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
// in ue 4.25
readonly int MinimumNDKToolchain = 210100;
readonly int MaximumNDKToolchain = 230100;
readonly int RecommendedNDKToolchain = 210200;

可以在Github上比较方便地查看不同引擎版本要求的NDK版本:UE_425_AndroidToolChain.cs

UE4:从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
// UnrealBuildTools/System/BuildVersion.cs
namespace UnrealBuildTool
{
/// <summary>
/// Holds information about the current engine version
/// </summary>
[Serializable]
public class BuildVersion
{
/// <summary>
/// The major engine version (4 for UE4)
/// </summary>
public int MajorVersion;

/// <summary>
/// The minor engine version
/// </summary>
public int MinorVersion;

/// <summary>
/// The hotfix/patch version
/// </summary>
public int PatchVersion;

/// <summary>
/// The changelist that the engine is being built from
/// </summary>
public int Changelist;

/// <summary>
/// The changelist that the engine maintains compatibility with
/// </summary>
public int CompatibleChangelist;

/// <summary>
/// Whether the changelist numbers are a licensee changelist
/// </summary>
public bool IsLicenseeVersion;

/// <summary>
/// Whether the current build is a promoted build, that is, built strictly from a clean sync of the given changelist
/// </summary>
public bool IsPromotedBuild;

/// <summary>
/// Name of the current branch, with '/' characters escaped as '+'
/// </summary>
public string BranchName;

/// <summary>
/// The current build id. This will be generated automatically whenever engine binaries change if not set in the default Engine/Build/Build.version.
/// </summary>
public string BuildId;

/// <summary>
/// The build version string
/// </summary>
public string BuildVersionString;

// ...
}
}

其中的MajorVersion/MinorVersion/PatchVersion分别对应X.XX.X。

UE4: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的路径的:

UE4:通过Commandline替换加载的ini

如项目下的DefaultEngine.ini/DefaultGame.ini等。
去掉Defaultini后缀之后是它们的baseName,可以通过下列命令行来替换:

1
2
3
4
# engine
-EngineINI=REPLACE_INI_FILE_PAT.ini
# game
-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
// Core/Private/Misc/ConfigCacheIni.cpp
FString FConfigCacheIni::GetDestIniFilename(const TCHAR* BaseIniName, const TCHAR* PlatformName, const TCHAR* GeneratedConfigDir)
{
// figure out what to look for on the commandline for an override
FString CommandLineSwitch = FString::Printf(TEXT("%sINI="), BaseIniName);

// if it's not found on the commandline, then generate it
FString IniFilename;
if (FParse::Value(FCommandLine::Get(), *CommandLineSwitch, IniFilename) == false)
{
FString Name(PlatformName ? PlatformName : ANSI_TO_TCHAR(FPlatformProperties::PlatformName()));

// if the BaseIniName doens't contain the config dir, put it all together
if (FCString::Stristr(BaseIniName, GeneratedConfigDir) != nullptr)
{
IniFilename = BaseIniName;
}
else
{
IniFilename = FString::Printf(TEXT("%s%s/%s.ini"), GeneratedConfigDir, *Name, BaseIniName);
}
}

// standardize it!
FPaths::MakeStandardFilename(IniFilename);
return IniFilename;
}

UE4:获取当前平台信息

可以使用FPlatformProperties来获取当前程序的平台信息。
同样是使用UE的跨平台库写法,FGenericPlatformProperties是定义在Core/Public/GenericPlatform/GenericPlatformProperties.h中的。

例:可以使用FPlatformProperties::PlatformName()运行时来获取当前平台的名字。

FPlatformPropertiestypedef是定义在Core/Public/HAL/PlatformProperties.h中。

UE4:COMPILED_PLATFORM_HEADER

在UE4.22之前,UE的跨平台库的实现方式都是创建一个泛型平台类:

1
2
3
4
struct FGenericPlatformUtils
{
static void GenericMethod(){}
};

然后每个平台实现:

1
2
3
4
5
6
7
8
// Windows/WindowsPlatformUtils.h
struct FWindowsPlatformUtils:public FGenericPlatformUtils
{
static void GenericMethod(){
//doSomething...
}
};
typedef FWindowsPlatformUtils FPlatformUtils;

在UE4.22之前,需要使用下面这种方法:

1
2
3
4
5
6
7
8
9
10
// PlatformUtils.h
#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
// Creates a string that can be used to include a header in the platform extension form "PlatformHeader.h", not like below form
#define COMPILED_PLATFORM_HEADER(Suffix) PREPROCESSOR_TO_STRING(PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#else
// Creates a string that can be used to include a header in the form "Platform/PlatformHeader.h", like "Windows/WindowsPlatformFile.h"
#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
// Creates a string that can be used to include a header with the platform in its name, like "Pre/Fix/PlatformNameSuffix.h"
#define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#else
// Creates a string that can be used to include a header with the platform in its name, like "Pre/Fix/PlatformName/PlatformNameSuffix.h"
#define COMPILED_PLATFORM_HEADER_WITH_PREFIX(Prefix, Suffix) PREPROCESSOR_TO_STRING(Prefix/PLATFORM_HEADER_NAME/PREPROCESSOR_JOIN(PLATFORM_HEADER_NAME, Suffix))
#endif

命名有规律是多么重要的一件事...

UE4:遍历UCLASS或USTRUCT的反射成员

可以通过TFieldIterator来遍历:

1
2
3
4
for (TFieldIterator<UProperty> PropertyIt(ProxyClass); PropertyIt; ++PropertyIt)
{
// ...
}

注意:4.25之后没有UProperty,变成了FProperty.

Lua:Metatable

UE4:控制打包时ini的拷贝

DeploymentContext.cs中的DeploymentContext函数中,有以下两行代码:

1
2
3
// Read the list of files which are whitelisted to be staged
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
/// <summary>
/// Determines if an individual config file should be staged
/// </summary>
/// <param name="SC">The staging context</param>
/// <param name="ConfigDir">Directory containing the config files</param>
/// <param name="ConfigFile">The config file to check</param>
/// <returns>True if the file should be staged, false otherwise</returns>
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

UE4:CharCast的坑

CharCast是定义在StringConv.h的模板函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Casts one fixed-width char type into another.
*
* @param Ch The character to convert.
* @return The converted character.
*/
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
/**
* Converts the [Src, Src+SrcSize) string range from SourceChar to DestChar and writes it to the [Dest, Dest+DestSize) range.
* The Src range should contain a null terminator if a null terminator is required in the output.
* If the Dest range is not big enough to hold the converted output, NULL is returned. In this case, nothing should be assumed about the contents of Dest.
*
* @param Dest The start of the destination buffer.
* @param DestSize The size of the destination buffer.
* @param Src The start of the string to convert.
* @param SrcSize The number of Src elements to convert.
* @param BogusChar The char to use when the conversion process encounters a character it cannot convert.
* @return A pointer to one past the last-written element.
*/
template <typename SourceEncoding, typename DestEncoding>
static FORCEINLINE typename TEnableIf<
// This overload should be called when SourceEncoding and DestEncoding are 'compatible', i.e. they're the same type or equivalent (e.g. like UCS2CHAR and WIDECHAR are on Windows).
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<
// This overload should be called when the types are not compatible but the source is fixed-width, e.g. ANSICHAR->WIDECHAR.
!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
/**
* Tests whether a particular character can be converted to the destination encoding.
*
* @param Ch The character to test.
* @return True if Ch can be encoded as a DestEncoding.
*/
template <typename DestEncoding, typename SourceEncoding>
static bool CanConvertChar(SourceEncoding Ch)
{
return IsValidChar(Ch) && (SourceEncoding)(DestEncoding)Ch == Ch && IsValidChar((DestEncoding)Ch);
}

所以:类似LoadFileToString去读文件如果编码不支持,那么读出来的数据和原始文件里是不一样的。

UE4: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的大小也减小的差不多。

UE4:Unreal Plugin Language

在UE中为移动端添加第三方模块或者修改配置文件时经常会用到AdditionalPropertiesForReceipt,里面创建ReceiptProperty传入的xml文件就是UE的Unreal Plugin Language脚本。

ReceiptProperty的平台名称在IOS和Android上是固定的,分别是IOSPluginAndroidPlugin,不可以指定其他的名字。

1
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(ThirdPartyPath, "Android/PlatformUtils_UPL_Android.xml")));

UE4:为IOS添加Framawork

IOS上的Framework有点类似于静态链接库的意思,相当于把.a+.h+资源打包到一块的集合体。更具体的区别描述请看:iOS库 .a与.framework区别

在UE中以集成IOS上操作Keycahin的SSKeychain为例,在Module的build.cs中使用PublicAdditionalFrameworks来添加:

1
2
3
4
5
6
7
PublicAdditionalFrameworks.Add(
new Framework(
"SSKeychain",
"ThirdParty/IOS/SSKeychain.embeddedframework.zip",
"SSKeychain.framework/SSKeychain.bundle"
)
);

构造Framework的第一个参数是名字,第二个是framework的路径(相对于Module),第三个则是解压之后的Framework的bundle路径(如果framework没有bundle则可以忽略这个参数,而且就算有bundle,但是不写这第三个参数貌似也没什么问题)。

这个可以打开SSKeychain.embeddedframework.zip文件看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SSKeychain.embeddedframework
└─SSKeychain.framework
│ Info.plist
│ SSKeychain

├─Headers
│ SSKeychain.h
│ SSKeychainQuery.h

├─Modules
module.modulemap

├─SSKeychain.bundle
│ └─en.lproj
│ SSKeychain.strings

└─_CodeSignature
CodeDirectory
CodeRequirements
CodeRequirements-1
CodeResources
CodeSignature

相对于.framework的路径,这个路径一定要填正确,不然是不能用的,因为打包时会把这个zip解压出来,然后拷贝到包体中,路径指定错了就无法拷贝了。

1
[2020.05.14-11.04.48:324][988]UATHelper: Packaging (iOS):     [2/183] sh Unzipping : /Users/zyhmac/UE4/Builds/ZHALIPENG/C/Users/imzlp/Documents/UnrealProjectSSD/MicroEnd_423/Plugins/PlatformUtils/Source/PlatformUtils/ThirdParty/IOS/SSKeychain.embeddedframework.zip -> /Users/zyhmac/UE4/Builds/ZHALIPENG/D/UnrealEngine/Epic/UE_4.23/Engine/Intermediate/UnzippedFrameworks/SSKeychain/SSKeychain.embeddedframework

UE4:打包时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的参数来把文件写入、

在Windows上查看iOS设备log

Andorid的设备可以使用adb logcat来捕获log,在想要看iOS的log却十分麻烦,还要Mac。

但是经过一番查找,找到了一个工具,可以在Windows上实时地查看当前设备log:IOSLogInfo

下载之后解压,执行sdsiosloginfo.exe就可以看到类似logcat的日志输出了,如果装了Git bash环境也可以使用|来进行过滤。

UE4: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
// RenderCore/Public/ShaderCodeLibrary.h
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是默认不导出的,但是我想知道原因是什么。

其实在有了上面的两个属性关键字之后不使用导入库也可以实现符号的导入和导出。

  1. 当某个PE文件被加载时。Windows加载器的其中一个任务就是把所有需要导入的函数地址确定并且将导入表中的元素调整到正确的地址,以实现动态链接的过程,导入表中有IAT,其中的每个元素对应一个被导入的符号。
  2. 编译器无法知道一个符号是从外部导入的还是本模块中定义的,所以编译器是直接产生调用指令
1
CALL XXXXXXXXX
  1. __declspec出现之前,微软提供的方法就是使用导入库,在这种情况下,对于导入函数的调用并不区分是导入函数还是导出函数,它统一地产生直接调用的指令,但是链接器在链接时会将导入函数的目标地址导向一小段桩代码(stub),由这个桩代码再将控制权交给IAT中的真正目标。
  2. 所以导入库的作用就是将编译器产生的调用命令转发到导入表的IAT中目标地址。

UE4:UCLASS的config

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Implements the settings for the Paper2D plugin.
*/
UCLASS(config=Engine, defaultconfig)
class PAPER2D_API UPaperRuntimeSettings : public UObject
{
GENERATED_UCLASS_BODY()

// Enables experimental *incomplete and unsupported* texture atlas groups that sprites can be assigned to
UPROPERTY(EditAnywhere, config, Category=Experimental)
bool bEnableSpriteAtlasGroups;

// Enables experimental *incomplete and unsupported* 2D terrain spline editing. Note: You need to restart the editor when enabling this setting for the change to fully take effect.
UPROPERTY(EditAnywhere, config, Category=Experimental, meta=(ConfigRestartRequired=true))
bool bEnableTerrainSplineEditing;

// Enables automatic resizing of various sprite data that is authored in texture space if the source texture gets resized (sockets, the pivot, render and collision geometry, etc...)
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

UE4:操作剪贴板Clipboard

有些需求是要能够访问到用户的粘贴板,来进行复制、和粘贴的功能。

在UE中访问粘贴板的方法如下:

1
2
3
4
5
6
FString PasteString;
// 从剪贴板读取内容
FPlatformApplicationMisc::ClipboardPaste(PasteString);

// 把123456放入剪贴板
FPlatformApplicationMisc::ClipboardCopy(TEXT("123465"));

注意:FPlatformApplicationMisc是定义在ApplicationCore下的,使用时要包含该模块。

UE4:在场景中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
/**
* Copies selected actors to the clipboard. Supports copying actors from multiple levels.
* NOTE: Doesn't support copying prefab instance actors!
*
* @param InWorld World to get the selected actors from
* @param bShouldCut If true, deletes the selected actors after copying them to the clipboard
* @param bIsMove If true, this cut is part of a move and the actors will be immediately pasted
* @param bWarnAboutReferences Whether or not to show a modal warning about referenced actors that may no longer function after being moved
*/
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
/*-----------------------------------------------------------------------------
Actor adding/deleting functions.
-----------------------------------------------------------------------------*/

class FSelectedActorExportObjectInnerContext : public FExportObjectInnerContext
{
public:
FSelectedActorExportObjectInnerContext()
//call the empty version of the base class
: FExportObjectInnerContext(false)
{
// For each object . . .
for (UObject* InnerObj : TObjectRange<UObject>(RF_ClassDefaultObject, /** bIncludeDerivedClasses */ true, /** IternalExcludeFlags */ EInternalObjectFlags::PendingKill))
{
UObject* OuterObj = InnerObj->GetOuter();

//assume this is not part of a selected actor
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)
{
// Add object to existing inner list.
Inners->Add( InnerObj );
}
else
{
// Create a new inner list for the outer object.
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
/**
* Pastes selected actors from the clipboard.
* NOTE: Doesn't support pasting prefab instance actors!
*
* @param InWorld World to get the selected actors from
* @param PasteTo Where to paste the content too
*/
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,符合上面的规则。

其他

相关资料:

工具:

UE4: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' };
// abc鸡de1
FString TestStr(UTF8_TO_TCHAR(TestArray));

UE4:编辑器SpawnActor

可以使用UEditorEngine中的SpawnActor函数。

1
2
// Editor/Private/EditorEngine.cpp
AActor* UEditorEngine::AddActor(ULevel* InLevel, UClass* Class, const FTransform& Transform, bool bSilent, EObjectFlags InObjectFlags)

使用这个方法Spawn出来的会自动选中。

虚拟制片的流程

最近想业余研究一下虚拟制片的工作流程,准备研究一下弄个个人版的方案玩玩,收集一些资料。

硬件要求:

  • Valve的定位基站(HTC Vive)
  • Vive Tracker一个
  • 相机+视频采集卡/网络摄像头
  • 绿幕

软件要求:

  • Unreal Engine
  • SteamVR
  • OBS

额外注意事项:

  • 拍摄时应该要和引擎内的帧率同步(高级点的相机
  • 拍摄时人物不应离绿幕太近会有反光的问题

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

UE4:枚举值与字符串的互相转换

有些需要序列化枚举值的需要,虽然我们可以通过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
template<typename ENUM_TYPE>
static FString GetEnumNameByValue(ENUM_TYPE InEnumValue, bool bFullName = false)
{
FString result;
{
FString TypeName;
FString ValueName;

UEnum* FoundEnum = StaticEnum<ENUM_TYPE>();
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
template<typename ENUM_TYPE>
static bool GetEnumValueByName(const FString& InEnumValueName, ENUM_TYPE& OutEnumValue)
{
bool bStatus = false;

UEnum* FoundEnum = StaticEnum<ENUM_TYPE>();
FString EnumTypeName = FoundEnum->CppType;
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;
}

UE4:PC远程打包IOS

首先在MAC的系统偏好设置-共享中启用远程登录:

然后在Windows上对项目添导入mobileproversion和设置BundleNameBundle Identifier
之后继续往下拉找到IOS-Build下的Remote Build Options:

填入目标MAC机器的IP地址和用户名。

然后点击Generated SSH Key会弹出一个窗口:

按任意键继续。
会提示你输入一个密码,按照提示输入,之后会提示你输入MAC电脑的密码,输入之后会提示:

1
Enter passphrase (empty for no passphrase):

这是让你输入生成的ssh Key的密码,默认情况下可以不输,直接Enter就好。
按照提示一直Enter会提示你ssh key生成成功:

再继续会提示让你输入第一次设置的密码,和目标MAC机器的密码,执行完毕之后就会提示没有错误,就ok了:

生成的SSH Key的存放路径为:

1
C:\Users\imzlp\AppData\Roaming/Unreal Engine/UnrealBuildTool/SSHKeys/192.168.2.89/imzlp/RemoteToolChainPrivate.key

如果要将其共享给组内的其他成员,则把这个RemoteToolChainPrivate.key共享,然后让他们把IOS-Build-RemoteBuildOptions下的Override existing SSH Permissions file设置为RemoteToolChainPrivate.key的路径即可。

之后就可以像打包Windows或者在Win上打包IOS一样了:

UE4:获取资源依赖关系

最近有个需求要获取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
// Engine\Plugins\Editor\AssetManagerEditor\Source\AssetManagerEditor\Private\ReferenceViewer\EdGraph_ReferenceViewer.cpp
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

UE4:地图的存储和加载

存储栈:

加载栈:

C++中delete[]的实现

注意:不同的编译器实现可能不一样,我使用的是Clang 7.0.0 x86_64-w64-windows-gnu

在C++中我们可以通过newnew[]在堆上分配内存,但是有没有考虑过下面这样的问题:

1
2
3
4
5
6
7
8
9
10
11
12
class IntClass{
public:
int v;
~IntClass(){}
};

int main()
{
IntClass *i = new IntClass[10];

delete[] i;
}

因为i就只是一个普通的指针,所以它没有任何的类型信息,那么delete[]的时候怎么知道要回收多少内存呢?

所以肯定是哪里存储了i的长度信息!祭出我们的IR代码:

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
; Function Attrs: noinline norecurse optnone uwtable
define dso_local i32 @main() #4 {
%1 = alloca i32, align 4
%2 = alloca %class.IntClass*, align 8
store i32 0, i32* %1, align 4
%3 = call i8* @_Znay(i64 48) #8
%4 = bitcast i8* %3 to i64*
store i64 10, i64* %4, align 8
%5 = getelementptr inbounds i8, i8* %3, i64 8
%6 = bitcast i8* %5 to %class.IntClass*
store %class.IntClass* %6, %class.IntClass** %2, align 8
%7 = load %class.IntClass*, %class.IntClass** %2, align 8
%8 = icmp eq %class.IntClass* %7, null
br i1 %8, label %21, label %9

; <label>:9: ; preds = %0
%10 = bitcast %class.IntClass* %7 to i8*
%11 = getelementptr inbounds i8, i8* %10, i64 -8
%12 = bitcast i8* %11 to i64*
%13 = load i64, i64* %12, align 4
%14 = getelementptr inbounds %class.IntClass, %class.IntClass* %7, i64 %13
%15 = icmp eq %class.IntClass* %7, %14
br i1 %15, label %20, label %16

; <label>:16: ; preds = %16, %9
%17 = phi %class.IntClass* [ %14, %9 ], [ %18, %16 ]
%18 = getelementptr inbounds %class.IntClass, %class.IntClass* %17, i64 -1
call void @_ZN8IntClassD2Ev(%class.IntClass* %18) #3
%19 = icmp eq %class.IntClass* %18, %7
br i1 %19, label %20, label %16

; <label>:20: ; preds = %16, %9
call void @_ZdaPv(i8* %11) #9
br label %21

; <label>:21: ; preds = %20, %0
ret i32 0
}

可以看到编译器给我们的new IntClass[10]通过@_Znay(i64 48)来分配了48个字节的内存!

但是按照sizeof(IntClass)*10来算其实之应该有40个字节的内存,多余的8个字节用来存储了数组的长度信息。

1
2
3
4
5
%3 = call i8* @_Znay(i64 48) #8
%4 = bitcast i8* %3 to i64*
store i64 10, i64* %4, align 8
%5 = getelementptr inbounds i8, i8* %3, i64 8
%6 = bitcast i8* %5 to %class.IntClass*

可以看到,它把数组的长度写入到了分配内存的前8个字节,在八个字节之后才可以分配真正的对象。

我们真正得到的i的地址就是偏移之后的,数组的长度写在第一个元素之前的64位内存中。

1
2
// 每个x代表一个byte,new IntClass[10]产生的内存布局
|xxxxxxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|

既然知道了它存在哪里,所以我们可以修改它(在修改之前我们delete[] i;会调用10次析构函数):

1
2
3
4
IntClass *i = new IntClass[10];
int64_t *ArrayLength = (int64_t*)((char*)(i)-8);
*ArrayLength = 1;
delete[] i;

这样修改之后delete[] i;只会调用1次析构函数,也印证了我们猜想。

v2ray中大量的67错误

看到v2ray里有大量的下列错误:

1
2020/04/12 17:31:30 tcp:127.0.0.1:53920 rejected  v2ray.com/core/proxy/socks: unknown Socks version: 67

官方说这是因为应用里设置里http代理,排查了一下,这是因为在win10的设置里开启了代理,在Win10设置-网络-代理关掉系统代理即可。

Chrome阅读模式

chrome://flags/中有Enable Reader Mode,开启即可。

VS内存断点

在使用VS调试的时候有在有些情况下需要知道一些对象在什么时候被修改了,如果按照单步一点一点来调试的话很不方便,这时候就可以使用VS的Data Breakpoint来进行断点调试:

添加Data Breakpoint的操作为Debug-New BreakPoint-Data Breakpoint(或者在Breakpoint窗口下):

需要在Address处输入要断点的内存地址,可以输入对象名字使用取地址表达式(&Test),如果想要断点的对象不是全局对象可以通过直接输入内存地址。

获取一个对象的内存地址的方法为在Watch下添加一条该对象的取地址表达式(可以使用&ival或者&this->ival):

其中Value的就得到了该对象的内存地址。

拿到内存地址之后就可以填到Data BreakpointAddress中了,然后指定它的数据大小(可选1/2/4/8):

当该地址的数据被修改时会提示触发了内存断点:

UE4: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;
};

UE4:FCommandLine过滤模式

FCommandLine是UE封装的启动参数的管理类,在Launch模块下的FEngineLoop::PreInit中被初始化(FCommandLine::Set)为程序启动的CmdLine
FCommandLine支持AppendParser这是比较常用的功能,但是今天要说的是另外一个: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
// target.cs
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被过滤掉了。

UE4:PIE的坑

UE在PIE中运行是和Standalone模式是不同的,在PIE运行时可以通过监听FEditorDelegates下的PreBeginPIE/BeginPIE以及PrePIEEnded/EndPIE等代理来检测PIE模式下的游戏运行和退出。
但是,PIE的PrePIEEndedEndPIE都是先于GameInstanceShutdown函数的,在一些情况下,如果PIEEnd中做了一些清理操作而在GameInstance的Shutdown函数中有使用的话会有问题。
解决办法为绑定FGameDelegatesEndPlayMapDelegate

1
FGameDelegates::Get().GetEndPlayMapDelegate().AddRaw(GLuaCxt, &FLuaContext::OnEndPlayMap);

UE4: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);

UE4: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

把上面两个目录都删掉。
如果启动项目时提示的是工程中的模块,则把工程和插件下的BinariesIntermediate都删了重新编译生成。

UE4: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

UE4:成员模板函数特化不要放在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)
{
// do something
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)
{
// do something
return *this;
}

UE4: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才行。

UE4:获取蓝图添加的所有接口

拿到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;
}
}

UE4:UnrealFrontEnd DeviceLog

之前在PC上看移动端的Log的方式是是用Logcat,但是发现UE其实提供了工具,就是UnrealFrontLog中的DeviceLog

最下面的那一行也可以执行控制台命令,很方便。

PS:我测试了在PC上连接Android设备没有问题,但是连上IOS不显示,在Mac上连接IOS没有问题,不过不显示Log,但可以执行命令。

UE4:获取某个类的所有子类

查找所有继承自某个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;
}

UE4:4.22.3打包IOS Crash的问题

在下列环境下:

  1. macOS Mojave 10.14.5
  2. XCode 11.2.1(11B500)
  3. UE4.22.3
  4. iPhone 7 IOS13.3.1

在这个环境下打包出来的IOS在运行时会Crash,但是换到4.23.1就没有这个问题。
Crash的Log:GWorld 2020-3-12 6-58-PM.crash

UE4: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 SettingsUSerInterface2D,但是据说非2次幂的贴图大小会有性能问题。

UE4:IOS证书申请和打包

IOS证书申请

首先在Mac上导出一个证书:
打开软件钥匙串访问-证书助理-从证书颁发机构请求证书

选择存储到磁盘,会生成一个CertificateSigningRequest.certSigningRequest的文件。
然后登录Apple Developer,进入Account-Certificates

进去之后创建Apple Development或者iOS App Development

添加设备:

可以使用UE的IPhonePackager.exe来查看ios设备的uuid:

生成Provision:

生成之后要下载provision文件:

UE4打包IOS项目设置

最重要的是下面几点:

  1. 导入mobileprovision
  2. BundleNameBundle Identifier设置为在Apple开发者网站上设置的Bundle ID,格式为com.xxxxx.yyyyyy.

UE4:最多75根骨骼的限制

因为移动平台缺少32位的索引支持,所以最多支持65k个顶点和75根骨骼。
但是可以通过拆分骨骼模型的材质来实现,每个材质支持75根,这是单次drawcall的限制,分成不同的批次就可以了。

PS:不能用uniform了,换其他方式,比如VTF,也可以实现超过75根骨骼。

UE4:Mac和ios平台问题收录

安装CommandLinTool

Xcode's metal shader compiler was not found, verify Xcode has been installed on this Mac and that it has been selected in Xcode > Preferences > Locations > Command-line Tools.

相关问题:

离线安装XCodeCommand Line Tools for Xcode可以从苹果的开发者网站下载:More Downloads for Apple Developers

在安装完Command Line Tools之后,如果cook时还是提示这个错误,则需要执行下列命令(当然要首先确保/Library/Developer/CommandLineTools路径存在,一般Command Line Tools的默认安装路径是这个):

1
$ sudo xcode-select -s /Library/Developer/CommandLineTools

当设置CommandLineTools之后打包时可能会提示:

1
ERROR: Invalid SDK MacOSX.sdk, not found in /Library/Developer/CommandLineTools/Platforms/MacOSX.platform/Developer/SDKs

这是因为通过xcode-select设置为CommandLineTools之后,打包时找不到Xcode里的库了。
解决的办法是在CommandLineTools的目录下创建一个Xcode中的Platforms目录的软连接:

1
sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms /Library/Developer/CommandLineTools/Platforms

actool错误

1
2
UATHelper: Packaging (iOS):   xcrun: error: unable to find utility "actool", not a developer tool or in PATH
PackagingResults: Error: unable to find utility "actool", not a developer tool or in PATH

这是因为把CommandLinTool设置为默认的命令行工具之后,CommandLinTool/use/bin下并没有actool等工具。
这就是个十分坑爹的问题,用xcode作为默认的命令行工具导致Cook不过,用CommandLineTool又编译时有问题。
我的解办法是把/Applications/Xcode.app/Contents/Developer/usr/bin通过软连接方式链接到/Library/Developer/CommandLineTools/usr:

1
2
3
4
# 当然要先备份CommandLineTool/usr/bin
$ mv /Library/Developer/CommandLineTools/usr/bin /Library/Developer/CommandLineTools/usr/Command_bin
# 创建xcode的bin目录的软连接
$ sudo ln -s /Applications/Xcode.app/Contents/Developer/usr/bin /Library/Developer/CommandLineTools/usr/bin

MAC上的文件位置

MacOS上打开UE项目的Log位置为~/Library/Logs/Unreal Engine/ProjectNameLocating Project Logs

CookResults: Error: Package Native Shader Library failed for MacNoEditor.

这是因为Project Settings-Packing-Shared Material Native Library,关掉之后就可以了。

PS:我在UE的issus里看到了类似的bug提交,但是标记在4.18时就修复了:UE-49105,不知道为什么还有这个问题。

UE4:使用HTTP请求下载文件的坑和技巧

使用HTTP可以请求下载文件,response的结果就是文件的内容。
在下载一个文件之前可以先使用HEAD请求来只获取头,可以从Content-Length头获取到文件的大小。

1
2
3
4
5
6
7
// head request
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
// get request
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
// openssl/md5.h
#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

/*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* ! MD5_LONG has to be at least 32 bits wide. If it's wider, then !
* ! MD5_LONG_LOG2 has to be defined along. !
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/

#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
/*
* _CRAY note. I could declare short, but I have no idea what impact
* does it have on performance on none-T3E machines. I could declare
* int, but at least on C90 sizeof(int) can be chosen at compile time.
* So I've chosen long...
* <[email protected]>
*/
#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]);

// result
pritf("MD5:%s",md5string);
}

这样当文件下载完,MD5计算就完成了。

注:在UE4中(~4.24)提供的OpenSSL在Win下只支持到VS2015,可以自己把这个限制给去掉(VS2015的链接库在VS2017中使用也没有问题)。

UE4:PlatformMisc的跨平台实现

当我们使用FPaths::ProjectDir()的是否考虑过它是怎么实现在不同的平台上为不同的路径的呢?
首先看一下FPaths::ProjectDir()的代码:

1
2
3
4
5
// Runtime/Core/Private/Misc/Paths.cpp
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
// Runtime/Core/Public/HAL/PlatformMisc.h
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#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
/**
* Windows implementation of the misc OS functions
**/
struct CORE_API FWindowsPlatformMisc
: public FGenericPlatformMisc{/*.....*/}

FGenericPlatformMisc中声明了我们常用的ProjectDir函数,供各个平台来独立实现,这样在通过FPlatformMisc来调用的时候就是所编译的目标平台的实现,这是UE实现跨平台代码的思路。

UE4:为APK添加外部存储独写权限

Project Settings-Platform-Android-Advanced APK Packaging-Extra Permissions下添加:

1
2
android.permission.WRITE EXTERNAL STORAGE
android.permission.READ_EXTERNAL_STORAGE

UE4:Android写入文件

当调用FFileHelper::SaveArrayToFile时:

1
FFileHelper::SaveArrayToFile(TArrayView<const uint8>(data, delta), *path, &IFileManager::Get(), EFileWrite::FILEWRITE_Append));

在该函数内部会创建一个FArchive的对象来管理当前文件,其内部具有一个IFileHandle的对象Handle,在Android平台上是FFileHandleAndroid

FArchive中写入文件调用的是Serialize,它又会调用HandleWrite

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
// Runtime/Core/Private/Android/AndroidFile.h
virtual bool Write(const uint8* Source, int64 BytesToWrite) override
{
CheckValid();
if (nullptr != File->Asset)
{
// Can't write to assets.
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;
}

// Update the cached file length
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
// bionic_errdefs.h
#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
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);

UE4:gituhb快捷代码搜索

可以把ue的官方仓库的搜索创建为Chrome的自定义搜索引擎:

1
https://github.com/EpicGames/UnrealEngine/search?q=%s&unscoped_q=%s

这样在chrome的地址栏就可以通过uesource来触发搜索。

UE4:控制写入文件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);

UE4: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:

npm换源

把npm从官方换到淘宝源:

1
$ npm config set registry https://registry.npm.taobao.org

国内速度会快很多。

UE4:打包时添加外部文件

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^关键字

UE4: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标准支持度不同。

NDKclang version
r14bclang 3.8.275480 (based on LLVM 3.8.275480)
r17cclang version 6.0.2
r18bclang version 7.0.2
r20bclang version 8.0.7

UE4:根据枚举名字获取枚举值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// get enum value by name
{
FString EnumName = TEXT("ETargetPlatform::");
EnumName.Append(Platform->AsString());

UEnum* ETargetPlatformEnum = FindObject<UEnum>(ANY_PACKAGE, TEXT("ETargetPlatform"), 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;
}
}

三大运营商个人轨迹证明方法

一、电信手机用户证明方法

编辑短信“CXMYD#身份证号码后四位”到10001,授权回复Y后,实现“漫游地查询",可查询手机号近15日内的途径地信息。

二、联通手机用户证明方法

手机发送:“CXMYD#身份证后四位”至10010,查询近30天的全国漫游地信息,便于返工辅助排查。

三、移动用户证明方法

编写CXMYD,发送到10086,再依据回复短信输入身份证后四位,可查询过去一个月内去过的省和直辖市(无地市)。

每人免费一天查询10次。

npm install报错

在npm安装时遇到下列错误:

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
[email protected]:/mnt/c/Users/Administrator/Desktop/hexo# npm install hexo-cli -g
npm http GET https://registry.npmjs.org/hexo-cli
npm http GET https://registry.npmjs.org/hexo-cli
npm http GET https://registry.npmjs.org/hexo-cli
npm ERR! Error: CERT_UNTRUSTED
npm ERR! at SecurePair.<anonymous> (tls.js:1370:32)
npm ERR! at SecurePair.EventEmitter.emit (events.js:92:17)
npm ERR! at SecurePair.maybeInitFinished (tls.js:982:10)
npm ERR! at CleartextStream.read [as _read] (tls.js:469:13)
npm ERR! at CleartextStream.Readable.read (_stream_readable.js:320:10)
npm ERR! at EncryptedStream.write [as _write] (tls.js:366:25)
npm ERR! at doWrite (_stream_writable.js:223:10)
npm ERR! at writeOrBuffer (_stream_writable.js:213:5)
npm ERR! at EncryptedStream.Writable.write (_stream_writable.js:180:11)
npm ERR! at write (_stream_readable.js:583:24)
npm ERR! If you need help, you may report this log at:
npm ERR! <http://github.com/isaacs/npm/issues>
npm ERR! or email it to:
npm ERR! <[email protected]>

npm ERR! System Linux 3.4.0+
npm ERR! command "/usr/bin/nodejs" "/usr/bin/npm" "install" "hexo-cli" "-g"
npm ERR! cwd /mnt/c/Users/Administrator/Desktop/hexo
npm ERR! node -v v0.10.25
npm ERR! npm -v 1.3.10
npm ERR!
npm ERR! Additional logging details can be found in:
npm ERR! /mnt/c/Users/Administrator/Desktop/hexo/npm-debug.log
npm ERR! not ok code 0

解决办法,在bash执行下列命令:

1
$ npm config set strict-ssl false

之后重新执行安装即可。

UE4:将BindAction暴露给蓝图

UInputComponent函数中的BindAction是个模板函数,可以在代码中使用,但是在蓝图中就很不方便了。

本来想着直接裹一个函数库的实现将BindAction暴露给蓝图,但是在BindAction需要传入的函数代理那里在蓝图里传递很不方便,看了一下BindAction的代码,写了下面这个函数,可以通过传递函数名来绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// .h
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);
};

// .cpp
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));
}

UE4:APK包中OBB文件

在选择把data文件打包APK之后,把打包出的APK解包,是可以看到obb文件的,在assets文件夹下有main.obb.webp,其就是Saved/StagedBuilds/目录下的PROJECT_NAME.obb文件,HASH值都是一样的。

其实OBB文件中存储的就是我们的pak文件,使用7z之类的压缩软件可以打开未加密的obb查看,可以看到它的目录结构就是PROJECT_NAME/Content/Paks/PROJECTNAME-Android_ETC2.pak这样的形式。

Adb命令

首先先要下载Adb

Adb安装Apk

1
$ adb install APK_FILE_NAME.apk

Adb启动App

安装的renderdoccmd是没有桌面图标的,想要自己启动的话只能使用下列adb命令:

1
adb shell am start org.renderdoc.renderdoccmd.arm64/.Loader -e renderdoccmd "remoteserver"

adb启动App的shell命令模板:

1
adb shell am start PACKAGE_NAME/.ActivityName

这个方法需要知道App的包名和Activity名,包名很容易知道,但是Activity如果不知道可以通过下列操作获取:

首先使用一个反编译工具将apk解包(可以使用之前的apktools):

1
apktool.bat d -o ./renderdoccmd_arm64 org.renderdoc.renderdoccmd.arm64.apk

然后打开org.renderdoc.renderdoccmd.arm64目录下的AndroidManifest.xml文件,找到其中的Application项:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.renderdoc.renderdoccmd.arm64" platformBuildVersionCode="26" platformBuildVersionName="8.0.0">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
<application android:debuggable="true" android:hasCode="true" android:icon="@drawable/icon" android:label="RenderDocCmd">
<activity android:configChanges="keyboardHidden|orientation" android:exported="true" android:label="RenderDoc" android:name=".Loader" android:screenOrientation="landscape">
<meta-data android:name="android.app.lib_name" android:value="renderdoccmd"/>
</activity>
</application>
</manifest>

其中有所有注册的Activity,没有有界面的apk只有一个Activity,所以上面的renderdoccmd的主Activity就是.Loader

如果说有界面的app,则会有多个,则可以从AndroidManifest.xml查找Category或者根据命名(名字带main的Activity)来判断哪个是主Activity。一般都是从lanucher开始,到main,或者有的进登陆界面。

PS:使用UE打包出游戏的主Activity是com.epicgames.ue4.SplashActivity,可以通过下列命令启动。

1
adb shell am start com.imzlp.GWorld/com.epicgames.ue4.SplashActivity

Adb传输文件

使用adb往手机传文件:

1
2
# adb push 1.0_Android_ETC2_P.pak /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks
$ adb push FILE_NAME REMOATE_PATH

从手机传递到电脑:

1
2
# adb pull /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks/1.0_Android_ETC2_P.pak A.Pak
$ adb pull REMOATE_FILE_PATH LOCAL_PATH

Adb:Logcat

使用logcast可以看到Android的设备Log信息。

1
$ adb logcat

会打印出当前设备的所有信息,但是我们调试App时不需要看到这么多,可以使用find进行筛选(注意大小写严格区分):

1
2
# adb logcat | find "GWorld"
$ adb logcat | find "KEY_WORD"

查看UE打包的APP所有的log可以筛选:

1
$ adb logcat | find "UE4"

如果运行的次数过多积累了大量的Log,可以使用清理:

1
adb logcat -c

Adb:从设备中提取已安装的APK

注意:执行下列命令时需要检查手机是否开放开发者权限,手机上提示的验证指纹信息要允许。

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
# 查看链接设备
$ adb devices
List of devices attached
b2fcxxxx unauthorized
# 列出手机中安装的所有app
$ adb shell pm list package
# 如果提示下问题,则需要执行adb kill-server
error: device unauthorized.
This adb servers $ADB_VENDOR_KEYS is not set
Try 'adb kill-server' if that seems wrong.
Otherwise check for a confirmation dialog on your device.
# 正常情况下会列出一堆这样的列表
C:\Users\imzlp>adb shell pm list package
package:com.miui.screenrecorder
package:com.amazon.mShop.android.shopping
package:com.mobisystems.office
package:com.weico.international
package:com.github.shadowsocks
package:com.android.cts.priv.ctsshim
package:com.sorcerer.sorcery.iconpack
package:com.google.android.youtube

# 找到指定app的的apk位置
$ adb shell pm path com.github.shadowsocks
package:/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk
# 然后将该文件拉取到本地来即可
$ adb pull /data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk
/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/...se.apk: 1 file pulled. 21.5 MB/s (4843324 bytes in 0.215s)

Adb刷入Recovery

下载Adb,然后根据具体情况使用下列命令(如果当前已经在bootloader就不需要执行第一条了)。

1
2
3
4
5
6
adb reboot bootloader
# 写入img到设备
fastboot flash recovery recovery.img
fastboot flash boot boot.img
# 引导img
fastboot boot recovery.img

UE4:移动端的启动参数

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
// Launch\Private\Android\LaunchAndroid.cpp
static void InitCommandLine()
{
static const uint32 CMD_LINE_MAX = 16384u;

// initialize the command line to an empty string
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);

// chop off trailing spaces
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());
}

// read in the command line text file from the sdcard if it exists
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)
{
// if that failed, try the lowercase version
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);

// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

// initialize the command line to an empty string
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)
{
// initialize the commandline
FCommandLine::Set(TEXT(""));

FString CommandLineFilePath = FString([[NSBundle mainBundle] bundlePath]) + TEXT("/ue4commandline.txt");

// read in the command line text file (coming from UnrealFrontend) if it exists
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)
{
// chop off trailing spaces
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);

// now merge the GSavedCommandLine with the rest
FCommandLine::Append(*GSavedCommandLine);

FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Combined iOS Commandline: %s") LINE_TERMINATOR, FCommandLine::Get());
}

关键就是[[NSBundle mainBundle] bundlePath]这一句,它获取的是App的包路径,所以把UE4CommandLine.txt放到包路径下就可以了。

UE4:获取UEnum

在UE4.22+的版本中可以使用下列方法:

1
const UEnum* TypeEnum = StaticEnum<EnumType>();

在4.21及之前的版本就要麻烦一点:

1
const UEnum* TypeEnum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EnumType"), true);

注意上面的TEXT("EnumType")其中要填想要获取的枚举类型名字。

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"
]
}

UE4:扫描资源引用时需注意Redirector

当我们在UE的资源管理器中进行rename/move等操作时,会产生一个与更名之前名字一样的Redirector

在使用UAssetManager::GetAssets进行资源的扫描时也会查询到这些redirector,但是他们不是真正的资源,在处理时需要过滤掉它们。
FAssetData中有一个成员函数IsRedirector可以用来判断扫描到的FAssetData是不是重定向器。

但是良好的项目规范是每进行delete/rename/move之后都手动在编辑器中执行Fix up Redirector In Folder,就会清理掉这些Redirector了,保持项目的干净。

UE4:Cook执行代码

UE中执行Cook 的代码位于UnrealEd模块下,源码位于:

1
Editor/UnrealEd/Private/Commandlets/CookCommandlet.cpp

其中有int32 UCookCommandlet::Main(const FString& CmdLineParams)是起始逻辑。

UE4: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的对象进去。

UE4:移动设备的渲染预览

ToolBar-Settings-PreviewRenderingLevel-Android ES3.1/Android ES2/IOS

Android SDK版本与Android的版本

可以在Google的开发者站点看到:Android SDK Platform

AndroidVersionSDK Version
Android 10(API level 29)
Android 9(API level 28)
Android 8.1(API level27)
Android 8.0(API level 26)
Android 7.1(API level 25)
Android 7.0(API level 24)
Android 6.0(API level 23)
Android 5.1(API level 22)
Android 5.0(API level 21)
Android 4.4W(API level 20)
Android 4.4(API level 19)
Android 4.3(API level 18)
Android 4.2(API level 17)
Android 4.1(API level 16)
Android 4.0.3(API level15)
Android 4.0(API level 14)
Android 3.2(API level 13)
Android 3.1(API level 12)
Android 3.0(API level 11)
Android 2.3.3(API level 10)
Android 2.3(API level 9)

UE4对Android的最低支持是SDK9,也就是Android2.3。

UE4:Android项目设置

  • EnableGradleInsteadOfAnt:使用Gradle替代Ant用来编译和生成APK。
  • EnableFullScreenImmersiveOnKitKatAndAboveDevices:全屏模式下隐藏虚拟按键;
  • EnableImprovedVirtualKeyboard:启用虚拟键盘;

预处理使用##时需要注意编译器的不统一

下列代码在MSVC中编译的过:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(ReturnType,InGetFuncName) \
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey,##ReturnType##& OutValue)\
{\
bool bLoadIniValueStatus = GConfig->##InGetFuncName##(\
GAME_EXTENSION_SETTINGS_SECTION,\
*InKey,\
OutValue,\
GAME_EXTENSION_SETTINGS_INI_FILE\
);\
return bLoadIniValueStatus;\
}

DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(FString,GetString);

但是在GCC/Clang中会又如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Preprocess.cpp:16:1: error: pasting formed ',FString', an invalid preprocessing token
DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(FString,GetString);
^
Preprocess.cpp:5:69: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey,##ReturnType##& OutValue)\
^
Preprocess.cpp:16:1: error: pasting formed 'FString&', an invalid preprocessing token
Preprocess.cpp:5:81: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey,##ReturnType##& OutValue)\
^
Preprocess.cpp:16:1: error: pasting formed '->GetString', an invalid preprocessing token
Preprocess.cpp:7:39: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool bLoadIniValueStatus = GConfig->##InGetFuncName##(\
^
Preprocess.cpp:16:1: error: pasting formed 'GetString(', an invalid preprocessing token
Preprocess.cpp:7:54: note: expanded from macro 'DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY'
bool bLoadIniValueStatus = GConfig->##InGetFuncName##(\
^
4 errors generated.

这是由于GCC/Clang要求预处理之后的的结果必须是一个已定义的符号,MSVC在这方面和它们不一样,解决办法为在非拼接顺序字符的地方删掉##

1
2
3
4
5
6
7
8
9
10
11
12
#define DEFINE_GAME_EXTENSION_TYPE_VALUE_BY_KEY(ReturnType,InGetFuncName) \
bool GetGameExtension##ReturnType##ValueByKey(const FString& InKey, ReturnType& OutValue)\
{\
OutValue = ReturnType{};\
bool bLoadIniValueStatus = GConfig->InGetFuncName(\
GAME_EXTENSION_SETTINGS_SECTION,\
*InKey,\
OutValue,\
GAME_EXTENSION_SETTINGS_INI_FILE\
);\
return bLoadIniValueStatus;\
}

相关文章:

UE4:在构造函数中用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

Chome79在Win10中崩溃

Chrome自动更新之后所有的页面都变成了这个样子:

解决办法是在chrome的快捷方式中添加-no-sandbox参数,但会提示您使用的是不受支持的命令行标记:-no-sandbox。稳定性和安全性会有所下降。,暂时我还没找到根本解决的办法,暂时先这样。

UE4:在其他的线程运行程序并获取程序输出

在写避编辑器功能的时候经常会启动外部的程序来执行任务,如果要求程序执行完成才走其他逻辑则会阻塞,这样的体验很不好,所以一般是开一个额外的线程来执行程序启动。废话不多说直接看代码:

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);
// std::cout << TCHAR_TO_ANSI(*mProgramPath) << " " << TCHAR_TO_ANSI(*mPragramParams) << std::endl;

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)
{
// process the string to break it up in to lines
Line += NewLine;
TArray<FString> StringArray;
int32 count = Line.ParseIntoArray(StringArray, TEXT("\n"), true);
if (count > 1)
{
for (int32 Index = 0; Index < count - 1; ++Index)
{
StringArray[Index].TrimEndInline();
ProcOutputMsgDelegate.Broadcast(StringArray[Index]);
}
Line = StringArray[count - 1];
if (NewLine.EndsWith(TEXT("\n")))
{
Line += TEXT("\n");
}
}
}
}

int32 ProcReturnCode;
if (FPlatformProcess::GetProcReturnCode(mProcessHandle,&ProcReturnCode))
{
if (ProcReturnCode == 0)
{
ProcSuccessedDelegate.Broadcast();
}
else
{
ProcFaildDelegate.Broadcast();
}
}

}
mThreadStatus = EThreadStatus::Completed;
return 0;
}
virtual void Exit()override
{
if (mProcessHandle.IsValid())
{

}
}
virtual void Cancel()override
{
if (GetThreadStatus() != EThreadStatus::Busy)
return;
mThreadStatus = EThreadStatus::Canceling;
if (mProcessHandle.IsValid() && FPlatformProcess::IsApplicationRunning(mProcessID))
{
FPlatformProcess::TerminateProc(mProcessHandle, true);
ProcFaildDelegate.Broadcast();
mProcessHandle.Reset();
mProcessID = 0;
}
mThreadStatus = EThreadStatus::Canceled;
CancelDelegate.Broadcast();
}

virtual uint32 GetProcesId()const { return mProcessID; }
virtual FProcHandle GetProcessHandle()const { return mProcessHandle; }

public:
FProcStatusDelegate ProcBeginDelegate;
FProcStatusDelegate ProcSuccessedDelegate;
FProcStatusDelegate ProcFaildDelegate;
FOutputMsgDelegate ProcOutputMsgDelegate;

private:
FRunnableThread* mThread;
FString mProgramPath;
FString mPragramParams;
void* mReadPipe;
void* mWritePipe;
uint32 mProcessID;
FProcHandle mProcessHandle;
};

可以通过监听ProcOutputMsgDelegate来接收程序的打印输出。

UE4:绑定FTicker

在非Actor的对象上如果想要使用Tick,可以使用下列代码:

1
FDelegateHandle TickHandle = FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this,&UGameExtensionSettings::Tick);

PS:FCoreDelegatesFCoreUObjectDelegates中有很多有用的代理。

UE4:地图加载的Delegate

FCoreUObjectDelegates中的这两个代理在地图加载时和地图加载完成时会调用,可以用来显示加载地图时的过场:

1
2
FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UMarinGameInstance::BeginLoadingScreen);
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMarinGameInstance::EndL

UE4:StaticLoadClass

可以从通过一个字符串来加载UClass:

1
UClass* UserWidgetClass = StaticLoadClass(UUserWidget::StaticClass(), NULL, TEXT("WidgetBlueprintGeneratedClass'/Game/TEST/BP_UserWidget.BP_UserWidget_C'"));

同理也有StaticLoadObject

UE4:ConstructorHelpers::FClassFinder

只能在构造函数中调用,否则引擎会崩溃,在外部使用可以使用StaticLoadClass

CMD的sudo

在Windows上时长会遇到要使用管理员权限执行的命令,而Win的cmd又不如Linux那样可以直接sudo来获取管理员权限,还要手动敲cmd然后右键管理员权限运行 十分麻烦,下面这个脚本可以实现类似bash的sudo操作:

1
2
3
@echo off
powershell -Command "(($arg='/k cd /d '+$pwd+' && %*') -and (Start-Process cmd -Verb RunAs -ArgumentList $arg))| Out-Null"
@echo on

将其保存为sudo.bat然后放入C:\Windows下即可,随便打开一个cmd可以输入sudo来执行命令了。

UE4:在游戏项目中添加编辑器模块

首先创建一个空的C++项目,其目录结构为:

1
2
3
4
5
6
7
8
9
10
C:\GWorld\Source>>tree /a /f
| GWorld.Target.cs
| GWorldEditor.Target.cs
|
\---GWorld
GWorld.Build.cs
GWorld.cpp
GWorld.h
GWorldGameModeBase.cpp
GWorldGameModeBase.h
  1. 我们在Source目录下添加一个GWorldEditor的文件夹,在其中新建GWorldEditor.Build.cs/GWorldEditor.cpp/GWorldEditor.h三个文件,并仿照GWorld模块下的文件内容修改;

  2. GWorldEditor.Build.cs中添加GWorld的模块依赖;

  3. 修改GWorldEditor.Target.cs文件将ExtraModuleNames.AddRange( new string[] { "GWorld" } );其中的GWorld修改为GWorldEditor

  4. 修改项目的uproject文件,在Modules下添加GWorldEditor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    {
    "FileVersion": 3,
    "EngineAssociation": "4.22",
    "Category": "",
    "Description": "",
    "Modules": [
    {
    "Name": "GWorld",
    "Type": "Runtime",
    "LoadingPhase": "Default",
    "AdditionalDependencies": [
    "Engine",
    "CoreUObject"
    ]
    },
    {
    "Name": "GWorldEditor",
    "Type": "Editor",
    "LoadingPhase": "PostEngineInit"
    }
    ]
    }

之后重新生成VS的解决方案,编译即可。

可能遇到的错误

1
LogInit: Warning: Still incompatible or missing module: GWorld

如果遇到上面的错误是因为没在GWorldEditor.Build.cs中添加GWorld的模块依赖。

参考文章:Creating an Editor Module

UE4:does not match the necessary signature

在蓝图事件绑定C++的Dispatcher时会有这样的提示:

1
XXXXEvent Signature Error: The function/event `XXXXEvent` does not match the necessary signature - the delegate or function/event changed?

这是因为C++里的Dispather的传入参数不是const&的(上面图里是TArray<Type>),在代码里把Delegate的声明参数改为const TArray<Type>&然后重新编译即可。

UE4:数据成员初始化顺序必须要与声明顺序一致

如:

1
2
3
4
5
6
7
8
class A
{
// error in UE4,ill in c++
A():dval(0.0f),ival(0){}
public:
int ival;
double dval;
};

会有下列错误:

1
error C5038: data member 'UTcpNetPeer::RecvMessageDataRemaining' will be initialized after data member 'UTcpNetPeer::ConnectionRetryTimes'

UE4:Delegate

UE的Delegate分了几个不同的种类,普通的代理是模板类,Dynamic Delegate是通过宏展开生成类,Dynamic Mulitcast Delegate需要UHT参与生成代码,所以动态多播代理只能写到包含generated.h的文件中。

Delegate

  • DECLARE_DELEGATE:普通代理,可以被值传递,本质实现是TBaseDelegare<__VA_ARGS__>的对象,可以使用BindUObject等函数。

TBaseDelegate里定义了很多的辅助函数,如BindSP/BindRaw/BindStatic/CreateSP等。

Dynamic Delegate

  • DECLARE_DYNAMIC_DELEGATE:动态代理可以序列化,其函数可按命名查找,执行速度比常规代理慢。

动态代理本质上是继承自TBaseDynamicDelegate的类,TBaseDynamicDelegate又继承自TScriptInterface,所以动态代理可以绑定UObject和绑定UFUNCTION。

其中BindUFunctionTScriptInterface的函数,而BindUbject是个宏,定义在Delegate.h中。

代码分析:

1
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnTestDynamicDelegate, const FString&, InStr);

DECLARE_DYNAMIC_DELEGATE_OneParam的宏定义为:

1
#define DECLARE_DYNAMIC_DELEGATE_OneParam( DelegateName, Param1Type, Param1Name ) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE) FUNC_DECLARE_DYNAMIC_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, FUNC_CONCAT( Param1Type InParam1 ), FUNC_CONCAT( *this, InParam1 ), void, Param1Type )

BODY_MACRO_COMBINE宏其经过UHT之后生成的代码为:

1
2
3
4
5
6
7
8
9
10
11
#define GWorldClient_Plugins_HotPackage_HotPatcher_Source_HotPatcherEditor_Private_CreatePatch_ExportPatchSettings_h_22_DELEGATE \
struct _Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms \
{ \
FString InStr; \
}; \
static inline void FOnTestDynamicDelegate_DelegateWrapper(const FScriptDelegate& OnTestDynamicDelegate, const FString& InStr) \
{ \
_Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms Parms; \
Parms.InStr=InStr; \
OnTestDynamicDelegate.ProcessDelegate<UObject>(&Parms); \
}

进行展开之后,可以看到使用DECLARE_DYNAMIC_DELEGATE_OneParam声明的代理实际上是就被定义了一个static函数。

FUNC_DECLARE_DYNAMIC_DELEGATE宏是裹了一个TBaseDynamicDelegate

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
/** Declare user's dynamic delegate, with wrapper proxy method for executing the delegate */
#define FUNC_DECLARE_DYNAMIC_DELEGATE( TWeakPtr, DynamicDelegateName, ExecFunction, FuncParamList, FuncParamPassThru, ... ) \
class DynamicDelegateName : public TBaseDynamicDelegate<TWeakPtr, __VA_ARGS__> \
{ \
public: \
/** Default constructor */ \
DynamicDelegateName() \
{ \
} \
\
/** Construction from an FScriptDelegate must be explicit. This is really only used by UObject system internals. */ \
explicit DynamicDelegateName( const TScriptDelegate<>& InScriptDelegate ) \
: TBaseDynamicDelegate<TWeakPtr, __VA_ARGS__>( InScriptDelegate ) \
{ \
} \
\
/** Execute the delegate. If the function pointer is not valid, an error will occur. */ \
inline void Execute( FuncParamList ) const \
{ \
/* Verify that the user object is still valid. We only have a weak reference to it. */ \
checkSlow( IsBound() ); \
ExecFunction( FuncParamPassThru ); \
} \
/** Execute the delegate, but only if the function pointer is still valid */ \
inline bool ExecuteIfBound( FuncParamList ) const \
{ \
if( IsBound() ) \
{ \
ExecFunction( FuncParamPassThru ); \
return true; \
} \
return false; \
} \
};

所有的宏都被展开之后:

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
struct _Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms
{
FString InStr;
};
static inline void FOnTestDynamicDelegate_DelegateWrapper(const FScriptDelegate& OnTestDynamicDelegate, const FString& InStr)
{
_Script_HotPatcherEditor_eventOnTestDynamicDelegate_Parms Parms;
Parms.InStr=InStr;
OnTestDynamicDelegate.ProcessDelegate<UObject>(&Parms);
}


class FOnTestDynamicDelegate : public TBaseDynamicDelegate<FWeakObjectPtr, void, const FString&>
{
public:
FOnTestDynamicDelegate() { }
explicit FOnTestDynamicDelegate( const TScriptDelegate<>& InScriptDelegate ) : TBaseDynamicDelegate<FWeakObjectPtr, void, const FString&>( InScriptDelegate ) { }
inline void Execute( const FString& InStr ) const { checkSlow( IsBound() ); FOnTestDynamicDelegate_DelegateWrapper( *this, InStr ); }
inline bool ExecuteIfBound( const FString& InStr ) const
{
if( IsBound() )
{
FOnTestDynamicDelegate_DelegateWrapper( *this, InStr );
return true;
}
return false;
}
};

Multicast Delegate

  • 多播代理:与普通代理的大部分功能相同,它们只拥有对象的弱引用,可以和结构体一起使用,可以复制。其本质是TMulticastDelegate<__VA_ARGS__>的对象。多播代理可以被加载/保存和远程触发,但多播代理不能使用返回值。

多播代理可以具有多个绑定,当Broadcast触发时,所有的绑定都会被调用。

多播代理可以使用Add/AddStatic/AddRaw/AddSP/AddUObject/Remove/RemoveAll等函数。

注意:RemoveAll会删除所有绑定时提供指针的代理,未绑定到对象的Raw代理不会被该函数删除。

Dynamic Multicast Delegate

  • DECLARE_DYNAMIC_MULITCAST_DELEGATE:动态多播代理。

动态多播代理必须要绑定到UFUNCTION的函数,使用宏AddDynamic或者AddUnique来添加绑定。

代码分析:

1
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTestDynamicMultiDelegate, const FString&, InStr);

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam其宏定义为:

1
2
3
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( DelegateName, Param1Type, Param1Name )\
BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE)\
FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, FUNC_CONCAT( Param1Type InParam1 ), FUNC_CONCAT( *this, InParam1 ), void, Param1Type )

其中BODY_MACRO_COMBINE经过UHT之后生成下列代码:

1
2
3
4
5
6
7
8
9
10
11
#define GWorldClient_Plugins_HotPackage_HotPatcher_Source_HotPatcherEditor_Private_CreatePatch_ExportPatchSettings_h_22_DELEGATE \
struct _Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms \
{ \
FString InStr; \
}; \
static inline void FOnTestDynamicMultiDelegate_DelegateWrapper(const FMulticastScriptDelegate& OnTestDynamicMultiDelegate, const FString& InStr) \
{ \
_Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms Parms; \
Parms.InStr=InStr; \
OnTestDynamicMultiDelegate.ProcessMulticastDelegate<UObject>(&Parms); \
}

FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE则创建了一个继承自TBaseDynamicMulticastDelegate类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Declare user's dynamic multi-cast delegate, with wrapper proxy method for executing the delegate */
#define FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(TWeakPtr, DynamicMulticastDelegateName, ExecFunction, FuncParamList, FuncParamPassThru, ...) \
class DynamicMulticastDelegateName : public TBaseDynamicMulticastDelegate<TWeakPtr, __VA_ARGS__> \
{ \
public: \
/** Default constructor */ \
DynamicMulticastDelegateName() \
{ \
} \
\
/** Construction from an FMulticastScriptDelegate must be explicit. This is really only used by UObject system internals. */ \
explicit DynamicMulticastDelegateName( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) \
: TBaseDynamicMulticastDelegate<TWeakPtr, __VA_ARGS__>( InMulticastScriptDelegate ) \
{ \
} \
\
/** Broadcasts this delegate to all bound objects, except to those that may have expired */ \
void Broadcast( FuncParamList ) const \
{ \
ExecFunction( FuncParamPassThru ); \
} \
};

FUNC_CONCT宏只是简单的拼接:

1
2
/** Helper macro that enables passing comma-separated arguments as a single macro parameter */
#define FUNC_CONCAT( ... ) __VA_ARGS__

展开所有宏之后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct _Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms
{
FString InStr;
};
static inline void FOnTestDynamicMultiDelegate_DelegateWrapper(const FMulticastScriptDelegate& OnTestDynamicMultiDelegate, const FString& InStr)
{
_Script_HotPatcherEditor_eventOnTestDynamicMultiDelegate_Parms Parms;
Parms.InStr=InStr;
OnTestDynamicMultiDelegate.ProcessMulticastDelegate<UObject>(&Parms);
}

class FOnTestDynamicMultiDelegate : public TBaseDynamicMulticastDelegate<FWeakObjectPtr, void, const FString&>
{
public:
FOnTestDynamicMultiDelegate() { }
explicit FOnTestDynamicMultiDelegate( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) : TBaseDynamicMulticastDelegate<FWeakObjectPtr, void, const FString&>( InMulticastScriptDelegate ) { }
void Broadcast( const FString& InStr ) const