Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

多人在线游戏的开发与部署:第1卷 development-and-deployment-of-multiplayer-online-games-vol1 #223

Open
WangShuXian6 opened this issue Nov 12, 2024 · 5 comments

Comments

@WangShuXian6
Copy link
Owner

多人在线游戏的开发与部署:第1卷 development-and-deployment-of-multiplayer-online-games-vol1

cover

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Nov 12, 2024

对《多人在线游戏的开发与部署》的早期评价

“迄今为止,关于多人游戏具体内容最全面的书籍。”
— Dmitri Dain,Amaya Software董事总经理

“终于来了!”
— Boris Taratine,Lloyds Bank Group网络安全项目首席架构师

“期待书籍完成后阅读。”
— Nuno Leiria,Polystream高级软件工程师,曾任Lionhead Studios核心技术程序员

“期待最终成书。这本书很有前景。终于有一本关于这个主题的书,既不过时也不模糊。”
— Özkan Can,曾任Blue Byte(育碧旗下工作室)后台技术总监

“TCP是一只复杂的‘野兽’,而你对它的了解远胜于我。感谢上帝!”
— Glenn Fiedler,GDC演讲者,AAA多人游戏开发资深人士,终生UDP拥护者

“你正在编写的这本巨著,看起来非常有前途和令人振奋。”
— Alessandro Alinone,Lightstreamer联合创始人兼CEO

“非常实用且极具实际价值的书籍。它将成为任何游戏开发人员书库中的重要补充。”
— Michael Morgovsky,Plarium联合创始人兼CTO

“我已经找这样的书籍找了十年。这本书对教学游戏开发中的多人游戏开发细节将是无价的。”
— Robert Zubek,GDC演讲者,SomaSim创始人,曾任Zynga首席软件工程师

“即便尚未完成,它已经是我见过的最全面的网络与多人游戏开发参考资料,并不断让我学到新东西。对于严肃的开发者而言,绝对不可或缺。”
— Matt Pritchard,AAA资深人士,作家,Forgotten Empires CTO,曾任Ensemble Studios / Valve / Disney的RTS/FPS/物联网开发者

“无虫”兔子

讽刺的开发者

G20 股市交易所的联合架构师

一款拥有 40 万同时在线玩家游戏的独立架构师

在《CUJ》、《C++ 报告》和《Overload》上发表文章的作者

多玩家在线游戏的开发与部署
从社交游戏到大型多人在线第一人称射击游戏,
以及夹在其中的股票交易所

第一部分 ARCH. 架构与前期开发

胜利的将领先赢而后战,失败的将领先战而后求胜。
——孙子,《孙子兵法》,公元前 500 年左右

在本部分 ARCH 中,我们将探讨在编码开始之前需要进行的一系列活动。内容涵盖从制定业务需求到设置源码控制和问题跟踪系统等多个方面,期间还涉及许多关键的架构决策。

第一卷
游戏设计文档 (GDD)、权威服务器、通信

移山之人,始于搬小石。
——孔子(公元前 5 世纪),《文明 IV》(公元 21 世纪)

多玩家在线游戏的开发与部署
第一卷 游戏设计文档 (GDD)、权威服务器、通信


多人在线游戏的开发与部署

第一卷:游戏设计文档(GDD)、权威服务器、通信


目录

  • 简介

    • 本书背后的故事
    • 作者和团队的背景
    • 现实世界的经验
    • 本书的主题是什么
    • 游戏类型:从社交游戏到大型多人在线第一人称射击游戏(MMOFPS),包含虚拟股市
    • 股市就是游戏,甚至更甚,是赌博游戏
    • 分布式交互系统概述
    • 涉及的主题:不包括玩法/人工智能/物理模拟/盈利/3D
    • 游戏引擎:自制、重用或第三方
    • 本书是否适合你
    • 不包含光盘
    • "关于一切的一无所有"
    • 先修知识:中级以上
    • 局域网(LAN)游戏和点对点(P2P)游戏
    • 推荐阅读材料
      • 通用编程
      • 游戏编程(与网络无关)
      • 3D编程
      • 网络编程(与游戏无关)
      • 游戏网络编程
      • C++
        • 对于C++初学者
        • 对于有C++经验,但需要升级到C++11/C++14的开发者
      • 安全性
    • 如何阅读本书
      • 约定
      • 代码示例
      • 我的“显而易见的事”风格
      • 术语
        • MMO vs MOG
        • 服务器
        • 专用服务器
        • 自带盐(BYOS)
  • 第一章 从MOG视角看游戏设计文档(GDD)

    • 游戏即是你的“孩子”
    • 给首次游戏开发者的3500字速成课
    • 关于GDD
      • 变动频繁,每周七天
      • 灵活和文档化
    • 泛化谬误
    • 项目干系人
    • 焦点测试和游戏测试
    • 关于市场和盈利:关键量
    • 干系人可用性
    • 项目干系人的摘要
    • 非MOG团队的典型结构
    • 上市时间、最小可行产品(MVP)和规划
      • 上市时间的处理
      • 关于保持节奏的重要性
      • 新开发者速成课的要点
    • 三条重要的GDD规则
    • 分离GDD和实现细节
    • 处理“难搞”的干系人和(请注意)管理者
    • 有限生命周期与无限生命周期的游戏
    • 客户端驱动和服务器驱动的开发流程
      • 服务器驱动流程
      • 客户端驱动流程
      • 处理客户端驱动流程
        • 方案1:“持续转换”
        • 方案2:“将服务器集成到工具链中”
    • 关于MOG的匹配系统和社交元素
      • 无效的匹配系统(经验法则)
      • 有效的匹配系统
    • 支持小型玩家群体
    • 影响市场和盈利的技术问题
    • GDD需求清单
    • MOG特有的团队
      • 网络团队
      • 服务器团队
      • 数据库团队
      • 后端团队
      • 后端团队的时间表
      • 所有MOG特有的团队都应是核心团队
    • 运营成本分析
    • 常见的GDD陷阱:随意添加多人模式
    • 游戏实体和交互
      • 游戏实体:你在处理什么?
      • 游戏实体间的交互
      • 你应获取什么?实体与交互图
      • 实体与交互示例
        • 社交农场和类似农场的游戏
        • 赌场多人游戏
        • 股市、体育博彩和拍卖网站
        • 大型虚拟世界游戏(MMOTBS/MMORTS/MMORPG/MMOFPS)
        • 团队竞争/电子竞技
      • 实体与交互图:构建游戏架构的起点
    • 第一章总结

  • 第二章 作弊、P2P和(非)权威服务器

  • 如果你足够受欢迎,他们会找到作弊的理由

  • 与电子商务的巨大差异

  • 应对作弊者

    • 游戏玩法
    • 架构
    • 对抗机器人
  • 攻击:主场的巨大优势

    • 主场游戏
    • 客场游戏
    • 已公开和未公开的攻击
      • 公开攻击:影响更大,但可以利用主场优势
      • 未公开攻击
  • 攻击类型

  • 法律问题与封禁难题

    • 游戏作弊
    • 游戏规则违规
    • 信息泄露
    • 反应增强
    • 断线处理的滥用
    • 挂机机器人
    • 多账号
  • 经典攻击

    • 数据库攻击
    • 源代码窃取
    • 密码钓鱼
    • 记录键盘按键/特洛伊木马/其他玩家设备上的后门
    • 分布式拒绝服务攻击(DDoS)
  • MOG攻击类型总结

  • 权威客户端:对作弊几乎无力(除非是仅限主机的游戏)

  • 代码签名在恶意环境中基本无效(除非是主机游戏)

  • 理论防护

    • 交叉检查:无法检测的攻击、控制世界、以及延迟
    • 多数投票机制:增加延迟
    • 可信节点:谁是可信任的?
    • 同态加密:根本无法实施
  • 权威客户端MOG总结

    • 确定性锁步:无规则违规,但信息泄露严重
  • 权威服务器:防作弊性最强

    • 权威服务器:可扩展性不完美但可接受
    • 示例计算
    • 总结:权威服务器不是理想,但唯一可行
  • 积极思考!或者,也许仍有希望……

  • 每一位都重要:多层防护

  • 第二章要点:是的,最终还是要选择权威服务器


  • 第三章 通信

    • 客户端与服务器之间的通信
    • 往返时间(RTT)、输入延迟及其缓解方式
    • 数据流图,初探
    • 输入延迟:MOG开发者的最大噩梦
      • 输入延迟:用户期望
      • 输入延迟:对MOG来说的剩余时间
    • 考虑数据包丢失和抖动
      • 互联网基于数据包传输,数据包可能丢失
    • 减少开销
    • 客户端和服务器端的接收缓冲
    • 时间同步
      • 一次性同步
      • 一次性同步加后续调整
      • 类似NTP的协议
      • 锁相环(PLL)
      • 时间同步要点
    • 关于TCP
    • 输入延迟:一点点缓解
    • 数据流图,再探:快节奏游戏的具体要求
    • 往返时间(RTT)
      • 局域网(LAN)RTT与互联网RTT
      • 关于内容分发网络(CDN)和地理服务器分布
    • 往返时间和玩家
    • 回到输入延迟问题
    • 数据流图,三探:客户端预测和插值
      • 客户端插值
      • 客户端外推,即死后预测
      • 碰壁与服务器校正
      • 客户端预测
      • 客户端预测:处理差异
    • 关于分布式授权
    • 第三版数据流图
    • 延迟补偿——作弊风险与玩家体验的平衡
      • 服务器回溯
      • 在服务器端扣除客户端RTT
      • 延迟补偿固有的作弊风险
      • …但玩家体验更重要
    • 各种选项!我需要哪一种?
    • 游戏世界状态和流量减少
      • 服务器端、可发布、客户端的游戏世界状态
      • 带宽限制
      • 优化带宽的其他原因
    • 三角形和带宽
    • MOG的三种状态
      • 客户端状态
      • 服务器端状态
      • 可发布状态
      • 为什么不将它们保持一致?
      • 非仿真游戏及总结
    • 可发布状态:传输、更新、兴趣管理和压缩
      • 兴趣管理:流量优化和防作弊
      • 实现兴趣管理
        • 基于网格的兴趣管理
        • 兴趣管理防信息泄露作弊
        • 基于视锥的兴趣管理
    • 压缩前:最小化数据
    • 压缩
      • 压缩的定义
      • “参考基准”的压缩
      • 用于不可靠通信的“参考基准”:低延迟可压缩状态同步
      • 差异压缩
        • 差异压缩的两种模式
        • 差异压缩的扩展至任意树
        • 任意树的差异压缩:动态收集更新
      • 死后预测作为压缩方式
      • 死后预测的变化
      • 经典压缩
        • 压缩可靠的流
        • 压缩独立的数据包
        • 利用确认包进行压缩
      • 组合不同的压缩机制及收益递减规律
      • 使用双精度浮点数和无损压缩
    • 自适应流量管理
      • 自适应流量管理—UDP
      • 自适应流量管理—TCP
      • 在权威服务器上下文中的自适应流量管理
      • 流量与实时策略
    • 流量优化:要点和建议
    • MMOG与可扩展性
      • 关于无共享架构
      • 一个显而易见的优化:分离NPC/AI
      • 按区域划分
      • 无缝世界:区域重叠!
    • 服务器端不确定性的问题
      • 完全消除不确定性:时间同步
        • 无回溯的同步:CMS/LBTS锁步机制
        • 通过服务器回溯进行同步:“价值日期”
    • 瞬时事件、转发的输入和(某种意义上的)广播消息
      • 瞬时事件
      • 转发的输入
      • 信息泄露的潜在风险
      • 实现
      • (某种意义上的)广播消息(带兴趣管理的广播)
    • 点对点通信和非阻塞RPC
      • 远程过程调用(RPC)
        • 实现非阻塞RPC
        • 空返回值与非空返回值的非阻塞RPC
        • 非空返回值的RPC
    • 客户端到服务器和服务器到客户端的点对点通信
      • 输入
      • 输入时间戳
      • “宏观”客户端行为
      • 服务器到客户端的通信
    • 服务器间的通信
      • 无缝处理临时断线
        • 方案1:分离调用方/被调用方处理
        • 方案2:两个保证交付的流
        • 深入探讨:具有事务完整性的跨数据库异步传输
    • 服务器端实体地址
    • 服务器端通信:TCP通常优于UDP
    • 使用消息队列进行服务器间通信
      • 消息队列与事务完整性
      • 关于AMQP中的事务
      • 有代理与无代理消息队列
        • 代理作为目录服务
        • 代理和事务完整性
      • 在服务器端使用消息队列:总结
    • 关于协议变更
      • 接口描述语言(IDL):编码、映射和协议变更
        • 语言内的接口描述与独立的接口描述
        • IDL的开发流程
        • IDL + 编码 + 映射
        • 示例:IDL
    • 输入数据的清理
    • 测试用例生成
    • 示例:映射
      • 映射到现有类
    • 示例:编码
    • 协议变更与向后兼容
      • 字段标识符
      • 消息扩展及约束
      • 版本控制
      • 应答的版本控制
      • 版本控制的优缺点
      • 应该选择哪一种?
    • 实现IDL和特定的编码
    • 第三章总结
  • 第一卷总结

    • 下一步

@WangShuXian6
Copy link
Owner Author

引言

本书背后的故事

从前有一只兔子……

  • 哎呀,他们是怎么想出这些吸引人的开头的呢?

——加菲猫

很久很久以前,在兔子王国 Bunnylore,有一只年轻的软件开发兔子。

他对编写无错误的软件有着极大的执着,很快他就得到了“无错误”(No Bugs)的绰号。

他迅速成长为一名架构师,并参与了不少项目,包括以下几类截然不同的项目:

  1. 某个G20国家的证券交易所;
  2. 一款处理数十万玩家同时在线的游戏(该游戏还带来了数亿美元的收入)。

在职业生涯的某个阶段,他开始为行业期刊撰写文章,并开设了一个软件开发博客。一切都进展顺利,直到有一天——无论是阳光明媚还是阴雨连绵,你可以自行选择——他拿起一本关于多人游戏开发的书,发现仅在两页上就有十六种不同的错误和三十九个这些错误的实例[Hare]。

从那时起,他开始研究关于多人游戏开发的其他书籍,发现只有两本相关书籍值得一读。

于是,“无错误”开始考虑撰写一本关于多人在线游戏开发与部署的书。

不过,他自己会更好地描述这一切。


作者及幕后团队

关于作者:本书的作者是一只来自 Bunnylore 王国的“无错误兔”。他以 Overload 期刊(ISSN 1354-3172)的专栏作家身份闻名,并对软件开发博客 ithare.com 做出了重要贡献。作为一只母语为“兔语”的兔子,他需要有人将本书翻译成“人类语言”。而且由于本书内容高度技术化,作者还需要一位具有丰富软件开发经验的译者来确保技术细节的精准性。

关于译者:本书的翻译由 Sergey Ignatchenko 完成,他自1996年起便是一名软件架构师,1998年起开始为行业期刊撰稿,其文章刊登于 CUJ、Overload、C++ Report 和 (IN)SECURE 杂志。他精通“兔语”,经常翻译“无错误”在 Overload 上的专栏。Sergey 的架构师生涯中主导过多个项目,包括作为某 G20 国家证券交易所的联合架构师(该软件还被多个国家的证券交易所采用),以及作为一个大型游戏平台的独立首席架构师(该平台支持数十万玩家同时在线,每年处理数十亿次数据库事务,并带来数亿美元的收入)。作为一种带薪的爱好,他还发明了若干专利(不幸的是,所有专利都归属他的雇主)。

关于插画师:本书的插图由 Sergey Gordeev 绘制,现隶属 gagltd.eu。他是一位专业的动画师,曾在各大动画节中获得十余项奖项,以执导《憨豆先生》的几集动画剧集最为著名。

关于编辑:本书的编辑 Erin McKnight 是一位屡获国际奖项的独立出版人,曾编辑多部小说和非虚构作品。她出生于苏格兰,在南非长大,现居于达拉斯——这是她第一次与“兔语”合作。


关于现实经验

“幸福的家庭都是相似的,不幸的家庭各有各的不幸。”

——列夫·托尔斯泰,《安娜·卡列尼娜》

如上所述,促使本书写作的契机是意识到当前市面上有关多人在线游戏(MOG)的书籍质量堪忧。不过,还有另一个经历也激发了写作的动机。

我多次与某些游戏开发公司的资深开发者、架构师或CTO交谈(或者更广泛地说,是与开发高度交互式分布式系统的公司交流),常常会有这样的对话:

  • 你们是如何做到这些的?
  • 嘘!说出来有些不好意思,我们的方法几乎违背了所有现有书籍中的指导,我们这么做、这么做和这么做……

(停顿)

  • 嗯,我们的方法也完全一样。

这实际上说明了两点:

  1. 业界确实存在一些多人在线游戏的开发实践,且这些实践对多个游戏都有效;
  2. 也许,某些实践可以视为多款游戏的“最佳实践”(虽然还不能断言所有成功的项目都是相似的)。

然而,这些实践大多未被记录下来(更不用说集中记录在一个地方),以致每个多人游戏开发团队都需要自己重新发明这些方法。<痛苦!>

而本书《多人在线游戏的开发与部署》正试图填补这一空白。总体而言,本书旨在总结行业内已知却很少发表的知识体系,试图将其集中于一体。

换句话说,本书(作为完整的九卷集)希望涵盖架构设计、开发和部署多人在线游戏的绝大多数问题(以下将列出一些例外)。

当然,鉴于任务的广度(可能过于雄心勃勃),我几乎可以肯定会遗漏一些内容。不过,我会尽力而为。

本书的主题是什么?

当你第一次拿起一本书,通常会有两个问题:“这本书的内容是什么?”以及“这本书适合我吗?”我们先来回答第一个问题。


游戏类型:从社交游戏到大型多人在线第一人称射击游戏(MMOFPS),以及中间的股票交易所

首先,我们来看看本书中涉及的游戏类型范围。令人惊讶的是,从一端的社交游戏到另一端的MMOFPS,这些多玩家游戏有许多共通之处。因此,本书旨在覆盖这些不同类型的游戏。

从宏观视角来看,所有多人在线游戏都依赖于互联网,而互联网的核心在于数据包的传输(每个数据包都存在丢失的风险)。即使从更高层次的抽象(从ISO/OSI网络模型的L3层的IP数据包到L4层)来看,用于游戏的L4协议实际上只有两种——UDP和TCP。并且,正如我们将在第四卷的“网络编程”章节中看到的,即使在交互目的中使用TCP,也需要考虑底层的IP数据包及其可能的丢失。

在服务器端,不同游戏类型也有很多相似之处。正如我们将在第三卷“服务器端架构”章节中看到的,即使是社交游戏常用的基于网页的架构,与“经典”模拟型服务器相比,也并没有那么大的区别。而在数据持久化方面(第三卷和第六卷的数据库章节将详细讨论),所有的多人在线游戏都需要数据库,这些数据库在不同游戏中也往往相似。此外,还未提及在大多数游戏中常见的话题,比如权威服务器、支付系统、随机数生成、客户关系管理(CRM)、数据中心的服务器管理、DDoS防护等。

当然,不同类型的游戏仍然会存在差异。特别是在客户端方面,各类游戏的客户端会有所不同,但某些概念会保持一致;并且,延迟要求也会大不相同,导致适用于MMOFPS的一些复杂操作对社交游戏来说完全不适用。我会尽量指出这些差异;不过,请务必“自带盐分”(BYOS,详见下文的“自带盐分”章节)在将本书的建议应用到你特定的游戏时保持批判性态度。通常,通用但不适用于特定情况的建议在软件开发中是一个“大问题™”(而游戏开发也不例外)。


股票交易所也是游戏,甚至更甚,它们是博彩游戏

“任何非内部人士参与股票市场的人,就如同在月光下买牛。”

——丹尼尔·德鲁

到这里,我希望已经成功地让你明白,所有的多人在线游戏有许多共同之处。然而,你可能仍会疑惑,为什么股票交易所也被视为一种游戏。

游戏(特别是与投注和奖励有关的游戏)往往带有一定的社会污名。换句话说,如果你告诉别人你靠玩扑克或投注体育赛事来支付生活开支,很可能不会被邀请参加重要的邻居烧烤聚会。如果你说你靠电子竞技为生可能会被接受,但前提是他们不知道你实际上是在“打游戏为生”(“你靠什么为生?”)。

另一方面,股票交易则通常被视为“非常体面的职业™”。不过我想说——

股票交易和游戏之间没有本质区别。甚至股票交易和博彩也没有本质区别。

当然,那些参与股票交易(尤其是通过其他方式赚钱的)的人会讲述许多有趣的故事来解释股票市场的不同之处。

然而,悲哀的事实是,赌博、体育博彩和股票交易都包括以下内容:

  1. 你赌一些钱(或等值物),期望能赢。
  2. 有一些基本上不在你控制范围内的事情发生(从发牌到公司发布盈利警告再到“球队A击败球队B”等)。
  3. 可能需要一些技能来影响结果,从估算扑克中的概率到预测马跑的速度或球队的表现或股票的走向;但运气依然是决定结果的重要因素。
  4. 你会赢或输,取决于那些不可控的因素。

写到这里,我希望这已经能让你看到,所有的{二十一点 | 扑克 | 博彩 | 股票交易}都符合上述描述。如果你仍然有疑问,可以查阅[31 U.S. Code § 5362 – Definitions],这是一个非常官方的定义;在该定义中,为了排除股票交易所(作为“任何受证券法监管的活动”)特意(!)将其从“赌注或投注”的定义中剔除。

如果没有这项明确排除,任何股票交易所都可以被视为“赌注或投注”。我的论点到此结束。

从技术角度而言(这是本书的重点),股票交易所和其他类型的游戏在本质上几乎没有差别。作为一个曾在股票交易所和大型游戏平台上工作的开发者,我可以亲自证明这种相似性。

因此,一本好的MOG书籍会顺带涵盖大部分适用于股票交易所的技术内容。而我希望这本书能达到这样的标准,所以也会包含这些内容。


关于互动式分布式系统的一般性讨论

如果将视角扩展到游戏和股票交易之外,鉴于我所见过的系统的数量和范围,我可以大胆地将我的经验推广到自己实践过的领域之外,得出以下结论——

几乎所有的交互式分布式系统(至少那些需要使用内存状态的系统)都与游戏类似。

换句话说:如果你的系统可以只依赖数据库状态,就可以使用传统的无状态中间件;然而,一旦需要超越缓存的内存状态,你就进入了本书所覆盖的领域。

此外,即使对于某些当前仅使用数据库存储状态的交互式分布式系统,本书中描述的某些技术(特别是第三卷的“服务器端架构”章节和第六卷的“数据库”章节)也被证实在性能和可扩展性上优于传统的将所有内容扔进数据库并期望其应对的方式。我们将看到,利用单台标准的4插槽/4机架单元(4S/4U)服务器也完全能够处理每年1000亿次的实际在线事务处理(OLTP)交易(每年写入约1万亿行数据)!


涉及的主题:除去玩法/人工智能/物理/盈利/3D

游戏的开发和部署是一项巨大的任务,因此我们需要明确讨论的具体内容。本书的野心很大:到第九卷结束时,目标是覆盖多人游戏开发与部署的方方面面,尽管有两个非常重要的例外。

首先,本书不会试图回答诸如“你的游戏应该是什么?”、“游戏应该呈现什么样子?”、“游戏机制应该如何设计?”或“如何从游戏中赚钱?”等问题;这些是你需要自己回答的重要商业问题。

在开始开发之前,你应该明确知道自己希望游戏如何呈现、玩法如何、人工智能或物理效果如何运作(如果适用),以及如何实现盈利。因此,这些问题完全不在本书的讨论范围之内。

第二个非常重要但并未深入探讨的主题是3D图形。尽管在第五卷中有一个关于图形的章节,但我可以提前告诉你,它仅是20,000字的简要概述,特别是关于3D图形的内容。遗憾的是,现代3D技术实在过于复杂(且篇幅巨大),无法涵盖于本书之中。幸运的是,已有大量优秀的书籍深入详尽地介绍了3D内容(参见下方推荐阅读部分的文献列表)。

好消息是,只要你已解决上述所有商业问题并掌握了图形内容,本书九卷内容基本上能满足你的需求。我们将探讨从总体架构到部署及部署后问题的几乎所有内容,以支持游戏发布和持续运营。

换句话说,尽管我无法回答“你想做什么”的问题,我将尽力回答“如何实现你想要做的事情”的问题,并尽量在九卷之中提供尽可能多的细节。


游戏引擎:自制 vs. 重用 vs. 第三方

从目前的宏观视角来看,无论你如何开发你的多人在线游戏,基本都能归入以下几种模式之一:

第一个选项(我们称之为DIY选项)是自己动手做整个引擎,实际上创建一个自制的游戏引擎。我个人更倾向于这种方式,但不可否认的是,这种方式并不总是可行的。特别是如果涉及3D,则需要在开发引擎上投入巨大的精力——不仅是引擎本身,还包括供游戏设计师和3D艺术家使用的工具链,而后者工作量极大。

第二个选项(称之为重用选项)无疑对AAA开发

团队有很大吸引力。它指的是使用已有的数百万行代码的3D/游戏引擎(包括所有工具等)并在其基础上构建一个多人游戏引擎。即保留所有现有的图形、脚本、关卡编辑器等,但我们将自行设计整个网络层,对现有引擎的更改最小。

第三个选项(称之为第三方选项)传统上更吸引独立开发者。这是指采用现有的带网络支持的第三方游戏引擎(如Unity或UE)并使用其开发游戏。从技术上看,与重用选项的不同在于,不仅3D/游戏逻辑引擎是重用的,所有的网络层也是第三方的。

在本书中,我们将讨论所有这些开发场景。尽管大部分讨论将围绕DIY选项和重用选项(均假设我们自行处理网络相关内容),但在第二卷中将有一个专门章节讨论“如何使用Unity 5、虚幻引擎4或Lumberyard进行多人游戏开发”(是的,在使用引擎之前仍需了解它在网络方面的工作原理)。

这些就是当前重要的内容;关于自制与重用的优缺点(以及更重要的:什么部分需要DIY,什么部分可以重用),将在第二卷中进一步探讨。

这本书适合你吗?

在解答了“这本书的内容是什么”之后,让我们来探讨第二个重要问题,“这本书适合你吗?”


本书不附带光盘

首先,要向某些可能会失望的读者稍作提醒。

我必须坦言,这本书并不是那种“教你如何快速致富”的书。更不是那种“教你复制粘贴一个游戏引擎来致富”的书。要以一种可扩展的方式推出自己的多人在线游戏(顺便可能因此赚到钱)的过程绝非易事,在开始开发你的MOG之前,你需要清楚认识到这一点。

由于本书并不是一本让你从中复制粘贴游戏引擎的书,因此没有附带光盘,也没有提供任何可直接使用的MOG引擎代码。书中确实会出现一些代码片段,但这些代码只是用于说明书中的观点,完全与可用作起点并供你稍后修改的游戏引擎无关。

没有提供这种“现成的游戏引擎”还有几个原因,但主要原因是,这种方式会将讨论限制在非常有限的、容易说明的内容上,进而大大缩小了本书的范围。


“关于一切的一无所有”

从某个角度来看,所有编程书籍都可以分为“告诉你关于某个特定问题的所有细节”的书籍,和“试图讲述各方面内容,但不深入细节”的书籍。前者内容非常具体,但通常也只能解决一个非常具体的问题,而超出该问题范围的内容则一概不谈。这类书籍通常对初学者学习某一项目有帮助,但其应用往往也仅限于学习项目。

而后一类书籍,即那种“关于一切的一无所有”的书籍,尝试尽可能地概括内容,代价是不会在每一个细节上深入。这类书籍通常不适合通过实例学习,但可以帮助经验丰富的开发者更深入地理解——不再是告诉你“如何完成低层次的操作”,而是“如何将这些低层次的操作整合成更大的整体,以及如何在这个整体中进行平衡以实现预期结果”。在试图进行这种平衡时,通常最好(也许是唯一可行)的方式是通过相关的真实经验来解释。

当然,这两类书籍的界限并不总是那么清晰,有些书介于两者之间;但本书坚定地属于“关于一切的一无所有”类型。这种特性也很好地契合了没有光盘(如上所述)以及面向中级及以上开发者(如下文所述)的定位。


先修知识:中级以上

本书面向的是具有一定经验的开发者(换句话说,这不是一本包含IDE截图和复制粘贴示例的“如何开发你的第一个程序”类型的书)。如果你的游戏项目是你的第一个编程项目,那么理解本书的内容可能会有困难。

我甚至会进一步指出——

本书的目标读者是那些希望从中级开发者成长为高级开发者的群体,范围一直延伸到CTO和架构师。

具体来说,本书不会解释事件驱动编程的原理、乐观锁和悲观锁的区别、为何需要源代码控制系统等基本概念。相反,本书会讨论诸如未来模式如何与事件驱动编程配合、在何种情况下游戏适合使用乐观锁,以及如何在不可合并文件存在的情况下使用源代码控制等话题。

另一方面,本书并不依赖于某一特定领域的深入知识。你不需要成为一位网络专家,对RFC 793的每个细节了如指掌;也不需要有着对着色器和/或CUDA的实践经验;甚至不需要成为一位C++大师,可以用模板编写任意的图灵完备程序,或是一位DB/2专家,可以预测在WHERE子句中加入“1=0”会如何影响执行计划,或是可以不用参考任何文档就配置基于BGP的DDoS防护的系统管理员(顺便说一句,老实说,这些事情我自己也做不到)。

当然,3D图形经验对于3D MOG游戏开发可能有帮助,网络基础和套接字知识也会有益。但当讨论超出“每个中级开发者理应掌握的内容”时,我会尽量提供指向相关材料的推荐,以便读者可以进一步学习。


最后,且同样重要的是——

即使你是一位经验丰富的开发者,但如果你从未开发过单人3D游戏或多人游戏,那么直接开始一个多人3D游戏并不明智。

无论是3D游戏还是多人游戏,哪怕单独来看都是庞大的学科,因此尝试在同一个开发项目中学习两者的基础知识,可能导致灾难性的结果。

话虽如此,我相信可以通过单人3D游戏的开发或非3D的多人游戏(包括社交游戏和股票交易)逐步迈入多人3D游戏的开发。


