UE4基础篇十二: 网络同步

25 篇文章 5 订阅 ¥39.90 ¥99.00

一、理解Controller、GameState 和 PlayerState

原文链接:大钊:《InsideUE4》GamePlay架构(五)Controller

1.1 思考:哪些逻辑应该写在Controller中?

如同当初我们在思考Actor和Component的逻辑划分一样,我们也得要划分哪些逻辑应该放在Pawn中,哪些应该放在Contrller中。上文我们也说过,Pawn也可以接收用户输入事件,所以其实只要你愿意,你甚至可以脱离Controller做一个特立独行的Pawn。那么在那些时候需要Controller?哪些逻辑应该由Controller掌管呢?可以从以下一些方面考虑:

  • 从概念上,Pawn本身表示的是一个“能动”的概念,重点在于“能”。而Controller代表的是动到“哪里”的概念,重点在于“方向”。所以如果是一些Pawn本身固有的能力逻辑,如前进后退、播放动画、碰撞检测之类的就完全可以在Pawn内实现;而对于一些可替换的逻辑,或者智能决策的,就应该归Controller管辖。
  • 从对应上来说,如果一个逻辑只属于某一类Pawn,那么其实你放进Pawn内也挺好。而如果一个逻辑可以应用于多个Pawn,那么放进Controller就可以组合应用了。举个例子,在战争游戏中,假设说有坦克和卡车两种战车(Pawn),只有坦克可以开炮,那么开炮这个功能你就可以直接实现在坦克Pawn上。而这两辆战车都有的自动寻找攻击目标功能,就可以实现在一个Controller里。
  • 从存在性来说,Controller的生命期比Pawn要长一些,比如我们经常会实现的游戏中玩家死亡后复活的功能。Pawn死亡后,这个Pawn就被Destroy了,就算之后再Respawn创建出来一个新的,但是Pawn身上保存的变量状态都已经被重置了。所以对于那些需要在Pawn之外还要持续存在的逻辑和状态,放进Controller中是更好的选择。

1.2 APlayerState

我们上文提到过Controller希望也能有一些记忆,保存住一些游戏状态。那么到底应该怎么保存呢?AController自身当然可以添加成员变量来保存,这些变量也可以网络复制,一般来说也够用。但是终究还是遗忘了一个最重要的数据状态。整个游戏世界构建起来就是为了玩家服务的,而玩家在游戏过程中,肯定要存取产生一些状态。而Controller作为游戏业务逻辑最重要的载体,势必要和玩家的状态打交道。所以Controller如果可以动态存取玩家的状态就会大为方便了。因此我们会在Controller中见到:

/** PlayerState containing replicated information about the player using this controller (only exists for players, not NPCs). */
    UPROPERTY(replicatedUsing=OnRep_PlayerState, BlueprintReadOnly, Category="Controller")
    class APlayerState* PlayerState;

而APlayerState的继承体系是:

至于为啥APlayerState是从AActor派生的AInfo继承下来的,我们聪明的读者相信也能猜得到了,所以也就不费口舌论证了。无非就是贪图AActor本身的那些特性以网络复制等。而AInfo们正是这种不爱表现的纯数据书呆子们的大本营。而这个PlayerState我们可以通过在GameMode中配置的PlayerStateClass来自动生成。
注意,这个APlayerState也理所当然是生成在Level中的,跟Pawn和Controller是平级的关系,Controller里只不过保存了一个指针引用罢了。注释里说的PlayerState只为players存在,不为NPC生成,指的是PlayerState是跟UPlayer对应的,换句话说当前游戏有多少个真正的玩家,才会有多少个PlayerState,而那些AI控制的NPC因为不是真正的玩家,所以也不需要创建生成PlayerState。但是UE把PlayerState的引用变量放在了Controller一级,而不是PlayerController之中,说明了其实AIController也是可以设置读取该变量的。一个AI智能能够读取玩家的比分等状态,有了更多的信息来作决策,想来也没有什么不对嘛。
Controller和网络的结合很紧密,很多机制和网络也非常强关联,但是在这里并不详细叙述,这里先可以单纯理解成Controller也可以当作玩家在服务器上的代理对象。把PlayerState独立构成一个Actor还有一个好处,当玩家偶尔因网络波动断线,因为这个连接不在了,所以该Controller也失效了被释放了,服务器可以把对应的该PlayerState先暂存起来,等玩家再紧接着重连上了,可以利用该PlayerState重新挂接上Controller,以此提供一个比较顺畅无缝的体验。至于AIController,因为都是运行在Server上的,Client上并没有,所以也就无所谓了。

1.3 思考:哪些数据应该放在PlayerState中?

从应用范围上来说,PlayerState表示的是玩家的游玩数据,所以那些关卡内的其他游戏数据就不应该放进来(GameState是个好选择),另外Controller本身运行需要的临时数据也不应该归PlayerState管理。而玩家在切换关卡的时候,APlayerState也会被释放掉,所有PlayerState实际上表达的是当前关卡的玩家得分等数据。这样,那些跨关卡的统计数据等就也不应该放进PlayerState里了,应该放在外面的GameInstance,然后用SaveGame保存起来。

1.4 GameState

上回说到了APlayerState用来保存玩家的游戏数据,那么同样的,对于一场游戏,也需要一个State来保存当前游戏的状态数据,比如任务数据等。跟APlayerState一样,GameState也选择从AInfo里继承,这样在网络环境里也可以Replicated到多个Client上面去。

比较简单,第一个MatchState和相关的回调就是为了在网络中传播同步游戏的状态使用的(记得GameMode在Client并不存在,但是GameState是存在的,所以可以通过它来复制),第二部分是玩家状态列表,同样的如果在Client1想看到Client2的游戏状态数据,则Client2的PlayerState就必须广播过来,因此GameState把当前Server的PlayerState都收集了过来,方便访问使用。
关于使用,开发者可以自定义GameState子类来存储本GameMode的运行过程中产生的数据(那些想要replicated的!),如果是GameMode游戏运行的一些数据,又不想要所有的客户端都可以看到,则也可以写在GameMode的成员变量中。重复遍,PlayerState是玩家自己的游戏数据,GameInstance里是程序运行的全局数据。

二、多人游戏编程快速指南

原文地址:官方文档 多人游戏编程快速指南

2.1 前置主题

为了能够理解并使用此页面中的内容,请确保您已掌握以下主题:

