UE4和VR开发技术笔记

平时随笔写下的一些UE4和VR开发中的技术笔记,以及一些相关资料的收录,之前零零散散放在imzlp.me/notes中,今天整理了一下,后续的笔记都会放到这篇文章中。

Engine Documents

UE4 Online Documents

UE GamePlay Framework Tutorial

使用Doxygen从UE代码生成文档

最近使用doxygen从UnrealEngine源码生成了Runtime/Engine模块文档,导出的chm都700M了(主要是各种grahp大),好大。
注意,使用doxygen从UE源码生成文档之前,记得删除源码中所有的.h/.hpp中的UE的宏(我自己写了个简单的程序来处理这个事情),不然生成出来的会有问题(比如函数名不见了,显示出来的时UFUNCTION,或者成员变量不见了,显示出来的是UPROPERTY)。主要删除以下这几个MACRO就可以。

1
2
3
4
5
6
7
8
"UFUNCTION",
"DEPRECATED",
"UCLASS",
"UINTERFACE",
"UPROPERTY",
"GENERATED_BODY()",
"GENERATED_UCLASS_BODY()",
"GENERATED_INTERFACE_BODY()"

使用sed就是如下的命令:

1
2
3
4
# 删除源文件中的宏并保存
$ sed -i -e /UFUNCTION/d -e /DEPRECATED/d -e /UCLASS/d -e /UINTERFACE/d -e /UPROPERTY/d -e /GENERATED_BODY/d -e /GENERATED_UCLASS_BODY/d -e /GENERATED_INTERFACE_BODY/d ${filename}
# Example Actor.h
$ sed -i -e /UFUNCTION/d -e /DEPRECATED/d -e /UCLASS/d -e /UINTERFACE/d -e /UPROPERTY/d -e /GENERATED_BODY/d -e /GENERATED_UCLASS_BODY/d -e /GENERATED_INTERFACE_BODY/d Actor.h

VR Devices

UE适配不同VR设备的高度问题

在UE适配Oculus Rift和HTC Vive时注意把TrackingOrigin(EHMDTrackingOrigin::Type)改为Floor,不然默认的追踪起源是在眼睛位置,导致头盔位置在游戏场景中高度不对。
UE-Adaptive-OculusRift-And-Vive-Stracking-Origin-Setting

还有一个问题是,使用PlayerStart放置时,PlayerStart是有高度的,所以高度可能是不对的(取决于你的Pawn是怎么写的),解决的办法是可以重写AGameModeBaseSpawnDefaultPawnAtTransform,来指定高度。

UE中游戏启动VR头显初始化

1
2
Stereo On/Off
Notice: Enables or Disables stereo rendering for Head Mounted Display (HMD) devices.

重置VR头显旋转和位置

因为VR头显默认的朝向X轴是与房间设置的定位有关的,所以,当我们初始就与定位时的朝向偏离时,进入游戏就会偏离我们设定让玩家看到的东西,所以需要进行修正。
可以使用以下函数:

1
2
3
4
5
6
7
8
9
/**
* Resets orientation by setting roll and pitch to 0, assuming that current yaw is forward direction and assuming
* current position as a 'zero-point' (for positional tracking).
*
* @param Yaw (in) the desired yaw to be set after orientation reset.
* @param Options (in) specifies either position, orientation or both should be reset.
*/
UFUNCTION(BlueprintCallable, Category="Input|HeadMountedDisplay")
static void ResetOrientationAndPosition(float Yaw = 0.f, EOrientPositionSelector::Type Options = EOrientPositionSelector::OrientationAndPosition);

蓝图中也是同名的节点。

在UE中使用Oculus Rift

在Oculus Rift设备安装完成之后需要在Oculus商店中启用Unknow Source,不然无法在UE中使用Oculus Rift预览。

Unreal的文档中适配Oculus Rift的页面:Developing for Oculus Rift.

Oculus官方提供的按键操作介绍:
Oculus-touch-controller-map

以及Oculus Rift在UE中的按键映射:
oculus-rift-key-mapping-on-ue4

HTC Vive的按键映射

与上面的Oculus Rift作为对比,附一张HTC Vive的按键映射:
htc-vive-key-mapping-on-ue4

HTC VIVE设备设置



使用Proxifier让Oculus走SS代理

昨天公司买了台Oculus Rift设备,在安装设备时需要全局代理,在Windows下我使用的是Proxifier来让Oculus相关软件走代理。
首先先添加Proxifier的代理:
proxifier-add-proxy-serves
测试连接成功之后即可执行下列操作。

上面连接成功后可以添加一个代理规则,而Oculus程序相关的进程如下,我们需要做的是让下列的进程走SS的代理:

1
OculusSetup.exe;OculusClient.exe;OVRServiceLauncher.exe;OVRServer_x64.exe;OculusVR.exe;OculusCompatCheck.exe;CompatToolCommandLine.exe;OculusLogGatherer.exe;OVRLibrarian.exe;oculus-driver.exe;OVRLibraryService.exe;oculus-overlays.exe;OVRRedistributableInstaller.exe;

将上面的内容填入应用程序文本框内之后,选择走SS的本地代理端口就可以了。
proxifier-ocukus-proxy-setting

其他的程序(如外服游戏、Steam等),如果需要强制走SS代理也是同样的方法。

VR画面模糊的问题

因为UE项目中默认的Screen Percentage值是很低的,所以会感觉很模糊,但直接调高之后会有严重的帧率下降问题。
一般设置为200以内画面质量就不错了,下面列有UE的建议值(理想值)。然后需要美术在此基础上对场景进行优化,保证接近满帧的帧率。

在4.19之前(不含4.19),VR HMD建议的理想值(r.ScreenPercentage)为:

DeviceTyper.ScreenPercentage
Oculus Rift133
HTC Vive140
PSVR140
GearVR120
GoogleVR120

在4.19之后UE增加了vr.PixelDensity,在r.ScreenPercentage保持为100时,就可以在不同平台的设备上使用标准化的值:

DeviceTyper.ScreenPercentagevr.PixelDensity
Oculus Rift1001
HTC Vive1001
PSVR1001
GearVR1001
GoogleVR1001

Lower values will perform faster but will be undersampled (more blurry) while values over 1 will perform slower and will supersample (extra sharp).

这样就可以通过控制vr.PixelDensity这个标准化的值来控制显示质量。
具体介绍链接:Significant Changes Coming To VR Resolution Settings in 4.19

HTC VIVE定位器故障(03)的解决办法