关于基于局域网的游戏和点对点游戏

历史上,很多多人游戏开发(特别是独立游戏开发者)集中在基于局域网和点对点(P2P)的游戏上。

我必须坦白,我并不热衷于点对点游戏架构(即使是选出其中一个节点充当临时权威服务器的形式)。其中一个原因是,这种架构本质上极易被作弊者利用,一旦你的游戏足够流行,吸引了成千上万彼此不相识的玩家,游戏很可能会被黑客入侵(关于作弊的讨论请见第二章)。

因此,本书大多在权威服务器的上下文中讨论相关问题(顺便说一句,业界大致也认为这是未来的方向);此外,本书假定服务器是由你的公司控制的(而不是在某个玩家家中,通过ADSL连接并坐在NAT后面)。不过,本书中描述的许多概念也适用于点对点游戏,甚至是基于局域网的游戏。但是,如果你的游戏是基于局域网的,请谨慎,不要依赖于我所写的所有内容;局域网游戏中的决策因素与广域网游戏有显著差异,结果是,针对局域网开发的某些内容可以显著简化。


推荐阅读

通用编程
  • 《计算机程序设计艺术》 by Donald E. Knuth(尤其是第一卷)

游戏编程(非网络相关)
  • 《Game Programming Patterns》 by Robert Nystrom
  • 《Game Engine Architecture》 by Jason Gregory
  • 《Game Coding Complete》 by Mike McShaffry 和 David “Rez” Graham
  • 《Game Programming Gems》系列
  • 《Game Engine Gems》系列

3D 编程
  • 《3D Game Engine Architecture: Engineering Real-Time Applications with Wild Magic》《3D Game Engine Design: A Practical Approach to Real-Time Computer Graphics》 by David H. Eberly
  • 《Real-Time Rendering》 by Tomas Akenine-Möller, Eric Haines, Naty Hoffman
  • 《GPU Pro》系列

网络编程(非游戏相关)
  • 《Unix Network Programming, Volume 1: The Sockets Networking API》 by W. Richard Stevens

游戏网络编程
  • 《Multiplayer Game Programming》 by Joshua Glazer 和 Sanjay Madhav
  • 《Massively Multiplayer Game Development》《Massively Multiplayer Game Development 2》 by Thor Alexander

C++ 编程

对于 C++ 初学者

  • 《C++ Primer (5th Edition)》 by Stanley Lippman
  • 《Programming: Principles and Practice Using C++ (2nd Edition)》 by Bjarne Stroustrup

对于已有 C++ 经验、需要升级至 C++11/C++14 的开发者

  • 《The C++ Programming Language (4th Edition)》 by Bjarne Stroustrup
  • 《Effective Modern C++》 by Scott Meyers

安全性
  • 《Applied Cryptography》 by Bruce Schneier
  • 《Security Engineering》 by Ross Anderson

如何阅读本书

约定

本书采用了较为传统的写作约定,但有一些地方可能需要进一步说明。

首先,书页边缘会有带有作者头像的小提示。它们只是书中已有句子的重复,但反映了作者对这些内容的情感态度。当我描述某些内容时,坦诚地说,我确实相信这些内容是准确的;然而,我是否喜欢这些内容是另一回事,我希望能够表达我的感受,而不影响正文的流畅性。

其次,书中还有“维基引用”,用于介绍一些在某些行业中较为人所知,但对部分读者而言可能全新的术语。我不会深入讨论这些术语(本书已经足够冗长),而是建议读者在其他地方查阅这些术语(通常推荐的资源是维基百科和谷歌)。


代码示例

作为一本开发相关的书籍,书中会包含一些代码示例。大部分代码示例使用C++编写;但这并不意味着这些概念只能适用于C++。恰恰相反,除第五卷中一章专门针对C++的内容外,其他大多数示例都适用于几乎所有编程语言,C++只是作为最常用于游戏开发的语言出现。

另外,请注意,这些示例只是用来说明某些概念。除非明确讨论,我并不试图教授C++或C++的最佳实践。因此,遇到“是遵循最佳实践,还是让主要思想更直观”的两难选择时,我更可能在保证思想易于理解的前提下牺牲一些最佳实践。


我的“显而易见”帽子

本书的目标受众相对广泛,因此不可避免地要解释某些对特定读者而言“显而易见”的内容(但对其他群体可能并不明确)。此外,书中每一部分的内容都可能早已为某些人所知。因此,请不要抱怨“书中的大部分内容都是众所周知的”,这本书的主要目标就是“总结业内已知但很少发表的知识体系”。

因此,如果我在书中提到你已熟知的内容,请不要责怪我。可以确信,书中的某些信息对其他开发者而言是新知(也不要急于称呼这些人为“白痴”,因为他们可能掌握着你尚未了解的其他知识)。

我会尽量在我已知某部分内容不适合特定人群时标明(例如,我关于图形的想法对3D专业人士来说可能显得过于基础)。尽管如此,难免会有疏漏之处,对此带来的不便我表示歉意。


术语

由于多人在线游戏(MOG)开发是一个宽泛但尚未完全标准化的领域,术语混乱时有发生(更糟的是,同一术语在不同子领域含义不同)。我不会讨论“哪些术语是‘正确’的”(这很主观,讨论术语本身也毫无意义)。因此,在使用这些术语时,我会简单定义我在本书中如何使用这些术语。


MMO与MOG

“多人在线游戏”(Massively Multiplayer Online Games, MMOG 或 MMO)这一术语容易引起混淆。

争议的焦点在于那些拥有大量玩家,但并非所有玩家都在同一个游戏世界的游戏。例如《反恐精英》(CS)或《英雄联盟》(LoL),尽管拥有大量在线玩家,但他们并不处于同一虚拟世界。有一种观点认为“嘿,这是多人游戏,它在线上,且拥有大量玩家,所以它是MMO。”而另一种观点(已占据维基百科的MMOG词条)则认为,若要称为MMOG,必须运行在单一的游戏世界实例中。

如前所述,我不会详细讨论术语问题,只是提及,为了避免混淆,我会尽量避免使用“MMO”一词(除非提到定义明确的MMORPG或MMOFPS)。这意味着——

本书讨论的内容会统称为多人在线游戏(MOG),即便它们拥有大量玩家。

事实上,大部分情况下我假设我们讨论的游戏能够支持数十万同时在线的玩家;这是唯一真正重要的事情(是称之为MMOG还是MOG并无太大区别)。


服务器

在多人在线游戏的世界中,“服务器”这一术语被广泛使用,可能指代多个不同的事物。

一个含义是“服务器”,即“物理服务器设备”;另一个含义是“玩家可以连接的场所”(例如“西欧服务器”)。尽管名称如此,后者实际上几乎都是由多个物理服务器组成(通常位于同一数据中心)。更令人困惑的是,术语“服务器”还常用于表示不同的游戏世界实例(可以是战斗竞技场的一个实例,也可以是复杂MMORPG的整个游戏世界实例)。

为了避免不必要的混淆,本书中将物理服务器设备称为“服务器”(Server),位于同一数据中心的多个物理服务器称为“数据中心”(Datacenter)。至于“游戏世界实例”,我们将每个运行在物理服务器上的逻辑分离实体称为“游戏服务器”(Game Server);而在讨论具体的游戏服务器类型时,我们将其称为“游戏世界服务器”(Game World Server)或“匹配服务器”(Matchmaking Server)等。需要再次强调的是,这些定义并不“正确”,只是我在本书中选择的约定。


专用服务器

另一个关于多人在线游戏的混淆来源是“专用服务器”的定义。在托管行业中,“专用服务器”通常指你拥有root/管理员访问权限的物理服务器设备;这种服务器通常可租用,术语用来区分“专用服务器”(物理设备)与“虚拟服务器”(即物理设备的一部分,在某些情况下,如云服务中,这些虚拟服务器还会随时间从一个物理设备迁移到另一个设备)。

然而,在多人在线游戏开发中,“专用服务器”通常指的是“没有图形直接连接的游戏实例”(此定义在独立游戏开发者中较为常见,源自以客户端为中心的开发流程,我们将在第一章讨论该流程)。

为避免混淆,我将尽量避免使用“专用服务器”一词;若偶然提到(或讨论从ISP租用服务器时),我指的是第一个定义(即通常从托管ISP处租用的物理服务器设备)。


自带盐分(BYOS)

在进入更实际的讨论之前,还有最后一点需要提及。没有一本书(包括本书)中的任何一句话应被视为“绝对真理”。在实际操作中(尤其在游戏开发中),几乎每一个“这样做”的建议都有相反的示例表明,有时这个操作可以(甚至应该)用不同甚至相反的方式进行。

所有的建议都有其适用限制,本书的任何建议也不例外。当我已知某些与游戏相关的场景可能超出这些限制(使建议不再适用)时,我会尽量提到。然而,在一个庞大的行业如游戏开发中,难以预测所有使用场景,因此你应做好准备,本书中的一些建议(或其他书中的建议)可能不适用于你的游戏,且未标明其适用范围。

因此,请带着适当的怀疑阅读任何书籍(包括本书)。盐分不随书附赠,你需要“自带盐分”(BYOS)。更具体地说——

对于每一个基于本书建议所做的决策,请问自己:

这个建议是否真正适用于我的具体情况?


参考文献

这就涵盖了阅读本书的基本指导。随着接下来的章节展开,您将更深入地了解多人在线游戏开发中的具体实践和应用。

@WangShuXian6
Copy link
Owner Author

第1章 从多人在线游戏(MOG)视角看游戏设计文档(GDD)

你作为游戏的“父母”

“你不‘制造’小提琴,小提琴——就像面包、葡萄和孩子一样,是‘孕育’和‘成长’出来的。”

——尼古拉·阿玛蒂(摘自《访问米诺陶》)

开发一款游戏就像养育你的孩子。游戏将经历从构想到婴儿期再到幼年期的所有典型发展阶段。虽然游戏的开发不会止步于此,但本书将主要讨论游戏从零到基础完善的过程,关于更高层次的游戏发展和成长期的问题则不在本书的讨论范围内,因为那更接近心理学的范畴,而非即将讨论的这些“物理”问题。

书中的“你”指的是“游戏的父母”,你可能是一个300人团队,也可能是一个独立开发者。我们关注的不是团队的规模,而是团队对项目的投入程度。

如果你(作为未来的“父母”)不认为你的游戏是你的“孩子”,那么在开始之前要三思而后行。若仅凭赚钱的念头去进行这样艰难的开发,可能不是你人生中最好的决定。如果你为了赚钱而开发游戏,却对项目本身毫无感情,那有两种可能的结果:第一种情况是,你会逐渐对项目产生感情,最终获得开发过程中所需的热情,从而大大增加成功的机会;第二种情况是,你始终为了钱在开发,但讽刺的是,以这样一种纯粹的逐利心态,成功制作出优秀游戏并赚到钱的机会微乎其微。

简而言之: 如果团队对即将到来的游戏没有激情,不要开始开发。


给首次游戏开发者的3500字速成课程

如前所述,我们假设你已经有了一个“伟大的游戏创意™”,并对其充满激情,已经充分了解了预期的用户体验、物理效果和人工智能(AI)等内容,并且迫不及待想要开始开发。

你应该采取的第一步是什么?开始编码?不对。选择编程语言?稍微接近一点,但依然不对。你的第一步应该是明白你想要达成的目标。

对于任何游戏,玩家(以及其他项目干系人)都对其有一些期望,这些内容通常记录在游戏设计文档(GDD)中。


关于游戏设计文档(GDD)

在游戏开发行业中,通常会编写GDD来高层次描述“游戏应该如何运作”,其中包括角色、玩法等内容。编写GDD不仅是业界常见的做法,而且非常有必要。GDD帮助你明确所要实现的目标,是成功开发的前提条件。虽然小团队可能不需要正式的GDD(实际上只是记在脑海中),但即便如此,花半天时间把它写下来并讨论往往会带来显著帮助。

对于来自其他领域的开发者而言,GDD类似于游戏的“业务需求”文档。

接下来,我们将讨论GDD的一些重要特性。暂时我们将集中于单人游戏和多人游戏中GDD的共同属性;而关于针对多人游戏的GDD特性,我们将在后续章节(“有限生命周期与无限生命周期的游戏”)中讨论。


每天都可能会变化

GDD往往会频繁变更,并不会“一成不变”。这是大多数软件项目的常态,尤其适用于游戏开发。因此:

要预期GDD会发生变化,并为这些变化留出足够的余地。

即使你被告知某项内容“永远不会”改变,请记住,“永远不会”可能会比你想象得来得更快。这并不是要你构建一个“绝对通用”的系统来应对任何变化(关于过度泛化的危害,我们稍后会提到),而是建议你在需要重写系统的一半时不要过于沮丧。哦,另外,请保留这些“绝不会改变”的承诺记录,这样当GDD变化时,你可以解释为什么从干系人角度看似简单的更改需要重写一半的系统。

毫无疑问,这一原则和敏捷开发的“随需应变”理念相契合,但请记住,敏捷开发的某些基本原则(包括这一点)在现实中确实适用。


灵活应变,并将其记录下来

需要理解的一个重要点是,GDD的“灵活性”并不意味着你不需要记录内容。虽然GDD的每项需求可能会在后续发生变化,但在任何时候,都应明确(且开发人员和干系人达成共识)你当前想要实现的目标。当GDD变化时,没关系,你可以更新它。

我通常建议将GDD视为源代码控制系统下的文档之一。无论如何,GDD的影响类似于C/C++中极高层次的头文件:更改头文件代价较高,但大多数情况下并不意味着要重写所有内容,尤其是在你已为某些变化做好准备时。


过度泛化的谬误

“雕刻很简单,只要把不像大卫的石头部分凿掉就行了。”

——(被误认为是)米开朗基罗

在追求灵活应变、极端理解“做好变更准备”的过程中,往往会产生编写一个“能应对一切”的系统的诱惑,这样的系统“永远不会改变”(因为能通过某种配置或脚本来处理“任何情况”)。

作为一名程序员,我完全理解“编写优秀代码™以避免以后更改”的倾向。然而,现实中并非如此。过度泛化的问题首先在于实现所需的时间,但真正的问题出现在过度泛化框架完成后。最终可能会发现:(a)系统中的“任何情况”对于实际应用而言过于狭隘(即无法真正使用,通常需要从头开始),或者(b)配置文件或脚本充其量只是勉强可用(不足、过于复杂、繁琐等)。在过度泛化的极端情况下,配置文件或脚本几乎成为了一个完整的编程语言,所以在完成过度泛化系统的所有工作后,我们需要学习如何使用这种(奇怪且通常不太方便)语言进行编程,然后再用它来编写游戏——这意味着在经历了所有的努力后,我们其实回到了起点。

事实上,能够“应对一切”的系统已经存在,且没有什么不好。实际上,任何图灵完备的编程语言都可以应对一切;从某种意义上说,图灵完备的编程语言代表了绝对的自由。然而,由于编写图灵完备的语言通常不在游戏开发的范畴内,我们作为游戏程序员的职责有所不同。

我们作为程序员所做的,实际上是对图灵完备的原始语言的绝对自由进行限制(就像雕塑家限制自己在原始石料上的绝对自由),并决定“我们的系统能做到这些,但代价是不能做到那些”。就像雕塑艺术的精髓在于知道何时停止打磨石头,软件设计的艺术在于感知何时停止对编程语言的自由进行限制。

回到实际开发——

在开发游戏(或任何其他项目)时,在过度泛化与过于具体之间找到合适的平衡极为重要。


关于项目干系人

每个软件开发项目都有其干系人。一般而言,干系人可以是投资人、管理者和/或客户。

在游戏领域,这往往包括制片人、市场与变现人员、客户支持人员(CSR,通常称为“支持人员”),当然还有玩家。对于游戏来说,玩家是一个极其重要的干系人类型。

为了让游戏成功,关键在于——

让项目干系人,包括未来的玩家,参与到开发过程之中。

如果项目干系人未能参与开发过程,游戏可能会在某种程度上失败。对于游戏来说,项目干系人必须包括未来的玩家。

如何在团队中代表未来玩家是一个相对复杂的问题,通常采用“焦点小组”的形式,但这种方式并不普遍,甚至有争议。实际上,是否使用焦点小组取决于你——

然而,无论如何,未来玩家的观点必须有人代表。

在不同的开发环境下,可能由制片人代表玩家的角度,也可能由游戏设计师负责,但通常而言,距离“字节如何流动以实现功能”越远越好;否则,可能会陷入“只见树木不见森林”的困境。

遗憾的是,当我们(作为程序员)编写代码时(以及在一定程度上,游戏设计师设计关卡时),我们对游戏的判断会受到影响;换句话说,我们对游戏内部工作机制

(以及实现特定功能所需的工作量)知道得太多,无法真正代表“普通玩家”的观点。虽然基于这些知识的建议可能非常有价值,但关于玩法的决策通常应由那些非程序员的未来玩家来做出。

关于焦点测试和游戏测试

在游戏开发过程中,玩家可能会在两个不同阶段参与测试。

在早期阶段,称为“焦点测试”(术语可能因人而异)。焦点测试的关键在于,它通常在玩家可以看到游戏内容之前进行。在游戏开发界,一些知名开发者对焦点测试评价不佳,比如“放弃焦点小组”[Brightman]和“焦点小组已成为‘f词’”[Donovan]。

在后期阶段,当游戏已经可以试玩时,这一阶段的测试称为“游戏测试”。为了让事情更复杂一些,在“游戏测试”中也可能会有“焦点小组”的参与。

我不会深入探讨这个颇具争议的话题,但会提到几个(希望是显而易见的)要点:

  1. 不应利用“焦点小组”来确定游戏的“大创意”(这项决策应完全由你做,否则可能会遇到大麻烦™)。
  2. 另一方面,消除一些相对次要的细节问题(例如3D模型和图形)通常是有益的,这可能为“焦点测试”提供了机会。
  3. 无论是否进行“焦点测试”,不进行“游戏测试”是极其不明智的行为,包括“Alpha测试”、“封闭测试”、“公开测试”等。
  4. “游戏测试”可以包括“焦点小组”,但我不太喜欢传统焦点小组的形式,因为玩家互动可能会抑制较少发声的成员表达意见。
  5. 总的来说,“游戏测试”应尽早开始,根据需要调整。

关于营销和变现:临界质量

然而,仅仅将未来的玩家作为项目干系人是不够的。为了让游戏生存,你大概率需要某种变现方式。因此,那些负责变现的人员也是重要的项目干系人,必须参与游戏开发。否则,你可能会推出一个人人喜爱的游戏,但由于忽视了变现,导致资金不足,无法运营服务器或支付开发人员薪酬。

更重要的是,营销和变现团队的帮助可能让你避免遗漏整个MOG开发中的关键因素——如何实现“临界质量”。简单来说,这是一个经典的“鸡生蛋蛋生鸡”难题:游戏未达到X个玩家时,玩家会因缺乏足够的对手而流失,这使得“临界质量”对独立开发团队来说至关重要。临界质量极大地依赖于游戏类型(以及你的匹配算法),但粗略估算,你至少需要在一天中的任何时间都有几百名玩家在线,才有成功的机会。


关于干系人的参与

需要记住的是,干系人的参与不是单向的“干系人提出需求,开发者执行”的流程。理想情况下,团队应有一种文化,即“开发者有疑问时可以询问干系人”。从我的经验来看,这类团队往往能制作出真正伟大的游戏™

不过,要实现这一点,需要干系人在整个游戏开发过程中的可用性。换句话说,当开发者在GDD相关问题上有疑问时,应该有干系人可以提供权威的意见。

项目干系人简要总结

  • 未来玩家和其他干系人(如市场和变现团队)参与GDD的开发和修改是绝对必要的。
  • 没有干系人就没有GDD,没有GDD就无法开发游戏。换种方式开发非常冒险。

非MOG团队的典型结构

在典型的非多人游戏团队中,以下团队通常会参与游戏开发(按重要性顺序排列):

  • 商务和变现
  • 制片人
  • 游戏设计师
  • 艺术家(各种类型)
  • 程序员(包括运行时程序员、工具程序员)

团队的规模可能从几人到数百人不等。在小型开发环境中,可以将这些视为角色,而不是团队。

需要注意的是,对于MOG团队,通常还需要额外的四个团队或角色,详见“MOG特定团队”一节。


上市时间、最小可行产品(MVP)和计划

开发游戏(或任何软件)时,重要的是在市场环境下尽快发布。若开发周期过长,整个主题可能会过时,或者图形会显得落伍。例如,如果你在90年代的恐龙热潮期间开始开发关于恐龙的游戏,但到2015年才完成,目标受众可能已显著减少。

这就是为什么我们通常被要求尽快交付游戏。若忽视这一点,最终可能导致匆忙上线,丢弃关键功能,减少测试,从而降低游戏质量。

应对上市时间的挑战

应对上市时间问题并不容易,但并非不可能。为避免在开发后期的仓促上线,需采取两个措施:

  1. 定义最小可行产品(MVP):明确首次发布所需的核心功能。可以将MVP视作“露营必需品”,从最重要的功能开始,逐步剔除非必要项。与干系人沟通优先级时,要坚持立场,因为项目的健康发展取决于合理设定优先级。

  2. 进行适当的计划:虽然计划往往不受程序员欢迎,但确实需要时间表(包含合理的缓冲时间)和里程碑,并且尽量遵守计划。

控制节奏的重要性

在开发首款游戏时,许多人可能会急于选择一个游戏引擎并围绕它实现游戏,或者(特别是有Web开发背景的开发者)围绕数据库构建游戏,或者围绕某种协议(TCP或UDP)构建游戏。

然而,在开发过程的早期阶段,务必要认识到,当前还没有足够的信息来做出架构决策。这些引擎、数据库和协议仅仅是实现细节,而我们还远未进入实现阶段。

虽然你的多人游戏可能需要某种图形引擎,可能会用到数据库来持久化数据,也需要基于某种IP协议运行,但此时将这些作为游戏开发的核心还为时过早。特别是,在没有全面理解游戏机制之前,过早决定游戏是否应以引擎、3D引擎、数据库或协议为核心是危险的。

在没有完整的GDD和实体-交互图的情况下做出这些决策(包括任何架构决策),可能会严重限制选择空间,若此时决策出现失误,极可能导致效率极低甚至无法实现的设计。

例如,若决定“系统应以数据库为核心,所有状态始终写入数据库”,而你的系统是一个二十一点游戏网站,那么这样的实现会产生比替代方案高出10倍的数据库负载,还会遇到实现回滚的困难(在多人游戏站点崩溃后,部分游戏可能会在中途被打断,而必须支持回滚)。事实上,对于许多赌场类多人游戏的最佳实现方案,是将游戏状态仅存储在内存中,仅在单局游戏完成后与数据库同步。然而,只有在绘制实体-交互图后,才会显而易见这一点。

再举一个例子,如果你决定“系统应以游戏引擎为核心”,而你选择的游戏引擎不支持所谓的“兴趣管理”(将在第三章讨论),那么你可能得到一个适用于小型虚拟世界的系统,但对较大世界则完全无法扩展,因为它会由于假设“每个人都与每个人交互”而产生O(N²)的流量。

TL;DR:对新手开发者速成课程的总结

  • GDD 是绝对必要的
  • 干系人参与游戏开发过程是必需的
  • 干系人必须包括未来玩家的代表、变现和营销团队
  • 开发者(即便是玩家出身的开发者)不能完全代表玩家,应有非开发者的玩家参与
  • 最低可行产品(MVP)和最大可行计划是明智之选
  • 在完成GDD和实体-交互图之前,确定实现细节(包括但不限于游戏引擎、数据库、协议)为时过早

三条极为重要的GDD规则

在GDD的编写上,有三条至关重要的规则(尽管它们的核心理念相似,但我仍倾向于单独列出)。第一个规则是:

GDD应由项目干系人编写(而非程序员)

作为程序员,我们应当积极参与GDD的开发,并在发现不可行的地方提出意见(最好用比“你们疯了吗?”更礼貌的方式),但当干系人坚持他们的决定时,我们也应当接受(前提是他们不干涉实现细节;见下文)。

经过多年的编程经验,我知道这是一条难以遵守的规则,但我也承认自己有时会被一些很有实现价值但对玩家几乎无影响的细节所吸引。同样,出于害怕实现困难而回避功能需求的倾向,也会对最终产品质量造成毁灭性影响。无论哪种情况,都说明了一个问题:由程序员编写的GDD存在天然问题——我们通常太关注实现细节,以至于难以把握“大局”。这实际上是典型的“只见树木不见森林”问题,与其他心理问题一样,难以找到有效的解决办法。

第二条需牢记的规则是:

GDD不是关于“我们如何实现它?”,而是关于“我们要实现什么?”

作为由干系人编写的任务定义文件,GDD不应讨论实现细节。当然,完全无法实现的内容应予以过滤,同时程序员在GDD讨论中指出“实现这个功能将需要额外三个月”是合理的反馈(这需要理解但不必解释“如何实现”)。然而,这两种反馈是GDD讨论中唯一应允许的与实现相关的反馈。

为了使第二条规则更具操作性(从而易于执行),我发现以下第三条规则效果良好:

GDD必须完全以玩家术语编写,其他内容属于实现细节,不应出现在GDD中。

例如,玩家关心的是游戏可以在哪些平台上运行,因此“支持哪些平台”显然属于GDD的一部分;但玩家并不关心你使用何种编程语言(只要该语言能在所有平台上运行)。再比如,玩家可能在意响应时间和应用在防火墙环境下的表现,但并不在意你是通过TCP还是UDP实现,只要功能正常即可。

关于分离GDD与实现细节的重要性

为什么GDD的第2和第3条规则如此重要?因为用实现术语而非玩家术语编写GDD需求,可能会严重限制你选择最佳实现方式的能力。

例如,假如在GDD中写下“我们的应用必须使用Java编写”这种不良需求(而不是“我们的应用必须在Windows、iPhone和Android上运行”这种良好需求),你可能根本不会考虑用C++开发并通过NDK移植到Android的方式(仅使用最少量的Java UI),也不会考虑使用emscripten(更多细节将在第II卷的客户端架构章节中讨论)。虽然这些选项不一定更好,但在没有充分考虑的情况下直接排除,通常不是明智之举。

再举一例,如果在GDD中写下“不良需求”:“我们必须使用UDP”(而不是良好需求:“在99.99%的情况下,用户按下按钮到其他用户看到响应的延迟必须小于200ms”),你可能不会去了解如何提升TCP的互动性(相关内容将在第IV卷的网络编程章节中讨论),可能错失通过TCP提升防火墙兼容性和简化开发的机会。同样,写下“我们必须使用TCP”(而非“我们必须达到TLS级别的安全性”)的需求也会导致错失在UDP上通过实现DTLS和/或可靠UDP加密来提高响应速度的机会。

简言之,用玩家术语编写GDD让你保持实现选项的开放性,而保持选项开放通常是明智之举™


处理“棘手”的干系人以及(尽量避免的)管理者

GDD与实现细节的分离意味着,如果项目干系人(未来玩家、市场人员、管理者、投资者等)表示“我们需要在GDD中写明游戏必须用Java编写”或“必须使用TCP”,你需要解释这是实现细节,要求以玩家能够理解的术语来重新定义需求。

此外,如果这样的“棘手”干系人是个管理者,在所有解释之后仍然坚持将{Java、TCP、UDP或任何其他实现细节}作为GDD的一部分,你需要认真考虑是否想参与这个项目,因为这种对基本概念的深层误解通常预示着即将来临的极端微观管理和可能的深度冲突。


有限生命周期 vs 无限生命周期游戏

即将开发的MOG中的一个GDD需求,尽管较为陌生,但对开发有重要影响,这就是游戏的预期寿命。游戏寿命对架构和设计有显著影响。

从1980年代的古老游戏开发时代起,大多数游戏的模式类似于书籍销售,即寿命有限。有限寿命的游戏在架构上与传统单人游戏相似,通常依赖一个图形引擎,且该引擎与游戏的其他部分紧密耦合。对于不打算在未来销售的游戏,这种模式是合理的:两年后会有另一款游戏和新的引擎接替它。

然而,进入21世纪后,游戏开发商提出了一个长寿命游戏的模式,即一次开发,长期使用,靠订阅或持续的游戏内购买盈利。这对MOG尤其适用:如果游戏足够好并获得忠实玩家群体,可以持续盈利。这种无限寿命游戏的理念是:“尽量从游戏中获取更多收益,保持其盈利,并持续开发。”

例如,股票市场游戏、魔兽世界、扑克游戏或Top Eleven足球经理,这些游戏都不打算在预设时间内消失。大多数此类游戏旨在长期存在,为开发者提供工作、为公司创造利润,而这一GDD层面的需求在架构选择上产生了深远的影响。

对于无限寿命的游戏而言,依赖第三方游戏引擎的风险过大。如果引擎不是100%自主开发,那么问题就出现了:“你能确保这个引擎在未来5-10年满足玩家需求吗?” 这使得DIY方案更具吸引力,或是设计出可以更换引擎的架构(例如在客户端架构章节中讨论的逻辑与图形层隔离的方案)。


客户端驱动与服务器驱动的开发工作流程

在MOG开发中,根据游戏的不同特性,通常会有两种截然不同的开发模式,称为“服务器驱动开发工作流程”和“客户端驱动开发工作流程”。这并不是说某种模式适用于所有游戏,而是每种模式对特定类型的游戏最为合适。

服务器驱动的工作流程

服务器驱动开发工作流程通常适用于主要由规则定义的游戏,且无需(或仅需少量)视觉元素让游戏设计师工作。换言之,游戏设计师主要关注游戏规则,几乎没有其他内容;尤其是关卡设计往往不存在或非常简单。此模式下,工具链较简单,服务器团队负责实现游戏规则,客户端仅负责执行来自服务器的指令。简言之,服务器为王。

客户端驱动的工作流程

相对而言,客户端驱动开发工作流程更常见于基于3D的模拟类游戏(如MMORPG或MMOFPS),更接近传统单人游戏的工作流程。在这种情况下,游戏设计师不仅仅关注规则,而是需要在设计过程中看到客户端的实际表现。游戏玩法与关卡设计强烈依赖3D引擎,这表明客户端驱动开发流程的适用性。