开发多人游戏的游戏进程需要在游戏的 Actor 中实现 复制。还必须设计特定于 服务器(充当游戏会话的主机)或 客户端(代表连接到会话的玩家)的功能。本分步指南将介绍创建简单多人游戏进程的流程,包括以下内容:

  • 如何向基本Actor添加复制。
  • 如何利用网络游戏中的 移动组件
  • 如何向 变量 添加复制。
  • 如何在变量更改时使用 RepNotify
  • 如何在C++环境下使用 远程过程调用(RPC)
  • 如何检查Actor的 网络角色,以过滤在函数中执行的调用。

最终将形成第三人称游戏,玩家可以向对方投掷爆炸性投射物。我们的主要工作是创建投射物并向角色添加伤害响应。

2.2 角色移动组件

大多数模板中的Pawn和角色默认启用了复制。在我们的示例中,ThirdPersonCharacter已拥有会自动复制移动的 角色移动组件

角色移动组件

化妆组件,如角色的 骨架网格体 及其 动画蓝图,不会被复制。但与游戏进程和移动相关的变量(如角色的速度)则会被复制,且动画蓝图会在变量更新时读取这些变量。因此,角色在每个客户端上的副本都会更新其视觉呈现,只要游戏进程变量准确更新,这种更新就是一致的。同样,游戏进程框架自动处理角色在玩家出生点的生成操作,并向角色分配 玩家控制器

若使用此项目启动服务器,并有客户端加入该服务器,这就已经是一个正常的多人游戏。但玩家仅可让其游戏化身移动和跳跃。因此要创建一些其他的多人游戏进程。

2.3.使用RepNotify复制玩家的生命值

玩家需要生命值,才能在游戏进程受到伤害。该值需要复制,使所有客户端都拥有各玩家生命值的同步信息,并需要在玩家受到伤害时向其提供反馈。本节将演示如何在不依赖RPC的情况下,利用RepNotify同步变量的所有必要更新。

注意,'Role' 已经被相应替换为 'GetLocalRole()' 和 'GetRemoteRole()'。你会注意到下述小节中有些地方之前使用的是 'Role',请注意更改。

  1. 打开 ThirdPersonMPCharacter.h。在 protected 下添加以下属性:
/** 玩家的最大生命值。这是玩家的最高生命值,也是出生时的生命值。*/
UPROPERTY(EditDefaultsOnly, Category = "Health")
float MaxHealth;

/** 玩家的当前生命值。降到0就表示死亡。*/
UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)
float CurrentHealth;

/** RepNotify,用于同步对当前生命值所做的更改。*/
UFUNCTION()
void OnRep_CurrentHealth();

我们要严格控制玩家生命值的变化,因此这些生命值有以下约束:

    • MaxHealth 不复制,仅可在默认值中编辑。此值是针对所有玩家预先计算得出的,不会更改。
    • CurrentHealth 复制,但无法在蓝图的任何地方编辑或访问。
    • MaxHealth 和 CurrentHealth 都是 受保护 的,以防被外部C++类访问。仅可在 AThirdPersonMPCharacter 或其派生类中进行修改。

这降低了实时游戏进程中玩家的 CurrentHealth 或 MaxHealth 发生意外更改的风险。在稍后的步骤中,我们会提供其他公共函数,用于获取和修改这些值。
Replicated 说明符在服务器上启用Actor的副本,以在变量值更改时,将该变量值复制到所有连接的客户端。ReplicatedUsing 也有同样的功能,但还能设置 RepNotify 函数,此函数将在客户端成功接收复制数据时触发。将基于此变量的更改,使用 OnRep_CurrentHealth 执行各个客户端的更新。

2.打开 ThirdPersonMPCharacter.cpp。在顶部的 #include "GameFramework/SpringArmComponent.h" 一行下添加以下 #include 语句:

//ThirdPersonMPCharacter.cpp
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"

它们提供用于复制变量以及访问 GEngine 中的 AddOnscreenDebugMessage 函数(用于将消息输出至屏幕)的必要功能。

3. 在 ThirdPersonMPCharacter.cpp 中,在构造函数底部添加以下代码:

//ThirdPersonMPCharacter.cpp
//初始化玩家生命值
MaxHealth = 100.0f;
CurrentHealth = MaxHealth;

这将初始化玩家的生命值。创建此角色的新副本时,角色当前生命值将设为其最大生命值。

7.在ThirdPersonMPCharacter.cpp中,添加以下实现:

// ThirdPersonMPCharacter.cpp
void AThirdPersonMPCharacter::OnHealthUpdate()
{
    //客户端特定的功能
    if (IsLocallyControlled())
    {
        FString healthMessage = FString::Printf(TEXT("You now have %f health remaining."), CurrentHealth);
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);

        if (CurrentHealth <= 0)
        {
            FString deathMessage = FString::Printf(TEXT("You have been killed."));
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, deathMessage);
        }
    }

    //服务器特定的功能
    if (GetLocalRole() == ROLE_Authority)
    {
        FString healthMessage = FString::Printf(TEXT("%s now has %f health remaining."), *GetFName().ToString(), CurrentHealth);
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);
    }

    //在所有机器上都执行的函数。 
    /*  
        因任何因伤害或死亡而产生的特殊功能都应放在这里。 
    */
}

2.4.使玩家响应伤害

现在我们已实现玩家的生命值,接下来需要想办法在此类之外修改玩家生命值。

  1. 在 ThirdPersonMPCharacter.h 中,在 Public 下添加以下函数声明:
// ThirdPersonMPCharacter.h
/** 最大生命值的取值函数。*/
UFUNCTION(BlueprintPure, Category="Health")
FORCEINLINE float GetMaxHealth() const { return MaxHealth; } 

/** 当前生命值的取值函数。*/
UFUNCTION(BlueprintPure, Category="Health")
FORCEINLINE float GetCurrentHealth() const { return CurrentHealth; }

/** 当前生命值的存值函数。将此值的范围限定在0到MaxHealth之间,并调用OnHealthUpdate。仅在服务器上调用。*/
UFUNCTION(BlueprintCallable, Category="Health")
void SetCurrentHealth(float healthValue);

/** 承受伤害的事件。从APawn覆盖。*/
UFUNCTION(BlueprintCallable, Category = "Health")
float TakeDamage( float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser ) override;

float AThirdPersonMPCharacter::TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float damageApplied = CurrentHealth - DamageTaken;
    SetCurrentHealth(damageApplied);
    return damageApplied;
}

2.5.使用复制创建投射物

  1. 在虚幻编辑器中,使用 文件(File) 菜单或 内容浏览器 创建 新C++类

  1. 在 选择父类(Choose Parent Class) 菜单中,选择 Actor 作为父类,并单击 下一步(Next)


单击图像以查看大图。

  1. 在 命名新Actor(Name Your New Actor) 菜单,将类命名为 ThirdPersonMPProjectile,然后单击 创建类(Create Class)