如果定位器面板上闪红灯且SteamVR显示定位器故障(03),请试着手动烧录定位器(基站)固件。步骤如下:
固件路径在Steam的安装路径下:Steam\steamapps\common\SteamVR\tools\lighthouse\firmware\lighthouse_tx\archive\htc_2.0,找到以下两个文件:

1
2
* lighthouse_tx_htc_2_0-calibration-rescue-244.bin
* lighthouse_tx_htc_2_0-244-2016-03-12.bin
  1. 在定位器(基站)未通电情况下,将其通过micro-B USB传输线连接到电脑。
  2. 按住定位器(基站)背后的模式键并插入电源线.
  3. 一旦电脑端确认为USB大容量存储设备(USB mass storage device),才可以释放模式键。
  4. 被连接的定位器(基站)储存设备名为“CRP_DISABLED”,打开后包含一个文件“firmware.bin”,删除它。
  5. 将“lighthouse_tx_htc_2_0-calibration-rescue-244.bin”文件复制到基站的储存空间上。
  6. 复制完成后,拔掉电源线。
  7. 等几秒,然后再次插上电源。在此过程中不要按模式键。几秒后定位器(基站)应该会快速的闪烁绿灯或者红灯。绿灯表示修复成功。
  8. 如果它闪烁红灯,这表示不能自动修复,请您送修。
  9. 再次拔下电源。
  10. 重复步骤1到7,但第5步复制文件改为“lighthouse_tx_htc_2_0-244-2016-03-12.bin"。
  11. 完整后定位器(基站)就恢复正常了,讲其频道设置为“A”并单独跟踪(另一个基站不通电)来确认运行情况。一旦确认工作正常,再打开另一个基站。

如果出现闪烁绿灯无法正常使用,重复步骤1到7,但在第五步清除"CRP_DISABLED"中所有文件,只复制 "lighthouse_tx_htc_2_0-244-2016-03-12.bin" 即可。

  • 如果手动烧录定位器(基站)固件和校准文还是无效,请使用手机拍照确定下列两个雷射点是否正常(不能用iphone/ipad雷射点拍不出來),如果这两个雷射点任一个没有显示,代表雷射损坏,请送客服检查。


参考链接:定位器故障(03)

Engine Analysis

UE Game的启动

游戏的初始化是从UGameInstance::StartGameInstance开始的。
通过FEngineLoop::Init()中调用GEngine->Start()多态调用到UGameEngine::Start()中调用GameInstance->StartGameInstance();

UE中Actor初始化

AActor的PreInitializeComponents/InitializeComponents/PostInitializeComponents以及DispatchBeginPlay均在ULevel::RouteActorInitialize中调用。

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
void ULevel::RouteActorInitialize()
{
// Send PreInitializeComponents and collect volumes.
for( int32 Index = 0; Index < Actors.Num(); ++Index )
{
AActor* const Actor = Actors[Index];
if( Actor && !Actor->IsActorInitialized() )
{
Actor->PreInitializeComponents();
}
}
const bool bCallBeginPlay = OwningWorld->HasBegunPlay();
TArray<AActor *> ActorsToBeginPlay;
// Send InitializeComponents on components and PostInitializeComponents.
for( int32 Index = 0; Index < Actors.Num(); ++Index )
{
AActor* const Actor = Actors[Index];
if( Actor )
{
if( !Actor->IsActorInitialized() )
{
// Call Initialize on Components.
Actor->InitializeComponents();
Actor->PostInitializeComponents(); // should set Actor->bActorInitialized = true
if (!Actor->IsActorInitialized() && !Actor->IsPendingKill())
{
UE_LOG(LogActor, Fatal, TEXT("%s failed to route PostInitializeComponents. Please call Super::PostInitializeComponents() in your <className>::PostInitializeComponents() function. "), *Actor->GetFullName() );
}
if (bCallBeginPlay && !Actor->IsChildActor())
{
ActorsToBeginPlay.Add(Actor);
}
}
// Components are all set up, init touching state.
// Note: Not doing notifies here since loading or streaming in isn't actually conceptually beginning a touch.
// Rather, it was always touching and the mechanics of loading is just an implementation detail.
Actor->UpdateOverlaps(Actor->bGenerateOverlapEventsDuringLevelStreaming);
}
}
// Do this in a second pass to make sure they're all initialized before begin play starts
for (int32 ActorIndex = 0; ActorIndex < ActorsToBeginPlay.Num(); ActorIndex++)
{
AActor* Actor = ActorsToBeginPlay[ActorIndex];
SCOPE_CYCLE_COUNTER(STAT_ActorBeginPlay);
Actor->DispatchBeginPlay();
}
}

UE引擎的Tick调用栈(call stack)

Game Flow on UE4

Game Flow Overview on UE4

Actor LifeCycle

Actor Lifecycle on Unreal Engine

UBT代码分析:检测是否为安装版引擎的方法

在EpicGameLauncher安装的引擎版本是不能通过UnrealBuildTool.exe -ProjectFiles "ProgramProjectName"来创建TargetProgram的项目的,会提示下列错误:

1
ERROR: UnrealBuildTool Exception: A game project path was not specified, which is required when generating project files using an installed build or passing -game on the command line

这个异常在UBT里的相关代码在ProjectFileGenreator.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
// ProjectFileGenreator.cs
protected virtual void ConfigureProjectFileGeneration( String[] Arguments, ref bool IncludeAllPlatforms )
{
// ...
else switch( CurArgument.ToUpperInvariant() )
{
// ...
case "-GAME":
// Generates project files for a single game
bGeneratingGameProjectFiles = true;
break;
// ...
}
// ...
if( bGeneratingGameProjectFiles || UnrealBuildTool.IsEngineInstalled() )
{
if (OnlyGameProject == null)
{
throw new BuildException("A game project path was not specified, which is required when generating project files using an installed build or passing -game on the command line");
}
GameProjectName = OnlyGameProject.GetFileNameWithoutExtension();
if (String.IsNullOrEmpty(GameProjectName))
{
throw new BuildException("A valid game project was not found in the specified location (" + OnlyGameProject.Directory.FullName + ")");
}
bool bInstalledEngineWithSource = UnrealBuildTool.IsEngineInstalled() && DirectoryReference.Exists(UnrealBuildTool.EngineSourceDirectory);
bIncludeEngineSource = bAlwaysIncludeEngineModules || bInstalledEngineWithSource;
bIncludeDocumentation = false;
bIncludeBuildSystemFiles = false;
bIncludeShaderSource = true;
bIncludeTemplateFiles = false;
bIncludeConfigFiles = true;
IncludeEnginePrograms = bAlwaysIncludeEngineModules;
}
}

可以看到,if这里如果传递给UBT-game参数并且是引擎是安装版本(EpicGameLauncher)时会检测有没有传入project参数,如果没有就会抛异常。
正常的UBT调用命令:

1
UnrelBuildTool.exe -ProjectFiles -project="D:\UnrealProjects\UEProject.uproject" -game

这种普通的Game参数在UBT里面是不会抛异常的。但是用生成Program的命令来调用UBT,安装版引擎就会抛异常:

1
UnrelBuildTool.exe -ProjectFiles ProgramName

根据上面ProjectFileGenreator.cs的检测代码,我们要做到的是把UnrealBuildTool.IsEngineInstalled()获取的结果变为false.
继续跟代码:UnrealBuildTool.IsEngineInstalled()获得的是UnrealBuildTool中的一个bool变量bIsEngineInstalled

1
2
3
4
5
6
7
8
9
// UnrealBuildTool.cs
static public bool IsEngineInstalled()
{
if (!bIsEngineInstalled.HasValue)
{
throw new BuildException("IsEngineInstalled() called before being initialized.");
}
return bIsEngineInstalled.Value;
}

查找引用发现在GuardedMain里有设置bIsEngineInstalled这个变量的地方:

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
// UnrealBuildTool.cs
private static int GuardedMain(string[] Arguments)
{
// ...
try{
// ...
foreach (string Argument in Arguments)
{
string LowercaseArg = Argument.ToLowerInvariant();
if (LowercaseArg == "-installed" || LowercaseArg == "-installedengine")
{
bIsEngineInstalled = true;
}
else if (LowercaseArg == "-notinstalledengine")
{
bIsEngineInstalled = false;
}
}
if (!bIsEngineInstalled.HasValue)
{
bIsEngineInstalled = FileReference.Exists(FileReference.Combine(RootDirectory, "Engine", "Build", "InstalledBuild.txt"));
}
// ...
}
}

从代码可以看到,UBT对是不是安装版引擎的检测有三种方法:

  1. 传入参数是否有-installled-installedengine
  2. 传入参数是否有-notinstallengine
  3. 判断引擎路径Engine\Build下是否具有InstalledBuild.txt文件

(说是三种其实也就是两种,就算指定了-notinstallengine也是要判断存不存在Engine\Build\InstalledBuild.txt这个文件)
而且检测的顺序是这样的:

1
-installled > -installedengine > -notinstalledengine > Engine\Build\InstalledBuild.txt

所以,当我想要让UBT认为我的引擎版本不是安装版,有两种办法:

  1. 对UBT的调用传入-notinstalledengine参数,并且删掉Engine\Build目录下的InstalledBuild.txt文件
  2. 对UBT的调用不传入-installled-installedengine参数,并且删掉Engine\Build目录下的InstalledBuild.txt文件

:UBT里还有下面两种检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UnrealBuildTools.cs
static public bool IsEnterpriseInstalled()
{
if(!bIsEnterpriseInstalled.HasValue)
{
bIsEnterpriseInstalled = FileReference.Exists(FileReference.Combine(EnterpriseDirectory, "Build", "InstalledBuild.txt"));
}
return bIsEnterpriseInstalled.Value;
}
static public bool IsProjectInstalled()
{
if (!bIsProjectInstalled.HasValue)
{
bIsProjectInstalled = FileReference.Exists(FileReference.Combine(RootDirectory, "Engine", "Build", "InstalledProjectBuild.txt"));
}
return bIsProjectInstalled.Value;
}

UE注册编译版引擎的安装路径

UE提供了UnrealVersionSelector工具用来选择引擎版本/生成sln等功能,其也可以用来注册本地的引擎(非Launcher安装的引擎,如自己编译的源码版引擎)到项目文件的右键Select Unreal Engine Version...列表中。
简单分析了一下UnrealVersionSelector的代码,它的注册流程如下:


向注册表写入的操作在FDesktopPlatformWindows::RegisterEngineInstallation这个函数中。
操作为将注册的引擎目录写入到HKEY_CURRENT_USER\Software\Epic Games\Unreal Engine\Builds中的字符串项中。
当右键Switch Unreal Engine Version...的时候,会修改.uproject文件中的EngineAssociation值:

其调用流如下:


执行项目文件写入的操作在FDesktopPlatformBase::SetEngineIdentifierForProject中:

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
bool FDesktopPlatformBase::SetEngineIdentifierForProject(const FString &ProjectFileName, const FString &InIdentifier)
{
// Load the project file
TSharedPtr<FJsonObject> ProjectFile = LoadProjectFile(ProjectFileName);
if (!ProjectFile.IsValid())
{
return false;
}
// Check if the project is a non-foreign project of the given engine installation. If so, blank the identifier
// string to allow portability between source control databases. GetEngineIdentifierForProject will translate
// the association back into a local identifier on other machines or syncs.
FString Identifier = InIdentifier;
if(Identifier.Len() > 0)
{
FString RootDir;
if(GetEngineRootDirFromIdentifier(Identifier, RootDir))
{
const FUProjectDictionary &Dictionary = GetCachedProjectDictionary(RootDir);
if(!Dictionary.IsForeignProject(ProjectFileName))
{
Identifier.Empty();
}
}
}
// Set the association on the project and save it
ProjectFile->SetStringField(TEXT("EngineAssociation"), Identifier);
return SaveProjectFile(ProjectFileName, ProjectFile);
}

所以如果你选择了一个项目使用编译版引擎,可以使用文本编辑器打开该项目的.uproject文件,可以看到其中的EngineAssociation项的值就是注册表中的引擎版本值。

GamePlay

UE宏库的问题

在UE蓝图中使用宏库,在编辑了宏库里面的宏之后,只保存宏库是没用的,必须在用到宏库的蓝图里编译一遍才会生效,相当于蓝图里面的编译才会直接展开宏,否则还是原来未修改之前的版本,这问题实在太操蛋了。
在UE4.18.3问题存在,其他版本没试。

UE使用Shipping模式打包的Saved目录

UE项目在工程和DevelementDebugGame打包出来的Saved目录均包含在项目目录打包到的目标目录下,其中包含Autosaves/Backup/Config/Logs/Crashs/SaveGames等等。
但是使用Shipping打包的不同,使用Shipping打包出来的游戏,Saved目录并不在打包到的目标目录下,而是在C:/User/%username%/AppData/Local/ProjectName下,ProjectName替换为你的项目名。

更多关于UE打包的内容可以看UE的文档:

CharacterMovement开启RVO避障

CharacterMovementComponent具有自动避障的功能,只需要启用UseRVOAvoidance
但是记得在角色死亡之后关闭RVO的壁障,不然会导致怪物在空地上避障的问题。
可以使用SetAvoidanceEnable来关闭。

