我的硬盘里现在躺着四十多个 Unity 工程,从最早那个连摄像机都不会摆的 Demo,到后来接手的数字孪生项目、微信小游戏、还有几个 VR 一体机上的实验。真正让我难受的不是工程多,而是笔记散:有的记在工程根目录的 txt 里,有的躺在聊天记录里,有的只存在于我脑子里,过三个月再遇到同一个阴影发黑的问题,还得从头查一遍。所以上个月我花了两天时间,把 Unity学习笔记 全部推翻重做了一遍,从"想到什么记什么"改成一套有骨架、能检索、能持续回填的目录体系。这篇就聊聊这套目录是怎么搭的、每一层该记什么、哪些条目是我踩过坑之后才补进去的,以及这套结构为什么比按功能模块平铺要好用得多。不管你是刚开始学 Unity 的新手,还是已经做过两三个项目、想把自己的经验沉淀下来的开发者,这套组织方式都可以直接抄走改成自己的版本。
1. 目录先分层再分册:为什么我放弃了按"功能模块"平铺的旧目录
1.1 旧目录的三个致命问题
我最早的笔记目录长这样:动画、UI、渲染、物理、脚本、优化、插件……看上去挺整齐,实际上用起来非常痛苦。第一个问题是条目归属模糊。比如"按钮点击范围扩大"这件事,它既算 UI 又算交互,还能扯到射线检测,我每次都不知道该往哪个文件夹里塞,最后就变成随手丢。第二个问题是层级太浅。所有东西都是平级的,一个"优化"文件夹里堆了六十多条笔记,找的时候跟翻垃圾堆一样。第三个问题最要命:没有时间轴。学习是有顺序的,先会装编辑器、再会写脚本、然后才谈得上渲染和优化,但平铺目录把这条顺序彻底抹平了,新人打开一看全是陌生名词,直接劝退。
1.2 四层骨架:工具链层、基础层、表现层、交付层
重做之后我换成了四层结构,核心逻辑是按"我在项目里处在哪个阶段"来分,而不是按"这是引擎的哪个模块"来分。工具链层管的是环境本身,装哪个版本、Hub 怎么用、工程目录里哪些东西不能删;基础层管的是写代码这件事,生命周期、C# 底子、场景对象的日常操作;表现层管画面,渲染、Shader、UI、相机、特效;交付层管上线,打包、部署、混淆、优化。这四层的好处是:任何一个具体问题,你都能顺着"我现在在干什么"快速定位到层,再在层里找册。比如"微信小游戏里视频播不出来",这明显属于交付层里的打包分册,而不是 UI 分册,归属问题一下就解决了。另外,四层天然带顺序,新人从第一层往下读就是一条完整的学习路径。
1.3 每条笔记的最小单元格式
光有目录不够,每条笔记本身也得有固定格式,否则三个月后你连自己写的是什么意思都看不懂。我固定的字段是五个:现象、环境、原因、解法、验证。现象用一句话写清症状,比如"SkinnedMesh 角色在屏幕边缘突然消失";环境写引擎版本和渲染管线,因为 URP 和内置管线的行为经常不一样;原因写机制层面的解释,哪怕当时没完全搞懂,也要留个 TODO;解法贴代码或截图;验证写清楚怎么确认问题真的解决了。额外加一个"关联"字段,指向相关的其他条目,这样笔记之间能连成网。我实测下来,坚持写"原因"这一栏是收益最大的,因为它逼着你去查机制,而不是停在"改了某个参数就好了"。
2. 工具链层笔记:安装、版本、工程结构与那些一次配好就不想再碰的设置
2.1 版本与安装:Hub、LTS、Intel Mac 与语言包
工具链层第一条笔记永远是安装。我的记录方式是把"版本选择"当成一个决策表来记,而不是记操作步骤。选版本的原则很简单:新项目优先选最新的 LTS,因为 LTS 有长期维护,第三方插件兼容性也最好;老项目保持原版本不要动,升级引擎带来的收益往往抵不上修兼容问题的时间成本。安装环节我推荐用 Hub 统一管理,不要单独下载离线安装包到处装,否则同一台机器上三四个版本混在一起,哪天空闲时想删都找不到路径。安装时把"Android Build Support""WebGL Build Support"这类模块按需勾选,用不到的先别装,模块装多了 Hub 的磁盘占用会非常夸张。
Mac 用户有额外的坑。Intel 芯片的 Mac 上装较新的编辑器版本时,要留意官方对 Intel 平台的支持节奏,有些新版本在 Intel 机器上的编辑器体验会明显慢于 Apple Silicon,我一般在 Intel Mac 上会保守地停在 2022.3 系列的稳定补丁版本。安装完成后在偏好设置里把语言切成简体中文,这一步很多新手不知道,翻半天菜单找中文包。另外顺手把脚本编辑器关联好,我习惯用 Rider 或 VS Code,关联好之后双击脚本能直接跳转,省掉每天几十次的手动打开。
2.2 工程结构:Assets 之外的隐藏主角
第二个大类是工程结构。绝大多数人的笔记里只记 Assets 文件夹,其实真正决定工程能不能被别人接手的,是 Assets 之外的那几个目录和文件。我的笔记里固定记录这几项:ProjectSettings存的是项目级配置,包括输入、图形、物理、标签层,这个目录一定要进版本管理;Packages里的 manifest.json 决定依赖了哪些包,出问题时第一件事就是看它;Library和Temp绝对不能进版本管理,它们是缓存,几千个文件会让仓库爆炸。还有一点,很多人不知道工程目录名带中文或空格会在某些平台打包时出问题,所以我在笔记里直接写明"工程路径只允许英文、数字、下划线"。
2.3 GameAssembly.dll、宏定义与特性:三个最容易被问到的冷知识
工具链层里我专门留了一册叫"冷知识",收的都是那种平时用不上、但一旦被问到或者出问题就很致命的东西。
GameAssembly.dll属于这一册。它是 IL2CPP 后端把 C# 转成 C++ 再编译出来的原生代码产物,换句话说,你的游戏逻辑最终就住在它里面。理解了这一点,很多事情就顺了:为什么 IL2CPP 打包比 Mono 慢很多、为什么有些反射代码在移动端会失效、为什么做代码保护时大家都会盯着这个文件。
宏定义是我建议每个人都要花十分钟搞明白的东西。代码里那些#if UNITY_EDITOR、#if UNITY_ANDROID的写法,本质是编译期裁剪,没命中的分支根本不会进包。我在笔记里整理了一张常用宏的对照表,标注每个宏在什么平台下生效、能不能在 Player Settings 的 Scripting Define Symbols 里自己加。自定义宏的价值在于做功能开关,比如把付费模块、调试面板整体包在一个宏里,出正式包时一键剔除,比重构代码省事得多。
特性这一条我记的是几个真正会用的,而不是把所有 Attribute 抄一遍。常用的就那几个:[SerializeField]把私有字段暴露到 Inspector,比写 public 干净;[RequireComponent]保证依赖组件不会被人误删;[ExecuteAlways]让脚本在编辑器里也跑;[Header]和[Range]纯属提升 Inspector 可读性,团队协作时很值。我在笔记里加了一句提醒:[ExecuteAlways]用不好会让编辑器不断刷新,严重的会卡到无法操作,加之前先想清楚。
| 条目 | 关键词 | 一句话记忆点 |
|---|---|---|
| GameAssembly.dll | IL2CPP 产物 | 逻辑最终都在这里,反射失效先想它 |
| 宏定义 | 编译期裁剪 | 没命中的分支不进包,可用于功能开关 |
| 特性 | 元数据 | 只记常用的五个,别当字典背 |
3. 基础层笔记:脚本、C# 底子与场景对象的日常操作
3.1 生命周期与执行顺序:笔记里必须画的那张表
基础层最重要的一条笔记,是生命周期与执行顺序。我不建议只抄文档里的顺序图,而是记**"什么时候必须用哪个"**的对应关系。Awake做自身初始化,OnEnable做事件订阅,Start做依赖其他对象的初始化,Update处理输入和逻辑,FixedUpdate处理物理,LateUpdate处理跟随相机和 IK。这里面有个非常经典的坑:所有跟相机有关的位移一定要放LateUpdate,因为角色位移在Update里算完,如果相机也在Update里跟随,你看到的就会是一帧抖动。我把这条写在最显眼的位置,因为我自己因为这个原因排查过两个下午。
还有一个执行顺序相关的细节值得记:脚本之间的执行顺序默认是随机的,需要固定顺序时用Script Execution Order面板,或者用事件中心解耦。我在笔记里标注了一句话——"依赖执行顺序本身就是设计问题",能用事件解决就不要靠顺序面板。
3.2 八股整理法:把面试题变成索引而不是背诵清单
面试题和 C# 八股这类内容,网上到处都是,但我发现直接抄一份清单进笔记是没用的,因为背诵清单和自己理解的笔记完全不是一回事。我的做法是只记"我自己解释得出来"的条目,解释不出来的先标红待补。比如协程,我笔记里写的不是"协程是基于 IEnumerator 的",而是这样一段:协程本质是一个返回 IEnumerator 的方法,Unity 在每一帧调用它的 MoveNext,遇到 yield 就记下当前状态并暂停,条件满足后继续。所以它运行的时机是在 Update 之后、LateUpdate 之前,而且它不是在多线程里跑的,阻塞操作放进协程一样会卡主线程。
GC 这一条我也记得比较细:值类型在栈上、引用类型在堆上,装箱会把值类型搬到堆上,频繁装箱是 GC 压力的常见来源。字符串拼接、每帧new数组、在 Update 里用 LINQ,这几个都是我实际遇到过导致掉帧的写法。值类型和引用类型、String 和 StringBuilder、委托与事件、接口和抽象类的取舍,这几条我都写成了"什么时候用哪个"的形式,比背定义有用得多。
3.3 不用脚本也能隐藏组件的几种做法与它们各自的下场
"在编辑器里不写代码就隐藏掉某些东西"是很多人会问的问题,我的笔记里把可行方案和代价一起记下来了,因为这里面没有免费午餐。
第一种是编辑器层面把对象或组件临时隐藏。场景视图的可见性开关、Inspector 上的折叠、Prefab 变体的覆盖开关都属于这一类。好处是完全不影响运行时,代价是实现不了任何运行时行为,纯属编辑期方便。第二种是用图层的 Culling Mask,把对象放到一个相机不渲染的层上,或者直接让主相机的 Culling Mask 排除它。这种方式的关键优势是对象还在场景里,脚本照样能访问到它,做"逻辑上存在但视觉上不出现"的需求很合适。第三种是HideFlags,运行时不渲染也不参与保存,但注意它是代码层面的东西,绕了一圈还是要写脚本。
我的结论写在笔记末尾:真正意义上的"不写代码就隐藏",只适合做编辑期整理;只要涉及运行时的显示与隐藏,就一定得写逻辑,区别只在于用哪种方式写代价更小。
4. 表现层笔记:渲染、Shader、UI 与相机里那些反复踩的坑
4.1 Renderer 的包围盒:模型"忽然消失"的头号嫌疑
表现层里我踩得最深的一个坑,是 Renderer 的包围盒。现象是角色在画面边缘时突然整个消失,转身或者挪回去又出现,看上去像是渲染坏了,实际上是被视锥体剔除掉了。原因在于 Unity 用 Renderer 的包围盒来判断"这个物体在不在相机视野内",而蒙皮网格的包围盒是烘焙的,如果动画幅度超过了烘焙范围、或者包围盒被算得太小,剔除就会发生在错误的位置。
我把两种边界记在了笔记里:bounds是世界空间的轴对齐包围盒,用来做剔除判断;localBounds是模型空间的,可以手动设置。解决办法就是给需要大幅度变形的对象手动扩大localBounds:
void Start() { var r = GetComponent<Renderer>(); // 留出足够余量,宁可稍微多渲染也不要闪消失 r.localBounds = new Bounds(Vector3.zero, Vector3.one * 10f); }这条笔记我额外补了一句经验:包围盒给大了会影响剔除效率,所以不要无脑给一个特别夸张的值,测试时就按动画最大位移的 1.5 倍来设。这个坑还有个变种,就是特效粒子系统在大范围移动时同样会闪消失,原理一样。
4.2 阴影、分辨率与相机:三个参数决定画面下限
阴影问题的表现特别多:边缘锯齿、条纹状的摩尔纹、物体上出现诡异的黑斑、近处有阴影远处没有。我在笔记里按表现分类记录:条纹和摩尔纹多半是深度精度问题,调Bias和Normal Bias;近处有远处没有基本是Shadow Distance设小了;整体发黑或者不发光要看光源的阴影类型和强度。移动端还有个额外限制,阴影贴图的级联数量越多开销越大,我一般在中端机型上只用两级。
分辨率设置这一条,我记的重点不是 API 本身,而是"什么时候该动它"。Screen.SetResolution在 PC 端做窗口化、做分辨率选项列表时很有用,但移动端千万不要手动改,交给系统就行。真正需要记录的坑是UI 适配:Canvas Scaler 的参考分辨率和匹配方式选错,会导致某些机型上 UI 被拉伸或者被裁掉。我的经验是按主流比例设参考分辨率,匹配模式优先选"按高度匹配",因为竖屏游戏和横屏游戏对高度的敏感度不一样,横屏通常更在意高度。
相机跟随这条我写了一段可直接用的代码,并且特意注明为什么用SmoothDamp而不是Lerp:
public Transform target; public Vector3 offset = new Vector3(0f, 2f, -5f); Vector3 vel; void LateUpdate() { // SmoothDamp 与帧率无关,Lerp 在不同帧率下速度会变 transform.position = Vector3.SmoothDamp( transform.position, target.position + offset, ref vel, 0.15f); }顺带在笔记里记了一个边界情况:如果目标对象在FixedUpdate里移动,相机放在LateUpdate仍然会出现轻微抖动,这时候要么把目标移动也统一到Update,要么开启相机的插值。
4.3 UI 的显示隐藏之争:SetActive、CanvasGroup、localScale、移出相机
这是我看过最多人争论的问题,我的笔记里干脆做成了一张对照表,写清每种方式的代价,而不是给一个"标准答案"。因为答案真的取决于你要什么。
| 方式 | 主要开销 | 能否参与布局 | 适用场景 |
|---|---|---|---|
| SetActive | 首次激活有实例化与重建成本,会触发布局重算 | 不参与 | 大面板、低频切换 |
| CanvasGroup.alpha | 几乎无抖动,但元素仍在渲染与参与射线检测 | 参与 | 高频淡入淡出 |
| CanvasGroup.interactable | 不控制视觉,只控制交互 | 参与 | 只做禁用交互 |
| localScale 置零 | 会触发布局重算,且 scale 为 0 时可能有除零风险 | 参与 | 简单缩放动画 |
| 移出相机可视范围 | 不改动层级,但仍在渲染队列里 | 参与 | 不想打断动画的临时隐藏 |
我实际项目里的默认策略是:高频切换用 CanvasGroup 配合blocksRaycasts,低频的大面板切换用 SetActive。因为 SetActive 关掉之后会触发合批重建,如果每帧都在开关一个复杂面板,帧率会肉眼可见地掉。还有一个细节很多人忽略:CanvasGroup.alpha = 0时对象依然参与射线检测,如果你只是想让玩家看不到但还能被点,这是特性;如果不想被点,一定要记得同时把blocksRaycasts关掉。
4.4 按钮点击范围与 Tooltip:交互手感的两件小事
按钮点击范围太小,是移动端最常被吐槽的问题。我的笔记里记了三种做法,按侵入性从低到高排列:最简单的是在按钮下加一张完全透明的 Image,勾上Raycast Target,靠它把热区撑大;第二种是把按钮的 Image 换成不透明的纯色再把透明度降下来,视觉上没变化但热区按实际尺寸算;第三种是自定义组件重写射线检测,能做不规则热区,适合做圆形按钮。
有个小坑必须记下来:用图片的 alpha 来决定是否响应点击时,需要纹理开启可读,运行时会多一份内存拷贝,图集里的大图这么干会明显吃内存。所以除非真的需要不规则热区,否则优先用透明 Image 的方式。
Tooltip 这类小功能我记的是"别自己造"的思路:如果只是想在编辑器里给美术和策划写备注,内置的 Tooltip 和自定义 Inspector 就够了;如果要的是运行时鼠标悬停提示,社区里有现成的轻量插件,与其花两天写一个还不如直接用。这一册我特意标注了每条笔记"是自己写的还是用的现成方案",避免半年后自己都忘了哪个模块是第三方代码。
4.5 Shader、Timeline、PerlinNoise 与程序化内容的笔记组织
Shader 的笔记我单独开了一册,因为它的知识结构跟其他部分完全不同。我的组织方式是"效果反查原理":先按效果分类(描边、溶解、水面、卡通渲染、热扰),每个效果下面记实现思路和关键代码片段,而不是按语法章节整理。因为实际工作中你是先有需求才去找语法,反过来记的话几乎用不上。性能提醒我也记在每一条里:片元着色器里的分支代价高,能用数学替代就别用 if;pow、sin这些函数在移动端要节制使用。
Timeline 我记的重点是它和 Animator 的边界:Timeline 适合做线性编排,比如过场、剧情、镜头切换;Animator 适合做状态机,比如角色的移动、攻击、受击切换。两者混用时容易出现"Timeline 播完角色卡在某个姿势",多半是 Track 的绑定和 Animator 的权重没处理好。我还记了一个实用技巧:Timeline 的轨道可以绑定到场景对象上,如果绑定的是 Prefab 里的对象,建议用变量引用而不是硬编码路径,改结构时不会全崩。
程序化内容这块,Mathf.PerlinNoise是最常被记的一个。它的返回值在 0 到 1 之间,输入相同的坐标永远得到相同结果,所以很适合做地形、云层、随机分布。我记了两个坑:一是采样频率要控制,频率太高会变成噪点,太低会看到明显的块状;二是它的周期性问题,做无缝贴图时需要按周期取模采样,否则接缝会露馅。
// 用两个不同频率叠加,得到更有层次的地形高度 float h = Mathf.PerlinNoise(x * 0.05f, y * 0.05f) * 0.7f + Mathf.PerlinNoise(x * 0.2f, y * 0.2f) * 0.3f;角色表现这一类里,SkeletonUtilityBone我也收了进来。它属于骨骼工具链的一环,主要用途是把挂点、特效、武器这些需要跟随骨骼的东西绑到骨架上,避免每帧手写代码去同步位置。我的笔记里写清了它的适用前提是骨架结构完整,并且提醒了性能:挂点数量多的时候会有额外开销,能用父子关系解决的不要用逐帧同步。类似的还有"根据对话变化表情",我的方案记录是表情用 BlendShape 或贴图切换配合状态机,驱动来源是数据配置而不是硬编码,这样策划改对话时不用改代码。
5. 交互与空间计算层:VR/MR、数字孪生、地图与硬件通信
5.1 Pico4 开发与 MR/VR 切换的笔记要点
一体机开发这一册是我最近补得最多的。Pico4 这类设备的开发流程我记录成了一条固定链路:安装设备厂商的 Unity 集成包、在 XR 插件管理里启用对应平台、配置项目为 Android 平台并设置最低 API 等级、处理渲染管线兼容(URP 基本是必选项)、然后才是写业务逻辑。这里最容易漏的是着色器变体:一体机上的渲染路径和 PC 端不一样,某些在 PC 上跑得好好的材质,打包到设备上直接变粉或者变黑,所以我在笔记里写了一条硬规则——"所有材质上设备前必须实机验证"。
MR 和 VR 的切换我单独记了一条。VR 是纯虚拟画面,MR 要在画面里叠加真实世界的透视画面。技术上通常体现为相机背景的处理方式不同,以及是否需要开启设备的透视能力。切换时的典型问题是透视画面和虚拟物体的对齐偏差,需要校准相机参数,并且注意不同设备的透视能力开放程度不一样。我的笔记里明确写了"不要假设所有设备都支持同一个功能,一定要先查能力清单",这条经验帮我省过不少返工。
5.2 Cesium for Unity 与离线地图:别把瓦片服务器写进笔记就忘了
数字孪生和地图类项目里,Cesium for Unity 是绕不开的一环。我的笔记把它分成三块记:数据格式、服务来源、性能代价。数据格式主要是 3D Tiles 和地形数据,它们是分层的,越靠近视点加载越细一级,这也是它能承载大场景的原因。服务来源这块我写得很明确:默认的在线服务需要网络和账号,做内网部署或者断网场景时,必须自建瓦片服务或者准备本地数据集,这一步最好在项目立项时就确定下来,不要等到验收前才发现环境没有外网。
性能代价这块我记了几个实测数字:分辨率调高一级,内存和显存占用会明显上升;倾斜摄影模型的三角形数量非常夸张,必须做 LOD 和分块加载;地形叠加多层影像时,纹理带宽会成为瓶颈。我的笔记里还专门留了一条"边界条件":如果场景很小、只在室内,其实完全没必要上这么大的方案,普通的模型加贴图就够了,用重型方案会让整个项目变复杂。
5.3 串口通信:System.IO.Ports 在 Unity 里的正确打开方式
串口通信这一册属于偏硬件的方向,做设备对接、展厅互动、工业可视化时会突然需要。核心写法很简单:
using System.IO.Ports; SerialPort port; void Start() { port = new SerialPort("COM3", 115200, Parity.None, 8, StopBits.One); port.ReadTimeout = 50; port.Open(); }但坑全在细节里。第一,串口号在不同机器上不固定,插入的顺序变了就会换号,所以正式项目里要提供选择串口的界面,或者按设备描述符去匹配。第二,ReadLine这类同步读取会阻塞主线程,直接放在Update里会让画面卡成幻灯片,正确做法是开一个后台线程收数据,再把数据丢进一个线程安全的队列,主线程每帧取。第三,Unity 的 API 兼容级别设置会影响System.IO.Ports的可用性,如果在 PC 上跑得好好的,切到别的平台报命名空间找不到,先检查这个设置。第四,Android 设备上的串口需要额外的权限和原生调用,不能直接用 PC 上那套写法,这条我在笔记里标成了红色。
5.4 数字孪生与天气可视化:数据驱动的场景组织
数字孪生这类项目,难点从来不是单个模型好不好看,而是大量对象怎么组织和怎么驱动。我的笔记里记录了一套固定模式:用配置表描述每个设备的唯一标识、模型路径、位置、需要绑定的数据字段;运行时由数据层定时拉取或订阅数据,通过唯一标识把数值分发到对应对象上;对象自身只负责把数值映射成视觉表现,比如温度映射成颜色、转速映射成旋转速度。这样组织的好处是加设备只改配置,不动代码。
天气可视化算是这个模式的一个具体应用。我的做法是把天气状态拆成几层:天空盒或环境光、粒子效果(雨雪)、地面湿滑反光、以及 UI 上的数值展示,每一层由一个独立的控制器驱动,共用一个天气状态数据源。这样切换天气时只需要改一个状态值,所有表现层自动跟着变。笔记里我把"共用单一数据源"这句话加粗了,因为多个脚本各自维护一份天气状态,是我见过最常见的状态不同步来源。
6. 交付层笔记:小游戏打包、Web 部署、混淆与优化清单
6.1 微信小游戏:打包、视频播放与广告接入
小游戏这一册是整个目录里更新最频繁的,因为平台侧的工具链一直在变。我的记录方式是"每次成功打包后立刻回填",包括:转换插件用的哪个版本、引擎版本、需要在转换设置里勾哪些项、包体多大、首包加载多久。这些数字过两个月就是最宝贵的参考。
视频播放是这一册里被问得最多的问题。我的笔记里写了几种方案的适用边界:如果视频只是播个开场动画,可以考虑转成序列帧或者直接内置短片;如果必须播真实视频文件,一般要借助平台提供的视频组件,把它当作覆盖在画布之上的独立层来处理,这时候要先想清楚层级关系、缩放和屏幕旋转带来的对齐问题,以及用户点击穿透怎么处理。别指望用普通的视频纹理组件直接搞定,在浏览器环境里视频纹理的限制比原生平台多。
广告接入相对直接,激励视频是最常用的一种。我的笔记里记了几个必查项:广告的加载和展示是两个独立步骤,必须等加载回调成功再展示,直接调用会失败;广告关闭后的奖励发放要区分"完整观看"和"中途关闭";测试阶段一定要用测试广告位,正式广告位在测试机上刷多了容易触发风控。这几条每一条我都踩过。
| 环节 | 最容易出错的地方 | 我的处理方式 |
|---|---|---|
| 打包转换 | 插件版本与引擎版本不匹配 | 固定版本组合,写进笔记不再随意升级 |
| 视频播放 | 层级与缩放对不齐 | 独立层处理,锁定横竖屏策略 |
| 激励广告 | 未等加载完成就展示 | 严格走回调,测试用测试广告位 |
| 首包体积 | 资源未做拆包 | 按场景拆分包,先跑通再优化 |
6.2 WebGL 部署到 IIS:缓存、压缩与跨域
WebGL 发布到 IIS 这一册,我踩的坑集中在"服务器不认文件类型"上。WebGL 的构建产物里有.wasm、.data、.js这些文件,IIS 默认的 MIME 类型表里不一定都有,缺失的话浏览器请求回来是 404 或者以错误类型解析,页面白屏。解决办法是在站点的配置里显式添加 MIME 映射,.wasm映射为application/wasm。
第二个坑是压缩。构建产物里的.js.gz、.wasm.br这类预压缩文件,需要服务器正确声明内容编码,否则浏览器拿到的还是压缩后的二进制流,直接报解析错误。我的笔记里记了一条检查清单:MIME 类型、内容编码、静态压缩开关、以及是否需要关闭动态压缩避免二次压缩。第三个坑是跨域,如果页面和数据文件不在同一个域名下,需要配置跨域响应头。这一册我写了一句总结性的话:"WebGL 上线的问题九成在服务器配置,不在代码",每次遇到白屏先查服务器控制台的请求记录。
6.3 混淆与 IL2CPP:保护与代价
代码保护这一册我保持理性记录,因为我知道大概没有绝对安全的方案,只有提高门槛。基本盘是使用 IL2CPP 后端,脚本会被转成原生代码,比 Mono 后端直接暴露 DLL 要好得多。在此之上再对生成的原生库做加壳或者加密处理,能进一步提高逆向成本。
混淆工具的笔记我记了两条关键约束:一是混淆和反射天生冲突,如果你的代码里大量使用字符串形式的反射调用、或者依赖序列化字段名,混淆之后就可能失效,所以要么给相关成员加排除规则,要么改成非反射的实现;二是混淆会拖慢构建、增加包体,并且出问题时的堆栈信息会变得难以阅读,因此我的建议是只在发布分支开启,开发分支保持关闭。这一册里我还写了一句提醒:与其花大力气防破解,不如先把游戏本身做好,成本收益完全不在一个量级。
6.4 优化清单:从 Profiler 到"限定数据块大小"
优化这一册我做成了一份可以直接照做的清单,按"先测后改"的顺序排列,因为上来就凭感觉改代码是最常见的浪费。第一步永远是 Profiler,看清楚到底是 CPU 卡还是 GPU 卡、是脚本耗时还是渲染耗时、内存峰值出现在加载阶段还是运行阶段。第二步针对结论动手:CPU 卡先看合批和 DrawCall,图集有没有正确合并、材质能不能共用;脚本耗时高先找每帧分配,字符串、数组、LINQ 都是嫌疑对象;GPU 卡看分辨率、后处理、阴影距离。
"限定数据块大小"这个说法我在笔记里展开成了具体的做法:单个网格、单张纹理都不要做得过大,超大的资源会带来加载时的内存峰值和卡顿,也不利于分帧加载。我的经验值是单张纹理按平台选择压缩格式,别拿一张原图一路用到底;模型按场景拆分,避免一个大文件里塞着整个场景。对象池也是清单里的固定项,子弹、特效、飘字这类高频生成销毁的对象必须池化,这条我标成了必做项。
6.5 桌面美化与模型替换:两个边缘但有趣的玩法
桌面美化这一类是玩票性质,但很有意思,所以我也留了一册。基本思路是把 Unity 应用做成一个无边框透明窗口常驻桌面,播放动画或者做互动。要注意的是窗口透明在不同系统上的实现方式不同,而且这类应用对资源占用的要求比游戏更严格,因为它要一直开着。我的笔记里写了一条实测体验:这类项目最大的性能消耗往往是动画和特效,把帧率降到 30 甚至 15,肉眼几乎看不出来,风扇声音小很多。
模型替换我记的是资源热更场景下的做法:用 AssetBundle 或者 Addressables 加载新的网格和材质,替换掉当前角色的对应部分。这里的关键是骨骼要能对上,如果新模型和旧模型的骨骼层级不一致,蒙皮就会错位。我的笔记里写了一条硬性要求:换模型的资源必须基于同一套骨架导出,并且在导出多段动画时确认每一段的帧范围和根节点设置正确,否则会出现换完模型动作乱飞的问题。
7. 让目录活起来:检索、复习与进阶路线
7.1 命名与标签规范
目录搭好只是开始,真正决定它能不能用得久的是命名规范。我现在的规则很简单:文件名用"领域-具体现象",比如"渲染-蒙皮网格边缘消失",不要用"问题1""笔记2"这种。给每条笔记打标签,标签分三类:技术栈标签(渲染、UI、打包)、设备标签(PC、Android、一体机)、状态标签(已解决、待验证、已知限制)。这三类标签配合搜索,基本能在十秒内定位到想要的条目,比翻文件夹快得多。
7.2 每周一次"回填"
我给自己定了一个习惯:每周花二十分钟把这一周踩过的坑补进目录,只写现象和一句话结论也行,细节等下次再遇到时补。这个习惯的价值在于,笔记的质量是靠次数堆出来的。同一条条目我可能改过三四次,第一次只记了现象,第二次补了原因,第三次补了代码,第四次补了边界条件,最后它才变成一条真正能帮到别人的内容。反过来,如果攒到月底再写,八成什么都想不起来了。
7.3 进阶书单与全栈方向
进阶这块我在目录里留了一册专门放"输入源",也就是书、课程和开源项目。书我推荐先啃渲染方向的经典和引擎原理方向的系统书,前者能让你把 Shader 从抄代码变成自己写,后者能让你理解引擎为什么这么设计。C# 语言本身也值得单独看一本系统的教材,很多写了两三年 Unity 的人对语言特性的理解是碎的,看一遍就串起来了。
全栈这个方向我也记了一条路径:客户端能力扎实之后,往上补服务端和数据层,理解通信协议、数据存储、并发和部署。这个方向的价值不在于多一个头衔,而在于你接到一个联网需求时,能自己想清楚数据怎么流、状态怎么同步,而不是等别人给接口。我的目录里这一册目前最短,但我知道它会慢慢变长——笔记本来就是跟着人走的,人在往前走,目录自然会跟着长。