单击图像以查看大图。

打开 ThirdPersonMPProjectile.h,并将以下代码添加到类定义中的 public 下:

// ThirdPersonMPProjectile.h
// 用于测试碰撞的球体组件。
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
class USphereComponent* SphereComponent;

// 用于提供对象视觉呈现效果的静态网格体。
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
class UStaticMeshComponent* StaticMesh;

// 用于处理投射物移动的移动组件。
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
class UProjectileMovementComponent* ProjectileMovementComponent;

// 在投射物撞击其他对象并爆炸时使用的粒子。
UPROPERTY(EditAnywhere, Category = "Effects")
class UParticleSystem* ExplosionEffect;

//此投射物将造成的伤害类型和伤害。
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
TSubclassOf<class UDamageType> DamageType;

//此投射物造成的伤害。
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage")
float Damage;

void AThirdPersonMPProjectile::Destroyed()
{
    FVector spawnLocation = GetActorLocation();
    UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
}

UFUNCTION(Category="Projectile")
void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

void AThirdPersonMPProjectile::OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{   
    if ( OtherActor )
    {
        UGameplayStatics::ApplyPointDamage(OtherActor, Damage, NormalImpulse, Hit, GetInstigator()->Controller, this, DamageType);
    }

    Destroy();
}

2.5.发射投射物

  1. 打开 编辑器(Editor),然后单击屏幕顶部的 编辑(Edit) 下拉菜单,并打开 项目设置(Project Settings)

  1. 在 引擎(Engine) 部分中,单击 输入(Input) 打开项目的输入设置。展开 绑定(Bindings) 部分,添加新条目。将它命名为"Fire",并选择 鼠标左键 作为此Actor的绑定键。


单击图像以查看大图。

在 ThirdPersonMPCharacter.cpp 中,在 #include "Engine/Engine.h" 一行下方添加以下 #include

UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile")
TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass;

/** 射击之间的延迟,单位为秒。用于控制测试发射物的射击速度,还可防止服务器函数的溢出导致将SpawnProjectile直接绑定至输入。*/
UPROPERTY(EditDefaultsOnly, Category="Gameplay")
float FireRate;

/** 若为true,则正在发射投射物。*/
bool bIsFiringWeapon;

/** 用于启动武器射击的函数。*/
UFUNCTION(BlueprintCallable, Category="Gameplay")
void StartFire();

/** 用于结束武器射击的函数。一旦调用这段代码,玩家可再次使用StartFire。*/
UFUNCTION(BlueprintCallable, Category = "Gameplay")
void StopFire();  

/** 用于生成投射物的服务器函数。*/
UFUNCTION(Server, Reliable)
void HandleFire();

/** 定时器句柄,用于提供生成间隔时间内的射速延迟。*/
FTimerHandle FiringTimer;

void AThirdPersonMPCharacter::StartFire()
{
    if (!bIsFiringWeapon)
    {
        bIsFiringWeapon = true;
        UWorld* World = GetWorld();
        World->GetTimerManager().SetTimer(FiringTimer, this, &AThirdPersonMPCharacter::StopFire, FireRate, false);
        HandleFire();
    }
}

void AThirdPersonMPCharacter::StopFire()
{
    bIsFiringWeapon = false;
}

void AThirdPersonMPCharacter::HandleFire_Implementation()
{
    FVector spawnLocation = GetActorLocation() + ( GetControlRotation().Vector()  * 100.0f ) + (GetActorUpVector() * 50.0f);
    FRotator spawnRotation = GetControlRotation();

    FActorSpawnParameters spawnParameters;
    spawnParameters.Instigator = GetInstigator();
    spawnParameters.Owner = this;

    AThirdPersonMPProjectile* spawnedProjectile = GetWorld()->SpawnActor<AThirdPersonMPProjectile>(spawnLocation, spawnRotation, spawnParameters);
}

2.6.测试游戏

  1. 在编辑器中打开项目。单击 编辑(Edit) 下拉菜单,并打开 编辑器首选项(Editor Preferences)

  1. 导航至 关卡编辑器(Level Editor) 部分,并单击 运行(Play) 菜单。找到 多人游戏选项(Multiplayer Options),并将 玩家数量(Number of Players) 更改为2。


单击图像以查看大图。

按 运行(Play) 按钮。在编辑器中运行(Play in Editor)(PIE) 主窗口将作为服务器启动多人游戏会话,之后第二个PIE窗口打开,作为客户端连接。

三、UE4 网络架构

原文:彻底掌握UE4网络-02网络构架_哔哩哔哩_bilibili

3.1 Server-Client架构

1.一个服务器,一个或多个客户端

2. 不能信任客户端,所有重要信息都要通过服务器验证

3. Listen Server(服务器上有玩家)&Dedicated Server(独立服务器)

4. 我们是客户端时,是在操作本地角色还是远程角色(客户端先行,服务器验证)

GameMode: 只存在于服务端

GameState/Pawn_Server/Pawn_A/Pawn_B/PlayerState_Server/PlayerState_A/PlayerState_B存在于所有客户端

PlayerController客户端只存在自身,服务器是都存在

UI界面/GameInstance 独立的 都自存在本地,

在关卡蓝图中

加上只在服务器端生成Actor, 并且勾上复制,便可在所有客户端生成同一个对象

3.2 属性复制:

蓝图

在关卡蓝图中添加如下代码,

如果Spawn Actor 不加复制,则只有在服务器生成,但是字符串会打印出现在每个客户端,查看下面的代码,printf 进行同步

void UKismetSystemLibrary::PrintString(const UObject* WorldContextObject, const FString& InString, bool bPrintToScreen, bool bPrintToLog, FLinearColor TextColor, float Duration)
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) // Do not Print in Shipping or Test

	UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull);
	FString Prefix;
	if (World)
	{
		if (World->WorldType == EWorldType::PIE)
		{
			switch(World->GetNetMode())
			{
				case NM_Client:
					// GPlayInEditorID 0 is always the server, so 1 will be first client.
					// You want to keep this logic in sync with GeneratePIEViewportWindowTitle and UpdatePlayInEditorWorldDebugString
					Prefix = FString::Printf(TEXT("Client %d: "), GPlayInEditorID);
					break;
				case NM_DedicatedServer:
				case NM_ListenServer:
					Prefix = FString::Printf(TEXT("Server: "));
					break;
				case NM_Standalone:
					break;
			}
		}
	}

复制通知:

3.3 Ownership所有权:

往上追溯,查看Owner就是所有权,比如Pawn 上面是PlayerController, PlayerController上面是Connection,

一些无关紧要的对象 比如NPC没有所有权

为什么Ownership重要?

