UE4的NetWork简单原理

时间:2022-03-21 00:46:46

UE4内部封装实现了大量和网络相关的代码,使得我们不需要再自行编写很多底层的网络同步代码了(连接主机,套接字传输等等)。我们只需要实现相关的函数接口,并进行一定的设置,就可以实现网络游戏的功能了。
下面先讲讲UE4NetWork的几个要点:
*1.UE的网络同步和Replicate密切相关,从字面上理解,就是Server到Client的信息同步。
2.大部分的自动化Replicate都是Server到Client的,Client向Server发信息同步需要额外的设置。
3.GameMode只有一个实例,位于Server上。
4.默认情况下每当一个Client连接到Server,会在Server和Client两边都生成一个相应的Controller和玩家Actor。
5.Server拥有的Actor是源实例,别的Client上看到的是副本,是simulating copy。
6.相应地,如果一个Client连接到Server,会在Server和所有连接的Client上给他生成一个玩家Actor,但是注意了,该Client本地的这个Actor不是simulating copy,而其他Client中生成的这个副本是simulating copy。比如如果有3个玩家,一个Server两个Client,则第二个Client玩家的ActorC2在第一个Client玩家本地生成的ActorC2是simulating copy。但是第二个Client自己的ActorC2不是simulating copy,因为这个是源实例。别人的才是simulating copy。
7.Server在默认情况下(你不修改Actor内部实现)仅仅对远端(Remote)是simulating copy的Actor进行位置同步。比如,Server上连接了两个Client,Client1的玩家实例是ActorC1,Client2的是ActorC2。那么在Server和两个Client上都会同时存在3个Actor:ActorS,ActorC1,ActorC2。同步Replicate发生时,ActorC1不会把自己的位置同步给Client1,因为Server上的ActorC1查看了自己在远端Client1上的副本,发现不是simulating copy,所以不同步位置。而ActorC1在查看自己在远端Client2上的副本时,发现是simulating copy,所以会把自己的位置同步给Client2。*

那么我们现在实现一个最简单的网络同步试试:

1.创建一个项目,创建新的Pawn类。
2.在Pawn类的构造函数里添加一句:

//这个语句就是启动UE4内建的同步功能,同步功能是UE4内建在Actor类内部的,可以同步很多Actor的细节信息
//比如Actor的位置,角度,Attach顺序,是否模拟物理等等,这就是为什么Controller,GameMode等等都继承自Actor且被放进场景的原因,因为这样他们就可以享受Actor内部同步逻辑带来的好处
this->SetReplicates(true);

3.编写其他控制代码,使得Pawn可以相应操作,可以移动。
4.更改Play的Options,把玩家数改为2,窗口模式改为弹出额外窗口。
5.把DefaultPawnClass改为我们编写的可以网络同步的Pawn。(这一步是因为两个玩家都是UE自动Spawn的,他必须知道类型)
6.点击Play,可以看到弹出两个窗口,一个是Client一个是Server。
7.移动Server端游戏里的PawnS,可以发现Client上,那个PawnS也是移动的。
7.但是如果移动的是Client的PawnC,Server上的PawnC是不会动的,这就是我们提到的Replicate方向问题。自动的Replicate都是Server向Client进行同步的,Client不会实时向Server同步数据。
这一点很重要,因为Replicate都是Server向Client进行同步,所以我们大部分的运算和判定都应发生在Server上,然后Server把运算好的游戏世界同步到Client,而Client怎么通知Server输入或者一些重要的事件呢?

我们再编写下一个小程序:
1.直接修改刚才的源代码,对Client的输入监听函数进行一些修改:
原来的代码:

//控制这个Pawn向前移动,幅度为in
void moveforward(float in);
//控制水平移动的函数
void moveleft(float in);

修改为:

//这个依然是监听输入的函数,但是实现有变
void moveforward(float in);
//这个函数就比较重要了,他有Server修饰,表示这个函数即使被Client调用,也会在Server上处理,同理也有Client修饰符号
//WithValidation表示这个函数在调用远端相应函数时会进行有效性检查,有一种防止作弊的味道
//而reliable表示这个函数在本地调用之后,无论网络传输是否丢包,一定会在远端得到调用,是一个可靠传输
UFUNCTION(Server, WithValidation, reliable)
void Servermoveforward(float in);
//这个函数是Servermoveforward真正的实现代码,Servermoveforward本身没有实现!
void Servermoveforward_Implementation(float in);
//这个函数会在Server上调用,是判断Client发来的参数是否正确可信,就是上边WithValidation对应的检查函数
bool Servermoveforward_Validate(float in);

void moveleft(float in);
UFUNCTION(Server, WithValidation, reliable)
void Servermoveleft(float in);
void Servermoveleft_Implementation(float in);
bool Servermoveleft_Validate(float in);

cpp实现为:

void ANetWorkPawn::moveforward(float in)
{
if (in == 0)
return;

if (this->Role < ENetRole::ROLE_Authority)//看看是不是在Server上
{
this->Servermoveforward(in);//不是的话,让Server处理这个移动
}
else
this->Servermoveforward_Implementation(in);//是的话,自己处理这个移动
}

void ANetWorkPawn::Servermoveforward_Implementation(float in)
{
auto locnow = this->GetActorLocation();
auto dir = this->GetActorForwardVector()*in * 10;
this->SetActorLocation(locnow + dir);
}

bool ANetWorkPawn::Servermoveforward_Validate(float in)
{
//暂时先不考虑防作弊,直接返回true,如果返回false,Server会强制断开请求这个函数的Client
return true;
}

void ANetWorkPawn::moveleft(float in)
{
if (in == 0)
return;

if (this->Role < ENetRole::ROLE_Authority)
{
this->Servermoveleft(in);
}
else
this->Servermoveleft_Implementation(in);
}

void ANetWorkPawn::Servermoveleft_Implementation(float in)
{
auto locnow = this->GetActorLocation();
auto dir = this->GetActorRightVector()*in * 10;
this->SetActorLocation(locnow + dir);
}

bool ANetWorkPawn::Servermoveleft_Validate(float in)
{
return true;
}

可以看到,我们如果在Client进行前进输入,依然会调用到moveforward函数,但是moveforward已经不再是之前那样本地修改Actor的位置了,而是进行了Role的判断,Role是这个Actor所在位置的说明,如果是ROLE_Authority,表明这个Actor是Server上的Actor,如果不是,表明这个Actor是Client上的副本。所以我们判断当前调用输入的Actor到底是不是Server上的,如果是,我们直接进行移动逻辑(就是调用Servermoveforward_Implementation,因为移动逻辑都在这里),如果不是,我们调用Servermoveforward,这个函数看似是调用了本地的函数,但是因为他有UFUNCTION(Server)的修饰,UE将发送一些必要信息,使得这个函数在Server端被调用,于是Server端的Servermoveforward_Implementation就被调用了。而在Servermoveforward_Implementation中我们实现了移动逻辑。所以,Client在进行输入,他的输入请求被发送到了Server上相应的Actor内部,在那里进行了处理,之后Server又再次把Server端更新过的位置同步到Client(想想文章开头的simulating copy问题,这一句真的正确吗?),所以Client上的PawnC最后也移动了。

那么。。。如果真的照这个方法写的话,会发现一个问题:Client端移动PawnC,Server端的PawnC移动了(说明我们的远程调用是成功的),但是Client窗口中,他自己(PawnC)却纹丝未动。这就是文章开头提到的simulating copy身份问题,因为你拥有这个PawnC,所以在Server端看这个Actor的远端身份,不是simulating copy,所以不会把信息同步给你,我们看看UE4的Actor同步相关的源代码:

//Pawn的同步函数调用了其父类Actor的同步函数,而位置的同步也发生在底层--Actor中,所以我们找到这个函数
void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
{
UBlueprintGeneratedClass* BPClass = Cast<UBlueprintGeneratedClass>(GetClass());
if (BPClass != NULL)
{
BPClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
}

DOREPLIFETIME( AActor, Role );
DOREPLIFETIME( AActor, RemoteRole );
DOREPLIFETIME( AActor, Owner );
DOREPLIFETIME( AActor, bHidden );

DOREPLIFETIME( AActor, bTearOff );
DOREPLIFETIME( AActor, bCanBeDamaged );
DOREPLIFETIME( AActor, AttachmentReplication );

DOREPLIFETIME( AActor, Instigator );

//可以看到,同步是有条件的,只有当该Actor的远端(Remote)是simulated或者使用了物理引擎时,才进行位置同步
//而我们作为Client,拥有这个Actor,所以远端身份不是simulating copy,自然不会得到同步,PawnC在Client窗口也就不会移动了
DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement, COND_SimulatedOrPhysics );
}

UE为什幺这样写?因为本地拥有的Actor应该自己本地实现数据更新,之后在更新Server的数据,不能指望Server全部算好给你同步,这样可以节省不少的带宽。

那么我们的第二个程序怎样才能这正确呢?最简单的方法就是给Actor加上物理引擎模拟了

this->mesh->SetSimulatePhysics(true);

这样会强制使得Server端同步你的位置信息给你。因为那里的同步条件就是simulated或者使用了物理引擎。
最好的方法当然不是这样,我们应该在自己的Client端先进行移动逻辑,之后把自己的位置变动通知给Server,让Server更新自己那边的信息,把我们的位置移动转发到所有玩家哪里去(因为对于别的玩家,我们是simulating copy),代码更改为:

void ANetWorkPawn::moveforward(float in)
{
if (in == 0)
return;

//无论是Server还是Client,我们都先进行移动逻辑,如果是Server,他的移动也会被同步到Client,所以没有问题,如果是Client,他先自己移动一下,之后通知Server,让Server上和自己同步,达到所有人都同步的效果
auto locnow = this->GetActorLocation();
auto dir = this->GetActorForwardVector()*in * 10;
this->SetActorLocation(locnow + dir);
if (this->Role < ENetRole::ROLE_Authority)
{
this->Servermoveforward(in);//如果是Client,我们要通知Server我们移动了
}
}

void ANetWorkPawn::Servermoveforward_Implementation(float in)
{
auto locnow = this->GetActorLocation();
auto dir = this->GetActorForwardVector()*in * 10;
this->SetActorLocation(locnow + dir);
}

bool ANetWorkPawn::Servermoveforward_Validate(float in)
{
return true;
}

void ANetWorkPawn::moveleft(float in)
{
if (in == 0)
return;

auto locnow = this->GetActorLocation();
auto dir = this->GetActorRightVector()*in * 10;
this->SetActorLocation(locnow + dir);
if (this->Role < ENetRole::ROLE_Authority)
{
this->Servermoveleft(in);
}
}

void ANetWorkPawn::Servermoveleft_Implementation(float in)
{
auto locnow = this->GetActorLocation();
auto dir = this->GetActorRightVector()*in * 10;
this->SetActorLocation(locnow + dir);
}

bool ANetWorkPawn::Servermoveleft_Validate(float in)
{
return true;
}

一个小小的移动同步就要写这么多吗?是也不是,UE4的Character类中的几个移动函数是内建网络同步了的,不需要在进行多于处理。因为这里我们从Pawn级别开始写,所以就比较麻烦。

这里的代码和宏基本能实现简单的网络同步逻辑,但是我们可以看到,服务器等待,客户端接入,游戏开始等等功能的代码我们都没写,都是UE的editor帮我们直接搭建好的(在我们按下play的时候)但是一个网络游戏肯定不是一打开就连接好切地图环境都搭建好的,所以想制作一个完整的网络游戏我们还需UE4中很重要的一个部分,就是Session,wiki上有相关的介绍https://wiki.unrealengine.com/How_To_Use_Sessions_In_C%2B%2B,非常详细,Session就可以帮助我们实现从寻找房间,等待接入,玩家接入,发送地图,初始化地图环境等等功能了。