客户端驱动工作流程中,游戏设计师大量使用视觉内容(如关卡设计),工具链通常较复杂。设计师可以不考虑分布式特性,只需在视觉地图中定义游戏逻辑。例如,“当PC靠近此点30米范围内且等级低于29时,遭受严重攻击”。这种方式可能是唯一的可行方案,否则设计师需要考虑的内容过于繁杂。

总体来说,客户端驱动的游戏开发流程围绕客户端,分布式机制的影响较小(至少对设计师而言)。简言之,内容和客户端为王。

客户端驱动工作流程不意味着客户端拥有权威性(例如权威客户端)。实际上,这种流程的游戏也可以包含服务器端权威对象和客户端的这些对象代理。但在增加新NPC(或其他游戏实体)时,工作流程通常是通过某种关卡编辑器创建它们,这本质上是客户端工具。


处理客户端驱动的工作流程

面对客户端驱动的工作流程时,有两种主要选项,虽然从游戏设计师的角度看,体验可能类似,但其实现细节截然不同。

选项1:“持续转换”

第一个选项(通常是独立开发者使用第三方游戏引擎时采用的方式)是将整个开发过程视作单人游戏,然后“转换”成多人游戏(尽管转换过程必须是持续的)。换言之,现有非MOG引擎的工具链在不涉及多人的情况下运作,而由服务器团队负责将游戏“转换”成真正的MOG。这种方法可能可行,但必须确保在游戏规则确定后并行进行此转换,并尽早进行模拟测试(如模拟延迟、丢包等)以消除分布式问题。将转换推迟至开发后期通常会导致灾难性的后果。

如果采用此方法,进一步的选择包括:

  • 选项1a:使用引擎集成的服务器端支持
  • 选项1b:编写一个导出工具,将客户端3D引擎中的关卡信息导出到自定义格式,并使用此格式创建独立服务器

具体的第三方游戏引擎及相关网络库将在第II卷中讨论。

选项2:“将服务器集成到工具链中”

第二个选项(推荐方案,如果预算允许)是将服务器端集成到工具链中。这意味着每次设计师运行游戏查看修改效果时,不仅仅是客户端启动,而是一整套进程,包括:

  • 她的客户端
  • 完整服务器
  • 模拟玩家
  • 模拟网络问题的网络环境

尽管此集成比“持续转换”难以实现,但能为设计师提供更及时的反馈。虽然无法消除所有网络相关问题,但确实能够更早发现部分问题(从而加快开发速度并提高整体质量)。

关于匹配机制与MOG的社交属性

在一个成功的MOG中,通常会有大量玩家分布在许多不同的游戏世界上。在这种情况下,如何将玩家分配到不同的游戏世界成为一个重要问题。这个过程通常称为“匹配”(Matchmaking)。

不成功的匹配机制(经验法则)

在技术实现上,许多人倾向于将玩家视为可分配的资源,永久性地将玩家分配到不同的游戏世界,并禁止他们之间的交互。这种方法通常涉及每个游戏世界有自己独立的数据库,彼此完全隔离。

然而,使用这种“随机永久分配”的方式需要非常谨慎,因为在社交方面,它往往会带来负面影响。

任何游戏外的社交整合(包括Facebook或游戏专属论坛)都需要玩家能够相互互动,而不仅仅是因为规则引擎决定了他们属于同一个服务器。因此,需要牢记一个重要的观察:

如果你认为玩家之间的自由互动不是GDD的需求,请重新考虑。

例如,“与Facebook好友一起玩”这样简单的功能都要求玩家能够“认识”彼此并互动。此外,某些玩家可能希望与特定玩家一起玩(无论是因为喜欢对方,还是享受击败对方的感觉),论坛、线下比赛等也促成了这种需求。

我曾见过一个流行的社交游戏试图在单一服务器中实现有限的社交互动,但因为玩家被随机分配到不同服务器,导致社交功能受限。即便使用了各种临时修补措施,问题依然没有得到有效解决。

成功的匹配机制

让玩家能够与他们认识的人一起玩,以下几种方法在实践中效果较好:

  1. 玩家选择的独立数据中心。这种方法在MOG中广泛应用,特别适用于对延迟敏感的游戏。尽量避免将玩家永久分配到单一数据中心中的某个特定服务器,例如“NA1”、“NA2”等,而是保持每个数据中心为一个整体。
  2. 支持跨数据中心的统一玩家账户。即使玩家不允许转移跨数据中心的游戏资产,拥有一个在所有数据中心通用的账户ID对打击作弊、信用卡欺诈以及客服支持都有巨大帮助。

另外,在不同数据中心或单一数据库中创建游戏世界实例时,通常有两种方法:

  • 按需创建游戏世界实例:例如当有足够玩家时创建比赛或活动。这通常基于大厅系统,允许玩家表达对某些类型游戏的兴趣,且可以选择与特定玩家一起玩(如加入“队伍”)。
  • 分级匹配机制:虽然排名匹配在社交性方面有所欠缺,但在特定游戏类型中可能更为适用。

对于MMORPG游戏,在分配玩家到不同游戏世界实例时,可以采取以下两种方式:

  1. 完全随机分配:没有任何玩家和游戏世界实例的固定关联。
  2. 基于概率的分配:考虑玩家之间的关系,使用概率分配。例如《激战2》的Megaserver系统虽引发了一些争议,但在社交互动方面效果不错。

简言之:

在设计匹配算法时,确保考虑到社交互动的需求。

支持较小玩家群体的重要性

关于较小玩家群体支持的GDD相关问题通常被忽视,例如“我们应该支持Windows 7玩家吗?”或“是否支持没有UDP访问的玩家?”虽然许多开发者倾向于不支持这些旧版本,但这类问题的答案不明显,需要开发团队与市场团队共同讨论。

特别是在竞争激烈的市场环境中,支持小众玩家群体可能带来巨大的优势。例如,如果在市场中大多数竞争对手不支持UDP连接,则那些不能使用UDP的5-10%玩家群体可能会转向我们的游戏。这一细分市场有时能成为增长的关键。

在做出关于支持平台和技术的决策时,考虑支持仍有一定人气的较旧技术可能会带来额外收益。

技术问题如何影响市场营销和货币化

不论你的MOG具体是什么类型,它大概率是以盈利为目的的,至少也需要支付服务器成本。因此,市场营销和货币化团队的参与非常必要。

在本书的范围内,我们不讨论“如何提升游戏乐趣”或“如何获得关键用户群”等营销问题,而是专注于可能影响市场营销或货币化的技术问题,以便您提前将这些因素考虑在内,并告知市场团队,以避免日后出现不愉快的意外。我能想到的对MOG的市场营销有影响的技术领域包括:

  1. 支持非最新的硬件/软件(前面已讨论过)。

  2. 匹配机制问题(前面也有所讨论)。

  3. “软启动”:即在不同区域和/或平台上分阶段发布游戏。“软启动”可以减轻技术团队的压力(尤其是在单个平台上逐步推出时),但对于MOG而言,这通常会在某种程度上负面影响“关键用户量”,尤其是在竞争激烈的情况下。

  4. 减少玩家开始游戏的步骤:一般而言,潜在玩家在开始游戏前需要经历的步骤越少越好。这方面可以通过以下技术手段支持:

    • 减少初次下载的大小:玩家等待时间越短,体验越好。可以在游戏启动后再从游戏内部下载附加内容,如主题、额外角色、额外关卡等。这样也减少了玩家试玩游戏30秒后放弃下载所带来的流量成本。
    • 支持无需登录的旁观者模式:如果潜在玩家可以快速下载某些内容并观看实时游戏(并且看到“有很多人正在玩,这可能很有趣”),通常会增加玩家数量。即便是竞技类游戏,也可以展示一些延迟的低级比赛或职业选手的录像。这需要技术团队和市场团队共同讨论,决定是否实现并纳入GDD中。
    • 允许第三方社交登录(例如Facebook、Twitter、Google+、Steam等):填写注册表单对玩家而言是一个相对较大的步骤,而第三方社交登录显著简化了这一过程。某些情况下,你甚至可以仅支持社交登录,这种“仅限社交登录”的策略有两个好处:一是有助于获取是否关联真实身份的关键信息,有助于反作弊;二是实现自己的登录系统通常需要大量开发时间,社交登录可能是更高效的选择。
  5. 无下载的网页客户端(即使只是旁观模式):如果能够在无需安装的情况下向玩家展示游戏内容,这往往能显著提高网站访客的转化率。这种旁观者网页客户端和上述无需登录的旁观者模式相辅相成。

  6. 避免双界面设计:与创建完整网页客户端完全不同,有些游戏要求玩家同时使用下载客户端(用于游戏)和网页界面(用于统计、购买等“附加”内容),这通常是一个不佳的设计选择™。虽然在表面上看似技术上简化,但在用户体验、安全性、市场营销和货币化上存在严重缺陷,比如切换界面增加支付步骤、增加了营销和货币化团队的整合难度等。如果游戏内购买、统计等内容可以直接在客户端内进行,而不是通过浏览器界面,这样的设计会更好。

这段内容的总结

在设计游戏的技术架构时,尽量减少玩家进入游戏的步骤,并确保技术方案支持市场和货币化的需求。

在做决策时,不要未经市场和货币化团队同意就仓促推进,避免日后被要求重新调整。

您的GDD需求清单

到目前为止(不论是否阅读了上述内容),您已经有了针对游戏的GDD需求列表。虽然每个游戏的需求列表都独一无二,但以下内容是所有项目都需要包含的关键点:

  1. 用户体验的详细描述(包括游戏逻辑、UI、图形、音效等)。这是传统GDD的核心部分,将占据GDD的大部分内容。

  2. 是否支持3D或2D:如果游戏寿命不确定,可以考虑同时实现2D/3D界面。

  3. 开发流程是客户端驱动还是服务端驱动

  4. 游戏的预期寿命:是有限寿命还是无限寿命?(请参考“有限寿命 vs 无限寿命游戏”章节的讨论)

  5. 客户端应用支持的平台清单,可能还包括网页端旁观者模式的实现。

  6. 支持的视频卡列表(如DirectX/OpenGL版本等),以及首发版本中要支持的平台/视频卡列表。

  7. 游戏时序要求:包括“玩家看到自身动作结果所需时间”(输入延迟)和“看到他人动作结果所需时间”。

  8. 同步/异步的游戏机制:游戏是需要玩家同时在线还是可以异步参与?

  9. 客户端连接的支持类型:是否支持拨号、3G,甚至GPRS网络?是否支持防火墙连接?

  10. 目标地理区域:是否全球覆盖?是否支持跨区域玩家交互?这可能会对市场营销产生影响。

  11. 社交化功能:如“邀请Facebook好友加入游戏”或“提醒玩家他们的好友在服务器上”。

  12. 匹配算法的详细说明:游戏世界实例的创建和填充方式以及其社会影响。

  13. 即时游戏支持:玩家是否能在无需等待大量下载的情况下快速开始体验游戏。

  14. 旁观者模式支持:是否可以不参与游戏而只是旁观他人对局?是否需要登录旁观?

  15. 决赛观战和录像:是否计划在决赛期间实时展示比赛,并允许之后观看录像?

  16. 数据库存储要求:支持客服和市场团队的数据需求。

  17. 国际化需求(i18n):首发版本是否需要支持多语言?是否需要支持亚洲语言或从右至左语言(如阿拉伯语)?

  18. 客户端和网页分离界面:是否允许使用单独的浏览器窗口进行购买等“附加”内容操作?

  19. 第三方社交登录支持:是否需要社交登录?是否可以只使用社交登录?

  20. 客户端更新需求:是否需要自动更新?是否可以强制更新客户端?

  21. 服务端更新需求:服务端更新时是否可以暂停游戏?游戏世界的停止和恢复是否会影响玩家体验?

  22. 容错性需求:当硬件或操作系统故障发生时,预期的行为是什么?是否需要对关键服务器或所有服务器进行全面的容错支持?

  23. 支付系统:需要支持的支付系统及其长期需求,甚至是否会通过第三方应用商店支付。

这个清单相当冗长,但在我们逐步完成游戏架构时,你会发现所有这些点都是不可或缺的。如果遗漏了任何一项,未来某个阶段您可能不得不重新回到设计阶段,向项目干系人获取更多信息。

关于多玩家在线游戏(MOG)特定团队

在构建一个MOG游戏的过程中,除了常规的游戏开发团队(如美术、设计、3D等)外,还需要额外的四个特定团队:网络团队服务器团队数据库团队后台团队。这些团队在确保游戏的网络连接、服务端逻辑、数据管理以及客户支持工具的高效性方面扮演关键角色,并且应该在GDD讨论中得到充分代表。


网络团队

网络团队负责开发网络通信层,处理数据封包的编解码以及UDP和TCP协议等网络细节。在理想情况下,网络团队还需构建一个支持事件驱动编程的基础架构(或“中间件”),这可以有效避免游戏逻辑与网络处理的混杂,从而降低项目复杂性。即使团队规模较小,保持网络代码和游戏逻辑的隔离仍然至关重要,否则极有可能导致开发上的灾难。


服务器团队

服务器团队负责服务器端的逻辑开发,包括模拟逻辑、支付处理、赛事管理等。即便在客户端驱动的开发流程中,服务器团队仍需参与客户端与服务器的“持续转换”过程,优化服务器性能,并处理非游戏世界的实体(如支付网关)。


数据库团队

任何MOG都离不开数据库,数据库团队主要负责数据库的开发与管理,包括逻辑和物理结构的维护、数据一致性保障、API的设计、数据库性能优化等。数据库通常是MOG系统的性能瓶颈,因此数据库团队对整个系统的可扩展性有着决定性的影响。


后台团队

后台团队是最容易被忽视的团队,但在MOG中,它的角色尤为重要。该团队的职责是为支持人员(客服)提供工具,从而提升他们的工作效率。MOG的竞争优势之一可能来自于出色的客户支持。后台团队需集成第三方客户关系管理(CRM)工具,开发针对数据库的报告和管理工具,设置警报系统等,以便支持人员可以快速响应用户请求或系统问题。

后台团队的工作负荷通常在游戏公开测试(“公测”)后显著增加,因此建议在项目初期仅分配最少的资源(如一个后台团队负责人),然后在公测阶段增加团队规模并优化工作流程。


所有MOG特定团队必须被平等对待

不幸的是,在许多开发公司中,MOG特定团队(如网络、服务器、数据库和后台团队)往往被视为“二等公民”,而不是与3D美术团队同等重要。历史上,许多公司是从单机游戏开发转向MOG开发的,受“内容为王”理念影响,3D美术团队通常被视为核心。但在MOG中,“玩法为王”,并非单靠3D内容就能成功。请务必确保MOG特定团队受到尊重,因为所有团队对游戏的成功都同样重要。

正如所见,即便是客户端驱动的开发流程,网络、服务器、数据库和后台团队的工作质量仍然直接影响游戏的整体体验。因此,成功的MOG开发团队应平等对待所有编程团队,以确保各个团队的需求得到充分考虑,从而提高开发的效率和整体士气。

运行成本明细

在编写GDD时,一个关键步骤是同时计算游戏上线后的运行成本细目。提前进行这一计算非常重要,因为如果游戏的每位玩家的运行成本高于预期的玩家变现收益,将面临严重的问题。除了常见的开发成本外,MOG还会引入一些额外的开销:

  • 软件维护成本:上线后,团队的规模通常会扩大,尤其是服务器团队和后台团队,维护和更新是持续的需求。
  • 游戏服务器成本:无论是使用云服务器还是从ISP租用物理服务器,都有相应的成本。根据游戏类型和玩家规模,可以估算服务器需求数量和单玩家服务器成本。租用“传统云”和“裸金属云”服务器的详细讨论将在后续章节展开。
  • 数据库服务器/备份成本:数据存储和备份需求将随游戏规模增加,尤其是营销和变现团队需要访问数据库进行多种分析报告。数据库服务器成本和额外存储费用应纳入预算。
  • 管理员成本:服务器的种类和数量增加将需要更多的运维和数据库管理员。
  • 出站流量成本:游戏每位活跃玩家的流量需求各异,需根据初步流量估算预估成本。
  • DDoS 保护成本:成功的游戏通常需要DDoS防护,成本取决于服务提供商、入站带宽和防护级别。对入站流量进行BGP级别流量重定向的DDoS防护可以极大降低影响,但成本相对较高。
  • 支持成本:根据玩家规模,客服团队的规模可能会非常庞大。大型游戏往往需要全天候客服团队来应对玩家邮件或其他请求,尽早制定相应的支持流程和工具。

这些粗略的估算将帮助你更全面地评估游戏运营的可行性。随着时间推移,大部分服务价格可能会下降,但仍需考虑潜在的意外成本。


常见的GDD陷阱:增加多人模式

试图将原本设计为单机游戏的项目改造成多人模式往往行不通。尽管从管理和营销角度来看,增加多人模式似乎是增加卖点的“免费”途径,但在现实中,这种尝试几乎从未成功。单机游戏和MOG在开发流程上存在根本性的区别,且需要额外的团队和持续的多方集成与测试。

游戏实体与交互

在完成了GDD并列出所有需求后,下一步就是绘制一个针对你的MOG(大型多人在线游戏)专属的“实体与交互图”(Entities-and-Interactions diagram)。

尽管这样的图表可能显得“显而易见”,将其绘制并讨论可以确保每个人对“显而易见”的理解一致。特别是,记得将非游戏世界的实体纳入其中,例如收银系统、支付处理器和社交网络(尽管这些可能不会由你直接实现,但你需要与它们集成,因此它们属于你的实体与交互图的一部分)。

游戏实体:涉及的要素

在每个游戏中,都存在着各种将要处理的游戏实体。例如,在MMORPG中,可能有玩家角色(PCs)、非玩家角色(NPCs)、区域和单元;在赌场游戏中,有大厅、桌子和玩家;在社交农场游戏中,有玩家及其农场。当然,每个游戏包含的实体远不止这些,具体取决于游戏的特点,因此你作为开发者需要亲自罗列它们。如果你发现“见树不见林”,可以将图表分解为多个分层图表,每个图表仅包含可管理数量的实体。

游戏实体间的交互

游戏实体通常需要彼此交互。比如玩家存在于单元内,而单元则位于区域中;玩家角色与NPC互动,玩家在赌场桌上游戏,玩家与其他玩家的农场互动。所有这些交互对游戏架构都非常重要,必须在你的实体与交互图中详细列出。而且,要确保当前能够想到的所有交互都已列出。

产出成果:实体与交互图

实体与交互图展示了所有主要的游戏实体,以及这些实体之间的所有可能交互。这张图不仅包括与游戏玩法相关的实体,还应涵盖变现(如支付、促销)和社交互动相关的实体。如果你计划通过社交网络进行病毒营销,提前将其纳入图表非常重要,否则可能会对架构产生重大甚至毁灭性的影响。

实体与交互示例

以下描述了一些常见游戏类型中的典型实体及其交互方式。需要注意的是,以下建议只是一个大致方向,你需要根据自己游戏的具体情况进行调整和扩展。此外,示例图表仅展示了游戏部分方面的概念,实际的图表通常要详细得多。

在这些图表中,许多实体可以归为游戏世界实体(Game World entities),即实际发生游戏玩法和玩家互动的地方,或匹配实体(Matchmaking entities)。这些术语并非通用,但在本书中会作为总结观察的方式使用。

尽管到此时你可能觉得自己已经完全了解了游戏,但绘制这个图表仍然非常重要,否则团队成员之间可能会产生不同的理解,造成后续开发中的成本浪费。例如,忘记包括收银系统及其相关交互在开发早期阶段很常见,而后期纠正会带来很大麻烦。

示例图:社交农场与类似农场的游戏

社交游戏的种类繁多,难以概括,但社交农场游戏作为子类型足够简单易描述。在农场类游戏中,实体种类和交互通常较少,通常包括玩家及其农场(农场包括农场上所有的元素)。玩家间的互动在游戏中可能很少,但从社交角度来看,这些互动至关重要。

image

提示:在实体与交互示例图中,外部实体(即非游戏中的实体)将以虚线表示。

需要注意的是,在大多数情况下,随机地将玩家分配到不同服务器上并只允许同一服务器内的互动通常是不合适的,这一点在上文的《MOG的匹配和社交方面》部分有详细讨论。

赌场类多人游戏

在赌场类多人游戏中,游戏的结构看似简单:主要有桌子和坐在桌子上的玩家。然而,在某些赌场游戏(如扑克)中,选择对手被视为一种技巧,因此玩家应能够选择他们想要对战的对手。这就引入了另一个游戏实体——大厅(lobby),即玩家选择对手的地方。多人二十一点的示例“实体与交互”图可以展示这种结构(未示出社交互动部分,需要根据具体游戏自行添加)。
image-1

股票交易、体育博彩和拍卖网站

如引言中所述,股票交易所和拍卖网站与博彩非常相似,甚至很难明确区分三者(博彩通常会带来社会偏见)。在股票交易、拍卖(如“eBay”)和博彩网站中,涉及的实体是相同的。无论是股票交易中的“交易者”、体育博彩中的玩家,还是拍卖中的“竞拍者”,他们的互动多为间接的,通过对股票、事件或产品进行操作(如“下单”或“下注”)实现。

以下图表展示了一个股票交易所的“实体与交互”示例图。

image-2

大型虚拟世界游戏 (MMOTBS/MMORTS/MMORPG/MMOFPS)

尽管这些大型虚拟世界游戏(如回合制策略游戏、实时策略游戏、角色扮演游戏、第一人称射击游戏)之间存在较大差异(包括不同的延迟容忍度,这会显著影响架构和协议),但从涉及的游戏实体角度来看,它们通常相似。在这些游戏中,一般有玩家角色(PC)、非玩家角色(NPC)、单元和包含这些单元的区域,这些区域构成了虚拟世界(VW),玩家和NPC在此进行互动。玩家可以选择与特定人玩或是被系统随机分配,然而,即使没有提供玩家选择对手的功能,添加社交功能(如“邀请好友并与之游戏”)后,玩家的随机分配变得不切实际,特别是当你期望玩家跨服务器互动时。进一步讨论见“无效的匹配规则”部分。

以下图展示了一个大型多人在线角色扮演游戏(MMORPG)的“实体与交互”示例图。

image-3

团队竞技/eSports

最后,让我们讨论当前最流行的两类多人游戏:多人在线战术竞技游戏(MOBA)和团队模式的第一人称射击游戏(FPS)。

尽管MOBA和FPS游戏的玩法机制截然不同,但从“实体与交互”的角度来看,这类游戏的基本结构几乎相同,图1.5展示了这一结构。
image-4

:如果你觉得此图与赌场类游戏的图(图1.2)相似,那是因为它们的确类似。在两种情况下,玩家首先进入大厅或匹配服务器,通过选择或匹配加入游戏世界。团队竞技与赌场类游戏的主要差别在于:(a) 团队竞技中存在团队,通常在匹配过程中用到;(b) 与赌场不同,团队竞技的游戏世界通常是系统分配而非玩家自行选择。玩家通过支付系统(Cashier)获得的资金如何消费,取决于具体游戏和货币化策略,通常有不少道具可购买(“付费获胜”项目的争议广泛存在,但在本书范围内无需深入讨论)。

实体与交互图:游戏架构的起点

这个“实体与交互”图是影响游戏架构的关键之一。它为理解你需要实现的“实现实体”(如服务器、操作系统进程、数据库表、行和列等)奠定了基础,并帮助你将游戏实体映射到实现实体中。

在第三卷(特别是在服务器端架构的章节),我们将讨论游戏服务器作为实现上述游戏实体的一种方法。通常来说,游戏服务器的类型会与游戏实体类型一一对应,例如游戏世界、锦标赛、支付系统(Cashier)以及与社交平台(如Facebook)的网关等。

第1章总结

以下是第1章的主要要点:

  1. GDD是必不可少的
    GDD(游戏设计文档)是游戏开发的基础,必须在开发的早期阶段完成。

  2. GDD的撰写需要项目利益相关者和开发人员共同参与
    虽然开发人员会提供技术建议,但项目的最终决策应由利益相关者决定。

  3. GDD应使用玩家能够理解的术语
    避免使用技术实现的细节,而是专注于玩家体验和功能描述。

  4. MOG(多人在线游戏)GDD及开发具有重要特性

    • MOG通常具有无限或不确定的生命周期,这带来了许多架构和设计方面的挑战。
    • MOG开发有两种主要工作流程:客户端驱动(Client-Driven)和服务器驱动(Server-Driven)。
    • 匹配机制(Matchmaking)对于游戏体验和架构至关重要。
    • 必须从一开始就考虑营销和货币化的策略,这包括技术选择以支持这些策略。
  5. 相较于单人游戏开发,MOG开发需要额外的四个团队
    除了通常的开发团队,还需要网络团队、服务器团队、数据库团队和后端团队。这些团队必须被视为与内容团队同等重要的“核心团队”。

  6. 运行成本可能会导致MOG项目失败,因此必须从一开始就进行估算
    必须对服务器、网络流量、数据库、DDoS保护和支持成本进行初步估算,确保项目在财务上可行。

  7. 在进一步开发之前,绘制“实体与关系”图
    该图应包括所有游戏世界实体(如角色、NPC等)、货币化和社交化实体(如支付系统、社交网络接口等)以及它们之间的已知交互关系。

参考文献

  • Aldridge, David. 2011. “I Shot You First: Networking the Gameplay of HALO: REACH.”
  • Brightman, James. 2012. “GDC: Cliff Bleszinski: ‘Screw focus groups, they suck’.”
  • Donovan, Tristan. 2011. “Focus Groups, Testing, And Metrics: Developers Speak.”
  • Elbaum, Dan, and Carlin Scott. 2013. “The Perfect Couple: Domain Models & Behavior-Driven Development.”
  • Kim, Joseph. 2015. “Mobile Game Design: Iteration vs. Planning, MVP = Dangerous!”
  • Pfister, Andrew. 2015. “Coming into Focus: Understanding Video Game Market Research.”
  • Roskind, Jim. 2013. “Quick UDP Internet Connections.”

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Nov 12, 2024

第2章. 关于作弊、P2P与[非]权威服务器

在开发多人在线游戏(MOG)时,有一个极其重要的现象必须牢记:玩家作弊问题。这是在非多人游戏中几乎不存在的情况,即便是在局域网(LAN)上的多人游戏中也不太重要,但在互联网游戏中却至关重要。

玩家作弊对所有成功的MOG而言都是一个大问题。可以毫不夸张地说:

如果你的MOG没有玩家作弊的问题,那可能只是你没有深入检查,或者你的游戏尚未取得成功。

在本章中,我们将简要介绍主要的作弊方式,并集中讨论那些对游戏架构决策至关重要的作弊手段。关于“如何处理作弊”的详细讨论将在第八卷(章节涉及自动化打击与其他玩家滥用行为)中进行。

如果你的游戏足够受欢迎,玩家总能找到理由作弊

“卡吉特……机智、敏捷,擅长偷窃,因为他们的自然灵巧和无与伦比的杂技技能。”
——《上古卷轴》

你可能认为,在你的游戏中玩家没有理由作弊。比如,如果你的游戏中没有任何可以兑换金钱的内容,或许你会觉得自己不会遇到作弊问题。然而,实际情况往往相反:只要你的游戏足够受欢迎,玩家总会找到作弊的理由,无论这种作弊行为是否有直接的金钱回报。

举个真实的例子:曾经有一个免费扑克网站,玩家可以获得“玩筹码”,并用这些筹码进行游戏。这些筹码无法兑换任何实物或现实价值,仅仅用于游戏。当时,团队认为在这个网站上没有作弊的动机。但现实证明了这种假设是错误的。

情况是这样的:玩家可以将所有的“玩筹码”押上桌。虽然从扑克角度来看这样做毫无意义,但他们用筹码数量来炫耀“我是多么优秀的玩家”。一位玩家于是想到:“我可以在eBay上卖这些筹码,其他玩家愿意付钱——只为了让自己看起来更厉害!”于是,eBay上的筹码销售出现了,随之而来的便是大量的作弊行为(通过创建多个账号获取免费筹码,或通过“筹码转移”向特定账号输送筹码)。

尽管我们可能无法理解花20美元买价值全无的“玩筹码”只是为了在游戏中炫耀自己的行为,但确实有一部分玩家会这么做。作弊的可能性就像是一个概率问题,只要玩家数量足够大,这类事情就会发生。

同样的玩家心理目前被许多现代游戏(特别是社交游戏)成功地利用来进行货币化。当然,此处我们关注的不是如何利用玩家的心理弱点来盈利(那是货币化团队的任务,超出了本书的范围),而是从技术角度来预防作弊。

对游戏开发者而言,故事的教训是:即使你认为玩家完全没有理由作弊,游戏一旦流行起来,他们就会找到理由。

当你的游戏达到1,000名同时在线玩家时,作弊行为可能开始出现;而当玩家人数达到100,000时,可以完全确定作弊者的存在(如果你没有察觉到他们的存在,说明你还没有深入调查)。尽管作弊玩家的数量会根据游戏中提供的奖励类型而有所变化,但我可以肯定地说,任何拥有10万同时在线玩家的游戏几乎肯定会有作弊者存在,无论游戏的具体类型如何。


本章后续将继续深入探讨MOG作弊防范的重要性,解释在设计架构时为什么要优先考虑防作弊机制。同时,本章还将引入对等网络(P2P)与权威服务器的概念,并分析它们在防作弊和游戏安全性方面的应用与局限。

游戏与电商欺诈的根本区别

在应对游戏作弊和电子商务欺诈时,有一个显著的区别需要特别注意。在电商领域,想绕过系统的人要么是利用促销漏洞,要么就是彻头彻尾的诈骗者。然而,对于游戏玩家来说,作弊的动机远比电商复杂多样。

例如,正如上文提到的“玩筹码”的情况(见“足够受欢迎时,玩家总能找到理由作弊”部分),玩家可能只是为了展示自己的实力而作弊,或因为他们觉得游戏规则对自己不公平而作弊,甚至只是因为认为“大家都在作弊”而决定作弊来“公平竞争”。此外,有些玩家会使用“机器人”来节省时间,而不是自己在游戏中“打怪练级”。这些行为将“作弊”与“诚实玩家”之间的界限变得模糊不清。

再加上电商欺诈属于法律定义的犯罪行为,而在游戏中,使用“机器人”来逃避枯燥的游戏内容顶多导致账号被封禁(通常很容易绕过,特别是对免费游戏而言),因此我们得出一个结论:

很多在电商中绝不会作弊的人,在网络游戏中会轻易尝试作弊。

尽管在线游戏中的“诚实玩家”数量远多于“作弊者”,但不同于电商的经验(如“只有0.3%的顾客是欺诈者”),在网络游戏中我们不能依赖同样的统计经验。

第二个关键区别:游戏作弊的破坏性

由于游戏中的玩家互动远比电商更加密切——

即使少数作弊玩家也能轻易破坏整个游戏生态系统。

举个例子:如果足够多的玩家使用机器人获取不公平的优势(比如对威胁快速反应的能力),你的游戏体验会迅速恶化,甚至可能会变得完全不可玩。因此,处理作弊不仅是为了保护收入,更是为了保护游戏本身的核心体验。


应对作弊的方法

正如前文所述,作弊现象几乎是所有大型游戏的必然现象。问题不在于是否存在作弊,而在于如何应对作弊,就像下雨时我们会选择带伞——伞不能完全防雨,但在一定条件下可以提供有效的保护。处理作弊同样如此,虽然不可能实现100%的防作弊效果,但通常可以达到一个较为可接受的防护水平。

游戏玩法调整

首先需要考虑的是,游戏玩法本身是否鼓励了作弊行为。这一点可能会引起争议(毕竟技术细节本不该影响玩法设计),但必须分析玩家可能采取的作弊方式。试着站在作弊者的角度思考:“如果我被雇来作弊,我会怎么做?”有时,这种分析会揭示某些简单的作弊途径,这些作弊途径如果不加阻止,将会严重破坏游戏体验,尤其是在电竞类游戏中。

许多开发者(尤其是非游戏开发背景的人)主张“如果玩法设计本身无法杜绝作弊,那游戏就不该发布”。我并不认同这种极端的观点,因为几乎没有一个游戏是完全防作弊的。重要的是寻找适度调整玩法的方法,使其不易被利用(而且通常这样的调整会让游戏规则更加简单明了)。

架构设计

下一步是确保游戏架构没有在无意中帮助作弊者。如果架构上存在漏洞,一旦游戏流行起来就会出现大问题。例如,如果你的射击游戏依赖于“权威客户端”(Authoritative Client)架构,玩家可能会利用各种漏洞实现“瞬间传送”等作弊手段,严重影响游戏体验。这样的漏洞可能迫使你进行痛苦的架构重构(如[Harton]所述),甚至在最坏的情况下可能导致游戏失败。因此,对于大多数游戏来说,通常需要依赖“权威服务器”(Authoritative Server)架构来防止作弊(我们将在后文详细讨论这一点)。

打击机器人

最后一个防作弊措施是直接的作弊者打击。通常情况下(除非你运营的是一个类似于证券交易所的系统),这一步可以等到游戏正式上线后再进行。游戏一经发布并稳定运行,便可以开始主动寻找作弊者(有关细节见第八卷关于机器人打击与其他玩家滥用行为的章节),并在发现作弊者后迅速处理。

目前,我们的目标是确保游戏架构能够支持后续的作弊打击,而不需要在应对作弊时推翻重做整个系统。

攻击:在“主场”的巨大优势

在应对作弊者时(在经典安全领域,通常称之为“攻击者”),了解攻击情境的两大类根本区别非常重要。

主场攻击

第一类作弊或攻击场景中,攻击者试图影响你直接控制的内容。对于游戏而言,这通常指的是你的服务器。在这种情况下,从一开始你就具备内在的优势;虽然攻击总是存在的可能性,但对于这种“主场”类攻击,基本上都与实现中的漏洞有关。换句话说:

只要是你控制的内容,通常在排除实现漏洞的情况下,相对是安全的。

虽然确实可能会有许多漏洞被利用,但你仍然有机会去防范。每修复一个漏洞,攻击者就需要重新找到新的漏洞,这对于一个构建完善的系统来说并不容易。

一个典型的“主场攻击”例子是针对你的服务器的攻击,目的是获取例如“战争迷雾”下的隐秘信息,甚至试图改变游戏玩法。虽然这样的攻击可能性存在,但你一般有机会抵御这种攻击。

客场攻击

第二类攻击场景涉及攻击者完全控制你的软件(例如客户端),能够随意对其进行操作。在这种情况下,对你而言,情况要糟糕得多。事实上,不论你在客户端上做了什么,攻击者通常都可以通过逆向工程破解,从而完全控制游戏。

典型的此类攻击包括“透视墙” (wallhack)(即“穿墙查看”)或者相关的“战争迷雾破解”(maphack)——如果你的客户端存储了这些信息的话,攻击者还可以随意更改数据包时间戳(滥用延迟补偿),以及运行在客户端之上的各种机器人程序。

当然,你可以尝试对客户端进行混淆处理,但实际上任何混淆方法在足够的时间和努力下都可以被破解。在经典安全领域,第二类攻击场景下,你唯一能依赖的是所谓的“安全性依赖于隐藏性”(Security by Obscurity),这在传统的安全模型中并不被视为真正的安全性;虽然在某些情况下我们不得不依赖“隐藏性安全”,但需要明白:

“隐藏性安全”虽有时是唯一的防护手段,但不能作为可靠的安全保障。

总结

在面对作弊者时,“主场优势”(控制软件或设备)会带来巨大的差异。基本上,对于交给攻击者的任何东西,实际上你很难做到彻底保护。

情况的严重性在于,即便你给每位玩家一个硬件设备,这些设备也可能被破解(硬件攻击手段的范围可参考[Skorobogatov])。通常,只要是交到玩家手中的东西都应视为可以被破解的;我们唯一能做的是增加破解的难度,但要完全阻止破解是不可能的。

另一方面,总体上,安全并非关于“完全无法被攻破”,而是提高破解的成本。理想情况下,安全系统的目标是将破解成本提升到超过数据本身的价值,使得攻击不具备经济效益,但实际上每一层安全措施都很重要。因此,我主张这样的观点:

尽管隐藏性不能替代真正的安全防护,隐藏性仍然可以用来辅助“真正的”安全防护(尤其是在“客场攻击”中无真正的安全防护的情况下)。

公开攻击与非公开攻击

接下来要考虑的是作弊和攻击是否被公开发布的问题。虽然[Pritchard]的规则#3指出“作弊者会主动避免开发者发现他们的作弊手段”,但这种情况并非总是如此。

有时,作弊者会选择将攻击方法公开,动机各不相同。我见过一些攻击方法被公开的原因包括打击某个网站、炫耀自己是“真正的黑客”、出于“公平”的心态,或者(最常见的情况)为了售卖攻击工具或脚本赚钱。

公开攻击:影响更大,但重获“主场”优势

无论公开攻击的原因是什么,都会对游戏产生诸多影响。一方面,它会使攻击的影响显著加剧。任何对作弊感兴趣的人都可以获取攻击工具(有时甚至是免费或象征性的费用),这样会导致相当数量的玩家使用它。这将对其他玩家的游戏体验造成严重的破坏(这是你在应对作弊时最不愿看到的情况)。更糟糕的是,一旦人们知道仅需几美元就能在你的游戏中作弊,玩家的信任感会大大下降,甚至可能开始怀疑每一个正常的游戏互动。

另一方面,公开攻击也让你重新获得一定的“主场”优势。尽管对于某些攻击类型而言,可能无法完全封堵整个攻击途径,但一旦攻击被公开发布,你便有机会在半数情况下拥有“主场”优势。

在这种情况下,通常战斗进程如下:

  1. 他们获取并逆向工程你的客户端,此时,他们拥有“主场”优势。
  2. 他们公开发布攻击方法。
  3. 你下载或购买攻击工具,并逆向工程攻击代码。此时,你重新占据“主场”。
  4. 你找到使客户端具备对该攻击的抵抗力的方法。
  5. 你发布更新后的客户端。
  6. 循环往复……

总体来说,一旦你掌握了作弊方法,就可以使用作弊者通常会用来对付你的工具和技术,包括使用IDA Pro和内核级调试等。此时,这往往演变成“谁更持久”的较量。由于你对自己的游戏充满热情(而且别无选择,只能战斗到底),通常是游戏开发者比各个攻击团队更持久(虽然这并不能阻止新的攻击者出现)。

非公开攻击

非公开攻击尽管更难应对,但其引发“末日情境”(即“整个游戏因所有人作弊而崩溃”)的风险较低。当然,这并不是说你不应该关注非公开攻击;这里的意思是,通常情况下,非公开攻击的优先级应该排在公开攻击之后。

还有一种介于公开和非公开攻击之间的变体,即“在封闭论坛内发布的攻击”。这种情况通常是为了防止开发者获取该作弊工具,无法在“主场”进行防御。这样的攻击虽然令人恼火,但如果该封闭社区规模较小,影响也相对有限;而如果该社区规模较大,通常可以(而且应当)渗透其中并获取作弊工具。只要反作弊团队工作到位,这种攻击的威胁通常也不至于太严重。

攻击类型概述

在讨论作弊和攻击的典型类型以及其成功后的影响之前,有几点需要提前了解。

法律问题与封禁挑战

在讨论与作弊相关的技术问题前,首先要考虑的是你的游戏条款和条件(T&C)。我曾与一位来自知名公司的从业者讨论过,他们在封禁作弊者时遇到了重大法律问题,因为他们需要在法庭上证明这些玩家确实存在作弊行为。因此,如果条款和条件编写不当,或法律倾向于保护作弊者,再先进的技术保护措施也无法真正有效

游戏作弊

接下来,我们将讨论游戏环境中特有的作弊行为,并将其归纳为两大类:游戏特定作弊经典攻击手段

1. 游戏规则违规

如果你的游戏规则允许玩家在某种情形下“违规得分”或超出合理游戏逻辑的优势,整个游戏的平衡将被打破。例如,在足球游戏中,有玩家能不借助球员直接改变球的轨迹得分,或在格斗游戏中,玩家能在毫无可能击中的方向完成攻击,这些都是严重的规则违规。

  • 影响:游戏平衡被破坏,玩家信任度下降,甚至可能导致游戏变得无法游玩。尤其在漏洞公开时,影响会更为严重。
  • 攻击途径:是否能防御此类攻击取决于架构设计。通常情况下,如果客户端具有某种权限,那么攻击者就能直接通过客户端来实施攻击。

2. 信息泄露攻击

另一类常见的攻击是利用客户端知道比玩家应知道的更多信息。例如,“透视墙”(wallhacks)、“解除战争迷雾”(maphacks)、或“显示隐藏属性”(ESP hacks)等。

  • 影响:攻击若未被公开,影响可能较小;但一旦公开,影响将迅速上升,甚至可能破坏游戏的公平性。
  • 攻击途径:任何客户端拥有的信息都可能被提取。兴趣管理(Interest Management)是防止信息泄露的关键,它确保客户端仅获取所需的非静态信息。

3. 反应增强

对于依赖快速反应的游戏(如射击类游戏),作弊者往往希望表现出超出人类反应极限的速度。这包括**自动瞄准(aimbots)自动触发(triggerbots)**等作弊工具。

  • 影响:公开后影响高,否则相对较小。
  • 攻击途径:主要通过三种途径实现:
    • 客户端外挂。
    • 使用代理的中介式外挂,通过伪装网络延迟获得延迟优势。
    • 伪装客户端延迟以影响延迟补偿机制。

4. 断线处理滥用

若游戏逻辑在断线情况下给予玩家某种优势,该优势往往会被滥用。例如,断线可避免失败或负面结果。

  • 影响:通常较低,但对竞技性游戏影响较大。
  • 攻击途径:断开网络连接(如拔掉网线或关闭Wi-Fi路由器)。

5. 挂机刷取

**挂机刷取(Grinding Bots)**指的是通过自动化工具帮助玩家重复性地完成“刷经验”等任务,常见于MMORPG和其他任何带有等级或经验系统的游戏。

  • 影响:影响通常在低至中等之间,视是否公开而定。
  • 攻击途径:客户端外挂和代理外挂。

6. 多账户

无论游戏内容如何,玩家通常有足够动机创建多个账户。通常,为了防止游戏内的作弊,游戏条款往往会禁止多账户操作。

  • 影响:通常较小,但若不采取防范措施可能导致各种滥用行为。
  • 攻击途径:防止多账户的措施多为“模糊安全”手段,完全防止并不现实。

这些常见攻击类型总结了作弊者在多人在线游戏(MOG)中常用的手段。了解每种类型的影响和攻击途径,有助于在设计游戏架构时提前采取相应的防范措施。

经典攻击类型

除了游戏特有的作弊行为,许多常见于非游戏领域的攻击方式同样适用于游戏环境。以下我们将讨论一些最常见的针对游戏的经典攻击。

数据库攻击(DB Attacks)

如果游戏需要持久化数据(几乎所有MOG都如此),通常需要一个数据库来储存这些信息。攻击者一旦获得数据库的访问权限,他们可以窃取所有玩家的密码、修改数据甚至篡改角色属性。

  • 影响:数据库攻击的影响可能非常严重,在极端情况下可以导致整个游戏的崩溃。
  • 攻击途径:攻击者通常需先突破服务器的防御,因此服务器安全是关键。只要服务器防护得当,即使无法完全防止攻击,也可以将风险降至最低,并及时发现攻击迹象。

源代码盗窃

源代码泄露对任何行业都是一个问题,但对于游戏尤其严重。源代码一旦泄露,会让客户端的所有混淆保护无效,暴露游戏的所有弱点并引发大范围的作弊问题。

  • 影响:源代码泄露会导致游戏保护机制的崩溃,使得大量作弊行为得以实施。
  • 攻击途径:最常见的攻击方式是“鱼叉式网络钓鱼”,通常结合社交工程。为减少源代码泄露的影响,建议减少对客户端混淆的依赖,将重要的游戏逻辑迁移到服务器端。

密码钓鱼

钓鱼攻击通常针对玩家,通过设置虚假网站诱使玩家输入游戏登录信息来获取密码。

  • 影响:密码钓鱼的直接影响较小,但会造成玩家账户被盗的投诉和游戏信誉的损害。
  • 攻击途径:攻击者依赖玩家的轻信来实施攻击。最有效的防范措施是鼓励玩家使用双重验证(2FA)以增加账户的安全性。

键盘记录器/木马/后门程序

此类攻击通过在玩家设备上安装恶意软件(如键盘记录器或木马)窃取其登录信息。尽管此类攻击并非直接针对游戏本身,但会对玩家体验和游戏安全产生负面影响。

  • 影响:虽然这些攻击不会直接影响游戏生态,但可能引发玩家的不满,尤其是在玩家账户中有高价值物品的情况下。
  • 攻击途径:双重验证同样是防范这类攻击的有效手段。此外,可以尝试检测常见的后门程序来减少风险。

分布式拒绝服务攻击(DDoS)

DDoS攻击相对易于实施,通常用于发泄不满或进行勒索。攻击会导致服务器过载,影响玩家正常访问游戏。

  • 影响:DDoS攻击通常不会长期持续,但在持续时间内会严重影响玩家体验,尤其是当攻击导致服务器完全无法访问时。
  • 攻击途径:针对大规模DDoS攻击的防御需要提前准备,且可能需要采用基于BGP的流量重定向保护措施。

这些经典攻击类型揭示了游戏开发中的潜在安全威胁。通过了解这些攻击的影响和防御途径,开发团队可以在设计阶段就考虑安全措施,以减少潜在的风险并提升游戏的安全性。

总结MOG攻击类型

在表2.1中,我们对前文提到的各种攻击方式进行了总结。

image12

image13
攻击类型 影响 攻击向量 “主场”优势 当攻击已知时的“主场”优势 保护将在哪一部分讨论
你不允许封禁我! 非常高 ToC / 法律 不适用 不适用 第2卷

作弊

攻击类型 影响 攻击向量 “主场”优势 当攻击已知时的“主场”优势 保护将在哪一部分讨论
游戏规则违规 高到极高 对于授权客户端:客户端 作弊者 来回争论 本章32
对于授权服务器:服务器 我们 不适用 第2卷,第3卷
信息泄露 中等到极高 对于确定性锁步:客户端 作弊者 来回争论 本章32
对于授权服务器,若兴趣管理已正确实施:无 不适用 不适用 第三章
反应增强 低到中等 瞄准机器人(客户端) 作弊者 来回争论 第3卷
延迟补偿(客户端) 作弊者 来回争论 第三章
滥用断开逻辑 连接 作弊者 依然作弊者 本章
刷怪机器人 低到中等 客户端 作弊者 来回争论 第3卷
以上任意攻击 见上 代理 若未加密:作弊者。若已加密:我们 与未公开的攻击相同 第2卷

多账户

攻击类型 影响 攻击向量 “主场”优势 当攻击已知时的“主场”优势 保护将在哪一部分讨论
多账户 非常低 客户端 作弊者 依然作弊者 第2卷

经典攻击

攻击类型 影响 攻击向量 “主场”优势 当攻击已知时的“主场”优势 保护将在哪一部分讨论
数据库攻击 高到极高 服务器 我们 不适用 第2卷,第3卷
盗取源代码 非常高 开发环境 我们 不适用 第九章(缓解)
密码钓鱼 玩家 作弊者(完全的) 依然作弊者 第3卷
键盘记录器/木马/在另一玩家设备上的后门 玩家设备 作弊者 依然作弊者 第3卷(仅讨论登录保护)
DDoS 服务器 不适用 第3卷

关键架构依赖

从表中可以看到,只有两种攻击(游戏规则违规和信息泄露)在很大程度上依赖于架构。这些攻击的防御效果与使用的架构密切相关。具体而言:

  • 游戏规则违规:在基于客户端的权威架构中容易出现这一问题。游戏规则应尽可能由服务器进行验证,而非完全依赖客户端。
  • 信息泄露:使用**确定性同步锁步(Deterministic Lockstep)**算法的游戏容易受到信息泄露攻击。这类攻击要求在客户端上隐藏尽量多的信息,服务器仅发送玩家所需的状态更新,避免不必要的数据传输。

通过选用权威服务器架构和实施兴趣管理等措施,游戏开发者可以在早期开发阶段设计更强的防作弊能力,降低这些攻击带来的风险。

权威客户端:对抗作弊几乎无望(仅适用于仅限主机的游戏)

有时会在不同论坛中看到这样的问题:“为什么还要麻烦用服务器?我们可以使用完全无单点故障且可无限扩展的P2P(点对点)架构。”此外,还有人认为客户端-服务器架构不够扩展性强,而MOG(多人在线游戏)的未来在于P2P系统。为便于讨论,我将以[Skibinsky]为例。

在P2P架构中,每个客户端负责自己的计算,并将结果发送给其他客户端以确定游戏世界的状态。例如,每位玩家模拟她的角色并将结果传送给其他客户端以同步其游戏世界状态。这种架构在没有作弊者的情况下是可行的,但一旦有玩家作弊,他便能修改客户端的行为,导致其他客户端也会应用作弊后的结果,从而获得诸如瞬间传送等不公平的优势,这类作弊行为会严重破坏游戏平衡。

严格来说,并非所有给客户端权限的架构都是P2P系统。在实践中,真正的P2P系统较少见,更常见的是选举某一客户端作为临时服务器的架构。另一种变体是所谓的非权威服务器,其仅作为客户端之间的数据中继。然而,出于反作弊的目的,任何形式的权威客户端架构在面对作弊时效果相似,因此我们暂时将它们统称为“权威客户端”。

对于“游戏规则违规”类型的攻击,权威客户端面临的处境非常不利,我们只能依赖“安全性通过混淆”的手段。然而,这一问题已经广泛地被认可,因此提出了几种解决方案;不幸的是,截至2017年,这些方案在实践中均未被证明为切实可行。

代码签名:在恶意环境中无效(主机游戏除外)

应对权威客户端作弊的首要手段是代码签名。表面上,这似乎是一个好主意:如果我们的应用程序被签名,那么我们可以确信它会按我们编写的方式运行。

然而,代码签名的问题在于,一旦最终用户自己想要破解代码签名,它实际上就变成了“安全性通过混淆”。这是因为用户拥有对设备的完全控制权,他们可以篡改根证书来生成自己的私钥/公钥对并伪造签名。由于攻击者可以更改用于验证签名的根证书,因此代码签名变得毫无意义。

另外,在这种恶意环境下,关键在于“谁在执行验证”。如果执行签名验证的是用户控制的代码,那么即使签名完全无效,用户也可以使验证通过。

[Skibinsky]也承认代码签名的局限性,指出:“这并不能提供100%的安全性。”对此我进一步认为,“当用户自己想绕过代码签名时,它提供的安全性提升是极其有限的;除非是在主机(控制台)环境下。”

控制台的保护能力

在此方面,控制台确实提供了更高水平的保护,直到被破解为止。控制台制造商会极力防止用户篡改其根证书和签名验证代码。因此在控制台环境中,这种保护能有效地防止作弊,直到控制台被破解为止(比如PS3曾保持了五年未被破解)。目前,控制台厂商正在努力防止已破解的控制台联网,从而实现对玩家环境的控制。这场攻防战在实践中可能对MOG开发有帮助,前提是游戏仅限于主机平台。

成功的控制台游戏示例

一些成功的游戏依赖代码签名防止在P2P类架构上的作弊。例如,《光环:致远星》就是一个主要依靠控制台安全机制防止作弊的成功案例,据我所知,控制台的安全性确实对防止作弊起到了相当大的作用。

然而,将游戏限制于仅在控制台上运行对MOG来说通常不是一种可行选择,因为一旦游戏同时在控制台和PC上运行,PC端将成为防御链中的薄弱环节,容易遭到攻击。

理论性保护措施

除了代码签名和主机之外,文献中还提出了其他反作弊方法(尤其是在[Skibinsky]中),但这些方法多为理论性,没有成功的游戏依赖它们来处理作弊问题。以下是这些主要理论性技术的简要概述及其局限性讨论。

交叉检查 —— 难以察觉的攻击、游戏控制权、延迟

解决权威客户端系统“游戏规则违规”漏洞的首个理论方法是通过其他节点交叉检查潜在作弊者的计算。虽然这个想法看似可行,但存在几个关键问题:

  • 不可检测的攻击:某些攻击(如重新排序或丢弃接收到的包)可通过作弊者巧妙处理而不被发现,因为互联网环境下包延迟和丢失非常普遍且无法验证。
  • 跨节点作弊:交叉检查节点本身易受攻击。例如,比特币系统有一个50%攻击问题,即如果攻击者控制网络50%以上节点,便能主导全网。而对于MOG,由于性能限制,跨节点检查往往无法全面进行。
  • 延迟问题:交叉检查必然增加延迟,或需异步进行,产生另一个问题:当检测到作弊时,如何处理作弊结果?是否回滚?回滚会影响未受影响的玩家,导致他们的不满。

总的来说,交叉检查在MOG中难以有效应用,且目前尚无成功案例。

共识机制(即多数投票)——延迟更严重

共识机制是交叉检查的进一步发展,例如比特币或Stellar共识协议(SCP)。虽然SCP将共识时间缩短至2-5秒,但这对大多数游戏而言仍然过于缓慢,难以适应游戏的实时性需求。

可信节点 —— 我们信任谁?

另一种理论方法是基于“可信节点”系统,仅允许可信节点参与计算。然而,对于MOG,这种系统有一个根本问题:我们如何定义并维持可信节点?

  • 节点身份难以识别:无法将节点与易变的信息(如IP或邮箱)绑定。即便生成密钥并存储在设备上,密钥易被删除,无法有效约束用户更改身份。
  • 节点群体攻击风险:若可信节点系统被攻击,攻击者可以创建多个账户形成“信任”网络,从而主导计算。作弊者甚至可能“接管”整个游戏世界。

这些问题使得“可信节点”方法在大型MOG中难以实践,风险极高。

同态加密 —— 实际上行不通

理论上,还有同态加密方案,其允许节点在完全不透明的情况下进行计算而不知晓数据内容。尽管有理论和实践支持,但同态加密的性能开销极高,难以应用于要求极高的游戏环境。在可预见的未来,这种方法在MOG中难以实现。

总结

这些理论保护措施在概念上很有吸引力,但在MOG中的实际应用往往不可行,尤其是面对延迟、性能和管理难题。目前,大多数成功的MOG仍然依赖权威服务器架构来应对作弊问题。

Authoritative Client MOG总结

关于权威客户端架构的讨论总结如下:虽然权威客户端架构(包括纯P2P和由客户端充当服务器的变体)在某些互相信任的社区中可以运行得相对良好,但从目前的情况来看,除了仅限控制台的游戏以外,很难为权威客户端MOG提供有效的反作弊保护

这种对权威客户端的排斥以及向权威服务器架构的转移在游戏开发行业中愈发明显。例如,[Sweeney]和[Fiedler在关于游戏网络的必知要点]都表达了对权威服务器架构的支持。

尽管理论上存在可能依赖权威客户端的游戏类型(即没有明确的证据表明这种游戏一定不存在),但在决定依赖权威客户端时需再三慎重,尤其是在非控制台平台上。务必参考“如果你够受欢迎,他们总会找到作弊理由”章节,以便了解风险。


确定性锁步(Deterministic Lockstep):无游戏规则违规,但信息泄漏严重

另一种在多玩家游戏中相当流行的设计理念是确保所有客户端保持完全一致的状态。这通过以下方式实现:

  1. 所有客户端代码相同且具确定性。
  2. 初始状态相同。
  3. 为所有客户端提供相同的输入。

详见[Terrano和Bettner]和[Fiedler在Deterministic Lockstep中的研究]。

尽管确定性锁步本质上不排除权威服务器的使用(理论上可运行与客户端相同的权威服务器作为判断胜负的标准),但在实践中,带权威服务器的确定性锁步很少被应用,主要有两个原因:

  1. 在许多游戏中,通过几个客户端之间的多数投票机制来判定游戏事件的结果已足够。
  2. 实现跨平台的100%确定性行为极为困难,这对非PC平台(如控制台)尤为不利。

从反作弊的角度来看,确定性锁步(无论是否带权威服务器)确实能有效防止游戏规则篡改类的作弊,尤其是在权威服务器存在或可以排除50%以上的玩家作弊可能性时。

然而,确定性锁步并非完全无缺点。问题在于,确定性锁步要求客户端保留整个游戏世界的状态。这意味着,作弊者可以轻松从客户端提取整个游戏状态,从而实现“透视墙”或“去除迷雾”的作弊手段(即wallhacks和maphacks)。

此外,确定性锁步还存在一些纯技术问题,包括不同平台上实现100%确定性行为的难度,以及不得不等待最慢玩家的延迟。这些问题促使Glenn Fiedler建议“仅将确定性锁步用于2至4人的游戏”。

确定性锁步的应用场景

尽管有这些限制,确定性锁步在即时战略(RTS)游戏中仍有其受欢迎的应用场景,尤其是在独立游戏开发者中。然而,除非特定游戏完全无法采用其他方法进行流量优化,否则我更倾向于“经典”权威服务器,即通过服务器同步状态给客户端,而非确定性锁步加权威服务器。对于RTS,唯一反对权威服务器的理由是流量问题,但该问题通常可以通过优化解决(见第3章对RTS流量优化的讨论)。

相对于确定性锁步,权威服务器的优点:

  1. 消除最慢玩家的影响:权威服务器架构中,最慢的玩家不会拖慢所有人,尤其对于超过5-10名玩家的游戏世界至关重要。
  2. 支持重新连接:权威服务器允许失去连接的玩家在有限时间内重新加入游戏,而确定性锁步则可能出现较大问题。
  3. 更低的可观测延迟:使用UDP进行最终一致性状态同步在权威服务器中可减少延迟,这在RTS中尤其有帮助。
  4. 跨平台兼容性:权威服务器可以让不同平台上的玩家在同一游戏世界中互动,而确定性锁步要求跨平台的确定性,这在实践中几乎无法实现。
  5. 兴趣管理(Interest Management)减少作弊风险:权威服务器可以只向客户端发送需要显示的信息,这样客户端只保留屏幕上显示所需的信息而非整个游戏世界,从而极大减少maphacks或wallhacks的潜在风险。

综上所述,在因带宽限制而无法避免的情况下才推荐采用确定性锁步,且在流量优化做得很好的情况下,这种情况不太可能出现。

权威服务器架构:接近防作弊的最佳选择

鉴于权威客户端和确定性锁步架构的种种问题,近年来“权威服务器”架构越来越受欢迎。实际上,对于绝大多数游戏来说,权威服务器是目前唯一真正可行的多玩家在线游戏(MOG)架构。

在一个典型的虚拟世界游戏的权威服务器架构中,客户端通常拥有3D引擎,但这个引擎仅用于渲染,而不是进行决策。所有玩家的输入(例如按键和点击)被发送到服务器,由服务器处理玩家和其他对象的移动、碰撞、命中等决策。服务器的游戏世界状态是唯一的权威版本,并会同步给客户端进行渲染。

对于快速反应的游戏来说,每次按键都要往返服务器会产生不可接受的延迟。在这种情况下,客户端通常会实现“客户端预测”,即通过本地处理玩家输入来暂时更新自己的游戏世界副本。然而,当客户端与服务器的视图不一致时,服务器的状态始终是“正确”的版本。因此,客户端预测产生的偏差是暂时的,不会对其他玩家造成影响。

从防作弊的角度来看,权威服务器架构是最佳选择。服务器可以对游戏规则进行强制执行,即便有客户端预测,服务器和客户端之间的冲突也可以通过服务器的权威性来解决。

权威服务器架构:可行但不完美的扩展性

尽管权威服务器在防作弊方面极具优势,但许多人认为它不具备良好的扩展性,特别是一些批评者声称其客户端-服务器架构流量需求可能呈现“O(P²)”的增长,即游戏中每个玩家的状态变化需要通知给其他所有玩家。这种理论会带来严重的扩展问题。

然而,实际情况并非如此。现实中玩家的互动范围往往限于他们的“邻近区域”,与世界总人口无关。这种方式在实践中更接近O(P)的增长,而非O(P²)。

这一技术在MOG开发中被称为“兴趣管理”(Interest Management),将在第3章中详细讨论。借助兴趣管理,客户端仅处理与自身相关的实体信息,极大降低了数据流量,使得游戏服务器在处理大量玩家时依然可以实现可行的扩展性。

实际计算示例

