中EN
综合页

游戏编程模式选型:解耦与性能

Choosing Game Programming Patterns

本页把《Game Programming Patterns》里的 19 个模式放到同一张取舍图上:大多数模式要么为了解耦(降低改代码时需要装进脑子里的知识量),要么为了性能(利用对数据和硬件的具体假设),而这两个目标经常互相拉扯。下面先讲全书的定位和 Nystrom 的架构观,再给出按问题选模式的路线和与 UE 的对照。

全书定位(Game Programming Patterns — introduction)

  • 动机:作者 2001 年进入 EA,发现图形、AI、动画等代码很出色,但承载它们的架构常常是事后补的,模块间耦合严重;同时代码库里埋着好方案,同事却在重新发明。本书就是把这些模式挖出来、打磨好,让大家把时间花在发明而不是重新发明上。
  • 单点式 (à la carte):领域专著很少讲那块代码如何与游戏其余部分衔接,整引擎书籍又过于整体、绑定特定类型(多为 3D 第一人称射击);本书每章是一个可以独立应用、自由组合的想法。
  • 结构:全书三部分:引言两章;“重访设计模式”,用游戏视角重讲 GoF 中的 6 个(命令、享元、观察者、原型、单例、状态);以及 13 个新模式,分为序列 (Sequencing)、行为 (Behavioral)、解耦 (Decoupling)、优化 (Optimization) 四类。每章按意图、动机、模式、何时使用、注意事项、示例代码、设计决策、参见的固定结构写。
  • 为什么特别适合游戏:时间与顺序常是架构核心;开发周期极度压缩,多人要快速迭代大量行为而互不踩脚;行为之间会大量交互(怪物咬英雄、药水混合、炸弹不分敌我),不能让代码变成一团乱麻;性能至关重要。
  • 示例代码:用 C++ 是因为它是商业游戏的通用语,C 系语法也易读;但示例刻意不用 C++11 以后的风格、不用标准库、很少用模板,“读代码表达的想法,而不是表达想法的代码”。本知识库各模式页的代码草图因此也按书中原样的风格整理。

架构是为了“改代码”

Nystrom 的定义:好的设计意味着做修改时,整个程序仿佛就是为这次修改准备好的。架构的价值只在代码被修改时才体现。修改代码的流程是“理解问题 → 学习相关代码 → 写出解决方案 → 清理”,其中最慢的往往是把相关代码装进脑子。两段代码耦合,意思是不理解其中一段就无法理解另一段;解耦之后可以只理解其中一边。因此架构的关键目标是:尽量减少动手之前需要装进脑子的知识量。解耦的另一个定义是:改一段代码不必连带改另一段,耦合越少,修改波及越小。清理阶段同样重要:做得好的话,下一个人看不出哪行代码是什么时候写的;维护设计就像园艺,不仅要种,还要除草修剪,否则“就加一个小 hack”会让漂亮的程序死于千刀万剐。(Game Programming Patterns — architecture-performance-and-games)

