cuihaoran 发表于 2023-3-22 08:38

[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("/Game/ThirdPerson/Maps/Lobby?listen"));
                }
根据找到的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]
查看完整版本: [UE4/UE5][网络]虚幻引擎的网络框架(虚幻的复制系统)