摘要:纵观过去 10 年的游戏领域,单机向网络发展已成为一个非常大的趋势。然而,为游戏添加网络支持的过程中往往存在着大量挑战,这里将为大家揭示游戏引擎网络开发者的 64 个做与不做。

【编者按】时下,游戏网络化已势不可逆,因此,对于游戏开发者来说,掌握网络引擎的打造技巧同样不可避免。近日,Research Industrial Systems Engineering GmbH 安全研究员 Sergey Ignatchenko「拥有 20 年以上的工程经验」在 IT Hare 上撰文,深入分享了游戏引擎网络开发的相关经验,由 OneAPM 工程师翻译。

以下为译文:

纵观过去 10 年的游戏领域,单机向网络发展已成为一个非常大的趋势。然而,为游戏添加网络支持的过程中往往存在着大量挑战,而据近几年的工作经验「不仅参与了这一衍变,同样也为大量开发者提供资讯支持」来看,许多游戏开发者甚至都违反了「打造一个优秀网络应用程序」应该坚守的一些基本原则。因此,应用程序往往会面临着「frozen」UIs、莫名的断线「在其他程序互联网访问正常时」、不定期崩溃,以及峰值期间的服务器过载等问题。毫无疑问,这些问题将直接影响到玩家的游戏体验,同时其直接程度也远超管理员和图形开发者的想象。庆幸的是,这些问题处理起来并不复杂,有些甚至是一点就明。

因此,这里将通过一系列博文来布道网络开发的某些理念,其中大部分是游戏引擎开发者不曾留意的。当然对于某些朋友来说,有些观点可能你已经接触到了,但毫无疑问的是,它对大量游戏开发者都是有价值的。因此,对于期望打造出类似游戏或证券交易这类高交互应用程序的开发者,这些建议值得一读。

作为系列的第一篇文章,这里将着重讨论不涉及协议的客户端应用程序网络开发。本系列文章将包括:

  • Protocols and APIs
  • Protocols and APIs (continued)
  • Server-Side (Store-Process-and-Forward Architecture)
  • Server-Side (deployment, optimizations, and testing)
  • Great TCP-vs-UDP Debate
  • UDP
  • TCP
  • Security (TLS/SSL)
  • ……

范围确定

总的来说,游戏引擎网络支持是个非常大的主题,因此本系列博文将圈定一个范围——聚焦拥有客户端应用程序的游戏,而不是那些基于 browser-/AJAX 的游戏,虽然这两种游戏在设计上有着很多共同点,但是其中的区别也足够让讨论分开。本系列博文将尝试覆盖游戏网络层开发的常见理念:

首先,不会只聚焦某种类型的游戏,比如 MMORPGs;毫无疑问, MMORPGs 确实在讨论的范畴中,但是也不乏社交游戏、多玩家战略「包括实时和回合制」、赌博类游戏、证券交易型等等。而出人意料的是,在做网络支持时,这些游戏存在着大量的共同点。「尽管许多取决于时间控制问题,这点将在 Great TCP-vs-UDP Debate 一节详述」。

其次,同样不会限制到某个特定的平台:事实上,这里更推荐开发者写跨平台引擎,其中就包含了网络引擎。在实践中,笔者也曾写过一个网络引擎,它可以在 5 个以上完全不同的平台上运行,这点将在第六条中进行详述。

再次,因为基于游戏引擎开发者的视角,所以这里有个背景是游戏开发者经常需要为他们的游戏开发游戏引擎。在这个情况下,大多数建议都是适用的。

最后,虽然类似「哪个引擎或者网络引擎是最好的?」这样的问题已经超出了讨论的范畴,但是本系列博文同样对回答这个问题有所帮助;毫无疑问,答案取决于游戏的具体需求,因此请详细阅读。换句话说:如果你的游戏引擎或者框架提供了一个支撑网络的方式,这些博文可以作为一个工具对其进行考量,从而弄清其网络实现是否对特定的游戏有益。

OK,在交代完大致的讨论方向后,下面言归正传。

1. 在客户端请使用事件驱动编程模型