连接所有权会在actor复制期间使用,用于确定哪些连接可以获取更新,对于那些将bOnlyRelevantToOwner设置为true的actor,只有拥有此actor的连接才会接收这个actor的属性更新,默认情况下,所有PlayerController都设置了此标志,正因为如此,客户端才会收到他们拥有的PlayerController的更新,这样做是出于多种原因,其中最主要的是防止玩家作弊和提高效率。

所以Connection只能获取到自己PlayerController信息,不能获取其他玩家的信息

涉及所有者的Actor属性复制条件,连接所有权具有重要意义,例如,当使用COND_OnlyOwner时,只有此actor的所有者才会收到这些属性更新, 如下属性赋值可以添加连接权

3.4 ActorRole:

Authority:权威者

Simulated Proxy: 模拟代理

Autonomous Proxy: 自主代理

Role和RemoteRole 区别,

比如Server 里 蓝色的Role 是Authority 那么RemoteRole在ClientA 是Simulated,在ClientB是Autonomous 说明这个引擎实例负责将此actor复制到远程连接

对于绿色的actor, 在ClientA Role是Simulated 那么RemoteRole是Authority

RPC:

EventPossessed只在服务器执行

四、《Exploring in UE4》关于网络同步的理解与思考 (转载)

原文链接:Jerish:《Exploring in UE4》关于网络同步的理解与思考[概念理解]

4.1. 关于Actor与其所属连接
UE4官网关于网络链接这一块其实已经将的比较详细了,不过有一些内容没有经验的读者看起来可能还是比较吃力。

按照官网的顺序,我一点点给出我的分析与理解。首先,大家要简单了解一些客户端的连接过程。

主要步骤如下:

  • 1.客户端发送连接请求
    2.服务器将在本地调用 AGameMode::PreLogin。这样可以使 GameMode 有机会拒绝连接。
    3.如果服务器接受连接,则发送当前地图
    4.服务器等待客户端加载此地图,客户端如果加载成功,会发送Join信息到服务器
    5.如果接受连接,服务器将调用 AGameMode::Login该函数的作用是创建一个PlayerController,可用于在今后复制到新连接的客户端。成功接收后,这个PlayerController 将替代客户端的临时PlayerController (之前被用作连接过程中的占位符)。
    此时将调用 APlayerController::BeginPlay。应当注意的是,在此 actor 上调用RPC 函数尚存在安全风险。您应当等待 AGameMode::PostLogin 被调用完成。
    6.如果一切顺利,AGameMode::PostLogin 将被调用。这时,可以放心的让服务器在此 PlayerController 上开始调用RPC 函数。

那么这里面第5点需要重点强调一下。我们知道所谓连接,不过就是客户端连接到一个服务器,在维持着这个连接的条件下,我们才能真正的玩“网络游戏”。通常,如果我们想让服务器把某些特定的信息发送给特定的客户端,我们就需要找到服务器与客户端之间的这个连接。这个链接的信息就存储在PlayerController的里面,而这个PlayerController不能是随随便便创建的PlayerController,一定是客户端第一次链接到服务器,服务器同步过来的这个PlayerController(也就是上面的第五点,后面称其为拥有连接的PlayerController)。进一步来说,这个Controller里面包含着相关的NetDriver,Connection以及Session信息。

对于任何一个Actor(客户端上),他可以有连接,也可以无连接。一旦Actor有连接,他的Role(控制权限)就是ROLE_AutonomousProxy,如果没有连接,他的Role(控制权限)就是ROLE_SimulatedProxy 。

那么对于一个Actor,他有三种方法来得到这个连接(或者说让自己属于这个连接)。

  1. 设置自己的owner为拥有连接的PlayerController,或者自己owner的owner为拥有连接的PlayerController。也就说官方文档说的查找他最外层的owner是否是PlayerController而且这个PlayerController拥有连接。
  2. 这个Actor必须是Pawn并且Possess了拥有连接的PlayerController。这个例子就是我们打开例子程序时,开始控制一个角色的情况。我们控制的这个角色就拥有这个连接。
  3. 这个Actor设置自己的owner为拥有连接的Pawn。这个区别于第一点的就是,Pawn与Controller的绑定方式不是通过Owner这个属性。而是Pawn本身就拥有Controller这个属性。所以Pawn的Owner可能为空。(Owner这个属性在Actor里面,蓝图也可以通过GetOwner来获取)

对于组件来说,那就是先获取到他所归属的那个Actor,然后再通过上面的条件来判断。

我这里举几个例子,玩家PlayerState的owner就是拥有连接的PlayerController,Hud的owner是拥有连接的PlayerController,CameraActor的owner也是拥有连接的PlayerController。而客户端上的其他NPC(一定是在服务器创建的)是都没有owner的Actor,所以这些NPC都是没有连接的,他们的Role就为ROLE_SimulatedProxy。

所以我们发现这些与客户端玩家控制息息相关的Actor才拥有所谓的连接。不过,进一步来讲,我们要这连接还有什么用?好吧,照搬官方文档。

  • 连接所有权是以下情形中的重要因素:
    1.RPC 需要确定哪个客户端将执行运行于客户端的 RPC
    2.Actor 复制与连接相关性
    3.在涉及所有者时的 Actor 属性复制条件

对于RPC,我们知道,UE4里面在Actor上调用RPC函数,可以实现类似在客户端与服务器之间发送可执行的函数的功能。最基本的,当我一个客户端拥有ROLE_AutonomousProxy权限的Actor在服务器代码里调用RPC函数(UFUNCTION(Reliable, Client))时,我怎么知道应该去众多的客户端的哪一个里面执行这个函数。(RPC的用法不细说,参考官方文档)答案就是通过这个Actor所包含的连接。关于RPC进一步的内容,下个问题里再详细描述。

第二点,Actor本身是可以同步的,他的属性当然也是。这与连接所有权也是息息相关。因为有的东西我们只需要同步给特定的客户端,其他的客户端不需要知道,(比如我当前的摄像机相关内容)。

对于第三点,其实就是Actor的属性是否同步可以进一步根据条件来做限制,有时候我们想限制某个属性只在拥有ROLE_AutonomousProxy的Actor使用,那么我们对这个Actor的属性ReplicatedMovement写成下面的格式就可以了。

void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
{
  DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement, COND_AutonomousOnly );
}

而经过前面的讨论我们知道ROLE_AutonomousProxy与所属连接是密不可分的。

最后,这里留一个思考问题:如果我在客户端创建出一个Actor,然后把它的Owner设置为带连接的PlayerController,那么他也有连接么?这个问题在下面的一节中回答。

4.2.Actor的Role是ROLE_Authority就是服务端么?

