UE4多人游戏初探
本文主要阅读UE4官方文档对网络和多人游戏部分的介绍后的梳理和总结,希望能对UE4的多人游戏有个基本的认识。水平有限,欢迎指正。
1. 基本概念
刚开始阅读UE4文档,会对其中的许多专业术语感到迷惑。所以首先简单解释UE4中出现的术语,帮助理解。
actor对象
actor对象可以在场景中生成和放置,是基本的游戏对象,常见的包括:actor,pawn,character。actor是基类,pawn,character依次继承,提供了更多的功能呢。ue4也是以组件(component)的形式对actor赋予更多的功能。
蓝图类
游戏对象可以创建为蓝图类,也可创建为C++类。其本质是一样的,都可以看做c++类,不过蓝图可以图像化的方式编辑类。设置中打开blueprint nativization
,引擎会自动将蓝图类编译成C++类。
actor网络角色
一个replicated actor会同时在服务器和客户端创建,但分配不同的角色(role)。一般服务器actor的角色为ROLE_Authority
,控制了actor的状态,并向客户端的actor同步状态信息。客户端为ROLE_SimulatedProxy
或者ROLE_AutonomousProxy
。
remote role是actor在对应远端机器上对应的角色。比如在服务器上actor,Role == ROLE_Authority,RemoteRole == ROLE_SimulatedProxy。
因为ue4客户端和服务器是公用一套代码,所有经常需要判断角色来执行代码。 通过判断角色执行服务器代码和客户端代码:
//客户端服务器都会调用
void AshootCharacter::SetCurrentHealth(float healthValue)
{
if (Role == ROLE_Authority)
{
CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth);
OnHealthUpdate();}
else {
FString deathMessage = FString::Printf(TEXT("set CurrentHealth in client"));
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, deathMessage);
}
}
actor链接
actor需要找到对应的连接(owning connection
)来进行消息同步。每个connection都会创建一个PlayerController。一个acotr的most outer owner如果是PlayerController。那这个actor也被同样的connection拥有。这个connection就是actor的连接。
游戏模式和游戏状态
游戏模式在GameMode类中定义,定义了对客户端不可见的游戏规则。在多人模式中,GameMode只会存在服务器中。游戏状态在GameState类中定义,同时存在客户端和服务器,能同步游戏中的状态。它们都继承自actor。
2. 服务器模式
ue4用client-server的模式实现服务器。底层支持TCP,UDP协议。默认为UDP协议。分为下面两类:
- 监听服务器(Listen server):即作为玩家,也作为服务器。
- 专用服务器(dedicated server):没有client玩家,只执行服务器相关代码,性能更高。
服务器和客户端公用一套代码,适合前后端不分工的开发模式,能快速开发出游戏。但不利于前后端分工和后端调优。
除非完完全全确定这个项目不需要多人功能,最好按照多人模式编码。避免后期再加入多人功能可能需要重构整个代码。
3. 状态同步
ue4用replication来在服务器和客户端之间同步游戏的状态,主要以actor为单位来同步。可以分为自动变量同步和手动RPC调用。
变量同步
对变量或者object的引用,指定为replicated,就会由服务器向客户端自动同步变量的值。值得更新是可靠的。如果属性值快速变化,不会将每个变化值同步,而会优化为只同步最终的值。
通过对变量注解来表明变量replication,有replicated,reqNotify两种。reqNotify,当变量变化时,还能调用额外的通知方法。变量变化之前的值可以通过preNetReceive
来缓存。
在注册变量replication的时候,可以调用DOREPLIFETIME_CONDITION
加入额外的同步条件制属性如何更新。但只能有限的控制。ue4在更多的控制和更好的性能中做了平衡。
如何要获取变量变化之前的值,可以在PreReplication
方法中缓存变量更新之前的值。
Variable Replication in C++:
class ENGINE_API AActor : public UObject
{
/** The player's current health. When reduced to 0, they are considered dead.*/
UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth)
float CurrentHealth;
UPROPERTY(replicated )
AActor * owner;
/** RepNotify for changes made to current health. the server will not receive the RepNotify.*/
UFUNCTION()
void OnRep_CurrentHealth();
};
void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
{
DOREPLIFETIME( AActor, owner );
//增加条件,减少更新的频率,降低带宽
DOREPLIFETIME_CONDITION( AActor, CurrentHealth, COND_SimulatedOnly );
}
RPC
除了变量同步还可以手动调用RPC来同步消息。RPC有三种形式:
- Client,服务器向客户端调用。
- Server,客户端向服务器调用,只有automonous的能调用。
- NetMulticast,服务器向所有连接客户端调用。
RPC分为可靠和不可靠。可靠的RPC会将消息压入队列,如果网络情况太差,队列数量超过阈值会断开连接。尽量选用不可靠的RPC已达到更好的性能。
服务端调用的RPC可以增加Validation函数来检查客户端的参数是否合法,不合法返回false断开客户端连接,防止外挂的非法调用。
RPC in C++:
class AExampleClass{
//Declaration of Server RPC MyFunction.
UFUNCTION(Server, Reliable, WithValidation)
void MyFunction(int myInt);
}
//Implementation of Server RPC MyFunction.
void AExampleClass::MyFunction_Implementation(int myInt)
{
//Gameplay code goes here.
}
//Validation of Server RPC MyFunction
bool AExampleClass::MyFunction_Validation(int myInt)
{
return myInt >= 0;
}
actor组件同步(component replication)
静态组件默认都会在服务器和客户端创建。只有当组件的属性和事件需要同步的时候,才用设置replication。
组件也能直接使用变量同步和RPC同步,组件同步会有额外的消耗,需要包括额外NetGUID
,所有在性能相关的地方,尽量不适用组件同步。例如角色移动组件中,移动组件通过组件的ower actor来代理RPC以减少组件RPC带来的额外消耗。
Online Beacon
特殊的actor,提供和服务器更轻量的交互。可以在游戏中请求其它玩家的名字列表,游戏的进行时间等。具体的介绍文档很少,只有深入底层实现才能更好的描述使用场景。
角色移动同步
ue4在角色移动组件里实现了一套角色移动的同步,大大方便开发多人游戏。实现了一些常见的移动方式:走路,跳,游泳,飞,也可以整合自定义移动方式。
角色移动情景如下:
sequenceDiagram
participant client1 as 客户端(autonomous)
participant server as 服务器
participant client2 as 其它客户端(simulated)
Note over client1: 移动并缓存移动数据
client1->>server: ServerMove unreliable RPC
Note over server: 根据数据重现移动
Note over server: 和上报位置是否一致
Note over server: 不一致则发送修正
server-->>client1: adjust or ack RPC
server->>client2: 同步ReplicatedMovement结构体
Note over client1: 修正位置
Note over client2: 根据结构体移动
一个角色的移动在主控端(autonomous proxy)和其它端(simulated proxies)的同步方式是不一样的。其他端相对比较简单,只需要保持更新角色位置就行了。而主控端必须在位置修正和玩家平滑体验找到最佳平衡点。
角色的移动同步,有大量可以调节的参数和可以重载的方法。包括位置同步频率,误差允许范围,位置修正频率,移动插值方法等。对于特定的项目,应该基于项目的特点,调整参数甚至重写方法。既保证客户端体验又保证服务器性能。
4. 相关优化
在大型多人游戏里,一个场景会有多个连接和很多需要同步的actor,对服务器的带宽和cpu都有非常大的压力,ue4文档介绍了如下的几种优化方法。
相关性(Relevancy)
actor的相关性的检测,应该是减少带宽最明显的方式。场景有时候会包含有非常多的actor,但实际上只有小部分的actor会影响到指定玩家。我们需要检测每个actor的相关性,剔除不相关的actor,只同步会对玩家造成影响的actor,来大大减少带宽。
UE4每帧会调用UNetDriver::ServerReplicateActors
(NetworkDriver.cpp
)对所有的actor做相关性检测,最后只将相关actor变化的值调用UChannel::ReplicateActor
发送到对应的链接。
UE4在AActor::IsNetRelevantFor()
对相关性检测做了默认实现,可以除了按照相关性配置进行检查,然后用距离检测来做actor剔除。
bool AActor::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
if (bAlwaysRelevant || IsOwnedBy(ViewTarget) || IsOwnedBy(RealViewer) || this == ViewTarget || ViewTarget == GetInstigator())
{
return true;
}
else if (bNetUseOwnerRelevancy && Owner)
{
return Owner->IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
}
else if (bOnlyRelevantToOwner)
{
return false;
}
.....
return !GetDefault<AGameNetworkManager>()->bUseDistanceBasedRelevancy ||
IsWithinNetRelevancyDistance(SrcLocation);
}
优先级
每个actor都能分配网络优先级,优先级越高,就会相比其他actor占用更多的网络带宽。优先级高的actor更可能被同步。
网络优先级在AActor::GetNetPriority
中进行计算。避免在极端情况,某些低优先级的actor永远得不到更新,网络优先级还会乘以等待actor等待同步的时间。同时网络优先级还会考虑actor和玩家的距离和位置。
同步图(replication graph)
replication graph将角色根据游戏和角色的特点,动态的分离到不同的组。这样在更新的时候对每个客户端不用遍历所有的actor来判断相关性,以节省CPU的消耗。
根据具体的游戏特点,会有多种不同的优化方法:
- 比如格子地图,可以根据actor在哪个格子,分配到不同的组。
- 将休眠的actor(在和玩家交互前都不需要同步),分配在单独的组。
- 如果能拾起或者穿戴actor,在穿戴后将他们放在一个组。
- 将所有可见或所有不可见的actor,单独成组。
5. 感受
通过ue4自带的同步框架,能够快速搭建出多人游戏,但是前后端的代码混在一起却不利于前后端的分工和后端的优化。除了状态同步之外,ue4引擎没有现成的数据库和缓存架构,单一的client-server的服务器架构,缺少服务器间的调用。如果要完成大型多人游戏,还需要为ue4填充额外的功能。
参考
- 官网文档Networking and Multiplayer: https://docs.unrealengine.com/en-US/Gameplay/Networking/index.html
- 数天ue4课程:http://school.digisky.com/home/course?id=12
- example: https://docs.unrealengine.com/en-US/Resources/Showcases/BlueprintMultiplayer/index.html
- onelin beacon: https://forums.unrealengine.com/community/community-content-tools-and-tutorials/1355434-onlinebeacons-tutorial-with-blueprint-access