当下,大多数客户端 UI 框架都包含一个所谓的「main thread」,或者叫「main loop」,运行于「main thread」之中,而这个「main thread」本质上会处理一些特定的事件「最原始的是 UI 事件」。这种模型存在所有客户端框架之中,从 Windows GUI、Direct X 和 Cocoa,到 Unity 3D、Android 和 iOS。同时,也确实有一个很好的理由来驱动大家这么做:因为其他的编程模型只能给你带来噩梦。事实上,在实际工作中,笔者也只碰到了一个「出格」的框架,即最初 Java 的 AWT,而在 AWT 中编写 APP 的痛苦也众所周知,有鉴于此,AWT 自始至终也没有流行起来;实际上,谷歌也确实需要为 Android 开发新的 GUI 框架。

那么,在给应用程序添加网络支持后,事件驱动模型究竟应该如何转变?其实,这里并不需要任何改变。实际生产中,所有游戏网络通信逻辑都由消息发送和接收构成;而每一个接收到的网络消息都应该被作为游戏事件驱动逻辑「除去传统的 UI 事件,比如鼠标和键盘输入」的另一个事件。

通常情况下,这个操作可以通过给 main thread 的「message queue」注入一条 message 轻松实现。举个例子,在 Win32 中,这个操作通常由 PostMessage()或者 PostThreadMessage()方法完成。如果你选择的图形框架不支持这个理念,你可能需要通过建立你的队列并进行轮询进行模拟「举个例子,Unity3D2012」。对比在单线程中强制处理所有事件「同时包含 UI 事件和网络消息」,将事件作为数据(win32)还是回调这样的问题并不重要。NB:如果使用 Unity,这个技巧很少会用到,因为 Unity 内置的网络「已经使用了 Unity 的事件处理线程」非常适用于「实时世界模拟real-time world simulator」游戏;然而根据具体游戏特征,使用 Unity 网络做 UDP 传输也并不一定就是最好的途径——特别是那些与实时世界模拟无关的游戏。

在有些用例中,事件处理线程可能与选择框架的「main thread」相去甚远,但是这里需要谨记的是,将所有与逻辑相关事件处理都放到同一个单线程中。然而,纯通信相关「与游戏逻辑完全无关),比如 marshalling、en/decryption 和 (de)compres,尽可能在「main thread」外部处理,在下面的第 3 条中会详细讨论线程隔离问题。

2. 别在事件处理线程之外调用应用程序级别回调

犹记那年,笔者还「很傻很天真」,那时候负责给一个证券交易业务开发网络框架「PS:别问我为什么这么重要的一个任务会交给一个没经验的工程师,笔者同样无解」。开始的时候,新网络库编写的确实比较顺利,但是在这里,笔者同样犯了一个原则错误——在应用程序层面调用了一个回调「它本应该是 1 个回调来响应 sendMessageOverTheNetworkAndCallbackOnReply()-style 函数」。这个蹩脚的错误曾一度给后续使用这个框架的同仁带去了大量麻烦。首先,交互「以及潜在的 races」让使用它的同事难以理解。其次,给 bugs 和 races 追踪带来了大量麻烦。最后,虽然并没有太坏的影响,而且框架总体运行良好,但是如果没有这个回调,开发将变得更加平顺。

数年后,笔者一直为大型多玩家游戏开发网络引擎——同时在线玩家 50 万,日消息数 5 亿条。而在吸取了之前的经验后,避免了类似线程回调,所有的工作都井井有条,同时在多平台切换上也异常平顺。

总结:如果你需要从网络层实现一个回调到应用程序层,首先你需要将事件传递给事件处理线程「通常情况下就是 main thread」,随后通过网络层库调用「发源于事件处理线程」来处理事件,并在必要时调用应用程序级回调。换句话说,下面才是一个完善的途径:

network thread –inter-thread-communication –event-processing thread –network-library-call –application-callback –no-thread-sync-needed

而下面虽然可行,但是不利于他人长期使用:

network thread –network-library-call –application-callback –thread-sync-required