以下是一个实际的示例来说明上述扩展性理论:

假设某游戏中每个玩家最多只与C=100名其他玩家直接互动。假设每个玩家交互的带宽需求约为15字节/秒,且游戏的平均玩家每月收入为0.05美元。在这种情况下:

  1. 当有10,000名同时在线玩家时,流量总需求为大约0.13 Gbit/s,按每月$40的成本来计算。
  2. 随着玩家增加至100万,假设每名玩家的流量需求增加5倍,整体流量需求将增长500倍至65 Gbit/s,每月费用为$13,000,而此时的收入约为$50,000/月,足以覆盖流量成本。

因此,只要扩展良好,权威服务器的收入和开销可以保持比例性增长,从而实现经济上的可持续性。

结论

权威服务器架构是当今防作弊效果最佳的MOG架构。尽管其扩展性存在挑战,通过合理的实现(如兴趣管理),其在大规模玩家环境下依然是可行的,且能够在游戏收入增长的同时平衡流量开销。这使得权威服务器成为大多数现代MOG开发的首选架构。

权威服务器架构:接近防作弊的最佳选择

鉴于权威客户端和确定性锁步架构的种种问题,近年来“权威服务器”架构越来越受欢迎。实际上,对于绝大多数游戏来说,权威服务器是目前唯一真正可行的多玩家在线游戏(MOG)架构。

在一个典型的虚拟世界游戏的权威服务器架构中,客户端通常拥有3D引擎,但这个引擎仅用于渲染,而不是进行决策。所有玩家的输入(例如按键和点击)被发送到服务器,由服务器处理玩家和其他对象的移动、碰撞、命中等决策。服务器的游戏世界状态是唯一的权威版本,并会同步给客户端进行渲染。

对于快速反应的游戏来说,每次按键都要往返服务器会产生不可接受的延迟。在这种情况下,客户端通常会实现“客户端预测”,即通过本地处理玩家输入来暂时更新自己的游戏世界副本。然而,当客户端与服务器的视图不一致时,服务器的状态始终是“正确”的版本。因此,客户端预测产生的偏差是暂时的,不会对其他玩家造成影响。

从防作弊的角度来看,权威服务器架构是最佳选择。服务器可以对游戏规则进行强制执行,即便有客户端预测,服务器和客户端之间的冲突也可以通过服务器的权威性来解决。

权威服务器架构:可行但不完美的扩展性

尽管权威服务器在防作弊方面极具优势,但许多人认为它不具备良好的扩展性,特别是一些批评者声称其客户端-服务器架构流量需求可能呈现“O(P²)”的增长,即游戏中每个玩家的状态变化需要通知给其他所有玩家。这种理论会带来严重的扩展问题。

然而,实际情况并非如此。现实中玩家的互动范围往往限于他们的“邻近区域”,与世界总人口无关。这种方式在实践中更接近O(P)的增长,而非O(P²)。

这一技术在MOG开发中被称为“兴趣管理”(Interest Management),将在第3章中详细讨论。借助兴趣管理,客户端仅处理与自身相关的实体信息,极大降低了数据流量,使得游戏服务器在处理大量玩家时依然可以实现可行的扩展性。

实际计算示例

以下是一个实际的示例来说明上述扩展性理论:

假设某游戏中每个玩家最多只与C=100名其他玩家直接互动。假设每个玩家交互的带宽需求约为15字节/秒,且游戏的平均玩家每月收入为0.05美元。在这种情况下:

  1. 当有10,000名同时在线玩家时,流量总需求为大约0.13 Gbit/s,按每月$40的成本来计算。
  2. 随着玩家增加至100万,假设每名玩家的流量需求增加5倍,整体流量需求将增长500倍至65 Gbit/s,每月费用为$13,000,而此时的收入约为$50,000/月,足以覆盖流量成本。

因此,只要扩展良好,权威服务器的收入和开销可以保持比例性增长,从而实现经济上的可持续性。

结论

权威服务器架构是当今防作弊效果最佳的MOG架构。尽管其扩展性存在挑战,通过合理的实现(如兴趣管理),其在大规模玩家环境下依然是可行的,且能够在游戏收入增长的同时平衡流量开销。这使得权威服务器成为大多数现代MOG开发的首选架构。

权威服务器:虽不完美,但唯一可行

总结我们在应对作弊的探讨中对于三种不同架构的分析:

模式 可扩展性 抵抗游戏规则违规能力 抵抗信息泄露能力
授权客户端 高达“非常好” 取决于情况
确定性锁步 非常好
授权服务器 可接受

通过以上讨论可知,权威客户端架构在面对游戏规则违规时的抗性较差,对于绝大多数MOG游戏来说这是致命缺陷。而确定性锁步尽管在小规模RTS游戏中可用,但信息泄漏及玩家数量限制使其无法适应大部分游戏需求。

**因此,对大多数MOG游戏来说,唯一可行的解决方案是权威服务器。**虽然有少数例外(如仅限控制台的权威客户端架构游戏或部分使用确定性锁步的RTS游戏),但作为一条普遍准则,大多数情况下权威服务器是最佳选择。

积极应对:希望仍在

尽管上文中谈及了众多与作弊相关的问题,可能让你觉得作弊者终将得势,但事实并非如此。虽然在抵御作弊的过程中,我们必须投入大量精力,实现零作弊几乎不可能(尤其是对于超过一千名玩家的游戏),但我们依然可以通过控制作弊者,防止他们对游戏生态系统造成过多影响。

一个在反作弊斗争中大有帮助的观察是:

“你不需要跑得比熊快,只要跑得比旁边的人快就行了。”
——Jim Butcher

换到反作弊的语境,我们可以这样说:

“为了保护游戏免遭作弊者侵害,你不需要100%防作弊,只需要做得比旁边的游戏好一点。”
——No Bugs Hare

作弊的经济规律——特别是商业化的作弊行为——表明,如果存在两个目标,其中一个既吸引人又防护严密,另一个稍逊一筹却保护薄弱,那么商业作弊者通常会选择后者。毕竟,这只是商业行为,无关私仇。

每一点都很重要:多层保护

这一规律带来的一个关键启示是:

在反作弊前线,每一个保护层都至关重要。

尽管我们无法打造一个牢不可破的反作弊系统,且反作弊措施越多,被攻击的可能性越小,因此采用多层保护,从不同角度去抓捕作弊者是很有意义的(前提是这些保护层不会对普通玩家造成明显的副作用)。

此外,另一个有趣的观察是,多层防御有助于消耗攻击者的信心。如果攻击者攻破了一个防护层,而你才开始修复漏洞,那他很可能会继续尝试,并且可能再次成功。然而,如果他面前有五层或更多防护,且在攻破一两层后没有获得任何正面反馈,他可能会丧失动力和信心。

换句话说:

不要让作弊者产生成就感,不要向他提供反馈。

现实案例:多层防护的效果

现实中有一个典型案例:某次,一名作弊者几乎破解了一个大型游戏的通信协议(并在相关论坛中分享,寻求最后的帮助)。尽管游戏采用了接近完美的权威服务器体系(即无法通过非法手段从外部操作游戏),仍然存在玩家编写刷经验机器人(Grinding Bots)的可能。

这个攻击方案当时颇为巧妙(攻击者替换了客户端中的根证书,然后对自己的客户端发动了中间人攻击,进而截获协议信息)。

对此,开发者布置了五个独立的保护层(每个都足以阻止该攻击),并同时上线部署。结果是,该攻击者从此销声匿迹,且在之后的数年中几乎没有类似的协议破解尝试。这一事件或许只是一个例子,但确实展示了多层防御的有效性,尤其是同时部署多层防护的威力。

第二章总结:确实,权威服务器是最佳选择

第二章总结要点:

  1. 作弊是MOG游戏的重大问题
    在多人在线游戏(MOG)中,作弊问题几乎无可避免。

  2. 即使没有理由,玩家也会作弊
    即便游戏本身没有明显的作弊动机,只要玩家基数足够大,总会有人找到作弊的方法。

  3. 游戏规则违规是一大潜在问题
    游戏规则的篡改或违反会严重影响游戏的公平性和体验。

  4. P2P和其他基于“权威客户端”的架构防作弊能力极差
    基于P2P的架构由于对客户端的权威性依赖,使其极易遭受游戏玩法作弊的攻击。

  5. 确定性同步存在信息泄露的固有缺陷
    确定性同步方式对信息泄露的防护效果极差,并且存在其他诸多限制,使其不适合大多数MOG游戏。

  6. 权威服务器可以实现可扩展性
    尽管有不同的观点,权威服务器在经过合理的设计和优化后,确实可以支持大规模游戏的需求。

  7. 权衡优缺点后,权威服务器是最佳方案
    鉴于上述各种考虑,权威服务器是目前最为稳妥的选择;事实上,在大多数情况下,权威服务器是唯一可行的方案。理论上存在一些例外,但这些情况极为罕见。

因此:

在本书余下的部分,我们将专注讨论权威服务器架构。

两个可能的例外情况:

  1. 仅限于主机(console-only)游戏的多人模式
    在这种情况下,可以让其中一个主机充当“选举产生的权威服务器”。尽管我个人并不完全支持这种方式,但这种架构在一些真实的游戏中确实成功实现了。

  2. 只包含少数玩家的RTS游戏
    某些RTS(即时策略)游戏可能需要“确定性同步”来处理成千上万的同步操作。然而,在排除“权威服务器”架构之前,建议先参考第三章中讨论的压缩技术,并针对具体游戏进行实验。如果在权威服务器模式下成功优化流量,权威服务器将为游戏带来众多好处。


参考文献

  • Bright, Peter. 2011. “Spearphishing + zero-day: RSA hack not ‘extremely sophisticated’.” Ars Technica.
  • Fiedler, Glenn. 2014. “Deterministic Lockstep.” Gaffer on Games.
  • Fiedler, Glenn. 2010. “What every programmer needs to know about game networking.” Gaffer on Games.
  • Harton, Eugen. 2016. “Once A Cheater Always A Cheater: Gotta Catch ‘Em All.” GDC Vault.
  • Kim, Joyce. 2015. “Stellar Consensus Protocol: Proof and Code.” Stellar Blog.
  • Parkin, Simon. 2016. “Catching up with the guy who stole Half-Life 2’s source code, 10 years later.” Ars Technica.
  • Pritchard, Matthew. 2000. “How to Hurt the Hackers: The Scoop on Internet Cheating and How You Can Combat It.” Gamasutra.
  • Skibinsky, Max. 2005. “The Quest for Holy Scale.” In Massively Multiplayer Game Development 2, 339-373.
  • Skorobogatov, Sergey. 2011. “Hardware Security of Semiconductor Chips: Progress and Lessons.” Cambridge.
  • Sweeney, Tim. 2009. “Unreal Networking Architecture.” UDK.
  • Terrano, Mark, and Paul Bettner. 2001. “1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond.” Gamasutra.

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Nov 13, 2024

第三章:通信

描述:

现在,在所有的前言之后,我们终于准备好讨论MOG(大型多人在线游戏)的核心内容——通信了。然而,请不要期待我会深入讨论炙手可热的“UDP与TCP”话题——我们还没有到那一步(这些问题以及如何缓解各自问题的方式将在第四卷的《网络编程》章节中详细讨论)。目前,我们需要理解MOG操作背后的原理;将这些原理映射到具体技术是一个相关但不同的主题,我们将在第四卷中继续讨论。


客户端-服务器与服务器-客户端通信

正如我们在第二章中详细讨论的那样,贯穿本书,我们将讨论权威服务器。虽然当服务器应用运行在某个玩家的计算机(如主机等)上时,很多相同的逻辑也适用,但我们仍将其称为“客户端”和“服务器”之间的通信。

在这个过程中,产生了几种不同类型的通信(例如,见[Aldridge]):

  1. 玩家输入,从客户端到服务器。这些输入包括玩家点击和控制器输入——简单明了。需要注意的是:正如在第二章中讨论的那样,使用权威服务器时,我们不能在客户端处理输入并做出与游戏相关的决策——而应该将这些输入(基本上是鼠标和/或键盘点击)发送到服务器。

  2. 状态同步,从服务器到客户端。状态同步是将来自权威服务器的游戏世界当前状态同步或复制到客户端。我们将通过网络同步的状态称为“可发布状态”(Publishable State),并且如下面所述,它通常会与服务器状态和客户端状态不同。需要注意的是,在延迟和流量效率方面,最终在客户端上实现这个同步的“可发布状态”副本并不简单(我们将在“非可靠通信的参考基础”部分进一步讨论),但现在我们只需要假设这通常是可行的。

  3. 瞬时事件,从服务器到客户端。这些事件包括诸如“在这个位置有子弹击中”的情况,通常是通过广播或多播消息实现的。与“可发布状态”同步的主要区别在于,瞬时事件仅在“现在”有意义,并且如果丢失了就没有必要重新发送,这使得它们成为理想的非可靠消息传递(或非可靠RPC调用)场景。

  4. 转发输入,从服务器到客户端。这些输入本质上是“提示”,允许客户端预测更好地考虑其他玩家的动作,可能是其他客户端的输入,或(更常见)是服务器基于其他玩家输入生成的派生数据。大致的思路是,“如果某个动作已经由玩家输入指示,但在可发布状态中尚未显示出来,那么为了让客户端的渲染表现更加精确,利用其他玩家的输入来改进客户端预测可能是有益的。”

另一方面,这些额外信息容易成为信息泄露作弊的来源,因此,作为一个经验法则,我反对使用转发输入(尽管我仍承认,在许多情况下,让玩家满意通常会超越反作弊的考虑)。关于转发输入的更多讨论将在下面的“转发输入”部分中进行。

RTT、输入延迟及其缓解方法

目前,我们将集中讨论两个最显而易见的问题——玩家输入和状态同步。换句话说,我们将讨论客户端如何将玩家输入发送到服务器,并接收返回的游戏世界状态更新。


数据流图,第一版

需要注意的是,如果你的游戏是快节奏的(比如MMOFPS或较小范围的第一人称MMORPG),那么关于第一版数据流图的描述方法将无法使游戏表现得不“迟钝”(它可以工作,但在互联网上运行时不会感觉响应迅速)。不过,请继续阅读,因为我们需要这个作为进一步讨论的起点,最终帮助我们找到适合快节奏游戏的架构。

首先,让我们来看一个非常简单的数据流图,适用于典型的较慢节奏MOG:

fig_3-1

尽管这个图表在视觉上看起来很简单,但仍有一些细节需要提到:

  1. 右侧的具体延迟数字仅供示例使用。实际情况可能会有所不同,甚至可能差异很大。不过,这些数字确实代表了一个现实的(甚至是“相当典型的”)案例。

  2. 客户端看起来似乎很“愚蠢”。是的,它确实如此;图中大部分的游戏逻辑都在服务器端执行。

  3. 另一方面,在大多数游戏中,有些玩家行为只会导致客户端的变化,而不会影响服务器端的游戏世界。这些行为可以并且应该仅在客户端处理。大多数情况下,这些行为是UI方面的内容(比如“显示和隐藏HUD”以及通常的“仰视”操作),但对于某些游戏来说,这些逻辑可能变得相当复杂。哦,对了,别忘了诸如购买之类的内容。如果你把这些保留在游戏中(有关更多讨论,请参阅第二卷),这将需要进行大量的客户端交互(如“选择物品”和“输入支付信息”等)。这些对话框通常也是完全由客户端处理,直到玩家决定进行购买为止。

  4. 最后,尤其是对于快节奏的游戏,图中所示数据流存在一个大问题,问题的名字就是“延迟”。显然,对于这个简单的数据流,玩家按下按钮到看到结果(即所谓的“输入延迟”)之间的延迟,至少会是客户端与服务器之间的所谓“往返时间”(RTT)(在图3.1中显示为100ms;关于典型RTT的更多讨论,请参见下文的RTT部分)。然而,在实践中,RTT之外还会增加相当多的延迟,对于图3.1中的例子,100ms的RTT最终导致了227ms的整体延迟。如果这个延迟超过了典型的人类期望,游戏就会开始感觉“滞后”,最终可能会变得“完全无法玩”。让我们仔细看看这些至关重要的输入延迟问题。

输入延迟:MOG开发者的噩梦

**注意:**如果你的游戏节奏较慢或中等(包括像扑克这样的赌场类游戏),你可以直接跳过到《游戏世界状态与减少流量》部分。

如前所述,对于MOG(大型多人在线游戏),最关键的问题之一与两个时间之间的关系有关:输入延迟和相关的用户期望。让我们详细讨论这两个方面。


输入延迟:用户期望

首先,让我们看看用户期望,当然,用户期望本身是非常主观的。不过,关于这一点,我们可以做出一些普遍性的观察。作为起点,让我们引用[Wikipedia, Input Lag]中的内容:

“测试发现,总体‘输入延迟’(从控制器输入到显示响应)的时间大约在200毫秒时,会对用户造成干扰。”

我们可以把这个200毫秒的魔法数字作为分析的起点,实际上,这个数字也得到了其他来源的验证。在[Aldridge]中,提到Halo: Reach的游戏体验中,100毫秒到300毫秒之间的延迟对游戏至关重要——不过不清楚这里是指的网络延迟,还是总体输入延迟。[West]指出,10/60秒(167毫秒)感觉“相当响应”,但是他说200毫秒“等待开枪太久了”。200毫秒这个大致的数字也与其他关于人类反应时间的独立观察一致。例如,[Lipps, Galecki和Ashton-Miller]基于对在北京奥运会上竞争的短跑选手的反应时间研究,指出“在99.9%的置信水平下,无论男女选手的反应时间都无法达到100毫秒,但他们可以在109毫秒和121毫秒之间作出反应”;虽然这不完全是指感知延迟,但它给了我们一个相似的量级,并且(考虑到这些数字是世界顶尖短跑选手的水平)似乎确认了200毫秒这个数字可能不会差得太远。在完全不同的场景下,[Human Benchmark]通过测量超过4000万个互联网用户的反应时间点击,发现平均反应时间为279毫秒(中位数为268毫秒),这个数据也非常接近200毫秒的魔法数字。

另一方面,我们应该注意,对于竞争性目的(如MMOFPS或MOBA游戏),每一毫秒都至关重要,但只要我们的MOG(a)保持在100-200毫秒以下,并且(b)不同玩家之间的延迟一致,我们应该就能应付得来。

从另一个角度来看,严格来说,这个数字并不是全球统一的200毫秒,估算数字会因人而异,容忍度在不同游戏类型之间也会有所不同。不过,即便是对于最为时效要求严格的游戏,低于100-150毫秒的延迟通常被认为是“足够好”,而任何实时交互中,300毫秒的延迟将会被很多玩家感知到(尽管是否会感觉“糟糕”是另一个问题)。为了更加具体,我们可以考虑两款示例游戏:一款是《OurRPG》,它的输入延迟容忍度为300毫秒(假设这款游戏没有战斗,更多的是社交互动,使得游戏对延迟的容忍度较高),另一款是《OurFPS》,它的输入延迟容忍度为150毫秒。

此外,我们还需要注意,这150-300毫秒的输入延迟容忍度只是人类生理或心理等因素的一个事实,我们实际上无法做太多改变。


输入延迟:MOG还能剩下多少时间

我们面临的第一个问题是,有许多因素会侵占原本150-300毫秒的延迟分配(即使MOG代码还没有开始工作)。这些因素包括游戏控制器引入的延迟、渲染引擎引入的延迟(这取决于许多因素,包括渲染队列的大小),以及显示延迟(主要由LCD显示器引起)。

典型的鼠标延迟在3-6毫秒之间[Pasini],对于游戏鼠标来说更低。为了讨论的方便,我们假设任何游戏控制器引起的延迟为5毫秒。

典型的渲染引擎延迟介于50毫秒到150毫秒之间。50毫秒(即在60帧每秒的情况下3帧)是相当难以实现的,且并不常见,但仍有可能。对于60fps的游戏,更常见的数字是67毫秒(即4帧),100-133毫秒的延迟也并不少见(见[Leadbetter])。

典型的显示延迟(与像素响应时间不同,后者较低且常常被大肆宣传,但通常不是影响游戏的因素),截至2017年,从10毫秒开始,中位数大约在40毫秒左右,最远可以达到100毫秒(见[DisplayLag.com])。

这意味着,在原始的150-300毫秒的延迟中,我们需要扣除60毫秒到255毫秒的时间。因此,对于许多玩家来说,游戏已经在MOG和网络延迟开始影响之前,就已经开始出现延迟了。

为了更具体地说明,我们需要注意的是,我们无法真正控制鼠标延迟和显示延迟;我们也无法现实地说“嘿,各位,为了玩我们的游戏,你必须拥有最好的显示器”,因此,我们至少应该以中位数玩家使用中位数显示器为目标。因此,我们假设在150-300毫秒的延迟中,需要扣除大约45毫秒(5毫秒左右来自游戏控制器或鼠标,40毫秒来自中位数显示器)。

接下来,让我们看看渲染引擎引入的延迟。这里,我们可以有所作为。此外,我主张——

对于MOG来说,渲染延迟比单机游戏更为重要。

要点是,对于单机游戏来说,如果我们能将整体输入延迟控制在100毫秒以下,玩家的体验并不会有太大提升,因为这个数字低于人类感知事物的典型能力。然而,对于MOG来说,由于RTT的影响,我们已经更接近150-300毫秒的魔法数字,减少延迟的效果将更加明显。换句话说,单机游戏中100毫秒和50毫秒之间的差异,不会像MOG中200毫秒和150毫秒之间的差异那样明显。

为了进行我们的示例计算,假设我们已经设法制作了一个渲染引擎,具备合理的50毫秒延迟(加上上面提到的45毫秒),这意味着我们已经消耗了150-300毫秒初始分配中的95毫秒。即使其他一切都非常快速,我们仍然需要确保《OurFPS》的RTT小于55毫秒,《OurRPG》的RTT小于205毫秒。尽管这些数字看起来不错,但它们仍然没有讲述完整的真相(我们很快会看到为什么)。

计算数据包丢失和抖动

乍一看,似乎我们之前的计算显示,即使是一些快节奏的FPS游戏,我们也可以使用图3.1中的简单示意图来表示。

但实际上,我们不能这样做,至少现在不能:还有一个与网络相关的重要复杂因素,我们需要考虑。为了将数据从客户端传输到服务器,我们需要通过互联网发送,而通过互联网发送数据有其特有的延迟问题。


互联网是基于数据包的,数据包可能会丢失

首先,我们来简要讨论一下互联网的机制(这里只讨论当前相关的部分)。我不会在这里深入细节或展开讨论(我们将在《网络编程》第四卷的章节中详细讨论这些内容);现在暂时把以下内容当作公理——

当数据通过互联网传输时,数据总是以数据包的形式进行传输,每个数据包可能会发生延迟或丢失。

这与所使用的协议无关(无论我们是使用TCP、UDP还是更特殊的协议,如GRE)。虽然TCP会在丢失数据包时进行内部处理(需要时重新传输数据包),但这些丢失不可避免地会导致延迟;换句话说,TCP实际上是在用数据包的延迟来交换数据包的丢失。

此外,我们再做一个假设——

每个数据包都有一定的开销。

对于TCP来说,开销是每个数据包40+字节;对于UDP来说,通常是每个数据包28字节(这里不计算以太网头部的开销,它会增加额外的开销)。对于我们的当前讨论,精确的数字并不重要;我们只需要注意,对于小更新来说,这些开销是相当可观的。

现在让我们看看这些观察如何影响我们的游戏数据流。


削减开销

我们需要解决的第一个因素是,对于快节奏的游戏来说,响应每个输入并发送一次世界更新是不现实的。这(至少部分原因)与我们之前提到的每个数据包的开销有关。如果我们需要发送一个更新,表示某个玩家开始移动(更新的大小可能只有8字节),但需要加上28-40字节的开销(这样就会使开销达到350-500%),这显然不可取。

这也是为什么游戏模拟通常在一个典型的“游戏循环”内运行,但将渲染替换为发送更新的原因之一:

while(true) {

    TIMESTAMP begin = current_time();

    process_input();

    update();  

    // update()通常包括所有世界模拟,
    // 包括NPC的移动等

    post_updates_to_clients();  

    // 在这里,我们实际上是将当前“网络刻度”内
    // 所有的世界更新合并到尽可能少的数据包中,
    // 从而有效地削减开销

    TIMESTAMP elapsed = current_time()-begin;

    if(elapsed<NETWORK_TICK)

        sleep(NETWORK_TICK-elapsed);

}

通过这种方式,我们一次处理“游戏世界”的所有更新,并以一个“网络刻度”为单位。每个游戏的“网络刻度”大小不同,但50毫秒每刻度(即20个网络刻度/秒)并不是一个罕见的数字(尽管不同的情况可能会有所不同)。

需要注意的是,在服务器端(与《第二卷》中通常的客户端游戏循环不同),时间步长的处理选择是有限的,通常与上面代码的变种相似(等待直到下一个“刻度”开始的剩余时间)。此外,服务器端的处理通常是以事件驱动的风格编写,代码如下:

void GameWorld::process_event(const Event& event) {

    // 这里‘event’包含了所有尚未处理的客户端输入

    process_input(event);

    update();

    post_updates_to_clients();

    post_timer_event_to_myself(SLEEP_UNTIL, event.started+NETWORK_TICK);

}

(有关事件驱动编程的详细讨论,请参见《第二卷》中的《(Re)Actors》章节。)

为了便于理解,假设我们的“网络刻度”是1/20秒(即50毫秒),那么我们将在每个玩家的输入到达“网络刻度”开始时,会增加0到50毫秒的额外延迟;我们将其表示为(0:50)毫秒。此时,OurFPS的剩余延迟分配变为(5:55)毫秒,而OurRPG则为(155:205)毫秒。


客户端和服务器端的缓冲处理

到目前为止,情况看起来还不错,但我们仍然没有处理数据包丢失和间歇性延迟(也就是“抖动”)。

如果我们坚持使用图3.1中显示的简单模式,那么从服务器到客户端的每个丢失(或大幅延迟)的数据包都会导致玩家屏幕上的明显(且令人不悦的)效果:一切暂时停顿,然后当下一个数据包到达时,画面会“跳跃”到正确的位置。

为了解决这个问题,我们需要在客户端引入一个缓冲区(我们称之为“接收时缓冲”),通过简单地将“正常”接收的数据包延迟一定的预定义时间dt来实现。这样做的目的是:如果某个数据包被延迟(延迟时间小于dt),我们将能够“假装”它没有被延迟(只需为该数据包减少dt)。这种情况可以通过以下图示进行说明:

fig_3-2

这段内容详细介绍了如何处理网络延迟、丢包以及抖动(jitter)等问题,特别是在多人游戏中,如何确保客户端和服务器之间的时间同步。最初提出的问题是如何应对丢包和延迟,避免对游戏体验产生不良影响。

丢包与抖动的影响
当数据包在客户端和服务器之间传输时,由于网络的不稳定,可能会发生丢包或延迟波动(即“抖动”)。例如,某些数据包可能会丢失,导致玩家屏幕上的场景突然停止,然后再跳跃到正确的位置。为了缓解这种问题,可以在客户端使用“接收缓冲区”(Buffer-on-Receipt),延迟接收的正常数据包一段时间,以便处理丢包或抖动造成的影响。通过这种方式,可以利用相邻的数据包进行插值,从而“估算”丢失的包,保持游戏体验流畅。

时间同步问题
随着游戏通信协议的复杂性增加,服务器与客户端的时间同步成为了必须考虑的一个重要问题。作者提到了一些常见的同步方法:

  1. Sync-Once:在游戏开始时进行一次时间同步,之后假设客户端和服务器的时钟运行在相同速度上。但这种方式可能会出现时间偏差,尤其是在网络环境不稳定的情况下。

  2. Sync-Once with Subsequent Adjustments:在初步同步后,根据收到的时间戳信息进行动态调整,解决时间漂移问题。通过这种方法,可以在游戏过程中调整时间同步,减少由于网络延迟引起的不同步。

  3. NTP-like Protocol:类似于网络时间协议(NTP),这种方式允许服务器充当时间源,帮助客户端同步时钟。

  4. Phase-Locked Loop (PLL):这种算法创建一个与接收信号同步的时钟(频率和相位同步)。它在很多电子设备中得到广泛应用,但在软件中较少使用。PLL可以有效地帮助客户端预测下一个数据包的到达时间,尤其是在有网络抖动和延迟的情况下。

总的来说,时间同步在网络游戏中的实现并不复杂,但要确保其稳定性和可靠性,尤其是在网络表现不稳定时,可能需要在公共测试阶段进行大量实验。

这段内容主要讨论了 TCP 和 UDP 在网络游戏中的应用,特别是它们在丢包和延迟方面的表现差异。同时,提到了减少玩家“输入延迟”的一些技巧,如客户端动画。

TCP 与 UDP 的对比

1. TCP 在无丢包时的表现:
当使用 TCP 时,如果没有丢包,TCP 的延迟表现可以和 UDP 相似。如果启用了 TCP_NODELAY(禁用 Nagle 算法),且网络中没有丢包,TCP 会像 UDP 一样快速传输数据,延迟几乎可以忽略不计。然而,TCP 的流式特性和拥塞控制机制意味着它在网络表现不稳定时会出现不同的延迟行为。

2. TCP 在丢包时的表现:
当出现丢包时,TCP 的表现大大逊色于 UDP。主要有以下两点问题:

  • 头部阻塞(Head-of-Line Blocking): 因为 TCP 是一个流式协议,丢失的数据包后面的所有数据包都必须等待丢失的数据包重新传输并被接收,即使这些数据包已经准备好。这会造成延迟,影响游戏的实时性。
  • 重传延迟: 丢失的数据包通常需要大约 200 毫秒的时间才会被重传。如果连续丢失多个数据包(如丢失两个数据包,延迟会增长到 600 毫秒,丢失三个数据包,延迟会达到 1.5 秒),则会严重影响游戏的流畅度。对于一个每秒发送 20 个数据包的游戏来说,5% 的丢包率意味着每五分钟就可能发生连续丢失三包的情况。

3. TCP 在服务器间通信中的应用:
尽管 TCP 在 Server-to-Client 的通信中由于丢包和延迟问题不受欢迎,但它在 Server-to-Server 的通信中却是一个理想选择,尤其是对于数据中心内的通信,因为在同一个数据中心内丢包的概率非常低。

