查看原文
其他

虚幻引擎的设计模式与性能优化

Mickie Zhou 腾讯游戏学堂 2023-11-24
文丨Mickie Zhou
腾讯互动娱乐 工程师

| 导语

本文以设计模式的角度探讨使用虚幻引擎时怎么做性能优化: 空间分区模式、对象池模式、脏标记模式、数据局部性模式、异步加载模式、批处理模式等。


空间分区模式

Spatial Partition


在游戏开发中,空间分区模式是一种通过有效组织和管理虚拟环境中的游戏对象来优化游戏性能的技术。当需要频繁更新大量对象(例如检查碰撞、在屏幕上渲染对象或执行AI计算)时,此模式尤为有用。


空间分区模式的主要目标是通过将游戏世界划分为更小的、可管理的区域或分区,最大限度地减少不必要的计算和对象之间的比较。这使得游戏引擎能够快速识别哪些对象对于特定操作(如渲染或碰撞检测)是相关的,并忽略其他对象。


在游戏开发中实现空间分区模式有几种技术,包括:


1. 网格:将游戏世界划分为大小相等的单元格。每个对象都放置在它所占据的单元格中,只需要检查相邻的单元格进行交互。这种方法实现简单,适用于2D游戏或对象分布相对均匀的游戏。


2. 四叉树(2D)/八叉树(3D):这些数据结构递归地将游戏世界划分为更小的区域,直到达到一定的阈值。树中的每个节点表示一个区域,其子节点表示子区域。对象存储在叶节点中,只需要检查附近的节点进行交互。对于对象分布不均匀的游戏,这种方法更高效,因为它可以适应对象密度。


3. 二叉空间分割(BSP):这种技术沿着任意平面(通常基于几何形状)递归地划分游戏世界,创建一个二叉树结构。树中的每个节点表示一个区域,其子节点表示分割平面的两侧区域。这种方法通常用于3D游戏,特别是用于渲染和可见性计算。


4. k-d树:这种数据结构类似于BSP树,但它沿着轴对齐的平面划分游戏世界,在树的每个级别交替使用不同的轴。k-d树通常用于具有大量对象的游戏中进行高效的最近邻搜索和碰撞检测。


5. R树:这种数据结构专为高效存储和查询边界框形式的空间数据而设计。对于具有不同大小对象的游戏,它尤为有用,因为它可以适应对象的空间分布和大小。

通过在游戏开发中使用空间分区模式,开发人员可以显著提高游戏性能,降低与对象交互和渲染相关的计算开销。这使得游戏世界更加复杂和沉浸,同时为玩家提供更流畅的游戏体验。


UE4 中的应用例子


1. 细节层次(LOD):UE4支持细节层次,这是一种根据3D模型与摄像机的距离自动降低模型复杂度的技术。这减少了渲染工作负载,有助于保持高帧率。LOD可以应用于静态网格和骨骼网格。


2. 剔除:UE4具有内置的剔除技术,例如视锥剔除、遮挡剔除和距离剔除。这些技术通过仅渲染对摄像机可见的对象并忽略其他对象来优化渲染性能。


3. 分层细节层次(HLOD):HLOD是UE4中的一种优化功能,根据摄像机与对象的距离将多个静态网格合并为一个网格。这减少了绘制调用并提高了渲染性能。HLOD特别适用于具有许多静态对象的大型开放世界环境。


4. 世界分区:UE在5.0版本中引入了世界分区系统,该系统旨在更有效地处理大型开放世界环境。世界分区根据玩家的位置自动将游戏世界划分为单元格并流式传输必要的数据。这有助于减少内存使用并提高性能。


5. 碰撞和物理:UE4具有内置的物理引擎(PhysX),该引擎利用空间分区技术(如边界体积层次结构和加速结构)来优化碰撞检测和物理模拟。


6. AI和导航:UE4的AI和导航系统使用空间分区技术来优化寻路和其他AI相关计算。例如,导航网格被划分为较小的区域,从而实现更高效的寻路和更新。


7. ReplicationGraph:UE4的ReplicationGraph是一个框架,通过根据角色对玩家的相关性来组织和优先处理角色,从而优化网络复制。它利用空间分区模式(Spatial Partition Pattern)来有效地对不同空间区域中的角色进行分类和管理,减少复制数据的数量。通过使用ReplicationGraph和空间分区模式,UE4确保了多人游戏中的高效网络性能和改进的可扩展性。


代码演示


以下代码片段展示了在虚幻引擎4中实现一个简单的ReplicationGraph。请注意,这仅仅是一个示例,而不是完整的实现。


// MyReplicationGraph.h

#pragma once

 

#include "CoreMinimal.h"

#include "ReplicationGraph.h"

#include "MyReplicationGraph.generated.h"

 

UCLASS()

class MYGAME_API UMyReplicationGraph : public UReplicationGraph

{

    GENERATED_BODY()

 

public:

    UMyReplicationGraph();

 

    virtual void InitGlobalActorClassSettings() override;

    virtual void InitConnectionGraphNodes(UNetReplicationGraphConnection* ConnectionManager) override;

};

 

// MyReplicationGraph.cpp

#include "MyReplicationGraph.h"

 

UMyReplicationGraph::UMyReplicationGraph()

{

    // 设置默认复制设置

    GlobalActorReplicationInfoClass = UBasicReplicationGraph_GlobalInfo::StaticClass();

}

 

void UMyReplicationGraph::InitGlobalActorClassSettings()

{

    Super::InitGlobalActorClassSettings();

 

    // 为特定的角色类设置复制周期和距离

    FGlobalActorReplicationInfoClassSettings& MyClassSettings = GlobalActorClassSettingsMap[AMyActor::StaticClass()];

    MyClassSettings.ReplicationPeriodFrame = 2; // 每2帧复制一次

    MyClassSettings.CullDistanceSquared = 10000.0f * 10000.0f; // 10000单位的剔除距离(平方)

}

 

void UMyReplicationGraph::InitConnectionGraphNodes(UNetReplicationGraphConnection* ConnectionManager)

{

    Super::InitConnectionGraphNodes(ConnectionManager);

 

    // 添加一个用于优先处理和复制特定角色的节点

    UReplicationGraphNode_ActorList* MyActorNode = CreateNewNode<UReplicationGraphNode_ActorList>();

    ConnectionManager->MyActorNode = MyActorNode;

}


[ 上下滑动查看代码 ]


此代码演示了如何创建一个自定义的ReplicationGraph类,为特定的角色类设置复制设置,并添加一个用于优先处理和复制特定角色的自定义节点。在项目中实际实现ReplicationGraph会更复杂,但这个示例应该让您了解它是如何工作的。


对象池模式

Object Pool


对象池模式是游戏开发中用于优化性能的设计模式,通过重用对象而不是反复创建和销毁它们。这种模式对于资源密集型对象(如游戏实体、粒子系统或复杂的网格渲染器)尤为有用,因为实例化和垃圾收集可能会导致显著的性能开销。


