第五次引擎编程尝试
在前面的章节中,我们已经尝试了制作特效、为游戏添加AI并设置AI的UI,到现在这一步我们希望游戏可以多人联网,我们一步步来。
开启多人游戏
通过这个我们可以看到角色移动在CS之间同步,但是真正运行的时候发现,距离真正的多人联网游戏还很遥远:
1)射击的时候CS端的弹道不同步
2)NPC的状态CS端不同步
3)游戏状态CS端不同步
... ...
针对上述问题,我们逐步来看:
让发射物在CS之间同步
原先的实现流程如下:
void AFPSTpl2Character::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) { ... ... // Bind fire event PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AFPSTpl2Character::OnFire); ... ... } void AFPSTpl2Character::OnFire() { ... ... // spawn the projectile at the muzzle World->SpawnActor<AFPSTpl2Projectile>(ProjectileClass, SpawnLocation, SpawnRotation, ActorSpawnParams); // try and play the sound if specified UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation()); AnimInstance->Montage_Play(FireAnimation, 1.f); ... ... }
在原先的流程中,`OnFire`接口和`IE_Pressed`这个事件绑定,指定事件触发之后本地执行接口`OnFire`,生成`APSTpl2Projectile`对象、播放音效、播放动画,但是在实际多人游戏过程中,子弹的生成应该是S端生成并通知C端,我们修改代码如下:
class AFPSTpl2Character : public ACharacter { ... ... protected: /** Fires a projectile. */ void OnFire(); UFUNCTION(Server,Reliable,WithValidation) void ServerOnFire(); ... ... } AFPSTpl2Projectile::AFPSTpl2Projectile() { ... ... SetReplicates(true); SetReplicateMovement(true); ... ... } void AFPSTpl2Character::ServerOnFire_Implementation() { // try and fire a projectile if (ProjectileClass != nullptr) { UWorld* const World = GetWorld(); if (World != nullptr) { if (bUsingMotionControllers) { const FRotator SpawnRotation = VR_MuzzleLocation->GetComponentRotation(); const FVector SpawnLocation = VR_MuzzleLocation->GetComponentLocation(); World->SpawnActor<AFPSTpl2Projectile>(ProjectileClass, SpawnLocation, SpawnRotation); } else { const FRotator SpawnRotation = GetControlRotation(); const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset); FActorSpawnParameters ActorSpawnParams; ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod:: AdjustIfPossibleButDontSpawnIfColliding; World->SpawnActor<AFPSTpl2Projectile>(ProjectileClass,SpawnLocation,SpawnRotation, ActorSpawnParams); } } } } bool AFPSTpl2Character::ServerOnFire_Validate() { return true; }
在上面的代码主要分为两部分,一个是定义了S端执行的接口,一个是设置字段属性同步:
SeverOnFire
ServerOnFire
是服务端执行的接口,这里分为三个部分:UFUNCTION
函数标签、ServerOnFire_Implementation
、ServerOnFire_Validate
UE的RPC机制
所谓RPC是指远程过程调用,即本地调用远端机器执行;从使用上看,调用端只需要关注远端开放的接口,而不需关注底层的网络细节,如:消息的序列化、反序列化、可靠传输等,UE通过UFUNCTION
的属性标签提供RPC机制给上层的开发者使用;这里属性标签包括:Server
、Client
、NetMulticast
,如:
Server
:意味着此接口是S端定义,C端调用
Client
:意味着这个接口是C端定义,S端调用
NetMulticast
:可以是S端调用,然后S和所有连接上来的C端执行;也可以是C端调用,但仅调用端本地执行
RPC可靠性
默认情况下UE4的RPC是不可靠的,需要保证有序性和送达性是需要reliable
关键字指定;指定reliable之后,RPC的调用是保送达,保有序的。
RPC的消息验证
WithValidation
是提供了RPC的参数验证,若是设置了,则需要实现FunctionName_WithValidate
接口;里面需要对输入的参数进行检查,若是返回false,则断开连接。
_Implementation
ServerOnFire
定义的是接口供对端远程调用用,而具体的处理逻辑则需要加上_Implementation
后缀
SetReplicates
在前面的逻辑中,我们提供了供C端调用的RPC,通过这种方式可以通知S端创建一个子弹对象,但是这个对象需要将属性同步给所有连接上来的客户端,此时我们就需要调用接口SetReplicates
若是一个AActor
调用接口SetReplicates
并设置参数为true
,则意味着这个Actor
会被同步到网络上的客户端,即此Actor
若是在服务器被创建,则创建消息也会被同步到客户端;此外若是在服务端状态被修改了,若是此属性是标记需要同步的(即UPROPERTY(Replicated)
),则修改也会被同步到客户端上;
可以看到后面设置了SetReplicateMovement
,即说明Actor
的运动数据是需要同步给客户端的
Property Replication
和属性同步相关的修饰符有以下几个:
NotReplicated
:仅适用于结构体的成员变量,用于说明指定字段不进行同步
Replicated
:意味着这个属性需要进行网络同步
ReplicatedUsing=FunctionName
:指的是,同步此属性之后,若是属性发生了更新,则回调指定的接口FunctionName
指定
ReqRetry
:仅用于结构体的属性,用于当结构体部分同步成功的时候决定是否进行重传(比如此结构体引用了其他对象,但其他对象还没来得及序列化时);对于简单结构,默认就是重传就可以了,但是对于复杂结构,可能同步数据量大,从节约带宽的角度,这个裁决权交给上层开发者。
每个Actor维护一个含有上述修饰符的属性列表,当属性列表里面的属性发生变化的时候,服务端会将这些属性同步给各个客户端;对于客户端来说,这个属性仅服务端可以修改;客户端修改属性时是不会被同步到服务端或者其他客户端的。
属性同步是可靠的数据传输,并且S端的属性同步是有固定频率的,即若是两次属性同步间隔中,某属性变更了多次,则C端只能获取到最终的属性值,中间的多次变化C端是感受不到的。
可调节的属性同步机制
NetUpdateFrequency
:为了能在带宽有限的场景下的游戏体验,UE4提供了NetUpdateFrequency
这个变量以便于游戏开发者针对不同对象提供差异化的属性同步服务,这个值越大,则更新频率越高,反之越小。通过为不同Actor
设置不同的更新频率,使得游戏内不同敏感度的对象有不同的同步速度,既保证带宽的高效利用,也对CPU的算力进行高效的分配;
MinNetUpdateFrequency
:最低的属性更新频率
Update Frequency Decrease Algorithm
:属性更新衰减算法,即若一个Actor
超过3s属性都没有变化,则会降低属性收集的频率,直到降到最低频率
Update Frequency Increase Algorithm
:属性更新强化算法,即在属性值变化较快,在两次更新之间也频繁发生变化,则自动增加属性收集和同步的频率。
条件属性复制
条件属性复制用于对属性复制过程进行更细化的控制;默认情况下,属性复制有一个内置条件,即属性不发生变化就不复制,但为了增加对属性复制的控制,UE4提供了一个宏DOREPLIFETIME_CONDITION
,里面的标志位提供了复制属性前一次额外的检查:
COND_OwnerOnly
:仅发送actor的所有者COND_SkipOwner
:除所有者之外的所有连接COND_SimulatedOnly
:仅发送给模拟actor
... ...
Connection Ownership
UE4的网络模块是一个比较复杂的模块,展开讲篇幅过大,这里简单介绍一下:
在UE4中,每个连接上来的客户端和服务器有一个connection
描述和管理连接,这个connection
会被分配一个player controller
,对于player controller
来说,他管理有一个pawn
,可以理解为玩家控制对象,对于游戏里面的物件,我们可以向上回溯,找到其所属的connection
。对于游戏中,角色、角色召唤出来的NPC以及角色身上的物品,他们都拥有同一个connection
;但是对于游戏中的AI控制的怪物、场景内不属于任何玩家的物件,他们是没有connection
的。
对于connection
,服务器需要知道这个connection ownership
,即连接的所有权,只有明确了这个所有权,才可以:
1)服务端可以确定在哪个客户端执行指定的RPC,以及客户端调用服务端的RPC最后作用于服务器哪个具体的对象;
2)当Actor
属性发生更新的时候,服务端可以决定哪些客户端可以获取这个更新的数据,UE有个bOnlyRelevantOwner
标识,当此标识设置为true
的时候表示仅actor
所属的connection
能获取到更新的信息,这个既节约了流量也节约了算力,还防止作弊;
3)条件属性复制的时候,选择将属性同步给指定的owner
拥有的connection
参考文献
RPCs | Unreal Engine Documentation
《Exploring in UE4》关于网络同步的理解与思考 - 知乎 (zhihu.com)
Property Specifiers | Unreal Engine Documentation
深入浅出UE4网络 - Leonhard- - 博客园 (cnblogs.com)