[UE4/UE5][网络]虚幻引擎的网络框架(虚幻的复制系统)
参考资料Multiplayer in Unreal Engine: How to Understand Network Replication - YouTube
虚幻引擎框架或者虚幻运行原理 - 知乎 (zhihu.com)
基本就是这个视频的笔记。
replicate的基本概念
gameserver
游戏服务器维护了绝对权威的世界状态。当游戏服务器的世界的状态改变,游戏服务器会把改变的状态传给每个客户端游戏的世界。这个过程称之为replicate(复制)。虚幻的复制系统让我们很容易开发出网络游戏,而不必去关注任何网络细节。你只要说我想要这个property replicate(复制)一下,然后它就可以被传输到客户端。
netmode(world的网络模式)
netmode是world的property之一。
World.h里的GetNetMode
netmode包含以下4个重要成员,NM_Standalone, NM_DedicatedServer, NM_ClientSever, NM_Client。
EngineBaseTypes.h
关于netmode的灵魂三问
这个游戏可以玩吗?
我们的gameinstance有一个localplayer吗?我们可以处理这个Player的input,并把世界渲染到viewport吗?
我们是一个服务器吗?换句话说,我们是否有最权威的世界副本,是否有gamemode这个actor?如果我们是服务器,我们是否对远程连接请求开放?其他的Player能加入,并扮演客户端?
这些问题的答案决定你的游戏的netmode。如下表格。
灵魂三问的表格
在我之前的一片文章中说过,engine的loadmap会去寻找一个url,这个url可以使本地的,也可以使远程的。
engine的loadmap
如果你的游戏已经连接了一个远程服务器,你的world就是NM_Client网络模式。所以你的world只能按照服务器的world来更新。
如果你的游戏本地加载世界,你的world就是NM_Standalone网络模式。因此你的游戏既是服务器也是客户端。你的游戏在本地运行,且不对任何外部请求开放。
但是如果你在本地运行,但是有监听选项,那么你的world就是listen sever网络模式。这个基本就是个NM_Standalone网络模式,但是别的本地游戏实例依然能作为客户端访问。
如果你的游戏实例是dedicate server网络模式,这个游戏实例既没有localplayer,也没有viewport。这只是一个服务器端的应用,玩家可以作为服务端连接。
区别图
虚幻复制系统基础
为了让虚幻复制系统有效运行,这三个类非常重要。UNetDriver,UNetConnection,UChannel。
一个server,两个客户端
无论客户端还是服务器都有GameEngine,而每个GameEngine都有自己的GameNetDriver。当server启动时,gameengine创建UNetDriver,UNetDriver开始监听远程连接请求。当客户端启动时,game engine同样创建UNetDriver,UNetDriver开始向服务器发送连接请求。一旦连接建立,server就会建立一个UNetConnection来维护连接。server会为每个客户端游戏建立一个UNetConnection。但是客户端只有一个UNetConnection来维护自己与服务器的连接。
每个UnetConnection有非常多的Channel,VoiceChannel,ActorChannel。
GameEngine.h
actor同步
如果你需要一个actor通过网络保持连接,你就需要设置bReplicates为TRUE。然后用IsNotRelevantFor来检测这个actor属于哪个player,然后通过netconnection中的actorchannel来交换信息。
Actor.h
Actor.h
如果服务器生成了一个actor,然后服务器会通知client要复制自己的actor。如果这个actor在服务器上被删除,客户端同样也会被删除。同样的,每个actor可能也有replicated properties, 这个property给我的感觉就是成员变量。如果actor的property被标记了replicated,那么如果服务器上的property改变,客户端的property也会随之改变。
ownership(所有权)
所有权对于actor的replication也很重要。但是ownership同样可以在runtime时设置。
playerController的ownership
playerController的所有权很重要。基本上,每个网络连接都代表着一个player。一旦这个player完全进入游戏,那个这个网络连接同样和playerController相连接。从服务器的角度来说,这个网络连接拥有这个playerController。除此之外,这个网络连接可以拥有这个playerController所拥有的所有actor。打个比方,如果你的游戏一个人物扔了个手榴弹,服务器可以追踪到这个手榴弹是你游戏人物扔的,并复制给其他所有的玩家。
相关性
并不是所有的actor都需要复制给每个客户端。所以我们这里需要设置相关性。但是有的actor就是对于所有的客户端都复制。比如下面这个。
PlayerState等actor对所有客户端复制
但是有些actor只对于一个客户端有相关性,所以这个actor只会复制给这个客户端。比如PlayerController这个actor只对自己的客户端有相关性,也只会复制给自己的客户端。
相关性还能被网络距离决定,如果一个actor没有设置相关性,但是如果你离一个actor过“近”的化,它也是和你有相关性。
更新频率和优先级
更新频率和优先级直接决定了服务器给相关客户端发送更新的频率。NetUpdateFrequency直接决定了服务器要多久check一下客户端的actor,并给他发送新的更新数据。但是网络延迟和网络带宽同样也是重要影响因素,所以玩游戏时常常会卡成ppt。服务器的netdriver会根据网络带宽和actor的优先级来决定放弃哪些数据的传播。优先级并不是固定的,比如离服务器近的actor就会有高优先级,很长时间没有更新的就有高优先级。但是同样的,你也可以手动设置这些actor的优先级。
手动设置NetPriority
如何设置replicated?
Property Replication | Unreal Engine Documentation
代码设置和蓝图设置
RPC (remote procedure calls)
如果你把一个方法设计为多播。当你在服务器上调用一个函数时,服务器将要发送信息让每个客户端调用同样的方法。多播RPC不能用于复制永久数据给所有客户端。
可靠RPC和不可靠RPC
这个就是TCP和UDP的区别。
RPC代码编写
RPCs | Unreal Engine Documentation
这个大致过程就是,我的客户端按攻击键,然后server_initiateAttack会启动RPC发给服务器,服务器会用后缀为
_validate的函数来验证然后执行。
这里你可以看到哪个方法在哪运行
Replicated Properties vs RPCs
Replicated Properties vs RPCs - Programming & Scripting / Multiplayer & Networking - Epic Developer Community Forums (unrealengine.com)
replicated properties同样和网络相关。尽管replicated properties可能发生延时,比如和你的pawn相关性没有了,但是一旦条件达成,最终会把property复制给你的pawn。可参见视频里的例子,那个例子视频里举得很好。
authority 和Role
一个actor可以有一些不同的role。但是在大部分情况下,你只需要考虑一个问题,我对这个actor有权限吗?
如果你运行一个actor的代码,你可以check一下权限,如果你有权限,你就有这个actor状态的绝对话语权。下图解释了gameinstance在什么网络模式下有权限。
权限代码
角色role代码
如果一个actor没有权限,那么它的role基本就是role_proxyProxy。
权限表格
autonomousProxy意味着这个客户端直接控制actor的移动和行为,尽管没有完全的权限
还有一个就是你的pawn是否为本地控制,如果为本地控制,GameInstance就在本地运行actor的代码。
如何在虚幻编辑器里测试多人游戏
numbers of players
number of players决定了有多少个GameInstance。
netmode
这个设置为paly as listen server,点击运行,你就可以看到两个player在地图上运行。点击一个就可以操作其中的一个,editor为listen server,而非editor为client。
两个player
online subsystem
什么是online subsystem?online subsystem是用于连接一些网络服务例如steam,xbox,ps等,它是一种抽象服务层,你只需要和unreal打交道,而不必和如steam的网络服务代码打交道。online subsystem同样为我们host sessions。比如steam就提供了一些如连接玩家,玩家列表等服务。
设置steam online subsystem
添加模块
DefaultEngine.ini里设置
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
DefaultPlatformService=Steam
bEnabled=true
SteamDevAppId=480
; If using Sessions
; bInitServerOnClient=true
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"
获取一个SubonlineSystem,并打印
成功,null并不代表这失败
在虚幻引擎中,确实有个online subsystem叫null, 不过要打包才能连接到steam
steam连接成功
create a session and join a session
delegate可以被认为是存储了函数的引用。delegate可以与函数绑定,它广播一个信号,让绑定函数执行,然后绑定函数再回复这个广播。这里的completedelegate实际上是用来接收成功消息,然后打印出来的。我用了两个steam账号和两台电脑实验成功的。
// called when pressed 1 key
if(!OnlineSessionInterface.IsValid())
{
return;
}
//检查是否有个session
auto ExistingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession);
if(ExistingSession != nullptr)
{
OnlineSessionInterface->DestroySession(NAME_GameSession);
}
//这里添加了一个用于接收session是否创建成功的delegate,这个delegate与一个函数绑定
OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);
//设置session
TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings());
SessionSettings->bIsLANMatch = false;
SessionSettings->NumPrivateConnections = 4;
SessionSettings->bAllowJoinInProgress = true;
SessionSettings->bAllowJoinViaPresence = true;
SessionSettings->bShouldAdvertise = true;
SessionSettings->bUsesPresence = true;
const ULocalPlayer *LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
// 创建session
OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings);
find a session
void AMenuSystemCharacter::JoinGameSession()
{
//Find Game sessions
if(!OnlineSessionInterface.IsValid())
{
return;
}
OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate);
SessionSearch = MakeShareable(new FOnlineSessionSearch());
SessionSearch->MaxSearchResults = 10000;
SessionSearch->bIsLanQuery = false;
SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
const ULocalPlayer *LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef());
}
当我们创建完成一个session时,我们可以travel到一个地图上,并等待其他玩家的到来
UWorld* World = GetWorld();
if(World)
{
World->ServerTravel(FString(&#34;/Game/ThirdPerson/Maps/Lobby?listen&#34;));
}
根据找到的game session并解决地址在哪
OnlineSessionInterface->GetResolvedConnectString(NAME_GameSession, Address)作为client加入进去
APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
if(PlayerController)
{
PlayerController->ClientTravel(Address, TRAVEL_Absolute);
}
GameInstanceSubsystem
Programming Subsystems | Unreal Engine 4.27 Documentation
上面我们其实把session的管理放到了character,这显示有些不合适,所以我们可以专门写个GameInstanceSubsystem来管理sessions。关于GameInstance的特点:
[*]在game创建的时候spawn
[*]不会被摧毁知道游戏被关闭
[*]当穿越不同的level的时候,GameInstance依然相同
总结
常用类表格
页:
[1]