组件复制

虚幻引擎 4 支持组件复制。尽管使用起来简单直观,但它却有些不同寻常:大多数组件都不会复制。其多数游戏逻辑都是在 Actor 类和组件中完成,而它们 通常只代表了构成 Actor 的零散部分。实际复制的是 Actor 中的游戏逻辑,而这样做的结果,有时会调用/更改组件。 但在有些情况下,组件本身的属性或事件必须要直接复制。这份指南将介绍如何使用这一功能。

组件会作为其所属 Actor 的一部分进行复制。Actor 仍然掌管角色、优先级、相关性、剔除等方面的工作。一旦复制了 Actor,它就可以复制自身组件。这些组件 可以按 Actor 的方式复制属性和 RPC。组件必须以 Actor 的方式实施 ::GetLifetimeReplicatedProps (...) 函数。

组件复制中涉及两大类组件。一种是随 Actor 一起创建的静态组件。也就是说,在客户端或服务器上生成 所属 Actor 时,这些组件也会同时生成,与组件是否被复制无关。服务器不会告知客户端显式生成这些组件。 在此背景下,静态组件是作为默认子对象在 C++ 构造函数中创建,或是在蓝图编辑器的组件模式中创建。静态组件无需通过复制存在于 客户端;它们将默认存在。只有在属性或事件需要在服务器和客户端之间自动同步时,才需要进行复制。

动态组件是在运行时在服务器上生成的组件种,其创建和删除操作也将被复制到客户端。它们的运行方式与 Actor 极为一致。与静态组件不同, 动态组件需通过复制的方式存在于所有客户端。另外,客户端可以生成自己的本地非复制组件。这适合于很多种情形。只有当那些在服务器上触发的 属性或事件需要自动同步到客户端时,才会出现复制行为。

使用方法

在组件上设置属性和 RPC 的过程与 Actor 并无区别。将一个类设置为具有复本后,这些组件的实际实例也必须经过设置后才能复制。

C++

要进行组件复制,只需调用 AActorComponent::SetIsReplicated(true) 即可。如果您的组件是一个默认子对象,就应当在生成组件之后通过类构造函数来完成此调用。

例:

ACharacter::ACharacter()
{
    // Etc...

    CharacterMovement = CreateDefaultSubobject<UMovementComp_Character>(TEXT("CharMoveComp"));
    if (CharacterMovement)
    {
        CharacterMovement->UpdatedComponent = CapsuleComponent;

        CharacterMovement->GetNavAgentProperties()->bCanJump = true;
        CharacterMovement->GetNavAgentProperties()->bCanWalk = true;
        CharacterMovement->SetJumpAllowed(true);
        CharacterMovement->SetNetAddressable(); // Make DSO components net addressable
        CharacterMovement->SetIsReplicated(true); // Enable replication by default

    }
}

蓝图

要进行静态蓝图组件复制,只需在组件默认设置中切换 Replicates 布尔变量。同样,只有当组件中拥有需要复制的属性或事件时,才需要 进行此操作。静态组件需要在客户端和服务器上隐式创建。

components_checkbox.png

需要注意的是,并非所有组件都会如此显示,必须要支持某种复制形式才会显示。

要通过动态生成的组件来实现这一点,可以调用 SetIsReplicated 函数:

components_function.png

时间轴

时间轴必须通过其属性中的 Replicated 选项来启用复制。这会将服务器控制的运行位置、速率和方向复制到客户端。这是一种基本的 实施,可以根据需求的变化而进行演变。大多数时间轴都无需复制。和所有游戏对象复本一样,时间轴复本只应当在服务器上直接 操作 (start/stop etc)。客户端只应当查看运行位置的复本,而不应尝试改变时间轴本身。在进行复制更新的间歇,客户端将推测 运行位置。

带宽消耗

复制组件时的资源消耗量是比较低的。要复制的 Actor 中的每个组件都需要添加一个额外的 NetGUID(4 字节)“标头”和一个大约 1 字节的“标脚”(footer) 及其属性。在 CPU 层面上,基于 Actor 的属性复制与基于组件的复制之间应当有一个最小差异。

一般性的子对象复制

对这个问题,我们可以更进一步:所有 Actor 子对象都可以复制,而不只限于组件。

这个结论的应用范围很窄,但在某些情形下却非常实用。实现这一目的所用的接口需要在类的层面上定义:

/** FActory 方法,用于对模板化 TobjectReplicator 类进行实例化,以便实现子对象复制 */
virtual class FObjectReplicatorBase * InstantiateReplicatorForSubObject(UClass *SubobjClass);

/** 能让 Actor 在其 Actor 通道上复制子对象的方法 */
virtual bool ReplicateSubobjects(class UActorChannel *Channel, class FOutBunch *Bunch, FReplicationFlags *RepFlags);

/** 通过复制来动态创建新的子对象时,在 Actor 上进行调用 */
virtual void OnSubobjectCreatedFromReplication(UObject *NewSubobject);

对于希望复制非 ActorComponent 子对象的类,应当实施以上三种方法。

使用情形

这是一种很有用的手段,因为它能在 Actor 通道的层面上使用 UObject 和多态(polymorphism)。之前用于复杂数据结构的复制方法只适合那些 在 Actor 类中对类型进行静态定义的结构。利用子对象复制,您可以享受诸多优势,例如建立一个道具栏系统,使其中的每个物品作为一个从基本道具栏类扩展而来的类, 也可以进行完整复制,同时无需让这些项成为 Actor(资源负担太大)。

优化

如果有很多子对象需要复制,Actor 只需了解哪些子对象(如存在)最近发生过变化且需要复制,从而节省了大量时间。这需要通过访问器(accessor)函数来持续跟踪子对象的更改情况。 这一过程所用的接口位于 AActorChannel 中:

bool KeyNeedsToReplicate(int32 ObjID, int32 RepKey);

该函数应当由 Actor 在其 ::ReplicateSubobjects 实施中调用。Actor 类可以设置一个任意的对象 ID 和复制键,供复制系统跟踪每个客户端。当 ReplicateSubobjects 返回 true 时,调用方应当在使用该对象 ID 接受跟踪的任意子对象上调用 ReplicateSubobjects。 相关示例请查看 AQAInventory::ReplicateSubobjects。一定不要忘记,对象 ID 和复制键完全是任意指定的。对象 ID 仅用于引用“事情”。它可以是整个子对象列表、部分列表或单个对象。复制键同样可以任意指定 - 它可以是一个在对象 ID 跟踪变化时递增的计数器。这里讨论的优化问题完全是选读内容。