UE4中C++的网络同步、RPC
一、需要知道的概念
(1)
如果生成子弹这个函数是普通函数且子弹在构造函数中设置了可复制,那么网络同步后带来的后果是:
如果在服务端窗口运行,生成的子弹客户端也能看到,因为生成函数在服务端执行,自动复制给客户端
如果在客户端窗口运行,生成的子弹服务端无法看到,因为生成的函数在客户端运行,没法复制给服务端
因此生成应该放在server端
(2)
连接过程
如果一个服务器需要从网络连接的角度实现某种目的,它就必须要有客户端连接!
当新的客户端初次连接时,会发生一些事情。首先,客户端要向即将连接的服务器发送一个请求。服务器将处理这条请求。如果它不拒绝连接,服务器会向客户端发回一个包含了继续运行所需信息的响应。
主要步骤如下:
-
客户端发送连接请求。
-
如果服务器接受连接,则发送当前地图。
-
服务器等待客户端加载此地图。
-
加载之后,服务器将在本地调用 AGameMode::PreLogin。
-
这样可以使 GameMode 有机会拒绝连接
-
-
如果接受连接,服务器将调用 AGameMode::Login
-
该函数的作用是创建一个 PlayerController,可用于在今后复制到新连接的客户端。成功接收后,这个 PlayerController 将替代客户端的临时 PlayerController (之前被用作连接过程中的占位符)。
-
此时将调用 APlayerController::BeginPlay。应当注意的是,在此 actor 上调用 RPC 函数尚存在安全风险。您应当等待 AGameMode::PostLogin 被调用完成。
-
-
如果一切顺利,AGameMode::PostLogin 将被调用。
-
这时,可以放心的让服务器在此 PlayerController 上开始调用 RPC 函数。
-
这里第五点找到服务器与客户端之间的这个连接。这个链接的信息就存储在PlayerController的里面,而这个PlayerController一定是客户端第一次链接到服务器,服务器同步过来的这个PlayerController(也就是上面的第五点,后面称其为拥有连接的PlayerController)。进一步来说,这个Controller里面包含着相关的NetDriver,Connection以及Session信息。
对于任何一个Actor(客户端上),他可以有连接,也可以无连接。一旦Actor有连接,他的Role(控制权限)就是ROLE_AutonomousProxy,如果没有连接,他的Role(控制权限)就是ROLE_SimulatedProxy 。
(3)官网的一段话值得思考:
连接所有权对于 RPC 这样的机制至关重要,因为当您在 actor 上调用 RPC 函数时,除非 RPC 被标记为多播,否则就需要知道要在哪个客户端上执行该 RPC。它可以查找所属连接来确定将 RPC 发送到哪条连接。
连接所有权会在 actor 复制期间使用,用于确定各个 actor 上有哪些连接获得了更新。对于那些将 bOnlyRelevantToOwner 设置为 true 的 actor,只有拥有此 actor 的连接才会接收这个 actor 的属性更新。默认情况下,所有 PlayerController 都设置了此标志,正因如此,客户端才只会收到其拥有的 PlayerController 的更新。这样做是出于多种原因,其中最主要的是防止玩家作弊和提高效率。
(4)
Actor 的复制过程中,有两个属性扮演了重要角色,分别是 Role
和 RemoteRole
。
就目前而言,只有服务器能够向已连接的客户端同步 Actor (客户端永远都不能向服务器同步)。始终记住这一点, 只有 服务器才能看到 Role == ROLE_Authority
和 RemoteRole == ROLE_SimulatedProxy
或者 ROLE_AutonomousProxy
。
例如,如果您的服务器上有这样的配置:
-
Role == ROLE_Authority RemoteRole == ROLE_SimulatedProxy
与此同时,客户端会将其识别为以下形式:
-
Role == ROLE_SimulatedProxy RemoteRole == ROLE_Authority
Actor身上还有一个RemoteRole来表示他的对应端(如果当前端是客户端,对应端就是服务器,当前端是服务器,对应端就是客户端)
(5)
服务器不会在每次更新时复制 actor。这会消耗太多的带宽和 CPU 资源。实际上,服务器会按照 AActor::NetUpdateFrequency
属性指定的频度来复制 actor。
因此在 actor 更新的间歇,会有一些时间数据被传递到客户端。这会导致 actor 呈现出断续、不连贯的移动。为了弥补这个缺陷,客户端将在更新的间歇中模拟 actor。
目前共有两种类型的模拟。
ROLE_SIMULATEDPROXY
这是标准的模拟途径,通常是根据上次获得的速率对移动进行推算。当服务器为特定的 actor 发送更新时,客户端将向着新的方位调整其位置,然后利用更新的间歇,根据由服务器发送的最近的速率值来继续移动 actor。
使用上次获得的速率值进行模拟,只是普通模拟方式中的一种。您完全可以编写自己的定制代码,在服务器更新的间隔使用其他的一些信息来进行推算。
ROLE_AUTONOMOUSPROXY
这种模拟通常只用于 PlayerController 所拥有的 actor。这说明此 actor 会接收来自真人控制者的输入,所以在我们进行推算时,我们会有更多一些的信息,而且能使用真人输入内容来补足缺失的信息(而不是根据上次获得的速率来进行推算)
C++代码中,使用一个Actor::Role与一个枚举值进行比较。我们进一步看一下枚举值还有什么。
-
UENUM() enum ENetRole { /** No role at all. */ ROLE_None, /** Locally simulated proxy of this actor. */ ROLE_SimulatedProxy, /** Locally autonomous proxy of this actor. */ ROLE_AutonomousProxy, /** Authoritative control over the actor. */ ROLE_Authority, ROLE_MAX, };
其中的ROLE_NONE表示这个对象不扮演网络角色,不参与同步(如果该actor没有setReplicated(true)时,Role就是Role_NONE)。ROLE_SimulatedProxy表示它是一个远程机器上的一个复制品,它没有权利来改变一个对象的状态,也不能调用RPC。ROLE_AutonomousProxy表示它既可以完成ROLE_SimulatedProxy的工作,又可以通过RPC来修改真正Actor的状态。
举例:
开启两个客户端一个服务端
在playerCharacter中打印出各自的Role状态
if (Role == ROLE_Authority) {
UKismetSystemLibrary::PrintString(this, TEXT("authority"));
}
if (Role == ROLE_AutonomousProxy) {
UKismetSystemLibrary::PrintString(this, TEXT("AutonomousProxy"));
}
if (Role == ROLE_SimulatedProxy) {
UKismetSystemLibrary::PrintString(this, TEXT("SimulatedProxy"));
}
if (Role == ROLE_None) {
UKismetSystemLibrary::PrintString(this, TEXT("None"));
}
服务端和两个客户端
,每个窗口产生3个人,服务端的人全部是Role_Authority状态,在每个客户端的窗口里有一个自己Role_AutonomousProxy,和另外2个玩家Role_SimulatedProxy
对于RPC函数,一共有三种RPCs,分别为Server函数,Client函数,Multicast函数。Server函数是客户端调用,服务器执行。所有客户端和服务器都同时拥有一个Actor的实例,服务器的网络角色为ROLE_Authority,而客户端有两种,一种为ROLE_SimulatedProxy,这种角色只能用来接收服务器给它同步的信息,而不能向服务器发送信息。而ROLE_AutonomousProxy角色,不仅可以用来接收服务器给它同步的信息,还可以利用调用Server函数,让服务器自行执行一段预设的代码,这个过程就是Server函数。
摘自https://blog.csdn.net/liulong1567/article/details/73201022
(6)
RPC要求和注意事项
您必须满足一些要求才能充分发挥 RPC 的作用:
-
它们必须从 Actor 上调用。
-
Actor 必须被复制。
-
如果 RPC 是从服务器调用并在客户端上执行,则只有实际拥有这个 Actor 的客户端才会执行函数。
-
如果 RPC 是从客户端调用并在服务器上执行,客户端就必须拥有调用 RPC 的 Actor。
-
多播 RPC 则是个例外:
-
如果它们是从服务器调用,服务器将在本地和所有已连接的客户端上执行它们。
-
如果它们是从客户端调用,则只在本地而非服务器上执行。
-
现在,我们有了一个简单的多播事件限制机制:在特定 Actor 的网络更新期内,多播函数将不会复制两次以上。按长期计划,我们会对此进行改善,同时更好的支持跨通道流量管理与限制。
-
下面的表格根据执行调用的 actor 的所有权(最左边的一列),总结了特定类型的 RPC 将在哪里执行。
从服务器调用的 RPC
Actor 所有权 | 未复制 |
|
|
|
---|---|---|---|---|
Client-owned actor | 在服务器上运行 | 在服务器和所有客户端上运行 | 在服务器上运行 | 在 actor 的所属客户端上运行 |
Server-owned actor | 在服务器上运行 | 在服务器和所有客户端上运行 | 在服务器上运行 | 在服务器上运行 |
Unowned actor | 在服务器上运行 | 在服务器和所有客户端上运行 | 在服务器上运行 | 在服务器上运行 |
从客户端调用的 RPC
Actor 所有权 | 未复制 |
|
|
|
---|---|---|---|---|
Owned by invoking client | 在执行调用的客户端上运行 | 在执行调用的客户端上运行 | 在服务器上运行 | 在执行调用的客户端上运行 |
Owned by a different client | 在执行调用的客户端上运行 | 在执行调用的客户端上运行 | 丢弃 | 在执行调用的客户端上运行 |
Server-owned actor | 在执行调用的客户端上运行 | 在执行调用的客户端上运行 | 丢弃 | 在执行调用的客户端上运行 |
Unowned actor | 在执行调用的客户端上运行 | 在执行调用的客户端上运行 | 丢弃 | 在执行调用的客户端上运行 |
这个表怎么理解:
比如owned by invoking client 这行,owned by invoking client表示执行RPC的actor是ROLE_AutonomousProxy,在客户端上调用,未复制、多播、服务器、本地客户端四种事件的效果
如果执行RPC的actor是ROLE_SimulatedProxy(owned by a different client ),那么调用服务端事件会被丢弃
(7)
Pawns
(包括 Characters
)也存在于服务器和所有客户端之上,而且也可以包含复制的变量和事件。至于对特定变量或事件使用 PlayerState
还是 Pawn
,则取决于具体情况,但最主要的问题是,PlayerState
将在玩家连接时一直保留下去,而 Pawn
就不一定了。例如,如果一个玩家人物在游戏中死亡,他所控制的 Pawn 就可能被销毁,并在玩家重生时重新创建。
(8)
Reliable 事件一定会抵达目的地(假设遵守了上述所有权规则),但为了确保可靠性,它们会占用更多的带宽,而且有可能造成延迟。您应当避免太过频繁的发送可靠事件,例如每个时钟单位发送一次,因为引擎内部的可靠事件缓冲区可能发生溢出 - 这时,相关的玩家就会断开连接!
Unreliable 事件同样名符其实:一旦出现某些状况,如网络 数据包丢失,或者引擎确信要发送较多的高优先级流量,它们也许就无法抵达目的地。因此,不可靠事件比可靠事件使用的带宽更小,也可以更经常被安全地调用。
(9)
Authority和server的区别
当actor设置为可复制时,一般来说authority和server都是指的服务器,因为服务器端的actor是权威的
但是当actor设置为不可复制,authority指的就是指actor所在的客户端,因为不可复制的actor事件是在本地客户端执行的,此问题一定要引起注意
二、在C++中的RPC如何使用
(1)如果一个actor拥有复制属性且移动时也能同步,在构造函数中
-
SetReplicates(true); SetReplicateMovement(true);
(2)actor中的变量要有复制属性,加上Replicated
-
UPROPERTY(Replicated,BlueprintReadOnly,Category="Gameplay") bool bIsCarryingObjective;
如果变量设置复制的时候,还要在.cpp中加上复制的规则,在DOREPLIFETIME的第一个参数是该actor,第二个参数是要复制的变量名,DOREPLIFETIME是一般属性复制,第二个 DOREPLIFETIME_CONDITION是条件属性复制
添加这个复制规则的好处是
1、节省带宽,不需要复制的物体就不复制属性,因为我们确信拥有此 actor 的自治代理版本的客户端无需了解这个属性
2、对于不接受属性的客户端而言,服务端无需干涉这个客户端的本地复本
为了加强对属性复制的控制,您可以使用一个专门的宏来添加附加条件。
这个宏被称为 DOREPLIFETIME_CONDITION
如下,COND_SimulatedOnly表示在复制属性前执行一次额外检查。这时,它只会复制到拥有此 actor 模拟复本的客户端
-
void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const { DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement, COND_SimulatedOnly ); }
这个COND_SimulatedOnly条件可以替换为其他条件,如下:
还有一个名叫 DOREPLIFETIME_ACTIVE_OVERRIDE
的宏,利用您想要的任何定制条件来决定何时复制/不复制某个属性
-
void AActor::PreReplication( IRepChangedPropertyTracker & ChangedPropertyTracker ) { DOREPLIFETIME_ACTIVE_OVERRIDE( AActor, ReplicatedMovement, bReplicateMovement ); }
现在 ReplicatedMovement 属性只会在 bReplicateMovement 为 true 时复制。
(3)如果函数要在服务端运行,函数这样声明
-
//WithValidation表示这个函数在调用远端相应函数时会进行有效性检查,有一种防止作弊的味道 //而reliable表示这个函数在本地调用之后,无论网络传输是否丢包,一定会在远端得到调用,是一个可靠传输 UFUNCTION(Server,Reliable,WithValidation) void ServerFire();
而且注意,如果是在服务端运行的函数,函数实现的名字必须后面加上_Implementation,把在服务端运行的逻辑写到下面函数里
-
void AFPSCharacter::ServerFire_Implementation() { }
同时,如果加了WithValidation关键字,除了实现ServerFire_Implementation这个函数,还需要实现函数名_Validate,只不过返回值为bool,返回为true,它会在服务端进行完整性检查时用到,如果客户端return false这一反馈时,服务端会取消连接,因为出现了问题,比如某处使用外挂,或出现了严重问题,它会强硬的断开
-
bool AFPSCharacter::ServerFire_Validate() { return true; }
WithValidation
关键字是RPC 增加验证函数的功能,比如如下,如果加的血大于一定血量,就强行断开
-
bool SomeRPCFunction _Validate( int32 AddHealth ) { If ( AddHealth > MAX_ADD_HEALTH ) { return false; // This will disconnect the caller } return true; // This will allow the RPC to be called }
UE4要求客户端 -> 服务器 RPC 具有一个 _Validate 函数。这样是为了鼓励使用安全的服务器 RPC 函数,同时尽可能方便其他人 添加代码以检查所有参数,确保其符合所有已知的输入限制。
最后在该调用的时候调用下面的函数即可
ServerFire();
(4)创建一个多播函数
-
UFUNCTION(NetMulticast,Reliable) void sdfsf();
函数实现后面加上_Implementation
-
void AFPSGameState::sdfsf_Implementation() { }
(5)对于回调函数如OnComponentBeginOverlap等函数或蓝图中的事件,它们既在客户端上执行也在服务端执行,因此对于生成销毁的操作只在服务端上写逻辑即可,如下:
-
void AFPSProjectile::OnHit(UPrimitiveComponent* HitComp,AActor* OtherActor,UPrimitiveComponent* OtherComp,FVector NormalImpulse,const FHitResult& Hit) { if(Role == ROLE_Authority) { Destroy(); } }
(6)、这个意思是如果是非本地角色操控执行,比如客户端2看到客户端1时,此时看到的客户端1角色不是本地的controlled,就执行里面的代码
-
if(!IsLocallyControlled) { }