从深入的技术术语来看,对象池模式包括以下组件和过程:


1. 对象池:这是一个容器或数据结构(例如,列表、队列或堆栈),用于保存一组预先实例化的对象,称为“池项目”。池负责管理这些对象的生命周期,包括它们的分配、重用和释放。


2. 池项目:这些是由对象池管理的对象。它们在池创建时被创建和初始化,并保持在池中,直到池被销毁。每个池项目都有一个状态(例如,活动或非活动),以表示它当前是否正在使用或可供重用。


3. 对象分配:当客户端从池中请求一个对象时,池会检查是否有一个非活动的池项目可用。如果有,池将对象返回给客户端,并将其标记为活动状态。如果没有可用的对象,池可能会创建一个新对象(取决于实现)或返回一个空引用,表示客户端必须等待或相应地处理情况。


4. 对象重用:当客户端完成对池项目的使用时,它将对象返回给池,池将其标记为非活动状态,并使其可供其他客户端重用。这个过程避免了昂贵的对象实例化和垃圾收集,因为对象不会被销毁,而只是返回到池中供将来使用。


5. 对象释放:当对象池被销毁时(例如,在关卡更改或应用程序关闭期间),所有池项目都会被释放,它们的资源将被释放。


对象池模式在游戏开发中有以下几个优点:


● 改进性能:通过重用对象,对象实例化和垃圾收集的开销大大减少,从而实现更平滑的帧速率和更低的内存使用。


● 更好的资源管理:池可以配置为维护特定数量的对象,确保资源使用高效,防止内存泄漏或资源耗尽。


● 更容易扩展:随着场景中对象数量的增长,使用对象池的性能优势变得更加明显。这使得更容易将游戏扩展到支持更复杂的场景和更大数量的实体。


● 可预测的行为:由于对象是预先分配并由池管理的,游戏性能变得更加可预测,降低了意外性能问题或崩溃的可能性。


总之,对象池模式是游戏开发中一种有价值的技术,可以实现高效的对象管理,降低性能开销,提高整体游戏性能和稳定性。


UE4 中的应用例子


1. 粒子系统:UE4的粒子系统Cascade以及更新的Niagara系统都使用池技术来有效管理粒子发射器。它们重用粒子,而不是不断地创建和销毁它们,从而减少了实例化和垃圾收集的开销。


2. 实例化静态网格和分层实例化静态网格:这些功能允许您使用单个绘制调用渲染静态网格的多个实例,当渲染大量类似对象时显著提高性能。这是一种对象池的形式,因为它重用网格数据,只需要转换实例的位置、旋转和缩放。


3. AI和角色生成管理:UE4的AI和角色系统可以配置为使用对象池进行高效的NPC或其他游戏实体的生成和消失。这可以通过使用内置的Spawn Actor功能或利用UE4的UObject和Actor类的自定义实现来实现。


4. 投射物池:在许多游戏中,投射物(如子弹、箭或火箭)经常生成和销毁。UE4允许您使用蓝图或C++创建自定义的投射物池系统,以有效管理这些投射物并减少与它们的创建和销毁相关的性能开销。


5. 声音和音频组件:UE4的音频系统也可以从池中受益,因为声音和音频组件可以重用,而不是在每次播放时创建和销毁。这可以使用内置的音频组件或自定义池系统来实现。


虽然UE4没有专用的内置对象池系统,但使用蓝图或C++创建自己的对象池系统以有效管理各种游戏对象相对容易。还有社区创建的插件和资源可用,提供对象池功能,可以集成到您的UE4项目中。


代码演示


以下是一个简单的Unreal Engine 4 C++对象池实现:


1. 创建一个继承自UObject的新C++类,并将其命名为“ObjectPool”:


// ObjectPool.h

#pragma once

 

#include "CoreMinimal.h"

#include "UObject/NoExportTypes.h"

#include "ObjectPool.generated.h"

 

UCLASS(Blueprintable)

class YOURGAME_API UObjectPool : public UObject

{

    GENERATED_BODY()

 

public:

    UFUNCTION(BlueprintCallable, Category = "Object Pool")

    void InitializePool(TSubclassOf<AActor> ObjectType, int32 PoolSize);

 

    UFUNCTION(BlueprintCallable, Category = "Object Pool")

    AActor* AcquireObject();

 

    UFUNCTION(BlueprintCallable, Category = "Object Pool")

    void ReleaseObject(AActor* Object);

 

private:

    TArray<AActor*> Pool;

    TSubclassOf<AActor> ObjectClass;

};

[ 上下滑动查看代码 ]


2. 在ObjectPool.cpp文件中实现InitializePool、AcquireObject和ReleaseObject函数:


// ObjectPool.cpp

#include "ObjectPool.h"

#include "Engine/World.h"

 

void UObjectPool::InitializePool(TSubclassOf<AActor> ObjectType, int32 PoolSize)

{

    ObjectClass = ObjectType;

 

    for (int32 i = 0; i < PoolSize; ++i)

    {

        AActor* NewObject = GetWorld()->SpawnActor<AActor>(ObjectClass, FVector::ZeroVector, FRotator::ZeroRotator);

        NewObject->SetActorHiddenInGame(true);

        NewObject->SetActorEnableCollision(false);

        NewObject->SetActorTickEnabled(false);

        Pool.Add(NewObject);

    }

}

 

AActor* UObjectPool::AcquireObject()

{

    for (AActor* Object : Pool)

    {

        if (!Object->IsPendingKill() && !Object->IsActorBeingDestroyed() && Object->IsHidden())

        {

            Object->SetActorHiddenInGame(false);

            Object->SetActorEnableCollision(true);

            Object->SetActorTickEnabled(true);

            return Object;

        }

    }

 

    // 如果没有找到可用对象,返回nullptr

    return nullptr;

}

 

void UObjectPool::ReleaseObject(AActor* Object)

{

    if (Object)

    {

        Object->SetActorHiddenInGame(true);

        Object->SetActorEnableCollision(false);

        Object->SetActorTickEnabled(false);

    }

}

[ 上下滑动查看代码 ]


要在游戏中使用对象池,请按照以下步骤操作:

1. 创建一个`UObjectPool`类的实例。
2. 调用`InitializePool`函数,用所需的对象类型和池大小初始化池。
3. 当需要新对象时,调用`AcquireObject`函数从池中获取可用对象。
4. 当完成对象的使用后,调用`ReleaseObject`函数将其返回到池中。


以下是如何在游戏中使用对象池的示例:


// MyGameActor.h

#pragma once

 

#include "CoreMinimal.h"

#include "GameFramework/Actor.h"

#include "ObjectPool.h"

#include "MyGameActor.generated.h"

 

UCLASS()

class YOURGAME_API AMyGameActor : public AActor

{

    GENERATED_BODY()

 

public:

    // 在编辑器中将其设置为所需的对象类型(例如:抛射物、角色等)

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Object Pool")

