2.1 引擎整体架构与模块系统 本节摘要:引擎不是一个单体程序,而是按模块拼装的联邦:核心对象系统管生灭,模块系统管加载,游戏框架管玩法角色分工。本节建立一张架构分层图,讲清 UObject、反射与垃圾回收这三块地基,后续所有章节的话题都能在图上找到位置。 为什么先学"生灭规则" 接手一个中型虚幻工程,最先让人迷惑的通常不是某个功能,而是两类事故:一个是明明还有变量指着的对象没了,另一个是明明没用到的资产打包时被拖了进去。两类事故指向同一个根源——你不了解引擎的对象生灭规则。虚幻引擎用统一的 UObject 体系管理所有游戏对象的诞生、引用与回收,用模块系统决定哪些代码与资产何时被装进内存。生灭规则不清,性能账就算不清:垃圾回收的尖峰、模块加载的卡顿、引用链拖出的显存开销,全都源于此。
本节摘要:引擎不是一个单体程序,而是按模块拼装的联邦:核心对象系统管生灭,模块系统管加载,游戏框架管玩法角色分工。本节建立一张架构分层图,讲清 UObject、反射与垃圾回收这三块地基,后续所有章节的话题都能在图上找到位置。
接手一个中型虚幻工程,最先让人迷惑的通常不是某个功能,而是两类事故:一个是明明还有变量指着的对象没了,另一个是明明没用到的资产打包时被拖了进去。两类事故指向同一个根源——你不了解引擎的对象生灭规则。虚幻引擎用统一的 UObject 体系管理所有游戏对象的诞生、引用与回收,用模块系统决定哪些代码与资产何时被装进内存。生灭规则不清,性能账就算不清:垃圾回收的尖峰、模块加载的卡顿、引用链拖出的显存开销,全都源于此。
本节在知识体系中的位置:它是第二章的骨架节,向上承接第一章"引擎是什么"的定性,向下为 2.2 渲染管线(谁生成渲染数据)与第三章双轨编程(蓝图和 C++ 都长在反射系统上)提供地基。读完后你再看任何系统,都应该习惯性问一句:它属于哪一层、生灭归谁管。
把引擎纵切成四层,从下往上依次是:核心层(内存管理、容器、数学库)、对象层(UObject、反射、垃圾回收、序列化)、模块层(按功能切分的编译与加载单元)、玩法框架层(GameMode、Controller、Pawn 等角色分工)。四层的职责边界决定了开销的记账位置:核心层几乎不直接向玩法收费,对象层的垃圾回收按帧分摊收费,模块层的加载在启动与热更新时集中收费,玩法框架层则是你自己代码的直接账单。
对象层值得单独展开。UObject 是引擎里几乎所有运行时对象的共同基类,它自带三件套:元数据(类型、属性信息,供编辑器与序列化读取)、引用跟踪(垃圾回收需要知道谁引用了谁)、网络序列化(属性同步的基础)。第三件套要到第八章才会显出分量,前两件从现在起无处不在。反射指引擎在运行时知道每个对象的类型与属性——C++ 原生没有这个能力,虚幻用宏加代码生成补上了:写在头文件里的 UCLASS、UPROPERTY、UFUNCTION 宏,经构建系统展开后生成反射数据。蓝图与 C++ 能互操作、细节面板能列出属性、资产能保存加载,全部拜反射所赐。
垃圾回收的规则一句话可以说清:从根集合出发,凡是被 UPROPERTY 标记且可达的对象存活,不可达的对象按周期回收。注意两个限定词——"UPROPERTY 标记"和"可达"。用裸指针指着某对象而不用 UPROPERTY 声明,回收器看不见这条引用,对象照样会被收走,于是出现"变量还在、对象没了"的经典事故;反之,两个对象用 UPROPERTY 互相引用形成环,回收器靠可达性而非引用计数判断,环不是问题。理解这一点,第三章讲智能指针时会有更深体会。
引擎与你的游戏代码都由模块组成。每个模块是一份独立的编译单元,带一份描述自身依赖与用法的构建规则文件。启动时,模块管理器按依赖顺序逐个加载:核心模块最先,渲染、物理等引擎模块随后,你的游戏模块最后。这份加载顺序解释了不少现象——为什么修改引擎代码后要重编很久(依赖链长),为什么插件能按需启停(模块粒度的装卸),为什么项目设置里的启用列表动一下就要重编(模块组合变了)。
给你的工程写 C++ 时,第一件事就是在构建规则文件里声明 Public 与 Private 依赖:Public 表示对外暴露、会传递给依赖你的模块,Private 表示只在模块内部使用。声明的每一条依赖最终都会变成链接的二进制与加载的时间。滥用 Public 依赖是工程膨胀的常见起点——编译时间随之失控,团队里每个人都在为别人的声明买单。

