UE5第三人称游戏项目结构全解析:从目录规划到模块化实战
1. 项目概述:为什么UE5第三人称项目结构如此重要?
如果你刚接触虚幻引擎5,尤其是想做一个第三人称游戏,打开编辑器创建新项目后,面对满屏的文件夹和文件,是不是有点懵?这感觉就像拿到了一套精装修房子的钥匙,但不知道每个房间是干嘛的,更不知道水管和电线都藏在哪里。很多新手会一头扎进蓝图或者代码里,折腾半天角色移动和摄像机,结果项目稍微复杂一点,就变得一团乱麻,想加个新功能都不知道该把文件放哪。
“深入UE5第三人称游戏:目录结构全解析与代码示例”这个主题,就是要帮你解决这个核心痛点。它不只是一个文件夹列表,而是理解UE5项目如何组织、各个模块如何协作的“施工蓝图”。一个清晰、合理的目录结构,是项目可维护、可扩展、团队协作顺畅的基石。对于独立开发者来说,它能让你在几个月后还能轻松找到并修改某个功能;对于团队项目,它更是确保不同成员的工作不会互相覆盖、资产能够高效复用的关键。
从最新的UE5.6开始,官方模板本身就已经提供了更模块化、更清晰的起点,比如新增的“战斗”、“横版卷轴”等变体,其实就是在基础结构上做了不同的功能预设。理解这个基础结构,你才能更好地利用这些模板,甚至打造出属于你自己的、适应特定类型游戏的项目框架。接下来,我会以一个标准的UE5.6第三人称模板项目为蓝本,带你一层层拆解它的目录,并穿插关键的C++和蓝图示例,让你不仅知道“是什么”,更明白“为什么这么放”以及“我该怎么用”。
2. 核心目录结构深度拆解
当你创建一个全新的“第三人称游戏(C++)”项目后,在内容浏览器中看到的Content文件夹,就是所有游戏资源(资产)的家。但UE5的项目远不止Content,我们得从项目根目录开始看起。
2.1 项目根目录:引擎与源码的桥梁
在文件资源管理器里打开你的项目文件夹,你会看到类似这样的结构:
MyThirdPersonGame/ ├── .vs/ # Visual Studio相关文件(如果使用) ├── Binaries/ # 编译生成的二进制文件(.dll, .exe等) ├── Build/ # 构建过程中的中间文件 ├── Content/ # **核心资产目录,编辑器内主要操作区域** ├── DerivedDataCache/ # 派生数据缓存,加速资源加载 ├── Intermediate/ # 编译中间文件(.obj, .pch等) ├── Saved/ # 自动保存文件、配置、日志、屏幕截图 ├── Source/ # **C++源代码目录** │ ├── MyThirdPersonGame/ # 主模块源代码 │ ├── MyThirdPersonGame.Target.cs # 打包目标设置 │ └── MyThirdPersonGameEditor.Target.cs # 编辑器目标设置 ├── MyThirdPersonGame.uproject # 项目描述文件 └── README.mdSource/目录:这是C++项目的命脉。MyThirdPersonGame子文件夹对应你的主游戏模块。里面的MyThirdPersonGame.Build.cs文件定义了模块的依赖(比如依赖GameplayAbilities模块才能用GAS)。Private和Public文件夹区分了头文件的可见性,这是良好的C++工程习惯。.Target.cs文件则决定了你打包出来的是客户端(Client)还是服务器(Server)程序。
Content/目录:这是我们在编辑器中花费最多时间的地方。它的结构逻辑直接决定了项目的可维护性。
2.2 Content目录的标准组织范式
一个经过良好规划的Content目录,应该像一座图书馆,有明确的分区,而不是一个大仓库。以下是一种经过大量项目验证的、高度可扩展的结构:
Content/ ├── **Art/** # 所有美术资源 │ ├── Characters/ # 角色模型、骨骼、动画 │ │ ├── Hero/ # 主角专用资源 │ │ ├── Enemy/ # 敌人资源 │ │ └── Common/ # 共享的骨骼、动画蓝图 │ ├── Environments/ # 环境资产 │ │ ├── Meshes/ # 静态网格体(岩石、建筑) │ │ ├── Materials/ # 材质 │ │ └── Textures/ # 纹理 │ └── UI/ # 用户界面素材(图标、字体、图集) │ ├── **Audio/** # 音效与音乐 │ ├── SFX/ # 音效(脚步声、枪声、UI反馈) │ └── Music/ # 背景音乐 │ ├── **Blueprints/** # **核心蓝图目录** │ ├── **Characters/** # 角色相关蓝图 │ │ ├── **BP_ThirdPersonCharacter** # 玩家角色主蓝图(核心!) │ │ ├── BP_Enemy_Base/ # 敌人基类 │ │ └── Components/ # 角色组件(如生命、背包) │ ├── **GameModes/** # 游戏模式 │ │ └── BP_ThirdPersonGameMode # 主游戏模式 │ ├── **Weapons/** # 武器系统 │ ├── **Interactables/** # 可交互物(门、宝箱) │ └── **Utilities/** # 工具类、管理器蓝图 │ ├── **Maps/** # 关卡文件 │ ├── MainMenu.umap # 主菜单关卡 │ ├── Level_01.umap # 游戏关卡 │ └── Test/ # 测试用关卡 │ ├── **Materials/** # **全局或通用材质** │ ├── M_Common_Base # 基础材质 │ └── Functions/ # 材质函数库 │ ├── **Plugins/** # 项目专用插件 │ └── **ThirdParty/** # 第三方资产包(整理后放入)注意:UE5.6的模板默认结构可能更简单,但当你开始正式开发时,强烈建议你尽早迁移到类似上述的结构。一开始就建立规范,比后期重构要轻松一百倍。
为什么这么分?核心思想是“按功能模块划分”,而不是“按文件类型划分”。你不会看到一个叫All_Blueprints的文件夹,里面塞满所有类型的蓝图。将BP_ThirdPersonCharacter放在Blueprints/Characters/下,将敌人AI放在Blueprints/AI/下,这样当你的角色系统变得复杂(比如增加技能、装备时),所有相关文件都在一个逻辑单元内,查找和修改的效率极高。
2.3 模板自带结构的解析与优化
打开UE5.6的第三人称模板,你可能会看到类似/Content/ThirdPerson/、/Content/ThirdPersonBP/或/Content/__ExternalActors__等目录。这是模板为了自身整洁和演示方便设置的。
ThirdPerson/或ThirdPersonBP/:这里通常存放了模板的核心资产,如角色网格体(Mannequin)、动画蓝图、示例地图和基础材质。对于新项目,我建议不要直接在模板目录里修改。更好的做法是:将其中的核心资产(如角色骨架、动画蓝图)复制或迁移到你自定义的Art/Characters/Common/目录下,然后在你的BP_ThirdPersonCharacter中引用这些迁移后的资产。这样可以将模板的“演示代码”与你的“项目代码”清晰分离。__ExternalActors__和__ExternalObjects__:这是UE5引入的“Actor分块”和“外部对象”系统生成的文件夹。它们用于支持世界分区(World Partition)功能,将大型开放世界关卡的Actor数据分散存储,提升加载和协作效率。对于新手项目,如果不用世界分区,可以暂时忽略它们。不要手动在里面创建或删除文件。
实操心得:我个人的习惯是在项目启动第一天,就按照Art/,Blueprints/,Maps/等结构创建好文件夹。然后从模板中只提取真正需要的“原料”(如骨骼模型、基础动画),像BP_ThirdPersonCharacter这样的核心逻辑蓝图,我会基于模板的C++类或蓝图重新创建一份,放在我自己的Blueprints/Characters/目录下,确保项目从根上就是整洁的。
3. 关键蓝图与C++类关联解析
目录是骨架,蓝图和C++类就是血肉。理解它们之间的继承和引用关系,是掌握项目结构的关键。
3.1 玩家角色:从C++基类到蓝图实例
这是第三人称游戏最核心的链条。在UE5.6 C++模板中,关系通常是这样的:
C++ 基类 (
Source/MyThirdPersonGame/): 你会有一个ATThirdPersonCharacter类(类名可能随项目名变化),它继承自ACharacter。这个类用C++编写了最基础的角色移动、摄像机控制逻辑。// 示例:在C++角色头文件中声明一个可供蓝图调用的跳跃力参数 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement") float JumpZVelocity = 1000.0f; // 以及一个在蓝图中可实现的事件 UFUNCTION(BlueprintNativeEvent, Category="Combat") void OnAttackHit();主角色蓝图 (
Content/Blueprints/Characters/): 在内容浏览器中,你会基于ATThirdPersonCharacter类创建一个蓝图,通常命名为BP_ThirdPersonCharacter。这个蓝图实例化了C++类,并在这里进行可视化配置和扩展:- 组件面板:添加摄像机弹簧臂(
SpringArm)、跟随摄像机(Camera)、骨骼网格体(SkeletalMesh)。 - 细节面板:设置网格体资产、动画蓝图类、碰撞胶囊体大小。
- 事件图表:编写更上层的游戏逻辑,如处理输入、播放特定动画、与UI交互。
- 蓝图会编译成一个特殊的
uasset文件,存放在你指定的目录下。
- 组件面板:添加摄像机弹簧臂(
游戏模式中的设置 (
Content/Blueprints/GameModes/):BP_ThirdPersonGameMode蓝图里,有一个关键属性叫“Default Pawn Class”,必须将其设置为你的BP_ThirdPersonCharacter。这样游戏运行时,才会生成你精心制作的角色。
为什么这么设计?C++负责性能关键和底层通用的逻辑(如移动组件计算),蓝图负责快速迭代和表现层的逻辑(如播放一个受击音效、触发粒子特效)。这种分工让程序员和设计师能高效协作。
3.2 动画蓝图与状态机的存放逻辑
动画蓝图是驱动角色动作的核心。在标准的Art/Characters/Common/目录下,你可能会看到:
ABP_ThirdPerson(动画蓝图):它引用了角色的骨骼。ThirdPerson_AnimBP(动画实例):这是动画蓝图使用的C++类(如果存在)。- 各种动画蒙太奇(
AM_Attack,AM_HitReact)也应放在角色附近的Animations/子文件夹中。
最佳实践:为不同类型的角色创建不同的动画蓝图子类。比如,ABP_Hero继承自ABP_ThirdPerson,并重写英雄特有的动画状态;ABP_Enemy_Melee则可能有一套更简单的状态机。它们都应该放在各自角色类型的目录下(如Art/Characters/Hero/和Art/Characters/Enemy/),而不是全部堆在Common里。
3.3 数据资产与配置的规范化管理
随着游戏系统复杂化,你会需要很多配置数据,比如角色的初始生命值、武器的伤害值、任务的描述文本。UE5提供了强大的DataAsset(数据资产)和DataTable(数据表)功能。
目录建议:
Content/ ├── **Data/** # 专门的数据目录 │ ├── **Characters/** # 角色数据 │ │ ├── DA_Hero_Stats # 英雄属性数据资产 │ │ └── DT_Enemy_Stats.csv # 敌人属性数据表(可用Excel编辑) │ ├── **Weapons/** # 武器数据 │ ├── **Items/** # 物品数据 │ └── **Gameplay/** # 游戏全局数据 │ └── DA_GameSettings # 游戏难度、物理常数等代码示例(C++定义数据资产):
// 在头文件中定义一种武器数据资产 UCLASS(BlueprintType) class MYTHIRDPERSONGAME_API UWeaponDataAsset : public UPrimaryDataAsset { GENERATED_BODY() public: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon") FString WeaponName; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon") float BaseDamage = 20.0f; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon") UStaticMesh* Mesh; // 引用武器模型 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon") USoundBase* FireSound; // 引用开火音效 };然后在Content/Data/Weapons/目录下创建基于此类的数据资产(如DA_AssaultRifle),像配置Excel一样填写数值和引用资产。最后在你的武器蓝图或C++代码中加载并使用这个数据资产。这样做的好处是:平衡调整无需重新编译代码或修改蓝图逻辑,策划人员可以直接在编辑器中修改数据资产。
4. 模块化设计与代码组织实战
当项目规模增长,把所有代码都写在角色类或游戏模式类里会变得难以维护。模块化设计是必然选择。
4.1 使用Actor组件分解功能
UE5的ActorComponent是模块化的利器。想象一下,如果你的BP_ThirdPersonCharacter蓝图事件图表有几千个节点,找起来简直是噩梦。正确的做法是把不同的功能抽离成组件:
- 健康组件 (
UHealthComponent):负责生命值、伤害处理、死亡事件。 - 背包组件 (
UInventoryComponent):负责物品的拾取、存储、使用。 - 技能系统组件 (
UAbilitySystemComponent):如果你使用Gameplay Ability System (GAS)。 - 交互组件 (
UInteractionComponent):处理玩家与场景中物体的交互逻辑。
目录对应:这些组件的C++类自然放在Source/MyThirdPersonGame/的Private/和Public文件夹中。而它们的蓝图实例(如果需要配置默认值)可以放在Content/Blueprints/Components/目录下。
代码示例(简化的健康组件头文件):
// HealthComponent.h #pragma once #include "Components/ActorComponent.h" #include "HealthComponent.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, NewHealth, float, Delta); UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class MYTHIRDPERSONGAME_API UHealthComponent : public UActorComponent { GENERATED_BODY() public: UHealthComponent(); UFUNCTION(BlueprintCallable, Category="Health") void TakeDamage(float DamageAmount); UPROPERTY(BlueprintAssignable, Category="Health") FOnHealthChanged OnHealthChanged; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Health") float MaxHealth = 100.0f; protected: virtual void BeginPlay() override; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Health") float CurrentHealth; UFUNCTION() void OnOwnerDied(AActor* DeadActor); };然后在角色蓝图中,你只需要添加这个组件,并在事件图表中绑定OnHealthChanged委托来更新UI或播放受伤动画。功能清晰,且可以在任何需要生命值的Actor(如敌人、可破坏物)上复用。
4.2 创建独立的游戏功能模块
对于更庞大、独立的系统,如“任务系统”、“对话系统”或“存档系统”,可以考虑将它们组织成独立的UE模块(Module)。这需要修改.Build.cs文件并创建新的模块目录。对于中型项目,一个常见的简化方法是,在Source/目录下为每个主要系统创建子目录,并在其中组织相关的C++类。
Source/MyThirdPersonGame/ ├── MyThirdPersonGame.Build.cs ├── **Public/** │ ├── Character/ │ ├── **QuestSystem/** # 任务系统相关头文件 │ │ ├── Quest.h │ │ └── QuestManager.h │ └── ... └── **Private/** ├── Character/ ├── **QuestSystem/** # 任务系统相关实现文件 │ ├── Quest.cpp │ └── QuestManager.cpp └── ...4.3 蓝图函数库与宏库的运用
对于一些在多个蓝图中重复使用的纯逻辑片段(比如计算屏幕位置、格式化时间字符串),不要到处复制粘贴节点。应该创建“蓝图函数库(Blueprint Function Library)”或“宏库(Macro Library)”。
- 蓝图函数库 (C++或蓝图):适合封装较复杂、通用的算法。放在
Content/Blueprints/Utilities/下,例如BFL_MathUtilities。 - 宏库:适合封装一组固定的节点序列,作为“子蓝图”使用。也放在
Utilities/下,例如ML_CommonAnimations。
这样做的好处是“一处修改,处处更新”。当你发现某个通用计算有bug时,只需要修改函数库或宏库,所有引用它的蓝图都会自动修正。
5. 项目设置、插件与打包配置
项目结构不仅体现在Content里,引擎和项目的全局设置也至关重要。
5.1 项目设置中的路径配置
打开编辑 -> 项目设置,有几个关键位置影响目录:
- 地图和模式:这里设置的“默认地图”和“游戏默认模式”,直接决定了项目启动时加载哪个
/Content/Maps/下的关卡和哪个/Content/Blueprints/GameModes/下的游戏模式。 - 输入:这里绑定的操作映射(Action Mappings)和轴映射(Axis Mappings),会被你的角色蓝图或C++代码引用。清晰的输入命名(如
IA_Jump,IA_Attack)对于后续维护非常重要。 - 打包:这里的“项目”设置决定了打包时包含哪些地图、是否使用
Pak文件等。务必在“要打包的附加非资产目录”中,添加任何你放在项目根目录下但不在Content里的配置文件目录(如Config/自定义的.ini文件)。
5.2 插件的管理与放置
插件分为引擎插件和项目插件。
- 引擎插件:安装在引擎目录下,对所有项目生效。不建议新手随意安装,可能导致项目迁移到其他电脑时因缺少插件而报错。
- 项目插件:放在你项目的
Plugins/目录下。这是推荐的方式。当你从市场购买或下载了一个插件(如高级地形工具、对话系统),应该将其解压到项目根目录/Plugins/下。这样,整个项目(包括Content和Source)就可以引用该插件的内容,并且项目拷贝到别处时,插件会一并带走。
注意事项:有些插件会向Content目录注入自己的内容文件夹(如Plugins/PluginName/Content)。在引用这些资产时,路径前缀要写对。最好在项目初期就规划好插件使用,避免后期因插件冲突导致项目混乱。
5.3 版本控制下的目录忽略策略
如果你使用Git、Perforce或SVN进行版本控制,正确设置忽略(.gitignore等)文件能节省大量空间和同步时间。以下目录通常应该被忽略:
Binaries/DerivedDataCache/Intermediate/Saved/.vs/Build/
必须纳入版本控制的核心:
Content/目录下的所有uasset和umap文件(但注意大文件管理)。Source/目录下的所有C++源代码文件(.h,.cpp,.Build.cs等)。Config/目录下的.ini配置文件。Plugins/目录下的项目插件。项目名.uproject文件。
对于Content中的临时测试资产或超大原始资源文件(如ZBrush源文件、PSD源图),可以考虑使用.gitignore规则过滤,或使用诸如git-lfs(大文件存储)来管理。
6. 从结构到实践:一个功能添加的完整流程
让我们通过一个具体的例子——为第三人称角色添加一个“蹲伏”功能,来串联上面讲的所有知识,看看清晰的目录结构如何指导开发。
步骤1:规划与设计功能需求:按下左Ctrl键,角色进入蹲伏状态,移动速度降低,碰撞胶囊体高度减小。
步骤2:在C++层实现核心逻辑(可选但推荐)考虑到蹲伏是一个与移动、状态相关的核心功能,在C++角色基类中添加更为合适。
- 打开你的C++角色类头文件(如
ThirdPersonCharacter.h)。 - 添加蹲伏状态变量和函数声明。
// ThirdPersonCharacter.h UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement") bool bIsCrouching; UFUNCTION(BlueprintCallable, Category="Movement") void StartCrouch(); UFUNCTION(BlueprintCallable, Category="Movement") void StopCrouch(); - 在源文件(
.cpp)中实现函数,调用UE自带的Crouch()和UnCrouch()函数,并可以在这里修改移动速度等属性。
步骤3:在蓝图层配置输入和表现
- 在
项目设置 -> 输入中,添加一个新的操作映射(Action Mapping),命名为IA_Crouch,绑定按键Left Control。 - 打开你的
BP_ThirdPersonCharacter(位于Content/Blueprints/Characters/)。 - 在事件图表中,获取
IA_Crouch的输入事件。Pressed时,调用C++暴露的StartCrouch函数(或直接调用Crouch节点)。Released时,调用StopCrouch函数(或UnCrouch)。
- (可选)在动画蓝图
ABP_ThirdPerson(位于Art/Characters/Common/)中,根据bIsCrouching变量,驱动一个蹲伏的姿势混合或状态机过渡。
步骤4:数据化配置如果觉得蹲伏的速度、胶囊体缩放比例可能需要频繁调整,可以将其数据化。
- 创建一个数据资产类
UCharacterMovementData(放在Source/的相应模块)。 - 在其中定义
CrouchSpeed,CrouchCapsuleHalfHeight等属性。 - 在
Content/Data/Characters/下创建该数据资产的实例DA_Hero_Movement。 - 修改你的C++角色类,持有对这个数据资产的引用,并在蹲伏时读取其中的数值。
步骤5:添加音效和特效
- 在
Content/Audio/SFX/目录下导入蹲伏起立的音效文件。 - 在
BP_ThirdPersonCharacter的蹲伏开始和结束事件中,添加播放音效的节点。 - 如果需要灰尘粒子,在
Content/Art/Effects/下导入粒子资产,并在蓝图中触发。
通过这个流程,你会发现每个步骤产生的文件,都自然而然地落入了我们之前规划好的目录结构中:C++代码在Source/,主蓝图在Blueprints/Characters/,动画在Art/Characters/Common/,数据在Data/Characters/,音效在Audio/SFX/。整个功能添加过程有条不紊,未来无论谁接手这个项目,都能快速定位所有相关资源。
7. 常见问题与排查技巧实录
即使结构清晰,开发中也会遇到各种路径和引用问题。这里记录几个高频问题:
问题1:编译成功,但编辑器提示“无法加载资产”或蓝图上有红色错误标记。
- 排查:这通常是资产引用丢失。首先检查错误信息,看是哪个文件找不到。然后:
- 在内容浏览器中搜索这个丢失的资产名,看它是否被移动或重命名。
- 如果资产确实存在,右键该蓝图 -> “资源操作” -> “修复重定向器”,尝试让引擎自动修复引用。
- 最根本的办法是,在蓝图编辑器中,找到报错的节点或属性,手动从内容浏览器里拖拽正确的资产重新赋值。
- 预防:永远在编辑器内进行移动和重命名操作,不要直接在操作系统文件管理器里操作
uasset文件。编辑器会处理引用更新。
问题2:从别处复制项目或资产后,材质、纹理显示为紫色或黑色。
- 排查:这是典型的材质或纹理引用丢失。打开有问题的材质实例,检查标红的纹理采样节点。
- 解决:
- 确保源纹理文件确实被复制到了当前项目的
Content目录下。 - 如果纹理在另一个项目的相同相对路径下,引用可能会自动连接。如果没有,需要手动重新指定纹理路径。
- 对于批量问题,可以尝试在内容浏览器中,选中所有丢失引用的材质,右键“重新导入”。
- 确保源纹理文件确实被复制到了当前项目的
问题3:打包后游戏运行时,某些功能失效(如特定音效不播放、角色缺少某个技能)。
- 排查:这常常是因为打包设置没有包含所有必需的资产。
- 检查
项目设置 -> 打包中的“要打包的附加非资产目录”。 - 更重要的是,检查你的主地图(在
项目设置 -> 地图和模式中设置的默认地图)是否通过某种动态加载的方式引用到了其他地图或资产。只有被主地图直接或间接引用的资产,才会被默认打包进去。 - 对于动态加载的资产(如通过
LoadObject或Streaming),需要在“打包设置”的“资产列表”中手动添加,或者确保它们在一个被打包的关卡中被引用(哪怕是一个隐藏的引用)。
- 检查
- 技巧:使用编辑器菜单栏的
窗口 -> 开发者工具 -> 引用查看器,输入你的主地图,可以查看所有被引用的资产链,确保没有遗漏。
问题4:团队协作时,合并蓝图或C++代码经常冲突。
- 对于蓝图:尽量将逻辑模块化,使用函数、宏和组件。减少在单个巨型事件图表中操作。合并时,冲突通常发生在
.uasset文件的二进制部分,很难手动解决,沟通和分工明确是关键。 - 对于C++:遵循良好的编程规范,一个
.h文件只声明一个类,功能职责单一。使用版本控制系统的分支策略,如Git-Flow。在合并代码前,确保在本地能成功编译。
问题5:项目越来越大,打开和加载速度变慢。
- 优化目录结构本身:避免在一个文件夹内放置成千上万个文件。使用子文件夹进行合理分类。
- 使用迁移功能:对于确定不再使用或只是暂时备份的资产,不要简单删除(会破坏引用)。可以将其迁移(右键资产 -> 资产操作 -> 迁移)到一个专门的
Deprecated或Archive内容文件夹中,然后从项目中移除该文件夹的引用。这样既清理了工作区,又保留了资产文件以备不时之需。 - 管理派生数据:定期清理项目根目录下的
DerivedDataCache和Intermediate文件夹(在关闭编辑器后进行),可以释放大量磁盘空间,但下次打开项目时需要重新生成(会变慢)。