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类的构造函数里添加一句:
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的输入监听函数进行一些修改:
原来的代码:
void moveforward(float in);
void moveleft(float in);
修改为:
void moveforward(float in);
UFUNCTION(Server, WithValidation, reliable)
void Servermoveforward(float in);
void Servermoveforward_Implementation(float in);
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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
cpp实现为:
void ANetWorkPawn::moveforward(float in)
{
if (in == 0)
return;
if (this->Role < ENetRole::ROLE_Authority)
{
this->Servermoveforward(in);
}
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)
{
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
可以看到,我们如果在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同步相关的源代码:
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 );
DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement, COND_SimulatedOrPhysics );
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
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;
auto locnow = this->GetActorLocation();
auto dir = this->GetActorForwardVector()*in * 10;
this->SetActorLocation(locnow + dir);
if (this->Role < ENetRole::ROLE_Authority)
{
this->Servermoveforward(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)
{
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
一个小小的移动同步就要写这么多吗?是也不是,UE4的Character类中的几个移动函数是内建网络同步了的,不需要在进行多于处理。因为这里我们从Pawn级别开始写,所以就比较麻烦。
这里的代码和宏基本能实现简单的网络同步逻辑,但是我们可以看到,服务器等待,客户端接入,游戏开始等等功能的代码我们都没写,都是UE的editor帮我们直接搭建好的(在我们按下play的时候)但是一个网络游戏肯定不是一打开就连接好切地图环境都搭建好的,所以想制作一个完整的网络游戏我们还需UE4中很重要的一个部分,就是Session,wiki上有相关的介绍https://wiki.unrealengine.com/How_To_Use_Sessions_In_C%2B%2B,非常详细,Session就可以帮助我们实现从寻找房间,等待接入,玩家接入,发送地图,初始化地图环境等等功能了。