今天先来补充一下关于Unity和UE的一些问题,后续开始深挖项目:
Unity
关于fixed update和update:同一帧中物理更新优先执行?
关于协程:
协程是基于迭代器实现的,而迭代器是基于状态机实现的。协程的本质是编译器将 yield
语法转化为状态机类,Unity 通过迭代器接口在帧生命周期中调度其执行,从而实现单线程上的异步编程体验。
cs
void Start() {
Debug.Log("【1】Start开始");
StartCoroutine(MyCoroutine()); // 启动协程
Debug.Log("【3】Start结束"); // ✅ 协程挂起后立即执行!
}
IEnumerator MyCoroutine() {
Debug.Log("【2】协程开始"); // ✅ 同步执行
yield return new WaitForSeconds(2); // ⏸️ 挂起点
Debug.Log("【4】2秒后恢复执行"); // ⏳ 等待2秒后触发
}
这就是一个协程运作的基本流程,我们通过StartCoroutine启动协程,协程正常运作到yield return之前都一切正常,遇到yield return之后我们会把协程挂起,然后这个时候我们会回到start函数中继续执行后续的逻辑,直到协程从挂起恢复后再执行协程后续的内容。
关于Resources,AB包和Addressable:

对于后两种方案来说,AB包和Addressables加载的资源是无法在Unity的构建过程中进行构建的,因为这些资源会被Unity识别为外部依赖,Unity的Build只会去构建Asset中的资源。
Unity必需的文件夹:之前也提到过了,Asset,Library,ProjectSettings,以及如果有第三方插件所需要的Packages文件夹。Unity 的资源构建过程严格限定在项目的 Assets
目录内,无论采用何种打包策略,最终构建的内容均源于此目录下的资源。在开发环境中,Assets
文件夹存储所有原始资源文件(如模型、贴图、音频),Library
文件夹由 Unity 自动生成,用于缓存导入后的中间数据(如 .meta
文件和优化后的资源副本),而 Project Settings
则保存项目的全局配置(如物理参数、渲染设置)。若项目包含第三方插件,通常会被置于 Packages
目录(本质是 Assets
的子集),同样参与构建。
关于LOD和Mipmap:LOD(Level of Detail)和Mipmap是Unity中两类核心的渲染优化技术,它们均通过动态调整资源精度来平衡画质与性能。

关于固定帧率模式和追赶帧率模式:
固定帧率模式其核心是通过主动休眠 强制延长帧时间,确保每帧耗时严格匹配目标帧率要求。例如目标为60FPS(每帧16.67ms),若某帧逻辑仅耗时5ms,系统会调用 Thread.Sleep()
或 WaitForSeconds()
休眠剩余11.67ms,再执行下一帧。
当帧耗时超过目标时间 (如目标16.67ms但实际耗时25ms),系统会跳过当前帧的渲染阶段,直接进入下一帧的输入处理与逻辑更新,通过多次执行逻辑更新(如连续调用 Update()
)消化累积的时间延迟。例如连续两帧超时后,第三帧若未超时则渲染最新逻辑状态。
关于UGUI如何触发事件:本质上就是基于射线检测实现的,UGUI会在鼠标点击的位置发射一条射线,如果检测到UI元素就会触发绑定的事件。
如何创建一个有限状态机:
针对不同需求的状态机大体上有两种实现方法:
在实现简单状态机时,我们首先定义一个枚举类型来明确所有可能的状态(如Idle
、Walk
),然后在状态机类中声明一个当前状态变量(currentState
),通过Update()
方法内的switch
语句根据当前状态执行对应逻辑;在每个case
分支中,我们直接编写状态转换的条件判断(例如检测输入事件或条件满足),一旦条件成立就立即更新currentState
的值,从而在下一帧切换到新状态。这种方法将状态行为与转换逻辑集中在一个类中,适合状态数量少(≤5个)且逻辑不复杂的场景,但扩展性较差,状态增多后代码会臃肿。
对于复杂状态机,我们采用状态模式:先定义一个基础状态接口(如IState
),要求所有具体状态类实现Enter()
、Update()
、Exit()
三个方法;接着为每个状态(如IdleState
、WalkState
)创建独立类,在这些类的Update()
方法内检测转换条件(如按键事件),并直接调用状态机类的ChangeState()
方法(需持有状态机引用)触发切换;状态机类负责管理当前状态(private IState currentState
),在ChangeState()
中依次执行旧状态的Exit()
、更新状态引用、新状态的Enter()
,同时通过自身的Update()
驱动当前状态的Update()
。最终,在角色控制器中初始化状态机并设置初始状态,每帧调用状态机的Update()
即可完成闭环。这种方式将状态逻辑分散到各状态类,通过多态实现动态行为分发,支持高扩展性和维护性,适用于状态多或逻辑复杂的系统。