    TSubclassOf<AActor> ObjectType;

 

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Object Pool")

    int32 PoolSize = 10;

 

protected:

    virtual void BeginPlay() override;

 

private:

    UPROPERTY()

    UObjectPool* ObjectPool;

};

 

// MyGameActor.cpp

#include "MyGameActor.h"

 

void AMyGameActor::BeginPlay()

{

    Super::BeginPlay();

 

    ObjectPool = NewObject<UObjectPool>();

    ObjectPool->InitializePool(ObjectType, PoolSize);

}

 

// 使用示例:从池中获取对象

AActor* NewObject = ObjectPool->AcquireObject();

if (NewObject)

{

    // 设置对象的位置、旋转等,并在游戏中使用它

}

 

// 使用示例:将对象释放回池中

ObjectPool->ReleaseObject(ObjectToRelease);

[ 上下滑动查看代码 ]


此代码演示了一个简单的Unreal Engine 4 C++对象池实现。您可以根据特定的游戏需求进一步定制此实现,例如添加调整大小功能或更详细地处理对象初始化和重置。


脏标记模式

Dirty Flag


脏标记模式(Dirty Flag pattern)是一种常用于游戏开发和计算机图形编程的设计模式,用于优化应用程序的性能。它在处理复杂、分层或资源密集型系统(如场景图、变换层次结构和渲染管道)时尤为有用。


从技术角度来说,脏标记模式涉及使用一个布尔变量(称为“脏标记”),表示自上次处理或更新某个对象或资源以来,该对象或资源是否已被修改。当对对象进行更改时,该标记设置为“脏”(true),当对象被处理或更新时,设置为“干净”(false)。


脏标记模式的主要目标是最小化冗余或不必要的计算、更新或重绘系统中的对象。通过在执行操作之前检查脏标记,您可以避免重复已完成的工作,或者由于对象未更改而不需要的工作。


以下是关于游戏开发中脏标记模式的更深入的解释:


1. 场景图:在游戏引擎中,场景图是一种表示游戏对象之间空间关系的分层数据结构。图中的每个节点都可能具有相对于其父节点的变换(位置、旋转、缩放)。当对节点的变换或层次结构进行更改时,可能会影响其所有子节点的全局变换。通过使用脏标记,引擎可以有效地仅更新受影响的节点及其子节点,而不是重新计算整个场景图。


2. 批处理:在渲染过程中,批处理是将具有相似属性(例如,材质、纹理)的对象分组以最小化渲染场景所需的绘制调用和状态更改的过程。脏标记可用于跟踪对象的属性何时发生更改,从而使引擎仅在必要时更新批处理,减少重建批处理数据的开销。


3. 缓存:游戏引擎通常使用缓存来存储昂贵计算或资源加载的结果,例如物理模拟、寻路或纹理解压缩。脏标记可用于跟踪这些计算的输入数据何时发生更改,使引擎仅在需要时使缓存失效并进行更新,而不是在每个帧中重新计算数据。


总之,脏标记模式是游戏开发中一种强大的优化技术,通过跟踪系统中对象和资源的更改,有助于最小化冗余工作并提高整体性能。通过使用脏标记,开发人员可以确保只执行必要的更新和计算,从而使应用程序更加高效和响应迅速。


UE4 中的应用例子


1. 变换层次结构:UE4使用变换层次结构作为其场景图,其中场景中的每个角色都相对于其父角色具有一个变换。当角色的变换发生变化时,它会设置一个脏标记,引擎仅在必要时更新角色及其子级的全局变换,而不是重新计算整个层次结构。


2. 材质批处理:UE4自动将具有相似材质的对象进行批处理,以减少渲染过程中的绘制调用和状态更改。当对象的材质属性发生变化时,脏标记用于仅在需要时更新批处理。


3. 蓝图:UE4的可视化脚本系统,称为蓝图(Blueprints),使用基于节点的方法来创建游戏逻辑。当蓝图节点被修改时,会设置一个脏标记,并且引擎仅在必要时重新编译蓝图,避免冗余编译。


4. 细节层次(LOD):UE4使用细节层次(Level of Detail,LOD)优化网格和纹理,根据距离摄像机的距离自动切换不同的细节层次。当对象的LOD属性发生变化时,脏标记用于更新LOD计算和渲染。


5. 剔除:UE4使用各种剔除技术,例如视锥剔除和遮挡剔除,以避免渲染摄像机看不见的对象。脏标记用于跟踪对象可见性的更改并相应地更新剔除计算。


6. PushModel:UE4的推送模型网络是一种优化技术,它仅将必要的数据更新发送给客户端,并利用脏标记模式来标记已修改的属性。通过仅发送变更,它减少了传输的数据量,从而最大限度地减少了处理冗余信息所需的CPU时间。脏标记模式有助于识别需要更新的属性,进一步提高效率并减少CPU使用。


这些仅是UE4在其系统和功能中如何整合脏标记模式的一些例子,以提高性能和资源管理。这种模式在整个引擎中被广泛使用,证明了其在优化游戏开发的各个方面的有效性。


代码演示


以下是如何使用C++在UE4中实现脏标记模式的示例。在此示例中,我们将创建一个具有变换属性和脏标记的简单自定义actor,以跟踪变换中的更改。


1. 首先,创建一个从AActor派生的新C++类,并将其命名为`MyCustomActor`。在头文件`MyCustomActor.h`中,添加以下代码:


#pragma once

 

#include "CoreMinimal.h"

#include "GameFramework/Actor.h"

#include "MyCustomActor.generated.h"

 

UCLASS()

class MYPROJECT_API AMyCustomActor : public AActor

{

GENERATED_BODY()

 

public:

AMyCustomActor();

 

// 设置新的变换并标记脏标记

void SetTransform(const FTransform& NewTransform);

 

// 检查变换是否已更改并更新actor

void UpdateTransformIfNeeded();

 

protected:

virtual void BeginPlay() override;

 

private:

// 用于跟踪变换中更改的脏标记

bool bIsTransformDirty;

 

// 新的变换值

FTransform UpdatedTransform;

};

[ 上下滑动查看代码 ]


2. 接下来,打开`MyCustomActor.cpp`文件并添加以下代码:


#include "MyCustomActor.h"

 

AMyCustomActor::AMyCustomActor()

{

PrimaryActorTick.bCanEverTick = true;

bIsTransformDirty = false;

}

 

void AMyCustomActor::BeginPlay()

{

Super::BeginPlay();

}

 

void AMyCustomActor::SetTransform(const FTransform& NewTransform)

{

UpdatedTransform = NewTransform;

bIsTransformDirty = true;

}

 

void AMyCustomActor::UpdateTransformIfNeeded()

{

if (bIsTransformDirty)

{

Super::SetActorTransform(UpdatedTransform);

bIsTransformDirty = false;

}

}