在完善的途径中,回调只存在事件处理线程环境中,这将显著简化应用程序开发。所有应用程序级处理都被严格确定,从而最大程度地减少 races 出现的可能,同时也减少了应用程序级所必要的同步。上面的过程听起来可能比较笨重,操作起来也有些繁琐,但是它可以切实地减少游戏开发者的后续麻烦。

3. 别从事件处理线程调用可能阻塞网络的函数

这是网络开发者所有可以提交中影响最大的错误之一。如上文所述,你需要在一个单独的线程中处理所有事件。这种操作得当且方便,但麻烦也因此产生,比如:在一个事件处理器中做一个简单如 gethostbyname() 的调用,这个操作在小范围中不会存在任何问题,但是在有些情况中现实世界的玩家却会因此阻塞数分钟之久!如果你在 GUI 线程中调用这样一个函数,对于用户来说,在函数阻塞时,GUI 一直都处于 frozen 或者 hanged 状态,这从用户体验的角度是绝对不允许的。

因此,通过 GUI 来做网络交互时所有函数都应该是非阻塞的,或者位于不同的线程中。在这种情况下,你需要让事件状态机更加复杂,你可以效率地取得类似「waiting for DNS resolution」这样的状态,同时它还需要可以避免「frozen」GUI ,并且可以让你处理网络延时,包括:

  • 在需要时通知用户。举个例子,在等待了 1 秒或者 5 秒后,你知道这里出现了问题,用户同样需要知道这个事情。因此,你最好让用户了解到你已经发现了这个问题,并着手处理。
  • 在需要时终止操作并重试。
  • 允许用户优雅地终止请求或应用程序,而不是逼迫他们去使用任务管理器。

需要注意的是,这点看起来似乎与第一条和第二条相违背,但事实上并不是这样。对于「hey, so should I do it single-threaded or multi-threaded?」这样的问题,答案是:系统级别网络调用要么是非阻塞的,要么是来自非事件处理线程;同时,所有事件处理必须在事件处理线程中完成。这就意味着,如果使用多线程,你需要在一个非事件处理的网络处理线程中调用类似阻塞 recv() 的函数,随后将调用的结果转换为一个事件,并通过队列的形式「如上文 1 中介绍」将这个事件传递给事件处理线程。严格来讲,decryption/decompression 就要进行这样的处理,虽然需要去做避免事件处理线程成为一个瓶颈的流程,但它通常比只将 encryption/compression 扔到网络处理线程中来得更有性价比。

网络线程的另一个替代是 non-blocking IO,这里同样存在一些需要注意的地方,包括 gethostbyname() 和 getaddrinfo() 在主流平台中并不存在 non-blocking IO 版本,同时笔者也不认为在客户端使用 non-blocking 带来的麻烦会更少。服务器端将是另一种情景,详情会在系列博文的第三部分服务器端讨论。

4. 不要将用户作为免费的错误处理程序

在游戏引擎开发中,很多开发者使用了一个异常简单的网络错误处理途径。也就是,他们简单的将错误抛到用户面前,只留下一句「服务器存在一点问题,请重试」。这个做法是非常讨厌的,并且不会带来任何效果「当然轻松了开发者,但是损害了用户」。除下开发人员太懒,不存在任何理由不将问题在内部解决。在问题产生并给用户提示后,没理由不自动重试而要求用户再次操作。为了通知这个问题,你可以在屏幕的显著位置进行显示,或者是弹出一个对话框「没有ok这个按钮,只有关闭」,同时将在问题解决后自动消失。这样一来,在问题产生用户离开后,如果你能短时间解决问题,你不会对用户体验产生任何影响。

有人可能争论不停重试会造成网络阻塞,但是作为一名开发者,你有责任让用户体验变得简单。当然,你也可以设置一个临界值,比如 5 分钟来关闭重试,并提示「对不起,我们已经尽力了,但问题在短时间内无法得到修复」。

综合上面的 1-3 条,你通常需要在网络处理线程中检测问题,并将它转换成 1 个事件,并在事件处理线程中处理事件,比如显示一个对话框。

5. 为用户提供有价值的错误消息

