问题

如何评价《守望先锋》架构设计?

回答
《守望先锋》的架构设计:一场对游戏体验极致追求的工程炼金术

《守望先锋》(Overwatch)自2016年横空出世以来,便以其快节奏、易上手但精通难的独特魅力席卷了全球游戏市场,并催生了蓬勃发展的电子竞技产业。然而,在这光鲜亮丽的表象之下,是 Blizzard Entertainment 团队精心雕琢的、对玩家体验的极致追求所驱动的庞大而复杂的架构设计。评价《守望先锋》的架构,不能仅仅停留在技术层面,更需要深入理解其背后对游戏玩法、服务器稳定性、玩家公平性以及可扩展性的深层考量。

核心设计理念:以玩家为中心,服务于“快节奏”与“易上手”

《守望先锋》的架构设计,可以说是一场围绕着“让每一局游戏都充满乐趣且尽可能流畅”的工程炼金术。Blizzard 在设计之初就明确了几个关键目标:

低延迟、高响应的战斗体验: 这是快节奏FPS游戏的生命线。玩家的操作需要被服务器近乎实时地感知并反馈,任何明显的延迟都会严重破坏沉浸感和公平性。
易于学习,但有深度: 英雄技能的设计、团队配合的策略,都需要在清晰易懂的基础上,提供足够的成长空间。架构需要支持这种“易于上手,难于精通”的设计。
强大的网络同步与容错能力: 在多人在线游戏中,网络问题是难以避免的。架构需要能够尽可能地处理各种网络抖动、丢包,甚至允许玩家在连接不稳定的情况下也能获得相对可玩的游戏体验。
高度的可扩展性与迭代能力: 游戏运营过程中,需要不断推出新英雄、新地图、新模式,甚至平衡性调整。架构必须能够支持快速、低风险的更新部署。

技术栈与关键模块拆解:

为了实现上述目标,《守望先锋》构建了一个复杂的、但高度模块化的系统。

1. 游戏引擎与渲染:
《守望先锋》基于 Blizzard 自家的 NetEase 引擎(Source Engine 的一个高度定制和优化的分支,也常被称为 “Titan” 引擎的前身)。这个引擎在早期开发过程中就积累了丰富的网络游戏经验。
关键考量: 引擎需要支持高帧率渲染,同时还要高效地处理大量动态对象(包括玩家、技能特效、地图元素)。对 DirectX 的深度优化、GPU 实例化技术的使用,都是为了在高负载下保持流畅的视觉体验。
架构体现: 引擎的渲染管线高度模块化,允许美术团队独立工作,并能快速集成新内容。物理引擎的计算也在优化之列,以保证技能碰撞、弹道轨迹的准确性。

2. 网络同步与服务器架构:
这是《守望先锋》架构设计的重中之重,也是其最令人称道的部分之一。
权威服务器 (Authoritative Server): 游戏中的所有重要状态(玩家位置、技能释放、伤害计算等)都由服务器决定,客户端只负责发送输入指令和渲染服务器的指令。这极大地杜绝了作弊,并保证了所有玩家看到的结果是统一的。
确定性网络同步 (Deterministic Lockstep): 这是《守望先锋》实现低延迟和高稳定性的核心技术。简单来说,服务器和所有客户端都运行着一套完全相同的游戏逻辑。当玩家输入发生时,服务器会广播这个输入给所有玩家,然后所有客户端基于这个输入,独立执行游戏逻辑,从而产生相同的结果。
优点: 极大地减少了服务器需要发送的同步数据量,因为大部分同步数据是由客户端自行计算得出的。一旦网络连接正常,客户端之间的同步会非常精确。
挑战: 对客户端的计算能力和游戏的确定性要求极高。任何一个客户端因为性能问题、bug 或作弊导致计算结果不同步,都会造成“不同步”现象,影响玩家体验。Blizzard 在此方面的优化达到了令人发指的地步,能够容忍一定程度的网络抖动。
客户端预测 (Clientside Prediction) 与延迟补偿 (Lag Compensation):
客户端预测: 当玩家按下某个按键时,客户端会立即预测操作结果并显示给玩家,而不需要等待服务器确认。这极大地提升了操作的即时反馈感。
延迟补偿: 服务器在处理玩家的攻击指令时,会根据该玩家的延迟,回溯历史数据,以“公平”地判断攻击是否命中。例如,当一个高延迟玩家射击一个移动中的目标时,服务器会回看目标在该玩家发送攻击指令时所处的位置,而不是当前位置。这使得网络延迟较高的玩家也能享受到相对公平的对局。
服务器架构: 采用了分布式服务器架构,玩家进入游戏后会被分配到最近的区域服务器(Region Server)。这些服务器负责处理具体的游戏对局,而有专门的 Matchmaking Service(匹配服务)来为玩家分配游戏,以及 Game State Service(游戏状态服务)来管理玩家的账号信息、排位分数等。
架构体现: 这种设计保证了游戏的扩展性,可以通过增加服务器数量来应对玩家数量的增长。同时,区域服务器的划分也降低了玩家连接到服务器的延迟。

3. 游戏逻辑与状态管理:
实体组件系统 (EntityComponent System, ECS): 《守望先锋》广泛采用了 ECS 模式。游戏中的每一个元素(玩家、技能、子弹、特效等)都被抽象为一个“实体”(Entity),实体可以拥有多个“组件”(Component),每个组件负责管理实体的特定数据或行为(例如,位置组件、生命值组件、移动组件、攻击组件)。
优点: 带来了极高的灵活性和代码复用性。新增一种英雄能力,可能只需要创建一个新的组件,然后将其附加到对应的英雄实体上,而无需修改大量现有代码。这对于需要不断迭代的游戏至关重要。
架构体现: ECS 使得游戏逻辑可以被解耦,各模块之间依赖性降低,易于测试和维护。同时,ECS 也非常适合 GPU 加速的并行计算,能够优化大量实体同时存在的场景。
状态机 (State Machine): 英雄的技能、玩家的状态(例如,移动、跳跃、施法、死亡)等,都通过状态机来管理。这使得游戏状态的流转清晰、可控。

4. AI 系统(Bot):
《守望先锋》的 AI 系统设计也非常出色,尤其是在训练场和自定义模式中,Bot 能够模拟不同难度和风格的玩家。
行为树 (Behavior Tree) / 状态机: Bot 的行为逻辑通常通过这些模式来实现,能够根据当前的游戏情境做出决策(例如,寻找掩体、追击敌人、使用技能)。
架构体现: AI 系统与核心游戏逻辑高度解耦,可以在不影响主程序的情况下进行迭代和优化。

5. 数据驱动设计 (DataDriven Design):
《守望先锋》的许多游戏数值(英雄生命值、技能冷却时间、伤害数值、移动速度等)都通过外部数据文件(例如,JSON、XML 或自定义格式)进行配置,而不是硬编码在代码中。
优点: 使得平衡性调整和数值改动变得极其方便,无需重新编译游戏。设计师可以直接修改数据文件,然后通过热更新(Hotfix)的方式应用到游戏中。
架构体现: 这种设计极大地提高了游戏的迭代效率和灵活性,是支撑游戏持续运营的关键。

优点与成就:

极致的同步体验: 在多人竞技游戏中,几乎没有什么比《守望先锋》的同步表现更令人印象深刻的了。即使在网络状况并非最优的情况下,玩家也能感受到相对公平和流畅的对抗。
强大的扩展性: 游戏上线四年多,不断推出新英雄、新地图、新模式,并且每一次更新都能保持相对稳定的服务器运行,这离不开其模块化、数据驱动的良好架构。
高容错性: 即使有玩家的网络出现短暂波动,游戏也能通过延迟补偿、预测等机制,尽量降低其对整个对局体验的影响。
易于维护与迭代: ECS、数据驱动等设计模式,使得团队能够高效地进行内容更新和平衡性调整。

挑战与潜在的改进空间:

对网络质量的依赖: 尽管有诸多优化,但确定性同步机制对网络的稳定性和延迟仍然有较高的要求。在极端网络环境下,玩家仍然会遇到“不同步”的问题。
开发复杂度: 确定性同步等技术的实现,极大地增加了开发的复杂度和调试难度。一个细微的 bug 可能导致全服务器的游戏逻辑出错。
对硬件的性能要求: 为了保证确定性计算的准确性,客户端需要具备一定的性能基础,并且游戏引擎在优化方面也需要不断投入。
作弊问题: 尽管权威服务器机制极大地遏制了作弊,但针对客户端的内存读取、外挂等依然是游戏公司需要持续对抗的难题。

总结:

《守望先锋》的架构设计,是现代在线多人竞技游戏工程实践的典范。它并非一味追求最前沿的技术,而是将玩家体验置于核心地位,围绕着“低延迟、高同步、易于迭代”的目标,巧妙地融合了确定性网络同步、客户端预测、ECS、数据驱动等多种先进的设计理念和技术。 Blizzard 团队在这些技术上的深耕和打磨,最终铸就了《守望先锋》在玩家体验上的卓越表现。 它的成功,不仅在于创意,更在于其背后强大的工程能力,以及对细节的极致追求。 即使面对网络等客观限制,《守望先锋》的架构依然展现了游戏工程师们如何在严苛的条件下,为玩家构建一个“尽在掌握”的虚拟世界。

网友意见

user avatar

@猴与花果山童鞋已经阐述了ECS的主要概念。此文主要从技术和工程角度简单探讨游戏行业中设计模式的演变历史和ECS的意义。

综述

设计模式产生的动机,往往源于尝试解决一些存在的问题。游戏开发领域技术架构的宏观目标,大体包括以下目标:

  • 适合快速迭代。无论是上线前的敏捷开发流程还是上线后根据市场反馈的调整,迭代都是基本需求。
  • 易于保证产品质量。良好的架构,可以降低 Bug 和 Crash 出现概率。
  • 开发效率。重要性不必多说。即使是更重视游戏质量的公司,越高的开发效率也有助于更短的时间打造出质量更高的游戏。
  • 运行效率。大部分游戏对实时响应和运行流畅度都有很高的要求,同时游戏中又存在大量吃性能的模块(比如渲染、物理、AI等)。
  • 协作扩展性。能够在开发团队扩张时尽可能无痛,同时方便支持美术、策划、音效等非程序同事的开发需求。


现代 Entity Component System 的概念,以及对游戏开发领域的意义:

  • Entity:代表游戏中的实体,是 Component 的容器。本身并无数据和逻辑。
  • Component:代表实体“有什么”,一个或多个 Component 组成了游戏中的逻辑实体。只有数据,不涉及逻辑。
  • System:对 Component 集中进行逻辑操作的部分。一个 System 可以操作一类或多类 Component。同一个 Component 在不同的 System 中,可以关联不同的逻辑。

ECS 并非《守望先锋》所独有和原创,事实上近年来以 ECS 为基础架构逐渐成为国际游戏开发领域的主流趋势。

采用 ECS 的范式进行开发,思路上跟传统的开发模式有较大的差别:

  • Entity 是个抽象的概念,并不直接映射为具体的事物:比如可以不存在 Player 类,而是由多个相关 Component 所组成的 Entity 代表了 Player。如 Entity { PositionComponent, RenderComponent, StateMachineComponent, ... } 等等。
  • 行为通过对 Component 实施操作来表达。比如简单重力系统的实现,可以遍历所有 Position Component 实施位移,而不是遍历所有 玩家、怪物、场景物件,或者它们统一的基类。
  • 剥离数据和行为,数据存储于 Component 中,而 Component 的相关行为,和涉及多个 Component 的交互和耦合,由 System 进行实施。

ECS 框架,至少有以下优点:

  • 模式简单。如果还是觉得复杂,推荐看看 GoF 的《设计模式》。
  • 概念统一。不再需要庞大臃肿的 OOP 继承体系和大量中间抽象,有助于迅速把握系统全貌。同时,统一的概念也有利于实现**数据驱动**(后面会提到)。
  • 结构清晰。Component 即数据,System 即行为。Component 扁平的表达有助于实现 Component 间的正交。而封装数据和行为的做法,不仔细设计就会导致 Component 越来越臃肿。
  • 容易组合,高度复用。Component 具有高度可插拔、可复用的特性。而 System 主要关心的是 Component 而不是 Entity,通过 Component 来组装新的 Entity,对 System 来说是无痛的。
  • 扩展性强。增加 Component 和 System,不需要对原有代码框架进行改动。
  • 利于实现面向数据编程(DOP)。对于游戏开发领域来说,面向数据编程是个很重要的思路。天然亲和数据驱动的开发模式,有助于实现以编辑器为核心的工作流程。
  • 性能更好优化。接上条,相比 OOP 来说,DOP 有更大的性能优化空间。(详见后面章节)

若要了解为何会出现 ECS 这样的模式,以及它所试图解决的问题,需要考虑一下历史进程:

演化路径

简单粗暴的上个世纪开发模式

注重于实现相关算法、功能和逻辑,代码只要能实现功能就行,怎么直观怎么来。比如

       class Player {     int hp;     Model* model;     void move();     void attack(); };      


类似这样完全没有或很少架构设计的代码,在项目规模增大后,很快变得臃肿、难以扩展和维护。

OOP 设计模式的泛滥 案例:OGRE

       设计模式是语言表达能力不足的产物。 —— 某程序员     

那么,作为他山之石,GoF 基于 Java 提出的设计模式,能否有效解决游戏开发领域的问题?

大家还记得当年国内风靡一时的游戏引擎 OGRE 么?

OGRE中用到的设计模式 - 逍遥剑客 - 博客频道 - CSDN.NET

@逍遥剑客

OGRE 总有那么些学院派的味道,试图通过设计模式的广泛使用,来提高代码的可维护性和可扩展性。

然而,个人对游戏开发领域大规模使用 OOP 设计模式的看法:

  • 设计模式的六大原则大部分仍值得遵循。
  • 基于 Java 实现的设计模式,未必适合其它语言和领域。想想 C# 的 event、delegate、lambda 可以简化或者消除多少种 GoF 的模式,再想想 Golang 的隐式接口。
  • C++ 是游戏开发领域最主要的语言,可以 OOP 但并不那么 OO,比如缺少语言层面纯粹的 interface,也缺少 GC、反射等特性。照抄 Java 的设计模式未免有些东施尿频,而且难以实现 C++ 所推崇的零代价抽象。(template 笑而不语)
  • 局部使用 OOP 设计模式来实现模块,并暴露简单接口,是可以起到提升代码质量和逼格的效果。然而在架构层面滥用,往往只是把逻辑中的复杂度转移到架构复杂度上。
  • 滥用设计模式导致的复杂架构,并不对可读性和可维护性有帮助。比如原本 c style 只要一个文件顺序读下来就能了解清楚的模块,滥用设计模式的 OOP 实现,阅读代码时有可能需要在十几个文件中来回跳转,还需要人脑去正确保证阅读代码的上下文...
  • 过多的抽象导致过多的中间层次,却只是把耦合一层一层传递。直到最后结合反射 + IoC框架 + 数据驱动,才算有了靠谱的解决方案。然而一提到反射,C++表示我的蛋蛋有点疼。

那么,有没有办法简化和沉淀出游戏开发领域较通用的模式?

未脱离 OO 思想的 Entity Component 模式 案例:Unity3D

Unity3D 是个使用了 Entity Component 模式的成功的商业引擎。

相信使用过 Unity3D 的童鞋,都知道 Unity3D 的 Entity Component 模式是怎么回事。(在Unity3D 中,Entity 叫 GameObject)。

其优点:

  • 组件复用。体现了 ECS 的基本思想之一,Entity 由 Component 组成,而不是具体逻辑对象。设计得好的 Component 是可以高度复用的。
  • 数据驱动。场景创建、游戏实体创建,主要源于数据而不是硬编码。以此为基础,引擎实现了以编辑器为中心的开发模式。
  • 编辑器为中心。用户可在编辑器中可视化地编辑和配置 Entity 和 Component 的关系,修改属性和配置数据。在有成熟 Component 集合的情况下,新的关卡和玩法的开发,都可以完全不需要改动代码,由策划通过编辑器实现。

看起来,Unity3D 已经在很大程度上解决了游戏设计领域通用模式的问题。然而,其 Entity Component 模式仍然存在一些问题:Component 仍然延续了一些 OOP 的思路。比如:

  • Component 是数据和行为的封装。虽然此概念容易导致的问题可以通过其它方式避免,但以不加思考照着最自然的方式去做,往往会造成 Component 后期的膨胀。比如 Component 需要支持不同的行为就定义了不同的函数和相关变量;Component 之间有互相依赖的话逻辑该写在哪个 Component 中;多个 Component 逻辑上互相依赖之后,就难以实现单个 Component 级别的复用,最后的引用链有可能都涉及了代码库中大部分 Component 等等。
  • Component 是支持多态的引用语义。这意味着单个 Component 需要单独在堆上分配,难以实现下文所提到的,对同类型多个 Component 进行数据局部性友好的存储方式。这样的存储方式好处在于,批量处理可以减少 cache miss 和内存换页的情况。

当前主流的 Entity Component System 架构 案例:EntityX

那么,综合以上所说的各种问题,一个基于 C++ 的现代 Entity Component System,应该是什么样子?

具体案例,可以参考 [EntityX](github.com/alecthomas/e),一个开源的 C++ ECS 框架。

一一实现了前述现代 ECS 的各种概念:Entity 只是个 ID,Component 存储数据,System 实现关联多个 Component 的行为。

代码味道:


       struct Position {   Position(float x = 0.0f, float y = 0.0f) : x(x), y(y) {}    float x, y; };  struct Direction {   Direction(float x = 0.0f, float y = 0.0f) : x(x), y(y) {}    float x, y; }; struct MovementSystem : public System<MovementSystem> {   void update(entityx::EntityManager &es, entityx::EventManager &events, TimeDelta dt) override {     es.each<Position, Direction>([dt](Entity entity, Position &position, Direction &direction) {       position.x += direction.x * dt;       position.y += direction.y * dt;     });   }; };      


如上,实现了两类 Component:Position 和 Direction。

MovementSystem 只关心同时具有两类 Component 的 Entity。

一些值得说的特点:

  • 低抽象代价。C++ 的模板特性,便于把不少在其他语言中难以避免的运行时开销,转移到编译时。
  • 同类的多个 Component 实现了紧凑连续的内存布局。这个特性为什么重要?请参考[这个问题](zhihu.com/question/2027) @Milo Yip 的回答。同时这也是 Unity3D 的 Entity Component 模式难以做到的。当遍历同类 Component 时,数据存储于连续的内存空间中,可以大大提高缓存命中率。
  • Component 只有数据,行为是 System 的事。这样的模式,避免了上一节提到的 Unity3D 中容易出现的问题。Component 没有逻辑上的互相引用,Component 的耦合和依赖由 System 处理。此外,由 System 进行统一的状态修改,也有利于定位和隔离问题。
  • System 间的解耦,主要通过事件回调。System 之间不提倡互相引用,通过 Signal 来实现 publish / subscribe 进行处理。《守望先锋》也提到了关于 System 间发生了耦合的麻烦情况通常用 Singleton 模式和把共用代码放进 Utils 解决。

2017/07/27 追加

ECS 的进一步优化

除了可以提高缓存命中率外,新世代的 ECS 还可以通过分析数据依赖和读写关系,来实现 System 间的并行。比如更新时, System A 需要读 组件1,System B 需要读 组件1、写组件2,System C 需要写 组件1,那么调度时可以把 System A 和 System B 分配到不同线程处理,之后再处理 System C。原贴中也一笔带过提到了这方面的优化。然而对于复杂的 C++ 游戏来说,这个目标在实践上的可行性具有比较大的障碍:难以确保团队中的熊孩子不小心写出非线程安全的代码。

不过,Rust 给这个问题带来了解决方案。可以参考 Rust 实现的并行 ECS 框架:slide-rs/specs

Rust 的语言特性在编译期保证了线程安全,只需声明一下 System 对 Component 的访问权限如:

           type SystemData = (ReadStorage<'a, Velocity>,                        WriteStorage<'a, Position>);      

这样,即可安全地获得多线程带来的性能提升。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有