背景:你需要在场景里放一个会自己旋转的装饰物。这是理解对象层与玩法层协作的最小样本——不用关心玩法框架,只看一个 UObject 子类的完整生存期。
操作:在编辑器里通过类向导新建一个继承 Actor 的 C++ 类,命名为 SpinActor。生成后打开头文件,在类声明里写三个关键标记:类声明前加 UCLASS,一个公共属性前加 UPROPERTY 并注明可在编辑器编辑,一个函数前加 UFUNCTION 并声明为蓝图可调用。源文件里,构造函数创建一个静态网格体组件挂到根组件下;BeginPlay 里设置每帧 tick 生效;Tick 函数里让根组件绕竖直轴按 DeltaTime 旋转。编译通过后,在内容浏览器找到该类拖进关卡。
// SpinActor.h(节选):三个宏就是反射的三张门票 UCLASS() class MYPROJECT_API ASpinActor : public AActor { GENERATED_BODY() public: ASpinActor(); // UPROPERTY:登记进反射与垃圾回收系统,编辑器可见可改 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spin") float RotationSpeed = 45.0f; // UFUNCTION:暴露给蓝图与其他系统调用 UFUNCTION(BlueprintCallable, Category = "Spin") void SetRotationSpeed(float NewSpeed); protected: virtual void BeginPlay() override; virtual void Tick(float DeltaTime) override; UPROPERTY(VisibleAnywhere) UStaticMeshComponent* MeshComp; }; // SpinActor.cpp(节选) ASpinActor::ASpinActor() { PrimaryActorTick.bCanEverTick = true; MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh")); RootComponent = MeshComp; // 组件树:一切 Actor 从根组件开始拼装 } void ASpinActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 用帧间隔时间计算旋转量:保证不同帧率下转速一致 AddActorLocalRotation(FRotator(0.f, RotationSpeed * DeltaTime, 0.f)); } void ASpinActor::SetRotationSpeed(float NewSpeed) { RotationSpeed = FMath::Clamp(NewSpeed, 0.f, 360.f); // 输入防御 }
结果:关卡里的物体持续旋转,细节面板出现 RotationSpeed 属性可实时调整,蓝图里也能调用 SetRotationSpeed。
解读:短短一段代码踩遍了本节的所有概念——UCLASS 宏把类登记进反射系统,编辑器才能列出它的属性;UPROPERTY 让 RotationSpeed 进入垃圾回收的可见范围并被序列化保存;CreateDefaultSubobject 演示了组件拼装模型,Actor 是容器,功能都长在组件上;Tick 里的 DeltaTime 乘法是帧率无关性的最基本写法,也是全册所有逐帧逻辑的通用套路。变式:把旋转逻辑从 Tick 改到时间线上(Timeline),可以体验"逐帧逻辑"与"驱动动画"两种范式;若把 RotationSpeed 的 UPROPERTY 改成 Transient,属性将不再随关卡保存——序列化粒度的一次直观实验。
常见坑:在 Tick 里做重量级查询(比如每帧全场景射线检测)。架构上它合法,预算上它是尖峰制造机——高频逻辑应该降频或事件驱动,这是第七章 AI 与第八章同步的通用原则。