3. 在此示例中,我们创建了一个自定义actor,该actor具有一个`SetTransform`方法,该方法设置新的变换并标记脏标记。`UpdateTransformIfNeeded`方法检查脏标记是否已设置,并仅在必要时更新actor的变换。


4. 要在游戏中使用此自定义actor,可以创建一个`MyCustomActor`实例,并调用`SetTransform`方法来更新变换。确保在游戏循环中调用`UpdateTransformIfNeeded`(例如,在`Tick`方法中),以在需要时应用变换更改。


此示例演示了如何使用C++在UE4中实现脏标记模式的简单实现。您可以根据具体的用例和优化需求,将此模式扩展到其他属性和组件。


数据局部性模式

Data Locality


在游戏开发中,数据局部性模式是指通过确保数据以连续的内存块存储和处理的方式组织和访问内存中的数据,以最大限度地提高底层硬件(尤其是CPU缓存)的效率。这样可以最小化缓存未命中,并减少从高延迟内存中获取数据所花费的时间。


从深入的技术术语来解释数据局部性模式如下:


1. 内存层次结构:现代计算机系统具有分层的内存结构,其中CPU缓存速度更快但容量更小,主内存(RAM)速度较慢但容量较大。CPU缓存进一步分为多个级别(L1,L2和L3),每个级别具有不同的访问速度和大小。数据局部性模式的目标是利用这种内存层次结构来提高性能。


2. 缓存行:数据在CPU缓存和主内存之间以固定大小的块(通常为64字节)传输,称为缓存行。当CPU请求数据时,包含该数据的整个缓存行都会从主内存中获取。数据局部性模式确保一起访问的数据在内存中彼此靠近,增加它们在同一缓存行中的可能性,从而最小化缓存未命中。


3. 时间和空间局部性:数据局部性模式利用了局部性的两个原则:时间局部性和空间局部性。时间局部性是指程序在短时间内反复访问相同数据的倾向,而空间局部性是指程序访问附近数据的倾向。通过以最大限度地提高时间和空间局部性的方式组织数据,数据局部性模式可以提高缓存利用率和整体性能。


4. 数据结构和布局:为实现数据局部性,选择支持连续内存访问的适当数据结构和内存布局至关重要。例如,使用数组而不是链表可以提高数据局部性,因为数组将数据存储在连续的内存块中。同样,使用数组的结构(SoA)而不是结构的数组(AoS)可以通过确保只有相关数据一起访问来提高数据局部性,从而减少缓存未命中。


5. 预取和缓存优化:现代CPU具有硬件预取机制,可以预测将要访问的下一个数据并在需要之前将其提取到缓存中。通过使用数据局部性模式组织数据,您可以帮助硬件预取器更有效地工作。此外,基于软件的缓存优化(如缓存感知和缓存无关算法)可以通过在访问数据时考虑缓存大小和缓存行大小来进一步提高性能。

在游戏开发中,数据局部性模式对于实现高性能至关重要,特别是在性能约束严格的场景中,例如实时渲染、物理模拟和AI。通过以缓存友好的方式组织数据,开发人员可以最小化缓存未命中,减少内存访问延迟,并确保CPU花费更多时间处理数据,而不是等待从内存中获取数据。


UE4 中的应用例子


1. 游戏玩法能力系统(Game Ability System):UE4的游戏玩法能力系统旨在通过以缓存友好的方式组织数据来最大限度地提高数据局部性。GAS 将数据(组件)与逻辑(系统)分离,允许数据以连续的方式存储和访问。游戏玩法能力系统还利用面向数据的设计原则来确保高效的内存访问模式。


2. 尼亚加拉粒子系统:UE4中的尼亚加拉粒子系统采用面向数据的方法来优化数据局部性。它将粒子数据存储在连续的内存块中,并以SIMD(单指令,多数据)友好的方式处理它们,确保高效利用CPU缓存并提高性能。


3. 实例静态网格和分层实例静态网格:UE4的实例静态网格和分层实例静态网格旨在通过减少绘制调用和提高数据局部性来优化渲染性能。它们将相同网格的实例存储在连续的内存中,使GPU能够更高效地处理它们。


4. 细节层次(LOD)和Impostors:UE4使用LOD和Impostor技术通过根据物体与摄像机的距离减少物体的复杂性来优化渲染性能。这有助于提高数据局部性,因为需要从内存中获取的顶点和纹理较少,从而减少缓存未命中。


5. 内存管理和垃圾回收:UE4具有强大的内存管理系统,在分配和释放内存时考虑数据局部性。它还包括一个垃圾回收系统,有助于减少内存碎片,确保数据连续存储并高效访问。


这些只是UE4在其系统和功能中遵循数据局部性模式的一些例子。通过以缓存友好的方式组织数据并优化内存访问模式,UE4可以在游戏开发的各个方面(包括渲染、物理模拟和AI)实现高性能。


代码演示


可以在类似于 Tick Manager 的系统中实现数据局部性模式,我们可以将其称为“Tick Assembly”。这个想法是拥有一个集中管理多个对象的 tick 的系统,确保以缓存友好的方式访问数据。


以下是在 UE4 中创建一个简单的 Tick Assembly 系统的示例:


1. 首先,为可以由 Tick Assembly 执行的对象定义一个接口:


class ITickable

{

public:

    virtual void Tick(float DeltaTime) = 0;

};


2. 创建一个管理可 tick 对象的 Tick Assembly 类:


class FTickAssembly

{

public:

    void AddTickable(ITickable* Tickable)

    {

        Tickables.Add(Tickable);

    }

 

    void RemoveTickable(ITickable* Tickable)

    {

        Tickables.Remove(Tickable);

    }

 

    void Tick(float DeltaTime)

    {

        for (ITickable* Tickable : Tickables)

        {

            Tickable->Tick(DeltaTime);

        }

    }

 

private:

    TArray<ITickable*> Tickables;

};

[ 上下滑动查看代码 ]


3. 修改前面示例中的 AMyParticleActor 类以实现 ITickable 接口:


UCLASS()

class MYGAME_API AMyParticleActor : public AActor, public ITickable

{

    GENERATED_BODY()

 

public:

    AMyParticleActor();

 

    virtual void Tick(float DeltaTime) override;

 

private:

    FParticleSystem ParticleSystem;

};

void AMyParticleActor::Tick(float DeltaTime)

{

    UpdateParticleSystem(ParticleSystem, DeltaTime);

}

[ 上下滑动查看代码 ]


4. 创建一个管理 Tick Assembly 的自定义游戏模式类:


UCLASS()

class MYGAME_API AMyGameMode : public AGameModeBase

{

    GENERATED_BODY()

 

public:

    AMyGameMode();

 

    virtual void Tick(float DeltaTime) override;

 

    void RegisterTickable(ITickable* Tickable);

    void UnregisterTickable(ITickable* Tickable);

 

private:

    FTickAssembly TickAssembly;

};

AMyGameMode::AMyGameMode()