4. 使用 TCP 时的缓冲:
由于 TCP 的重传延迟,通常需要在客户端和服务器之间引入较大的接收缓冲(通常需要 500 毫秒或更高的延迟),这比 UDP 所需要的 1-2 个网络时钟周期要大得多。

输入延迟:减少玩家的延迟感知

1. 输入延迟的缓解技巧:
为了减少玩家感知到的输入延迟(Input Lag),可以通过客户端动画来缓解。例如,在玩家按下按钮后,客户端立即开始播放一个动画(例如开枪动画),而在同一时刻,客户端会将请求发送给服务器。这样,即使由于网络延迟需要等待服务器响应,玩家仍然会看到动画正在播放,从而减少了他们对延迟的感知。

例如,在射击游戏中,如果添加一个 50 毫秒的开枪动画,并且在按钮按下时立即发送射击请求,那么从玩家的角度来看,输入延迟会推迟 50 毫秒开始,从而减少了玩家感知的延迟。

2. 减少输入延迟的效果:
这种技巧虽然有限,但它能有效地改善玩家体验,尤其在快速反应游戏中,每一毫秒都非常重要。例如,如果通过动画使得 50 毫秒的延迟“回收”了,那么游戏的整体延迟就会稍微减少,尤其是对于 FPS 游戏(如 OurFPS)和 RPG 游戏(如 OurRPG)。在减少了输入延迟后,FPS 的延迟可以达到(5:-45)毫秒,而 RPG 游戏的 RTT 可以得到更好的优化,延迟可以控制在(105:155)毫秒之间。

总结

  • TCP 和 UDP:TCP 在无丢包时表现与 UDP 类似,但在有丢包时,TCP 的重传机制会导致较大的延迟,特别是对于实时性要求高的游戏,UDP 更为适合。
  • 输入延迟:通过客户端动画或其他方式,可以减轻玩家感知的输入延迟,尤其是在快节奏的游戏中,这些技巧能有效提升体验。

数据流图,第二版:快节奏游戏的具体问题

概述

在快节奏的游戏中(如 MMOFPS),即使采用了第二版数据流图(Take 2)所描述的方案,依然可能会感觉“滞后”。不过,值得继续阅读,因为我们将在第三版(Take 3)中讨论剩余的问题,并提出相应的解决方案,而第三版又是基于第二版的基础之上。

先前讨论的要素

前面讨论的内容(如游戏循环、客户端接收缓冲、服务器端接收缓冲和“先行”动画)引出了图 3.3:

(此处为图 3.3)

图 3.3 描述了一个游戏架构,其中许多延迟和缓冲问题已经被考虑。但即使如此,如下所述,依然存在一些显著的问题。

问题 1:OurFPS 已经延迟

如上所述,在考虑网络延迟(RTT)之前,OurFPS(每秒帧数)就已经存在延迟。换句话说,尽管我们在客户端和服务器之间进行了一些优化和缓冲操作,游戏的帧率已经因为网络缓冲和延迟而受到影响。因此,OurFPS 的帧率会比理想情况下低,显著影响了游戏的响应性。

问题 2:视觉帧率和网络时钟同步

图 3.3 中的另一个问题是,视觉帧率 实际上等于“网络时钟”(network tick)。而“网络时钟”通常会比每秒 60 帧的帧率低得多(在我们的例子中,它是每秒 20 次)。这意味着我们的渲染帧率将是 20fps,而不是 60fps,这对于玩家来说是非常糟糕的视觉体验。20fps 的帧率远低于理想的游戏体验标准,会使游戏显得卡顿和不流畅。
fig3_3_1_stranica

问题 3:OurRPG 的 RTT 处理

尽管如此,对于 OurRPG,我们依然有 105:155 毫秒的时间储备来应对 RTT(往返延迟)。这个时间储备是否能够应对现实世界中的 RTT 延迟呢?我们需要进一步探讨。

小结

  • OurFPS 在面对网络延迟和缓冲时已经滞后,导致帧率低于预期,无法提供平滑的游戏体验。
  • 网络时钟 的低频率导致了视觉帧率的下降,降低了游戏的流畅性,影响了玩家的体验。
  • 对于 OurRPG,虽然我们有一些时间储备来应对 RTT,但我们仍然需要面对实际网络环境中的挑战和延迟。

通过这些考虑,我们可以意识到,尽管采取了第二版的方案,仍然会有不少问题需要解决。接下来的讨论将进一步深入探讨这些问题,提出如何在快节奏游戏中优化网络延迟和帧率的策略。

RTT(往返时间)

让我们来仔细看一下那只常常出现在夜晚、在我们最糟糕的噩梦中出现的 RTT 怪兽,它吞噬了我们所有藏在枕头下的饼干,也就是我们所有的 输入延迟 预算。

1. RTT 基本概念

首先需要注意的是,RTT(Round-Trip Time,往返时间) 主要受到玩家 ISP 的影响(特别是“最后一公里”连接)。即便在非常理想的情况下,RTT 也有一个“下限”,无法无限降低。大致来说,RTT 的范围可以参考 表 3.1 中给出的数字(假设是非常好的 ISP 等情况)。但需要注意的是,网络延迟变差是很容易的,而显著改善 RTT 的情况则通常不现实。

表 3.1:理想情况下,不同玩家位置的 RTT 范围(以毫秒为单位)
(此处插入表格)
image20

玩家连接 RTT(不包括“最后一公里”)
在同一城市“环网”或“互联网交换”作为服务器(参见Wikipedia.InternetExchanges,但请记住,离开同一城市会增加RTT) ~10–20毫秒
跨城市,城市之间相距距离D 理论上至少需要 2×D/Cfib2×D/Cfib​(其中 CfibCfib​ 为光纤中的光速,大约是 Cvacuum/1.5Cvacuum​/1.5,即约 2×1082×108 米/秒)。实际上,根据国家不同,增加大约20–50毫秒。
跨美国(纽约到旧金山) 理论最低延迟(受限于 CfibCfib​)约为42毫秒;实际上至少为80毫秒。
跨大西洋(纽约到伦敦) 理论最低延迟(受限于 CfibCfib​)约为56毫秒(Grigorik);实际上至少为80毫秒。
跨太平洋(洛杉矶到东京) 理论最低延迟(受限于 CfibCfib​)约为90毫秒;实际上至少为120毫秒。
非常长的连接(纽约到悉尼) 理论最低延迟(受限于 CfibCfib​)约为160毫秒(Grigorik);实际上至少为200毫秒。

2. 最后一公里延迟

此外,还需要考虑玩家的“最后一公里”延迟,如 表 3.2 所示:

表 3.2:玩家的最后一公里延迟
(此处插入表格)
image21

额外的“最后一公里”RTT…  
…由玩家的“最后一公里”增加:有线连接 (Grigorik 报告约 25 毫秒,我自己的游戏体验大约是 15–20 毫秒)
…由玩家的“最后一公里”增加:DSL连接 (Grigorik 报告约 45 毫秒,我自己的游戏体验大约是 20–30 毫秒)
…由玩家的Wi-Fi增加 约 2–5 毫秒(假设直接连接到Wi-Fi路由器,无中继器/无线接入点)
…由玩家的并行下载增加 从 0 到 1000 毫秒不等,甚至更多

有两点需要特别注意:

  • 如果你的服务器位于一个良好的 ISP 中(这是理想的情况),那么其延迟基本上接近核心网络。这意味着在大多数“合理良好”的情况下,玩家的真实延迟将是 表 3.1 中的某个值,加上 表 3.2 中的一个或多个值(因为服务器端的“最后一公里”延迟几乎可以忽略不计)。但仍然需要进行验证(例如,通过从另一个服务器进行 ping 测试)。
  • 上述的数字是针对位于数据中心的硬件服务器来说的。虚拟化的云服务器通常有更高的 RTT(详见第七卷),并且如果你的云服务商突然开始占用更多的 CPU 或带宽时,延迟可能会飙升到几百毫秒。顺便提一下,一些云服务提供商提供不含虚拟化的云,通常被称为“裸金属云”,这种云服务能有效消除这些额外的延迟。有关云与传统租赁服务器的比较,请参考第七卷中关于“准备上线”的章节。

3. LAN RTT 与互联网 RTT

基于局域网(LAN)的游戏(典型的有线 LAN RTT 通常低于 1 毫秒,即使是 Wi-Fi 通常也低于 5 毫秒)与大型在线游戏(MOGs)在延迟上是无法相提并论的。如果你的 MOG 需要类似于 LAN 的 RTT 才能玩得下去——抱歉,这几乎是不可能的(但请参考下文关于客户端预测的内容,这可能能够缓解许多情况下的问题,尽管会带来更复杂的实现)。

4. CDN 和地理分布的服务器

有些人可能会想,“嘿,为了提高延迟,不如使用 CDN,问题就解决了。”不幸的是,事情并没有那么简单。传统上用于提高网站加载速度的 CDN 并不能有效降低游戏的延迟。原因是传统的 CDN 主要通过将数据缓存到更接近终端用户的地方来提高延迟(即数据请求可以直接从缓存中获取,而不需要重新请求服务器)。然而,对于游戏(特别是快节奏的游戏)来说,数据仍然需要从客户端传输到服务器,这就消除了 CDN 提供的任何延迟改善。

然而,类似 CDN 的延迟改进可以通过分布式游戏服务器来实现,这样每个玩家都有一个相对接近的服务器可用。更多关于如何通过分布式服务器来减少延迟的讨论将在 “返回输入延迟” 部分进一步展开。

5. RTT 与玩家

关于 RTT,我们还需要提醒玩家们以下三点,这些信息你的客服人员肯定需要告诉玩家:

  1. 更好的带宽并不意味着更好的延迟:你需要告诉玩家,像“为什么我换成了更好的 100Mbit/s 连接后,你们的服务器反而开始卡顿了?你们是不是在惩罚带宽好的玩家?”这种问题的答案。带宽和延迟并不完全挂钩。

  2. 客户端显示的延迟数字是没有意义的:我们可以轻松地在客户端上显示一个“当前延迟”的数字,但不同游戏之间报告的数字没有任何可比性。这实际上是一个 “Big Fat Argument™”,最好避免显示延迟数字(尽管发布这些数字仍然是 GDD(游戏设计文档)中的一项决策)。

  3. “昨天好很多”:你确定家里没有人正在下载大文件吗?

小结

RTT 是影响快节奏游戏体验的一个重要因素。它与玩家的网络环境、服务器的物理位置、以及最后一公里的网络连接有着密切关系。在优化 RTT 时,传统的 CDN 方案并不适用于游戏延迟的优化,分布式服务器和合理的云部署才能有效改善延迟。对于玩家来说,了解网络带宽和延迟的关系也非常重要,特别是在面对游戏中的延迟问题时,合理解释这些现象有助于提高玩家的理解和体验。

从表 3.1 和表 3.2 可以看出,在最佳情况下(服务器和客户端都连接到同一城市内的交换环,所有设备都顶级配置,最后一公里使用非过载的有线连接,没有并行下载影响等),RTT 大约为 35-45 毫秒。这意味着——

对于类似 FPS 的游戏来说,即使所有玩家都在同一个城市的交换节点上,且没有特殊优化,延迟仍然不够理想。

在同一个(大型)国家内,最佳可能的 RTT 增加到大约 80-100 毫秒。这意味着通过图 3.3 中的简单图示,我们仍然可以处理类似 OurRPG 的游戏;但前提是将玩家限制在一个国家内(例如创建“美国服务器”,事实上这会是一个装满服务器的数据中心)。实际上,国家特定的数据中心非常常见,且相对容易实现和维护,但它们仍然限制了玩家的灵活性(并且可能对“玩家临界质量”产生不利影响,详见第 1 章)。虽然在某些情况下你可能别无选择,但理解这种决策的所有影响仍然很重要。

单一大陆的数据中心(RTT 范围在 100-120 毫秒之间)类似于国家特定的数据中心,也经常用于快节奏游戏。即使使用客户端预测等特殊技术,对于快节奏的游戏如 MMOFPS,也可能需要按大陆或按国家设置数据中心。另一方面,对于单一大陆数据中心,甚至对于 OurRPG,我们已经开始接近“延迟明显”的门槛,因此即使是非 FPS 游戏,我们也可能需要一些额外的优化(如下所述)。

从纯地理位置来看,对于美国,时间敏感型游戏的最佳数据中心位置应在阿肯色州附近。更现实的考虑(结合实际网络电缆布局)是,如果想用一个数据中心覆盖整个美国,达拉斯或芝加哥会是不错的选择;这样的选择可以限制最大 RTT,并在一定程度上使游戏更公平。

如果你想要一个全球性的游戏,那么最大可能的 RTT 会超过 220 毫秒,即使对于 OurRPG,没有特殊的优化也会显得迟缓。更糟的是,不同玩家之间的延迟会有显著差异。尽管图 3.3 所示的简单数据流仍然适用于相对慢节奏的全球 RPG(比如模拟人生),但对于全球性的 MMOFPS 和 MOBA 游戏来说通常不太可能实现。

所有这些观察引导我们进入流程图的下一次迭代,这将引入客户端侧的实质性(且非简单的)处理。

数据流图,第三版:客户端预测和插值

到目前为止,图3.3带来了两个烦人的问题:一个是过多的延迟,另一个是客户端帧率低。后一个问题的出现是因为如果完全按照图3.3实现游戏,客户端帧率会被限制在网络更新频率上,而典型的网络更新频率是每秒20次,这会导致客户端渲染帧率为20fps,视觉效果会非常不理想。

为了解决这些问题,我们需要在客户端增加一些处理逻辑。在此不深入探讨,只简要介绍涉及的算法;进一步的讨论可以参考Gabriel Gambetta的优秀系列文章[Fast-Paced Multiplayer],虽然他的视角略有不同,但讨论的技术完全相同。

客户端插值

首先可以做的是与客户端接收缓冲区(在第二版和图3.3中引入的缓冲区)相关。为了确保我们不以“网络更新”速率渲染(而是以60fps渲染),我们可以(且应当)在接收缓冲区内对“当前网络更新”和“前一网络更新”之间的数据进行插值。

例如,如果我们的“网络更新”频率是典型的每秒20次,通过客户端插值,我们可以让客户端以60fps渲染——其中三帧中的两帧是通过插值生成的。

这种方法可以使运动视觉上更平滑,并恢复60fps渲染速率,无需增加流量。客户端插值相对简单,不会带来复杂的问题。不过,虽然它能使运动更平滑,却无法改善输入延迟。

客户端外推,又称死算

接下来可以做的是超越插值,进行外推。换句话说,如果我们将速度数据添加到游戏世界状态中,当下一个更新包尚未到达(可能由于延迟),我们可以外推对象的移动情况,假设没有意外发生。

最简单的外推可以通过计算 (x_1 = x_0 + v_0) 实现,但也可以更复杂,例如考虑加速度。这通常被称为“死算”(Dead Reckoning),但这个术语在几个相似但略有不同的情况下使用,因此我会继续使用“外推”来描述此处的逻辑。

外推的好处在于我们可以对缓冲区更乐观,而不必考虑最坏情况,比如丢失三包的情况(在这种罕见情况下通过外推处理)。在实际中,这通常意味着我们可以减少因数据包丢失而导致的“卡顿”,这对我们非常激进的单一“网络更新”缓冲策略尤为重要。

撞墙与服务器校正

我能听到砖墙在呻吟的声音。

——来自《The Furchester Hotel》的超级石头

另一方面,不像插值,外推会引发显著的复杂问题。第一类复杂性是内部不一致性。比如,当我们在外推NPC的移动时,假如它撞到了墙上会怎样?如果这种情况在外推过程中有可能发生并产生明显的负面影响,我们需要在外推时考虑并检测到NPC的碰撞,甚至可能需要启动适当的动画。这个问题的处理深度取决于具体情况(也参见客户端预测部分),但可能是必要的。

第二类外推相关的问题与所谓的“服务器校正”有关。当服务器的更新到达时,我们在客户端的外推位置可能与服务器的不一致。

即使我们在客户端完全复制了100%的服务器逻辑,这种差异也会发生,因为我们在外推时缺乏足够的信息。例如,如果另一名玩家按下了“跳跃”,并且该操作到达了服务器,但在客户端我们至少还需等待100毫秒左右才能知晓,因此我们的完美外推结果会与服务器不一致。

当这种冲突更新到达客户端时,我们需要将客户端的游戏世界视角与服务器视角“校正”。因为服务器是权威并“永远正确”,这不是真正意义上的校正,而是“让客户端世界看起来像服务器所指示的那样”。

如果我们简单地在接收到权威服务器消息后修正坐标,那么对象的位置会在“当前”和“新”位置之间产生非常不愉快的视觉“跳动”。

为避免这种情况,一种常见的做法(而不是直接将对象跳到接收的位置)是启动一个基于新坐标的预测,同时继续运行基于当前显示坐标的“当前”预测,并在“混合期”内显示“混合”位置(混合位置从“当前”预测逐渐移动到“新”预测)。例如:

[
\text{displayed_position}(dt) = \text{current_predicted_position}(dt) \times (1 - \alpha(dt)) + \text{new_predicted_position}(dt) \times \alpha(dt)
]

其中 (\alpha(t) = dt / \text{BLENDING_PERIOD}),且 (0 \leq dt < \text{BLENDING_PERIOD})。

其他的校正方法包括样条曲线或贝塞尔曲线,以及各种混合变体(包括所谓的投影速度混合,[Murphy] 认为在预测快速移动对象时这种方法问题最少)。要获得这些校正技术的完整概述(以及与基于物理的预测相关的其他问题),请参阅 [Murphy]。

客户端预测

通过客户端插值和客户端外推,我们可以减少卡顿现象(并将渲染帧率提升到60fps,<长舒一口气 />)。然而,即使有了这些改进,游戏仍然可能会显得“迟缓”(我们的计算表明,即便是OurRPG,在服务器超出单一国家时也可能会感到“延迟”)。

为了进一步改善效果,通常会使用“客户端预测”。其核心思想是,当玩家按下按钮时立即开始移动玩家的角色,这样可以完全消除角色移动的“迟缓”感。确实,在客户端我们可以知道角色正在做什么,并可以展示出来;如果操作足够谨慎,预测结果将会几乎与服务器的权威计算一致,至少在角色被突然改变轨迹的物体击中(或者突然出现的物体)之前的300毫秒内会一致。

在实现方面,客户端预测可以通过在客户端复制部分服务器游戏逻辑来实现。需要注意的是,为了客户端预测的目的,我们并不需要客户端和服务器之间100%的跨平台确定性(更多讨论见第二卷关于(Re)Actors的章节),客户端和服务器“几乎一致”的行为已经足够(任何小的差异,如不同的舍入处理,将在随后进行的“校正”过程中解决——同样是在最多约300毫秒内)。

如果你打算深入了解客户端预测,建议阅读[Gambetta, Fast-Paced Multiplayer (Part II): Client-Side Prediction and Server Reconciliation]和[Fiedler, State Synchronization],两者都提供了比我这里可以容纳的更深入的分析。

客户端预测:处理差异

不利的一面是,客户端预测可能会导致“服务器视角的游戏世界”和“客户端视角的游戏世界”之间出现严重差异(即服务器状态和客户端状态之间的差异)。虽然这个效果与我们在“客户端外推”中讨论的“校正问题”非常相似,但对于客户端预测,这种差异通常比单纯的客户端外推更为严重。客户端预测导致差异增加的原因有两个:

  1. 客户端预测和从服务器获取权威数据之间的时间间隔显著更大。
  2. 在客户端预测中,其他玩家正在添加他们的输入,这些输入会影响服务器,但通常不会被客户端预测考虑。

然而,这种影响可以通过服务器将其他玩家的输入转发给所有客户端来减轻,以便客户端可以更好地进行预测(更多细节见下文的“转发输入”部分,其中也包括信息泄露作弊风险的讨论)。

在实现客户端预测时需要记住的一些事项:

  • 通常,你需要维护一个“未确认的”输入动作列表(尚未由服务器确认的输入),并在接收到每次权威更新后重新应用它们;否则,会产生不愉快的视觉效果(详细讨论见[Gambetta, Fast-Paced Multiplayer (Part II): Client-Side Prediction and Server Reconciliation])。
  • 正如[Fiedler, State Synchronization]指出的,在某些情况下,你可能需要向发布状态中添加更多信息(如速度)以支持客户端预测。
  • 处理角色撞墙问题(类似于客户端外推,但由于时间间隔更大,效果更严重)通常是必要的。
  • 进一步复杂化的是,玩家间的交互可能不如我们所希望的那样可预测,因此在客户端上做不可逆的决定(如“对手已死,因为我击中了他,他的健康值降到零以下”)通常不是最好的主意(如果他使用了治疗药水,你还不知道,因为服务器的消息还在传输中)。在这种情况下,最好在客户端上暂时保持对手存活几毫秒,只有当服务器确实声明对手死亡时才开始显示倒地动画;否则,视觉效果可能会显得很奇怪,比如他开始倒地,但又突然复活(因为客户端预测和服务器权威版本略有不同)。

分布式权威

关于客户端预测,还有一个需要提及的危险陷阱。在实现客户端预测的过程中,可能会产生一种将少量权威分布到客户端的倾向。

这种带有客户端和服务器分布式权威的系统应尽量避免。

这种系统的问题在于,一旦你将一部分权威转移到客户端,就很容易逐步增加更多的权威。最终,你可能会得到一个可以运行的游戏,但由于大量决策在客户端进行,游戏在中期甚至短期内就可能会遭遇大量的作弊问题。

关于权威客户端的危险我不想再次展开(在第2章已有详细讨论)。仅需注意,曾有多个大型游戏因这种分布式权威方法遭遇严重的作弊问题,其中一个公司目前已经进入第二年重写代码,试图将所有客户端决策移回服务器(本应一开始就在服务器上)。

第三版图示

通过添加这三个客户端改进(客户端插值、客户端外推和客户端预测),我们得到了图3.4:
fig3_4_1_stranica

图示说明

正如我们所见,从服务器来的权威数据的处理速度仍然相对较慢。但对于由玩家发起的动作(正是这些动作带来了“延迟”感,因为其他玩家的动作时机对玩家而言并不那么明显),主要的响应性改善来自客户端预测以及预测结果的渲染。客户端预测完全在客户端侧处理,从接收到控制器输入,到客户端预测,再直接进入渲染,完全不涉及服务器。这种方法(如预期的那样)显著地减少了延迟,使得玩家角色的T7大约为T0+112毫秒,比非玩家角色的T7提前了约200毫秒。

当然,这仅仅是“预测”(在某种意义上是“假的”),但如果它在99.99%的情况下有99%的准确率(而在少数情况下差异并不大),玩家会觉得这样的体验是可以接受的,而这种体验正是玩家所期望的。

通过图3.4(尤其是客户端预测),我们在玩家角色发起的动作上得到了显著的改善;在112毫秒的延迟下,游戏不会显得太迟缓。但我们能说这些数字已经完全解决了问题吗?某种程度上是的,但也不完全。

剩余问题

剩下的问题是,从服务器做出的任何更新到玩家看到它的这一刻之间,仍然存在显著(且不可避免)的延迟。正如[Gambetta, Fast-Paced Multiplayer (Part IV): Headshot! (也称延迟补偿)]恰如其分地描述的,这类似于生活在一个光速变慢的世界中,因此我们看到发生的事情会有一个可察觉的延迟。

因此,对于一些真正快节奏的游戏(如射击类游戏),这种延迟会导致令人不快的情况。比如,作为玩家,我可能用激光武器做出了完美的射击,但由于当我的射击信号到达服务器时已晚了约100毫秒(目标在这段时间内已经移动),结果却错失了目标。这时我们就进入了备受争议的领域,即“延迟补偿”。

延迟补偿——作弊风险与玩家满意度的权衡

我们讨论的三个客户端改进(客户端插值、客户端外推和客户端预测)在快节奏游戏中非常常见,尽管实现复杂,但效果显著。而接下来要讨论的延迟补偿,则更具争议性。

延迟补偿至少有两种不同的形式(可能不止两种)。其中一种称为“服务器回溯”。

服务器回溯

服务器回溯的经典形式旨在解决上述问题,即玩家进行精准射击时,因“按下按钮”消息延迟到达服务器,导致目标已经移动而错失射击。

服务器回溯的核心思想是,服务器(作为权威)可以重建任何时刻的游戏世界。因此,当服务器收到你的数据包,表明你在T时刻进行射击(并包含瞄准角度等数据)时,服务器可以“回溯”到你射击时的T时刻,根据该信息判断是否命中。这样可以补偿延迟,从而使玩家的“干净射击”更精准。

另一方面,“服务器回溯”可能会导致已经躲到墙后的其他玩家被击中。在某种程度上,我们是在用一种延迟问题(“明显命中却未命中”)替代另一种延迟问题(“在安全的地方被击中”)。不过,由于后一种问题往往对玩家的烦扰较小,因此服务器回溯通常是为了让玩家更满意的合理做法。

服务器端减去客户端 RTT

在[Aldridge]中,描述了另一种类型的延迟补偿。在《光环:致远星》中,他们有一个“装甲锁定”功能,问题是无论如何优化,当“装甲锁定”开始保护玩家的时间总是不如预期(在公测中引起了大量玩家投诉)。

为了解决这个问题,他们找到的最佳解决方案是在服务器端更改游戏机制:服务器端的延迟不再是固定的3帧,而是“3*帧时间 - 此玩家的RTT”,使得玩家在客户端的动画播放3帧后,正好开始体验到保护效果。这种方法虽然在服务器空间上不一致,但让玩家满意,而这才是最重要的。

延迟补偿天然容易被作弊利用

延迟补偿表面上对所有人来说都是一种纯粹的胜利。然而,有一个非常重要的考虑因素在你开始实施任何形式的延迟补偿之前必须清楚——

所有类型的延迟补偿都天然容易被用于作弊。

例如,在服务器回溯中,如果我可以发送时间戳到服务器,而服务器隐式地信任它,我可以作弊,使得射击稍晚发生,但伪装成稍早的射击。在减去客户端RTT的情况下,作弊难度更大,但仍然可以通过模拟更高的RTT来作弊,当作弊者真正需要时就会获得几乎即时的响应。

例如,如果游戏是类似于《好、坏、丑》的决斗游戏,我是坏人作弊者,我可以编写一个机器人,使我的数据包始终引入额外的延迟(模拟更高的RTT),然后在按下“射击”时去掉这个额外的延迟。这样会给我带来决斗中的优势(相当于多得了一段额外的反应时间)。而且,这种延迟可以通过单独的代理盒子实现,这对任何反作弊软件来说本质上是不可检测的,作弊者没有任何风险。即使加密流量(可以防止大多数代理机器人)也无法对抗这种作弊,因为数据包可以在不解密的情况下延迟。

换句话说,延迟补偿不仅可以用于网络延迟补偿,还可以用于玩家延迟(反应慢),因为从服务器的角度来看它们几乎无法区分(这导致了这种“人工延迟攻击”几乎无法检测)。

因此,延迟补偿与我们之前讨论的三个客户端改进完全不同:客户端插值、外推和预测并不需要服务器信任客户端,因此本质上不容易被滥用。

这就是延迟补偿具有争议的原因,我建议尽量避免使用延迟补偿。

…另一方面,玩家满意度更重要

另一方面——

如果使用延迟补偿可以让玩家满意,那就去做吧!不满意的玩家会比任何作弊者更早地“杀死”你的游戏。

在现实世界中,我们经常需要做出一些妥协。为了让诚实的玩家满意,有时可能需要允许一定的作弊风险,尤其是在快节奏游戏(如FPS)中。

在允许延迟补偿时,请确保:

  1. 确保除了延迟补偿之外没有其他方法可以让玩家满意。实际上,只有非常快节奏的游戏(如FPS)才可能需要延迟补偿;如果你为RPG考虑延迟补偿,请三思是否已经尝试了其他所有方法(尤其是客户端预测)。
  2. 注意延迟补偿引发的潜在作弊风险,并尽量减轻它。特别是,应检测时间戳和RTT波动过大的情况,并忽略此类波动的包。20毫秒左右的抖动是常见的;100毫秒的抖动偶尔会发生,但300毫秒以上的抖动可能过多(十秒的抖动显然不在考虑之列)。并不是说这些情况不可能发生(我当然不建议禁止这样的玩家),但忽略这种大抖动的包(或将抖动调整到允许的最近值)会是明智之举。
  3. 还需注意,尤其是在射击游戏中,存在大量其他作弊手段需要应对(例如瞄准机器人),因此基本上注定要进入反作弊的模式。只要应对了机器人,你可能也就应对了大多数的延迟补偿攻击。我们将在第八卷的“反机器人”章节讨论如何应对机器人,简而言之,为了让机器人的编写更加困难,你需要加密流量(以防止代理机器人)并保护客户端的完整性。
  4. 在每个动作基础上实施延迟补偿,监控变化对玩家的影响。同时,请务必观看GDC的[Aldridge]演讲,其中包含了大量现实世界的经验。

对于类似FPS的游戏,延迟补偿可能是必要的,以让玩家满意。如果延迟补偿对玩家满意度至关重要,那么不管是否喜欢,我们别无选择只能实施延迟补偿。

总体而言,以上理由可以概括为以下原则——

如果某个可能被作弊者滥用的功能对玩家满意度至关重要,请尽量实现它,但前提是你已穷尽所有无法被滥用的方法。

太多选项!我需要哪些?

在所有这些选项面前,一个显而易见的问题是“我究竟需要哪些?”这是一个“大问题”,在尝试你特定游戏的具体环境(真实网络或延迟模拟器)之前没有确切的答案。不过,有些观察可以作为分析的起点:

  • 如果你的游戏节奏较慢或中等(即动作的单位时间是“秒”),那么最简单的数据流(如图3.1)通常足够。
  • 如果你的游戏节奏更快(如MMORPG或MMOFPS),你可能需要图3.3或图3.4中的数据流。
    在这种情况下,最好从图3.3的简单数据流开始,并逐步添加功能(如客户端插值、外推、预测),以观察是否已达到理想效果而无需复杂化。
  • 如果在添加了所有“客户端”功能后仍有问题(通常仅FPS游戏才会出现),你可能需要考虑延迟补偿,但要警惕作弊者!