解耦的代价与性能的代价

  • 抽象是对未来的投机:每加一层抽象或扩展点,都是赌以后会用到这份灵活性;赌错了,它就变成要维护的额外代码。过度热情会导致到处是接口、插件系统和虚函数,找一段真正干活的代码要穿过层层脚手架,抽象层本身反而塞满了脑子;还有人花多年做“引擎”,却始终没想清楚它是做什么用的引擎。对抗这种投机冲动的口号是 YAGNI(You aren't gonna need it)。(Game Programming Patterns — architecture-performance-and-games)

  • 性能靠的是假设:“敌人不会超过 256 个”就能把 ID 压进一个字节;“这里只会调用一种具体类型”就能静态分派或内联;“所有实体同一个类”就能放进连续数组。而解耦恰恰是减少假设:接口、观察者、消息系统都让代码适配更多情况,代价是虚分派、指针和消息这些运行时开销。一个有趣的反例是 C++ 模板:调用具体类是在编写时固定,虚函数或接口推迟到运行时,模板元编程则介于两者之间,在编译时决定,有时能提供接口式抽象而没有运行时开销。(Game Programming Patterns — architecture-performance-and-games)

  • 灵活性对做出好玩的游戏仍然必要:没人(包括 Will Wright)能在纸上设计出平衡的游戏,必须反复试验,找到机制后还要大量调优。Nystrom 的经验是“让一个好玩的游戏变快,比让一个快的游戏变好玩更容易”。一种折中是:设计稳定之前保持代码灵活,之后再拆掉部分抽象来换性能。(Game Programming Patterns — architecture-performance-and-games)

  • 烂代码也有它的时间和地点:原型(只为回答一个设计问题、勉强能跑的代码)是完全正当的做法,但写了要扔的代码就必须真的能扔。作者描述过常见的坏经理套路:“只是原型”,几天后变成“花几个小时清理一下就当正式代码吧”。一个技巧是用与游戏不同的语言写原型,这样它进游戏前必须重写。(Game Programming Patterns — architecture-performance-and-games)

三股力量互相牵制:长期可理解的架构、运行时性能、今天就把功能做完。作者指出它们都是某种“速度”:长期开发速度、运行速度、短期开发速度。好架构要求每次修改都多花一点力气保持整洁;最快写完的实现很少是跑得最快的实现,优化要花大量工程时间,完成后代码会固化、难以再改;而只顾塞功能,代码库会变成一堆 hack 和 bug,拖垮以后的效率。没有正确答案,只有不同口味的错误;但作者认为这正是有趣之处,就像国际象棋因为各子完美平衡而永远无法穷尽,设计糟糕的游戏才会坍缩成一招鲜。(Game Programming Patterns — architecture-performance-and-games)

下图把 19 个模式按主要目标放在这条光谱上:越靠左越偏解耦和灵活,越靠右越偏性能。

flowchart LR
  subgraph "解耦与灵活"
    OBS["观察者"] --- EQ["事件队列"]
    CMD["命令"] --- SL["服务定位器"]
    CMP["组件"] --- TO["类型对象"]
    BC["字节码"] --- SS["子类沙盒"]
    PRO["原型"] --- STA["状态"]
  end
  subgraph "时序与结构"
    GL["游戏循环 + 更新方法"] --- DB["双缓冲"]
  end
  subgraph "性能"
    DL["数据局部性"] --- OP["对象池"]
    DF["脏标记"] --- SP["空间分区"]
    FLY["享元"]
  end

注意并没有严格的界线:组件模式本是为了解耦,却恰好让数据局部性优化更容易做(按领域切开的组件天然适合按类型连续存放);享元既省内存,又保留了面向对象的接口。

按问题选模式

遇到的问题首选备选或配合主要代价
A 要通知 B,但 A 不该知道 B观察者模式想在时间上也解耦时用 事件队列运行时才能确定的通信关系难以推理;要严格注销
把“触发动作”与“执行动作”分开,要撤销、重放、改键命令模式排队执行时配合 事件队列命令类数量多
发送方是推、接收方是拉,节奏不同事件队列—全局可见;世界状态可能已变;反馈回环
很多代码都要用某个系统(日志、音频)先把对象传进去从基类取(子类沙盒)、挂在已有全局对象上、单例与服务定位器全局访问都会带来耦合
一个类横跨输入、物理、渲染等多个领域组件模式性能敏感时与 数据局部性 结合(实体即 ID)装配与通信复杂
很多“种类”,希望设计师不改代码就能加类型对象数据条目间复用用原型委托(原型模式);行为也要数据化时用 字节码模式很难按类型定义行为
大量派生类共享一组底层操作子类沙盒基类过胖时拆成 组件模式脆弱基类
实体行为随内部状态变化,标志位越来越乱状态模式与有限状态机复杂 AI 用行为树或规划系统(书中所提);UE 中对应 StateTreeFSM 表达力有限
让游戏时间与机器速度无关;许多对象同时模拟游戏循环与更新方法读写要一致时用 双缓冲更新顺序与列表修改的坑
每帧遍历大量对象很慢,并且分析显示是缓存未命中数据局部性对象池 让同类对象连续存放放弃部分抽象
频繁创建和销毁同类对象,分配慢或产生碎片对象池—上限与溢出策略;复用时要完整初始化
派生数据计算昂贵,而原始数据频繁变化脏标记能增量更新时直接维护累加值漏置标记
大量相同数据重复存储或传输享元模式在 GPU 上就是实例化渲染间接访问(要先分析)
按位置查找对象太慢空间分区静态几何用自适应结构,动态对象用网格或四叉树移动时要更新;额外内存
有个类总要全局唯一重新审视 单例与服务定位器用静态标志在运行时断言单实例单例就是全局变量

Nystrom 的五条建议(Game Programming Patterns — architecture-performance-and-games)

  1. 抽象和解耦让程序演进更快更容易,但除非确信这段代码需要那份灵活性,否则别在上面花时间。
  2. 在整个开发周期里思考并设计性能,但把那些会把假设锁死在代码里的底层优化尽量往后推。
  3. 快速探索游戏的设计空间,但别快到留下一地烂摊子,你还得在里面住。
  4. 确定要扔掉的代码,就别花时间把它写漂亮(“摇滚明星砸酒店房间,是因为知道第二天就退房”)。结合前文:原型代码必须真的能扔掉;只要有可能被留下来,就得防御性地写好。
  5. 最重要的是:想做出好玩的东西,就享受做它的过程。

他还说,如果有什么方法能缓解上述种种约束,那就是简单:写最干净、最直接的解法,读完后让人想不出别的写法;先把数据结构和算法做对(大致按这个顺序),简单的代码更少、更容易装进脑子,也常常因为开销少而更快。但简单的代码并不更快写出来,好的解决方案不是代码的堆积,而是提炼(作者说每修订一章,它就变短一些,有的章节最终压缩了 20%)。问题通常以一堆用例的形式出现,新手会逐个用条件分支实现,遇到稍有不同的输入就崩;优雅的解法是通用的:用一小段逻辑正确覆盖一大片用例,找到它像解谜一样需要看穿零散用例背后的秩序。(Game Programming Patterns — architecture-performance-and-games)

对照 UE:引擎已经替你选好的模式

UE 作为一个成熟引擎,已经内置了其中许多模式的“官方答案”。以下对应均来自本知识库的 UE 页面(详见各模式页的“UE 中的对应”一节):

模式UE 中的对应
游戏循环 + 更新方法引擎拥有主循环;Tick、Tick 组与 Tick 依赖(委托与 Tick 机制)
观察者多播委托与动态多播(同一页)
组件Actor 与 Actor 组件(Gameplay 框架);Game Feature 用 Add Components 注入组件
组件 + 数据局部性MassEntity:实体即 ID、原型与内存块(MassEntity 数据导向框架)
单例 / 服务定位器生命周期绑定宿主的子系统(子系统)
状态StateTree、动画蓝图状态机(行为树与 StateTree、动画蓝图与动画系统)
字节码蓝图编译为字节码在虚拟机执行(蓝图与 C++ 协作)
类型对象Data Asset、Gameplay Effect 资产与 Spec(资产管理与异步加载、Gameplay 能力系统)
原型类默认对象 CDO 与默认值传播(UObject 与反射系统)
命令增强输入把输入动作与按键映射分开(增强输入)
脏标记MarkRenderStateDirty()(UE5 渲染架构)
空间分区世界分区网格、复制图空间网格(世界分区、复制图)

UE 官方在多处给出的取舍,与 Nystrom 的观点一致:能用事件或定时器就不用 Tick(轮询);蓝图适合快速迭代行为,C++ 适合性能关键的系统;同质对象数量巨大时才换到数据导向的 Mass。在 UE 里自己再实现一套这些模式之前,先确认引擎的现成机制是否已经足够。

  1. Game Programming Patterns — architecture-performance-and-games
  2. Game Programming Patterns — introduction
  3. Game Programming Patterns — architecture-performance-and-games(全文版)
  4. Game Programming Patterns — introduction(全文版)
  5. Robert Nystrom《Game Programming Patterns》(2014) introduction 章,gameprogrammingpatterns.com/introduction.html
  6. Robert Nystrom《Game Programming Patterns》(2014) architecture-performance-and-games 章,gameprogrammingpatterns.com/architecture-performance-and-games.html

相关页面

游戏开发命令模式Command Pattern命令模式 (Command Pattern) 把一次方法调用“物化”成对象,从而可以存储、传递、延迟执行、排队或撤销。游戏里最典型的三种用法是:可配置的按键映射、让玩家输入和 AI 共用同一套“驱动角色”的接口、以及撤销/重做。游戏开发享元模式Flyweight Pattern享元模式 (Flyweight Pattern) 把对象的数据拆成可共享的“内在状态”和每个实例独有的“外在状态”,让大量对象共用一份内在状态。森林里成千上万棵树共用一份网格和纹理,以及瓦片地图中每格只存一个指向共享地形对象的指针,都是它的典型应用。游戏开发观察者模式Observer Pattern观察者模式 (Observer Pattern) 让一段代码宣布“发生了某件事”,而不必关心谁会收到通知:主体 (Subject) 维护一个观察者列表,在事件发生时逐个同步调用它们。它适合连接彼此几乎无关的子系统(例如物理与成就),但要注意同步阻塞、观察者的注销,以及运行时才能确定的通信关系难以推理。游戏开发原型模式Prototype Pattern原型模式 (Prototype Pattern) 的核心思想是“一个对象可以生成与自己相似的对象”:生成器持有一个原型实例,通过 clone() 复制出类和状态都相同的新对象。Nystrom 认为这个设计模式本身在现代游戏中很少是最佳答案,更有价值的是“原型委托 (delegation)”用于数据建模,让数据条目继承另一条目的属性,减少重复。游戏开发单例与服务定位器Singleton & Service Locator单例 (Singleton) 同时承诺两件事:一个类只有一个实例,并且提供全局访问点。Nystrom 认为它在游戏里弊大于利,因为单例本质上就是全局变量。服务定位器 (Service Locator) 是更灵活的“近亲”:它同样给出全局访问点,但调用者只依赖抽象服务接口,具体实现由定位器在运行时或编译期决定,还可以退化为空服务。两者都应当慎用,首选仍是直接把对象传进去。游戏开发状态模式与有限状态机State Pattern & Finite State Machines状态模式 (State Pattern) 把对象“依赖状态的行为”分散到各个状态类中,对象只持有指向当前状态的指针并把调用委托给它,状态切换就是换指针。它是实现有限状态机 (Finite State Machine, FSM) 的一种方式。FSM 能用很强的结构约束理清一团乱麻的标志位代码;状态组合爆炸、需要复用和需要“回到之前状态”时,分别用并发状态机、分层状态机和下推自动机来扩展。游戏开发双缓冲Double Buffer双缓冲 (Double Buffer) 让一系列按顺序执行的修改,在外界看来像是瞬间、同时完成的:类内部保留“当前缓冲”和“下一缓冲”两份状态,读取总是读当前缓冲,写入总是写下一缓冲,改完后用一次原子交换让新状态整体生效。图形里的前后缓冲是最经典的例子;同样的思路也能让一帧内依次更新的游戏对象互相看到的都是上一帧的一致状态。游戏开发游戏循环与更新方法Game Loop & Update Method游戏循环 (Game Loop) 让游戏时间的推进与玩家输入和处理器速度脱钩:每一轮非阻塞地处理输入、更新状态、渲染,并跟踪真实时间来控制节奏;最常推荐的做法是“固定更新步长、可变渲染”。更新方法 (Update Method) 是它的搭档:世界维护一组对象,每个对象实现一个模拟“一帧”行为的 update(),每帧依次调用。两者和组件模式一起常被视为游戏引擎的核心。游戏开发字节码模式Bytecode Pattern字节码模式 (Bytecode Pattern) 把行为编码成虚拟机 (VM) 能执行的指令序列,让行为拥有数据一样的灵活性:可以热加载、与引擎核心隔离(沙盒化),还可以由非程序员编写。它是全书最复杂的模式,代价是要写编译器或创作工具,调试器也用不上了,而且这类小语言容易无节制地膨胀。游戏开发子类沙盒Subclass Sandbox子类沙盒 (Subclass Sandbox) 让基类提供一组受保护的“提供的操作”和一个抽象的沙盒方法,派生类只用这些操作来实现沙盒方法。这样大量派生类(例如几十种超能力)只耦合到基类,不直接接触引擎的各个系统;代价是基类会越来越臃肿,变成脆弱基类。游戏开发类型对象Type Object类型对象 (Type Object) 用“一个类的不同实例”来表示不同的逻辑类型:例如不为每种怪物写一个子类,而是一个 Monster 类加一个 Breed(种族)类,每个 Breed 实例描述一种怪物,怪物只持有指向自己 Breed 的引用。这样新增或修改种类只需要改数据,不必重新编译;代价是要自己管理这些类型对象,而且给每种类型定义不同的“行为”会变难。游戏开发组件模式Component Pattern组件模式 (Component Pattern) 让一个实体横跨多个领域(输入、物理、图形、AI……),同时不让这些领域彼此耦合:每个领域的代码放进自己的组件类,实体本身退化为组件的容器。它是现代引擎实体模型的基础,代价是对象变成需要正确装配的一组对象,组件之间如何通信也成了要做的设计决策。游戏开发事件队列Event Queue事件队列 (Event Queue) 用一个先进先出队列,把“发送消息或事件”和“处理它”在时间上解耦:发送方入队后立即返回,接收方在自己方便的时候再取出处理。它比观察者和命令模式多做的一件事是时间上的解耦,适合“推”与“拉”两端节奏不同的场合(例如音频请求),代价是全局可见、世界状态可能已经改变,以及可能出现隐蔽的反馈回环。游戏开发数据局部性Data Locality数据局部性 (Data Locality) 是一种优化思路:CPU 读取内存时会把相邻的一整条缓存行一起载入,所以把数据按处理顺序连续存放,就能大幅减少缓存未命中。具体手段包括:组件按类型存进连续数组,不再到处跟随指针;把活跃对象排在前面;把冷热数据拆开。代价是要放弃一部分继承、接口与指针带来的抽象。游戏开发脏标记Dirty Flag脏标记 (Dirty Flag) 用一个布尔标记记录“派生数据是否已经过时”:原始数据变化时置位;需要派生数据时,如果标记已置位就重新计算并清除标记,否则直接用缓存的结果。场景图里由局部变换求世界变换是经典例子。它只在原始数据变化比派生数据被使用更频繁、而且难以增量更新时才划算,最大的风险是漏置标记导致使用过时数据。游戏开发对象池Object Pool对象池 (Object Pool) 预先一次性分配一组可复用对象,需要时从池里取一个未使用的对象并初始化,用完标记为未使用,而不是逐个 new/delete。它能避免频繁分配的开销和堆碎片,常用于粒子、子弹、正在播放的声音等。代价是池大小要调校、同时活跃的数量有上限、复用的对象不会被自动清零,在有 GC 的环境里还可能让本该回收的对象一直活着。游戏开发空间分区Spatial Partition空间分区 (Spatial Partition) 把对象按位置存进空间数据结构,使“找出某位置附近的对象”这类查询不必遍历所有对象,把 O(n) 或 O(n^2) 的操作降下来。最简单的是固定网格;还可以选层次结构(四叉树、BSP、k-d 树、BVH),以及边界固定或随对象自适应的划分方式。代价是对象移动时要更新结构,并且要额外占用内存。游戏开发多线程渲染与任务系统Multithreaded Rendering & Task SystemsCPU 时钟频率约在 2003 年停在 3.4 GHz 附近后,性能来自多核。应用阶段的优化一是让代码与内存访问更缓存友好,二是利用多核:多处理器流水线(时间并行,提高吞吐但增加延迟)、并行处理(空间并行,延迟更低)以及两者结合的任务系统;图形 API 用命令缓冲让多个线程并行录制绘制命令,新一代 API(DirectX 12、Vulkan、Metal)把更多责任交给应用以降低驱动开销。游戏开发运行时引擎分层架构Runtime Engine ArchitectureGregory 把游戏视为“软实时、交互式、基于代理的计算机模拟”,把游戏引擎定义为数据驱动、可复用来做多款游戏的软件。运行时引擎按层组织:从目标硬件、驱动、操作系统、第三方 SDK、平台无关层、核心系统、资源管理器,到渲染、物理、动画、音频、网络等子系统,再到游戏性基础层和游戏专属子系统;上层依赖下层,避免循环依赖。综合引擎运行时架构总览Engine Runtime Architecture Overview本页把 wiki 中分散在 GEA 第 1、6、8、17 章及 GPP、UE5 相关页面里的内容串成一条线,回答"一个游戏引擎在运行时是怎样组织起来的":静态结构上是自下而上的分层,上层依赖下层;启动关闭按依赖顺序显式进行;每一帧由主循环驱动各子系统批量更新,游戏对象的逻辑被安排在子系统阶段之间,并按依赖分桶或设先后关系;多核上一帧被拆成成百上千个作业;玩法基础系统(对象模型、事件、脚本、世界加载流送、高层流程)位于引擎与游戏的分界线附近。各结论的细节与出处见所链接的概念页。游戏策划游戏空间与关卡架构Game Space & Level Architecture第 12 章的功能空间只是骨架,本章讨论玩家实际体验到的有血有肉的空间。建筑的首要目的是控制人的体验,因此建筑师与游戏设计师是近亲:都造人要进入才能使用的结构,都只能靠间接控制引导体验,所造之物都只为让人快乐。内容包括五种空间组织方式、地标、Christopher Alexander 的"无名特质"与生命结构十五性质、虚拟建筑的特殊性(尺度、第三人称畸变),以及"关卡设计就是细节层面的游戏设计"(第 3 版第 21 章 Worlds Contain Spaces;第 1 版为第 19 章)。游戏策划机制设计模式Mechanics Design PatternsAdams & Dormans 把游戏机制结构中反复出现的模式称为设计模式,用 Machinations 图描述,组成一套以"内部经济结构→涌现玩法"为质量观的模式语言 (Pattern Language)。第 7 章介绍历史与用法,附录 B 按 GoF 格式详述 16 个模式:引擎 (Engines) 4 个、摩擦 (Friction) 4 个、升级 (Escalation) 3 个、杂项 5 个。模式之间存在细化 (Elaboration) 关系:所有引擎都是源的细化、所有摩擦都是泄的细化,可借细化/简化调整设计的复杂度与焦点。游戏开发 · UE5Gameplay 框架Gameplay FrameworkGameplay 框架 (Gameplay Framework) 是 UE 提供的一组相互配合的类,用来搭建游戏的基本结构:Actor 与组件 (Component) 构成场景中的一切;GameMode 在服务器上定规则并生成玩家;玩家由“控制器 (Controller) + 棋子 (Pawn)”组成;GameState 与 PlayerState 负责把全局与每个玩家的状态复制给所有客户端;GameInstance 跨关卡存活。游戏开发 · UE5Gameplay 能力系统Gameplay Ability SystemGameplay 能力系统 (Gameplay Ability System, GAS) 是 UE 用来构建「Actor 可拥有并反复触发的能力」的框架:能力系统组件 (Ability System Component, ASC) 是持有者,Gameplay 能力 (Gameplay Ability) 编排执行过程,属性集 (Attribute Set) 存数值,Gameplay 效果 (Gameplay Effect, GE) 改数值和标签,Gameplay 标签 (Gameplay Tags) 做状态与互斥,Gameplay Cue 负责表现。它内建复制与客户端预测,代价是概念多、上手重,游戏开发 · UE5MassEntity 数据导向框架MassEntity FrameworkMassEntity 是 UE5 面向玩法的数据导向 (Data-Oriented) 计算框架,属于实体-组件-系统 (Entity Component System, ECS) 一类设计:实体 (Entity) 只是一个 ID 加上一组片段 (Fragment) 数据,相同组成的实体归入同一原型 (Archetype) 并按内存块 (Chunk) 连续存放,无状态的处理器 (Processor) 通过实体查询 (EntityQuery) 批量处理片段。它适合成千上万个同质对象(人群、交通),MassGameplay 插件在其上提供表现、生成、LOD、复制、StateTree 等功能。游戏开发 · UE5UE5 渲染架构UE5 Rendering ArchitectureUE5 的渲染器是独立的 Renderer 模块,在渲染线程 (Rendering Thread) 上以「场景代理」镜像游戏线程的组件状态,每帧按固定顺序执行深度预通道、G-buffer 基础通道、光照、雾、半透明和后处理。现代渲染代码用渲染依赖图 (Render Dependency Graph, RDG) 声明通道与资源,由 RDG 自动排屏障、复用瞬态内存、并行录制;最终命令经 RHI 线程翻译成 D3D12/Vulkan 等图形 API 调用。默认走延迟着色 (Deferred Shading),也可切换前向着色 (Forward Shading)。游戏开发 · UE5UObject 与反射系统UObject & Reflection System虚幻引擎 (Unreal Engine) 的对象基类是 UObject。在 C++ 类、属性、函数上加 UCLASS/UPROPERTY/UFUNCTION 等宏,再由构建前的预处理工具虚幻头文件工具 (Unreal Header Tool, UHT) 扫描头文件生成代码,引擎就能在运行时“看见”这些类型——这就是反射系统 (Reflection System)。垃圾回收、序列化、编辑器面板、蓝图、网络复制都建立在它之上。游戏开发 · UE5世界分区World Partition世界分区 (World Partition) 是 UE5 的自动数据管理与基于距离的关卡流送 (Level Streaming) 系统:整个世界存为一个持久关卡,按可配置的运行时网格划分为单元格 (Cell),由流送源 (Streaming Source,默认是玩家控制器) 按距离加载/卸载。它配合一 Actor 一文件 (One File Per Actor, OFPA) 支持多人协作编辑,用数据层 (Data Layers) 按玩法状态切换内容,用分层细节层次 (Hierarchical Level of Detail, HLOD) 在远处以代理网格显示未加载的单元格。游戏开发 · UE5动画蓝图与动画系统Animation Blueprint & Animation SystemUE5 角色动画以动画蓝图 (Animation Blueprint) 为核心:它每帧为骨骼网格体 (Skeletal Mesh) 计算最终姿势。动画蓝图内部用状态机 (State Machine) 组织“何时播什么”,用混合空间 (Blend Space) 按速度、方向等参数连续混合,用蒙太奇 (Animation Montage) 在插槽 (Slot) 中插播攻击、交互等一次性动作,用 Control Rig 做程序化骨骼控制(如贴地)。算出的骨骼姿势最终交给蒙皮 (Skinning) 驱动顶点。游戏开发 · UE5增强输入Enhanced Input增强输入 (Enhanced Input) 是 UE5 默认启用的输入插件,把输入拆成四层:输入动作 (Input Action) 表示“玩家想做什么”,输入映射上下文 (Input Mapping Context) 规定“哪些按键在什么情境下触发它”,输入修饰器 (Input Modifier) 预处理原始值,输入触发器 (Input Trigger) 判定是否触发。上下文可在运行时按优先级增删,从而用数据资产而非代码处理情境切换、按键冲突与改键。游戏开发 · UE5复制图Replication Graph复制图 (Replication Graph) 插件解决的是服务器 CPU 瓶颈:标准做法是让每个复制 Actor 对每个连接各自判断是否需要发送,玩家和 Actor 一多(Fortnite 大逃杀开局有 100 名玩家、约 5 万个复制 Actor)就撑不住。复制图由持久存在的节点组成,按空间网格、休眠、常驻相关等规则预先分组,并缓存可跨帧、跨连接共享的复制列表。游戏开发 · UE5委托与 Tick 机制Delegates & TickUE 有两种驱动游戏逻辑的方式:事件驱动——委托 (Delegate) 在某事发生时回调绑定的函数;轮询驱动——Tick 每帧(或按最小间隔)调用 Actor/组件的更新函数,并按 Tick 组 (Tick Group) 与物理模拟排序。官方建议能用定时器、委托就不用 Tick,必须 Tick 时用 Tick 组和 Tick 依赖保证顺序,避免“差一帧”问题。游戏开发 · UE5子系统Subsystems子系统 (Subsystem) 是自动实例化、生命周期绑定在宿主对象上的类:Engine、Editor、GameInstance、World、LocalPlayer 五种。宿主创建时自动创建并 Initialize(),宿主关闭时 Deinitialize(),之后无引用即被 GC。它替代了「继承 GameInstance 塞管理器」的老做法,自带按类型的蓝图节点和 Python 访问,特别适合插件。官方编程页只列四种,World 子系统依据 5.8 Python API 补充。游戏开发 · UE5蓝图与 C++ 协作Blueprint & C++ InteropEpic 官方建议:多数项目混用蓝图 (Blueprint) 与 C++,以 C++ 作为基础、在其上构建蓝图类。C++ 适合“编程”——定义系统;蓝图适合“脚本”——在已有系统上定义具体行为。两者通过反射说明符互相调用:C++ 用 BlueprintCallable/BlueprintReadWrite 等暴露给蓝图,用 BlueprintImplementableEvent/BlueprintNativeEvent 让蓝图实现或覆盖 C++ 声明的函数。游戏开发 · UE5行为树与 StateTreeBehavior Tree & StateTreeUE 的 AI 决策有两套工具:行为树 (Behavior Tree) + 黑板 (Blackboard) 是事件驱动的,条件写成装饰器 (Decorator),用 Simple Parallel、Services 和 Observer Aborts 代替传统并行节点;StateTree 是结合了行为树选择器和状态机转换的通用分层状态机,只在转换触发时按需选择状态,激活路径上的任务并发运行,通过属性绑定传递数据,也是 Mass 的行为层。优先级抢占多的战斗 AI 适合行为树,状态显式、分层或大规模的场景适合 StateTree。游戏开发 · UE5资产管理与异步加载Asset Management & Async LoadingUE 资产默认按引用自动加载:硬引用 (Hard Reference) 会让被引用资产随引用者一起加载,不加控制就可能在启动时加载全部内容。软引用 (Soft Reference,TSoftObjectPtr/FSoftObjectPath) 只存路径,配合流式管理器 (Streamable Manager) 异步按需加载;资产管理器 (Asset Manager) 进一步把内容划分为主资产 (Primary Asset) 和次资产 (Secondary Asset),用资产包 (Asset Bundle) 管理加载、卸载、烘焙分块与内存审计。

被这些页面引用1 篇

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原