{

    PrimaryActorTick.bCanEverTick = true;

}

 

void AMyGameMode::Tick(float DeltaTime)

{

    Super::Tick(DeltaTime);

 

    TickAssembly.Tick(DeltaTime);

}

 

void AMyGameMode::RegisterTickable(ITickable* Tickable)

{

    TickAssembly.AddTickable(Tickable);

}

 

void AMyGameMode::UnregisterTickable(ITickable* Tickable)

{

    TickAssembly.RemoveTickable(Tickable);

}

[ 上下滑动查看代码 ]


5. 更新 AMyParticleActor 类以向 Tick Assembly 注册和注销自身:


AMyParticleActor::AMyParticleActor()

{

    PrimaryActorTick.bCanEverTick = false;

 

    // Initialize the particle system with some particles

    // ...

}

 

void AMyParticleActor::BeginPlay()

{

    Super::BeginPlay();

 

    AMyGameMode* MyGameMode = Cast<AMyGameMode>(GetWorld()->GetAuthGameMode());

    if (MyGameMode)

    {

        MyGameMode->RegisterTickable(this);

    }

}

 

void AMyParticleActor::EndPlay(const EEndPlayReason::Type EndPlayReason)

{

    AMyGameMode* MyGameMode = Cast<AMyGameMode>(GetWorld()->GetAuthGameMode());

    if (MyGameMode)

    {

        MyGameMode->UnregisterTickable(this);

    }

 

    Super::EndPlay(EndPlayReason);

}

[ 上下滑动查看代码 ]


在这个示例中,我们创建了一个 Tick Assembly 系统,以集中方式管理多个对象(在本例中为 AMyParticleActor 实例)的 tick。这种方法可以通过确保由可 tick 对象访问的数据以缓存友好的方式访问来提高数据局部性。然而,需要注意的是,这个示例相当简单,更复杂的场景可能需要额外的优化以确保最大的缓存效率。


异步加载模式

Asynchronous Loading


在游戏开发中,异步加载模式是指一种与游戏执行同时进行的加载游戏资源(如纹理、模型、声音或关卡数据)的技术,而不会导致游戏过程中出现明显的延迟或中断。这种模式对于维护流畅且无缝的游戏体验至关重要,特别是在大型开放世界游戏或资源加载较多的游戏中。


从深入的技术术语来解释,异步加载模式可以解释为以下几点:


1. 多线程:异步加载依赖于多线程的概念,即在单个进程中同时运行多个执行线程。在游戏开发的背景下,一个线程(主线程)负责处理游戏逻辑、渲染和用户输入,而一个或多个其他线程(工作线程)负责在后台加载资源。


2. 任务队列:主线程维护一个任务队列,这是一个包含需要加载的资源列表的数据结构。随着游戏的进行以及需要加载新资源,主线程将任务添加到队列中。工作线程不断检查任务队列以查找新任务以进行处理。一旦任务完成,工作线程将其从队列中删除并通知主线程。


3. 非阻塞 I/O:异步加载需要使用非阻塞 I/O 操作从磁盘读取资源数据。这意味着工作线程可以启动 I/O 操作并在等待 I/O 操作完成之前继续执行其他任务。当 I/O 操作完成时,工作线程可以处理加载的数据并相应地更新游戏状态。


4. 双缓冲:为确保主线程始终可以访问最新的资源数据,可以采用双缓冲技术。这涉及为每个资源维护两个单独的缓冲区:一个供主线程访问,另一个供工作线程更新。当工作线程完成资源加载时,它会更新辅助缓冲区,然后通知主线程交换缓冲区。


5. 同步:异步加载需要在主线程和工作线程之间进行仔细同步,以防止竞态条件和其他并发相关问题。这可以通过使用各种同步原语(如互斥锁、信号量或条件变量)来实现,以确保以安全且受控的方式访问共享资源。


6. 渐进式加载:在某些情况下,资源可以逐步加载,这意味着它们是按块或细节级别加载的,使游戏在仍在加载更高质量版本的资源时显示较低质量的资源版本。这有助于最大程度地减少加载时间的视觉影响,并为玩家提供更无缝的体验。


总之,游戏开发中的异步加载模式是一种允许在不中断游戏过程的情况下同时加载游戏资源的技术。这是通过使用多线程、非阻塞 I/O、任务队列、双缓冲、同步和渐进式加载来实现的。


UE4 中的应用例子


1. 异步资源加载:UE4 提供了在运行时使用蓝图中的 Async Load 节点或 C++ 中的 FStreamableManager 类异步加载资源的功能。这些方法允许您在后台加载资源,而不会阻塞游戏的主线程,确保流畅的游戏体验。


2. 关卡流式加载:关卡流式加载是 UE4 的一个功能,允许您在运行时异步加载和卸载子关卡(也称为流式关卡)。这对于开放世界游戏或关卡较大的游戏特别有用,因为它确保只将关卡的必要部分加载到内存中,从而减少内存使用并提高性能。


3. 多线程动画:UE4 的动画系统支持多线程,允许动画更新在与主游戏线程分开的线程上运行。这有助于将一些处理开销从主线程卸载,提高整体性能。


4. 纹理流式加载:UE4 的纹理流式加载系统根据纹理的可见性和与摄像机的距离动态加载和卸载纹理。这确保只将必要的纹理加载到内存中,减少内存使用并提高性能。纹理流式加载系统还支持异步加载,允许在后台加载纹理而不会导致游戏卡顿。


5. 音频流式加载:UE4 的音频系统支持长音频文件(如背景音乐或对话)的流式加载,以减少内存使用并提高性能。音频文件可以异步流式加载,允许它们在后台加载而不会阻塞主游戏线程。


UE4 中的这些内置系统和功能遵循异步加载模式,通过在后台加载资源和数据而不会导致中断或性能问题,帮助确保流畅且无缝的游戏体验。


代码演示


以下是一个简单的代码示例,说明了如何在 UE4 中使用 C++ 进行异步资源加载。在此示例中,我们将异步加载一个静态网格资源,并在加载完成后将其生成到游戏世界中。


// 头文件(MyAsyncLoader.h)

#pragma once

 

#include "CoreMinimal.h"

#include "GameFramework/Actor.h"

#include "MyAsyncLoader.generated.h"

 

UCLASS()

class MYPROJECT_API AMyAsyncLoader : public AActor

{

    GENERATED_BODY()

 

public:

    AMyAsyncLoader();

    virtual void BeginPlay() override;

 

private:

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Async Loading", meta = (AllowPrivateAccess = "true"))

    TSoftObjectPtr<UStaticMesh> MyStaticMeshAsset;

 

    void OnAssetLoaded();

};

 

// 源文件(MyAsyncLoader.cpp)

#include "MyAsyncLoader.h"

#include "Components/StaticMeshComponent.h"

#include "Engine/StreamableManager.h"

 

AMyAsyncLoader::AMyAsyncLoader()

