
入行这些年被问得最多的一个问题不是“Unity难不难”而是“我该从哪一步开始”。我的答案一直没变过别先啃系统教程先用Unity做一个小Demo出来。这个Demo不需要多漂亮也不需要什么美术资源能用键盘把一个球推着在地上跑、撞到会消失的金币能加分、镜头能稳稳跟住它最后能打包成一个能双击运行的程序这一整套流程走完你就已经跨过了入门Unity最难的那道坎。下面这篇内容我把自己带新人时反复用过的这套流程完整写出来包括每一步为什么这么做、参数大概给多少、以及那些教程里基本不会讲但一定会踩的坑适合完全零基础、想快速上手Unity并做出第一个可玩Demo的人。全文围绕一个“滚球吃金币”的小案例展开但里面涉及的场景组织、物理参数、脚本生命周期、打包排错思路换成任何玩法都通用。1. 入门Unity最快的路径不是“学完再做”而是“做坏了再修”1.1 为什么大多数人卡在“看完一套教程还是不会”我刚带新人的时候习惯性先扔一堆教程链接结果发现三周过去对方还是不敢新建工程。问题不在教程质量而在于看教程是一件被动的事视频里鼠标点哪个菜单、参数填多少你都能看懂但你自己面对一个空场景的时候脑子里是没有任何“下一步该干嘛”的肌肉记忆的。Unity的知识密度很高同时又高度耦合——一个最简单的“球往前滚”就牵扯到 Transform 的位置更新、Rigidbody 的物理模拟、Collider 的碰撞形状、输入读取方式、以及 Update 和 FixedUpdate 两个生命周期函数的分工。你单独去背其中任何一项都记不住只有把它们串在一个能跑起来的东西上记忆才会生根。所以你的第一目标不是“学完”而是“让它动起来”哪怕动得很丑。动起来之后你才有具体的问题可问为什么球会卡在地上为什么镜头会抖为什么金币撞不动这些问题本身就是最好的教材。1.2 我要求这个Demo必须亲手实现的五件事我不建议一上来就追求“完整游戏”但也不能只做一个“按一下键方块动一下”的空壳那样学到的东西太薄。我一般会让新人把下面五件事全部亲手实现一遍任何一步查文档可以直接抄完整代码不行序号要实现的功能实际覆盖的Unity知识点1键盘推着球在地面上滚动Transform、Rigidbody、输入读取、FixedUpdate2球碰到金币金币消失Collider、Is Trigger、Tag、OnTriggerEnter3相机始终跟着球LateUpdate、位置偏移、插值跟随4屏幕左上角显示分数和提示Canvas、TextMeshPro、Canvas Scaler5按 R 键重置打包成 exe 双击可玩场景重载、Build Settings、Player Settings你会发现这五件事几乎没有一行是“为了炫技”的全都是任何一个商业项目里都会用到的地基。尤其第 5 条很多人学了一个月都没打过一次包等到真要交付时才发现在编辑器里跑得好好的东西打包出来是黑屏的——这种事我见过太多次。1.3 顺序不能乱为什么是“移动 → 碰撞 → 相机 → UI → 打包”这个顺序不是随便排的它是一条依赖链。先有移动你才能验证物理参数是否合理球会不会穿墙、会不会飘有了移动和地面相机跟随才有意义否则你对着一个不动的球调跟随是调不出手感的有了球和金币的碰撞分数才有来源UI 才有东西可显示有了一套闭环的玩法打包才值得做因为你要验证的是“这个循环能不能脱离编辑器运行”。很多新人反过来先花两天搭一个漂亮的场景摆了一堆模型然后才开始写移动结果一移动发现地面没有碰撞体全部推倒重来。先把最脏最核心的逻辑跑通再考虑美化这是我这些年一直坚持的顺序。2. 环境这一关Unity安装、版本选择与工程目录的隐性成本2.1 Unity Hub与版本选择LTS、渲染管线与“装少了要重装”现在的 Unity 都通过 Unity Hub 来管理官网下载 Hub 之后在 Hub 的 Installs 标签页里点 Add 安装编辑器版本。这里第一个决策就是版本。我的建议是优先选带 LTS 后缀的长期支持版比如 2022.3 LTS 或者 Unity 6 的 LTS 分支原因是 LTS 版本的 API 稳定、文档齐全、第三方插件适配得最好遇到问题在社区里搜到的答案也最多。不要一上来就装最新的技术预览版那种版本经常几周一个小更新改掉一些 API 或者改掉编辑器行为对新手来说是纯粹的干扰。安装界面里的模块勾选是最容易被忽略也最要命的一步默认只勾了当前平台的支持等你哪天想打安卓包或者网页包就得回 Hub 里补装而补装有时会因为版本对不上导致编译报错。我一般会勾的模块Windows Build Support (IL2CPP)做桌面 demo 必装注意括号里这个 IL2CPP 是打包后端和 Mono 后端不一样后面排错章节会细讲。Android Build Support含 OpenJDK、Android SDK NDK Tools就算暂时不做手机游戏勾上也不亏装起来慢但省得以后再等。WebGL Build Support想做网页 demo 或者后面接小游戏平台的话必装。Documentation本地文档断网时救命其实比在线文档快很多。语言包中文界面包个人建议别装。原因很实在——你将来搜报错、看教程、读 API 名字全是英文的用一个中文界面的编辑器去对照英文文档反而增加认知负担。安装路径这一步千万别偷懒。Unity 的安装目录和之后的工程目录一律用纯英文、不带空格的路径比如D:\Unity\Hub、D:\Projects\BallDemo。中文路径和带空格路径引发的问题非常隐蔽通常不是立刻报错而是某些第三方工具、某些命令行编译步骤、某些 Gradle 打包环节莫名其妙失败排查起来能耗掉你一整天。这不是迷信是路径转义和编码在不同工具链里处理不一致导致的经典问题。2.2 新建工程的模板选择3D Built-in还是URP打开 Hub 的 Projects 页面点 New Project会看到一排模板。新手最容易纠结的就是 3D 和 3D (URP) 到底选哪个。简单说Built-in 渲染管线是 Unity 老的内置管线配置简单、不挑材质、随便从 Asset Store 下载的素材丢进去就能用URP 是可编程渲染管线画面效果更好、移动端性能更可控但材质必须用 URP 专用的 Shader你从别处拿来的老材质球导进来会变成刺眼的粉红色——这是因为 Shader 找不到不是模型坏了。入门 Demo 我建议直接用3D (Built-in)模板把精力集中在玩法逻辑上不要一上来就被渲染管线、Shader Graph、后处理体积这些东西分散注意力。等你把 Demo 跑通、想优化画面的时候再新建一个 URP 工程把脚本搬过去那时候你就有判断力了。另外提醒一句Unity 6 之后新建工程默认就是 URP如果你确实想用内置管线注意看模板名字别点错了。工程名字和保存路径同样遵守英文规则。2.3 目录结构与版本管理哪些文件夹能删、哪些不能动新建完工程Assets 里空空如也工程根目录下却有一堆文件夹很多新人第一反应是“这些是不是垃圾可以删掉腾空间”。我把它们分成三类文件夹作用能否删除是否要提交到版本库Assets你所有的场景、脚本、模型、材质不能删必须提交ProjectSettings工程的各项全局设置含输入、物理、质量、标签不能删必须提交Packages依赖包清单 manifest.json不能删必须提交Library导入资源后的缓存和数据库可以删重开会自动重建不要提交Temp / Obj / Logs临时文件、编译中间产物、日志可以删不要提交Build / Builds打包输出目录可以删不要提交Library 文件夹的体积通常比整个工程大好几倍这也是为什么用 Git 管理 Unity 工程必须写.gitignore。如果你不忽略它几万个二进制小文件会把仓库撑爆每次提交都要等半天。另外强烈建议在 Edit → Project Settings → Editor 里把 Asset Serialization 设成 Force Text、Version Control 的 Mode 设成 Visible Meta Files这样场景和预制体在版本库里的差异是可读的文本多人协作时冲突也好解决。Meta 文件千万别删每个资源旁边那个.meta记录的是它的 GUID 和导入设置删了之后引用关系就断了场景里会出现一堆 Missing 的对象。2.4 我遇到的三个环境坑中文路径、杀软、首次导入第一个坑是路径前面说过了但我要补充一种更隐蔽的情况工程路径是英文但你引用的某个资源包或者 SDK 解压在中文目录下一样会出问题。第二个坑是杀毒软件和实时防护。Unity 导入资源和编译脚本的时候会产生海量临时文件读写某些实时防护会把每个文件都扫一遍结果就是首次导入一个中等规模的工程能卡到你以为死机了。做法很简单把 Unity 安装目录、工程目录、以及系统的临时目录加入白名单导入速度能肉眼可见地变快。这个经验是我在一台新机器上折腾了大半天才总结出来的当时一度以为是硬盘坏了。第三个坑是首次打开工程的等待时间。Unity 第一次打开工程要对所有资源做导入和编译如果你的硬盘是机械盘等十几分钟都正常这时候别乱点别以为卡死了就强杀进程强杀很容易让 Library 缓存处于半损坏状态下次打开报一堆莫名其妙的错。真遇到这种怀疑缓存损坏的情况稳妥的做法是关掉 Unity删掉 Library 文件夹重新打开让它重建——这个操作能解决新手期一大半的玄学报错。3. 搭出可玩的场景从地面、球体到物理参数3.1 场景最小清单与层级组织空场景默认只有两样东西Main Camera 和 Directional Light。我们要在它基础上加几个物体。全部通过 Hierarchy 面板右键 → 3D Object 创建不需要任何外部素材Plane地面默认尺寸是 10×10 单位我们把它 Scale 设成 (5, 1, 5)就变成 50×50 的活动区域对一个小 Demo 来说够用了。Sphere球也就是玩家Scale 设成 (0.5, 0.5, 0.5)这样球的直径是 1 个世界单位半径 0.5跟地面的距离关系好算。Cube金币Scale 设成 (0.4, 0.4, 0.4)压扁一点更像金币或者干脆用 Cylinder 转 90 度更形象。CanvasUI 层用来放分数文本和提示文字。关于层级组织我给新人的第一个“专业习惯”建议就是不要把所有东西平铺在 Hierarchy 根目录下。点右键 → Create Empty 建几个空物体当文件夹命名成_Environment、_Player、_Pickups、_UI把对应的东西拖进去。这看起来是小事但等你场景里有上百个对象的时候找东西的效率差距是数量级的。另外我会把玩家对象命名成Player而不是默认的Sphere因为你的代码里GameObject.Find(Sphere)这种写法一旦改名就全废了从一开始就用语义化命名能省掉后面大量重命名和改代码的工作。3.2 Rigidbody Collider一次穿模引出的物理理解这一步是真正的核心。选中球Inspector 里点 Add Component 添加Rigidbody刚体。加完你会看到一堆参数我按重要性排一下Mass质量默认 1小 Demo 不用改它影响的是受力后的加速度和碰撞时动量交换的比例。Drag阻力默认 0。如果你希望球松开按键后能慢慢停下来设 0.5 到 1 之间比较自然设成 0 的话球会一直滚。Angular Drag角阻力控制旋转的衰减默认 0.05可以不动。Use Gravity保持勾选球才会落地。Constraints约束把 Freeze Rotation 的 X 和 Z 勾上。这一步非常关键不勾的话球会像真球一样到处乱转你的前进方向会被自身的旋转带着跑偏操控会变得极其难受。然后是 Collider。Sphere 默认自带 Sphere ColliderPlane 自带 Mesh Collider所以碰撞本身是通的。但这里有一个新手必踩的问题为什么我的球会穿墙 / 掉出地面原因通常有三个。第一地面用了 Plane 但被 Scale 改了之后Mesh Collider 在极端缩放下的表现会不稳定正确做法是给地面换成一个 Box Collider或者用一个 Cube 压扁当作地面。第二你把移动逻辑写在了 Update 里直接改 Transform.position而物理计算在 FixedUpdate 里以固定步长运行两者不同步的时候就会出现视觉上的穿透。第三速度太快物体在两帧之间直接跨过了薄薄的碰撞体这叫“隧穿”解决办法是把碰撞体加厚或者把 Rigidbody 的 Collision Detection 从 Discrete 改成 Continuous。我当年第一次做跑酷 Demo角色高速撞墙直接穿过去查了两个小时才定位到是碰撞检测模式的问题。顺便说一下物理时间步长。Edit → Project Settings → Time 里的 Fixed Timestep 默认是 0.02也就是每秒 50 次物理更新。这个值调小会让物理更精确但更耗性能调大会让物理更省但更容易漏检测。小 Demo 用默认值就行知道有这么个开关就够了。3.3 光照与阴影为什么新手画面总是“糊”和“死黑”场景搭好之后很多人会发现画面不对劲要么暗得看不清要么影子糊成一团马赛克。这两个问题几乎每个新人都遇到过。“死黑”的根源一般不在灯光而在环境光。打开 Window → Rendering → Lighting 面板看 Environment 那一栏的 Ambient Source。默认是 Skybox会从天空盒取环境色画面是亮的。如果你不小心把它改成了 Color 并且颜色调得很暗整个场景就会像停电一样。入门阶段就保持 Skybox 不要动。“影子糊”的根源在阴影分辨率和阴影距离。Directional Light 的 Inspector 里Shadow Type 建议用 Soft Shadows下面的 Quality Settings 里找到 Shadows 一栏Shadow Distance 默认是 150不同版本可能不同意思是只有离相机 150 单位内的物体才投影超出范围就没有影子。如果你的场景很大但物体很集中把 Shadow Distance 调小到 50 左右同等分辨率预算下阴影会清晰很多——这是个非常实用的调优技巧本质是把有限的阴影贴图分辨率集中用在玩家附近。另外 Shadow Resolution 从 Medium 提到 High 或 Very High边缘的锯齿感会明显改善代价是多一点性能开销桌面 demo 完全承受得起。我的经验是先调 Shadow Distance再调 Shadow Resolution最后才考虑 Soft/Hard 的切换。顺序反了容易反复折腾。3.4 材质与预制体把金币做成可复用的Prefab现在的球和金币都是默认的灰白材质很难看。在 Project 面板右键 → Create → Material建两个材质球命名成Mat_Player和Mat_CoinBase Map 里选个颜色直接把材质球拖到场景物体上就能生效。更重要的一个动作是把金币做成 Prefab预制体。做法很简单把 Hierarchy 里的金币拖到 Project 面板里它会变成一个蓝色图标的资源然后你可以把场景里那个删掉。以后你要放金币直接从 Project 拖出来或者用代码Instantiate(coinPrefab, position, Quaternion.identity)动态生成。为什么要这么做因为 Prefab 是“一处修改、处处生效”的。等你调好了金币的碰撞体大小、Tag、材质、以及挂在它上面的脚本之后复制一百个都带着这些设置。我见过新人手动复制六十个金币然后发现 Tag 忘了加只能一个一个点开改那是真的痛苦。这里提前说一个后面会用到的设置金币需要打一个 Tag。在 Inspector 顶部的 Tag 下拉里选 Add Tag新建一个叫Coin的标签然后给金币预制体选上。这个 Tag 是脚本判断碰撞对象的依据忘了加会导致后面代码完全不起作用。4. 把玩法写进脚本输入、移动、相机跟随与计分的实现细节4.1 Update还是FixedUpdate位移为什么必须乘Time.deltaTime先建脚本。在 Project 面板右键 → Create → C# Script命名PlayerController拖到球上。打开后先删掉默认的Update空函数以外的东西写成自己的逻辑。新手第一个疑问通常是Update 和 FixedUpdate 到底用哪个我的判断标准很简单只要你的操作对象带 Rigidbody位移和力的施加就写在 FixedUpdate 里。原因是物理引擎按固定时间步长计算而 FixedUpdate 正好在这个节拍上被调用Update 的调用频率跟着帧率走帧率一波动你在 Update 里施加的力就会不一致表现出来就是有时走得很顺有时突然一顿。具体怎么写移动我给两个方案。方案 A 是直接给速度读取输入得到方向和大小然后赋值给刚体的 velocity这样响应最干脆适合街机手感。方案 B 是施加力用rb.AddForce(direction * force)手感更“物理”球会有惯性、会滑看起来真实但不好控制。入门我建议先用方案 B体会一下物理引擎在替你做什么手感不合适再切 A。一个必须解释清楚的点为什么Time.deltaTime这个系数不能省。deltaTime是“上一帧到这一帧用了多少秒”。如果你写成transform.position Vector3.forward * speed那么速度的单位是“每帧米数”帧率 60 的时候每秒走 60 个 speed帧率 30 的时候就只走 30 个 speed——同一台机器上跑得快换台机器就变慢。乘上Time.deltaTime之后变成了“每秒米数”物理含义就正确了。在 FixedUpdate 里严格来说要用Time.fixedDeltaTime不过因为固定步长是恒定的两者在默认配置下数值一样所以很多教程混着用也没出问题。但你要知道这个区别面试里被问到就是个加分项。输入读取这里还有个坑Unity 有新旧两套输入系统。老的是Input.GetAxis(Horizontal)新的是 Input System 包需要在 Package Manager 里装然后 Player Settings 里切换 Active Input Handling。入门 Demo 用老的Input类就够项目设置里保持默认即可别折腾。如果你装了 Input System 包之后发现所有Input.GetAxis都报错或者无响应那就是 Active Input Handling 被切成了 Input System Package改回 Both 就好了这个坑我替人排查过至少五次。4.2 相机跟随的三种写法和各自代价相机跟随是新手 Demo 观感好坏的分水岭。我按从笨到好的顺序说三种写法你可以逐个试。写法一把相机拖成玩家的子物体。最简单一行代码都不用写。但问题立刻出现球是会滚动的球一转作为子物体的相机跟着一起转整个画面天旋地转玩十秒就晕。这种写法只适合你的玩家对象永远不旋转的情况比如某些俯视角游戏。写法二在 LateUpdate 里手动同步位置。新建脚本CameraFollow挂到 Main Camera 上在LateUpdate里执行transform.position target.position offset;。为什么是 LateUpdate 而不是 Update因为 Unity 的调用顺序是 Update 全部跑完之后才跑 LateUpdate把相机放在最后更新就能保证它读到的是本帧所有物体移动完之后的最新位置。如果你写在 Update 里相机和玩家在同一帧里争抢更新顺序就会出现画面抖动或者相机落后一帧的感觉。写法三带插值的平滑跟随。直接在 LateUpdate 里硬赋值相机是“焊死”在玩家身上的玩家一动相机就动看起来有点生硬。更进一步是用Vector3.SmoothDamp或者Vector3.Lerp做插值让相机有个轻微的追赶感。下面是我常用的一段结构public class CameraFollow : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0, 6, -8); public float smoothTime 0.15f; private Vector3 velocity Vector3.zero; void LateUpdate() { Vector3 desired target.position offset; transform.position Vector3.SmoothDamp(transform.position, desired, ref velocity, smoothTime); transform.LookAt(target); } }三种写法的对比如下写法代码量手感适用场景父子关系无生硬且有旋转污染固定视角、玩家不旋转LateUpdate 硬跟随极少精准但机械需要精确操作的平台跳跃SmoothDamp 插值少顺滑有质感第三人称、跑酷、大多数 demosmoothTime这个参数别乱给0.1 到 0.3 之间比较自然给到 0.5 以上相机会像喝醉了一样晃。这个值本质上描述的是“追上目标大约需要多久”它的单位和帧率无关所以换设备也不会走样这一点比直接乘个 0.1 的插值系数要可靠得多。4.3 触发检测与UI计分OnTriggerEnter为什么不触发金币的拾取逻辑看起来是最简单的但它是新手遇到“代码明明写了却没反应”最多的地方。先把两个东西准备好金币的 Collider 勾上Is Trigger球上必须有Rigidbody和 Collider。然后给金币写一个脚本public class Coin : MonoBehaviour { void OnTriggerEnter(Collider other) { if (other.CompareTag(Player)) { GameManager.Instance.AddScore(1); Destroy(gameObject); } } }这段代码跑不起来的常见原因我列一下基本覆盖了九成情况Tag 没设或者拼错。CompareTag(Player)里的字符串必须是 Tag 列表里真实存在的大小写敏感。玩家对象忘了打 Player 标签是头号原因。双方都没有 Rigidbody。触发检测的规则是至少有一方带 Rigidbody且碰撞双方至少一方是 Trigger。如果你的球是纯 Transform 移动、没有刚体那这两个 Collider 只会物理阻挡永远不会触发 OnTriggerEnter。Rigidbody 勾了 Is Kinematic。运动学刚体不参与物理模拟也不触发常规的触发回调较新版本里可以用OnTriggerEnter配合 Kinematic 的 Contact Pairs 模式但那是进阶话题新手阶段保持不勾。忘了在金币的 Trigger 上同时勾掉Is Trigger之外的东西比如把 Collider 的Enabled取消了那它连触发都不会发。脚本没挂到金币上或者挂到了金币的子物体上但 Collider 在父物体上这样回调不会按你预期触发。至于 UICanvas 建好之后默认是 Screen Space - Overlay 模式这是最省事的模式UI 永远画在所有 3D 内容之上。Canvas 下面的 Canvas Scaler 组件记得把 UI Scale Mode 设成Scale With Screen SizeReference Resolution 填 1920×1080Match 拉到 0.5。这样不管你的窗口拉成什么比例UI 都会等比缩放不会出现换个分辨率字就糊成一团的情况。文本建议用 TextMeshPro菜单里叫 Text - TextMeshPro第一次创建会弹窗让你 Import TMP Essentials点一下导入就好不导入的话文本框是空的。TMP 的优点是放大不糊因为它用的是有向距离场字体。分数更新的写法有个小技巧不要每帧去改文本而是只在分数变化的时候更新一次用一个事件或者直接调用 UpdateScore 方法。每帧赋值文本会触发不必要的 UI 重建虽然小 Demo 感知不到但这是应该养成的习惯。4.4 让代码更“专业”的几个特性SerializeField、Header、平台宏这段是给已经跑通流程、想让代码看起来不那么“新手味”的人看的。C# 的特性Attribute在 Unity 里用得非常多几个我几乎每个脚本都会用的[SerializeField]让私有字段也能在 Inspector 里显示和调参同时保持外部代码无法访问。比起把字段全设成 public这个写法封装性更好也避免了误改。[Header(移动参数)]在 Inspector 里给一组字段加个标题参数一多的时候非常救命。[Range(0, 10)]把 float 变成滑条调参时手感好很多。[Tooltip(说明文字)]鼠标悬停在字段上显示提示协作时价值很高。[RequireComponent(typeof(Rigidbody))]挂在物体上时自动确保有刚体少了就自动加防止忘加组件导致空引用。还有一个绕不开的话题是平台宏。你写的代码在编辑器里跑得好好的打包之后可能就报错因为某些 API 只存在于编辑器。标准做法是用条件编译#if UNITY_EDITOR Debug.Log(这段只在编辑器里执行); #endif #if UNITY_ANDROID // 安卓专属逻辑 #elif UNITY_WEBGL // 网页平台专属逻辑 #endif常见的宏有UNITY_EDITOR、UNITY_ANDROID、UNITY_IOS、UNITY_WEBGL、UNITY_STANDALONE_WIN。你也可以在 Player Settings → Other Settings → Scripting Define Symbols 里自定义宏用来做“正式版 / 调试版”的功能开关。这个技巧在做 SDK 接入的时候几乎是标配我从第一次接触就再也没放下过。5. 从能跑到像个作品打包、分辨率与打包后的排错5.1 打包前的检查清单与Build Settings编辑器里跑通不代表能交付。点 File → Build Settings 打开打包面板这个面板里有几件事必须确认我做成清单Scenes In Build 里要有你的场景。这是黑屏的头号原因。很多人下意识以为“我打开的就是这个场景”但打包器只打列表里勾选的场景列表是空的就会打出一个只有默认空场景的包运行起来当然是黑的。Platform 选对。桌面端选 Windows、Mac、Linux网页选 WebGL。切换平台会触发一次全量资源重新导入第一次会慢耐心等。Player Settings 里的 Company Name 和 Product Name。这两个会影响存档路径和窗口标题随意填也行但别留默认值。Resolution and Presentation。Default Is Full Screen 建议关掉默认分辨率设成 1280×720这样双击运行时是个窗口方便调试。Quality 设置。如果打包后画面明显比编辑器差通常是 Quality Level 被设成了最低档去 Quality 面板确认一下。设置好之后点 Build选一个输出目录同样英文路径等进度条走完你会得到一个 exe 加一个 Data 文件夹。这两个东西必须放在一起才能运行单独把 exe 拷走是打不开的这一点新人经常犯错交付时把 exe 拷到别人电脑上对方双击报错说缺文件。5.2 打包后黑屏、报错、体积过大的排查链路我把打包后最常见的问题和排查顺序整理成一张表按从上到下的顺序查效率最高现象最可能的原因排查动作启动就是黑屏场景没加进 Build Settings打开打包面板看列表黑屏但能听到声音相机被删了或 Camera 组件被禁用检查场景里的 Main Camera一启动就崩无窗口脚本里用了编辑器专属 API 没做条件编译用#if UNITY_EDITOR包起来报“找不到资源”用了Application.dataPath之类的编辑器路径改用 StreamingAssets 或 Resources某些材质变粉色Shader 被打包剥离掉了在 Graphics 设置里加 Always Included Shaders打包体积几个 GLibrary 或测试资源被一起打进去了检查 Assets 里有没有冗余素材WebGL 打开是白屏直接用 file:// 打开浏览器安全策略拦截起一个本地 HTTP 服务访问WebGL 那一条特别值得说。很多人打完 WebGL 包双击 index.html 打开发现是白的以为是打包失败其实是被浏览器的同源策略拦住了必须通过 HTTP 服务来访问。你可以用 Python 一行命令起个临时服务在输出目录下执行python -m http.server 8000然后浏览器访问localhost:8000就能看到。5.3 分辨率与画面适配Canvas Scaler与多平台差异分辨率的坑在“编辑器里好看、打包后变形”这个场景下最集中。桌面端还好玩家可以自己拉窗口一旦涉及安卓、iOS 或者网页屏幕比例千奇百怪从 4:3 到 21:9 都有。前面提到的 Canvas Scaler 设成 Scale With Screen Size 是必须的Match 值决定缩放时偏向宽度还是高度0 是完全按宽度适配、1 是按高度适配、0.5 是折中。横屏游戏一般 0.5竖屏游戏可以偏向高度。对于 3D 内容本身相机是透视投影的话屏幕比例变化会改变可视范围——宽屏能看到更多左右窄屏能看到更多上下。如果你希望不管什么比例玩法区域都完整可见可以在相机上写一小段脚本根据当前宽高比动态调整 Camera 的 fieldOfView。这是很多商业项目的做法思路是先按一个基准比例算出“应该看到多少垂直范围”然后反推当前的 FOV。这个技巧在竞技类游戏里几乎是标配因为屏幕比例带来的视野优势是实打实的不公平。5.4 GameAssembly.dll、混淆与IL2CPP的取舍打包目录里你会看到一个叫 GameAssembly.dll 的文件体积还不小。这个文件是干什么的它只有在使用 IL2CPP 作为脚本后端时才会生成。简单说Unity 的托管代码有两种打包后端Mono 会把 C# 代码编译成 Assembly-CSharp.dll这是一个标准的 .NET 程序集用现成的反编译工具就能还原出接近源码的东西IL2CPP 则是先把 C# 代码翻译成 C再用平台的原生编译器编译成机器码最终逻辑就落在 GameAssembly.dll 里。反编译原生代码的难度比反编译 .NET 程序集高得多。所以如果你的项目对代码保护有要求打包时就应该把 Scripting Backend 从 Mono 切成 IL2CPP。代价是打包时间明显变长尤其是大项目可能从几分钟变成几十分钟而且构建过程中会生成一堆中间 C 文件占用大量磁盘空间。另一个副作用是 IL2CPP 对反射和动态代码生成的限制更严某些依赖反射的库需要额外配置。我个人的取舍是Demo 和内部工具用 Mono正式发布的产品用 IL2CPP。如果需要更强的保护还有专门的代码混淆工具思路是打乱类名和方法名让反编译结果难以阅读但混淆配置不当会导致反射失效属于双刃剑要留足够的测试时间。6. 这个Demo能延伸到哪里几个我实际跟过的方向6.1 程序化生成用Mathf.PerlinNoise把平地变成地形Demo 跑通之后最自然的第一个扩展就是把那块平面换成像样的地形。Unity 自带Mathf.PerlinNoise(x, y)这是一个柏林噪声函数输入坐标返回 0 到 1 之间的值而且相邻位置的返回值是连续变化的不会跳变特别适合生成自然起伏。用法上有两个必须知道的坑。第一PerlinNoise在整数坐标上的返回值基本恒定在 0.5 附近所以你不能直接把物体坐标当参数传进去要先乘一个频率系数比如 0.1把采样点拉开。第二单层噪声看起来很有规律像海浪一样重复想更自然要叠加多个八度低频决定大地形轮廓高频叠加细节每层比上一层频率翻倍、振幅减半。这个思路是所有程序化地形的基础掌握了之后你能拿它生成云、生成洞穴、做随机扰动用途非常广。6.2 数字孪生与地图类项目Cesium for Unity这类方案的思路如果你做 Demo 不只是为了玩而是想往数字孪生或者地图可视化方向走那接下来会接触到把真实地理数据搬进 Unity 的思路。这类方案的核心是用地理坐标系经纬度加高程转换到 Unity 的直角坐标系然后按相机位置动态加载不同精度的瓦片数据远了加载粗糙的、近了加载精细的这样才不至于把整个地球的模型一次性塞进内存。离线场景下还需要自己准备瓦片数据源和坐标转换参数或者用专门的地图组件来加载本地地图切片。这条路的技术栈比做游戏 Demo 复杂不少但底层的东西你已经会了坐标系转换无非是矩阵运算动态加载无非是触发检测加资源实例化性能优化无非是 LOD 和对象池。所以我经常跟人说不要小看那个滚球 Demo它练的是通用的基本功。6.3 硬件交互与串口通信让Demo连上真实设备另一个有意思的方向是让 Unity 跟外部设备通信。比如你有一个外接的传感器或者控制板想通过串口把数据读进 Unity 里驱动画面。C# 本身有System.IO.Ports.SerialPort这个类但在 Unity 里用它有几个注意事项。第一串口读取是阻塞式的绝对不能在主线程里循环读否则整个游戏会卡死。正确做法是开一个后台线程专门读串口把读到的数据塞进一个线程安全的队列里然后在 Update 里从队列取出来消费。第二System.IO.Ports的可用性跟脚本后端有关Mono 后端下支持比较完整IL2CPP 下在某些平台会有限制WebGL 平台则完全不支持——因为浏览器里根本不存在串口的原生接口。第三串口参数必须和硬件一致波特率、数据位、停止位、校验位任何一项对不上读到的就是乱码。我调试这类问题的时候习惯先用一个通用的串口调试工具确认硬件在正常发数据再去写 Unity 那边的代码这样能把问题范围砍掉一半。6.4 小游戏平台打包与视频播放的坑最后说一个这两年问得很多的场景把 Unity 项目发布到小游戏平台。这条路的基本思路是先把项目打成 WebGL 包再用平台提供的转换工具把 WebGL 产物转换成小游戏能识别的包体结构。听起来只是换了个打包方式实际上有几个硬性限制要提前知道。最典型的就是视频播放。Unity 自带的 VideoPlayer 组件在网页和小游戏环境里支持得很有限很多平台根本跑不起来标准做法是换成平台提供的媒体组件通过脚本调用平台接口来播放UI 层用一个透明的区域占位。这意味着你的视频播放逻辑在不同平台要写两套最好在项目早期就把播放器抽象成一个接口桌面端用 VideoPlayer 实现小游戏端用平台组件实现上层调用不变。这种事如果等到项目后期才发现改造成本会很高。另外就是包体大小和内存。小游戏平台的包体通常有严格限制首次加载的包越小越好所以资源要做分包、要做按需加载贴图要压缩音频要选低比特率。这些优化在桌面端可有可无在小游戏端是生死线。我个人的习惯是每做完一个阶段性功能就打一次包看体积而不是等全部做完再优化因为体积失控往往是几十个小决定累积的结果越晚发现越难拆。这个滚球 Demo 的意义大概就在这里它是最小的但每一个变量——物理、相机、UI、打包、平台限制——未来都会被放大到真实的项目里。