并不是,有了前面的讲述,我们已经可以理解,如果我在客户端创建一个独有的Actor(不能勾选bReplicate)。那么这个Actor的Role就是ROLE_Authority,所以这时候你就不能通过判断他的Role来确定当前调试的是客户端还是服务器。这时候最准确的办法是获取到NetDiver,然后通过NetDiver找到Connection。(事实上,GetNetMode()函数就是通过这个方法来判断当前是否是服务器的)对于服务器来说,他只有N个ClientConnections,对于客户端来说只有一个serverConnection。

如何找到NetDriver呢?可以参考下面的图片,从Outer获取到当前的Level,然后通过Level找到World。World里面就有一个NetDiver。当然,方法不止这一个了,如果有Playercontroller的话,Playercontroller上面也有NetConnection,可以再通过NetConnection再获取到NetDiver。
 


4.3.Owner是如何在RPC调用中生效的?

答案在AActor::GetFunctionCallspace里面,每次调用RPC函数时,会调用该函数判断当前是不是在远端调用,是的会就会通过网络发送RPC。GetFunctionCallspace里面会通过Owner找到connection信息。

五. 进一步理解RPC与同步

5.1. RPC函数应该在哪个端执行?

对于一个形如UFUNCTION(Reliable, Client)的RPC函数,我们知道这个函数应该在服务器调用,在客户端执行。可是如果我在Standalone的端上执行该函数的时候会发生什么呢?

答案是在服务器上执行。其实这个结果完全可以参考下面的这个官方图片。

刚接触RPC的朋友可能只是简单的记住这个函数应该从哪里调用,然后在哪里执行。不过要知道,即使我声明一个在服务器调用的RPC我还是可以不按套路的在客户端去调用(有的时候并不是我们故意的,而是编写者没有理解透彻),其实这种不合理的情况UE早就帮我想到并且处理了。比如说你让自己客户端上的其他玩家去调用一个通知服务器来执行的RPC,这肯定是不合理的,因为这意味着你可以假装其他客户端随意给服务器发消息,这种操作与作弊没有区别~所以RPC机制就会果断丢弃这个操作。

所以大家可以仔细去看看上面的这个图片,对照着理解一下各个情况的执行结果,无非就是三个变量:

  1. 在哪个端调用
  2. 当前执行RPC的Actor归属于哪个连接
  3. RPC的类型是什么。


5.2. 客户端创建的Actor能调用RPC么?

不过看到这里,再结合上一节结尾提到的问题,如果我在客户端创建一个Actor。把这个Actor的Owner设置为一个带连接PlayerController会怎么样呢?如果在这里调用RPC呢?

我们确实可以通过下面这种方式在客户端给新生成的Actor指定一个Owner。

好吧,关键时候还是得搬出来官方文档的内容。

您必须满足一些要求才能充分发挥 RPC 的作用:
1. 它们必须从 Actor 上调用。
2. Actor 必须被复制。
3. 如果 RPC 是从服务器调用并在客户端上执行,则只有实际拥有这个 Actor 的客户端才会执行函数。
4. 如果 RPC 是从客户端调用并在服务器上执行,客户端就必须拥有调用 RPC 的 Actor。 5. 多播 RPC 则是个例外:
o 如果它们是从服务器调用,服务器将在本地和所有已连接的客户端上执行它们。
o 如果它们是从客户端调用,则只在本地而非服务器上执行。
o 现在,我们有了一个简单的多播事件限制机制:在特定 Actor 的网络更新期内,多播函数将不会复制两次以上。按长期计划,我们会对此进行改善,同时更好的支持跨通道流量管理与限制。

看完第二条,其实你就能理解了,你的Actor必须要被复制,也就是说必须是bReplicate属性为true, Actor是从服务器创建并同步给客户端的(客户端如果勾选了bReplicate就无法在客户端上正常创建,参考第4部分)。
所以,这时候调用RPC是失效的。我们不妨去思考一下,连接存在的意义本身就是一个客户端到服务器的关联,这个关联的主要目的就是为了执行同步。如果我只是在客户端创建一个给自己看的Actor,根本就不需要网络的连接信息(当然你也没有权限把它同步给服务器),所以就算他符合连接的条件,仍然是一个没有意义的连接
同时,我们可以进一步观察这个Actor的属性,除了Role以外,Actor身上还有一个RemoteRole来表示他的对应端(如果当前端是客户端,对应端就是服务器,当前端是服务器,对应端就是客户端)。你会发现这个在客户端创建的Actor,他的Role是ROLE_Authority(并不是ROLE_AutonomousProxy),而他的RemoteRole是ROLE_None。这也说明了,这个Actor只存在于当前的客户端内。

5.3. RPC与Actor同步谁先执行?

下面我们讨论一下RPC与同步直接的关系,这里提出一个这样的问题
问题服务器ActorA在创建一个新的ActorB的函数里同时执行自身的一个Client的RPC函数,RPC与ActorB的同步哪个先执行?(原答案不准确已修改)
答案是不确定。这个涉及到UE4网络消息的发送机制与发送时机,一般来说,RPC的数据会立刻塞到SendBuffer里面,而Actor的同步要等到NetDriver统一处理。所以RPC的消息是相对靠前的,不过由于存在丢包延迟等情况,这个结果在网络环境下不能确定。

那么这个问题会造成什么后果呢?

  1. 当你创建一个新的Actor的同时(比如在一个函数内),你将这个Actor作为RPC的参数传到客户端去执行,这时候你会发现客户端的RPC函数的参数为NULL。
  2. 你设置了一个bool类型属性A并用UProperty标记了一个回调函数OnRep_Use。你先在服务器里面修改了A为true,同时你调用了一个RPC函数让客户端把A置为true。结果就导致你的OnRep_Use函数没有执行。但实际上,这会导致你的OnRep_Use函数里面还有其他的操作没有执行。

如果你觉得上面的情况从来没有出现过,那很好,说明暂时你的代码没有类似的问题,但是我觉得有必要提醒一下大家,因为UE4代码里面本身就有这样的问题,你以后也很有可能遇到。下面举例说明实际可能出现的问题:

情况1:当我在服务器创建一个NPC的时候,我想让我的角色去骑在NPC上并控制这个NPC,所以我立刻就让我的Controller去Possess这个NPC。在这个过程中,PlayerController就会执行

UFUNCTION(Reliable, Client) 
void ClientRestart (APawn*NewPawn)