{

    PrimaryActorTick.bCanEverTick = false;

    MyStaticMeshAsset = TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(TEXT("StaticMesh'/Game/MyStaticMesh.MyStaticMesh'")));

}

 

void AMyAsyncLoader::BeginPlay()

{

    Super::BeginPlay();

 

    // 检查资源是否已加载

    if (MyStaticMeshAsset.IsPending())

    {

        // 设置一个委托,在资源加载时调用

        FStreamableDelegate OnAssetLoadedDelegate;

        OnAssetLoadedDelegate.BindUObject(this, &AMyAsyncLoader::OnAssetLoaded);

 

        // 请求异步加载资源

        UStreamableManager& StreamableManager = UStreamableManager::Get();

        StreamableManager.RequestAsyncLoad(MyStaticMeshAsset.ToSoftObjectPath(), OnAssetLoadedDelegate);

    }

    else

    {

        // 如果资源已经加载,直接调用 OnAssetLoaded 函数

        OnAssetLoaded();

    }

}

 

void AMyAsyncLoader::OnAssetLoaded()

{

    // 检查资源是否已加载

    if (MyStaticMeshAsset.IsValid())

    {

        // 在游戏世界中生成静态网格

        UStaticMeshComponent* StaticMeshComponent = NewObject<UStaticMeshComponent>(this);

        StaticMeshComponent->SetStaticMesh(MyStaticMeshAsset.Get());

        StaticMeshComponent->RegisterComponent();

        StaticMeshComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);

    }

}

[ 上下滑动查看代码 ]


在此示例中,我们使用 TSoftObjectPtr 存储对静态网格资源的引用。然后我们使用 UStreamableManager 类请求异步加载资源。当资源加载时,将调用 OnAssetLoaded 函数,该函数将静态网格生成到游戏世界中。


请注意,此示例假定您有一个位于 "/Game/MyStaticMesh.MyStaticMesh" 的静态网格资源。确保将资源路径替换为指向所需资源的适当路径。


批处理模式

Batching


批处理是游戏开发中用于减少在屏幕上渲染多个对象所产生的开销的性能优化技术。它涉及将具有相似属性(如纹理、着色器或材质)的对象分组在一起,然后在单个绘制调用中渲染它们。这样可以将 GPU 所需的状态更改降至最低,从而显著提高渲染性能。


从深入的技术术语来解释批处理模式:


1. 排序和分组:在第一步中,根据对象的属性(如纹理、材质、着色器或其他渲染属性)对游戏对象进行排序和分组。这个过程有助于在渲染过程中最小化状态更改。


2. 生成顶点和索引缓冲区:一旦对象被分组,为每个批次生成顶点和索引缓冲区。顶点缓冲区是一个数组,包含批次中所有对象的顶点数据(如位置、颜色、纹理坐标和法线)。索引缓冲区是一个索引数组,定义了顶点绘制的顺序。


3. 合并几何体:批处理通常涉及将多个对象的几何体合并为一个网格。这可以通过合并批次中每个对象的顶点和索引来完成,并注意更新索引缓冲区以适应新的顶点位置。


4. 绘制调用:在生成并合并顶点和索引缓冲区后,发出单个绘制调用以渲染整个批次。这比为每个对象发出单独的绘制调用更有效,因为它将渲染所需的 GPU 管道设置开销降至最低。


5. 动态批处理:在某些情况下,对象可能在运行时被添加或删除,或者它们的属性可能发生变化。动态批处理是一种允许根据这些更改有效更新批次的技术。这通常涉及根据需要重新排序和重新分组对象,以及更新顶点和索引缓冲区以反映场景的新状态。


6. 实例化:与批处理相关的另一种技术是实例化,它涉及使用单个绘制调用渲染同一对象的多个实例。这对于共享相同几何体的对象(如树或人群中的角色)特别有用。实例化可以与批处理相结合,以进一步优化渲染性能。


总之,游戏开发中的批处理模式是通过最小化状态更改和绘制调用来优化渲染性能的关键技术。它涉及将具有相似属性的对象分组,生成和合并顶点和索引缓冲区,并发出单个绘制调用以渲染整个批次。这种方法可以显著提高游戏的帧速率和整体性能,特别是在渲染大量对象时。


UE4 中的应用例子


UE4提供了一些内置的高级技术,遵循批处理模式以优化渲染性能。这些技术包括:


1. 分层实例化静态网格(Hierarchical Instanced Static Meshes,HISM):这是实例化静态网格的扩展,允许您在树形层次结构中组织实例。这可以在渲染大量实例时提高性能,因为它使UE4能够更有效地执行视锥裁剪和遮挡裁剪。要使用HISM,只需在之前提供的代码示例中将UInstancedStaticMeshComponent替换为UHierarchicalInstancedStaticMeshComponent。


2. 网格距离场(Mesh Distance Fields):这是一种用于各种渲染优化的技术,如遮挡剔除、动态阴影和全局光照。通过在项目设置中启用网格距离场,UE4可以自动为您的静态网格生成距离场,并使用它们执行更高效的剔除和渲染。


3. 自动细节层次(LOD)和分层细节层次(HLOD):这些技术涉及创建简化版本的3D模型(LOD),并根据相机与对象的距离在它们之间切换。这可以显著减少需要渲染的多边形数量,提高性能。HLOD进一步将多个对象合并为一个网格,进一步减少绘制调用。


4. 冒牌精灵(Impostor Sprites):这种技术涉及将远离摄像机的3D对象渲染为2D精灵。这可以大大降低远距离对象的渲染复杂性,提高性能。UE4通过使用Simplygon插件提供了对冒牌精灵的内置支持。


5. GPU实例化:这是一种硬件级优化技术,允许GPU在单个绘制调用中渲染相同网格的多个实例,进一步降低渲染大量对象的开销。UE4会在可能的情况下自动使用GPU实例化,因此您不需要手动启用它。

这些只是UE4提供的遵循批处理模式的高级技术的一些例子。通过在项目中使用这些技术,您可以显著提高渲染性能,即使在处理复杂场景和大量对象时也能保持高帧速率。


代码演示


以下是一个简单的示例,说明如何在UE4中使用C++创建和使用实例化静态网格:


1. 首先,在您的C++类中包含必要的头文件:


#include "Components/InstancedStaticMeshComponent.h"

#include "Engine/StaticMesh.h"


2. 在您的类中创建一个UInstancedStaticMeshComponent:

UInstancedStaticMeshComponent* InstancedMeshComponent;


3. 在构造函数中初始化实例化静态网格组件:


// 构造函数

AMyActor::AMyActor()

{

    // 设置此actor每帧调用Tick()

    PrimaryActorTick.bCanEverTick = true;

 

    // 创建实例化静态网格组件

    InstancedMeshComponent = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("InstancedMeshComponent"));

    RootComponent = InstancedMeshComponent;

 

    // 为实例化静态网格组件设置静态网格

    static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Path/To/Your/StaticMesh"));

    if (MeshAsset.Succeeded())

    {

        InstancedMeshComponent->SetStaticMesh(MeshAsset.Object);

    }

}

[ 上下滑动查看代码 ]


4. 在不同的位置和旋转角度添加静态网格实例:

// 在不同的位置和旋转角度添加静态网格实例

FTransform InstanceTransform;

InstanceTransform.SetLocation(FVector(0.f, 0.f, 0.f));

InstanceTransform.SetRotation(FQuat(FRotator(0.f, 0.f, 0.f)));

InstancedMeshComponent->AddInstance(InstanceTransform);

 

InstanceTransform.SetLocation(FVector(100.f, 0.f, 0.f));

InstanceTransform.SetRotation(FQuat(FRotator(0.f, 90.f, 0.f)));

InstancedMeshComponent->AddInstance(InstanceTransform);

 

InstanceTransform.SetLocation(FVector(200.f, 0.f, 0.f));

InstanceTransform.SetRotation(FQuat(FRotator(0.f, 180.f, 0.f)));

InstancedMeshComponent->AddInstance(InstanceTransform);

[ 上下滑动查看代码 ]


在这个例子中,我们创建了一个实例化静态网格组件,为其设置了一个静态网格,然后在不同的位置和旋转角度添加了三个静态网格实例。这些实例将在一个绘制调用中渲染,这展示了UE4中的批处理技术。


请注意,这是一个简单的示例,UE4中可以使用更高级的技术,如分层实例化静态网格或动态批处理,以获得更好的性能。


享元模式

Flyweight


在游戏开发中,享元模式是一种用于最小化内存使用和优化性能的结构设计模式,通过共享和重用相似对象实现。当处理具有一些共享特征的大量对象时,此模式尤其有用。


从深入的技术术语来看,享元模式涉及以下组件:


1. 享元:这是一个接口或抽象类,为将共享的对象定义通用方法和属性。享元对象通常是不可变的,这意味着在创建后无法更改其内部状态。


2. 具体享元:这个类实现了享元接口,代表共享的对象。它存储对象的内在状态(共同特征)并提供操作它的方法。


3. 享元工厂:这个类负责创建和管理享元对象。它维护一个现有享元的池(通常是哈希映射或字典),并确保仅在池中不存在新对象时才创建新对象。如果请求现有对象,工厂将返回对该对象的引用。


4. 客户端:这是与享元工厂交互以请求和使用共享对象的类。客户端负责维护对象的外部状态(独特特征)并在需要时将其传递给享元。


在游戏开发中,享元模式可以应用于各种场景,例如:


1. 纹理和精灵管理:与加载多个相同纹理或精灵的实例相反,可以在多个游戏对象之间共享单个实例,从而减少内存使用并提高性能。


2. 粒子系统:在粒子系统中,具有相似属性(如颜色、形状和行为)的数千个粒子可以由单个享元对象表示,使系统更加高效。


3. 地形和环境:大型游戏世界通常包含重复元素,如树、岩石和建筑物。使用享元模式,这些元素可以重用,节省内存并加快渲染速度。


4. 人工智能和寻路:在具有众多AI控制角色的游戏中,它们的共享行为逻辑和寻路算法可以使用享元模式实现,以减少冗余和提高性能。


总之,享元模式是游戏开发中的一种强大技术,通过共享和重用相似对象来帮助最小化内存使用和优化性能。通过分离内在和外在状态,它允许高效管理具有共同特征的大量对象。


UE4 中的应用例子


1. 实例化静态网格:UE4允许使用实例化静态网格,通过单个绘制调用渲染相同静态网格的多个实例。这种技术通过在所有实例之间共享网格数据和材质来遵循享元模式,从而减少内存使用并提高渲染性能。


2. 分层细节级别(HLOD):HLOD是UE4中用于优化大型复杂场景渲染的技术。它将多个静态网格合并为一个网格,共享相同的材质和纹理,从而减少绘制调用次数和内存使用。这种方法通过在合并的网格之间共享内在状态(网格数据和材质)遵循享元模式。


3. 材质实例:UE4提供了材质实例,这是一种与父材质共享相同着色器和纹理数据的轻量级版本。材质实例允许快速更改材质属性,如颜色或粗糙度,无需创建全新的材质。这通过在所有材质实例之间共享内在状态(着色器和纹理数据)遵循享元模式。


4. 粒子系统:UE4的粒子系统(Cascade)也可以通过在多个粒子系统之间共享相同的粒子发射器属性、材质和纹理来实现享元模式。这减少了内存使用和渲染开销,尤其是在处理大量粒子时。


UE4中的这些内置技术展示了如何在游戏引擎中有效地使用享元模式来优化内存使用和提高性能。通过共享和重用相似对象及其内在状态,UE4可以高效地管理大量对象和复杂场景。


代码演示


在UE4中,可以使用材质实例来实现享元模式,材质实例是与父材质共享相同着色器和纹理数据的轻量级版本。以下是一个如何使用C++和蓝图在UE4中创建和使用材质实例的简单示例:


1. 在内容浏览器中创建一个新的材质。将其命名为“BaseMaterial”并打开它。


2. 在材质编辑器中,创建一个简单的材质设置,例如,基本颜色和粗糙度。要使这些属性可自定义,请添加两个标量参数节点(名为“BaseColor”和“Roughness”),并将它们连接到材质的适当输入。


3. 保存并关闭材质编辑器。


现在,让我们使用C++和蓝图创建材质实例:

C++:

在您的C++类中(例如,自定义Actor或Component),您可以创建并设置材质实例,如下所示:


#include "Materials/MaterialInstanceDynamic.h"

 

// ...

 

// 从内容浏览器加载基本材质

static ConstructorHelpers::FObjectFinder<UMaterial> BaseMaterial(TEXT("Material'/Game/BaseMaterial.BaseMaterial'"));

if (BaseMaterial.Succeeded())

{

    // 创建一个动态材质实例

    UMaterialInstanceDynamic* MaterialInstance = UMaterialInstanceDynamic::Create(BaseMaterial.Object, this);

 

    // 设置参数值

    MaterialInstance->SetScalarParameterValue("BaseColor", 0.5f);

    MaterialInstance->SetScalarParameterValue("Roughness", 0.8f);

 

    // 将材质实例分配给网格组件

    MeshComponent->SetMaterial(0, MaterialInstance);

} 

[ 上下滑动查看代码 ]


蓝图:


1. 在内容浏览器中,右键单击“BaseMaterial”并选择“创建材质实例”。将其命名为“MaterialInstance_BP”。


2. 打开“MaterialInstance_BP”,并根据需要设置“BaseColor”和“Roughness”的参数值。


3. 在您的蓝图类中(例如,自定义Actor或Component),添加一个网格组件并将“MaterialInstance_BP”分配给它。


此示例演示了如何在UE4中使用享元模式在多个实例之间共享材质的内在状态(着色器和纹理数据)。通过使用材质实例,您可以创建具有不同属性的材质变体,同时最小化内存使用并提高性能。


缓存

Caching


缓存是一种应用于优化的设计模式,旨在通过将昂贵计算或频繁访问数据的结果存储在内存中,以便稍后可以快速检索而无需重新计算或重新加载,从而提高系统的性能和响应速度。


以下介绍缓存相关概念及其实现:


1. 缓存数据结构:缓存通常使用允许快速访问的数据结构来实现,例如哈希表或字典。缓存存储键值对,其中键是数据的唯一标识符(例如,文件路径、资源ID或查询),值是实际数据或计算结果。


2. 缓存查找:当系统需要访问数据或执行计算时,首先检查缓存中是否已经存在相应的结果,方法是搜索相应的键。如果找到键,系统从缓存中检索值并直接使用,避免了重新计算或重新加载的需要。


3. 缓存更新:如果在缓存中找不到键,系统将执行计算或从原始源加载数据。获取结果后,系统将其与相应的键一起存储在缓存中以备将来使用。


4. 缓存驱逐:由于内存是有限的资源,因此需要管理缓存大小以避免使用过多的内存。一种常见的技术是使用缓存驱逐策略,该策略确定何时以及如何从缓存中删除项目。一些常见的缓存驱逐策略包括:

 - 最近最少使用(LRU):当缓存满时,删除最近最少访问的项目。
 - 先进先出(FIFO):当缓存满时,删除缓存中最旧的项目。
 - 存活时间(TTL):为缓存中的每个项目分配一个时间限制,并在时间限制到期时将其删除。


5. 缓存一致性:在某些情况下,由于原始数据源或底层计算的更改,缓存中存储的数据可能会过时或无效。为了维护缓存的一致性,系统需要检测并处理这种情况,方法是用新数据更新缓存或使受影响的缓存条目无效,从而在下次访问数据时强制重新计算或重新加载。


6. 缓存粒度:缓存粒度是指被缓存数据的大小和范围。根据系统的具体需求,可以在不同的粒度级别应用缓存,例如单个对象、对象组或整个场景或级别。选择适当的缓存粒度对系统的性能和内存使用具有重要影响。


UE4 中的应用例子


1. 纹理流(Texture streaming):UE4 使用纹理流系统动态管理纹理内存使用。该系统根据纹理的可见性和与摄像机的距离加载不同细节级别(LOD)的纹理。这减少了内存使用和渲染时间。流系统将纹理缓存在内存中,使引擎在需要时可以快速访问它们,而无需从磁盘重新加载它们。


2. 材质缓存(Material caching):UE4 采用着色器缓存系统优化渲染管线。当材质被编译时,其着色器排列被缓存并用于类似的材质,减少了重新编译的需要,提高了渲染性能。


3. 光照贴图缓存(Lightmap caching):UE4 使用预计算光照贴图存储场景的静态光照信息。光照贴图在光照烘焙过程中生成并缓存到磁盘。在运行时,引擎从磁盘加载光照贴图,并将其用于渲染静态光照,减少实时光照计算并提高性能。


4. 动画缓存:UE4 通过使用可配置网格组件支持动画缓存。这些组件允许开发人员缓存复杂骨骼网格动画的结果,并在同一网格的多个实例之间重用它们。这减少了多次重新计算相同动画的开销,提高了性能。


5. NavMesh 缓存:UE4 使用名为 NavMesh 的导航系统处理寻路和 AI 导航。NavMesh 在开发过程中预先计算并缓存到磁盘。在运行时,引擎从磁盘加载 NavMesh 数据,使 AI 代理能够执行寻路查询,而无需实时计算。


6. Instanced Static Mesh(ISM)和 Hierarchical Instanced Static Mesh(HISM):UE4 中的这些组件允许开发人员使用单个绘制调用渲染同一静态网格的多个实例。引擎缓存每个实例的变换和其他数据,减少渲染单个网格的开销并提高性能。


7. 共享序列化(SharedSerialization):在UE4中,共享序列化(SharedSerialization)通过减少多个连接之间的冗余序列化来优化跨多个连接的数据序列化和复制过程。这在多人游戏中尤其有用,因为许多玩家连接到服务器,服务器需要向多个客户端发送相同的数据。


代码示例


以下是一个简单的C++代码示例,演示如何实现共享序列化。


#include 

#include 

#include 

#include 

 

class SharedBuffer {

public:

    void store(const std::string& key, const std::string& data) {

        buffer[key] = data;

    }

 

    std::string get(const std::string& key) {

        return buffer[key];

    }

 

private:

    std::map<std::string, std::string> buffer;

};

 

class SharedData {

public:

    SharedData(const std::string& key, const std::shared_ptr<SharedBuffer>& sharedBuffer)

        : key(key), sharedBuffer(sharedBuffer) {}

 

    std::string getData() {

        return sharedBuffer->get(key);

    }

 

private:

    std::string key;

    std::shared_ptr<SharedBuffer> sharedBuffer;

};

 

void serializeData(const std::string& data, std::shared_ptr<SharedBuffer> sharedBuffer) {

    // 执行序列化

    std::string serializedData = "Serialized_" + data;

 

    // 将序列化数据存储在共享缓冲区中

    sharedBuffer->store(data, serializedData);

}

 

int main() {

    // 创建一个共享缓冲区

    auto sharedBuffer = std::make_shared<SharedBuffer>();

 

    // 序列化数据并将其存储在共享缓冲区中

    std::string data = "ExampleData";

    serializeData(data, sharedBuffer);

 

    // 为多个连接创建共享数据对象

    SharedData connection1(data, sharedBuffer);

    SharedData connection2(data, sharedBuffer);

 

    // 从连接访问序列化数据

    std::cout << "Connection 1: " << connection1.getData() << std::endl;

    std::cout << "Connection 2: " << connection2.getData() << std::endl;

 

    return 0;

}

[ 上下滑动查看代码 ]


在此示例中,我们有一个`SharedBuffer`类,它将序列化数据存储在映射中,以及一个表示使用共享缓冲区的连接的`SharedData`类。`serializeData`函数执行序列化并将序列化数据存储在共享缓冲区中。


在`main`函数中,我们创建一个共享缓冲区并序列化数据,将其存储在共享缓冲区中。然后,我们创建两个表示使用相同共享缓冲区的两个连接的`SharedData`对象。当从这些连接访问序列化数据时,它们都使用存储在共享缓冲区中的数据,避免了冗余序列化。


此示例只是个简单演示,UE4 源码中有 SharedSerialization 的完整实现。


<文中的示例代码均未经过测试,仅供参考>


参考文献:

https://gameprogrammingpatterns.com/contents.html

  互动有奖!  

我们将在2023年8月25日抽出3名幸运粉丝,分别送出100Q币。参与方式如下:

①点击文末右下角的“在看”
②评论留言
③发送关键词“打卡”至公众号后台完成验证

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存