在本文中,我将告诉您如何在虚幻引擎4中创建门户。我没有找到任何详细描述此类系统的资源(通过门户进行监视并通过门户),因此我决定编写自己的系统。
什么是门户?
让我们从门户的示例和解释开始。 将门户网站描述为从一个空间到另一个空间的通道的最简单方法。 在某些流行游戏中,此概念用于视觉效果,甚至用于游戏机制:
游戏入口范例(GIF)Antichamber(2013)和Portal(2007)猎物,2006年 在这三款游戏中,最著名的可能是《传送门》,但我个人一直很喜欢《猎物》,我想复制的就是她。 曾经尝试在Unreal Engine 4中实现自己的版本,但由于引擎缺乏功能,因此并没有真正成功。 不过,我设法进行了以下实验:
但是,只有在新版本的虚幻引擎中,我才最终设法达到预期的效果:
门户-它们如何工作?
在继续具体操作之前,让我们看一下门户如何工作的一般情况。
实际上,门户网站是一个没有外在,而是到另一个地方的窗口,也就是说,我们在本地设置了相对于对象的特定视点,并将该视点复制到其他位置。 使用此原理,即使两个空间彼此距离很远,我们也可以将它们连接起来。 窗口就像一个遮罩,使我们可以找出何时何地显示另一个空间而不是原始空间。 由于视图的起点已复制到其他地方,因此给了我们连续性的错觉。
在此图像中,捕获设备(UE4中的SceneCapture)位于与从玩家的角度看到的空间相对应的空间的前面。 该行之后可见的所有内容都将替换为捕获内容。 由于捕获设备可以位于门和其他物体之间,因此使用所谓的“裁剪平面”很重要。 对于门户,我们希望封闭的剪切平面遮盖门户前面可见的对象。
总结一下。 我们需要:
- 玩家位置
- 门户入口点
- 门户出口点
- 带有剪切平面的剪切装置
如何在虚幻引擎中实现呢?
我基于
PlayerController和
Character管理的两个主要类构建了系统。
Portal类是一个真实的门户入口点,其视图/出口点是Target actor。 还有一个
Portal Manager ,由PlayerController生成并由Character更新,以在级别上管理每个门户并对其进行更新,以及操作SceneCapture对象(所有门户网站都通用)。
请记住,本教程希望您可以从代码访问Character和PlayerController类。 就我而言,它们称为ExedreCharacter和ExedrePlayerController。
创建门户网站演员类
让我们从门户网站的参与者开始,它将用于设置“窗口”,通过这些窗口我们可以查看关卡。 演员的任务是提供有关玩家的信息,以计算各种位置和转弯。 他还将从事识别玩家是否穿过门户及其传送的活动。
在开始对参与者进行详细讨论之前,让我解释一下我为管理门户网站系统而创建的一些概念:
- 为了方便拒绝计算,门户网站处于活动/不活动状态。 此状态由Portal Manager更新。
- 门户的正面和背面由其位置和方向(正向向量)确定。
- 为了找出玩家是否跨过门户,他存储了玩家的先前位置并将其与当前位置进行比较。 如果在以前的衡量标准中,玩家位于门户的前面,而当前玩家位于门户后面,那么我们认为玩家越过了门户。 反向行为将被忽略。
- 门户网站有一个限制数量,以便在玩家进入此数量之前不进行计算和检查。 示例:如果玩家实际上没有触摸门户,则忽略路口。
- 播放器的位置是根据摄像头的位置计算得出的,以确保当视点穿过门户而不是播放器的身体时正确的行为。
- 门户接收一个“渲染目标”,如果下次纹理不正确并且需要替换,则在每个度量中显示一个不同的视点。
- 门户网站存储到另一个名为Target的actor的链接,以便知道要在何处联系其他空间。
使用这些规则,我创建了一个新的ExedrePortal类,该类继承自AActor。 这是它的标题:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortal.generated.h" UCLASS() class EXEDRE_API AExedrePortal : public AActor { GENERATED_UCLASS_BODY() protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override;
如您所见,这里描述了大多数行为。 现在,让我们看看它们是如何在体内(.cpp)处理的。
设计人员正在准备根组件。 我决定创建两个根组件,因为门户网站参与者将结合图形效果和碰撞/识别。 因此,我需要一种简单的方法来确定窗口/门户平面的位置,而无需蓝牙功能或其他技巧。 PortalRootComponent将成为与门户相关的所有计算的基础。
如果Blueprint类将其动画化(例如,使用打开/关闭动画),则门户网站根设置为动态。
只有Get和Set函数,仅此而已。 我们将从另一个地方管理活动状态。
bool AExedrePortal::IsActive() { return bIsActive; } void AExedrePortal::SetActive( bool NewActive ) { bIsActive = NewActive; }
蓝图事件,我在C ++类中不做任何事情。
void AExedrePortal::ClearRTT_Implementation() { } void AExedrePortal::SetRTT_Implementation( UTexture* RenderTexture ) { } void AExedrePortal::ForceTick_Implementation() { }
目标角色的获取和设置功能。 这部分也没有什么更复杂的。
AActor* AExedrePortal::GetTarget() { return Target; } void AExedrePortal::SetTarget( AActor* NewTarget ) { Target = NewTarget; }
使用此功能,我们可以轻松地检查点是否在平面前面,在我们的情况下,该点是入口。 该函数使用UE4引擎的FPlane结构执行计算。
bool AExedrePortal::IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point );
此功能检查该点是否已穿过门平面。 在这里,我们使用旧位置来找出该点的行为。 此功能很常见,因此可以与任何演员一起使用,但在我的情况下,仅与播放器一起使用。
该函数在先前位置和当前位置之间创建方向/线段,然后检查它们是否与平面相交。 如果是这样,则我们检查它是否沿正确的方向交叉(从前到后?)。
bool AExedrePortal::IsPointCrossingPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FVector IntersectionPoint; FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point ); bool IsCrossing = false; bool IsInFront = PortalDot >= 0; bool IsIntersect = FMath::SegmentPlaneIntersection( LastPosition, Point, PortalPlane, IntersectionPoint );
传送演员
我们将讨论的门户网站参与者的最后一部分是
TeleportActor()函数。
将角色从A点传送到B点时,您需要复制其移动和位置。 例如,如果玩家进入门户,然后结合适当的视觉效果,在他看来他就穿过了普通的门。
门户的交集感觉就像是直线移动,但是实际上发生了完全不同的事情。 退出门户网站后,玩家可能会处于完全不同的环境中。 考虑一个来自门户网站的示例:
如您所见,穿过门户时,摄像机相对于其前向矢量旋转(旋转)。 这是因为起点和终点平行于不同的平面:
因此,为了使其正常工作,我们需要将玩家的动作转换为门户的相对空间,以便将其转换为目标空间。 通过执行此操作,我们可以确保进入门户并从另一侧退出之后,播放器将相对于空间正确对齐。 这不仅适用于演员的位置和旋转,还适用于他的
速度 。
如果我们不做任何改变就传送一个角色,将其转换为局部旋转,那么结果是,该角色可以倒立。 这可能适用于对象,但不适用于角色或玩家本人。 您需要更改actor的位置,如上面Portal示例中所示。
void AExedrePortal::TeleportActor( AActor* ActorToTeleport ) { if( ActorToTeleport == nullptr || Target == nullptr ) { return; }
您可能已经注意到,要调用旋转/位置,我要调用外部函数。 从UTool用户类中调用它们,该类定义了可以在任何地方(包括蓝图)调用的静态函数。 它们的代码如下所示,您可以按照最合适的方式实现它们(将它们放在Portal actor类中可能会更容易)。
FVector ConvertLocationToActorSpace( FVector Location, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FVector::ZeroVector; } FVector Direction = Location - Reference->GetActorLocation(); FVector TargetLocation = Target->GetActorLocation(); FVector Dots; Dots.X = FVector::DotProduct( Direction, Reference->GetActorForwardVector() ); Dots.Y = FVector::DotProduct( Direction, Reference->GetActorRightVector() ); Dots.Z = FVector::DotProduct( Direction, Reference->GetActorUpVector() ); FVector NewDirection = Dots.X * Target->GetActorForwardVector() + Dots.Y * Target->GetActorRightVector() + Dots.Z * Target->GetActorUpVector(); return TargetLocation + NewDirection; }
这里的变换是通过计算向量的标量积来确定不同角度来执行的。 方向向量未归一化,也就是说,我们可以再次将点结果与目标向量相乘,以在目标角色的局部空间中获得完全相同距离的位置。
FRotator ConvertRotationToActorSpace( FRotator Rotation, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FRotator::ZeroRotator; } FTransform SourceTransform = Reference->GetActorTransform(); FTransform TargetTransform = Target->GetActorTransform(); FQuat QuatRotation = FQuat( Rotation ); FQuat LocalQuat = SourceTransform.GetRotation().Inverse() * QuatRotation; FQuat NewWorldQuat = TargetTransform.GetRotation() * LocalQuat; return NewWorldQuat.Rotator(); }
转变转型有点难以实施。 最后,最好的解决方案是使用
四元数 ,因为这比使用正常的
欧拉角更精确,并且只需要几行代码。 四元数的旋转使用乘法进行,因此在我们的示例中,将Inverse()应用于要转换的旋转,我们会将其移动到局部空间。 接下来,我们只需要将其再乘以目标转弯数即可获得最后一转弯。
创建门户网格
为了从玩家的角度看起来很漂亮,我的门户网站系统使用了特定的网格。 网格划分为两个不同的平面:
- 平面1 :显示门户网站渲染目标的主平面。 这架飞机有一种非常不寻常的行为,因为它的任务是在玩家接近时将其向后推一点,以免被相机夹住。 由于平面的边界不移动,而仅其中间峰移动,因此允许玩家叠加渲染门户而没有视觉伪像。 边缘的边缘在上半部分具有自己的UV,而内部边缘的下半部分具有自己的UV,这使在着色器中轻松遮罩它们成为可能。
- 平面2 :此平面仅用于扩展网格的标准边界框。 顶点的法线朝下,因此,即使在非平面地面上,默认情况下也将看不到网格(因为渲染材料将不会是双面的)。
为什么要使用这样的网格?
我决定随着玩家的接近,“平面1”将伸展。 这允许播放器重叠门户并通过它而不会修剪(剪切)。 例如,如果摄像头尚未越过门户的平面,但玩家的脚已经触摸过门户,则可能发生这种情况。 这样一来,您就无需切断播放器并复制网格。
“平面2”任务是扩展网格的标准边界框。 由于“平面1”是平坦的,因此一轴上的边界框的厚度为0,并且如果摄影机在其后方,则引擎会将其切除(即,它将不会渲染)。 平面1的尺寸为128×128,因此可以使用引擎轻松缩放。 平面2稍大且在地板下方(0之下)。
创建网格后,我们只需从第三方3D编辑器中将其导出并导入到Unreal中即可。 它将在下一步中使用。
创建门户资料
要显示门户的另一侧,我们需要创建自己的材料。 在内容浏览器中创建新材料(我称之为
MAT_PortalBase ):
现在打开它并创建以下图形:
材料的工作方式如下:
- FadeColor是当其距离很远时将通过门户可见的颜色。 这是必需的,因为我们并不总是渲染所有门户,因此当播放器/摄像机距离很远时,我们会遮挡渲染。
- 为了找出玩家离门户有多远,我确定了摄像机位置和演员位置之间的距离 。 然后,我将距离除以要进行比较的最大值。 例如,如果我设置的最大值为2000,而距播放器的距离为1000,则得到0.5。 如果玩家走得更远,那么我会得到一个大于1的值,所以我使用饱和节点对其进行限制。 接下来是Smoothstep节点,用于将距离缩放为渐变并更精确地控制门户着色。 例如,我想要在玩家关闭时阴影完全消失。
- 我将距离计算用作Lerp节点的Alpha通道值,以混合阴影颜色和将渲染门户目标的纹理 。
- 最后,我隔离UV坐标的Y分量以创建一个遮罩,使您知道将推动网格的哪些顶点。 我将此面罩乘以我所需的排斥力。 我使用一个负值,以便当顶点的法线与顶点相乘时,它们沿相反的方向移动。
完成所有这些之后,我们创建了即用型材料。
在钝器中创建门户网站参与者
让我们建立一个新的继承自Portal actor的蓝图类。 右键单击内容浏览器,然后选择Blueprint类:
现在,在搜索字段中输入“门户”以选择门户类别:
如果尚未打开蓝牙,请打开它。 在组件列表中,您将看到以下层次结构:
如我们所料,有一个根组件和门户网站根。 让我们向PortalRootComponent添加一个静态网格物体组件,并将上一步中创建的网格物体加载到其中:
我们还添加了“碰撞盒”,该盒将用于确定玩家是否在门户区域内:
碰撞框位于与主根相关联的场景组件下方,而不位于门户网站根下方。 我还添加了一个图标(广告牌)和一个箭头组件,以使门户在各个级别上更加可见。 当然,这不是必需的。
现在,让我们在蓝图中设置材料。
首先,我们需要两个变量-一个变量为
Actor类型,名称为
PortalTarget ,第二个变量为
Dynamic Material Instance类型,并称为
MaterialInstance 。 PortalTarget将是对门户窗口正在查看的位置的引用(因此,该变量是常见的,带有睁开的眼睛图标),以便我们可以在将actor放置在水平位置时对其进行更改。 MaterialInstance将存储到动态材质的链接,以便将来我们可以动态分配门户的渲染目标。
我们还需要添加我们自己的事件节点。 最好在“
事件图”中打开右键单击菜单,然后找到
事件的名称:
在这里创建下图:
- 开始播放 :在这里,我们调用门户网站的父函数SetTarget()为其分配到演员的链接,该链接随后将用于SceneCapture。 然后,我们创建一个新的动态材质,并为其指定MaterialInstance变量的值。 使用这种新材料,我们可以将其分配给“静态网格物体组件”。 我还为材质提供了虚拟纹理,但这是可选的。
- 清除RTT :此功能的目的是清除分配给门户材料的“渲染目标”纹理。 它由门户网站管理器启动。
- 设置RTT :此功能的目的是设置门户网站的渲染目标材料。 它由门户网站管理器启动。
到目前为止,我们已经完成了蓝牙的工作,但是稍后将返回到蓝牙以实现Tick功能。
门户网站经理
因此,现在我们有了创建从AActor继承的新类(即Portal Manager)所需的所有基本元素。 您的项目中可能不需要Portal Manager类,但就我而言,它极大地简化了某些方面的工作。 这是门户网站管理器执行的任务列表:
- 门户管理器是由玩家控制器创建并附加到其上的角色,用于跟踪游戏级别内玩家的状态和演变。
- 创建并销毁渲染目标门户 。 该想法是动态创建与播放器的屏幕分辨率匹配的渲染目标纹理。 此外,在游戏过程中更改分辨率时,经理将自动将其转换为所需的大小。
- 门户网站管理器查找并更新门户网站参与者级别,以为他们提供渲染目标。 以确保与级别流兼容的方式执行此任务。 当新演员出现时,他应该得到一个纹理。 此外,如果“渲染”目标发生变化,则管理者还可以自动分配一个新对象。 这使管理系统更加容易,而不是让每个Portal actor手动联系管理员。
- SceneCapture组件已附加到门户网站管理器,以便不为每个门户网站创建一个副本。 此外,它使您每次我们在级别上切换到特定门户网站参与者时都可以重用它。
- 门户决定传送播放器时,它将向门户管理器发送请求。 为了同时更新源门户和目标门户(如果有),这是必需的,这样过渡就可以实现。
- 在“ 角色”勾号()函数的末尾会更新门户网站管理器,以便正确更新所有内容,包括玩家的相机。 这样可以确保屏幕上的所有内容都同步,并避免引擎渲染期间延迟一帧。
让我们看一下Portal Manager标头:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortalManager.generated.h"
在详细介绍之前,我将展示如何从PlayerController类(从BeginPlay()函数调用)创建actor:
FActorSpawnParameters SpawnParams; PortalManager = nullptr; PortalManager = GetWorld()->SpawnActor<AExedrePortalManager>( AExedrePortalManager::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams); PortalManager->AttachToActor( this, FAttachmentTransformRules::SnapToTargetIncludingScale); PortalManager->SetControllerOwner( this ); PortalManager->Init();
因此,我们创建一个actor,将其附加到播放器的控制器(this),然后保存链接并调用Init()函数。
还需要注意的是,我们从Character类中手动更新actor:
void AExedreCharacter::TickActor( float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction ) { Super::TickActor( DeltaTime, TickType, ThisTickFunction ); if( UGameplayStatics::GetPlayerController(GetWorld(), 0) != nullptr ) { AExedrePlayerController* EPC = Cast<AExedrePlayerController>( UGameplayStatics::GetPlayerController(GetWorld(), 0) ); EPC->PortalManager->Update( DeltaTime ); } }
这是Portal Manager的构造函数。 请注意,由于我们将通过播放器手动更新Portal Manager,因此Tick被禁用。
AExedrePortalManager::AExedrePortalManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = false; PortalTexture = nullptr; UpdateDelay = 1.1f; PreviousScreenSizeX = 0; PreviousScreenSizeY = 0; }
这是get / set Portal Manager的功能(此后,我们将继续介绍更有趣的事情):
void AExedrePortalManager::SetControllerOwner( AExedrePlayerController* NewOwner ) { ControllerOwner = NewOwner; } FTransform AExedrePortalManager::GetCameraTransform() { if( SceneCapture != nullptr ) { return SceneCapture->GetComponentTransform(); } else { return FTransform(); } } UTexture* AExedrePortalManager::GetPortalTexture() {
显然,首先要开始的是
Init()函数。
此功能的主要目的是创建SceneCapture组件(即上述捕获设备)并正确配置它。首先创建一个新对象,并将其注册为该参与者的组件。然后,我们继续设置与此捕获有关的属性。要提到的属性:- bCaptureEveryFrame = false:我们不希望在不需要捕获时打开捕获。我们将手动对其进行管理。
- bEnableClipPlane = true:一个非常重要的属性,用于正确呈现门户捕获。
- bUseCustomProjectionMatrix = true:这使我们可以根据玩家的观点将Capture投影替换为我们自己的。
- CaptureSource = ESceneCaptureSource :: SCS_SceneColorSceneDepth:此模式有点昂贵,但是必须呈现足够数量的信息。
其余属性主要与后处理参数有关。它们是控制质量并因此捕获性能的便捷方法。最后一部分调用创建渲染目标的函数,我们将在下面看到。 void AExedrePortalManager::Init() {
当您需要为门户网站创建新的“渲染目标”纹理时,GeneratePortalTexture()是在必要时调用的函数。这发生在初始化功能中,但也可以在Portal Manager升级期间调用。这就是为什么此功能具有内部检查以更改视口分辨率的原因。如果未发生,则不执行更新。就我而言,我为UCanvasRenderTarget2D创建了一个包装器类。我称它为ExedreScriptedTexture,它是可以附加到actor的组件。我创建了此类,以便与具有渲染任务的角色方便地管理渲染目标。他对渲染目标进行了正确的初始化,并且与我自己的UI系统兼容。但是,在门户网站的上下文中,常规的RenderTarget2D纹理绰绰有余。因此,您可以简单地使用它。 void AExedrePortalManager::GeneratePortalTexture() { int32 CurrentSizeX = 1920; int32 CurrentSizeY = 1080; if( ControllerOwner != nullptr ) { ControllerOwner->GetViewportSize(CurrentSizeX, CurrentSizeY); } CurrentSizeX = FMath::Clamp( int(CurrentSizeX / 1.7), 128, 1920);
如上所述,我创建了自己的类,因此此处设置的属性必须适应通常的“渲染目标”。重要的是要了解捕获将在何处显示。由于渲染目标将显示在游戏中,因此这将在整个后期处理之前发生,因此我们需要使用足够的信息渲染场景(存储大于1的值以创建Bloom)。这就是为什么我选择RGBA16格式的原因(请注意,它具有自己的Enum,您将需要使用ETextureRenderTargetFormat)。有关更多信息,请参见以下资源:
此外,我们将考虑更新功能。基本功能非常简单,导致更复杂。调用GeneratePortalTexture()函数之前有一个延迟,以避免在调整视口大小时(例如,在编辑器中)重新创建渲染目标。在游戏发行期间,可以消除此延迟。 void AExedrePortalManager::Update( float DeltaTime ) {
我们调用UpdatePortalsInWorld()来查找当前世界中存在的所有门户(包括所有已加载的级别)并进行更新。该功能还确定哪个是“活动的”,即 对玩家可见。如果找到活动的门户,则调用UpdateCapture(),它控制SceneCapture组件。
这是世界更新在UpdatePortalsInWorld()中的工作方式:- ( )
- iterator ,
- , , ClearRTT() , . (, ).
- , , , , .
确定传送门正确性的检查很简单:我们将最接近玩家的传送门作为优先级,因为从他的角度来看,他很可能是最可见的。要放下亲戚,但例如,放置在玩家身后的门户,则需要进行更复杂的检查,但我不想在本教程中着重介绍这一点,因为这可能会变得非常困难。 AExedrePortal* AExedrePortalManager::UpdatePortalsInWorld() { if( ControllerOwner == nullptr ) { return nullptr; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
现在该考虑UpdateCapture()函数了。这是一项升级功能,可捕获门户的另一端。从注释中,所有内容都应该清楚,但是这里有一个简短描述:- 我们获得了角色和玩家控制器的链接。
- 我们检查一切是否正确(Portal,SceneCapture组件,Player)。
- Camera Target .
- , SceneCapture.
- SceneCapture Target.
- , SceneCapure , , .
- Render Target SceneCapture, .
- PlayerController.
- , Capture SceneCapture .
如我们所见,在传送玩家时,SceneCapture自然无瑕的行为的关键要素是将门户的位置和旋转正确转换到本地目标空间。有关ConvertLocationToActorSpace()的定义,请参见“传送角色”。
void AExedrePortalManager::UpdateCapture( AExedrePortal* Portal ) { if( ControllerOwner == nullptr ) { return; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
功能GetCameraProjectionMatrix()在类的默认的PlayerController不存在,我说我自己。如下图所示: FMatrix AExedrePlayerController::GetCameraProjectionMatrix() { FMatrix ProjectionMatrix; if( GetLocalPlayer() != nullptr ) { FSceneViewProjectionData PlayerProjectionData; GetLocalPlayer()->GetProjectionData( GetLocalPlayer()->ViewportClient->Viewport, EStereoscopicPass::eSSP_FULL, PlayerProjectionData ); ProjectionMatrix = PlayerProjectionData.ProjectionMatrix; } return ProjectionMatrix; }
最后,我们需要实现对Teleport函数的调用。通过门户网站管理器进行部分传送处理的原因是,必须保证必要门户的更新,因为只有管理器才具有有关场景中所有门户的信息。如果我们有两个已连接的门户,那么从一个门户切换到另一个门户时,我们需要在一个标记中更新两个门户。否则,播放器将进行传送并位于传送门的另一侧,但是目标传送门将在下一帧/小节之前处于活动状态。这将与我们在上面看到的平面网格的偏移材质产生视觉间隙。 void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { Portal->TeleportActor( TargetToTeleport );
就是这样,我们终于完成了Portal Manager!完成蓝图
完成Portal Manager之后,我们只需要完成Portal actor本身,然后系统即可工作。唯一缺少的是刻度线功能:运作方式如下:- 我们正在更新Material,以使其不会保持活动状态。
- 如果门户网站当前处于非活动状态,则其余度量将被丢弃。
- 我们获得了Character类来访问Camera Location。
- 第一部分检查摄像头是否在门户的冲突框中。如果是这样,那么我们用其Material偏移门户网格。
- 第二部分是重新检查碰撞盒内的位置。如果执行了该命令,那么我们将调用一个函数来检查是否穿越门户。
- , Portal manager, Teleport .
在我的图表的屏幕截图中,您可以注意到两个有趣的地方:盒子里面是点和获取门户网站管理器。我还没有解释这两个功能。这些是我在自己的类中定义的静态函数,因此您可以从任何地方调用它们。这是一种帮助程序类。这些功能的代码如下所示,您可以自行决定将它们插入到哪里。如果不需要在门户网站系统之外使用它们,可以将它们直接插入Portal演员类中。起初,我想使用碰撞系统来确定碰撞盒内的门户网站演员是否在门户网站中,但是在我看来这不够可靠。另外,在我看来,这种方法使用起来更快,并且具有一个优点:它考虑了actor的旋转。 bool IsPointInsideBox( FVector Point, UBoxComponent* Box ) { if( Box != nullptr ) {
AExedrePortalManager* GetPortalManager( AActor* Context ) { AExedrePortalManager* Manager = nullptr;
蓝图演员的最后一部分是ForceTick。请记住,当玩家越过一个门户并且与Portal Manager强制对其进行更新的另一个门户相邻时,将调用Force Tick。由于我们只是传送,因此不必使用相同的代码,您可以使用其简化版本:该过程大约与Tick函数同时开始,但是我们只执行序列的第一部分,从而更新材料。完成了吗
差不多了如果以这种形式实现门户网站系统,那么很可能会遇到以下问题:这是怎么回事在此gif中,游戏的帧频限制为6 FPS,以便更清楚地显示问题。在一个帧中,该多维数据集消失了,因为虚幻引擎剪辑系统认为它是不可见的。这是因为发现是在当前帧中执行的,然后在下一个帧中使用。这样会造成一帧的延迟。通常可以通过展开对象的边界框来解决它,以便在它变得可见之前就对其进行注册。但是,这在这里不起作用,因为当我们穿越门户网站时,我们从一个地方传送到一个完全不同的地方。禁用裁剪系统也是不可能的,尤其是因为在具有许多对象的级别上,这会降低性能。另外,我尝试了许多使用Unreal引擎的团队,但没有得到积极的结果:在所有情况下,都只延迟了一帧。幸运的是,在详细研究了虚幻引擎源代码之后,我设法找到了一个解决方案(路径很长-花了一个多星期的时间)!与SceneCapture组件一样,您可以告诉播放器的相机我们进行了跳跃剪切-摄像机位置跳到了两帧之间,这意味着我们不能依赖前一帧的信息。当使用Matinee或Sequencer时,例如在切换相机时,可以观察到此行为:运动模糊或平滑不能依赖于前一帧的信息。为此,我们需要考虑两个方面:- LocalPlayer:此类处理各种信息(例如,玩家的视口),并与PlayerController关联。在这里我们可以影响播放器相机的渲染过程。
- PlayerController:当玩家进行传送时,由于可以访问LocalPlayer,因此此类开始拼接。
该解决方案的最大优势在于,引擎渲染过程中的干预最少,并且易于在将来的虚幻引擎更新中进行维护。
让我们从创建一个继承自LocalPlayer的新类开始。下面的标题标识了两个主要组件:重新定义“场景视口”计算和用于调用相机胶合的新功能。 #pragma once #include "CoreMinimal.h" #include "Engine/LocalPlayer.h" #include "ExedreLocalPlayer.generated.h" UCLASS() class EXEDRE_API UExedreLocalPlayer : public ULocalPlayer { GENERATED_BODY() UExedreLocalPlayer(); public: FSceneView* CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) override; void PerformCameraCut(); private: bool bCameraCut; };
这是所有事情的实现方式: #include "Exedre.h" #include "ExedreLocalPlayer.h" UExedreLocalPlayer::UExedreLocalPlayer() { bCameraCut = false; } FSceneView* UExedreLocalPlayer::CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) {
PerformCameraCut()只是使用布尔值启动Camera Cut。当引擎调用CalcSceneView()函数时,我们首先运行原始函数。然后我们检查,我们需要执行粘合。如果是这样,我们将在FSceneView结构中重新定义Camera Cut布尔变量,该变量将由引擎渲染过程使用,然后重置该布尔变量(使用它)。
在播放器控制器方面,更改很小。您需要在标头中添加一个变量,以存储指向LocalPlayer本机类的链接: UPROPERTY() UExedreLocalPlayer* LocalPlayer;
然后在BeginPlay()函数中: LocalPlayer = Cast<UExedreLocalPlayer>( GetLocalPlayer() );
我还添加了一个功能来快速启动Cut: void AExedrePlayerController::PerformCameraCut() { if( LocalPlayer != nullptr ) { LocalPlayer->PerformCameraCut(); } }
最后,在门户网站管理器函数RequestTeleportByPortal()中,我们可以在Camera Cut隐形传送期间执行: void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { if( ControllerOwner != nullptr ) { ControllerOwner->PerformCameraCut(); } [...]
仅此而已!必须在更新SceneCapture之前调用Camera Cut,这就是为什么它在函数的开头。最终结果
现在我们已经学会了在门户中思考。如果系统运行良好,那么我们应该能够创建以下内容:如果遇到问题,请检查以下内容:- 验证正确创建和初始化了Portal Manager。
- 正确创建了渲染目标(可以使用在内容浏览器中创建的渲染目标开始使用)。
- 门户已正确激活和停用。
- 门户网站已在编辑器中正确设置了目标参与者。
问与答
有人问我关于本教程的最流行的问题:是否可以用钝器而不是C ++来实现?除了以下两个方面,大部分代码都可以用钝器实现:- 蓝图中不提供用于获取投影矩阵的LocalPlayer GetProjectionData()函数。
- 蓝图中没有LocalPlayer CalcSceneView()函数,这对于解决裁剪系统问题至关重要。
因此,您需要使用C ++实现来访问这两个函数,或者修改引擎源代码以使其可以通过蓝图进行访问。我可以在VR中使用此系统吗?是的,在大多数情况下。但是,某些部分将必须进行修改,例如:- 您需要使用两个渲染目标(每只眼睛一个),并在门户材料中对其进行遮罩,以在屏幕空间中并排显示。每个渲染目标应为VR设备分辨率的一半。
- 您需要使用两个SceneCapture来以正确的距离(眼睛之间的距离)渲染目标,以创建立体效果。
主要问题将是性能,因为门户的另一侧将必须呈现两次。另一个对象可以穿过门户吗?我的代码中没有。但是,使其更通用并不是那么困难。为此,门户网站需要跟踪有关附近所有物体的更多信息,以检查它们是否越过。系统是否支持递归(门户内部的门户)?本教程不是。对于递归,您需要其他渲染目标和SceneCapture。还需要确定首先渲染哪个RenderTarget,依此类推。这是相当困难的,并且我不想这样做,因为对于我的项目而言,这是没有必要的。我可以穿过墙附近的入口吗?不幸的是,没有。但是,我看到了两种方法(理论上):- 禁用玩家的碰撞,以便他可以穿过墙壁。它很容易实现,但是会带来很多副作用。
- 破解一个碰撞系统以动态创建一个洞,这将允许玩家通过。为此,您需要修改引擎的物理系统。但是,据我所知,加载关卡后,静态物理无法更新。因此,支持此功能将需要大量的工作。如果您的门户网站是静态的,则可以使用级别流在不同的冲突之间切换来解决此问题。