本文主要是介绍随便聊聊网络游戏开发模式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本文仅是闲聊罢了,并非开发教程,有意依此学习的同学注意一下.
就网络游戏开发而言,目前主流的同步方式大概是以下两种:
- 帧同步
顾名思义,就是按"帧"(一般指逻辑帧)来进行网络同步,一般实现上,都是客户端按"帧"来发送自己的操作数据(无操作也是一种操作(也要发送)),服务器收取到所有客户端的操作数据之后统一进行分发,客户端收取到分发数据之后则进行"帧"模拟,由于只同步操作数据(客户端各自进行逻辑模拟),为了能让各个客户端保持同步,对于客户端逻辑的编写有比较高的确定性需求(随机、浮点等不确定因素都需要进行确定性处理,当然纯表现类的内容可以忽略),游戏项目中比较典型使用帧同步技术的应该就是"王者荣耀"(以下简称"王者")了.
- 状态同步
相比于帧同步,状态同步则是按"状态"(动态单位的位置速度等数据)来进行网络同步,一般实现上,服务器都是在游戏一开始进行一次基准"状态"同步(给客户端),之后在游戏过程中按需进行增量"状态"同步,客户端同样按需对接收到的"状态"数据进行设置处理(也可做些插值外推等操作),一般数据量较大的游戏(尤其是涉及各类LOD处理时),逻辑是很难做到高确定性的,所以帧同步基本就不能使用了,其实"王者"最初选用帧同步应该也并非完全出自技术层面的考量,更多的可能还是当时项目周期的限制,如果现在回过头来重新评估的话,"王者"改用状态同步应该也是可行的.
有些游戏也可能同时使用 帧同步 和 状态同步,具体则根据游戏实际需求来定了,不过不论哪种同步方式,都有一个棘手的问题需要处理:主控端的(本地)操作响应问题:
在帧同步中,由于(本地)操作需要经历上传接收的网络流程才能进行逻辑模拟,所以操作延迟基本是不可避免的,虽然可以做一些本地(预测)表现来优化(譬如技能前摇动作等等),但是因为高确定性的要求,逻辑可优化的空间比较小.
而在状态同步中,如果逻辑允许,客户端本地可以直接执行本地操作(逻辑模拟)并将操作数据上传,服务器接收到客户端操作数据后执行逻辑模拟,然后将结果下发,客户端再依据接收到的下发结果对操作进行"纠错重放",如果一切顺利,主控端将感受不到延迟.
对于诸如云游戏之类将客户端直接作为终端显示的同步模式,更多的考量应该是流量控制方面的(而非具体的同步技术),同时因为同步模式的限制,这类游戏对主控端的(本地)操作响应基本没有优化空间,所以一般来讲这类游戏只适合对(本地)操作响应要求不高的游戏.
接着我们来聊下一个更细节的问题:
如何进行实际的网络同步呢 ?
一般来讲,主流的网络同步操作如下(省略加解密之类的操作):
- 定义生成数据格式(使用 Protobuf 等工具)
- 使用对应数据格式编解码数据
- TCP/UDP 发送接收数据
- (TCP 可靠但不灵活,游戏中使用 TCP 的话可能会出现更多的延迟问题;UDP 不可靠但灵活,如果游戏使用 UDP 的话可能需要去自行实现 TCP 的某些子集功能以达到一定的可靠性要求,总的来说各有利弊)
当然,上述的流程比较底层,一般游戏开发都会对其进行上层封装,最终的结果大概是以下两个接口(伪代码):
void SendMessage(Proto);void ReceiveMessage(Proto);
再以 服务器开发 与 客户端开发 的角度来看下网络游戏的开发流程(仅涉及程序开发流程):
- 服务器开发 与 客户端开发 对齐逻辑开发需求
- 拆解 服务器程序内容 与 客户端程序内容
- 定义相关同步协议
- 服务器开发 与 客户端开发 各自编写代码并持续进行阶段联调
可以看到,该流程下 服务器端代码 与 客户端代码 是完全隔离的,双方仅仅通过协议进行协作交流(其他方面双方可以一无所知).
这本身其实是一个很好的开发范式,很多网络程序也是如此实践的,但对于网络游戏而言,有时候却捉襟见肘了:
考虑我们要开发网络游戏中的技能系统,首当其冲的一个问题是如何进行伤害判定,过去像 MMO 一类的游戏中,伤害判定一般都是在服务器进行的,判定方式往往也很简单,基本上就是检测一些距离方位的限制,同时为了增强手感,客户端还会配合进行一些攻击帧的表现处理,总的来说使用上面那种开发"隔离"的方式还能应付.
但是我们对新的技能系统有更高的需求,我们希望伤害判定能够严格按照动画中配置的攻击帧数进行,同时动画本身也可能带有 Root Motion 和各类动画姿态处理(IK等),此时我们便遇到难题了,摆在我们面前的选项似乎只剩两个:
- 服务器实现相同一套动画系统(A 方案)
- 伤害判定转移至客户端进行(B 方案)
两种方案都不尽如人意.
因为 路径依赖 的关系,很多项目可能会选择 B 方案,毕竟 A 方案基本没有可行性,尤其是当项目有之前的服务器继承代码时,而我们又需要坚持上述的开发"隔离"模式 …
但实际上,如果我们抛弃所谓的开发"隔离",我们还有更好的选择:
- DedicatedServer
所谓的 DedicatedServer,即专用服务器,可以理解为游戏逻辑在开发上不再"隔离",而是同时支持服务器和客户端,由于代码基是相同的,基础功能自然也是相同的,上面提到的动画难题也便迎刃而解了.
说的有些抽象,我们拿 UE 的同步框架来举个例子:
先看下 ENetMode:
enum ENetMode
{/** Standalone: a game without networking, with one or more local players. Still considered a server because it has all server functionality. */NM_Standalone,/** Dedicated server: server with no local players. */NM_DedicatedServer,/** Listen server: a server that also has a local player who is hosting the game, available to other players on the network. */NM_ListenServer,/*** Network client: client connected to a remote server.* Note that every mode less than this value is a kind of server, so checking NetMode < NM_Client is always some variety of server.*/NM_Client,NM_MAX,
};
国情关系,我们一般开发时都不太关心 NM_Standalone 和 NM_ListenServer,但实际从实践角度来讲,一个游戏本身同时支持单机游玩和联机游玩其实是件很正常的事情,但在我们先前的开发"隔离"模式下,同时支持单机游玩和联机游玩基本是不可能实现的 …
再来看下 ENetRole:
/** The network role of an actor on a local/remote network context */
UENUM()
enum ENetRole : int
{/** No role at all. */ROLE_None,/** Locally simulated proxy of this actor. */ROLE_SimulatedProxy,/** Locally autonomous proxy of this actor. */ROLE_AutonomousProxy,/** Authoritative control over the actor. */ROLE_Authority,ROLE_MAX,
};
这个是对 Actor 的网络角色的定义,也是代码同时支持 服务器 和 客户端 的关键,譬如我们做以下判断(伪代码):
if (ActorNetRole == ROLE_Authority)
{// ...
}
基本可以认为是在 服务器 端执行代码(而不会在客户端执行)
接着就是 RPC(远程过程调用)了, UE 中的 RPC 大概可以分为下面两个大类:
-
按可靠性分
- Reliable
- Unreliable
-
按同步方式分
- Client
- Server
- NetMulticast
另一个重要概念就是 Replicate 了,可以理解为一种服务器向客户端自动同步数据的方式.
简单来说, RPC 可以理解为事件通知(或者狭义上类似于之前的那种协议同步方式(只是方式更简单一些)),Replicate 则可以理解为状态同步,仅会由服务器同步给客户端,简单来讲也可以通过 RPC 来模拟,但是逻辑上使用 Replicate 会更简单,并且是自动化的.
总结来看, DedicatedServer 是更广义上的一种网络游戏开发框架,先前的那种协议同步方式可以认为是其框架下的子集,相互"隔离"的网络游戏开发方式有其适用的场景,但是随着游戏复杂度的提高(因为游戏需求提高等原因), DedicatedServer 在架构层面的优势会愈加明显.
“兼听则明,偏信则暗”
这篇关于随便聊聊网络游戏开发模式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!