对于进一步的优化,你可能需要超越本书描述的技术(和/或以不同寻常的方式组合它们);然而,这通常是高度游戏特定的,很难进行概括。不管怎样,在尝试发明新的方法之前,确保理解并熟悉本章讨论的技术是必要的。

游戏世界状态与减少流量

到目前为止,我们已经完成了适用于游戏的数据流描述,现在可以更深入地了解客户端和服务器之间的消息具体细节。首先,让我们仔细看看通常会引发大多数问题(并消耗最多带宽)的消息——即图3.1、图3.3和图3.4中的“世界更新”消息。它与“可发布的世界状态”概念密切相关。

服务器端、可发布和客户端的游戏世界状态

image-7

在新兴的基于模拟的游戏开发者中,通常存在关于游戏世界状态的误解,这导致“为什么我们需要关心游戏世界的不同状态,而不只使用一个状态,使服务器端状态和客户端状态相同?”这样的疑问。当你的开发流程是“客户端驱动”时(如第1章定义),这种问题尤其常见。

答案是:“嗯,这取决于你的游戏类型。在某些情况下,你可能可以让客户端、服务器端和可发布状态相同,但对于很多游戏来说,这并不可行。”

带宽限制

这里的问题纯粹是技术性的,但非常令人头疼——这是带宽的问题。到2017年为止——

如果你的游戏每个玩家的带宽使用超过1Mbit/s,那你会遇到严重麻烦™

大多数严肃的多人游戏使用的带宽在每个同时在线玩家1kbit/s(如社交游戏)到200kbit/s(如第一人称3D模拟)之间。进一步降低带宽虽然可取(因减少流量成本等原因),但通常不是严格要求。

另一方面,如果你的游戏每个同时在线玩家的带宽使用超过500-1000kbit/s,你将面临很大的麻烦。主要问题是,对于不少玩家来说,这样的流量会超载他们的“最后一公里”,从而导致延迟和/或数据包丢失,使得游戏难以继续进行。尽管许多互联网服务提供商声称提供“高达100Mbit/s”的速度,但 (a) 大多数玩家不会支付这种带宽费用(截至2017年初,世界上超过一半的宽带连接仍然是(A)DSL),(b) 那些10Mbit/s的带宽通常只是理论值(即“只在纸上显示”),并非实际体验,(c) 通常还有大量流量与游戏流量竞争(从同一家庭的下载和种子到邻居的下载和种子;有关超额订阅机制的更多信息,见第IV卷的网络编程章节)。

因此,我建议即使在2017年,200kbit/s也是你应当力争的带宽目标,即便你的游戏是模拟类的。对于移动游戏(或正式地说,对于那些预计在移动网络上进行的游戏),通常允许的带宽会更低——我建议范围在20-50kbit/s,越低越好。

进一步优化带宽的理由

此外,请记住,减少服务器的数据包大小有时即使在客户端的“最后一公里”超载是由并行下载引起的情况下也有帮助,因为某些路由器会优先处理较小的数据包。我们将在第四卷的网络编程章节中粗略讨论互联网数据包优先级的一些方面。

另一个尽可能减少带宽的潜在原因是流量费用可能非常高——具体取决于你的盈利模式。尽管流量费用在过去至少二十年里稳步下降——到2017年初,你可以以大约$300/月的价格获得不限流量的1Gbit/s带宽,$2000/月则可以获得不限流量的10Gbit/s——但它仍远未免费。举例来说:如果你每位玩家每月可赚取$0.05,且每位玩家消耗200kbit/s的带宽(20%的玩家在峰值时在线),在1Gbit/s链接上,你可以支持约1Gbit/s / 200kbit/s/玩家 * 5 活跃玩家/在线玩家 = 250,000活跃玩家,支付$2000/月的流量费(即每位活跃玩家每月$0.008),但可以赚取$0.05*250,000 = $12,500/月。

由此可见,关键在于你能从每位玩家身上赚取多少。如果你的收入团队能从每位活跃玩家中获得$1/月,你无需过多担忧;但如果接近$0.01/月(这种情况很常见,因为大多数玩家可能是免费用户,且免费用户比例逐日增加),你将面临巨大的流量成本优化压力。换个角度来看,通过降低流量成本,你可能获得显著的商业优势,或进入其他方式难以盈利的游戏领域。

另一方面,对于许多游戏来说,租用服务器的费用往往会远高于流量成本。对于一个典型的模拟游戏,支持1000个同时在线玩家/服务器,5个活跃玩家/在线玩家,服务器租金为$200/服务器/月,那么每位玩家的服务器成本可能达到$0.04/活跃玩家/月(因此减少$0.008/活跃玩家/月的流量成本帮助不大)。不过,如果你选择支付“优质”/“实时”流量(而非默认的“最佳努力”),这种平衡可能会大幅改变;我们将在第七卷的“准备上线”章节中讨论不同类型的流量(不同类型的SLA)。

三角形与带宽

希望我已说服你“每个同时在线玩家几百kbit/s”是当前能承受的最大带宽。现在我们可以进入第二部分,观察——

如果尝试将3D三角形的信息从服务器推送到客户端,我们会远超1Mbit/s的限制。

让我们考虑一个非常简单的3D游戏场景,其中附近只有五个移动角色,每个角色由1万个三角形组成;即使不考虑其他物品(武器、道具、环境等),也意味着5万个三角形(在数量级估计中,我们可以假设顶点数相同)。由于这五个角色都在移动,所有顶点也随之移动;这意味着每个“网络更新”都需要传输五个角色 * 1万个顶点/角色 * 30位/顶点 ~= 1.5Mbit/场景,以典型的每秒20次“网络更新”计算,我们每秒会消耗30Mbit/场景,这远超我们的1Mbit/s限制,而这只是一个非常简单的场景。<痛苦!/>

正如我们在下文中看到的,通过使用单独的可发布状态,我们可以将这样一个简单场景的流量减少到每个角色每次网络更新约100位,总流量为100位/角色/网络更新 * 五个角色 * 每秒20次网络更新 = 10Kbit/秒/场景,相比传输三角形或顶点,这提升了3000倍。

MOG的三种不同状态

现在,让我们来看看如何实现3,000倍的流量优化。需要注意的是,以下分析是针对一个3D模拟游戏(具体而言,是我们的RPG游戏OurRPG);对于某些游戏(尤其是社交类游戏),下述不同的状态可以合并在一起,甚至可能三个状态完全一致。然而,考虑将所有三个状态分开,然后再决定是否合并还是有益的;特别是,它可以帮助你识别合并可能带来的潜在问题。

客户端状态

考虑一个MMORPG游戏,如OurRPG。假设玩家可以在3D世界中移动,进行对话、战斗、获取经验等。物理方面,我们希望实现刚体物理和布娃娃动画,但战斗很简单,不真正模拟物理效果,而是以动画形式表现战斗动作(类似于“上古卷轴”)。

如果游戏是单人模式,我们只需要一个客户端状态,包含所有网格(每个角色由数千个三角形组成)、纹理等内容。

服务器端状态

对于有权威服务器的MOG,我们需要一个服务器端状态。服务器端状态与客户端状态不同的是,它不需要如此详细的信息。

特别是,由于服务器端无需渲染,我们通常可以(且应当)丢弃服务器端的所有纹理,并使用更低多边形的3D模型。

实际上,为了保持合理的服务器数量,我们需要将服务器端的处理量降到最低,这个“绝对最低”可以定义为“丢弃一切不影响游戏玩法的内容”。对于大多数经典RPG(没有需要四肢位置影响游戏玩法的格斗),可以将每个PC和NPC简化为一个盒子(平行六面体)或棱柱(如六边形或八边形棱柱)。对于服务器端的房间模型,也可以大幅简化,只需知道房间中有一堵墙和一个杠杆,无需了解杠杆的确切形状。

在极端情况下(幸运的情况下),服务器端甚至可能不需要3D模型。尽管这种情况并非必然,我建议先检查是否可以在服务器端使用2D模拟。即使需要3D,这样的分析也能帮助去除服务器端不必要的元素。

对于OurRPG,我们确实需要在服务器端使用3D(例如刚体模拟和布娃娃效果,多层建筑等)。但我们不需要比一个带额外属性的六边形棱柱(如“攻击中”或“蹲伏中”以及“动画帧号”)更多的信息来表示我们的PC/NPC;刚体对象也只需用少量三角形来表示。

在服务器端模拟布娃娃效果时,我们甚至不会尝试模拟所有四肢的运动。我们只会计算死亡角色的质心运动。尽管这种方法可能对某些游戏显得不够真实,但对于其他游戏可能足够(并且节省服务器端大量CPU资源),所以我们会首先尝试这种方法。如果质心模拟无法满足要求,我们可能会稍微复杂化一点,如[Aldridge]中描述的逻辑,但传输所有布娃娃模拟数据仍然是不必要的。

这种多边形简化将显著减少服务器端状态的大小(相比完整的客户端状态),并减少所需的CPU循环。

可发布状态

现在,我们有了服务器端状态和客户端状态,还需要将数据从服务器端传递到客户端。为此,我们将引入另一个状态——称之为可发布状态。

可发布状态的最重要之处在于,它通常应当比服务器端状态更简单。任何时候,只要可以简化可发布状态,我们都应当这样做(参见上文关于减少带宽的理由)。

作为经验法则™,可发布状态的简化通常是可能的。例如,对于OurRPG,我们可以进行如下操作:

  • 用于表示PC/NPC,我们通常可以(因此也应当)丢弃所有网格,只保留一个(x,y,z,x-y-角度,动画状态,动画帧)的元组。
  • 除了渲染所需的元组外,还可能有几十个字段(如“库存”、“与他人的关系”等);是否需要发布取决于客户端逻辑。
  • 默认情况下(除非证明客户端需要特定字段),避免发布这些内容。可发布状态越小越好。
  • 在某些情况下,你可能需要发布这些信息。例如,如果游戏允许玩家从PC/NPC处偷取物品,那么客户端UI可能需要显示其他角色的库存以显示可以偷取的内容。这些信息可以通过向服务器请求获得,或通过发布成为可发布状态的一部分。
  • 注意,将库存信息发布不会对更新大小产生太大影响,因为可以通过增量压缩来优化它;但在初始化/转换期间会增加流量。
  • 另一方面,发布此类信息可能会增加信息泄露攻击的风险,因此,如果“尝试偷窃”时向服务器请求并不显得太迟缓,最好采用这种方式。
  • 即使需要发布一些变化频率低的字段,通常也应当将其与变化频率高的字段分离(例如,分为不同的发布树)。快速更新与慢速更新的时序需求不同,可能需要不同的同步策略(如UDP级别),分开处理可以简化这些策略。例如,库存很少更新,对200ms左右的延迟较为容忍,因此通常不需要频繁重新发送(可靠的UDP通道即可);而坐标和其他渲染相关内容则需要实时更新,应该更积极地重新发送。

为什么不保持一致?

现在回到这个问题:为什么不使用相同的客户端状态、服务器端状态和可发布状态?虽然前面已经回答了这个问题,但由于新手MOG开发者经常提出这个问题,这里再详细说明:

  • 视你的游戏而定,你可能可以保持三个状态一致。
  • 然而,对于3D模拟,这将可能导致:
    • 服务器端CPU负载显著增加(从而导致服务器运行成本增加)。
    • 流量大幅增加(到大多数互联网玩家无法接受的地步)。
    • 包括流量变为O(N²),这会导致大型游戏世界无法承受(因为所有移动都需要传输给所有客户端)。
    • 信息泄露攻击。若三个状态相同,黑客可以提取所有信息,实现“透视挂”或“地图挂”。

这种状态分离不仅限于3D模拟,其中一些考虑同样适用于其他类型的游戏。例如,对于即时战略游戏(RTS),除CPU负载外,3D游戏的所有考虑仍然适用。

非模拟类游戏和总结

对于某些非模拟类和非RTS类游戏(如社交游戏或黑杰克),可发布状态、服务器端状态和客户端状态之间的区别不那么显著,在许多情况下,服务器端状态可以与可发布状态相同(尽管客户端状态通常仍不同)。

例如,考虑一个黑杰克游戏,服务器端状态与可发布状态相同。每当一张牌被发出时,它可以表现为服务器端状态的更新以反映该牌已发出;由于可发布状态与服务器端状态相同,该更新将被推送到客户端。然而,发牌的动画通常完全在客户端处理(而不是在服务器端模拟牌在桌上飞行并以每秒20次网络更新的速度传输坐标变化)。

一般化观察

我们可以将这些发现概括为MOG游戏的通用规则:

  1. 可发布状态 <= 服务器端状态 <= 客户端状态
  2. 我们应努力减少可发布状态的大小

可发布状态的传输、更新、兴趣管理和压缩

确定了可发布状态的内容后(并知道如何在服务器端更新它),我们可以进一步考虑如何从服务器向客户端传递可发布状态(包括更新)。

最直接的方式是客户端连接时传输整个可发布状态,之后每当游戏世界发生更新时(对许多基于模拟的游戏来说,这可能是“每次网络更新”)传输相应的更新数据。

然而,我们通常可以在减少流量方面做得更好。减少流量有助于降低服务器成本和玩家的延迟,因此我们来深入探讨这些优化。

兴趣管理:流量优化和防止作弊

优化流量的第一步(并有助于防范作弊)是所谓的兴趣管理。兴趣管理只向每个客户端发送其需要渲染场景的更新数据。兴趣管理对许多游戏非常重要,不仅可以减少流量,还可以减少信息泄露攻击的风险。

例如,假设OurRPG是一个拥有10,000名玩家的开放世界,每个角色的可发布状态需要50字节/网络更新。如果将所有PC的数据发送给所有玩家,将意味着每个玩家需要接收10,000角色 * 50字节/更新/角色 * 20更新/秒 = 10MB/秒,总流量达到100GB/秒(大约1Tbit/秒)。这是一个巨大的数据量,成本高昂,而且10MB/秒(约100Mbit/s)对大多数玩家来说都过多。

但如果我们注意到在任何时候,每个玩家最多只能看到20个其他玩家(这是大多数现实场景的常态),那么我们可以实现“兴趣管理”,只向每个客户端发送该客户端“感兴趣”的数据。这样,每个玩家仅需接收20角色 * 50字节/更新/角色 * 20更新/秒 = 20KB/秒,总流量为200MB/秒,成本约为$1,000/月,效果大幅改善。

兴趣管理将流量从O(N²)降低到O(N),从而将每个玩家的数据量限制在一定范围内,不受游戏总玩家数量的影响。

实现兴趣管理

在实现兴趣管理时,有不同的方法可以选择。最简单的方式是只发送PC一定半径内的角色信息,或者“仅向同一‘区域’内的玩家发送更新”(无论“区域”如何定义)。也可以根据不同的距离采用不同的细节级别(LOD),例如发送更少的属性、减少更新频率或使用有损压缩来降低远距离对象的信息精度。

网格化兴趣管理是一种常用的实现方式。通过在2D或3D地图上建立网格,维护所有对象在网格中的位置,可以快速找到“接近”给定玩家的对象,将其作为候选对象发送至客户端。

防止信息泄露作弊

兴趣管理的另一个重要作用是防止信息泄露作弊。通过不向客户端发送“战争迷雾”区域或墙后面发生的事情,可以避免“透视挂”或“地图挂”等作弊行为。

例如,在一个扑克游戏中,不应将底牌信息作为可发布状态的一部分,避免所有客户端都接收到底牌数据。兴趣管理(或更好的方法是将底牌从可发布状态中剥离,并点对点传输)在这种游戏中是绝对必要的。

基于视锥的兴趣管理

视锥是3D游戏世界中的一个金字塔形区域,包含玩家当前可见的所有内容。基于视锥的兴趣管理的思想是,由于玩家看不到视锥之外的物体,因此无需传输这些物体的信息,从而减少流量和作弊风险。

然而,基于视锥的兴趣管理有两个问题:一方面,大多数情况下对流量帮助不大;另一方面,基于视锥的兴趣管理在防止信息泄露作弊方面潜力很大。第二个问题是急转弯的问题:如果PC突然转身,服务器需要在一个RTT后向客户端传送新进入视锥的所有对象信息,这可能导致突然的流量峰值。

一种缓解急转弯问题的方法是加入转向惯性,并使用基于距离的兴趣管理来处理较近的物体,确保即使出现延迟,场景仍能平滑渲染。

小结

有效的兴趣管理既能显著减少流量,又能防止信息泄露作弊。对许多快节奏的多玩家游戏来说,兴趣管理是不可或缺的,同时优化可发布状态大小、减少不必要的数据传输也是重要的流量优化手段。

压缩前的数据最小化

在开始压缩(更早之前是传输)可发布状态之前,我们通常可以(也应该)最小化包含在可发布状态中的数据量。往往我们会直接将服务器端的字段原样发布出去,但这种做法通常会导致冗余,从而产生不必要的数据传输。以下是一些数据最小化的通用经验法则:

  1. 避免传输双精度(double)数据:虽然在x86/x64上双精度计算便宜,但传输它们并不便宜。在99%的情况下,用单精度(float)替代双精度不会对渲染产生明显影响,并能将流量减少一半。此外,可以考虑使用定点数表示。

  2. 考虑使用定点数替代浮点数:这可以通过将整数与特定的倍率来表示数值。例如,将角度值用2字节的定点数表示通常足够准确(16位的精度为0.005度)。如果在位流中使用10位,则可以提供0.35度的精度,通常足以满足渲染需求。

  3. 使用定点数表示坐标:对于固定的空间分辨率,定点编码比浮点更高效。例如,在一个10,000米x10,000米的RPG世界中,如果需要1cm精度,则每个坐标需要20位,总计5字节,而浮点数则需要8字节(32位),增加了60%的带宽需求。

  4. 使用定点数表示货币:大多数货币可以以“分”为单位传输,这样就不必传输小数位,可以减少数据量。客户端在显示时再转换为常用单位。

  5. 将旋转数据转换为欧拉角或压缩四元数:在3D引擎中,旋转通常表示为四元数或旋转矩阵。将它们转换为欧拉角或压缩四元数可以去除冗余,更好地应对舍入误差。与四元数或矩阵相比,欧拉角通常只需30位(或4字节),而矩阵则需36字节,四元数需16字节。这种转换仅限于网络传输,服务器端不必对每个网格三角形进行此操作。

  6. 相对于父对象编码旋转:在某些情况下,独立舍入角度可能导致对齐问题,例如桌布的角度与桌子的角度不一致。这种情况下,可以将物体的旋转相对于场景图中的父对象编码,或在极端情况下相对于父对象的某个面或边进行编码,以确保良好的对齐。

总结

数据最小化是流量优化的第一步,通过减少不必要的数据字段和选择高效的编码格式,可以显著降低带宽需求,进而提升传输效率和渲染质量。这种预处理操作可以在不影响游戏体验的前提下,将流量优化至最小,为压缩和传输奠定良好的基础。

压缩技术

image-8

在应用兴趣管理和最小化数据后,我们的可发布状态已经减少了不必要的数据,但仍然可以通过“压缩技术”来进一步优化流量。

什么是“压缩”?

这里所指的“压缩”不仅仅是传统的ZIP或JPEG压缩,而是包含了多种数据处理技巧,尽管这些技术通常并不被称为“压缩”。我们在此将其统称为“压缩技术”,因为它们具备相似的处理模式:

  1. 在源端(这里是服务器端)获取数据。
  2. 将数据“压缩”为某种“压缩数据”。
  3. 通过网络传输压缩数据。
  4. 在接收端将数据“解压缩”成接近原始数据的形式(可以是无损也可以是有损,见后文关于“无损”和“有损”压缩的讨论)。
  5. 最终在目标端接收到与源端数据近似的数据。

这里值得注意的是,一些传统上不被视为“压缩”的技术在此也被视为“压缩技术”,从而帮助我们分类和理解这些方法。

为了使这些压缩技术更加实用且范围有限(尤其是避免在压缩过程中涉及到整个游戏世界状态),我们需要更严格地定义“压缩技术”所允许的操作(及不允许的操作):

  • 我们的“压缩技术”允许使用“参考基准”来减少数据发送量。

    • 当然,这个“参考基准”应该是发送方和接收方都已知的内容。例如,它可以是可靠数据流中的缓冲区(如LZ77或deflate算法中的情形),也可以是“已同步的可发布状态的快照”。我们将在下文中更详细地讨论“参考基准”(包括可靠流和不可靠数据包的参考基准)。
  • 我们的“压缩技术”允许了解特定字段的性质;这些特性可以通过IDL(接口描述语言)来描述(在“编码示例”部分中将有IDL相关的示例)。

    • 例如,如果我们有两个字段,其中一个是坐标,另一个是沿同一坐标的速度,那么压缩技术可以利用这一关系来减少数据量。
  • “压缩技术”可以依赖游戏中特定的常数,只要这些常数是全局的。

    • 例如,如果我们知道在OurRPG游戏中玩家按下“前进”按钮的典型模式是“以A m/s²的线性加速度加速到达速度V后保持恒速V”,我们可以利用这个信息(以及常量A和V)来减少流量。
  • “压缩技术”不允许使用其他内容。换句话说,我们不会将“基于客户端预测,考虑到撞墙情况的压缩”视为“压缩”(因为这会要求压缩知道墙的位置,而我们希望保持压缩的范围在一定的实用限制之内)。

  • “压缩技术”可以是“无损的”或“有损的”。但是,如果我们的压缩是“有损的”,则必须能够限定最大可能的“损失”(例如,如果我们的压缩以有损方式传输“x”坐标,则client_x与server_x之间的最大可能差异应是可控的)。除非另有说明,下文中的所有压缩技术均为无损压缩。

压缩的“参考基准”

“参考基准”在实现高效压缩方面非常重要。许多算法都依赖它——从游戏特定的“增量压缩”和“死值预测”到经典的“LZ77”算法。此外,基于参考的算法通常可以节省最多的带宽(例如,在deflate算法中,主要是LZ77而非Huffman编码带来了大部分压缩效果)。

一般来说,在游戏中有两种主要的“参考基准”类型:

  1. 状态参考基准:指某个之前的状态或“当前状态”。在这种情况下,发送方可以不直接传输“NPC坐标为X”,而是传输“NPC坐标在此刻没有改变”或“NPC坐标在此刻改变了dX”。这种“参考基准”通常用于增量压缩和死值预测。

  2. 流参考基准:指最近在双方之间传递的流中的一部分。例如,当LZ77压缩器检测到流中有一大段数据重复时,可以用“从当前位置往前跳N字节并从该位置获取M字节”来替代原数据段。

不可靠传输的低延迟可压缩状态同步

大多数现有的压缩算法针对可靠传输和可靠流(如TCP或有序可靠的UDP)设计,但可靠流会导致“行首阻塞”,这对延迟影响很大。为了在保持压缩的前提下消除行首阻塞,可以在不可靠的低延迟传输中实现“参考基准”。

一种实现压缩的低延迟不可靠同步的方法见于[Fiedler,快照压缩],我们可以称之为“低延迟可压缩状态同步”。其基本步骤如下:

  1. 从服务器到客户端的每个数据包都包含其编号(可以是时刻编号)。
  2. 从客户端到服务器的数据包(可能是玩家输入数据包)包含一个确认,即客户端接收到的最后一个数据包的编号。
  3. 对于每个客户端,服务器维护一个“已确认数据包的列表”。
  4. 每当服务器发送一个数据包时,它可以使用以下参考基准:
    • 该客户端确认的所有最近数据包(从而支持类似LZ77的算法)。
    • 这些最近确认的数据包产生的游戏世界状态。这是最常用的参考基准。

这种方案允许在不可靠连接中实现增量压缩、死值预测和类似LZ77的算法。

增量压缩

增量压缩是MOG(大型在线游戏)中最常用的压缩技术之一,分为两种主要类型:

  1. 全字段增量压缩:对于没有改变的字段传输一个位表示未变更的字段即可。此技术非常通用,适用于所有类型的字段,不论是否为数值。

  2. 递增增量压缩:当数值字段变化时传输差值而非绝对值。例如,坐标变化通常较小,使用更少的位编码差值可以减少数据量,或使用变长编码进一步压缩。

死值预测(Dead Reckoning)作为压缩技术

死值预测不仅用于客户端侧的运动预测,还可以在服务器端用于压缩流量。服务器通过利用客户端的运动预测减少传输的数据量。例如,服务器传输初始速度,客户端根据速度预测运动轨迹,只有当误差超过阈值时才发送修正信息。这种方法通常是有损的,但可以通过定义最大误差控制损失程度。

经典压缩算法

在游戏中,经典的压缩算法如Huffman编码或算术编码可以根据参考基准应用于可靠流或无参考基准的独立数据包。

通过这些压缩技术,我们可以在游戏网络流量中减少不必要的数据传输,从而提高效率并减少延迟。这些技术为游戏的流量优化提供了基础,使得网络传输更加高效,同时提升了用户的游戏体验。

经典无损压缩算法与小更新流量的优化

许多经典的无损压缩算法(如ZIP,或更具体的deflate)主要针对文件压缩,或更广泛地说是可靠流(如TCP或有序的可靠UDP流)的压缩。大多数经典压缩方法都基于两个基本算法:LZ77和Huffman编码。

LZ77 和 Huffman 编码

  1. LZ77:通过在流的早期缓冲区中找到相似数据片段并传输对该“早期缓冲区”的引用,从而替代直接传输流中的内容。
  2. Huffman编码:通过找到在流中出现频率较高的符号,使用较少的比特来编码这些频繁使用的符号。

ZIP的deflate算法便是LZ77和Huffman编码的组合,而LZ4则是LZ77的实现之一,不包含Huffman编码。

为什么经典压缩算法不适用于游戏流量?

经典压缩算法(如deflate)不适合游戏流量的一个原因在于这些算法通常没有针对小更新进行优化。尤其是“刷新”操作在deflate和其他面向流的算法中开销较大,因此不适合处理频繁的小更新。

不过,也有针对小更新优化的压缩算法,例如“LZHL”算法。它结合了类似LZ77和Huffman编码的压缩方法,专门优化小更新传输。

独立数据包的压缩

对于独立数据包的压缩,任何使用LZ77的算法(即使像LZHL这种小更新优化的算法)都难以高效工作,因为这些算法需要较大的参考基准,而独立数据包通常限制在一个1500字节以下的单个数据包中,这对于LZ77来说太小了。

在这种情况下,可以尝试使用Huffman编码,但要注意实现上通常会将“符号频率表”作为压缩数据的一部分传输。在丢包的情况下,这会变得复杂(或者每个数据包都传输表会使数据包变得庞大)。在游戏中,可以通过在实际游戏会话中收集数据统计信息,预先计算符号频率表,并将其硬编码到服务器和客户端中,这样丢包不会影响频率表,从而简化了Huffman编码的实现。

利用已确认的数据包进行压缩

除了以上两种情景(可靠流和独立数据包),还可以基于“已确认的数据包”构建压缩算法,这种算法可以通过不可靠通信中的确认数据包作为参考基准,类似于LZ77或LZHL。

这种算法类似于LZ77或LZHL,但引用的方式是“从包T的偏移O处使用M字节”,仅引用已被确认接收的数据包。它也类似于“差异更新”算法,将即将传输的数据包与之前的某个数据包进行差异压缩。

不同压缩机制的组合与递减回报法则

可以结合不同的压缩技术,例如:

  • 对于静态数据(如物品清单),使用全字段增量压缩,然后使用经典压缩。
  • 对于动态的坐标类数据,使用死值预测作为有损压缩,并结合增量增量压缩,最后使用VLQ对差异进行编码。

需要注意的是,组合不同压缩机制时,压缩的效果并不是简单相加的。即便一个压缩方案减少20%流量,另一个再减少20%,组合后的减少幅度通常小于20%+20%,甚至可能少于36%。

使用浮点数与无损压缩

在某些游戏中,开发者可能会直接将浮点数据传入deflate等无损压缩算法。这种方法虽然能工作,但效率极低。因为浮点数中的噪声位无法被有效压缩,尤其是当数据中包含大量8字节的double类型变量时,许多位只是噪声。

一种更好的方法是:

  1. 将角度等值转化为固定点数表示,仅保留8-10个有意义的位。
  2. 使用特定于该字段的Huffman编码来处理这些有意义的比特值。

通过这种方式,可以避免传输不必要的“噪声”位,从而有效地减少传输数据量。

自适应流量管理

即使您的游戏限制在250kbit/s,有些玩家的客户端带宽可能仍不足以处理您发送的数据。尽管在2017年家庭网络中带宽饱和较少见,但在以下两种情况下仍可能发生:(a) 当需要通过移动网络运行游戏,或 (b) 当每位玩家确实需要超过250kbit/s的流量时。

在这种情况下,可以考虑使用自适应流量管理。自适应流量管理的核心是:(a) 检测到特定客户端的带宽过载,(b) 相应地减少流量。

UDP中的自适应流量管理

自适应流量管理的一个经典例子可以追溯到1999年的《Tribes》引擎,之后类似的方法也被应用于《Halo: Reach》中。其基本思路是:优先将特定客户端需要的数据推送,尽量避免产生延迟累积。

简而言之,Tribes引擎中包含一个流管理器,流管理器会为每个客户端设定带宽限制。在此基础上,流管理器定期生成将要发送给客户端的数据包,让不同实体填充该数据包,优先填充高优先级的数据。数据包填满后,立即发送至客户端。

这个方法概念上简单(尽管实现和调优较复杂),但可以根据客户端带宽限制,尽可能利用客户端的通道带宽并提供足够的数据,这正是自适应流量管理的核心所在。

为确保低优先级的对象不被“永远”忽略,可以使用“优先级累加器”,具体如下:

  • 每个对象有静态优先级和动态“优先级累加器”。
  • 每帧中,将对象优先级累加到其“优先级累加器”。
  • 在决定发送哪些对象时,使用“优先级累加器”而不是静态优先级。
  • 发送对象后,重置其“优先级累加器”为零。

TCP中的自适应流量管理

在TCP中,自适应流量管理的一个实际应用是用于实时通信的Lightstreamer服务器。TCP中的自适应流量管理包括两部分:

  1. 带宽检测:动态检测每个客户端的带宽(尤其是移动连接,由于带宽质量波动较大)。
  2. 基于检测结果的限速:根据每个客户端的带宽限制流量。