VR弓的模型标准

VR中需要用的弓为SkeleMesh,模型的root骨骼点要位于抓取的位置(最好是弓箭握持中心点),而且为了程序计算方便模型方向要求下图:

这是为了方便计算VR玩家抓取弓到手上时的旋转计算:

拉弓时抓弓的手提供Roll的旋转,Pitch与Yaw则由抓弓的手LookAt到拉弓的手所需要的旋转提供。

所有的弓使用相同的模型旋转标准,这点美术上很好实现,如果不同程序调整上很麻烦。

而且箭的模型原点要在箭羽尾部:

做一把体验好的弓是个挺麻烦的事,重要的就是细节优化。

UE换装的一个实现

在UE的USkinnedMeshComponent中有这样一个函数:

1
2
3
4
5
6
7
8
9
10
11
class ENGINE_API USkinnedMeshComponent : public UMeshComponent
{
public:
/**
* Set MasterPoseComponent for this component
*
* @param NewMasterBoneComponent New MasterPoseComponent
*/
UFUNCTION(BlueprintCallable, Category="Components|SkinnedMesh")
void SetMasterPoseComponent(USkinnedMeshComponent* NewMasterBoneComponent);
};

大概意思就是让当前的SkinnedMeshComponent随着NewMasterBoneComponent的姿态运动。
我们可以使用一组使用相同骨骼的模型来实现换装的功能,比如说头部、胸部、腿、脚、双手,让一个角色具有这五个单独的USkinnedMeshComponent模型组件来表示不同的部位。
让角色有一个带有动画的USkinnedMeshComponent当作身体,然后将上面的五个USkinnedMeshComponentMasterPose设置为它。
此时各个部位都会随着身体的动画运动的,实现换装就直接修改各个部位的模型就可以了。

从蓝图编译到C++代码

UE支持从蓝图编译到C++代码,需要在Project Setting-Packaging-Blueprints里选择Exclusive(只有选择的蓝图资源会生成C++)或者Inclusive(为所有的蓝图资源生成C++)。
选择之后打包项目,打包成功后可以在项目的Intermediate\Plugins\NativizedAssets\Windows\Game\Source\NativizedAssets路径下的public/private目录里找到以蓝图名字开头的.cpp.h文件。

UE蓝图函数库无法在UObject对象中调用

在编辑器中创建一个蓝图函数库(Blueprint Function Library)并创建一个函数,该函数是不能在蓝图的Object中调用的,会提示:

1
Function Requires A World Context.

写一个测试的蓝图函数库生成C++代码:

生成的C++代码如下(移除多余的部分,全部的代码可以在这里查看):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TestFuncLib_pf1448227310.h
UFUNCTION(BlueprintCallable, BlueprintPure, meta=(WorldContext="__WorldContext", Category, OverrideNativeName="GetComponents"))
static TArray<UActorComponent*> bpf__GetComponents__pf(AActor* bpp__pActor__pf, UClass* bpp__pCompClass__pf, UObject* bpp____WorldContext__pf);
// TestFuncLib_pf1448227310.cpp
TArray<UActorComponent*> UTestFuncLib_C__pf1448227310::bpf__GetComponents__pf(AActor* bpp__pActor__pf, UClass* bpp__pCompClass__pf, UObject* bpp____WorldContext__pf)
{
TArray<UActorComponent*> bpp__ReturnValue__pf{};
TArray<UActorComponent*> bpfv__CallFunc_GetComponentsByClass_ReturnValue__pf{};
FString bpfv__CallFunc_GetDisplayName_ReturnValue__pf{};
bpfv__CallFunc_GetDisplayName_ReturnValue__pf = UKismetSystemLibrary::GetDisplayName(bpp____WorldContext__pf);
UKismetSystemLibrary::PrintString(bpp____WorldContext__pf, bpfv__CallFunc_GetDisplayName_ReturnValue__pf, true, true, FLinearColor(0.000000,0.660000,1.000000,1.000000), 2.000000);
if(IsValid(bpp__pActor__pf))
{
bpfv__CallFunc_GetComponentsByClass_ReturnValue__pf = bpp__pActor__pf->AActor::GetComponentsByClass(bpp__pCompClass__pf);
}
bpp__ReturnValue__pf = bpfv__CallFunc_GetComponentsByClass_ReturnValue__pf;
return bpp__ReturnValue__pf;
}

直接把上面的代码抄到一个C++的蓝图函数库里是可以在UObject的对象中调用的...这就有毒了。

UE中放置组件到骨骼插槽(socket)

在蓝图中直接可以在蓝图编辑器中设置parent socket属性来将一个组件放置到骨骼插槽中。
在C++中也一样,不过略微麻烦一点,来看一下ShooterGame中的代码示例:

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
// ShooterGame\Private\Weapons\ShooterWeapon.cpp
void AShooterWeapon::AttachMeshToPawn()
{
if (MyPawn)
{
// Remove and hide both first and third person meshes
DetachMeshFromPawn();
// For locally controller players we attach both weapons and let the bOnlyOwnerSee, bOwnerNoSee flags deal with visibility.
FName AttachPoint = MyPawn->GetWeaponAttachPoint();
if( MyPawn->IsLocallyControlled() == true )
{
USkeletalMeshComponent* PawnMesh1p = MyPawn->GetSpecifcPawnMesh(true);
USkeletalMeshComponent* PawnMesh3p = MyPawn->GetSpecifcPawnMesh(false);
Mesh1P->SetHiddenInGame( false );
Mesh3P->SetHiddenInGame( false );
Mesh1P->AttachToComponent(PawnMesh1p, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint);
Mesh3P->AttachToComponent(PawnMesh3p, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint);
}
else
{
USkeletalMeshComponent* UseWeaponMesh = GetWeaponMesh();
USkeletalMeshComponent* UsePawnMesh = MyPawn->GetPawnMesh();
UseWeaponMesh->AttachToComponent(UsePawnMesh, FAttachmentTransformRules::KeepRelativeTransform, AttachPoint);
UseWeaponMesh->SetHiddenInGame( false );
}
}
}

同样是用的也是AttachToComponent,蓝图中也可以用相同名字的蓝图节点来attach到骨骼插槽上。

Unreal中获取OpenLevel的Options

在UE中加载关卡时,可以传一个FString的Options过去:

在被加载的关卡中可以通过GetGameMode来获取:

使用C++的代码是这样:

1
2
AGameModeBase* OurGameMode=UGameplayStatics::GetGameMode(this);
FString LevelOptionString=OurGameMode->OptionsString;

注意:在切换关卡后,原关卡中创建出来的对象都会被销毁,解决的办法是使用GameMode的GetSeamlessTravelActorList来留存指定的Actor在切换关卡时不会被销毁。

