一、UE 核心框架整体分层
UE 的核心框架采用分层设计,底层屏蔽平台差异,上层提供游戏开发的通用能力,整体可分为 4 层:
| 层级 | 核心职责 | 关键组件 / 模块 |
|---|---|---|
| 平台抽象层 | 屏蔽 Windows/PS/Xbox/ 移动端等平台的底层差异,提供统一接口 | Core/Public/Platform(平台 API 封装)、RHIs(渲染硬件接口,如 DX12/Vulkan/Metal) |
| 核心系统层 | 引擎的基础支撑,提供内存管理、线程、反射、GC、序列化等核心能力 | UObject 体系、TaskGraph(线程任务)、Memory Manager、Serialization System |
| 引擎层 | 游戏开发的核心能力集,提供渲染、物理、音频、输入、资源管理等通用功能 | Renderer、PhysX、Audio Engine、Input System、Asset Manager |
| 游戏框架层 | 基于引擎层封装的游戏专属逻辑框架,提供开箱即用的游戏对象和规则 | World、Actor/Component、GameMode/GameState、PlayerController、UI 系统(UMG) |
核心设计原则
- 高内聚低耦合:每层仅依赖下一层,模块间通过接口通信,便于扩展和替换(如替换渲染后端、物理引擎);
- 可定制化:底层核心系统稳定,上层游戏框架可通过 C++/ 蓝图自定义;
- 多线程并行:核心逻辑拆分到不同线程,最大化利用多核 CPU。
二、核心线程模型(UE 框架的 "动力系统")
1. 核心线程模型 (The "Big Three" + Others)
UE 的架构是典型的 "主从式 + 任务图" 混合结构。
|---------------------------|------------------------------------------------------------------------|-----------------------------------------------------------------|
| 线程名称 | 核心职责 | 关键特性 (必须要背的) |
| 游戏线程 (Game Thread) | **大管家 / 权威:**处理 Gameplay 逻辑、Blueprint 虚拟机、动画更新(部分)、AI 决策、UI 布局、网络逻辑同步。 | 只有它能修改 Actor/UObject 的状态。 所有其他线程要修改游戏数据,必须"请求"游戏线程来做。 |
| 渲染线程 (Render Thread) | 翻译官:接收游戏线程的命令,生成渲染指令。处理可视性裁剪 (Culling)、LOD 计算。 | 游戏线程运行比渲染线程快 1-2 帧 。 不能直接访问游戏线程的 UObject,只能访问其镜像数据 (Proxy)。 |
| RHI 线程 (RHI Thread) | **驱动层:**Render Hardware Interface。负责将渲染线程的指令提交给显卡驱动 (DX12/Vulkan)。 | 用于并行渲染,减轻渲染线程压力,防止驱动层阻塞。 |
| 音频线程 (Audio Thread) | **调音师:**处理声音播放请求、混音、DSP 音效处理、3D 空间化计算。 | 独立于游戏帧率,保证声音不卡顿。 |
| 物理任务 (Async Physics) | 物理模拟: 基于 Chaos 引擎。计算刚体碰撞、布料模拟、破坏效果。 | UE5 支持"异步物理 Tick",允许物理模拟频率与游戏帧率解耦。 |
| 工作线程池(Worker Threads) | 打工人 通过 TaskGraph 系统动态调度的线程池。处理光照构建、资源流式加载、复杂的数学计算。 | 用完即走,高度并行。 |
2. 线程通信机制
UE 严禁跨线程直接访问数据(会崩溃或死锁),必须使用以下机制:
|-----------------|----------------------------------|---------------------------------------------------------------------------------|
| 通信方向 | 核心机制 | 代码/宏示例 |
| 游戏线程 → 渲染线程 | 渲染命令队列(Render Command Queue) | ENQUEUE_RENDER_COMMAND(...) 游戏线程把数据打包成包,扔进队列就不管了(非阻塞)。 |
| 任意线程 → 游戏线程 | 任务图系统 (TaskGraph System) | FFunctionGraphTask::CreateAndDispatchWhenReady(...) 让游戏线程在下一帧空闲时执行某个回调函数。 |
| 游戏线程 ↔ 物理线程 | 双缓冲数据 (Double Buffering) | 物理引擎计算时使用的是数据的副本。计算完成后,在同步点 (Sync Point) 将位置/旋转写回 Actor。 |
| 线程同步与互斥 | 锁与原子操作 | FScopeLock (作用域锁), FThreadSafeCounter (原子计数器)。 警告:在 Gameplay 逻辑中应尽量避免用锁,改用任务通知。 |
三、模块系统(Module System)------ UE 框架的 "组织单元"
UE 的所有功能都以模块(Module) 为单位组织,模块是代码编译、链接、加载的最小单元,也是框架扩展性的核心:
1. 模块的核心特性
- 模块化封装 :每个模块对应一个功能域(如
Core、Engine、RenderCore、UMG),内部代码高内聚; - 依赖管理 :模块间通过
Build.cs声明依赖(公有 / 私有),引擎自动处理加载顺序; - 动态加载:支持运行时动态加载 / 卸载模块(如插件模块),降低内存占用;
- 跨平台编译 :通过
Target.cs配置不同平台的编译选项、宏定义、链接库。
2. 核心模块组成(UE 必选模块)
| 核心模块 | 职责 |
|---|---|
Core |
最底层核心模块,提供基础类型(FString/FVector)、内存管理、线程、容器(TArray/TMap) |
CoreUObject |
UObject 体系的核心模块,提供反射、GC、序列化、元数据系统 |
Engine |
引擎核心模块,封装 Actor、World、输入、物理、音频等游戏基础功能 |
RenderCore |
渲染核心模块,提供渲染硬件接口(RHI)、渲染资源管理 |
Renderer |
渲染实现模块,提供光栅化、光照、粒子、后期处理等渲染逻辑 |
InputCore |
输入核心模块,封装跨平台输入设备(键鼠 / 手柄 / 触屏)的统一接口 |
3. 模块配置文件
Build.cs:定义模块的名称、依赖、包含路径、链接库、编译选项(如:PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" }););Target.cs:定义编译目标(客户端 / 服务器 / 编辑器)、平台配置、编译宏(如bBuildEditor = true;表示编译编辑器版本)。
四、UObject 体系
UObject 体系是 UE 框架最核心的设计,是所有游戏对象、资源的基础,支撑了反射、蓝图、GC、序列化等关键能力,也是 UE 区别于其他引擎的核心特征:
1. UObject 体系的结构
bash
UObject (万物之源:垃圾回收 / 反射 / 序列化)
│
│ # 1. [实体与逻辑] World Actors
├─────── AActor (场景实体基类)
│ ├── APawn
│ │ └── ACharacter (胶囊体 + CharacterMovement)
│ ├── AController (控制者)
│ │ ├── APlayerController (玩家输入 / HUD / 摄像机管理)
│ │ └── AAIController (AI 寻路 / 行为树驱动)
│ ├── AInfo (纯逻辑)
│ │ ├── AGameModeBase (服务器端规则)
│ │ ├── AGameStateBase (全网同步的游戏状态)
│ │ └── APlayerState (全网同步的玩家个人数据:计分、名字)
│ ├── AHUD (传统的 UI 绘制类,用于调试或管理 Widget)
│ └── ACameraActor (摄像机演员)
│
│ # 2. [组件系统] Components (能力的积木)
├─────── UActorComponent (逻辑组件)
│ ├── USceneComponent (带 Transform 坐标)
│ │ ├── UPrimitiveComponent (渲染与物理)
│ │ │ ├── UStaticMeshComponent
│ │ │ ├── USkeletalMeshComponent (带动画)
│ │ │ ├── UShapeComponent (碰撞盒: Box/Sphere/Capsule)
│ │ │ └── UParticleSystemComponent (粒子特效)
│ │ ├── UAudioComponent (音效)
│ │ ├── UCameraComponent (摄像机视角)
│ │ └── UChildActorComponent (在 Actor 里嵌套另一个 Actor)
│ ├── UMovementComponent (移动逻辑)
│ │ ├── UProjectileMovementComponent (投射物/子弹)
│ │ └── URotatingMovementComponent (自转)
│ └── UInputComponent (接收按键输入)
│
│ # 3. [核心框架] Framework & System
├─────┬── UWorld (地图容器,管理 Level / Physics / Rendering)
│ ├── ULevel (关卡数据)
│ ├── UGameInstance (全局单例,跨关卡持久化)
│ ├── ULocalPlayer (本地玩家代表,管理分屏和 Slate 视口)
│ ├── USaveGame (存档对象,序列化成 .sav 文件)
│ └── USubsystem (生命周期自动管理的子系统)
│ ├── UGameInstanceSubsystem (如:成就系统)
│ ├── UWorldSubsystem (如:怪物生成器)
│ └── UEditorSubsystem (仅编辑器工具)
│
│ # 4. [动画系统] Animation (角色表现的核心)
├─────┬── UAnimInstance (动画蓝图的基类,处理动画状态机)
│ ├── UAnimSequence (动画序列资源)
│ └── UAnimMontage (动画蒙太奇,用于播放攻击动作等)
│
│ # 5. [UI 系统] User Interface
├──── UVisual
│ └── UWidget (Slate 包装器)
│ ├── UUserWidget (UMG 蓝图基类)
│ └── UPanelWidget (布局容器:CanvasPanel, VerticalBox)
│
│ # 6. [静态资产] Assets (Content Browser 中的文件)
├─────┬── UDataAsset (通用数据表)
│ ├── UStreamableRenderAsset
│ │ ├── UTexture (纹理)
│ │ └── UStaticMesh / USkeletalMesh (模型)
│ ├── USoundBase
│ │ ├── USoundCue (音频逻辑图)
│ │ └── USoundWave (音频源文件)
│ ├── UMaterialInterface (材质)
│ ├── UInputMappingContext / UInputAction (UE5 增强输入资源)
│ └── UBehaviorTree / UBlackboardData (AI 行为树资源)
│
│ # 7. [反射与元数据] Reflection
├────── UField
│ ├── UStruct
│ │ ├── UClass (类的类型信息)
│ │ ├── UFunction (函数)
│ │ └── UScriptStruct (结构体)
│ └── UEnum (枚举)
│
│ # 8. [接口] Interface
└────── UInterface (C++ 接口基类)
2. 各子类详细解析:
👑 根基:UObject
作用:所有 UE 类的祖先。提供垃圾回收 (GC)、反射 (Reflection)、序列化 (Serialization) 和网络复制的基础。
常用函数:
CreateDefaultSubobject<T>("Name"):仅构造函数用。创建组件或子对象。
GetWorld():获取当前对象所属的世界(用于生成 Actor、设置定时器等)。
GetName():获取对象的名称字符串。
IsA(UClass*):判断当前对象是不是某个类(或子类)。
1.1. 🌍 实体与逻辑 (World Actors)
所有能被 Spawn 到世界里的东西。
AActor
作用:世界中存在的实体的基类。
常用函数:
BeginPlay():游戏开始时调用(初始化逻辑写这)。
Tick(float DeltaTime):每帧调用(高频逻辑)。
GetActorLocation() / SetActorLocation():获取/设置位置。
Destroy():销毁自己。
SetActorHiddenInGame(bool):隐藏/显示。
GetDistanceTo(OtherActor):计算距离。
AActor -> APawn (继承自 Actor)
作用:可以被 Controller "附身 (Possess)" 的代理体。
常用函数:
AddMovementInput():输入移动向量(需配合 MovementComponent)。
GetController():获取当前控制它的控制器。
PossessedBy(Controller):当被附身时触发的回调。
AActor -> APawn -> ACharacter (继承自 Pawn)
作用:人形角色,自带胶囊体碰撞和强大的移动组件。
常用函数:
Jump() / StopJumping():跳跃逻辑。
Crouch() / UnCrouch():蹲伏逻辑。
GetCharacterMovement():获取移动组件指针(修改速度、重力等)。
AActor -> AController
作用:控制 Pawn 的大脑。
常用函数:
Possess(APawn*):控制某个 Pawn。
UnPossess():释放当前 Pawn。
AActor -> AController -> APlayerController
GetHUD():获取 HUD 实例。
GetMousePosition(...):获取鼠标在屏幕的位置。
SetInputMode(...):切换 UI 模式或游戏模式。
AActor -> AController -> AAIController
MoveToActor(...):让 AI 走到目标面前。
RunBehaviorTree(...):运行行为树。
AActor -> AInfo (及其子类)
AActor -> AInfo -> AGameModeBase
作用:
bash核心管理:控制玩家进入流程(PreLogin/Login/PostLogin)。 角色生成:决定默认生成哪个 Pawn、哪个 Controller 和哪个 HUD。 位置选择:决定玩家在哪个 PlayerStart 处出生。 权限模型:纯服务器逻辑,不允许客户端修改,保证游戏公平。函数:SpawnDefaultPawnAtTransform(...)(生成玩家)、PostLogin(...)(玩家连入时触发)。
bashAGameModeBase (基类:定义游戏基础规则、玩家进入、Pawn生成) │ ├── AGameMode (引擎核心子类:增加了"匹配状态机" Match State) │ │ │ ├── AShooterGameMode (UE官方示例:如《暗影枪神》中的对战模式实现) │ │ # 功能:处理杀敌得分规则、连杀奖励逻辑、团队平衡校验。 │ ├── ACommonGameMode (Modular Gameplay 插件中用于模块化开发的变体) │ └── [YourProject]GameMode (你的项目自定义:如 AMyGameMode) │ # 功能:处理特定的胜负条件(如:塔楼全灭则胜利)、关卡特有的初始化逻辑。 ├── ALyraGameMode (Lyra 示例项目:现代模块化架构,支持通过 Experience 定义规则) │ # 功能特性: │ # Experience 系统:不再硬编码规则,而是根据"游戏体验"配置动态加载不同的规则集。 │ # 集成模块化插件:能够动态启用或禁用特定的游戏功能插件。 │ # 高度可扩展:适合支持多种不同玩法模式(如:团战、占点、夺旗)的同一个游戏。 │ ├── ATemplateGameModeBase (各种项目模板生成的默认类) │ ├── AThirdPersonGameMode (第三人称模板) │ │ # 功能:自动关联 ThirdPersonCharacter 和控制逻辑。 │ ├── AFirstPersonGameMode (第一人称模板) │ ├── AVRGameMode (VR模板) │ └── ATopDownGameMode (顶视角模板) │ └── AOnlineGameMode (某些旧版插件中针对网络对战优化的基类)AActor -> AInfo -> AGameModeBase -> AGameMode
作用:
bashMatch State 管理:支持 WaitingToStart(等待)、InProgress(进行中)、LeavingMap(离开地图)等状态。 比赛流程控制:提供 BeginMatch()、EndMatch()、RestartGame() 等控制函数。 超时逻辑:处理玩家长时间不操作或比赛时限到的逻辑。 状态同步:与 AGameState 配合,将比赛进度广播给所有客户端。AActor -> AInfo -> AGameStateBase
作用:
bash时间同步:提供 GetServerWorldTimeSeconds(),确保所有客户端的时钟与服务器对齐。 玩家列表:维护 PlayerArray(存储所有 APlayerState),用于获取当前房间内的所有玩家。 基础同步:负责最基本的游戏数据同步(如:关卡是否加载完成)。 对应关系:通常与 AGameModeBase 配对使用。函数:GetServerWorldTimeSeconds()(获取服务器时间)。
bashAGameStateBase (基类:负责同步世界时间、基础游戏状态,存在于服务器和所有客户端) │ ├── AGameState (引擎核心子类:增加了"匹配状态"的同步,与 AGameMode 配合使用) │ │ # 匹配状态同步:同步MatchState变量, 如:WaitingToStart, InProgress, WaitingPostMatch)。 │ │ # 流程广播:当比赛状态改变时,通知所有客户端更新 UI(如:显示"游戏开始"或"结算画面")。 │ │ # 比赛时长:通常在此处处理比赛剩余时间的倒计时同步。 │ ├── AShooterGameState (UE官方示例:同步复杂的对战数据,如全队总击杀数) │ │ #功能:专门针对射击游戏优化,同步团队得分,当前占领点进度,每局剩余时间等复杂竞技数据。 │ └── [YourProject]GameState (你的项目自定义:如 ABattleBlasterGameState) │ ├── ALyraGameState (Lyra 示例项目:现代架构,负责管理活跃的游戏插件和模块化状态) │ # 功能: 支持"Game Experience"系统,能根据不同的插件动态改变同步的数据;| │ # 集成高级能力系统(GAS)的全局状态。 ├── ATemplateGameStateBase (各种项目模板生成的默认类) │ ├── AThirdPersonGameState (第三人称模板) │ ├── AFirstPersonGameState (第一人称模板) │ └── ...其他模板 │ └── ANetworkTestGameState (内部用于网络压力测试或调试的变体)AActor -> AInfo -> APlayerState
作用:同步玩家个人数据(即使 Pawn 死了/被销毁,这个还在)。
函数:GetPlayerName()、SetScore()。
bashAPlayerState (基类:负责同步玩家非物理状态数据,如分数、延迟、名称) │ ├── ALyraPlayerState (Lyra 示例项目:现代模块化架构的典范) │ # 功能:支持团队分配、高级角色属性同步、经验值与等级系统、与 GAS 组件集成 │ ├── AModularPlayerState (Modular Gameplay 插件子类) │ # 功能:允许通过"游戏特性"插件动态地向玩家状态添加组件或行为 │ ├── AShooterPlayerState (官方 ShooterGame 示例) │ # 功能:专门处理射击游戏逻辑,如:击杀数 (Kills)、死亡数 (Deaths)、助攻数 (Assists) │ └── [YourProject]PlayerState (开发者自定义子类,如 ABattleBlasterPlayerState) # 功能:存储坦克生命值、弹药量、当前关卡得分、持有的道具等1.2. 🧩 组件系统 (Components)
功能的积木,必须挂载在 Actor 上。
UActorComponent (纯逻辑组件)
作用:没有物理位置的功能模块。
函数:
GetOwner():获取拥有这个组件的 Actor。
Activate() / Deactivate():开启/关闭组件 Tick。
UActorComponent -> USceneComponent
作用:有位置、旋转、缩放。
函数:
SetRelativeLocation():设置相对父组件的位置。
AttachToComponent(...):挂载到另一个组件上。
GetComponentLocation():获取世界坐标。
UActorComponent -> USceneComponent -> UPrimitiveComponent (渲染与物理)
作用:有几何形状,能被看到或发生碰撞。
常用子类与函数:
UStaticMeshComponent:SetStaticMesh(...)(换模型)。
USkeletalMeshComponent:PlayAnimation(...)(播动画)、GetAnimInstance()。
通用函数:SetSimulatePhysics(bool)(开启物理模拟)、AddImpulse(Vector)(施加力/冲量)、SetCollisionEnabled(...)(设置碰撞类型)。
UActorComponent -> UMovementComponent
作用:负责移动计算。
函数:SafeMoveUpdatedComponent(...)(移动并处理碰撞滑移)。
1.3. ⚙️ 核心框架 (Framework)
UWorld
作用:代表当前地图。
函数:
SpawnActor<T>(...):在世界中生成一个 Actor。
LineTraceSingleByChannel(...):发射射线检测。
GetTimeSeconds():获取游戏运行时间。
UWorld -> UGameInstance
作用:游戏进程单例,切换关卡不销毁。
函数:
Init():游戏启动时调用一次。
Shutdown():游戏关闭时调用。
(通常用来存放跨关卡的变量,如 int TotalCoins)。
UWorld -> USaveGame
作用:存档数据容器。
用法:不直接调用函数,而是作为数据对象。配合 UGameplayStatics::SaveGameToSlot(...) 和 LoadGameFromSlot(...) 使用。
UWorld -> USubsystem
作用:自动管理的单例系统。
函数:Initialize()、Deinitialize()。
UWorld -> ULocalPlayer
1.4. 💃 动画系统 (Animation)
UAnimInstance
作用:动画蓝图的 C++ 父类,管理动画状态机。
函数:
NativeUpdateAnimation(DeltaTime):相当于动画蓝图的 Update,用于每帧计算变量(如速度、方向)。
TryGetPawnOwner():获取正在播放该动画的 Pawn。
Montage_Play(...):播放蒙太奇动作(攻击、受击)。
UAnimSequence
UAnimMontage
1.5. 🖼️ UI 系统
UVisual -> UWidget -> UUserWidget
作用:UMG 界面基类。
函数:
AddToViewport():把 UI 显示到屏幕上。
RemoveFromParent():关闭 UI。
NativeConstruct():UI 初始化时调用。
1.6. 📦 静态资产 (Assets)
这些类通常作为指针变量存在(例如 UTexture* MyIcon),用于读取数据。
UDataAsset:自定义数据表。通常没有复杂函数,主要是 UPROPERTY 变量供策划配置。
UTexture / UMaterialInterface:作为参数传递给 MeshComponent->SetMaterial()。
USoundBase:作为参数传递给 UGameplayStatics::PlaySoundAtLocation(...)。
1.7. 🪞 反射与元数据 (Reflection)
底层系统,日常开发主要用来做"类型查询"。
UClass:描述一个类的类型。
- GetDefaultObject() (CDO):获取该类的默认对象实例。
UFunction:描述一个函数。
- 用于 ProcessEvent 手动调用蓝图函数(高级用法)。
2. UObject 体系的核心能力
| 核心能力 | 作用 | 应用场景 |
|---|---|---|
| 反射系统(Reflection) | 运行时获取类 / 结构体 / 函数的元数据(如类名、属性、函数参数) | 蓝图交互、序列化、网络复制、编辑器可视化 |
| 垃圾回收(GC) | 自动管理 UObject 对象的内存,无需手动delete |
避免内存泄漏,简化游戏对象的内存管理 |
| 序列化(Serialization) | 将 UObject 对象的状态转换为字节流(或反向) | 游戏存档、资源加载 / 保存、网络同步 |
| 元数据系统 | 通过UCLASS()/UPROPERTY()等宏注入元数据,控制对象行为 |
标记蓝图可见性、网络复制规则、存档规则 |
| 生命周期管理 | 统一的对象创建(NewObject/SpawnActor)、销毁(Destroy)流程 |
保证对象创建 / 销毁的一致性,避免野指针 |
3. 关键设计点
- UClass:每个 UObject 子类都对应一个 UClass 对象("类对象"),存储该类的元数据(如属性、函数、蓝图配置);
- GC 机制 :采用 "标记 - 清除" 算法,通过
UPROPERTY追踪对象引用,自动回收无引用的 UObject; - 非 UObject 对象 :通过
TSharedPtr/TWeakPtr管理(如 Slate UI 对象),不参与 GC,需手动控制生命周期。
五、游戏框架
游戏框架层是基于 UObject 体系和引擎层封装的游戏专属逻辑框架,提供了开箱即用的游戏对象和规则,是开发者日常开发接触最多的部分:
1. 核心游戏对象层级
在 Unreal Engine 中构建一个结构合理、逻辑清晰且易于扩展的游戏框架,关键在于严格遵守 UE 的设计哲学(Gameplay Framework)。UE 是一套"有主见"的引擎,它已经为你规定好了**"什么数据应该放在哪里"**。
如果你的代码结构混乱,通常是因为把本该属于 B 的逻辑写到了 A 里面(例如:把"游戏胜利判定"写在了"玩家角色"里,或者把"UI 刷新"写在了"游戏模式"里)。
以下是构建**"最合理游戏结构"的标准范式,我将其分为四层架构**来解析。
第一层:全局持久层 (Persistent Layer)
作用 :贯穿整个游戏生命周期,切关卡不销毁。
|---------------------------------------------|------------|----------------------------------------------------------------------------------------------------|
| 类 | 职责归属 | 最佳实践 |
| UGameInstance | 全局大管家 | 存放跨关卡数据(如:音量设置、玩家选的皮肤ID、存档的元数据)。<br>不要在这里写复杂的战斗逻辑。 |
| USubsystem<br>(GameInstanceSubsystem) | 模块化管理器 | 比如 UQuestManager (任务系统)、UInventoryManager (全局背包)。<br>尽量把逻辑从 GameInstance 拆分到 Subsystem 中,保持代码解耦。 |
- 架构关系:GameInstance 包含并管理各个 Subsystem。它们是所有 Level 的"上级"。
第二层:战局规则层 (Match Rules Layer)
作用 :定义当前关卡的玩法。切换关卡时会被销毁并重建。
|--------------------|----------------|----------------------------------------------------------------------|
| 类 | 职责归属 | 最佳实践 |
| AGameModeBase | 裁判 (仅服务器) | 判定游戏输赢、重生规则、生成玩家。<br>例如:"杀够30人结束"、"倒计时归零结束"。<br>注意:客户端无法访问它。 |
| AGameStateBase | 记分员 (全网同步) | 存放全场公开的数据。<br>例如:当前剩余时间、红蓝队总比分、当前是第几波怪。<br>所有客户端都能读到它。 |
架构关系:
GameMode 拥有最高权威,负责修改 GameState。
Actor 询问 GameState 获取比赛信息。
第三层:玩家逻辑层 (Player Layer) ------ 最核心的"铁三角"
作用:处理单个玩家的输入、表现和数据。这是新手最容易搞混的地方。
请务必把"玩家"拆分为以下三个部分,不要把所有逻辑都堆在 Character 里!
1. 意志 (Mind) ------ APlayerController
职责:处理输入(按键)、管理 UI(创建 HUD)、控制摄像机管理器。
生命周期 :比 Pawn 长。玩家死后,Controller 还在(此时看着尸体或进入观战模式)。
合理安排:
按键绑定 (Input Mapping) 放在这里。
打开/关闭背包界面的逻辑放在这里。
不要在这里写"开枪扣血"的逻辑。
2. 身体 (Body) ------ APawn / ACharacter
职责:在世界中的物理表现。移动、播放动画、受击判定。
生命周期 :脆弱。很容易被 Destroy(被打死)。
合理安排:
血量 (Health) 放在这里(因为身体换了,血量通常重置)。
开枪特效、走路动作放在这里。
3. 档案 (Data) ------ APlayerState
职责:玩家的个人数据(即使身体死了,数据也得留着)。
生命周期 :与关卡同在。玩家死了重生,PlayerState 不会重置。
合理安排:
个人击杀数 (K/D)。
玩家昵称、Ping 值。
当前的等级、经验值(如果在单局内有效)。
第四层:表现与物品层 (Presentation & Content)
|------------------------|----------|-----------------------------------------------------------------------------------------------------------------|
| 类 | 职责归属 | 最佳实践 |
| AHUD / UUserWidget | 显示器 | 纯被动显示。<br>UI 应该监听数据的变化,而不要去存数据。<br>错误做法:在 UI 里存"int CurrentHealth"。<br>正确做法:UI 绑定 Character->Health。 |
| AActor (武器/物品) | 交互道具 | 武器应该是一个独立的 Actor,被 Attach 到 Character 上。<br>不要把枪械逻辑直接写在 Character 代码里。 |
🚀 终极架构图:理想的数据流向
为了让结构最合理,应该遵循**"单向依赖"或"事件驱动"**的原则。

✨ 一个具体的实战案例:射击游戏 (FPS)
假设你正在做一个"团队死斗"游戏,最合理的逻辑分配如下:
玩家按鼠标左键:
- PlayerController 收到输入 -> 告诉 Character "开火"。
角色开火:
- Character 播放开枪动画 -> 调用手中的 WeaponActor 发射射线。
击中判定:
- WeaponActor 检测到击中了敌人 EnemyCharacter -> 调用 Enemy->ApplyDamage()。
敌人死亡:
EnemyCharacter 血量归零 -> 告诉服务器的 GameMode "我死了,凶手是 PlayerA"。
EnemyCharacter 播放死亡动画,变成布娃娃,3秒后 Destroy。
得分与重生:
GameMode 收到消息:
给 PlayerA 的 PlayerState 加 1 分(杀敌数)。
给 AGameState 的红队总分加 1 分。
执行重生成逻辑:在随机出生点生成一个新的 EnemyCharacter 并让敌人的 Controller 附身。
UI 刷新:
PlayerA 的 HUD 监听到 PlayerState 分数变动 -> 更新屏幕右上角的 K/D 显示。
所有人的 HUD 监听到 GameState 分数变动 -> 更新顶部比分条。
💡 总结:怎么判断"合不合理"?
当你写代码时,问自己三个问题:
如果角色死了(Destroy),这个数据还需要吗?
需要 -> 放 PlayerState。
不需要 -> 放 Character。
换了关卡,这个数据还需要吗?
需要 -> 放 GameInstance。
不需要 -> 放 GameState。
这个逻辑是针对全场所有人的,还是针对我自己的?
全场 -> GameMode / GameState。
自己 -> PlayerController / Character。
遵循这套 UE 官方推荐架构,你的项目在后期扩展(比如加多人联机、加新模式)时,会比"把代码全写在 Actor 里"轻松一百倍。
2. 核心游戏对象的职责
| 核心对象 | 核心职责 | 作用范围 |
|---|---|---|
UWorld |
游戏世界的根对象,管理所有关卡、Actor、游戏规则,是游戏的 "容器" | 全局(单例) |
UGameInstance |
游戏生命周期的全局对象,跨关卡保存数据(如玩家信息、配置),不随关卡销毁 | 全局(单例) |
AGameModeBase |
服务器专属,定义游戏规则(出生点、胜利条件、玩家加入 / 退出逻辑) | 服务器 |
AGameStateBase |
存储全局游戏状态(比分、剩余时间),同步给所有客户端 | 服务器 + 客户端 |
APlayerController |
关联玩家和 Pawn,处理输入、相机控制、网络同步(客户端 <-> 服务器) | 每个玩家一个 |
APawn/ACharacter |
玩家 / AI 的可控制实体,包含物理、动画、碰撞等组件 | 场景对象 |
UActorComponent |
组件化设计,封装单一功能(如网格、音频、碰撞),附属于 Actor | Actor 的子对象 |
3. 核心设计理念:组件化(Component-Based)
UE 游戏框架采用组件化设计,而非传统的继承式设计:
- Actor 作为 "容器",本身仅管理变换(位置 / 旋转 / 缩放)和组件;
- 具体功能(如渲染、物理、音频)由不同的 Component 实现;
- 优势:功能解耦、复用性高(如多个 Actor 可复用同一个 "音频组件")、灵活扩展(动态添加 / 移除组件)。
| 子类 | 作用 |
| USceneComponent | 带 Transform 的组件(所有空间组件的基类) |
| UStaticMeshComponent | 渲染静态网格模型 |
| USkeletalMeshComponent | 渲染骨骼动画模型 |
| UCapsuleComponent | 胶囊体碰撞组件(Character 的核心碰撞) |
| UInputComponent | 处理玩家输入 |
| UAudioComponent | 播放音频 |
| UCameraComponent | 相机视角组件 |
|---|

六、核心子系统(Engine Subsystems)
子系统 (Subsystem) 是 Unreal Engine (从 4.22 版本引入,4.24 成熟) 中最棒的架构特性之一。
简而言之,它们是引擎自动管理的、模块化的"单例"。
如果你曾经把所有的全局逻辑(背包、任务、网络、音频管理)都塞进 UGameInstance 或 APlayerController 里,导致那个类变成了几千行的"上帝类 (God Class)",那么子系统就是用来拯救你的。
以下是关于 UE 子系统的核心知识点全解:
1. 核心概念:它是什么?
本质:继承自 UObject 的类。
特性:
自动创建:你不需要 NewObject,引擎会在特定的生命周期节点自动帮你创建实例。
自动销毁:引擎会在生命周期结束时自动销毁它们。
易于访问:在任何地方都能通过全局上下文轻松获取。
模块化:把代码拆分到不同的子系统里,互不干扰。
2. 五大子系统类型 (按生命周期划分)
这是面试和实战中最关键的部分,决定了你的逻辑写在哪里。
|----------------------------|----------------------------------------|------------------------------------|-----------------------------------------------------------|
| 子系统基类 | 生命周期 | 适用场景 | 获取方式 (C++) |
| UEngineSubsystem | 最长。游戏启动 ~ 游戏进程结束。 | 硬件接口、全局统计、跟具体存档无关的工具。 | GEngine->GetEngineSubsystem<T>() |
| UEditorSubsystem | 编辑器运行期间。 (仅编辑器模式有效) | 制作编辑器工具、自动化脚本、资源检查插件。 | GEditor->GetEditorSubsystem<T>() |
| UGameInstanceSubsystem | 中等 。游戏启动 ~ 关闭。<br>(跨地图存在) | 绝大多数全局逻辑:背包、任务、成就、网络连接管理、存档管理。 | GameInstance->GetSubsystem<T>() |
| UWorldSubsystem | 较短 。加载地图 ~ 卸载地图。<br>(切图即销毁) | 关卡内的逻辑:怪物生成器、天气控制器、当前关卡得分统计。 | World->GetSubsystem<T>() |
| ULocalPlayerSubsystem | 特殊。与本地玩家存在时间一致。<br>(分屏游戏尤其重要) | UI 管理器、输入映射管理 (Enhanced Input)。 | PlayerController->GetLocalPlayer()->GetSubsystem<T>() |3. 实战代码示例
A. 如何定义一个子系统 (C++)
比如我们要写一个分数的管理系统。
cpp// MyScoreSubsystem.h #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "MyScoreSubsystem.generated.h" UCLASS() class MYGAME_API UMyScoreSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: // --- 1. 生命周期函数 --- virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; // --- 2. 你的逻辑 --- UFUNCTION(BlueprintCallable) void AddScore(int32 Delta); UFUNCTION(BlueprintPure) int32 GetCurrentScore() const { return Score; } private: int32 Score = 0; };
cpp// MyScoreSubsystem.cpp void UMyScoreSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); Score = 0; UE_LOG(LogTemp, Log, TEXT("Score System Started!")); } void UMyScoreSubsystem::Deinitialize() { // 保存数据或清理 UE_LOG(LogTemp, Log, TEXT("Score System Shutdown!")); Super::Deinitialize(); }B. 如何在蓝图中使用
子系统的一个巨大优势是蓝图极其友好。
只要你的函数加了 UFUNCTION(BlueprintCallable)。
在蓝图里右键,直接搜 "MyScoreSubsystem",你会发现引擎自动生成了节点的上下文,你甚至不需要手动获取 "Get GameInstance"。
4. 高级技巧与知识点
技巧一:控制是否创建 (ShouldCreateSubsystem)
默认情况下,子系统会无条件创建。但如果你只想在服务器 上创建,或者只想在客户端创建,怎么办?
复写 ShouldCreateSubsystem 函数:
cppbool UMyScoreSubsystem::ShouldCreateSubsystem(UObject* Outer) const { // 例子:只在客户端创建这个子系统(比如 UI 管理器) if (UWorld* World = Outer->GetWorld()) { return World->GetNetMode() == NM_Client; } return true; }技巧二:让子系统支持 Tick (FTickableGameObject)
默认情况下,子系统是不 Tick 的(为了性能)。如果你需要它每帧更新逻辑:
继承 FTickableGameObject。
实现 Tick(), GetStatId(), IsTickable()。
cppclass UMySystem : public UGameInstanceSubsystem, public FTickableGameObject { // ... virtual void Tick(float DeltaTime) override; virtual TStatId GetStatId() const override { return TStatId(); } };技巧三:子系统之间的依赖
如果 Subsystem A 初始化时需要用到 Subsystem B 怎么办?
在 Initialize 函数中,使用 Collection 参数:
cppvoid UMySystemA::Initialize(FSubsystemCollectionBase& Collection) { // 强制依赖:确保 SystemB 在 SystemA 之前初始化完成 Collection.InitializeDependency(UMySystemB::StaticClass()); // 现在可以安全访问 B 了 UMySystemB* SysB = Collection.GetSubsystem<UMySystemB>(UMySystemB::StaticClass()); }5. 为什么要用子系统?(对比传统做法)
|------------|---------------------------------------------|---------------------------------------|--------------------------------------|
| 场景 | 传统做法 (旧时代的眼泪) | 子系统做法 (现代架构) | 优势 |
| 全局任务系统 | 在 UMyGameInstance.h 里写 StartQuest() 等几百行代码。 | 创建 UQuestSubsystem。 | GameInstance 不再臃肿,任务逻辑独立封装。 |
| 关卡刷怪器 | 在 AGameMode 里写生成逻辑。 | 创建 UMonsterWorldSubsystem。 | 即使换了 GameMode,刷怪逻辑依然可以复用;且逻辑从裁判类中剥离。 |
| UI 管理 | 在 APlayerController 里存一堆 Widget 指针。 | 创建 UUIManager (LocalPlayerSubsystem)。 | 支持本地分屏,每个玩家有独立的 UI 管理器。 |总结
Engine Subsystem 是 UE C++ 开发者的神兵利器。
什么时候用? 当你需要一个管理类 (Manager),且希望它能自动管理生命周期时。
选哪个?
跨关卡数据 -> GameInstanceSubsystem (最常用)。
单关卡逻辑 -> WorldSubsystem。
UI/输入 -> LocalPlayerSubsystem。
七、核心运行机制:Tick 与事件驱动
"能用事件驱动(Delegate/Event/Timer)解决的问题,绝对不要写在 Tick 里!" 这是 UE 开发优化的第一准则。
1. Tick 机制:不知疲倦的心跳 (主动轮询)
本质是什么?
Tick 是游戏循环(Game Loop)的直接体现。引擎的每一帧(Frame)都在进行一次巨大的 while 循环。在这一帧里,引擎会问场景里的每一个 Actor:"你现在有什么要做的吗?"
如果有 1000 个 Actor 开启了 Tick,引擎这一帧就要把这 1000 个对象的 Tick() 函数全部跑一遍。
核心参数:DeltaTime (Δt)
因为电脑性能不同,有的电脑一秒跑 60 帧(Tick 60次),有的跑 30 帧(Tick 30次)。
为了保证游戏逻辑一致(比如子弹飞行速度),Tick 函数会携带一个 DeltaTime 参数,代表**"上一帧到这一帧过去了多少秒"**。
公式: 位移 = 速度 × DeltaTime。
这样无论帧率多少,物体在 1 秒内的移动距离都是一样的。
Tick Group (Tick 组/时序)
所有的 Tick 不是乱序执行的,UE 把它们分成了"组"。比如:
TG_PrePhysics:在物理模拟计算前执行(重置力、计算意图)。
TG_DuringPhysics:与物理模拟并行(通常用于物理无关的逻辑)。
TG_PostUpdateWork:在所有东西都算完后执行(通常用于修正摄像机位置,防止画面抖动)。
代价
Tick 是性能杀手。哪怕你的 Tick 函数里是空的,引擎调用它也有开销(虚函数开销 + 内存寻址)。
- 原则:默认情况下,应该把 Actor 的 PrimaryActorTick.bCanEverTick 设置为 false。
2. 事件驱动:静待花开的反射 (被动响应)
本质是什么?
这是"好莱坞原则":Don't call us, we'll call you.(不要打给我们,有事我们会打给你)。
对象平时处于休眠/空闲状态,完全不占用 CPU 资源。只有当特定的"条件"满足时,才会瞬间被唤醒执行代码。
核心工具:Delegate (委托)
UE 的事件驱动建立在 Delegate 机制之上。它像一个**"订阅列表"**。
场景:Boss 死了。
Tick 的做法:UI 每帧检测 if (Boss.IsDead()) UpdateUI();(极其浪费)。
事件的做法:
Boss 类里有一个 OnDeath 委托(广播站)。
UI 在初始化时说:"我要订阅 OnDeath"。
Boss 死了,发出广播。UI 收到信号,执行刷新。
常见的事件类型
物理事件:OnComponentHit(撞击)、OnBeginOverlap(进入区域)。
输入事件:SetupPlayerInputComponent 绑定的按键按下。
生命周期:BeginPlay(出生)、EndPlay(销毁)。
定时器 (Timer):这是 Tick 的替代方案。比如每隔 1 秒执行一次,而不是每帧(每 0.016 秒)执行一次。
3. 总结与最佳实践 (这也是面试必考题)
如何选择代码的执行方式。
|--------------|-------------|---------------------------|----------------------------------------------------------|
| 场景 | 应该用 Tick 吗? | 为什么? | 正确做法 |
| 每秒回血 | ❌ 不要用 | 用 Tick 太浪费性能,且需要复杂的计时器逻辑。 | 使用 Timer (定时器),每1秒触发一次事件。 |
| UI 刷新血条 | ❌ 不要用 | 不要每帧去检测血量变了没。 | 绑定 OnHealthChanged 委托,只有掉血时才刷新 UI。 |
| 导弹追踪目标 | ✅ 必须用 | 每一帧位置都在变,必须实时计算。 | 在 Tick 里更新 Location。 |
| 检测是否踩到陷阱 | ❌ 不要用 | 不要每帧去算距离。 | 使用 Collision Component 的 OnComponentBeginOverlap 事件。 |