摘要:纵观过去 10 年的游戏领域,单机向网络发展已成为一个很是大的趋势。然而,为游戏添加网络支持的过程当中每每存在着大量挑战,这里将为你们揭示游戏引擎网络开发者的 64 个作与不作。html
【编者按】时下,游戏网络化已势不可逆,所以,对于游戏开发者来讲,掌握网络引擎的打造技巧一样不可避免。近日,Research Industrial Systems Engineering GmbH 安全研究员 Sergey Ignatchenko「拥有 20 年以上的工程经验」在 IT Hare 上撰文,深刻分享了游戏引擎网络开发的相关经验,由 OneAPM 工程师翻译。android
如下为译文:编程
纵观过去 10 年的游戏领域,单机向网络发展已成为一个很是大的趋势。然而,为游戏添加网络支持的过程当中每每存在着大量挑战,而据近几年的工做经验「不只参与了这一衍变,一样也为大量开发者提供资讯支持」来看,许多游戏开发者甚至都违反了「打造一个优秀网络应用程序」应该坚守的一些基本原则。所以,应用程序每每会面临着「frozen」UIs、莫名的断线「在其余程序互联网访问正常时」、不按期崩溃,以及峰值期间的服务器过载等问题。毫无疑问,这些问题将直接影响到玩家的游戏体验,同时其直接程度也远超管理员和图形开发者的想象。庆幸的是,这些问题处理起来并不复杂,有些甚至是一点就明。安全
所以,这里将经过一系列博文来布道网络开发的某些理念,其中大部分是游戏引擎开发者未曾留意的。固然对于某些朋友来讲,有些观点可能你已经接触到了,但毫无疑问的是,它对大量游戏开发者都是有价值的。所以,对于指望打造出相似游戏或证券交易这类高交互应用程序的开发者,这些建议值得一读。服务器
做为系列的第一篇文章,这里将着重讨论不涉及协议的客户端应用程序网络开发。本系列文章将包括:网络
总的来讲,游戏引擎网络支持是个很是大的主题,所以本系列博文将圈定一个范围——聚焦拥有客户端应用程序的游戏,而不是那些基于 browser-/AJAX 的游戏,虽然这两种游戏在设计上有着不少共同点,可是其中的区别也足够让讨论分开。本系列博文将尝试覆盖游戏网络层开发的常见理念:多线程
首先,不会只聚焦某种类型的游戏,好比 MMORPGs;毫无疑问, MMORPGs 确实在讨论的范畴中,可是也不乏社交游戏、多玩家战略「包括实时和回合制」、赌博类游戏、证券交易型等等。而出人意料的是,在作网络支持时,这些游戏存在着大量的共同点。「尽管许多取决于时间控制问题,这点将在 Great TCP-vs-UDP Debate 一节详述」。app
其次,一样不会限制到某个特定的平台:事实上,这里更推荐开发者写跨平台引擎,其中就包含了网络引擎。在实践中,笔者也曾写过一个网络引擎,它能够在 5 个以上彻底不一样的平台上运行,这点将在第六条中进行详述。框架
再次,由于基于游戏引擎开发者的视角,因此这里有个背景是游戏开发者常常须要为他们的游戏开发游戏引擎。在这个状况下,大多数建议都是适用的。socket
最后,虽然相似「哪一个引擎或者网络引擎是最好的?」这样的问题已经超出了讨论的范畴,可是本系列博文一样对回答这个问题有所帮助;毫无疑问,答案取决于游戏的具体需求,所以请详细阅读。换句话说:若是你的游戏引擎或者框架提供了一个支撑网络的方式,这些博文能够做为一个工具对其进行考量,从而弄清其网络实现是否对特定的游戏有益。
OK,在交代完大体的讨论方向后,下面言归正传。
当下,大多数客户端 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 条中会详细讨论线程隔离问题。
犹记那年,笔者还「很傻很天真」,那时候负责给一个证券交易业务开发网络框架「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 出现的可能,同时也减小了应用程序级所必要的同步。上面的过程听起来可能比较笨重,操做起来也有些繁琐,可是它能够切实地减小游戏开发者的后续麻烦。
这是网络开发者全部能够提交中影响最大的错误之一。如上文所述,你须要在一个单独的线程中处理全部事件。这种操做得当且方便,但麻烦也所以产生,好比:在一个事件处理器中作一个简单如 gethostbyname() 的调用,这个操做在小范围中不会存在任何问题,可是在有些状况中现实世界的玩家却会所以阻塞数分钟之久!若是你在 GUI 线程中调用这样一个函数,对于用户来讲,在函数阻塞时,GUI 一直都处于 frozen 或者 hanged 状态,这从用户体验的角度是绝对不容许的。
所以,经过 GUI 来作网络交互时全部函数都应该是非阻塞的,或者位于不一样的线程中。在这种状况下,你须要让事件状态机更加复杂,你能够效率地取得相似「waiting for DNS resolution」这样的状态,同时它还须要能够避免「frozen」GUI ,而且可让你处理网络延时,包括:
须要注意的是,这点看起来彷佛与第一条和第二条相违背,但事实上并非这样。对于「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 带来的麻烦会更少。服务器端将是另外一种情景,详情会在系列博文的第三部分服务器端讨论。
在游戏引擎开发中,不少开发者使用了一个异常简单的网络错误处理途径。也就是,他们简单的将错误抛到用户面前,只留下一句「服务器存在一点问题,请重试」。这个作法是很是讨厌的,而且不会带来任何效果「固然轻松了开发者,可是损害了用户」。除下开发人员太懒,不存在任何理由不将问题在内部解决。在问题产生并给用户提示后,没理由不自动重试而要求用户再次操做。为了通知这个问题,你能够在屏幕的显著位置进行显示,或者是弹出一个对话框「没有ok这个按钮,只有关闭」,同时将在问题解决后自动消失。这样一来,在问题产生用户离开后,若是你能短期解决问题,你不会对用户体验产生任何影响。
有人可能争论不停重试会形成网络阻塞,可是做为一名开发者,你有责任让用户体验变得简单。固然,你也能够设置一个临界值,好比 5 分钟来关闭重试,并提示「对不起,咱们已经尽力了,但问题在短期内没法获得修复」。
综合上面的 1-3 条,你一般须要在网络处理线程中检测问题,并将它转换成 1 个事件,并在事件处理线程中处理事件,好比显示一个对话框。
从终端用户的角度来看,「网络不可用」、「链接拒绝」以及「链接终止」没有任何区别;若是可能的话,你多是想告诉他们网线未插入或者是服务器故障或者是二者之间的一些问题,可是仅仅由于一些专业用语让用户没法肯定问题真相是彻底不可取的。更糟糕的作法是,试图将技术细节隐藏于一些模棱两可的话语之间,好比「服务器有一点问题」和「你丢失了链接」。
总之,切记将错误消息从你能理解的语句转换到用户能理解的提示,而不是让用户没法辨别各类提示间的区别——让全部消息看起来彻底相同。
纵观当下游戏领域,单平台游戏已经再也不有吸引力。即便引擎只为一款游戏打造,可是你又真的能肯定游戏将来不会过渡到其余平台?实践中,让网络代码跨平台并非一件难事,所以你没理由不作多平台的准备。笔者我的的网络库就覆盖了 Windows、Linux、Mac OS X、FreeBSD、iOS 等引擎。
若是你的游戏引擎是基于 C 或者 C++,而且将应用程序定义为只 Windows 平台,那么就可能尝试一些 Windows 特有的函数「那些以 WSA*()为前缀的」来通讯。请不要这么作,转而使用 Berkeley sockets「那些 socket()/connect()/send()/recv()函数」进行取代;关于使用细节,请自行 Google。对于其余提供了跨平台 APIs 的编程语言,选择一个合适的网络库一般不会有太多问题。
一般状况下,自动升级并不会考虑为游戏引擎的一部分。然而,我的以为将自动升级归入网络层会有一些相应的好处,其缘由是:
注意:尽管与网络库集成,你一样须要在 HTTP「而不是基于你的协议」上实现初始自动更新「会在游戏应用启动前启动」;这么操做并不会带来太多的复杂性,可是极可能会完全地修改你的协议。
其余在启动更新上的操做是很是复杂的,所以会单独开一篇文章来表述、
To Be Continued……
原文连接:64 Network DO’s and DON’Ts for Game Engine Developers. Part I: Client Side