实现了三个Update的驱动。
OnPreProcessTexture和OnPostProcessTexture的作用:一句话总结就是前者是导入纹理前执行的方法后者是导入纹理后执行的方法。
AssetModificationProcessor是什么?这是Unity提供的一个编辑器类,用于监听资源在编辑器中的操作(如创建、删除、移动),而非导入流程。其核心方法包括:
•OnWillCreateAsset
:资源创建前触发。
•OnWillDeleteAsset
:资源删除前触发。
•OnWillMoveAsset
:资源移动前触发。
如果要使用这个类中的方法只需要去继承这个类然后重写调用相关的函数即可。
AB包底层原理:
AB包本质是一种平台特定的二进制压缩文件,用于存储Unity资源(如模型、贴图、材质、预制体、音效等)。它通过将资源打包成独立于应用安装包的二进制文件,实现资源的动态加载与更新。
这里补充一下Resources,我们总说尽量少使用这个文件,但具体为什么呢?Unity 在构建项目时,会将所有 Resources
文件夹内的资源整合为一个序列化文件(resources.assets
),并生成一个红黑树 作为索引数据结构。游戏启动时,Unity 会完整加载整个红黑树索引到内存中 ,且该索引不可卸载,会持续占用内存直至游戏结束,红黑树本身占据的内存就不小,且构建和维护红黑树也要很多时间,所以如果Resources中的文件过多,会导致游戏加载卡顿。
在Unity中打包AB包时,首先需要在编辑器中为资源手动标记所属的包名(如将角色模型标记为characters/hero
),相同包名的资源会被合并打包;接着选择压缩格式------LZMA适合最小化包体但需整体解压,LZ4则支持按需加载局部资源 ,更推荐用于平衡性能与体积;打包过程中Unity会自动分析资源间的引用关系(如多个模型共享的材质),将公共依赖提取到独立AB包避免冗余,最终通过BuildPipeline.BuildAssetBundles()
生成二进制文件及记录依赖链的主清单文件,此过程必须指定目标平台(如Android/iOS)以确保兼容性。
在加载与卸载AB包时,若资源在本地存储(如StreamingAssets),可通过AssetBundle.LoadFromFile()
同步加载或LoadFromFileAsync
异步加载,后者避免主线程阻塞;加载后AB包数据暂存于内存镜像区 (压缩态),需调用LoadAsset()
解压到活动内存才能实例化使用;关键的是必须通过主清单递归加载所有依赖包(如材质包),否则资源引用失效(如模型变粉);卸载时若调用Unload(false)
仅释放内存镜像区的压缩数据,已加载资源保留(需后续手动管理),而Unload(true)
则强制卸载所有关联资源,但若场景物体仍引用这些资源会导致材质丢失或报错,因此最佳实践是在场景切换时先Unload(false)
释放AB包,再调用Resources.UnloadUnusedAssets()
清理残留资源实例。
UE
UE必需的文件?
- 项目名.uproject - 项目的主配置文件,定义项目信息和模块结构
- Content/ - 存储所有游戏资源(蓝图、材质、贴图、网格、音频等),这是最重要的目录
- Config/ - 包含引擎配置文件(DefaultEngine.ini、DefaultGame.ini、DefaultInput.ini等)
- Source/ - C++源代码目录(仅C++项目需要),包含模块文件和构建配置
这四个文件是必须的,还会额外生成诸如Binaries(编译输出)、Intermediate(中间文件)、Saved(日志和用户数据)、DerivedDataCache(派生数据缓存)这些文件。
Binaries/存储编译后的可执行文件和库文件,当你双击.uproject文件或运行项目时,引擎会读取这个文件夹中的.exe和.dll文件来启动游戏程序;Intermediate/存储编译过程中的临时文件,当你在Visual Studio中编译C++代码时,编译器会在这里生成.obj文件和预编译头文件,这些是编译的中间产物;Saved/存储运行时和编辑器生成的数据,当你需要查看项目运行日志、调试信息、性能分析数据或用户配置时,就会在这个文件夹中查找相应的文件;DerivedDataCache/存储引擎处理后的资产数据,当你导入贴图、音频等原始资产时,引擎会进行压缩、优化等处理,处理后的数据就缓存在这里,下次加载相同资产时直接读取缓存,避免重复处理以提升加载速度。
UE的资产管理系统?
虚幻引擎的资产管理系统基于UObject和反射机制构建,每个资产都有唯一GUID标识并通过FAssetRegistry全局注册管理。系统采用异步加载机制,通过UAssetManager实现按需加载和内存优化,同时维护派生数据缓存(DDC)来存储处理后的资产数据以提升加载速度。所有资产都继承自UObject,通过序列化系统保存为.uasset文件,支持版本控制和热重载。系统还提供完整的依赖关系追踪,当父资产更新时会自动触发相关子资产的重新编译,并通过反射机制实现与蓝图系统和编辑器的无缝集成。
虚幻引擎的派生数据缓存(Derived Data Cache)是什么?
虚幻引擎的派生数据缓存(DDC)是一个本地缓存系统,用于存储引擎处理过的资产数据。当引擎导入原始资产(如贴图、音频、模型)时,会进行各种处理操作(压缩、优化、编译等),这些处理后的数据就存储在DDC中。主要的作用在于避免重复处理,大幅提升加载速度。比如一张高分辨率贴图第一次导入时需要压缩和生成mipmap,处理完成后会缓存到DDC,下次加载时直接读取缓存数据,无需重新处理。
什么是蓝图?它的主要用途是什么?
蓝图是虚幻引擎的可视化编程系统,允许开发者通过节点式图形界面来创建游戏逻辑,而无需编写传统代码。蓝图采用节点连接的方式,每个节点代表一个函数或变量,通过连线定义数据流和控制流,支持事件驱动编程、函数封装、类继承等面向对象概念。
蓝图有哪些类型?和C++的关系是什么?性能特点是什么?
主要包括:Level Blueprint(关卡蓝图,控制关卡级别的逻辑)、Class Blueprint(类蓝图,创建可重用的游戏对象)、Function Library(函数库,封装可重用的函数)、Interface(接口蓝图,定义通用接口)、Widget Blueprint(UI控件蓝图)、Animation Blueprint(动画蓝图,控制角色动画)。
蓝图可以继承C++类,C++可以调用蓝图函数,两者可以相互通信。蓝图实际上是C++类的可视化包装,编译后生成C++代码,支持热重载(修改后无需重启编辑器)。
蓝图是解释执行的,性能相对C++较低,但开发效率高。蓝图代码会被编译成字节码,在运行时由虚拟机执行,对于大部分游戏逻辑来说性能足够,但对于计算密集型的操作建议使用C++。
蓝图中的事件图表和函数图表有什么区别?
事件图表(Event Graph)是蓝图的主要执行入口,处理各种事件触发,比如游戏开始、按键输入、碰撞检测、定时器等。事件图表中的节点通常以事件开始(如Event BeginPlay、Event Tick),然后连接执行逻辑,是蓝图程序的控制流程;函数图表(Function Graph)用于封装可重用的逻辑代码,类似于传统编程中的函数。你可以在函数图表中创建自定义函数,然后在事件图表或其他地方调用这些函数,实现代码复用和模块化设计。
C++的代码具体是如何演变到蓝图的?
C++类通过反射系统暴露给蓝图。使用UCLASS()宏标记类,UPROPERTY()宏暴露属性,UFUNCTION()宏暴露函数,UFUNCTION(BlueprintCallable)让蓝图可以调用C++函数,UFUNCTION(BlueprintImplementableEvent)让C++可以调用蓝图函数。编译器扫描这些宏生成元数据,存储在UClass中,蓝图编辑器读取这些元数据生成可视化节点。当你在蓝图中调用C++函数时,实际上是调用生成的包装函数,该函数通过反射系统调用真正的C++实现。
UE定义宏 → 编译器扫描宏 → 生成元数据 → 反射系统读取元数据 → 蓝图可视化
具体来说,虚幻引擎定义了自己的宏(如UCLASS、UPROPERTY、UFUNCTION),当编译器扫描到这些宏时,会生成额外的元数据代码,包含类的结构、属性、函数等信息。然后引擎的反射系统读取这些元数据,将C++类的信息提供给蓝图编辑器,蓝图编辑器就能将这些C++类转换为可视化的节点和界面,让开发者可以通过图形化方式调用C++代码。
虚幻引擎的内存管理机制是什么?
虚幻引擎的内存管理机制基于垃圾回收 (Garbage Collection)和智能指针系统构建。
所有继承自UObject的对象都参与垃圾回收,引擎会定期扫描对象引用关系,自动释放不再被引用的对象。UObject使用引用计数和标记清除算法,当对象没有强引用时会被自动回收。UE提供TSharedPtr、TWeakPtr、TUniquePtr等智能指针,自动管理内存生命周期,避免内存泄漏。TSharedPtr提供共享所有权,TWeakPtr提供弱引用(不阻止垃圾回收),TUniquePtr提供独占所有权。(类C++智能指针)
UE中的类继承关系?
UObject是所有UE对象的根基类,提供垃圾回收、序列化、反射等核心功能。AActor继承自UObject,是游戏世界中可放置对象的基类,增加了位置、旋转、生命周期管理等特性。UComponent继承自UObject,是功能组件的基类,不能独立存在。APawn继承自AActor,是玩家控制的对象的基类。ACharacter继承自APawn,是角色类的基类,增加了角色特有的功能。AController继承自AActor,是控制器的基类,负责控制Pawn的行为。UClass、UFunction、UProperty等继承自UObject,是反射系统的工具类。UActorComponent继承自UObject,是附加到Actor上的功能组件。USceneComponent继承自UActorComponent,是具有空间变换功能的组件基类。