从终端用户的角度来看,「网络不可用」、「连接拒绝」以及「连接终止」没有任何区别;如果可能的话,你可能是想告诉他们网线未插入或者是服务器故障或者是两者之间的一些问题,但是仅仅因为一些专业用语让用户无法确定问题真相是完全不可取的。更糟糕的做法是,试图将技术细节隐藏于一些模棱两可的话语之间,比如「服务器有一点问题」和「你丢失了连接」。

总之,切记将错误消息从你能理解的语句转换到用户能理解的提示,而不是让用户无法辨别各种提示间的区别——让所有消息看起来完全相同。

6. 支持多平台

纵观当下游戏领域,单平台游戏已经不再有吸引力。即使引擎只为一款游戏打造,但是你又真的能确定游戏未来不会过渡到其他平台?实践中,让网络代码跨平台并不是一件难事,因此你没理由不做多平台的准备。笔者个人的网络库就覆盖了 Windows、Linux、Mac OS X、FreeBSD、iOS 等引擎。

6a. 在客户端使用 Berkeley Sockets

如果你的游戏引擎是基于 C 或者 C++,并且将应用程序定义为只 Windows 平台,那么就可能尝试一些 Windows 特有的函数「那些以 WSA*()为前缀的」来通信。请不要这么做,转而使用 Berkeley sockets「那些 socket()/connect()/send()/recv()函数」进行取代;关于使用细节,请自行 Google。对于其他提供了跨平台 APIs 的编程语言,选择一个合适的网络库通常不会有太多问题。

7. 提供一个自动升级 APP 的途径

通常情况下,自动升级并不会考虑为游戏引擎的一部分。然而,个人觉得将自动升级纳入网络层会有一些相应的好处,其原因是:

  • 用户可能期望多一些选择「从主题到 DLCs」。
  • 如果可以边玩边下载,那么他们会很开心。
  • 如果下载干涉到娱乐,那他们肯定会不开心。
  • 在你的网络层提供可选下载,你可以优先考虑流量,将下载对游戏的影响降到最低「在本系列博文的 Part IIb 一节将讨论更多技巧」。因为 QoS 并不适用于互联网,两个并行的连接很可能产生相互影响。
  • 如果你支持可选下载,他们同样需要自动化更新,因此结合自动下载和自动更新是件不错的事情。
  • 因此,在网络引擎实现整个自动更新功能是个不错的事情。
  • 此外,用户可以边玩边下载,从而最大化了娱乐时间。
  • 上面的推理并不是在任何条件下都成立的,但是我看到了类似系统的实现,同时也取得了非常好的效果。

注意:尽管与网络库集成,你同样需要在 HTTP「而不是基于你的协议」上实现初始自动更新「会在游戏应用启动前启动」;这么操作并不会带来太多的复杂性,但是很可能会彻底地修改你的协议。

其他在启动更新上的操作是非常复杂的,因此会单独开一篇文章来表述、

To Be Continued……

原文链接:64 Network DO’s and DON’Ts for Game Engine Developers. Part I: Client Side

本文系 OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客

