目录
前言
作为一个游戏开发小白,写这个专栏的初心是总结这三个月自己从零开始接触联机系统开发过程中遇到的困惑和磕磕绊绊,尽可能把自己总结的经验和参考大佬们的思路系统地整理出来,以供日后复习纠察。
为什么需要联网
即使是完全单人游玩的单机游戏也有必要实现联网功能,如女神异闻录、哈迪斯这类单人游戏,在游戏开发时通过网络连接至steam、epic或专用服务器,玩家可以享受云存档功能,开发者也可获取玩家游玩数据以反馈游戏设计。双人成行、糖豆人等多人竞技合作类游戏注重人与人的强连接,因此有着无限潜力;艾尔登法环、死亡搁浅等单机流程类游戏也加入了玩家之间的互动元素,这类联机设计更加巧妙,中和了玩家在虚拟世界中的孤独感;2077、泰坦陨落2、P5R等纯单机游戏,联网可以带来云存档、玩家统计信息、防盗版等好处。总而言之无论是纯单机、弱联机还是强联机游戏,游戏开发中实现联网功能可以带来一些便利。
联网的两种形式
我这里把联网分为两种形式,一类是与后端通信获取相应服务如成就系统/排行榜/游戏大厅与匹配等,一类是多个玩家出现在同一个画面中共同影响游戏世界,并可以互相通信交互的联机体验。本质上联网就是两个网络IP之间的数据通信。
(一)与后端/平台服务器通信
①使用游戏平台免费的后端服务:如通过Steam服务器解锁成就、创建易于管理的云排行榜,或通过Epic平台使用其语音服务、反作弊和惩罚服务等。通常使用Steam或Epic的后端服务只需要在项目代码中接入平台的SDK,初始化后通过RPC(远程过程调用)调用函数向平台服务器请求服务,非常方便。这种方法优点是不要钱,缺点就是网络可靠性较差,毫无扩展性。
以使用Steam平台的后端服务为例:
链接至 steam_api[64][.dll/.so/dylib]
后,它将提供对一小部分 C 语言函数的访问权限,这些函数前缀为 S_API
宏,均在 steam_api.h
和 steam_gameserver.h
中暴露。 steam_api
模块本身很小,只暴露基本功能,以初始化和关闭 Steamworks API。
Steamworks API 在初始化时,会找到正在运行的 Steam 客户端进程,并从该路径载入 steamclient.dll
。 steamclient.dll
本质上是 Steam 客户端的核心引擎, 包含并维护运行 Steam 所需的信息。 Steam 客户端 UI 用与此处暴露的函数类似的一个函数集访问 steamclient.dll
提供的数据。 由于此数据在 Steam 进程中,因此所有 Steam API 调用都透明封送并通过 RPC/IPC 机制发送 (跨进程管道 ISteamClient::HSteamPipe)。
Steam 进程通过 steamclient.dll
保持与 Steam 后端服务器的持续连接。 所有验证、匹配、好友列表与 VAC 通信均通过此连接发生。 如果用户有网络问题,或者如果用户连接至的 Steam 服务器要更新,此连接会断开。 如果连接断开或重新连接,回调将发布至任何运行的应用。 Steam 客户端会自动尝试重新连接,且正在运行应用的客户端有优先权,因此,重新连接时间一般不超过 10 秒,但在一些罕见的情况下,可能会长达 5 分钟。
②连接到专用服务器:如果对联机要求比较高的话,技术和资金允许的情况下还是自己搭建游戏服务器好一些,可以将用户数据上传至数据库,针对网络环境做进一步优化,玩家体验也会更好。
(二)玩家之间通信(联机)
玩家之间的网络通信一般是基于服务器-客户端模型的,在多人游玩的场景中,只有服务器端的游戏状态是唯一视作“绝对正确的权威”,比如FPS游戏中开枪击中敌人,只有服务器上判定击中才会同步到所有客户端,作为客户端的计算机上运行的游戏画面和状态都是由服务器发来的数据确定的。由此便出现了两种联机方案:基于专用服务器的和基于P2P的联机。UE5对于P2P的联机架构已经非常成熟,因此在小型联机游戏中使用P2P方案十分便于开发。之后会专门写一篇UE5联机架构的总结,在这里先提一下概念。
①专用服务器:
一台专门的计算机作为主持网络多人游戏会话的服务器运行。其接受远程客户端中的连接,但无本地玩家,因此为了高效运行,其将废弃图形、音效、输入和其他面向玩家的功能。此模式常用于需要更固定、安全和大型多人功能的游戏。
专属服务器成本更高,更难以配置,需要独立于所有参与玩家的计算机,并需要完成自身网络连接。但所有加入专属服务器的玩家均使用相同类型的连接进行游戏,从而保证了公平性。由于专属服务器不会渲染图形或执行仅与本地玩家相关的其它逻辑,因此还可高效处理gameplay和网络。因此,出于安全、公平或可靠方面的原因,专属服务器更适用于需要大量玩家或需要高效执行、可信服务器的游戏。此类游戏包括MMO、竞技MOBA,或快节奏网络射击游戏。
②P2P(端到端):
运行游戏的计算机作为主持网络多人游戏会话的服务器运行,其本身也是自己的客户端。就是玩家自己可以作为专用服务器接受远程客户端中的连接,且直接在服务器上拥有所有玩家的数据。客户端玩家的输入逻辑、伤害判定等等gameplay逻辑均在作为服务器的玩家计算机上处理,因此具有一定网络延迟,同时为服务器增加了额外的运算负载。此模式通常用于临时合作和竞技多人游戏如双人成行、四位一体、原神(大概也是P2P?)。
UE5网络架构概述
网络世界中的两个用户(或者玩家和平台后端)可以通过(服务端监听/客户端请求连接)IP地址和端口号建立Socket连接,从而实现服务端和客户端的数据通信。UE5底层也是通过Socket通信和三次握手机制实现了两个IP地址的网络连接,基于UDP协议在服务端和客户端之间实现了一种可靠的数据传输。UE将网络连接与数据通信封装在UNetDriver、UNetConnection、UChannel等类中进行管理,要想理解这些类的作用,我们还是要从最底层开始熟悉:服务器客户端是如何建立起连接的?一个数据是如何进行传输的?
(一)基本的Socket通信
(二)基本的数据传输
通过socket通信,虽然双方已经建立了连接,可以直接收发数据了,但需要一套标准来指引原始数据在网络海洋中正确到达目的地。
以一个例子通俗地描述这一过程:客户端A向服务端B发送一句“Hello”
1.首先我们需要将这个字符串信息转码为二进制的数据,这样计算机之间才能通过串口的电信号正确收发二进制的数字信息。
2.接着我们需要给原始数据加上TCP/UDP通信协议头(或自定义标准),告知此消息是该计算机上哪类应用程序(进程)产生的(源端口号)、要发往对方的哪类应用程序(目标端口号)、序列号和确认号(TCP协议中用来保证接收数据的有序性)、控制位(TCP协议中用来控制网络的连接和终止)、校验位(用来检测数据在传输过程中是否发生错误)等等一些额外的数据,从而在原始数据的传输过程中实现校验、重传、流量控制等功能。
3.数据的基本包装完成了,即将在摆渡人(路由器和交换机)的指引下旅行到遥远的网络的另一头,于是我们把数据包的所在地和目的地地址也添加在数据中,这就是IP协议头。摆渡人根据数据包给出的IP地址,在网络世界中辗转腾挪,最终才能顺着网线到达目的地。
4.IP地址代表着一台计算机在网络中的位置,数据包虽然通过IP协议头将对方所在的xx省xx市xx村xx户告知给了摆渡人,但摆渡人并不知道其在物理世界中的具体坐标(x,y,z)。因此数据包要想从目标计算机的网线中出来,还要知道目标IP地址的物理地址(MAC地址),通常可以由ARP协议实现IP地址到MAC地址的解析,这样一个数据包终于包含了到达对方计算机的网线(无线网通过电磁波收发数据,道理应该类似)的所有信息。
5.接着数据包在计算机的网卡设备中由一串二进制数转化为一系列电信号,顺着网线发送到无形的网络世界中去,经过交换机出了村,再由路由器翻山越岭,终于来到了服务端B的家门口。
6.服务端通过网线收到这个数据包后,反向拆解这些协议头,便知道了这个源数据要发往哪个应用进程、这个数据在完整数据中的位置(以进行拼接),最终通过Socket套接字的接口读取到原始数据。
(三)UE5网络架构
UE将上述基本的数据传输过程封装在FBitReader、PacketHandle、FSocket等类中,将基本的网络通信控制流程封装在UNetDriver/UIpNetDriver、UNetConnection/UIpConnection、UChannel等类中,在不同平台上通过实现相应的网络管理类实现玩家之间的联机通信。关于UE网络架构的细节会在另一篇中详细总结。
UE5联网解决方案
这里的联网不是指玩家与玩家之间的联机游玩,而是游戏运行后和开发者/平台之间的数据传输,如激活成就、读写排行榜、验证游戏账户合法性、读写云存档、游戏对局之间的网络匹配等。目前想要使用游戏后端服务,有一些免费的平台如Steam、Epic在线服务,也可以自己搭建服务器处理游戏逻辑,下面简单总结一下如何为游戏配置这些服务。
(一)Steam平台
文献库主页 (Steamworks 文献库) (steamgames.com)Steam提供一系列免费的后端服务:
其文献库:文献库主页 (Steamworks 文献库) (steamgames.com)
在项目中使用SteamAPI有三种途径:
①使用引擎源码自带的
②下载完整SDK并接入项目
③使用UE封装的在线子系统插件
UE已经把SteamSDK放在源码中编译好了,无需额外操作,只要加上头文件就可调用:#include "ThirdParty/Steamworks/steamv153/sdk/public/steam/steam_api.h",那为什么还有第二种方法多此一举呢?因为我一开始不知道源码就有- -所以找了很多资料发现可以把SteamSDK放在项目文件夹下使用,然后引用头文件路径也相应的在项目文件夹下而不是在UE源码中,属实是脱裤子放屁了,不过这样有一个优势就是源码中的代码都是编译好的改不了,项目文件夹下可以自己扩展SteamSDK(一般也不需要)。
②下载完整SDK并接入项目
1.官网下载最新SDK
2.将public下的steam文件夹(包含lib动态链接库和steamAPI头文件)复制到项目文件下,并在vs中重新生成项目目录结构
3.将sdk文件中的redistributable_bin文件夹中的win64中的steam_api.lib/dll文件复制到项目的steam的lib中~
4.在项目的可执行文件的目录中加入steam_appid.txt文件,里面填上为游戏开发而申请的Steam APP ID,可以使用免费的480
5.在项目的bulid文件中,添加上steam动态链接库的路径
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "Steam", "lib", "steam_api64.lib"));
要使用系统IOusing System.IO;
6.在调用api时头文件路径记得用完整一些的,不然读不出来
7.再调用任何Steam API函数之前必须经过初始化,调用Steam_Init()
8.然后就可以使用全局函数调用steamAPI了
9.使用虚幻引擎 测试的时候,直接运行的话是连不上steam的,必须在以独立进程的窗口中才可以连接到steam,或者通过打包好的开发版本启动
视频教程
https://www.youtube.com/watch?v=SyCeumSqtbM
请注意:只使用SteamSDK底层API进行开发也完全ok,可以不使用UE的在线子系统或其相关模块(因为在线子系统底层也是调用的SteamAPI)两者同时使用有一定风险(UE会自动生成和删除steam_appid.txt文件,就不需要上面第四步操作了,如果手动创建在打包时有可能因为这个文件导致SteamAPI在初始化时读取AppID出错)
③使用UE封装的在线子系统插件
如果不想直接使用SteamSDK的接口开发,也可以使用UE封装好的OnlineSystemSteam插件开发,这个插件把底层的通信回调都写好了(但是有些不完善),UE把各个平台的在线服务都集成在一个接口插件OnlineSubsystem中,可以把插件放进项目中二次开发,是个很好的多线程编程学习对象。
其底层的一些实现:
1.依赖的插件:OnlineSubsystem、OnlineSubsystemUtils、SteamShared
2.在SteamShared插件中实现了对SteamAPI的初始化与关闭:
3.在插件目录下实现了对底层API调用回调以及多线程的处理。
下面这个专栏中详细的介绍了如何使用在线子系统实现一个联机系统的开发。
(二)Epic平台
通过《[GDC2023]用Epic在线服务将不同平台和商店的玩家联系起来 (官方字幕)》这个视频我了解到EOS(Epic在线服务)似乎也是一个不错的联机后端服务提供者,似乎可以实现跨平台(steam、ps、switch)网络环境,而且其语音服务与大厅服务也很实用。
来自 <[GDC2023]用Epic在线服务将不同平台和商店的玩家联系起来 (官方字幕)_哔哩哔哩_bilibili>
其使用方法和steam平台类似,可以下载SDK集成到项目,也可以使用UE封装好的EOS插件。具体使用流程还得之后进一步研究。
(三)专用服务器
监听服务器 是在机器上托管游戏并充当服务器的客户端。其他客户端将连接到该托管客户端,并在托管客户端的实例上运行游戏。在此模型中,托管客户端是 权威主机 。这样一来,托管客户端比联网客户端更具优势,因为托管客户端在以实际游戏状态主动运行。
专用服务器 是无界面运行的服务器。这意味着没有客户端直接在专用服务器游戏实例中运行。每个玩家来自联网的远程客户端。无界面服务器不会呈现视觉效果,并且玩家不会在无界面服务器本地运行游戏。
相对于监听服务器而言,这有多项优势:
-
体量更小
-
消除了主机客户端的不当优势
-
专注于Gameplay逻辑和调整来自客户端的信息
监听服务器通常适用于多人休闲游戏和合作游戏,但专用服务器是大型或竞技游戏的理想之选。
在UE中使用专用服务器开发具体可以看官网案例