1. 先想清楚一件事:Unity学习笔记的目录,整理的到底是什么
做Unity这些年,见过太多人笔记记得不少,真到用的时候一个都翻不出来。我自己也经历过这个阶段:三四个文件夹里躺着几十个md,命名从笔记1到新建文本文档(7),某个阴影闪烁的解决方案明明记过,硬是搜了三遍关键词都没找着,最后重新踩了一遍坑才想起来——那次之后我才下定决心把笔记重做一遍。
这篇东西就是把我这套Unity学习笔记目录体系完整拆开讲一遍。它解决的核心问题很具体:当你的Unity知识面从"能跑个球"扩展到渲染、UI、XR、小游戏发布、工程优化以后,怎么让笔记变成能检索、能复用、能反复回看的知识资产,而不是一堆死档案。
适合谁看?刚学Unity两三个月、开始觉得东西越来越多的新手;做了两三年、知识点分散在各处但没体系的中间层;还有准备系统补课、打算把零散经验沉淀成册的人。目录结构这块我会给一套可以直接抄的,但更重要的是背后的分类逻辑,因为每个人的项目方向不一样,照搬结构不照搬逻辑,三个月后又乱了。
先说一个反常识的结论:Unity笔记目录的第一层分类,不该按"知识模块"分,而该按"我能用它干什么"分。原因后面细说。
2. 目录结构的分层设计:为什么很多人第一层就分错了
2.1 我试过的三种分类法,最后留下了哪个
最早我按官方文档的结构来分:基础、脚本、渲染、物理、UI、网络。听起来很正规,实际用起来是灾难。因为一个真实问题往往横跨好几个模块。比如"微信小游戏里视频播放不出来",它既沾渲染也沾平台适配还沾资源加载,你按模块分,这条笔记放哪都是错的,最后就是随手扔一个地方,再也找不到。
第二种我试过按"时间"分,2023-03、2023-04这样。优点是记录方便,缺点是检索全靠猜时间,而且同一个知识点会被拆到好几个月份里,形成不了体系。
第三种,也是我最后留下来的,是按"使用场景/能力域"分。核心区别是:模块分类回答的是"这是什么",场景分类回答的是"我遇到这类问题该去哪找"。这个转变看着小,实际影响很大。
具体第一层我留了七块,覆盖从入门到工程化的完整链路:
| 序号 | 目录名 | 收录范围 | 典型条目 |
|---|---|---|---|
| 00 | 环境与工具 | 安装、版本、编辑器、插件 | Unity Hub、版本选择、Tooltips插件 |
| 10 | 引擎基础 | 生命周期、组件、脚本、特性 | Update顺序、宏定义、特性Attribute |
| 20 | 渲染与图形 | Shader、材质、光照、阴影 | 阴影问题、Renderer包围盒 |
| 30 | UI与交互 | UGUI、点击、显隐、对话表情 | 按钮点击范围、UI显隐方案 |
| 40 | 运行时系统 | 摄像机、导航、物理、通信 | 摄像机跟随、Navigation、串口 |
| 50 | 工程化与优化 | 性能、混淆、打包、资源 | 数据块大小、gameassembly.dll |
| 60 | 跨平台与XR | 小游戏、Web、Pico4、MR | 微信小游戏、Cesium for Unity |
这套分类的好处是,我遇到"UI显隐用哪种方案"这种问题,脑子的第一反应是"UI与交互",直接进30。而"MR切VR怎么搞",进60。路径是一步到位的,不需要先在脑子里做一次模块归类。
2.2 顶层目录之后,第二层怎么切
顶层定了之后,第二层我统一用"问题域+解决方案"的方式命名,而不是用"知识点"命名。举个对比:
- 不好的第二层:
UI显隐、按钮点击 - 好的第二层:
UI显隐的三种做法对比、按钮点击范围扩大
差别在哪?前者是名词,后者是一个带结论的短句。笔记的本质是"我解决了什么问题",用结论做标题,检索的时候你搜的是问题,匹配到的却是方案,命中率高很多。这一点我是从代码的命名习惯里迁移过来的——函数名写GetVisible不如写IsPanelVisible,一个道理。
第三层才是真正的具体条目,每一条笔记一个文件。文件名我强制要求带上日期前缀和技术关键词,格式是20240612-UGUI-Button点击范围扩大.md。日期放前面是为了排序稳定,技术关键词是为了模糊搜索。别看这个规范很土,它救过我很多次——grep一个关键词,按时间排序,能直接看到自己对这个知识点的认知是怎么演进的。
注意:目录不要超过三层。我见过有人分到第五层,结果每次存笔记都要想半天放哪,最后干脆新建一个文件夹。超过三层的部分,用标签解决,不要用文件夹解决。
3. 环境与工具层:安装、版本、编辑器这三块最容易反复踩坑
3.1 版本选择:为什么我坚持把这条单独记一个大文件
Unity的版本管理是个长期折磨人的问题。每年一个新的大版本,LTS和Tech Stream两条线并行,项目一旦定型就很难动版本。我见过团队因为用了非LTS版本,升级时第三方插件集体不兼容,光适配花了两周。
我的笔记里专门有一份版本选择决策.md,记录的是这种规则:
- 商业项目锁LTS,因为LTS至少两年维护期,bug修复有保障,插件生态也跟得上。
- 学习阶段可以跟最新Tech Stream,能提前接触到新特性,但要在笔记里标注"此处API可能在下一个版本变动"。
- 同一个工程不要混装多个Unity版本,尤其是用了
Library缓存的情况下,切版本容易出莫名其妙的问题。
安装本身没什么好讲的,Unity Hub走正规流程就行。真正值得记的是安装后的环境检查清单,我列一下:
- 确认Editor版本号完整(例如2022.3.x LTS的具体patch号),记进笔记,因为很多问题只在特定patch上出现。
- 检查目标平台模块是否勾选(Android/iOS/WebGL/Windows),没勾的话后面打包报错会很难查。
- 检查脚本编辑器绑定(VS/Rider/VSCode),外部工具没绑定的话双击脚本打不开。
- 记录磁盘路径,Unity工程路径带中文或空格会让某些构建工具出问题,这个坑很经典。
关于网上常说的"中文版下载""国际版下载"之类,我的建议是:走官方渠道,选对应语言包即可,不要为了图快去装来路不明的整合包。工程一旦被污染,排查成本远超省下的那几分钟。这条我写在了笔记最上面,用加粗标注。
3.2 编辑器与工程设置里的高频条目
这一块看着琐碎,但几乎每个项目都会碰到。我的笔记里固定在00-环境与工具下维护三份清单:
分辨率设置。这里面有个容易忽略的点:Game视图的分辨率和实际构建出来的分辨率不是一回事,而且UI Scale Mode选Constant Pixel Size还是Scale With Screen Size,直接决定你在不同机型上的表现。我习惯把每个项目的分辨率方案连同一张真机截图一起存进笔记,过半年回来看一眼就明白当时为什么这么定。
宏定义(Scripting Define Symbols)。这是做平台差异化的核心手段,比如给微信小游戏单独定义一个WX_MINIGAME,用#if包住平台代码。我自己踩过的坑是:宏定义写完后没切换平台,代码不生效,以为是自己代码写错了,查了半天。所以笔记里我特意加了一条:改完宏定义,先切一次平台再编译。
Tooltips插件这类辅助工具。有些项目会用Tooltips插件做运行时提示,好处是不用改代码就能看到Inspector字段说明。但插件版本和Unity版本强绑定,记的时候必须写清楚配套版本,否则半年后升级Editor,插件直接报错。
4. 引擎基础层:脚本生命周期、宏定义、特性这些"基础"其实最不基础
4.1 脚本执行顺序和生命周期,值得单独建一份速查表
新人最常问的就是"为什么我的代码不执行""为什么Awake里拿不到另一个对象"。这些问题的答案基本都在执行顺序里。我笔记里有一份表,专门记Awake、OnEnable、Start、FixedUpdate、Update、LateUpdate、OnDisable、OnDestroy的执行时机和典型误用:
| 回调 | 执行时机 | 适合做什么 | 常见误用 |
|---|---|---|---|
| Awake | 对象实例化后、Start前 | 初始化自身引用 | 拿其他对象的Start结果 |
| OnEnable | 每次启用时 | 注册事件、重置状态 | 重复注册导致事件多次触发 |
| Start | 第一帧Update前 | 依赖其他对象的初始化 | 在Start里做重计算拖慢首帧 |
| FixedUpdate | 固定时间步长 | 物理相关 | 在里面读输入导致丢帧 |
| LateUpdate | 所有Update后 | 摄像机跟随 | 在这里改物理状态 |
这份表的价值不在于背,而在于出问题时能三秒定位。比如"摄像机跟随抖动"这个问题,答案基本就在LateUpdate这一行——摄像机跟随必须放LateUpdate,否则会出现和主角同帧执行导致的抖动。这种"问题→原因→修正位置"的三段式记录,比单纯抄官方文档有用得多。
4.2 宏定义和特性(Attribute),我按"用途"归档
[SerializeField]、[RequireComponent]、[ExecuteInEditMode]这些特性,官方文档是按字母顺序排的,但你实际用的时候是按场景想的:"我想在Inspector里显示一个私有字段"、"我想让这个组件强制依赖另一个组件"。所以我笔记里的特性条目全部按用途重写,比如:
- 想在Inspector调试私有变量 →
[SerializeField] - 想让组件自动挂载依赖 →
[RequireComponent(typeof(Rigidbody))] - 想让脚本在编辑模式下跑 →
[ExecuteInEditMode] - 想自定义Inspector显示 → 配
[CustomEditor]
有个实操心得:[ExecuteInEditMode]用起来很爽,能在Scene视图实时看到效果,但它会在编辑器里持续执行,写不好会拖慢编辑器甚至导致崩溃。我现在只在必须实时预览的工具脚本上用,业务脚本一律不用。这条也写在笔记里了,属于"自己踩过才知道"的类型。
5. 渲染与图形层:阴影、包围盒、Shader,笔记要记"现象"而不是"概念"
5.1 阴影问题:用"现象描述"当笔记标题
Unity的阴影问题是个大类,接缝、闪烁、漏光、锯齿、远处阴影丢失,每一种原因都不一样。如果笔记标题写"阴影原理",你回头查的时候根本匹配不上。我的做法是用现象当标题:
阴影边缘锯齿严重怎么办两个物体接触处阴影出现闪烁条纹远处阴影突然消失(阴影距离设置)实时阴影和烘焙阴影交界处出现硬边
每条笔记的结构固定是:现象截图 + 排查路径 + 根因 + 参数改动记录。比如"远处阴影消失",根因通常是Quality设置里的Shadow Distance太小,或者摄像机远裁剪面和阴影距离不匹配。我把当时的参数值一起记下来,包括改了哪个数值、改前改后画面差异。参数值这东西,不记一定会忘。
5.2 Renderer包围盒:这是个容易被忽略但很实用的点
Renderer.bounds(世界空间包围盒)和Mesh.bounds(局部空间包围盒)的区别,我当初也绕了很久。笔记里我用一个具体场景记录:做相机取景或者动态加载剔除的时候,需要判断物体是否在视野内,这时用的是世界空间的Renderer.bounds,因为它已经包含了Transform的缩放。而Mesh.bounds是模型本地的,不受Transform影响。
实操上有个坑:SkinnedMeshRenderer的bounds是估算的,不一定准确,动画幅度大时会出现物体明明在视野内却被剔除掉的情况。解决办法是手动扩大bounds或者强制不剔除。这个我在做角色动画项目时踩过,画面里角色突然消失,查了半天才发现是剔除问题。笔记里我附了一段手动扩大bounds的代码:
// 手动扩大SkinnedMeshRenderer包围盒,避免动画导致误剔除 void ExpandBounds(SkinnedMeshRenderer smr) { Bounds b = smr.localBounds; b.Expand(0.5f); // 按需扩大,别太夸张,影响剔除效率 smr.localBounds = b; }5.3 Shader笔记的归档方式
Shader这块我单独在20-渲染与图形下开了一个子目录,因为它的学习曲线和普通脚本完全不是一个量级。归档逻辑是按"效果"分,不是按"语法"分:描边、溶解消失、水面、卡通渲染各一个文件。
有个具体需求热词里出现过——"脚本控制逐渐消失"。这种消失效果,纯脚本能做(改材质透明度),但更稳的是Shader配合。我的笔记里记了两种方案对比:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 脚本改透明度 | 改Material的Alpha | 简单、无需写Shader | 半透明排序问题、性能一般 |
| Shader溶解 | 用噪声贴图裁剪 | 效果好、可控 | 需要写Shader、调试成本高 |
脚本方案里要注意,直接改material.color会创建材质实例,批量对象时会有性能开销,正确做法是用MaterialPropertyBlock。这个细节我在笔记里专门用红字标了出来,因为它是那种"不踩一次绝对想不到"的点。
6. UI与交互层:显隐方案、点击范围、对话表情,全是实战细节
6.1 UI显隐:SetActive、LocalScale、移出相机,到底用哪个
这是热词里出现频率极高的问题,也是我在项目里反复纠结过的。三种做法的差异我整理成了笔记里的一张核心表:
| 方案 | 原理 | 性能开销 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| SetActive(false) | 禁用GameObject | 有激活/禁用开销,会触发OnEnable/OnDisable | 长期不用的面板 | 频繁切换会有GC和事件重注册问题 |
| 改LocalScale为0 | 缩放隐藏 | 开销小,但仍在渲染管线里 | 高频切换的小元素 | 布局组件可能报错,锚点会乱 |
| 移出相机视野 | 改位置 | 开销最小 | 极致性能场景 | 逻辑上对象仍存在,容易误触发 |
我个人的结论写得很明确:常规面板用SetActive,高频小元素用LocalScale,性能敏感的批量元素才考虑移出相机。原因在于SetActive会触发完整的启用/禁用生命周期,如果你在OnEnable里注册了事件,频繁开关会不断产生委托分配,久了就是GC压力。这个分析过程我也记在笔记里了,因为它是"为什么"层面的东西,比结论本身更值钱。
还有个细节:如果把面板移出相机视野,它上面的按钮理论上还是能通过代码触发点击的,如果逻辑上有依赖"不可见即不可点",就得自己加状态判断。这个坑我在做弹窗系统时遇到过,弹窗"隐藏"了但还能被点到,排查了很久。
6.2 按钮点击范围扩大:不用改图片的几种做法
UGUI默认的点击范围就是Image的矩形。想扩大点击范围,常见做法有几种,我笔记里记了三种并做了对比:
- 加一个透明的Image当父物体或子物体,设置
raycastTarget为true,尺寸放大。简单直接,但要注意它会拦截其他元素的点击,父子层级和Canvas的排序要理清楚。 - 用
Image.alphaHitTestMinimumThreshold,这个属性可以让点击检测只响应像素不透明区域,反向用来做异形按钮。但要注意它需要把图片的Read/Write Enabled打开,会增加内存。 - 自己写一个实现
IPointerClickHandler的组件,在OnPointerClick外扩判断,或者重写IsRaycastLocationValid来扩大有效区域。
第三种最灵活,我给了一段参考实现:
// 扩大按钮点击范围:通过重写射线检测有效性 public class ExpandClickArea : MonoBehaviour, ICanvasRaycastFilter { public Vector2 padding = new Vector2(20f, 20f); public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) { RectTransform rt = transform as RectTransform; Vector2 local; RectTransformUtility.ScreenPointToLocalPointInRectangle( rt, sp, eventCamera, out local); Rect r = rt.rect; r.xMin -= padding.x; r.xMax += padding.x; r.yMin -= padding.y; r.yMax += padding.y; return r.Contains(local); } }这段代码的好处是不改动原有UI结构,加个组件就能扩范围,但要注意它只能扩展本元素的有效区域,如果外层有遮挡元素,还是会被挡。这个先后关系必须搞清楚,否则会出现"我明明扩了范围还是不响应"的情况。
6.3 对话与表情:表现层笔记的组织方式
热词里有个"根据对话变化表情",这是个典型的剧情系统需求。我在笔记里把它归到30-UI与交互下的表现层条目,记录的是数据驱动思路:对话数据结构里带一个表情字段,播放时根据字段切换Sprite或者切换Animator状态。核心是把"文案"和"表现"解耦,不要让每句话都硬编码表情。
同一个目录下还有Timeline相关的笔记。Timeline做剧情演出很方便,但有个经典问题:Timeline播放时如果对象被销毁或者被其他逻辑改动,轨道会报错。我的笔记里记录了一条预防措施——Timeline播放前先确认它要控制的对象的生命周期,必要时用PlayableDirector的信号(Signal)机制在关键帧回调,而不是让轨道直接控制对象属性。
7. 运行时系统与工程化:摄像机、导航、优化、混淆
7.1 摄像机跟随:几种写法对应的场景
摄像机跟随看着简单,实际有细分。我的笔记里记了三种:
- 硬跟随:每帧同步位置,最简单,但画面会有生硬感。
- 平滑跟随(Lerp/SmoothDamp):用插值让镜头有延迟感,适合大多数第三人称。注意必须放LateUpdate。
- 带死区的跟随:角色在屏幕中间一个小范围内移动时镜头不动,超出才跟,适合横版或者2.5D。
SmoothDamp比Lerp更适合跟随,因为Lerp在帧率变化时表现不稳定,而SmoothDamp是基于时间步长的。这个区别我写在笔记里,配了一小段对比说明。还有个大坑:摄像机跟随和物理、动画同时作用时会出现抖动,通常是更新顺序问题,解决办法是把跟随时机放到LateUpdate,或者用FixedUpdate配合插值。
7.2 Navigation导航:烘焙和动态障碍
Unity自带的Navigation系统,笔记里记了几个实用点:NavMesh烘焙时,Agent半径和高度设得太小会导致导航网格贴着墙角,角色走位容易卡;动态障碍用NavMeshObstacle配合Carve,但车轮式开合(carve)有性能开销,大量使用要谨慎。这些结论都是具体的参数经验,比"Navigation是什么"有用一百倍。
7.3 工程优化:数据块大小、资源、gameassembly.dll
热词里"限定数据块大小""gameassembly.dll的作用"这两个点,其实都指向构建产物层面。gameassembly.dll是IL2CPP构建后存放编译后C++代码的产物,它体积大是正常的,因为它包含了你所有脚本编译出的原生代码。理解这一点之后就知道:压缩它没意义,能优化的是代码本身和剥离(strip)等级。
数据块大小(比如AssetBundle的chunk)限制,目的是控制加载时的内存峰值。笔记里我记的思路是:按使用场景切Bundle,别按资源类型切。比如一个关卡的所有资源打一个包,而不是把所有贴图打一个包,因为前者加载是整块进出的,后者会导致你只需要一张图却要加载一堆。
7.4 代码混淆与保护
代码混淆这块,笔记里的结论是:混淆解决的是反编译门槛,不是绝对安全。实际做法通常是配合IL2CPP(本身就把IL转成C++了)再做符号混淆,重点是保护核心算法和敏感数据。要注意混淆后可能影响反射调用,如果你的代码里大量用了字符串反射(比如按名字找方法),混淆后可能直接崩。所以混淆前必须做一轮完整的回归测试。这个测试清单我也存在笔记里。
8. 跨平台发布与XR:小游戏、Web、Pico4、数字孪生
8.1 微信小游戏打包的常见卡点
微信小游戏打包,Unity这边有官方的转换工具,但坑不少。笔记里记了几个高频问题:
- 包体限制,首包有大小限制,资源要拆分和CDN化。
- 视频播放方案,小游戏环境对视频的支持和原生平台不同,直接播放在部分环境会失败,需要用平台提供的接口播放,或者在必要时候降级到序列帧/WebGL方案。
- 广告接入,激励视频和插屏广告的调用时机,必须在用户交互之后触发,否则可能被限制。
我的笔记里对视频播放这一条记得特别细,因为当时排查了很久。核心结论是:小游戏环境的视频要走平台专门接口,不能完全依赖原生VideoPlayer,同时要做好播放失败的兜底。
8.2 Web发布到IIS:部署要点
Unity发布WebGL后要部署到服务器,IIS上最常见的两个问题:一是MIME类型没配,.wasm、.data这类文件返回404或者被当成下载;二是压缩格式不匹配,服务器开了gzip但文件不是对应格式。笔记里的解决步骤是:先在IIS里补全MIME类型,再确认IIS的静态压缩和构建时的压缩选项一致。所有配置我都附了截图和具体的配置项路径。
8.3 Pico4、MR切VR、Cesium for Unity与数字孪生
XR这块我笔记里单独开了子目录。Pico4基于Android,打包流程和普通Android工程类似,但要注意SDK版本匹配和渲染管线(URP为主)的兼容。MR切VR这种需求,核心是在运行时切换XR的显示模式或者加载不同的相机配置,笔记里记的是配置切换流程和切换时的资源重新加载问题。
Cesium for Unity做数字孪生场景,离线地图这块的关键是瓦片数据的本地化方案,要把地形和影像数据落到本地存储再加载,否则网络波动会直接影响场景。我笔记里记的是数据准备、路径配置、加载顺序这三步,以及首次加载慢要做的预加载处理。数字孪生项目通常对帧率要求高,所以优化笔记和这部分是交叉引用的,用标签串起来。
9. 让笔记真正能被检索、复用,并且在面试里发挥作用
9.1 命名规范和标签体系,比目录结构更重要
前面说了目录不要超过三层,那多出来的维度怎么表达?靠标签。我在每篇笔记的头部固定写一小段元信息:
# 20240612-UGUI-Button点击范围扩大 tags: #UI #交互 #射线检测 #UGUI platform: Android/iOS/Editor unity_version: 2022.3.x LTS status: 已验证tags用来跨目录检索,platform和unity_version用来过滤,status标记这条笔记是"已验证"还是"待验证"。这个status字段救过我不少次,因为有些笔记是当时随手记的猜想,没实测,如果和已验证的混在一起,下次直接用就会翻车。我现在只信任标记为"已验证"的条目。
命名规范我定了三条硬规则:日期前缀、模块关键词、动作短句。看起来死板,但grep配合排序,检索效率比任何笔记软件的花哨功能都高。
9.2 把笔记变成面试复习清单
热词里有"Unity面试题""Unity和C#八股",这个其实和笔记体系高度重合。我的做法是:每隔一段时间,从现有笔记里反向生成一份自测清单。具体就是把每个H2小节转成一个问题,比如渲染那节转成"Renderer.bounds和Mesh.bounds有什么区别",UI那节转成"UI显隐三种方案的取舍"。能答上来的标绿,答不上来的回去补笔记。
这样做的额外好处是,你的复习材料和你真实做过的东西是绑定的,不是网上抄来的通用八股。面试官问到某个点,你能顺口说出"我在XX项目里遇到过,当时是这么解决的",这个说服力完全不一样。我的笔记里甚至保留了当时的错误截图和排查日志,这些在面试里讲出来就是加分项。
9.3 定期整理:一个每季度做一次的动作
笔记体系不整理一定会烂。我现在每季度做一次清理,动作固定三步:
- 合并重复条目,同一个问题在不同时间记了好几遍的,合并成一条,保留最新的结论。
- 升级status,把"待验证"的要么验证掉,要么直接删掉,别留着当噪音。
- 交叉链接,把相关的条目互相加引用,比如"UI显隐"和"性能优化"之间互链,形成网状结构。
这三步花不了多少时间,但能让笔记一直保持在"可用"状态。我见过太多人的笔记系统死于"只进不出",越堆越多,最后自己都不想打开。
10. 我自己跑这套体系下来的几点真实体会
这套目录我用了挺长时间,中间改过两版。最大的感受是:整理笔记这件事的价值,不在于笔记本身,而在于整理的过程逼你重新理解一遍。很多知识点你以为懂了,写到"为什么要这么做"的时候卡住了,那就是没真懂。
另一个体会是关于"已验证"这个标记的。刚开始我嫌麻烦,什么都写"已验证",结果有次直接照搬一条没验证的笔记去改项目,排查了半天才发现当时记错了。从那以后我严格区分,不确定的就写"待验证",宁可标注得保守一点。
还有个特别小的习惯,我觉得挺有用:每篇笔记最后留一行"下次再看要先确认什么"。比如阴影那条,我写的是"先确认是实时阴影还是烘焙阴影,再看Quality设置"。这行字能让我重新进入这个知识点的时候,不用从头读,直接进入状态。笔记写给别人看是一回事,写给自己看,这种"重启提示"特别省时间。
如果你现在笔记还很乱,别想着一次性重构成完美结构。先从最高频的那一类问题开始,比如UI或者渲染,单独理出一个目录,用一两个星期,跑顺了再扩到其他模块。一次性大重构,坚持不过三天,这个我有过教训。