游戏引擎网络开发者的 64 做与不做 | Part 1 | 客户端方面的更多相关文章

  1. 游戏引擎网络开发者的64做与不做(二A):协议与API

    [编者按]在这个系列之前的文章"游戏引擎网络开发者的64做与不做(一):客户端方面"中,Sergey介绍了游戏引擎添加网络支持时在客户端方面的注意点.本文,Sergey则将结合实战 ...

  2. 开发者不容错过的10款免费JavaScript游戏引擎

    摘要:使用HTML5.JavaScript可以帮助开发者开发出各种与众不同的游戏及游戏特效,比如3D动画.Canvas等.本文介绍10款被广泛使用的基于HTML5的JavaScript游戏引擎. 在G ...

  3. 20个免费的 JavaScript 游戏引擎分享给开发者

    这篇文章收集了20个免费的 JavaScript 游戏引擎分享给开发者.这些游戏引擎能够帮助游戏开发人员更快速高效的开发出各种好玩的游戏. 使用 HTML5.CSS3 和 Javascript 可以帮 ...

  4. 第1部分: 游戏引擎介绍, 渲染和构造3D世界

    原文作者:Jake Simpson译者: 向海Email:GameWorldChina@myway.com ---------------------------------------------- ...

  5. AI:从游戏引擎--到AI

    原文链接:http://blog.csdn.net/left_la/article/details/6358911#t9 这是我在Gameres上看到的一篇文章,文章很长,全文分为11个部分,看后感觉 ...

  6. 游戏引擎架构 (Jason Gregory 著)

    第一部分 基础 第1章 导论 (已看) 第2章 专业工具 (已看) 第3章 游戏软件工程基础 (已看) 第4章 游戏所需的三维数学 (已看) 第二部分 低阶引擎系统 第5章 游戏支持系统 (已看) 第 ...

  7. 开源免费的HTML5游戏引擎

    青瓷引擎的成长 青瓷引擎自2015年4月项目启动开始,7月首次亮相2015年ChinaJoy,便得到业界的极大关注,随后开启限量测试,收到数百个开发者团队的试用申请及反馈,期间经历了18个内测版本,完 ...

  8. GJM : 各大开发游戏引擎

    感谢您的阅读.喜欢的.有用的就请大哥大嫂们高抬贵手"推荐一下"吧!你的精神支持是博主强大的写作动力以及转载收藏动力.欢迎转载! 版权声明:本文原创发表于 [请点击连接前往] ,未经 ...

  9. 开源战棋 SLG 游戏框架设计思考(一)简介和游戏引擎

    战棋 SLG 游戏 SLG(Simulation Game)游戏是模拟游戏的简称.战棋类的SLG有两种:一种是 War Game 中的兵棋推演分支,常见的游戏有战争艺术3(TOAW3 — The Op ...

随机推荐

  1. jquery.validate新的写法(jquery.validate1.13.js)

    <script src="../js/jquery.js"></script> <script src="../js/jquery.vali ...

  2. 使用checked关键字处理“溢出”错误

    在进行算术运算时,可以使用checked关键字有效处理溢出错误,使用checked关键字可能对程序的性能会有一点点的影响,这是一种以牺牲性能换取健康的做法. private void button1_ ...

  3. UITextField 属性详解

    //初始化textfield并设置位置及大小   UITextField *text = [[UITextField alloc]initWithFrame:CGRectMake(20, 20, 13 ...

  4. windows CE 6.0编译报BLDDEMO: There were errors building MY283错误解决办法

    今天开始正式进入windows ce程序开发. 第一次编译windows ce6.0的系统,25分钟编译后报:BLDDEMO: There were errors building MY283 错误. ...

  5. Linux系统木马后门查杀方法详解

    木马和后门的查杀是系统管理员一项长期需要坚持的工作,切不可掉以轻心.以下从几个方面在说明Linux系统环境安排配置防范和木马后门查杀的方法: 一.Web Server(以Nginx为例) 1.为防止跨 ...

  6. How to Notify Command to evaluate in mvvmlight

    How to Raize Command to evalituate in mvvm In mvvmlight, we bind our control to the relaycommand obj ...

  7. gvim 常用配置

    set nocompatible set nu! filetype on syntax on set autoindent set ai! "set bg=dark set autoinde ...

  8. <梦断代码>读后感2

    <梦断代码>这本书读了一半,我的心情久久不能平静. 为什么好软件如此难做?这是我本人,我想也是很多人都在苦苦思索的一个问题,虽然没有人能有完全确定的答案,但通过书中的记述,和个人思考,还是 ...

  9. 玩耍Hibernate之缓存

    2.在持久化层,对象分为哪些状态?分别列出来. 答:瞬时态(Transient).持久态(Persistent).脱管态(Detached). 瞬时态(Transient) 是对象是创建时,瞬时对象在 ...

  10. 在Mac中如何显示和隐藏文件

    1.显示Mac隐藏文件的命令: 在终端中输入"defaults write com.apple.finder AppleShowAllFiles YES":  鼠标单击窗口左上角 ...