当客户端收到这个RPC函数回调的时候就发现我的APlayerController::ClientRestart_Implementation (APawn* NewPawn)里面的参数为空~原因就是因为这个NPC刚在服务器创建还没有同步过来。同理,对于刚出生的玩家来说是不是也存在这个问题呢?
确实存在。不过,UE4本身其实有考虑到这个问题,ClientRestart函数有特殊的处理,服务器在移动的时候会通过SafeRetryClientRestart让客户端执行ClientRetryClientRestart,如果发现Pawn不对(也就是第一次ClientRestart执行失败的话)就会触发再次执行。

更新:我们可以通过设置控制台变量net.DelayUnmappedRPCs 1允许客户端等到这个对象生成的时候再去执行,但是仅限于可靠的RPC。

情况2:对于Pawn里面的Controller成员声明如下

UPROPERTY(replicatedUsing=OnRep_Controller)
AController*Controller;

OnRep_Controller回调函数里面回去执行Controller->SetPawnFromRep(this);
进而执行
Pawn = InPawn;
OnRep_Pawn();


下面重点来了,OnRep_Pawn函数里面会执行

OldPawn->Controller= NULL;


将客户端之前Controller控制的角色的Controller设置为空。

到现在来看没有什么问题。那么现在结合上面第二个问题,如果一个RPC函数的执行发生在客户端的Controller同步前同时修改为正确的Controller,那么OnRep_Controller回调函数就不会执行。所以客户端的原来Controller控制的OldPawn的Controller就不会置为空,导致的结果是客户端和服务器竟然不一样。
实际上,确实存在这么一个函数,这个RPC函数就是ClientRestart。这看起来就很奇怪,因为ClientRestart如果没有正常执行的话,OnRep_Controller就会执行,进而导致客户端的oldPawn的Controller为空(与服务器不同,因为服务器并没有去设置OldPawn的Controller)。不知道这是不是引擎的一个bug。

不管怎么说,你需要清楚的是RPC的执行与同步的执行是有先后关系的,而这种关系会影响到代码的逻辑,所以之后的代码有必要考虑到这一点。

最后,对使用RPC的朋友做一个提醒,有些时候我们在使用UPROPERTY标记Server的函数时,可能是从客户端调用,也可能是从服务器调用。虽然结果都是在服务器执行,但是过程可完全不同。从客户端调用的在实际运行时是通过网络来处理的,一定会有延迟。而从服务器调用的则会立刻执行。

5.4. 多播MultiCast RPC会发送给所有客户端么?(已修改)

原答案:

看到这个问题,你可能想这还用说么?不发给所有客户端那要多播干什么?但事实上确实不一定。
考虑到服务器上的一个NPC,在地图的最北面,有两个客户端玩家。一个玩家A在这个NPC附近,另一个玩家B在最南边看不到这个NPC(实际上就是由于距离太远,服务器没有把这个Actor同步到这个B玩家的客户端)。我们现在在这个NPC上调用多播RPC通知所有客户端上显示一个提示消失“NPC发现了宝藏”。这个消息会不会发送到B客户端上面?

  • 情况一:会。多播顾名思义就是通知所有客户端,不需要考虑发送到哪一个客户端,直接遍历所有的连接发送即可。
  • 情况二:不会。RPC本来就是基于Actor的,在客户端B上面连这个Actor都没有,我还可以使用RPC不会很奇怪?

第一种情况强化了多播的概念,淡化了RPC基于Actor的机制,情况二则相反。所以看起来都有道理。实际上,UE4里面更偏向第二种情况,处理如下:

如果一个多播标记为Reliable,那么他默认会给所有的客户端执行该多播事件,如果其标记的是unreliable,他就会检测该NPC与客户端B的网络相关性(即在客户端B上是否同步)。但实际上,UE还是认为开发者不应该声明一个Reliable的多播函数。下面给出UE针对这个问题的相关注释:(相关的细节在另一篇进一步深入UE网络同步的文章里面去分析)
// Do relevancy check if unreliable. // Reliables will always go out. This is odd behavior. On one hand we wish to garuntee "reliables always getthere". On the other // hand, replicating a reliable to something on theother side of the map that is non relevant seems weird. // Multicast reliables should probably never beused in gameplay code for actors that have relevancy checks. If they are, the // rpc will go through and the channel will be closedsoon after due to relevancy failing.
修改答案:
我发现引擎在这块已经更改了,引擎已经不考虑多播函数是否是reliable了,只要不满足NetRelevant都不会发送,参考UNetDriver::ProcessRemoteFunction。

5.5. RPC参数与返回值

参数:RPC函数除了UObject类型的指针以及constFString&的字符串外,其他类型的指针或者引用都不可以作为RPC的参数。对于UObject指针类型我们可以在另一端通过GUID识别(后面第五部分有讲解),但是其他类型的指针传过去是什么呢?我们根本就无法还原其地址,所以不允许传输其指针或者引用。
而对于FString,传const原因我认为是为了不想让发送方与接收方两边对字符串进行修改,而传引用只是为了减少复制构造带来的开销。在FString发送与接收的处理细节里面并不在意其是否是const&,他只在意他的类型以及相对Object的偏移。
返回值:一个RPC函数是不能有返回值的,因为其本身的执行就是一次消息的传递。假如一个客户端执行一个Server RPC,如果有返回值的话,那么岂不是服务器执行后还要再发送一个消息给客户端?这个消息怎么处理?再发一次RPC?如果还有返回值那么不就无限循环了?因此RPC函数不可以添加返回值。

5.6 合理使用COND_InitialOnly

前面提到过,Actor的属性同步可以通过这种方式来实现。

声明一个属性并标记

UPROPERTY(Replicated) 
uint8 bWeapon: 1;

UPROPERTY(Replicated)
uint8 bIsTargeting: 1;

