UE4和VR开发技术笔记

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

UE4 Online Documents

使用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

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)

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的到进程上的。

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

Game Flow on UE4

Game Flow Overview on UE4

Actor LifeCycle

Actor Lifecycle on Unreal Engine

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

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

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

在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

使用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

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在切换关卡时不会被销毁。

重置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换装的一个实现

在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的对象中调用的...这就有毒了。

VR弓的模型标准

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

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

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

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

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

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

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);这一行注释掉就可以了。

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

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

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

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

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的文档:

HTC VIVE设备设置



CharacterMovement开启RVO避障

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

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

扫描二维码,分享此文章

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