UWorld是什么?如何理解虚幻引擎中的关卡和世界的概念?
虚幻引擎中的UWorld是游戏世界的容器和管理者,代表一个完整的游戏世界,负责管理所有Actor对象、物理世界、音频系统、网络复制等各个子系统。关卡(Level)是UWorld中的子集,对应编辑器中的.umap文件,代表具体的游戏场景或区域。一个UWorld可以包含多个关卡,通过Level Streaming系统动态加载和卸载关卡。项目文件(.uproject)对应一个完整的游戏项目,可以包含多个不同的游戏世界,每个世界通过GameMode定义游戏规则,通过关卡流送管理多个关卡的组合。GameInstance作为全局单例管理整个游戏的生命周期,而World Settings在关卡中设置世界级别的参数。简单来说,UWorld是"游戏世界的管理者",关卡是"具体的游戏区域",项目是"包含多个世界的容器"。
如何在C++中创建自定义UE中可使用的组件?
在虚幻引擎中,如果你想通过C++编写自定义类并让蓝图系统能够识别和使用,就必须使用UE提供的反射宏系统。具体来说,你需要使用UCLASS()宏来标记类,添加GENERATED_BODY()宏让代码生成系统插入必要的代码,然后根据需求选择性使用UPROPERTY()宏暴露属性给蓝图,使用UFUNCTION()宏暴露函数给蓝图。对于自定义组件,你需要继承UActorComponent类,使用UCLASS()宏标记,添加GENERATED_BODY(),重写必要的生命周期函数(如BeginPlay、Tick等),并根据需要添加UPROPERTY()和UFUNCTION()宏来暴露特定的属性和函数。这样通过UE的反射系统,你的C++代码就能被蓝图系统识别和使用,实现C++和蓝图的无缝集成。
虚幻引擎的委托系统是什么?
虚幻引擎的委托系统包含Delegate(单播委托)、Multicast Delegate(多播委托)、Event(事件)和Dynamic Delegate(动态委托),底层原理是对函数指针的封装,使用模板和反射系统实现,当委托被触发时引擎会调用所有绑定的函数并支持参数传递和返回值处理。使用方法包括使用DECLARE_DELEGATE等宏声明委托类型,使用Bind()、AddDynamic()等方法绑定函数到委托,使用Execute()、Broadcast()等方法触发委托,使用Unbind()、RemoveDynamic()等方法解绑函数。权限控制方面,普通委托可以在任何地方触发,Event只能在声明它的类内部触发但外部可以绑定解绑,Dynamic Delegate专门用于蓝图集成支持在蓝图中绑定和触发。实际应用常用于事件通知、回调处理、UI交互、游戏逻辑解耦等场景,实现对象间的松耦合通信。
哎哟我,不行了,今天下午做了个笔试,那编程题的IDE不支持cout的,我顶不住了,有点累有点累今天,先到这,明早继续。