void Character::GetLifetimeReplicatedProps(TArray<FLifetimeProperty > & OutLifetimeProps ) const
{
    DOREPLIFETIME(Character,bWeapon );
    DOREPLIFETIME_CONDITION(Character, bIsTargeting, COND_InitialOnly
);

这里面的第一个属性一般的属性复制,第二个就是条件属性复制。条件属性复制无非就是告诉引擎,这个属性在哪些情况下同步,哪些情况下不同步。这些条件都是引擎事先提供好的。

这里我想着重的提一下COND_InitialOnly这个条件宏,汉语的官方文档是这样描述的:该属性仅在初始数据组尝试发送。而英文是这样描述的:This property will only attempt to send on the initial bunch。对比一下,果然还是英文看起来更直观一点。

经过测试,这个条件的效果就是这个宏声明的属性只会在Actor初始化的时候同步一次,接下来的游戏过程中不会再同步。所以,我们大概能想到这个东西在有些时候确实用的到,比如同步玩家的姓名,是男还是女等,这些游戏开始到结束一般都不会改变的属性。也就是说,上限一般调整的次数很少,如果真的有调整并需要同步,他会手动调用函数去同步该属性。这样就可以减少同步带来的压力。 然而,一旦你声明为COND_InitialOnly。你就要清楚,同步只会执行一次,客户端的OnRep回调函数就会执行一次。所以,当你在服务器创建了一个新的Actor的时候你需要第一时间把需要改变的值修改好,一旦你在下一帧(或是下一秒)去执行那么这个属性就无法正确的同步到客户端了。

5.7.客户端与服务器一致么?

我们已经知道UE4的客户端与服务器公用一套代码,那么我们在每次写代码的时候就有必要提醒一下自己。这段代码在哪个端执行,客户端与服务器执行与表现是否一致?

虽然,我很早之前就知道这个问题,但是写代码的时候还是总是忽略这个问题,而且程序功能经常看起来运行的没什么问题。不过看起来正常不代表逻辑正常,有的时候同步机制帮你同步一些东西,有时候会删除一些东西,有时候又会生成一些东西,然而你可能一点都没发现。

举个例子,我在一个ActorBeginPlay的时候给他创建一个粒子Emiter。代码大概如下:

void AGate::BeginPlay()
{
   Super::BeginPlay();
  //单纯的在当前位置创建粒子发射器
   GetWorld()->SpawnActor<AEmitter>(SpawnEmitter,GetActorLocation(), UVictoryCore::RTransform(SpawnEmitterRotationOffset,GetActorRotation()));
}

代码很简单,不过也值得我们分析一下。

首先,服务器下,当Actor创建的时候就会执行BeginPlay,然后在服务器创建了一个粒子发射器。这一步在服务器(DedicateServer)创建的粒子其实就是不需要的,所以一般来说,这种纯客户端表现的内容我们不需要在专用服务器上创建。

再来看一下客户端,当创建一个Gate的时候,服务器会同步到客户端一个Gate,然后客户端的Gate执行BeginPlay,创建粒子。这时候我们已经发现二者执行BeginPlay的时机不一样了。进一步测试,发现当玩家远离Gate的时候,由于UE的同步机制(只会同步一定范围内的Actor),客户端的Gate会被销毁,而粒子发射器也会销毁。而当玩家再次靠近的时候,Gate又被同步过来了,原来的粒子发射器也被同步过来。而因为客户端再次执行了BeginPlay,又创建了一个新的粒子,这样就会导致不断的创建新的粒子。

你觉得上面的描述准确么?

并不准确,因为上述逻辑的执行还需要一个前置条件——这个粒子的bReplicate属性是为false的。有的时候,我们可能一不小心就写出来上面这种代码,但是表现上确实正常的,为什么?因为SpawnActor是否成功是有条件限制的,在生成过程中有一个函数

bool AActor::TemplateAllowActorSpawn(UWorld* World,const FVector& AtLocation, const FRotator& AtRotation, const struct
FActorSpawnParameters& SpawnParameters)
{
    return !bReplicates || SpawnParameters.bRemoteOwned||World->GetNetMode() != NM_Client;
}

如果你是在客户端,且这个Actor勾选了bReplicate的话,TemplateAllowActorSpawn就会返回false,创建Actor就会失败。如果这个Actor没有勾选bReplicate的话,那么服务器只会创建一个,客户端就可能不断的创建,而且服务器上的这个Actor与客户端的Actor没有任何关系。

另外,还有一种常见的错误。就是我们的代码执行是有条件的,然而这个条件在客户端与服务器是不一样的(没同步)。如

void Gate::CreateParticle(int32 ID)
{
   if(GateID!= ID)
   {
      FActorSpawnParameters SpawnInfo;
      GetWorld()->SpawnActor<AEmitter>(SpawnEmitter, GetActorLocation(),GetActorRotation(), SpawnInfo);
   }
}

这个GateID是我们在GateBeginPlay的时候随机初始化的,然而这个GateID只在服务器与客户端是不同的。所以需要服务器同步到客户端,才能按照我们理想的逻辑去执行。

5.8. 属性同步的基本规则与注意事项

非休眠状态下的Actor的属性同步:只在服务器属性值发生改变的情况下执行
回调函数执行条件:服务器同步过来的数值与客户端不同
休眠的Actor:不同步

首先要认识到,同步操作触发是由服务器决定的,所以不管客户端是什么值,服务器觉得该同步就会把数据同步到客户端。而回调操作是客户端执行,所以客户端会判断与当前的值是否相同来决定是否产生回调。

然后是属性同步,属性同步的基本原理就是服务器在创建同步通道的时候给每一个Actor对象创建一个属性变化表(这里面涉及到FObjectReplicator,FRepLayout,FRepState,FRepChangedPropertyTracker相关的类,有兴趣可以进一步了解,在另一深入UE网络同步文章里有讲解),里面会记录一个当前默认的Actor属性值。之后,每次属性发生变化的时候,服务器都会判断新的值与当前属性变化表里面的值是否相同,如果不同就把数据同步到客户端并修改属性变化表里的数据。对于一个非休眠且保持连接的Actor,他的属性变化表是一直存在的,所以他的表现出来的同步规则也很简单,只要服务器变化就同步。

动态数组TArray在网络中是可以正常同步的,系统会检测到你的数组长度是否发生了变化,并通知客户端改变

5.8.1. 结构体的属性同步

注意,UE里面UStruct类型的结构体在反射系统中对应的是UScriptStruct,他本身可以被标记Replicated并且结构体内的数据默认都会被同步,而且如果里面有还子结构体的话也仍然会递归的进行同步。如果不想同步的话,需要在对应的属性标记NotReplicated,而且这个标记只对UStruct有效,对UClass无效。

有一点特别的是,Struct结构内的数据是不能标记Replicated的。如果你给Struct里面的属性标记replicated,UHT在编译的时候就会提醒你编译失败。
最后,UE里面的UStruct不可以以成员指针的方式在类中声明。

5.8.2. 属性回调

问题:属性回调与RPC在使用结果上的差异?

属性回调理论上一定会执行,而RPC函数有可能由于错过执行时机而不再会执行。例如:我在服务器上面有一个宝箱,第一个玩家过去后,宝箱会自动开启。如果使用RPC函数,当第一个玩家过去后,箱子执行多播RPC函数触发开箱子操作。但是由于其他的玩家离这个箱子很远,所有这个箱子没有同步给其他玩家,其他玩家收不到这个RPC消息。(如果对结果有疑问参考第二节的第四个问题)当这些玩家之后再过去之后,会发现箱子还是关闭的。如果采用属性回调,但第一个玩家过去后,设置箱子的属性bOpen为true,然后同步到所有客户端,通过属性回调执行开箱子操作。这时候其他玩家靠近箱子时,箱子会同步到靠近的玩家,然后玩家在客户端上会收到属性bOpen,同时执行属性回调,这时候可以实现所有靠近的玩家都会发现箱子已经被别人开过了。

问题:服务器上生成一个Actor,他在客户端上的UObject类型指针的属性回调与他的Beginplay谁先执行?

这个问题这么看有点奇怪,我进一步描述一下。有一个类MyActor,他有一个指针属性PropertyB指向一个同步的MyActorB,同时这个指针属性有一个回调函数。现在我在服务器创建一个新的MyActor A,并设置A的PropertyB为MyActorB。那么在客户端上,是A的BeginPlay先执行,还是PropertyB的属性回调先执行?

答案是不确定,一开始的时候,我一直认为是属性回调在Actor的BeginPlay之前执行,测试了很多次也是这样的。但是某种情况下, BeginPlay会先执行。这个问题的意义就在于,一个Actor同步过去执行BeginPlay的时候,你发现他的属性还没有同步过来(而且只发现指针可能没有同步过来,其他内置类型都会在BeginPlay 前同步过来)。为什么指针没有同步过来?因为这个指针同步过来的时候,他指向的对象在客户端还不存在,他在客户端上也没有对应的GUID缓存 。由于找不到对应的对象,他只能先暂时记录下这个指针指向对象的GUID,然后在其他的Tick时间再回来检测这个对象是否存在。这种情况一般来说很难重现,不过这个问题有助于我们进一步加深对网络的理解。

5.8.3. UObject指针类型的属性同步

属性同步也好,RPC参数也好。我们都需要思考一下,我在传递一个UObject类型的指针时,这个UObject在客户端存在么?如果存在,我如何能通过服务器的一个指针找到客户端上相同UObject的指针?

答案是通过FNetworkGUID。服务器在同步一个对象引用(指针)的时候,会给其分配专门的FNetworkGUID并通过网络进行发送。客户端上通过识别这个ID,就可以找到对应UObject。

那么如此说来,是不是只有标记Replicate的对象才能同步其引用或指针呢?

也不是。对于直接从数据包加载出来的对象(如地图里面实现搭建好的建筑地形),我们可以直接认为服务器上的该地形对象与客户端上对应的地形对象就是一个对象,那么在服务器上指向该地形的指针发送到客户端也应该就是指向对应地形的指针。所以总结来说一个UObject对象是否可以通过网络发送他的引用有如下条件(参考官方文档):

您通常可以按照以下原则来确定是否可以通过网络引用一个对象:
任何复制的 actor 都可以复制为一个引用
任何未复制的 actor 都必须有可靠命名(直接从数据包加载)
任何复制的组件都可以复制为一个引用
任何未复制的组件都必须有可靠命名。
其他所有 UObject(非actor 或组件)必须由加载的数据包直接提供
什么是拥有可靠命名的对象?
拥有可靠命名的对象指的是存在于服务器和客户端上的同名对象。
1.如果Actor 是从数据包直接加载(并非在游戏期间生成),它们就被认为是拥有可靠命名。

2.满足以下条件的组件即拥有可靠命名:
● 从数据包直接加载
● 通过construction scripts脚本添加
● 采用手动标记(通过  UActorComponent::SetNetAddressable 设置)
● 只有当您知道要手动命名组件以便其在服务器和客户端上具有相同名称时,才应当使用这种方法(最好的例子就是  AActor C++ 构造函数中添加的组件)

最后总结一下就是有四种情况下UObject对象的引用可以在网络上传递成功

  1. 标记replicate
  2. 从数据包直接Load
  3. 通过Construction scripts添加或者C++构造函数里面添加
  4. 使用UActorComponent::SetNetAddressable标记(这个只针对组件,其实蓝图里面创建的组件默认就会执行这个操作)

5.9.组件同步

组件在同步上分为两大类:静态组件与动态组件

对于静态组件:一旦一个Actor被标记为同步,那么这个Actor身上默认所挂载的组件也会随Actor一起同步到客户端(也需要序列化发送)。什么是默认挂载的组件?就是C++构造函数里面创建的默认组件或者在蓝图里面添加构建的组件。所以,这个过程与该组件是否标记为Replicate是没有关系的。

对于动态组件:就是我们在游戏运行的时候,服务器创建或者删除的组件。比如,当玩家走进一个洞穴时,给洞穴里面的火把生成一个粒子特效组件,然后同步到客户端上,当玩家离开的时候再删除这个组件,玩家的客户端上也随之删除这个组件。

对于动态组件,我们必须要attach到Actor上并设置他的Replicate属性为true,即通过函数 AActorComponent ::SetIsReplicated(true)来操作。而对于静态组件,如果我们不想同步组件上面的属性,我们就没有必要设置Replicate属性。

一旦我们执行了SetIsReplicated(true)。那么组件在属性同步以及RPC上与Actor的同步几乎没有区别,组件上也需要设置GetLifetimeReplicatedProps来执行属性同步,Actor同步的时候会遍历他的子组件查看是否标记Replicate以及是否有属性要同步。

注意:动态组件的同步是有限制的。由于组件里面的很多成员是无法同步的(比如skeletalmesh)导致很多组件服务器创建后并不会显示在客户端上面,所以客户端在收到之后还要再进行一些相关的处理,比如说把组件放到一个属性并在回调里面重新加载一遍skeletalmesh(记录skeletalmesh的TSoftObjectPtr并同步,客户端收到后执行LoadSynchronous本地加载)。

bool AActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
{
     check(Channel);
     check(Bunch);
     check(RepFlags);

     bool  WroteSomething = false;
     for(UActorComponent* ActorComp : ReplicatedComponents)
     {
         if(ActorComp && ActorComp->GetIsReplicated())
         {
           //Lets the component add subobjects before replicating its own properties.
            WroteSomething|= ActorComp->ReplicateSubobjects(Channel, Bunch,RepFlags); 
           //(this makes those subobjects 'supported', and from here on those objects mayhave reference replicated)  子对象(包括子组件)的同步,其实是在ActorChannel里进行
            WroteSomething |= Channel->ReplicateSubobject(ActorComp,*Bunch,*RepFlags);
         }
      }
     return  WroteSomething;
}

对于C++默认的组件,需要放在构造函数里面构造并设置同步,UE给出了一个例子:

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);
     //Make DSO components net addressable 实际上如果设置了Replicate之后,这句代码就没有必要执行了
     CharacterMovement->SetNetAddressable();
     // Enable replication by default
     CharacterMovement->SetIsReplicated(true);
    }
}
  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张乂卓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值