Lightstreamer通过“合流”实现限速。当发送端队列积压时,可以将多个更新合并为单个更新,仅保留每个字段的最新值,从而在节省流量的同时保证数据的最新状态。

自适应流量管理在权威服务器中的应用

在采用自适应流量管理的游戏中,大多为P2P架构,而非权威服务器架构。这主要是因为:

  1. 权威服务器游戏长期以来在流量需求上更多地受到服务器端流量成本的约束。
  2. 随着流量成本的下降,服务器CPU成本的增长使更多游戏可以利用客户端带宽,从而提供更丰富的信息。

然而,自适应流量管理的不同客户端可能导致视觉表现差异,从“优雅退化”角度看是好的,但对公平性(如电子竞技)而言,可能并不理想。

自适应流量管理的价值

自适应流量管理在避免延迟的同时,可以尽可能多地向每个客户端提供信息,虽然其实现和缺点可能会使开发者权衡其使用的价值。但在移动游戏中,带宽相对较低,“优雅退化”常常是唯一可行的选项。

交通与实时战略

实时战略(RTS)在交通方面非常特殊。如果你的游戏有一千个单位,在玩家点击时会以相似但不完全相同的方式移动,那么任何幼稚的“可发布状态”实现方式都会导致交通问题变得不可接受。这个问题的严重性大到已经促使了基于“确定性同步步伐”(Deterministic Lockstep)的RTS游戏的广泛使用;然而,确定性同步步伐往往会通过解决交通问题来交换许多显著的问题,特别是它本质上容易受到“地图作弊器”(即“消除战争迷雾”)的影响,并且有玩家数量的上限;具体问题请参见第二章。因此,我强烈建议即便是RTS游戏,也尽量使用“权威服务器”和“可发布状态”,除非证明某个特定RTS的交通无法优化。

在优化RTS的交通时,有一些可以并且应该采用的技术(参见[Amir和Axelrod];尽管与下面的列表有重叠,但也存在显著差异,所以要确保同时阅读两者):

  1. 兴趣管理(Interest Management)是我们的朋友(特别是在更大的世界中)。我们不需要发送玩家无法看到的信息(并且仅发送他能看到的组的信息也大大减少了墙壁作弊器的潜力)。

  2. **“一千个单位在一个玩家点击下移动”**这一现象有助于优化交通。特别是,几乎普遍的情况是,这1000个单位将按照相似(但不是完全相同)的模式移动。这意味着,如果我们将这些移动编码为“这个拥有1000个单位的组移动到(x,y),并且它们在移动完成后应该站成一个著名的阵型#N”,那么我们将大大减少交通量。

  3. 在某些情况下,可能需要指定增量(从组位置到单个单位位置),而不是引用已知阵型。然而,由于我们将客户端仅用于渲染,这些增量(实际上,连组位置)可以是近似值;而且,一旦它们是近似值,它们就变得可以压缩(尤其是,它们很可能只会有几个非常常见的值,这使它们成为霍夫曼编码的理想候选)。另一方面,近似值本身就具有不精确性,因此我们需要小心避免它们的累积(见下文)。

  4. 大多数AI动作可以分为简单的动作(例如“沿路径移动…”或“跟随…”等;见[Amir和Axelrod]的详细信息),这些简单的AI动作可以委托给客户端。换句话说,我们可以对客户端说:“嘿,开始将这个组移动到(x,y),速度为V,带有这样的路径点,同时也考虑到组内单个单位的修正。” 请注意,虽然这是一种非常有效的交通优化技术,但它很容易成为另一种不精确的来源(尤其是由于客户端和服务器之间缺乏100%的确定性,或者由于在决策时客户端和服务器可用的信息不同)。

  5. 关于将路径规划/A*委托给目标客户端的问题,这可能部分可行,但你需要小心委托可能导致客户端和服务器之间产生显著不同移动的决策。特别是,如果有两条几乎相同的路径,且成本几乎相同,那么不精确性和/或缺乏100%确定性可能会导致这些小差异导致客户端和服务器选择非常不同的路径。为了避免这种情况,确保你发送给客户端和服务器的每一段路径足够小,这样它们就不会造成麻烦(换句话说,将“跨越整个地图的移动”命令分解成几个较小的命令,以确保客户端不会选择与服务器路径差异巨大的路径)。

  6. 请注意,这与在源客户端上执行路径规划是不同的。在源客户端上做决策是一种与交通优化无关的技术,而是优化服务器端CPU使用的手段。从作弊角度来看,它不能用来违反游戏规则(即,只要我只是做关于移动自己单位的决策),但它可能会促使某些可能被T&C禁止的辅助应用程序(这些应用程序也可能会改变游戏平衡)。

  7. 如果我们允许不精确性(如上文所述,它们可能在多个场景中出现),我们必须确保这些不精确性不会积累。解决这一问题的一个简单方法是使用指定单位最终目标的命令,而不是增量;这样,所有不精确性将自动变成“自我修复”的。

我必须承认,我没有亲眼见过(即“亲眼看到”)一个按照这些思路构建的RTS;然而,从我听说的情况来看(并尝试根据自己在不同领域的经验进行推测),似乎完全有可能在使用权威服务器的情况下,实现RTS游戏的可接受交通水平。尽管如此,没有任何形式的保证,且在采用此模型之前,务必先进行压缩测试。

交通优化:总结和建议

虽然我们花了一些时间才走到这一步,但我们终于到了。现在,我可以总结一下我关于优化MOG(多人在线游戏)交通的当前看法,并提供一套个人建议。作为初步的近似,我建议按以下顺序优化:

  1. 首先,确保从分离的客户端状态、服务器状态和可发布状态开始分析。即使后来发现这些状态在你的游戏中可能是相同的,也必须在眼睛睁开、充分意识到的情况下做出这种决策。

  2. 然后,最小化你的服务器端状态。这不仅是为了减少交通量,也为了减少服务器端的CPU负载。

  3. 接着,最小化你的可发布状态。要积极进取:丢掉一切,只有在客户端无法离开时,才向可发布状态添加字段。

  4. 在此过程中,将可发布状态拆分为多个具有不同时间要求的组

  5. 仔细检查你将要传输的数据类型。尽量避免使用双精度浮点数和浮动数;更好的选择是定点数字。同时,也要特别处理角度和旋转。

  6. 确保实现兴趣管理(Interest Management)

  7. 对于RTS游戏,参照上文讨论的RTS特定优化

  8. 确保使用“全场景增量压缩”(Whole-Field Delta Compression),以允许跳过未变化的物体更新。

  9. 广泛定义“未变化的物体”;例如,对于许多游戏而言,持续以相同速度和方向移动的物体可以被视为“未变化物体”(或者,你可以通过“死算”(Dead Reckoning)来处理它)。

  10. 考虑“死算”压缩,同时确保控制不良的视觉效果(如果必要,可以降低阈值)

  11. 别忘了“死算”的不同变体;根据游戏的具体情况,它们可能会带来显著的区别。

  12. 考虑在先前技术压缩的数据之上运行经典压缩,但不必指望其带来太大收益

  13. Deflate算法对大多数游戏无效(主要是因为“flush”的开销)。

  14. LZHL算法对于TCP和可靠、有序的UDP工作得不错,但要将其适配到不可靠和/或无序的UDP中,还需要额外的工作

  15. 霍夫曼编码(Huffman coding)及类似的编码(如霍夫曼式和算术编码),结合预先填充的频率表(见上文)将适用于UDP,但提升空间有限

  16. 在结合不同的压缩技术时,务必牢记它们的顺序非常重要

  17. 我强烈建议将所有类型的压缩代码与其他代码(包括模拟代码)分离

  18. 此外,我建议通过IDL(接口定义语言)编译器根据IDL中的规范生成压缩代码,而不是手写压缩代码。有关IDL的更多内容,请参阅下面的IDL: 编码、映射和部分。

MMOG与可扩展性

如果你的游戏世界(Game World)设计上只有十几个玩家(例如MOBA类游戏),那么每个世界的规模可能足够小,可以在单个CPU核心上模拟。在这种情况下,将你的游戏世界扩展到数十万玩家(运行在数万个游戏世界上)是非常简单的。

然而,如果你的游戏中有数千个玩家在同一个游戏世界中(根据维基百科的定义,这就是MMOG),你就无法再依赖于单个CPU核心来承载整个游戏世界了。更糟的是,一旦你的游戏世界足够大,你甚至无法依赖单一服务器来承载它(截至2017年初,市面上最大的一台服务器在不使用高度专业化硬件的情况下通常配备的是4个插槽,每个插槽24核,总共96核——这已经算很多了,但通常仍然只能支撑大约1万名玩家的某种模拟)。另外,值得一提的是,如果将同样的96核分布到4台“工作服务器”(每台2个插槽、24核),你将节省约2倍的服务器租赁成本。

换句话说,当我们讨论在同一个游戏世界中容纳数万名同时在线的玩家时,我们将无法通过“扩展”来解决问题,而是必须通过“扩展外部资源”来应对。


共享无关架构(Shared-Nothing Architecture)

一旦我们进入多个服务器框架,就需要通过某种消息系统进行通信(每个服务器框架将变成自包含的共享无关单元)。

顺便说一句:通常我会主张将你的游戏世界分割成更小的“核心大小”共享无关单元(即,每个共享无关单元的限制不仅是整个服务器,而是单个核心)。这种做法有很多好处,我们将在第二卷中关于“(重)演员”(Reactor)一章中详细讨论;此时,我只想说,核心大小的单元:

  • 允许使用传统的游戏循环风格编写模拟代码(非常直接);
  • 允许显著更好的调试(包括生产后的回溯调试和基于回放的回归测试);
  • 通常能表现得更好(因为没有跨线程的竞争)。

不过,为了本章的目的,我们可以忽略游戏世界单元的大小,仅假定:

  • 我们的可扩展服务器端由多个自包含的(“共享无关”)单元组成。
  • 这些单元仅通过某种消息传递进行通信。实现这些消息传递的任务将是一个独立的任务,我们将在“服务器到服务器通信”一节中进行讨论。

最后值得一提的是,当然,在本章中,我们仅仅触及了MMOG可扩展性的话题的表面(特别是“如何拆分大型游戏世界”这一问题)。总体来说,可扩展性是一个比单一话题更复杂的领域,我们将在整本书中继续讨论可扩展性相关的问题(特别是第三卷、第六卷和第九卷涉及了大量可扩展性的内容)。


一个显而易见的解决方案:分离NPC/AI

如前所述,拆分游戏世界的关键原因是避免让单一CPU核心(通常是运行模拟/游戏循环的核心)过载。而一个显而易见的解决方案(早在2003年就已经被[Beardsley]提出)就是将NPC(非玩家控制角色)和它们的AI分离到独立的单元中(这些单元可以运行在独立的核心或独立的服务器上,这样可扩展性就得到了提升)。

在这种情况下,从我们的游戏世界单元(模拟游戏的部分)来看,NPC/AI单元的作用与普通客户端相似,它们通过以下两种方式进行通信:

  1. 获取当前游戏世界可发布状态(Publishable State)的一个或多或少是最新的副本。
  2. 向游戏世界发送输入。

此方法的一个附带好处是,它能显著简化游戏世界逻辑的代码,因为所有的角色,无论是由玩家控制还是由AI控制,都能以类似的方式进行处理。


拆分为区域

然而,尽管分离AI通常被认为是一个好主意™,但它几乎从未足以实现真正的可扩展性,尤其是要支持数十万玩家的规模(通常,在进行这样的AI分离时,我们得到的可扩展性提升仅是2-3倍,而不是10倍到100倍的提升)。

因此,当你的大型游戏世界超出某一规模时,通常需要将其拆分成多个区域(zone、cell或其他你想用的名称);然后,每个子游戏世界都可以运行在独立的单元上(每个单元又运行在独立的核心或服务器上)。这样做将带来极佳的可扩展性:只要你能将游戏世界拆分为足够多的区域,系统就能几乎完美地扩展。

在这种情况下,每个子游戏世界将以类似通常的游戏世界方式运作,但它们需要将你的玩家角色(PC)和NPC转移到其他子游戏世界。

如果你的大型游戏世界本身天然地划分为不同的区域(房间等),且每个区域足够小,可以在单个CPU核心上运行,那么实现子游戏世界的拆分并不难。然而,如果你的游戏世界没有这样的明显边界,我们就谈到了所谓的“无缝游戏世界”,这时事情会变得更加复杂。

无缝游戏世界:重叠!

一种常见的技术,用于实现无缝游戏世界的拆分,可以描述为“带重叠拆分”(split-with-an-overlap)。这一技术在[Beardsley]中有详细描述,至今仍然被广泛使用;例如,可以参考[Baryshnikov]中的应用。

基本思想是将大型无缝游戏世界拆分成多个子游戏世界(sub-Game-Worlds),并使得接近边界的物体(即位于预定义的“共享区域”内)同时出现在两个子游戏世界的单元中。所谓的“共享区域”通常定义为从两个子游戏世界都可见的区域(或者更准确地说,定义为确保每个玩家的“兴趣区域”始终适配到一个子游戏世界内的区域)。

接下来,我们需要确保所有位于“共享区域”内的物体同时出现在两个子游戏世界单元中。这里有一种常见的实现方式,尽管从两个略微不同的角度来看,这种方式是相同的。

作为第一种选择,我们可以认为在两个子游戏世界中同时模拟相同的“共享”物体(尽管在任何时刻,只有一个物体的状态是权威性的,因此我们始终知道如何进行协调)。来自“权威”子游戏世界的信息会被推送到非权威的子游戏世界,它们会调整物体的位置,以使物体在位置上与权威版本同步(如果需要的话,使用协调算法)。

第二种看待相同问题的方式是,对于那些在我们子游戏世界中非权威的物体,我们从另一个子游戏世界(该子游戏世界目前对该物体是权威的)获取信息(就像客户端一样),然后我们也会对这些非权威物体进行客户端预测(再次使用协调算法,如果需要的话)。值得注意的是,无论我们如何看待它(第一种选择或第二种选择),最终的结果几乎是一样的。

需要注意的是,在任何情况下,我们都需要处理从一个子游戏世界到另一个子游戏世界的物体所有权转移问题;通常这并不困难。因为只有一个子游戏世界是权威的,因此只有它在物体跨越子游戏世界边界时做出物体转移的决定。

关于子游戏世界的一个更复杂的问题是,“我们的游戏世界究竟应该如何拆分?”通常情况下,你需要依靠每个子游戏世界当前负载的模型来决定子游戏世界的拆分方式,这就导致了子游戏世界边界的动态变化。处理这些动态边界是可行的(特别是如[Beardsley]和[Baryshnikov]中所描述的那样),但它是高度依赖游戏的,且通常并不简单(并且别忘了在动态子游戏世界边界算法中加入某种滞后机制,否则你可能会遇到不必要的边界振荡,试图追逐不断变化的最佳状态)。

服务器端的不确定性

在子游戏世界的上下文中,经常出现的一个问题是它们之间的时间同步问题,以及相关的不确定性问题。如前所述,当某些物体在子游戏世界之间共享时,我们无法始终保证在非权威子游戏世界中的物体副本与权威服务器上的原始物体完全一致。

这种情况类似于客户端的情形,客户端通常拥有与服务器几乎相同但又不完全相同的数据(例如,在“撞墙”和“服务器协调”部分中有描述)。然而,对于服务器(及其子游戏世界)而言,情况甚至更糟,因为——

  • 这些非权威且不完全相同的物体可能与权威物体发生交互,并可能导致权威物体行为的不同。

在游戏的实际环境中(即除非你在进行科学模拟),大多数情况下这种不确定性并不会成为问题。虽然确定性和可预测性在一般情况下是好的,但在实际应用中,我们通常可以接受组件级别的确定性(例如在《第II卷》中的(Re)Actors章节讨论的那种),而不必追求整个系统的确定性。

然而,你仍然需要注意服务器端的不确定性,因为根据你的游戏特点和实现细节,它可能会导致一些不愉快的宏观效应。例如,在某些实现中,玩家可能会因为跨越子游戏世界时传递其权威数据包的延迟,相比于传递子弹数据包的延迟,错过了原本不可避免的死亡时刻(也就是玩家曾短暂不在任何一个子游戏世界中的时刻,恰好是子弹应该击中的时刻)。你永远无法确定这是否会成为一个大问题,但在某些情况下,确实可能会。

完全消除不确定性:时间同步

谈到不确定性,实际上存在一种严格的方式(实际上有多种方式)可以彻底消除服务器与服务器之间的不确定性(这等同于使整个服务器端系统具备确定性)。

在深入讨论之前,我们需要指出,消除不确定性相当于建立一个覆盖所有子游戏世界的统一时间,从而在不同子游戏世界之间进行时间同步,并根据同步的时间执行所有计算。另一方面,这种子游戏世界之间的时间同步机制与客户端与服务器之间的时间同步(我们在上面的“时间同步”章节讨论过)有很大不同:特别是对于子游戏世界之间的时间同步,通常没有单一的时间权威(尽管每个物体在每个时刻都有其权威,子游戏世界通常并不从属于彼此),这就带来了一些复杂性。

无回溯同步:CMS/LBTS,锁步

一种消除不确定性和实现时间同步的方式是通过学术界开发的算法,例如CMS和LBTS。我不打算详细讨论这些算法(如果需要更多讨论,请参考[Smith and Stoner]),但简而言之,这些算法的思路是延迟所有节点上的仿真,直到服务器接收到所有相关服务器的上一“网络时钟”的计算结果。换句话说,这是一种非常接近“锁步”算法的方式。虽然在客户端环境中,锁步算法因其对客户端的高依赖性而容易出问题,但在服务器端(尤其是同一数据中心的服务器之间),这种方法可能会奏效。

然而,我仍然不太倾向于使用任何形式的阻塞同步。首先,如果一个服务器的速度较慢导致整个系统停止,通常不是一个好选择,尤其是在服务器数量较少的情况下还勉强能忍受,但一旦服务器数量增多,这通常会变得灾难性。除此之外,我不喜欢让整个系统按最慢的服务器速度运行(并且每时每刻都如此)。不过,如果没有其他方法能够奏效,这些方法是有报道显示可以工作的。

通过服务器回溯同步:“价值日期”

另一类实现时间同步和消除不确定性的方法基于与延迟补偿相关的服务器回溯(Server Rewind),这一概念我们在“服务器回溯”部分已有讨论。

我们考虑一个示例,假设子游戏世界A和子游戏世界B分别模拟自己的游戏世界部分,并且需要将子游戏世界A计算出的变化应用到子游戏世界B。在这种情况下,子游戏世界A只需要向子游戏世界B发送一条消息,并附上一个时间戳,表示“该变化应该发生的时间”;就这么简单。

在接收到该消息后,子游戏世界B将会查看其自身仿真中的当前时间与消息中指定的时间戳的对比情况,然后根据结果执行以下操作之一:(a) 等待直到其“当前时间”达到该时间戳,(b) 立即应用该消息,或 (c) 将其游戏世界回溯到时间戳所指定的时间,再应用该消息,就像它在那个时间点发生过一样。

从逻辑上讲,这个“应该发生的时间”时间戳字段在概念上与与SWIFT银行转账相关联的“价值日期”字段完全相同。由于SWIFT转账需要时间(即使在2016年,转账也可能需要3-5个工作日!),而我们作为客户当然不希望我们的资金“停滞”在某个地方而不产生任何利息,因此每个SWIFT转账都带有一个“价值日期”字段。这意味着“当接收银行收到这个转账时,确保按照‘价值日期’将其录入目标账户;这包括计算所有相关的利息等”。为了实现这一点,接收银行实际上“回溯”时间到“价值日期”字段中指定的日期(由于银行账户大多是独立的,这对银行来说并不难),然后应用转账,并重新计算自该时间点以来的所有利息和其他计算。

这个类比在将“价值日期”样的时间戳与数据库间异步转移(下文在“进一步探讨:具有事务一致性的数据库异步转移”部分描述)结合使用时变得更加明显。

无论是否使用“价值日期”类比,“服务器回溯”实际上可以实现完美的最终同步,无论数据包和计算的相对顺序如何,并且没有额外的延迟。同时需要注意的是,对于服务器到服务器的通信(假设你控制所有服务器,因此它们是值得信赖的),作弊问题(这是在客户端回溯时基于客户端发来的时间戳可能存在的问题)不再适用,因此“服务器回溯”在服务器间的应用不受这一缺点的影响。

另一方面,对于大多数游戏来说,我认为将系统做得“严格”并为消除不确定性做特别努力通常是过度的。另一方面,如果目标是构建一个完全正确的游戏世界,我可能会尝试采用“服务器回溯”方式(即应用于子游戏世界之间的通信)。

短暂事件、转发输入与(类似)广播消息

在我们讨论了状态同步和复制之后,接下来需要考虑的是其他类型的服务器与客户端之间的通信。

短暂事件

“短暂事件”(Transient Events)是指一些临时性、瞬时发生的事件,通常用于实现爆炸、子弹击中等效果。在这一章的开头,已经提到过,短暂事件有一个重要的特性——如果它们被延迟传送,就没有什么意义了;因此,从我们的角度来看,我们要么“立刻”传送它们,要么“永远”不传送。除此之外,短暂事件通常需要同时发送给所有感兴趣的玩家,尽管它们可能会受到兴趣管理的影响。

转发输入

前面提到的短暂事件在游戏中非常常见(大多数爆炸、子弹击中等事件都是基于它们实现的)。与此不同的是,另一种服务器到客户端的通信方式——有效地将其他客户端的输入转发到我们的客户端,在游戏中并不常见;然而,它至少在一款AAA游戏中得到了成功应用(参考[Aldridge])。

转发输入的思路如下:假设另一个玩家已经按下了向左移动的按钮,服务器已经知道了这一点。然而(例如由于惯性),直到这一动作在可发布状态中体现出来,可能需要一段时间——然后这个过程还会被服务器和客户端之间的延迟进一步拖延。因此,如果我们将“另一个客户端按下了向左移动按钮”这一信息传递给我们的客户端,就可以提高客户端预测的精确度(有效减少其他玩家动作与在客户端屏幕上显示的延迟)。

信息泄露的潜力

虽然上述逻辑是成立的,但转发输入也存在一个重大问题——它与作弊有关(更具体地说,是信息泄露攻击)。如果作弊者的客户端已经知道对方按下了“左移”按钮,而左移的动作尚未显示出来,那么一个从作弊者客户端提取此信息的作弊行为(例如在游戏界面上以箭头指示玩家将要向左移动),可能会彻底改变游戏的平衡,极大地偏向作弊者。

尽管如此,正如前文所述,让玩家满意通常比防止更微妙的作弊更为重要,因此我可以想到一款或几款游戏(都是第一人称射击游戏),在这些游戏中,转发输入可能是必要的。另一方面,如果你确实需要转发输入,务必考虑它是否真的对你的游戏至关重要(或者也许你只需稍微调整游戏玩法,就能去除这一需求)。

总的来说,正如在“另一方面,玩家幸福更重要”一节中讨论的相同逻辑也适用于转发输入。我们还要指出,转发输入所导致的作弊影响可能会远远大于延迟补偿,因此在决定是否使用转发输入之前,它们应该接受比延迟补偿更严格的审查。

实现

在实现上,转发输入与短暂事件有相似的属性。它们同样需要尽快传送,或者根本不传送,同时也往往面向所有玩家(受兴趣管理的影响)。

(类似)广播消息(带兴趣管理的广播)

如上所述,短暂事件和转发输入有非常相似的要求,因此不难理解它们可以通过相同的机制来实现。我所说的就是(类似)广播消息。

(类似)广播消息的思路很简单——我们只是将某些内容发送给所有可见的玩家,不关心消息是否已经到达(因为如果消息没有到达,再次发送消息将是浪费资源,因为它无论如何已经“迟到”)。

通常来说,不可靠的UDP数据包最适合实现(类似)广播消息。需要注意的是,这些消息需要通过多个单播UDP数据包实现(即通过一个数据包发送给每个玩家),而不是单一的多播UDP数据包。

点对点通信与非阻塞RPC

在讨论了可发布状态和(类似)广播消息之后,我们需要的最后一种通信方式是点对点通信。尽管可发布状态和(类似)广播消息主要是关于服务器与客户端之间的通信,但点对点通信既可以发生在客户端与服务器之间,也可以发生在两个服务器之间。这两种点对点通信方式有很多相似之处,但也有一些显著的差异。

在客户端与服务器之间的点对点通信中,大多数情况下我们讨论的是客户端将其输入(以及其他请求,如指令)发送到服务器端。而在服务器与服务器之间的通信中,可能会有许多不同类型的信息需要传递(具体内容将在下文讨论)。

需要注意的是,TCP与UDP的区别仍然超出了本章的范围;我们现在讨论的主要是我们需要什么,而不是如何实现它。

image-9

RPCs

无论是客户端与服务器之间的通信,还是两个服务器之间的通信,所有的点对点通信都共享某些共同的特性。

尤其是,在游戏中,点对点通信通常通过非阻塞远程过程调用(RPC)来实现。虽然这并不是强制要求(你也可以使用简单的消息交换,手动编写或者基于IDL的封送),但非阻塞RPC通常比较方便且直接。

然而,需要注意的是,尽管非阻塞RPC对于游戏来说是完全可行的,但你应该尽量避免使用阻塞RPC(例如DCE RPC/COM/CORBA/ICE使用的RPC);至少在我们谈论的是广域网(WAN)时,阻塞RPC通常是不适用的。我们将在《第二卷》中的(Re)Actors章节讨论阻塞RPC不适用于互联网应用的原因。

实现非阻塞RPC

要实现非阻塞RPC,你需要一种方式来指定远程调用函数的签名;这种签名定义了RPC调用方和被调用方之间的接口(通常也涉及协议,尽管关于编码的更多内容将在下面的“示例:编码”部分中讨论)。

有时候,这种规范是通过为你常用的编程语言中的现有函数和方法添加特定属性来完成的。例如,在Unity中,使用[RPC]/[ClientRpc]/[Command] C#方法属性来实现,在UE4中则通过UFUNCTION() C++宏来实现。

然而,我通常更喜欢在一个单独的接口定义语言(IDL)中定义这些签名,并使用我自己的IDL编译器来处理它,生成存根(这些存根将在通信的双方的应用层代码中使用)。

我们将在下面的“IDL:编码、映射和协议更改”部分详细讨论IDL(包括语言内IDL与独立IDL的区别);目前,对于我们的目的而言,只需要理解我们会在某个地方指定函数签名,并能够在通信的一方实现这些函数——并在另一方调用它们。

空返回值与非空返回值的非阻塞RPC

当讨论非阻塞RPC时,我们需要意识到,通常有两种情况。

第一种情况是返回void的非阻塞RPC(并且不能抛出任何异常)。对于这样的void RPC,一切都很简单——调用方只需要封送RPC参数并将消息发送给被调用方,被调用方再解封它并执行RPC调用。仅此而已。从所有角度来看(除了语法本身),调用这样的RPC和发送一条消息完全相同。换句话说,返回void的非阻塞RPC只是封送其参数的一种方式。

一个定义void RPC(客户端到服务器)的IDL示例如下:

STRUCT Input {
    bool left;
    bool top;
    bool right;
    bool bottom;
    bool shift;
    bool ctrl;
};

void moveMe(Input in);

非空返回值的RPC

第二种情况(也是更复杂的情况)是一个RPC,它返回一个值或可能抛出一个异常(通常是两者都有)。这样的非void RPC通常是服务器到服务器之间的RPC,示例如下:

STRUCT PLAYERDATA {
    int level;
    int xp;
    INVENTORY inv;
    RELATIONS rel;
    ETC etc;
};

PLAYERDATA dbGetPlayer(int user_id);

这里的要点很简单:提供一种方式,让游戏世界服务器向数据库服务器请求有关某个特定玩家的数据(其中PLAYERDATA足以在游戏世界中实例化该玩家)。需要注意的是,这个RPC(与上面的moveMe()例子不同)本质上是非void的:我们确实需要从对方(数据库服务器)获得一个回复,而且在得到结果之前,我们无法继续进行其他相关的任务(如玩家实例化)。

这样的非阻塞非void RPC实现起来要复杂得多,且大多数流行的游戏引擎并不支持它们(有关Unity/Photon/UE4/Lumberyard等游戏引擎的更多信息,请参见《第二卷》中的第三方游戏引擎章节)。

实现非void非阻塞RPC的主要问题是调用方需要指定在函数返回(或抛出异常)时应该做什么。在事件驱动编程的背景下,有几种方式可以实现这个逻辑(从简单的消息处理到协程,再到回调地狱、lambda金字塔、future和promise,甚至“代码生成器”),我们将在《第二卷》的(Re)Actors章节中详细讨论所有这些方法。目前,我们只需要注意的是,所有这些方法在功能上是等价的,所以选择的标准并不是“是否能工作”,而是“哪种方法使用起来最方便”。就我个人而言,我更倾向于使用“代码生成器”和/或协程,它们比future和promise更有优势(比其他方法,如lambda金字塔,也有很大优势)。

当你的引擎不支持非void RPC时

如果你的引擎不支持非void RPC,你可以在一个返回void的RPC函数调用之上实现一个额外的void RPC函数调用,来返回结果。在这种情况下,我们的最后一个示例将需要按照以下方式重写:

// 游戏世界服务器到数据库服务器:
void dbRequestPlayer(SERVERID where_to_reply, int user_id);

// dbRequestPlayer()的实现
// 在其中调用gameWorldPCData()

// 游戏世界服务器到数据库服务器:
void gameWorldPlayerData(PLAYERDATA data);

或者以更通用的方式(允许同一游戏世界支持多个未完成的请求,这几乎总是一个好主意):

// 游戏世界服务器到数据库服务器:
void dbRequestPlayer(SERVERID where_to_reply, int request_id, int user_id);

// dbRequestPlayer()的实现
// 依然在其中调用gameWorldPCData()

// 游戏世界服务器到数据库服务器:
void gameWorldPlayerData(int request_id, PLAYERDATA data);

虽然这种方式可行(并且严格等同于《第二卷》中关于(Re)Actors章节中讨论的其他替代方法),但是实现调用与回复之间的匹配(这反过来需要存储当前未完成调用的映射)是相当繁琐且不方便的。关于更多的细节和替代方法,请参见《第二卷》。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant