本文深入揭秘了《英雄联盟》的底层架构重构过程,拳头游戏为解决旧引擎的内存管理与性能瓶颈,大胆地在C++引擎中删除了原始指针,通过引入更安全的内存管理机制,他们有效避免了内存泄漏和崩溃风险,显著提升了游戏的稳定性与运行效率,这一技术革新展示了拳头游戏对底层架构优化的决心与实力。
在游戏开发领域,尤其是像《英雄联盟》这样拥有十多年历史、承载着庞大代码库的超大型项目,内存管理一直是核心痛点,拳头游戏在维护 LoL 的过程中,面临着严峻的挑战:数千万行代码中充斥着原始指针,导致内存泄漏、悬空指针和崩溃频发。
拳头游戏究竟是如何“删除”指针,从而提升引擎的稳定性和性能的?这并非简单的代码替换,而是一场从编程范式到内存管理策略的深刻变革。
拥抱现代 C++:从原始指针到智能指针
早期 LoL 的代码基于较老的 C++ 标准,大量使用 new 和 delete 以及原始指针,这种方式要求开发者手动管理生命周期,一旦逻辑复杂,极易出错。
拳头游戏在重构过程中,全面引入了 C++11 及后续标准中的 RAII(资源获取即初始化) 机制,核心武器就是智能指针。
-
std::unique_ptr的普及: 这是拳头游戏用来“删除”原始指针的主力,对于具有独占所有权的对象(比如一个技能特效对象),开发者不再手动delete,而是将其封装在unique_ptr中,当指针离开作用域或对象被销毁时,内存会自动释放,这从根本上杜绝了忘记释放内存导致的泄漏。 -
std::shared_ptr与std::weak_ptr: 对于复杂的引用关系(比如多个单位同时指向一个全局 Buff 对象),拳头游戏使用了引用计数指针,为了解决循环引用导致的内存泄漏,他们配合使用了weak_ptr来打破引用环。
通过这种方式,拳头游戏在代码逻辑层面“删除”了绝大多数手动管理内存的 delete 操作,将内存管理的责任交给了编译器和标准库。
引入 ECS 架构:用 ID 取代指针
在游戏引擎的核心循环中,原始指针还有一个致命弱点:不稳定,如果对象在内存中被移动或重新分配,原本指向它的指针就会变成“悬空指针”,引发程序崩溃。
为了解决这个问题,拳头游戏在部分新系统(尤其是为了支持云游戏和新的特效系统)中,逐步转向了 ECS(实体-组件-系统) 架构。
在 ECS 模式下,对象不再通过直接的内存指针相互引用,而是通过 ID(通常是一个整数或句柄 Handle) 来访问。
- 如何“删除”指针:系统维护一个 ID 到实际内存位置的映射表,当组件 A 需要访问组件 B 时,它持有一个 ID,而非指针,访问时,通过 ID 查询当前的内存地址。
- 优势:即使对象在内存中搬家了,ID 依然有效,彻底消除了悬空指针的风险。
自定义内存分配器:告别 malloc 和 free
虽然智能指针解决了“何时释放”的问题,但在高性能游戏循环中,频繁调用底层的 new 和 delete 会造成内存碎片和性能瓶颈。
拳头游戏并没有完全依赖操作系统默认的内存管理,而是开发了自定义的内存分配器和对象池。
-
Arena / Region Allocators(竞技场分配器): 对于一帧内生成的临时对象(比如路径查找的临时数据),拳头游戏使用竞技场分配器,它们不是一个个被删除,而是当这一帧结束时,整个竞技场的内存被一次性“重置”或“清空”,这实际上是对大量微小指针删除操作的一种极端优化——不删单个,只清整体。
-
对象池: 对于像小兵、子弹这种频繁创建销毁的对象,拳头游戏预先申请一大块内存,当对象“死亡”时,并没有真正执行
delete,而是回收到池子中等待复用。“删除指针”的操作变成了“标记为空闲”。
拳头游戏“删除”指针的过程,本质上是一场抽象层级的提升。
他们并没有真的让 C++ 不支持指针,而是通过智能指针(自动化生命周期)、ECS 架构(用句柄解耦引用)以及自定义内存池(优化释放策略),将开发者从繁琐且危险的底层内存操作中解放出来。
这一系列举措不仅大幅降低了《英雄联盟》的崩溃率,延长了这款老游戏的寿命,也为后续开发更复杂的游戏功能(如全新的引擎工具和云游戏支持)打下了坚实的地基,对于任何 C++ LoL 的重构史都是一部关于如何优雅地管理内存的教科书。
