1 网络同步机制

UE 提供了强大的网络同步机制:

  • RPC :可以在本地调用,对端执行
  • 属性同步:标记一个属性为 UPROPERTY(Replicated) 就可以自动将其修改后的值同步到客户端
  • 移动复制:Actor 开启了移动复制后会自动复制位置,旋转和速度
  • 创建和销毁:Server 创建 Actor 时根据其权限会在所有连接客户端生成远程代理

    UE 基本上都是基于 Actor 进行同步的。Actor 同步的前提需要标记 Actor 为 bReplicated 的 。首先来了解下如何应用 UE 中的属性同步。

2 Actor 同步

2.1 如何同步一个 Actor

首先思考一下,如何创建一个 Actor 然后让他同步到各个客户端?

  • 在哪里创建?创建 Actor 的操作显然需要在服务端执行,如果在客户端执行,这个 Actor 只会在这个客户端可见。

  • 如何让 Actor 同步? 标记 Actor 的 bReplicated 为 True。

2.2 如何同步 Actor 的属性

创建并同步完 Actor 之后,下一步是能够支持 Actor 的数据能够正常同步到客户端,首先在应用层如何支持这一操作?

假设我们有一把武器,需要同步武器的弹药数量,那么需要进行如下定义

/** weapon.h **/
class AWeapon : public {
UPROPERTY(replicatedUsing=OnRep_Ammo) // 可选属性,当 Ammo 成功同步后会调用该函数
int32 Ammo; // 弹药数量
UFUNCTION()
virtual void OnRep_Ammo();
virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const override; // 属性复制条件控制
}
/** weapon.cpp **/
void AWeapon::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const {
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AWeapon, Ammo); // 具体的复制属性
}

上述定义主要有如下特点:

  • Actor 支持同步时,如果有自定义需要同步的属性,需要重写 GetLifetimeReplicatedProps 函数,并在其中标注要复制的具体属性
  • 同步属性时,可以通过 URPOPERTY 宏中 replicatedUsing 属性来指定同步后要执行的回调函数

2.3 Actor 同步流程

现在我们需要考虑一下,Actor 的属性在什么情况下会被复制?通常来说我们只需要在 Actor 属性被修改时就需要同步到客户端,但是什么时候会被修改我们并不知道,因此引擎中会根据 Actor 复制频率 来做同步检查。参考后文中 4.4 优先级和复制频率 的内容。我们可以梳理出如下流程:

基本上每帧都需要检查有哪些 Actor 需要同步,显然这种检查也是比较耗时的,由此 UE 也引入了 PushModel 技术,手动标记 Actor 哪些属性已修改需要更新,从而节约检查属性的消耗。

2.4 小结

如何创建,同步一个 Actor 的应用层流程基本梳理完毕,但是显然需要知道其后面的原理,由此引出如下问题在后续的文章中解决:

UE

  • UE 中网络同步的整体框架是如何驱动的?
  • UE 中如何利用这个网络同步的框架同步一个 Actor?其从服务端执行 SpawnActor 开始到客户端接收到请求并创建出 Actor 的流程及调用堆栈是怎样的?
  • 属性同步中 replicatedUsing 是如何实现的?这个宏是如何作用到属性上的?
  • 如何检查属性是否修改?如何对比?

[!warning]

Actor 同步只能从 Server 同步到 Client,Client 唯一向 Server 发送请求的方式只有 RPC,属性同步是单向的

3 RPC 使用分析

3.1 什么是 RPC

RPC(Remote Procedure Call,远程过程调用)是一种用于实现分布式应用程序的技术。通过 RPC,可以使分布式应用程序中的各个部分像本地代码一样交互,即使它们不在同一台计算机或在不同的网络上。

在 RPC 中,一个应用程序可以调用另一个应用程序中的函数或方法,就像调用本地函数一样。这些函数和方法在不同的进程或计算机上执行,但对调用方来说,它们是透明的。调用方不需要了解远程代码的具体实现细节,只需要知道如何调用它们并处理返回值。

[! RPC 的使用有一些前提准则,必须满足这些条件才能调用它]

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

3.2 RPC 的种类

UE 中有 3 种 RPC :

  • Server : 仅在 Server 上调用
  • Client :仅在 Client 上调用
  • NetMulticast :在与服务器连接的所有客户端及服务器本身上调用

    这三种 RPC 只需要在函数调用的声明中加上对应的标记即可。

3.2.1 如何确定 RPC 在哪里被执行

当 RPC 函数在服务器上调用时,有如下情况:


Actor 所有权 未复制 NetMulticast Server Client
Client Owned Actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在 actor 的所属客户端上运行
Server Owned Actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在服务器上运行
Unowned Actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在服务器上运行

当 RPC 函数在客户端上调用时,如下:


Actor 所有权 未复制 NetMulticast Server Client
Owned By Invoking Client 在执行调用的客户端上运行 在执行调用的客户端上运行 在服务器上运行 在执行调用的客户端上运行
Owned By a different client 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行
Server Owned Actor 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行
Unowned Actor 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行

事实上最终判断 RPC 在哪里被执行,主要根据如下三个条件:

  1. 调用端是谁(Client/Server)
  2. 调用的 Actor 属于哪个连接
  3. RPC 的类型(Server/Client/NetMulticast)

举一个例子,有两个客户端 c1 和 c2 各自有 Pawn p1 和 p2,c1 的客户端上能够获取到 p2 这个对象,但是无法利用 p2 调用 RPC,因为在 c1 上 p2 只是一个普通的 Pawn,其没有对应的 c2 的 PlayerController(参考 [[总体框架#3. PlayerController|PlayerController 定义]])。也没有对应的 Connection,因此无法执行 RPC。

[!note]

  1. 实际上是否会调用到对端,主要根据 UObject::GetFunctionCallspace 这个接口返回的枚举来判定的。
  2. 其次根据 Actor 所属的 Connection,如果 Actor 不属于任何一个 Connection(Owner 递归查找找不到 PlayerController),那么也是无法调用 RPC 的。

3.3 RPC 的使用

UE 中,一个 RPC 函数的声明和定义如下(以 Client 调用 Server 执行的 RPC 为例):

/** weapon.h **/
class AWeapon : public {
UFUNCTION(Server)
void Fire();
} /** weapon.cpp **/
void AWeapon::Fire_Implementation() {
/** do weapon fire **/
}

此时只需要在 Client 端使用如下操作:

AWeapon* Weapon = GetWeapon();
Weapon->Fire();

就能直接调用 Server 端的 Fire 接口了。关于其背后实现的原理,可以参考 [[原理#4. QA#4.5 RPC 函数如何执行的|RPC函数执行原理]]。

这里需要注意一点,UE 的 RPC 是没有返回值的,统一都是 void。个人如果需要获取返回值,那么就需要一个类似协程的概念,来获取返回值,否则只能阻塞等待或者异步等待,后者显然代码可读性也不是很好。

3.4 小结

RPC 与属性同步有些不同,RPC 可以 Server To Client 也可以 Client To Server,是一种双向的通信方式,而属性同步只能 Server To Client,属于单向同步。对于 RPC 的实现,有如下问题可以再进行深究:

UE

  • 如何调用对端的函数?RPC 的前提是利用 Actor 去调用,Actor 本身存有反射信息,因此问题简化为,告诉对端哪一个 Actor 要执行名称为 xxx 的函数
  • 函数的参数如何封装好并传递到对端?
  • NetMulticast 如何转发函数?

4 Actor 同步概念

4.1 NetRole

每个 Actor 都有一个 LocalRole 和 RemoteRole 的概念,分别对应于 Actor 在本地和在对端的 Role,Role 主要分为 3 种:

  • ROLE_SimulatedProxy
  • ROLE_AutonomousProxy
  • ROLE_Authority

    通常 LocalRole=Authority 只存在于服务器(但是客户端也有可能存在,比如 Spawn 一个 Actor 但是不标记为 Replicated)。关于各种 Role 常见的设置可以参考下图:

4.1.1 AutonomousProxy 和 SimulatedProxy 的区别

  • AutonomousProxy 和 SimulatedProxy 基本只存在于客户端,ROLE_AutonomousProxy 用于处理本地玩家的输入,并将这些输入发送到服务器进行处理,而 ROLE_SimulatedProxy 用于处理其他玩家的输入,并在客户端上模拟 Actor 在服务器上的运行。因此通常 AutonomousProxy 只存在于 PlayerController 和其 Possess 的 Pawn。
  • SimulatedProxy 是标准的模拟途径,通常是根据上次获得的速率对移动进行推算。当服务器为特定的 actor 发送更新时,客户端将向着新的方位调整其位置,然后利用更新的间歇,根据由服务器发送的最近的速率值来继续移动 actor。
  • AutonomousProxy 通常只用于 PlayerController 所拥有的 actor。这说明此 actor 会接收来自真人控制者的输入,所以在我们进行推算时,我们会有更多一些的信息,而且能使用真人输入内容来补足缺失的信息(而不是根据上次获得的速率来进行推算)。

4.1.2 小结

那么这个 Role 有什么用呢?个人认为有如下用处:

  • 在 C/S 模式下,基本可以认为 LocalRole 为 Authority 的 Actor 当前就是处于服务器环境下,用来区分服务器还是客户端
  • 引擎对于 AutonomousProxy 和 SimulatedProxy 做了区分,用来更好的模拟玩家输入

[!note]

就目前而言,只有服务器能够向已连接的客户端同步 Actor (客户端永远都不能向服务器同步)。始终记住这一点, 只有 服务器才能看到 Role == ROLE_Authority 和 RemoteRole == ROLE_SimulatedProxy 或者 ROLE_AutonomousProxy

NetRole

4.2 关联连接

UE 中 Actor 有关联连接的概念,即这个 Actor 属于哪个连接。在传统的 C/S 服务器中,每个客户端和服务器会有一条连接,在 UE 中会为每个连接创建一个 PlayerController,这样这个 PlayerController 就归这条连接所有。

而如果一个 Actor 的 Owner 为 PlayerController 或者为 Pawn 并且这个 Pawn 拥有一个 PlayerController,那么这个 Actor 就归属于拥有这个 PlayerController 的连接。

这里的关联连接有什么用呢?

考虑如下三种情况:

  • 需要确定哪个客户端将执行运行于客户端的 RPC
  • Actor 复制与连接相关性(比如 bOnlyRelevantToOwner 为 True 的 Actor,只有拥有这个 Actor 的 Connection 才会收到这个 Actor 的属性更新,比如 PlayerController)
  • 涉及 Owner 的 Actor 属性复制条件(比如 COND_OnlyOwner 只能复制给 Owner)

连接所有权

4.3 相关性

相关性是用于判断 Actor 是否需要进行同步的重要依据。其主要判断相关性的接口为 AActor::IsNetRelevantFor 。个人认为相关性最重要的一点是可以有效的节约带宽和同步操作所带来的 CPU 消耗。

比如场景的规模可能比较大,玩家特定时刻只能看到关卡中部分 Actor。被服务器认为可见或者能够影响客户端的 Actor 组会被是为该客户端的相关 Actor 组,服务器只会让客户端知道其相关组内的 Actor。

  1. 如果 Actor 是 bAlwaysRelevant、归属于 Pawn 或 PlayerController、本身为 Pawn 或者 Pawn 是某些行为(如噪音或伤害)的发起者,则其具有相关性。
  2. 如果 Actor 是 bNetUseOwnerRelevancy 且拥有一个所有者,则使用所有者的相关性。
  3. 如果 Actor 是 bOnlyRelevantToOwner 且没有通过第一轮检查,则不具有相关性。
  4. 如果 Actor 被附加到另一个 Actor 的骨架模型,它的相关性将取决于其所在基础的相关性。
  5. 如果 Actor 是不可见的 (bHidden == true) 并且它的 Root Component 并没有碰撞,那么则不具有相关性,
    • 如果没有 Root Component 的话,AActor::IsNetRelevantFor() 会记录一条警告,提示是否要将它设置为 bAlwaysRelevant=true
  6. 如果 AGameNetworkManager 被设置为使用基于距离的相关性,则只要 Actor 低于净剔除距离,即被视为具有相关性。

[!note]

Pawn 和 PlayerController 将覆盖 AActor::IsNetRelevantFor() 并最终具有不同的相关性条件。

4.4 优先级和复制频率

4.4.1 优先级

每个 Actor 都有一个名为 NetPriority 的浮点变量。这个变量的数值越大,Actor 相对于其他"同伴"的带宽就越多。和优先级为 1.0 的 Actor 相比,优先级是 2.0 的 Actor 可以得到两倍的更新频度。唯一影响优先顺序的就是它们的比值。

计算 Actor 的当前优先级时使用了函数 AActor::GetNetPriority。为避免出现饥荒(starvation),AActor::GetNetPriority 使用 Actor 上次复制后经过的时间去乘以 NetPriority。同时,GetNetPriority 函数还考虑了 Actor 与观察者的相对位置以及两者之间的距离。

4.4.2 复制频率

Actor 不是每一帧都进行复制的,每个 Actor 有个自己的每秒复制频率 NetUpdateFrequency,每次检查 Tick 的 DeltaTime > 1/NetUpdateFrequency,满足条件才可以进行下一步复制检查。

比如默认 PlayerState 每秒更新 1 次,而 Pawn 每秒更新 100 次(默认情况下服务器 30 fps 运行,基本上每帧都会做复制检查)。

UnrealEngine - 网络同步入门的更多相关文章

  1. UE4网络同步属性笔记

    GameMode只有服务端有,适合写游戏逻辑.PlayerController每个客户端拥有一个,并拥有主控权.GameState在服务端同步到全端. CLIENT生成的Actor对其有Authori ...

  2. 脑残式网络编程入门(六):什么是公网IP和内网IP?NAT转换又是什么鬼?

    本文引用了“帅地”发表于公众号苦逼的码农的技术分享. 1.引言 搞网络通信应用开发的程序员,可能会经常听到外网IP(即互联网IP地址)和内网IP(即局域网IP地址),但他们的区别是什么?又有什么关系呢 ...

  3. 脑残式网络编程入门(五):每天都在用的Ping命令,它到底是什么?

    本文引用了公众号纯洁的微笑作者奎哥的技术文章,感谢原作者的分享. 1.前言   老于网络编程熟手来说,在测试和部署网络通信应用(比如IM聊天.实时音视频等)时,如果发现网络连接超时,第一时间想到的就是 ...

  4. 脑残式网络编程入门(四):快速理解HTTP/2的服务器推送(Server Push)

    本文原作者阮一峰,作者博客:ruanyifeng.com. 1.前言 新一代HTTP/2 协议的主要目的是为了提高网页性能(有关HTTP/2的介绍,请见<从HTTP/0.9到HTTP/2:一文读 ...

  5. 脑残式网络编程入门(二):我们在读写Socket时,究竟在读写什么?

    1.引言 本文接上篇<脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手>,继续脑残式的网络编程知识学习 ^_^. 套接字socket是大多数程序员都非常熟悉的概念,它是计算机 ...

  6. 脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手

    .引言 网络编程中TCP协议的三次握手和四次挥手的问题,在面试中是最为常见的知识点之一.很多读者都知道“三次”和“四次”,但是如果问深入一点,他们往往都无法作出准确回答. 本篇文章尝试使用动画图片的方 ...

  7. 百度云+ KeePass 网络同步你的密码

     百度云+ KeePass 网络同步你的密码   百度云一个目前不限流量不限格式能直链的网盘,速度在我这里很快,难得了!KeePass(小众介绍过 KeePass.) 是一个免费开源的密码管理类软件, ...

  8. 【网络爬虫入门02】HTTP客户端库Requests的基本原理与基础应用

    [网络爬虫入门02]HTTP客户端库Requests的基本原理与基础应用 广东职业技术学院  欧浩源 1.引言 实现网络爬虫的第一步就是要建立网络连接并向服务器或网页等网络资源发起请求.urllib是 ...

  9. 【网络爬虫入门04】彻底掌握BeautifulSoup的CSS选择器

    [网络爬虫入门04]彻底掌握BeautifulSoup的CSS选择器 广东职业技术学院  欧浩源 2017-10-21 1.引言 目前,除了官方文档之外,市面上及网络详细介绍BeautifulSoup ...

  10. 【网络爬虫入门05】分布式文件存储数据库MongoDB的基本操作与爬虫应用

    [网络爬虫入门05]分布式文件存储数据库MongoDB的基本操作与爬虫应用 广东职业技术学院  欧浩源 1.引言 网络爬虫往往需要将大量的数据存储到数据库中,常用的有MySQL.MongoDB和Red ...

随机推荐

  1. nodejs实现页面的增删查

    一.在mong0db.js中写如下代码 1.导入 const mongoose = require("mongoose"); // 建立数据库连接 mongoose.connect ...

  2. linux 修改password

    passwd 命令,手动修改: [root@localhost testuser]# passwd testuser Changing password for user testuser. New ...

  3. dbeaver,执行SQL时,空行导致SQL执行报错" ERROR: syntax error at or near "case"Position: 1"

    dbeaver,执行SQL时,空行导致SQL执行报错" ERROR: syntax error at or near "case"Position: 1" 解决 ...

  4. SpringMvc配置和原理

    运行原理 DispatcherServlet通过HandlerMapping在MVC的容器中找到处理请求的Controller,将请求提交给Controller,Controller对象调用业务层接口 ...

  5. vs2013安装完VASSISTX助手之后字体变成斜体如何解决?

    VC助手为最新版本. 1. 打开vc助手选项 2. 取消勾选"show stable symbols in italics"

  6. window nginx php ci框架环境搭建

    下载nginx 后修改配置文件: location / { #try_files $uri $uri/ /index.php?$query_string; root C:\Software\serve ...

  7. gensim

    官方文档: https://radimrehurek.com/gensim/models/word2vec.html 1.训练模型定义 from gensim.models import word2v ...

  8. postman打开白屏

    1.打开高级系统设置:2.在"高级"选项卡中,单击"环境变量":3.添加一个新的系统变量:POSTMAN_DISABLE_GPU=true4.关闭Postman ...

  9. C++基础之字符输出 cout

    找了一些视频教程,课程的示例是输入一个三角形 要输入这样的图案,思路是可以进行拆解,分为两三角形分别输出,先输入一部分,比如,先输出上半部分三角形: 找到规律,可以发现,上面的三角形是7颗星,也就是说 ...

  10. pandas 某几列转为json/dic 格式

    #%% import pandas as pd df=pd.read_excel('工作表.xlsx') col_list=list(df.columns) del_col_list =['c','d ...