转换游戏场景中的3D位置到屏幕位置

设置鼠标的光标在viewport内的相对位置

首先安装LowEntry-ExtendedStandardLibrary这个插件,可以使用其中的ULowEntryExtendedStandardLibrary::SetMousePosition函数来设置鼠标的光标在viewport内的相对位置,viewport的大小可以通过UWidgetLayoutLibrary::GetViewportSize来获取。

UE启动游戏后鼠标焦点不在窗口内

可以使用SetInputModeGameAndUI来设置鼠标的焦点在窗口内,InMourseLockMode设置为LockAlways即可。
在启动游戏时使用这个操作就可以在开始游戏时鼠标的焦点就在游戏窗口内,从而避免想要点击窗口上的UI需要先点击一遍游戏窗口。

解决.uproject右键生成失败

首先要查看右键菜单所使用的UnrealVersionSelector.exe的路径,可以打开注册表:

1
HKEY_CLASSES_ROOT\Unreal.ProjectFile

然后找到shell\open\command条目查看UnrealVersionSelector.exe的位置(如果没有安装过源码版引擎则默认在EpicGamesLauncher的Binaries路径下),如:

1
"C:\Program Files (x86)\Epic Games\Launcher\Engine\Binaries\Win64\UnrealVersionSelector.exe" /editor "%1"

然后打开C:\Program Files (x86)\Epic Games\Launcher\Engine\Binaries\Win64,将UnrealVersionSelector.exe设置为以管理员权限启动即可(右键-属性-兼容性-以管理员身份运行此程序)。

判断一个特效是否循环

从一个UParticleSystemEmitterRequiredEmitter Loops为0则是循环特效:

UE提供了方法可以判断一个特效是不是循环特效:

1
bool UParticleSystem::IsLooping()const{ /*...*/ }

只要该特效里有一个循环的Emitter,则该特效就是循环的。
但是该方法没有暴露给蓝图,可以写个函数库封装给蓝图。

跨模块使用类注意是否导出符号

在继承/访问UE引擎内的类时,一定要注意类是否具有导出符号,没有导出符号的类是没办法链接到定义的。
如UEEngine模块下的FParticleSpriteEmitterInstance这个类就是没有导出符号的。

Blueprint创建异步节点

在工程中有一些异步操作的需求,比如下载文件的行为,一个下载命令等待下载完成之后回调,而且要尽量避免太多的事件绑定和解绑操作。
在蓝图的节点中有一些异步的操作,比如PlayMontage/DownloadImage等,都是异步操作中有多个输出节点的:

我们能不能自己写一个这样的异步操作的节点呢?那必然是可以的。
可以在Engine\Source\Runtime\UMG\Public\Blueprint\AsyncTaskDownloadImage.h中查看DownloadImage节点的实现(C++API)。
可以自己仿照这自己写一个出来:

  1. 关键就是要继承UBlueprintAsyncActionBase,这个是必须的。
  2. 然后写一个static的函数,返回类的指针,并用UFUNCTIONmate标记为BlueprintInternalUseOnly="true"
  3. 声明并定义几个派发器成员,这些派发器成员就是异步的节点,也就是蓝图节点右侧的exec节点。

我们先来看一下DownloadImage的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class UTexture2DDynamic;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDownloadImageDelegate, UTexture2DDynamic*, Texture);
UCLASS()
class UMG_API UAsyncTaskDownloadImage : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()
public:
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" ))
static UAsyncTaskDownloadImage* DownloadImage(FString URL);
public:
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnSuccess;
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnFail;
public:
void Start(FString URL);
private:
/** Handles image requests coming from the web */
void HandleImageRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded);
};

对比上面的DownloadImage的节点,可以看到:

  1. 首先声明了一个动态多播代理FDownloadImageDelegate,并且需要传入一个参数
  2. UAsyncTaskDownloadImage类中声明了两个事件派发器OnSuccessOnFail,这也是蓝图节点右侧的Exec和参数Texture,本质都是派发器(动态多播代理)
  3. UAsyncTaskDownloadImagestatic函数DownloadImage接收一个FString的参数,返回一个UAsyncTaskDownloadImage*,这个返回就是把派发器的执行节点在蓝图中显示出来

即:声明的动态多播的成员和该多播的参数都会显示在蓝图节点的右侧。

我自己实现了一个异步行为的操作,先在蓝图中看操作:

行为就是,先创建一个AsyncActionObject的对象作为后期触发异步操作的对象,然后执行CreateAsyncTask里面对上一步创建的AsyncActionObject进行事件绑定。
然后我们就可以在那个ActionObj调用OnActionStart之类的操作就可以调用CreateAsyncTask右侧的相关节点。
注意:我做的限制是,一个对象对应一个Task,如果当前传入的AsyncActionObject正在被其他的task绑定,则创建task会失败。
然后就是代码:

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
// AsyncTask.h
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AsyncActionObject.h"
// unreal header
#include "Array.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AsyncTask.generated.h"
#ifndef PRINT_LOG
#define PRINT_LOG(lOG_TEXT) UKismetSystemLibrary::PrintString(this,lOG_TEXT,true,true);
#endif
#ifndef PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
#define PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO 0
#endif
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAsyncTaskDelegate,UAsyncActionObject*,ActionObj);
UCLASS(BlueprintType)
class UAsyncTask : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()
public:
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" ))
static UAsyncTask* CreateAsyncTask(UAsyncActionObject* ActionObj);
public:
UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnStart;
UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnAbort;
UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnUpdate;
UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnFinishd;
protected:
void StartTask(UAsyncActionObject* ActionObj);
virtual void OnActionStart(UAsyncActionObject* ActionObj);
virtual void OnActionAbort(UAsyncActionObject* ActionObj);
virtual void OnActionUpdate(UAsyncActionObject* ActionObj);
virtual void OnActionFinishd(UAsyncActionObject* ActionObj);
};

然后是实现:

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
// AsyncTask.cpp
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#include "AsyncTask.h"
#include "Modules/ModuleManager.h"
//----------------------------------------------------------------------//
// UAsyncTask
//----------------------------------------------------------------------//
UAsyncTask::UAsyncTask(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
if ( HasAnyFlags(RF_ClassDefaultObject) == false )
{
AddToRoot();
}
}
UAsyncTask* UAsyncTask::CreateAsyncTask(UAsyncActionObject* ActionObj)
{
UAsyncTask* AsyncActionTask = NewObject<UAsyncTask>();
AsyncActionTask->StartTask(ActionObj);
return AsyncActionTask;
}
void UAsyncTask::StartTask(UAsyncActionObject* ActionObj)
{
if(ActionObj && !ActionObj->Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::StartTask Bind Event"));
#endif
(ActionObj->OnStart).BindUObject(this,&UAsyncTask::OnActionStart);
(ActionObj->OnAbort).BindUObject(this,&UAsyncTask::OnActionAbort);
(ActionObj->OnUpdate).BindUObject(this,&UAsyncTask::OnActionUpdate);
(ActionObj->OnFinishd).BindUObject(this,&UAsyncTask::OnActionFinishd);
}
}
void UAsyncTask::OnActionStart(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnStart"));
#endif
OnStart.Broadcast(ActionObj);
}
void UAsyncTask::OnActionAbort(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionAbort"));
#endif
OnAbort.Broadcast(ActionObj);
}
void UAsyncTask::OnActionUpdate(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionUpdate"));
#endif
OnUpdate.Broadcast(ActionObj);
}
void UAsyncTask::OnActionFinishd(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionFinishd"));
#endif
OnFinishd.Broadcast(ActionObj);
}

AsyncActionObject类的声明:

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
// AsyncActionObject.h
// Fill out your copyright notice in the Description page of Project Settings.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
// unreal header
#include "Array.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "AsyncActionObject.generated.h"
#ifndef PRINT_LOG
#define PRINT_LOG(lOG_TEXT) UKismetSystemLibrary::PrintString(this,lOG_TEXT,true,true);
#endif
#ifndef PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
#define PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO 0
#endif
class UAsyncActionObject;
DECLARE_DELEGATE_OneParam(FAsyncActionDelegate,UAsyncActionObject*);
UCLASS(BlueprintType,Blueprintable)
class UAsyncActionObject : public UObject
{
GENERATED_BODY()
public:
UAsyncActionObject(const FObjectInitializer& objectInitializer);
FAsyncActionDelegate OnStart;
FAsyncActionDelegate OnAbort;
FAsyncActionDelegate OnUpdate;
FAsyncActionDelegate OnFinishd;
public:
UFUNCTION(BlueprintCallable)
virtual bool OnActionStart(FString& rReason);
UFUNCTION(BlueprintCallable)
virtual void OnActionAbort();
UFUNCTION(BlueprintCallable)
virtual void OnActionUpdate();
UFUNCTION(BlueprintCallable)
virtual void OnActionFinishd();
public:
// Action status (BP Executable function)
UFUNCTION(BlueprintCallable)
virtual bool Action_IsRunning()const;
UFUNCTION(BlueprintCallable)
virtual bool Action_ExecutableStart()const;
protected:
// End Action
virtual void EndAction();
virtual void UnBindAll();
virtual void InitDelegateList();
virtual bool Action_IsStarted()const;
virtual bool Action_EventIsBinded()const;
protected:
bool mActionStarted=false;
TArray<FAsyncActionDelegate*> DelegateList;
};

以及它的实现:

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// AsyncActionObject.cpp
// Fill out your copyright notice in the Description page of Project Settings.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// Fill out your copyright notice in the Description page of Project Settings.
#include "AsyncActionObject.h"
UAsyncActionObject::UAsyncActionObject(const FObjectInitializer& objectInitializer)
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::UAsyncActionObject"));
#endif
InitDelegateList();
}
bool UAsyncActionObject::OnActionStart(FString& rReason)
{
rReason.Reset();
bool local_bResault=false;
if(Action_ExecutableStart())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionStart"));
#endif
mActionStarted=true;
OnStart.ExecuteIfBound(this);
local_bResault=true;
}else{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("call UAsyncActionObject::OnActionStart Faild."));
#endif
local_bResault=false;
if(Action_IsStarted())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("StartFaild: Action is Started."));
#endif
rReason.Append(FString(TEXT("Action is Started.\n")));
}
if(!Action_EventIsBinded())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("StartFaild: Action is not bind anything event."));
#endif
rReason.Append(FString(TEXT("Action is not bind to anything Task")));
}
}
return local_bResault;
}
void UAsyncActionObject::OnActionAbort()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionAbort"));
#endif
OnAbort.ExecuteIfBound(this);
EndAction();
}
}
void UAsyncActionObject::OnActionUpdate()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionUpdate"));
#endif
OnUpdate.ExecuteIfBound(this);
}
}
void UAsyncActionObject::OnActionFinishd()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionFinishd"));
#endif
OnFinishd.ExecuteIfBound(this);
EndAction();
}
}
void UAsyncActionObject::EndAction()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::EndAction"));
#endif
UnBindAll();
mActionStarted=false;
}
}
void UAsyncActionObject::UnBindAll()
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::UnBindAll"));
#endif
for(auto& DeleIndex:DelegateList)
{
if(DeleIndex->IsBound())
{
DeleIndex->Unbind();
}
}
}
void UAsyncActionObject::InitDelegateList()
{
if(!Action_IsStarted())
{
DelegateList.AddUnique(&OnStart);
DelegateList.AddUnique(&OnAbort);
DelegateList.AddUnique(&OnUpdate);
DelegateList.AddUnique(&OnFinishd);
}
}
bool UAsyncActionObject::Action_EventIsBinded()const
{
bool EventIsBinded=true;
for(auto& DeleIndex:DelegateList)
{
if(!DeleIndex->IsBound())
EventIsBinded=false;
}
return EventIsBinded;
}
bool UAsyncActionObject::Action_IsStarted()const
{
return mActionStarted;
}
bool UAsyncActionObject::Action_IsRunning()const
{
return Action_IsStarted() && Action_EventIsBinded();
}
bool UAsyncActionObject::Action_ExecutableStart()const
{
return !Action_IsStarted() && Action_EventIsBinded();
}

其实就是本质上裹了两层派发器而已...
举个例子的用途:可以在行为树里监听某个动画被终止或者结束了之后然后再执行其他的行为,可以解决不同模块之间之间的耦合。

UE引用windows头文件的警告/错误

如果你在UE中使用了windows.h在VS可能会爆出4668这样的错误,在之前的UE版本中是警告,但是在UE4.18变成了错误,UE本身对windowsAPI的头文件做了封装,可以包含相应的:

1
2
3
#include <windows.h>
// UE中
#include "Windows/MinWindows.h"

或者不用这个办法,使用:

1
#pragma warning(disable:4668)

也是可以的.

而且,如果你在代码里用到了WindowsAPI,如windows.h等.
可能会遇到下面类似这样的错误:

1
2
3
4
1>C:\Program Files\Epic Games\UE_4.15\Engine\Source\Runtime\Core\Public\Async/TaskGraphInterfaces.h(892): error C2039: '__faststorefence': is not a member of 'FWindowsPlatformMisc'
1>c:\program files\epic games\ue_4.15\engine\source\runtime\core\public\Windows/WindowsPlatformMisc.h(33): note: see declaration of 'FWindowsPlatformMisc'
1>C:\Program Files\Epic Games\UE_4.15\Engine\Source\Runtime\Core\Public\Async/TaskGraphInterfaces.h(868): note: while compiling class template member function 'void TGraphTask<FFunctionGraphTask>::ExecuteTask(TArray<FBaseGraphTask *,FDefaultAllocator> &,ENamedThreads::Type)'
1>C:\Program Files\Epic Games\UE_4.15\Engine\Source\Runtime\Core\Public\Async/TaskGraphInterfaces.h(1379): note: see reference to class template instantiation 'TGraphTask<FFunctionGraphTask>' being compiled

解决这个问题的办法是把这些windowsAPI的头文件不要在UE的头文件中包含,移动到使用这些windowsAPI的.cpp中即可.

UE获取光标在窗口的相对比例

首先先要获取当前的窗口大小,可以通过UGameViewportClient获得SWindow,进而可以通过SWindow::GetSizeInScreen得到窗口的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void GetWindowSize(bool& Success, int32& Width, int32& Height)
{
Success = false;
Width = 0;
Height = 0;
if(GEngine == nullptr)
{
return;
}
UGameViewportClient* ViewportClient = GEngine->GameViewport;
if(ViewportClient == nullptr)
{
return;
}
TSharedPtr<SWindow> Window = ViewportClient->GetWindow();
if(!Window.IsValid())
{
return;
}
FVector2D Size = Window->GetSizeInScreen();
Success = true;
Width = Size.X;
Height = Size.Y;
}

然后得到光标在当前窗口的位置,可以通过APlayerController::GetMousePosition来得到(其实他也是通过Viveport来得到的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool APlayerController::GetMousePosition(float& LocationX, float& LocationY) const
{
bool bGotMousePosition = false;
ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(Player);
if (LocalPlayer && LocalPlayer->ViewportClient)
{
FVector2D MousePosition;
bGotMousePosition = LocalPlayer->ViewportClient->GetMousePosition(MousePosition);
if (bGotMousePosition)
{
LocationX = MousePosition.X;
LocationY = MousePosition.Y;
}
}
return bGotMousePosition;
}

然后两者相除即可得到鼠标在当前窗口的相对位置。

UE SceneCapture2D捕获的gamma值

在使用USceneCapture2D捕获画面到RenderTarget使用Spectator Screen再贴到屏幕上时发现颜色不对:

解决的办法是:

  1. 设置SceneCapture2D的CaptureSouceFinal Color (LDR) in RGB
  2. 设置RenderTarget资源的Gamma值为2.2就看起来和原图十分接近了

蓝图Event参数的一个骚操作

在一些插件中看到了类似这种的操作:

其实实际上,Unreal蓝图的事件节点(Event)的参数是类的成员变量(data member of class),所以可以在其他的地方直接使用他的参数。

类似的问题还有delay的操作:

UE使用UProjectileMovementComponent的碰撞检测问题

在UE中使用UProjectileMovementComponent弹射组件来做一些比如弓箭之类的功能,可能会因为速度太快的原因导致碰撞无法被触发。
记录一下这个问题留待后面研究。

Config and Editor

修改UE的编辑器模式下的Esc退出快捷键

打开Editor Preferences-Keybord Shortcuts修改Play World(PIE/SIE)的按键即可。

UE在编辑器中显示插件的Content

修改UE编译时的parallel数量

修改下列文件:

1
2
3
Engine/Saved/UnrealBuildTool/BuildConfiguration.xml
User Folder/AppData/Roaming/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
My Documents/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml

如下:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
<BuildConfiguration>
<MaxProcessorCount>1</MaxProcessorCount>
</BuildConfiguration>
</Configuration>

MaxProcessorCount即是修改本地执行的最大处理器数。
更多的confiuration参数可以看:UBT configuration

UE Android打包将数据存放在obb文件中

UE编辑器模式下焦点不在窗口内的卡顿

在Windows平台上运行UE时如果在编辑器内Play游戏,若当前的焦点不在编辑器/VR应用内,会减少对CPU的占用,从而出现卡顿的问题(打包出来不会出现这个问题)。
其实是因为UE在编辑器环境下做了不在焦点内的检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Runtime/ApplicationCore/Private/Windows/WindowsPlatformApplicationMisc.cpp
void FWindowsPlatformApplicationMisc::PumpMessages(bool bFromMainLoop)
{
if (!bFromMainLoop)
{
TGuardValue<bool> PumpMessageGuard( GPumpingMessagesOutsideOfMainLoop, true );
// Process pending windows messages, which is necessary to the rendering thread in some rare cases where D3D
// sends window messages (from IDXGISwapChain::Present) to the main thread owned viewport window.
WinPumpSentMessages();
return;
}
GPumpingMessagesOutsideOfMainLoop = false;
WinPumpMessages();
// Determine if application has focus
bool HasFocus = FApp::UseVRFocus() ? FApp::HasVRFocus() : FWindowsPlatformApplicationMisc::IsThisApplicationForeground();
// If editor thread doesn't have the focus, don't suck up too much CPU time.
if( GIsEditor )
{
static bool HadFocus=1;
if( HadFocus && !HasFocus )
{
// Drop our priority to speed up whatever is in the foreground.
SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL );
}
else if( HasFocus && !HadFocus )
{
// Boost our priority back to normal.
SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_NORMAL );
}
if( !HasFocus )
{
// Sleep for a bit to not eat up all CPU time.
FPlatformProcess::Sleep(0.005f);
}
HadFocus = HasFocus;
}

只需要把FPlatformProcess::Sleep(0.005f);这一行注释掉就可以了。

Win10下解决UE编辑器内编译日志的中文乱码

注意:可能会出现某些软件内的编码问题(如GBK编码出现烫烫烫/锟斤拷等)。

Debug

VS调试独立运行的UE项目

在UE的编辑器模式下以Standalone方式运行,在VS中是没办法及时Attach to process上的。
比如我们想要在standalone模式下调试引擎和项目,在editor下直接启动会创建一个新的进程,在VS中手动点Attach to process是很不方便的,也没有那么及时(因为点完启动在等我们在VS中attach到进程上有可能引擎都已经启动完毕了)。
幸好UE提供了一个插件:UnrealVS,该插件的VS安装程序在引擎的Engine\Extras\UnrealVS目录下,根据你的VS版本选择安装。
安装完之后启动VS,在View->Toolbars->UnrealVS启用,就会在VS的工具栏看到了。在这里选择你的项目:

后面的框是命令行框,填入的参数会在启动时传递给程序(具体介绍看UnrealVS里面的描述),所以我们可以在后面填参数使其以独立模式启动(可以从Command-Line Arguments查看支持的参数):

1
"$(SolutionDir)$(ProjectName).uproject" -game -windowed -log -verbose

这样就会在VS中使用F5启动项目时自动attach的到进程上的。

Package

ObservedKeyNames.Num()>0

解决详情具体看:UE Package Error:ObservedKeyNames.Num()>0

UE Package Error:RenderDocPlugin

RenderDocPlugin: Error: unable to initialize the plugin because no RenderDoc libray has been located.


解决办法:打包之前禁用掉RenderDocPlugin这个插件。

UE4打包32bit要求16byte对齐错误

在引擎中选择32bit平台打包的时候会出现下列错误:

1
error C2719: 'SpawnTransform': formal parameter with requested alignment of 16 won't be aligned

这是因为在代码里使用pass-by-value的方式传递了FTransform

1
AProps* SpawnProps(FTransform SpawnTransform);

因为FTransform要求使用16byte对齐:Math/TransformVectorized.h#L36

1
2
3
4
MS_ALIGN(16) struct FTransform
{
//...
}

MS_ALIGN这个宏是在Core/Public/Windows/WindowsPlatform.h#L131中定义的:

1
2
3
4
5
6
7
8
9
10
// Alignment.
#if defined(__clang__)
#define GCC_PACK(n) __attribute__((packed,aligned(n)))
#define GCC_ALIGN(n) __attribute__((aligned(n)))
#if defined(_MSC_VER)
#define MS_ALIGN(n) __declspec(align(n)) // With -fms-extensions, Clang will accept either alignment attribute
#endif
#else
#define MS_ALIGN(n) __declspec(align(n))
#endif

所以FTransform要求的是16byte对齐。

FTransform is implemented using vector intrinsics, and instances of it need to have 16-byte alignment. I think the stack is guaranteed to be aligned to 16-bytes on Win64 so the compiler can pass it on the stack correctly, but there's no such guarantee on Win32 (which is what we use for shipping builds).

那么改为传递指针或引用就可以了(传引用和传指针均是sizeof(void*)的大小):

1
AProps* SpawnProps(const FTransform& SpawnTransform);

相关问题:

UE打包时提示文件占用导致打包失败

  • 引擎版本:4.18.3
  • 打包平台:Windows
  • 运行模式:VR
  • BuildConfiguration: Shipping

打包中的错误信息log:

1
UATHelper: Packaging (Windows (64-bit)): Program.Main: ERROR: AutomationTool terminated with exception: System.IO.IOException: The process cannot access the file** ‘C:\YourProjectPath\Saved\StagedBuilds\WindowsNoEditor\Engine\Extras\Redist\en-us\UE4PrereqSetup_x64.exe’ **because it is being used by another process**.

UE打包时的错误提示为is being used by another process的原因是进程里也启动了EpicGameLauncher,打包之前把它关掉就可以了。

Online

UE编辑器内测试多人时禁用自动连接


使用UE联网的部分问题

UE里提供了AController::IsLocalControlled来判断当前控制器控制的是否是本地实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool AController::IsLocalController() const
{
const ENetMode NetMode = GetNetMode();
if (NetMode == NM_Standalone)
{
// Not networked.
return true;
}
if (NetMode == NM_Client && Role == ROLE_AutonomousProxy)
{
// Networked client in control.
return true;
}
if (GetRemoteRole() != ROLE_AutonomousProxy && Role == ROLE_Authority)
{
// Local authority in control.
return true;
}
return false;
}

注意:千万谨慎不要在Pawn里使用AutoPossessPlayer选项。这会使加入进来的玩家的Controller自动作为Player0,造成奇葩的bug.

还有UKismetSystemLibrary::IsServer/UKismetSystemLibrary::IsDedicatedServer,他们获取的都是当前WorldGetNetMode来判断的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool UKismetSystemLibrary::IsServer(UObject* WorldContextObject)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
return World ? (World->GetNetMode() != NM_Client) : false;
}
bool UKismetSystemLibrary::IsDedicatedServer(UObject* WorldContextObject)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (World)
{
return (World->GetNetMode() == NM_DedicatedServer);
}
return IsRunningDedicatedServer();
}
bool UKismetSystemLibrary::IsStandalone(UObject* WorldContextObject)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
return World ? (World->GetNetMode() == NM_Standalone) : false;
}

enum ENetMode的成员及其含义如下:

NameDescript
NM_StandaloneStandalone: a game without networking, with one or more local players.
NM_DedicatedServerDedicated server: server with no local players.
NM_ListenServerListen server: a server that also has a local player who is hosting the game, available to other players on the network.
NM_ClientNetwork client: client connected to a remote server.
NM_MAX

使用UE的OnlineSubsystem联网

UE的联网可以通过CreateSession/FindSeccsion/JoinSession来实现,但要先进行一些简单配置。
编辑Config\DefaultEngine.ini,加入以下内容:

1
2
3
4
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Null

注意和对接Steam的不同,要把DefaultPlatformService改为Null,不然无法加入Session.

UE联网客户端没有寻路

需要在Project Settings-NavigationSystem里设置Allow Client Side Navigationtrue

UE的Session在LAN中搜索不到

使用UE的CreateSession创建,然后在LAN的另一台机器上使用FindSession无法找到Session列表。
出现这个问题的原因是电脑上装了VM或者VirtualBox之类的虚拟机,需要把VMware Network Adapter或者VirtualBox NetWork Adapter禁用掉就可以了。
在控制面板的具体位置为控制面板/网络和Internet/网络和共享中心/更改适配器设置

Other Script

UE莫名其妙的编译和链接错误

修改代码后最好把下面的文件删了:

1
2
3
4
5
Binaries
Debug
Intermediate
Saved(注意打包缓存可以不清理)
*.VC.db

bat脚本如下:

1
2
3
4
5
del *.VC.db
rd /s /q Binaries
rd /s /q Debug
rd /s /q Intermediate
rd /s /q Saved

还有搜索所有插件目录的Binaries和Intermediate:

1
2
$ find . -name "Binaries"|xargs rm -rf
$ find . -name "Intermediate"|xargs rm -rf

全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:UE4和VR开发技术笔记
文章作者:ZhaLiPeng
发布时间:2018年06月06日 08时16分
更新时间:2019年01月17日 14时00分
本文字数:本文一共有9,072字
原始链接:https://imzlp.me/posts/3380/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!