2026/9/4 8:32:49

零基础一周Unity Demo实战:从角色控制到打包全流程解析

零基础一周Unity Demo实战:从角色控制到打包全流程解析 如果你是 27 届在校生或者刚开始接触 Unity 的准开发者想用一周时间做一个能放进作品集的 Demo那么这篇文章很适合作为一条可参考的路线。经常看到有同学问“零基础自学 Unity 一周到底能不能做出看得过去的小项目”我的回答是能做出来但前提是你不要从零去啃完一本 600 页的引擎书而应该带着一个明确的小目标边做边补知识。本文就按这个思路复盘一个“一周自学 Unity Demo”的完整过程。Demo 体量不大但基本包含了移动控制、碰撞触发、跳跃、UI 计分、协程渐隐、程序化地形等常见功能很适合作为新手第一个“五脏俱全”的练习项目。需要说明的是这篇文章不会教你怎么下载来路不明的工具而是基于 Unity 官方渠道完成安装、激活与打包。Demo 里用到的核心代码全部会贴出来附带运行思路和踩坑经验目标是让完全没接触过 Unity 的同学也能跟着复现一版。1. 一周 Demo 复盘零基础做 Unity 小游戏可行吗1.1 这个 Demo 解决什么问题很多初学者一开始学 Unity 有个误区先去看引擎菜单里每一个面板的作用然后再决定做什么。结果学了两三天还停留在“设置了场景里的方块往左移动”的层次到第七天赋闲时发现自己什么都没做出来。更合理的方式是选定一个小的完整玩法把流程先跑通。本文的 Demo 目标是做一个第三人称视角的“收集小游戏”玩家在 3D 场景中控制胶囊体角色移动与跳跃收集场景里的金币型 Pickup 物体集齐所有收集物后屏幕中央会显示“恭喜完成”提示并渐隐消失最后把项目打包成 Windows 可运行程序。从技术角度看这个 Demo 涉及以下知识点Unity 项目创建与场景组织。GameObject、Component、Prefab 的关系。基于 Rigidbody 的角色移动。基于物理触发器的拾取判定。摄像机跟随角色。使用 UGUI 显示得分与进度。使用协程和 CanvasGroup 实现 UI 渐隐。使用 Mathf.PerlinNoise 自动生成起伏地面。一周时间把这些点走一遍收获会比单纯看教程大很多。因为这个过程是在解决一个又一个具体问题而不是被动吸收抽象概念。1.2 一周时间怎么分配如果每天大约投入 3 到 4 小时可以参考下面的时间表天次重点方向建议产出第 1 天安装 Unity熟悉编辑器布局创建 3D 项目能摆放简单物体第 2 天C# 脚本基础与生命周期写一个让物体旋转或移动的脚本第 3 天物理系统与碰撞器角色能移动、跳跃并落到地面第 4 天Prefab 与场景搭建场景里布置障碍物和收集物第 5 天UGUI 与游戏管理器得分、进度、完成提示能显示第 6 天整合与细节完成全部流程加入音效和特效第 7 天打包与复盘Build 出 exe并写一篇项目笔记这个安排的核心思路是先做“最小的可玩闭环”再逐步加内容。不要第一天就去研究光照烘焙或动画状态机那不是一周新手阶段的主线任务。2. Unity 安装与项目环境准备2.1 Unity Hub、LTS 与项目创建新手安装 Unity 时最容易踩的坑是直接下载编辑器然后再找 License 激活。更规范的做法是安装 Unity Hub通过 Hub 来管理和下载不同版本的 Unity 编辑器。Unity Hub 的主要作用有三个管理多个 Unity 编辑器版本。创建新项目并选择模板。管理许可证激活。版本方面建议长期支持版本 LTS。LTS 版本通常更稳定适合学习和项目开发。本文示例不绑定某一个具体小版本核心代码在 Unity 2021.3 和 Unity 2022.3 及以上版本都可以运行。如果你电脑上有 Unity 6 或更新版本也可以直接使用。安装完成后打开 Unity Hub点击“New project”选择 3D 模板输入项目名称比如CollectDemo然后创建。这时如果弹出许可证相关提示不要尝试绕过或使用非官方激活工具正确做法是在 Unity Hub 右上角点击头像登录 Unity 账号。进入 Manage License 页面。选择个人版 Personal 许可证或学生版 Unity Student。返回 Hub 启动项目即可。启动后你能看到默认场景中包含了一个平行光 Directional Light 和一个自带相机。2.2 三个基础概念GameObject、Component、PrefabUnity 的核心概念并不复杂第一周需要搞清楚下面三个词GameObject场景里的每一个物体都是 GameObject。它可以是一个圆球、一个空节点、一个 UI 文本也可以是一盏灯。Component组件挂在 GameObject 上用来决定这个物体“能做什么”。比如 Transform 负责位置旋转缩放Camera 负责渲染Rigidbody 负责物理运动。Prefab将某个 GameObject 连同它上面的组件和子物体一起保存到磁盘形成可复用的预制体。可以在场景中多次摆放修改 Prefab 后所有实例同步更新。一个常见的比喻是GameObject 像一个空容器Component 是插进容器的能力模块。脚本也是一种 Component只是它是开发者自己定义的。在 Demo 开发中收集物、障碍物、特效都适合做成 Prefab。这样场景里的重复物体就不用一个个单独配置。2.3 项目目录结构建议项目创建后Assets 目录是 Unity 项目最重要的文件夹。新手为了省事把所有脚本和模型都放在根目录等资源变多后非常难找。建议在一开始就建立清晰目录Assets/ Scenes/ Scripts/ Player/ Manager/ World/ Prefabs/ Materials/ UI/ Textures/其中Scenes存放场景文件Scripts/Player存放角色相关脚本Scripts/Manager存放 GameManager 这类全局管理脚本Scripts/World存放地面生成、收集物等物体脚本。在 Unity 中手动创建这些文件夹或直接在文件系统里创建都可以。每次新建脚本时尽量根据职责放到对应文件夹中能大幅减少后期维护成本。3. 搭建 3D 场景并完成角色控制3.1 场景搭建首先打开默认场景文件删除默认的 Main Camera准备重新加一个相机并挂上跟随脚本。实际开发中经常先摆放角色再根据角色位置调整相机。在 Hierarchy 窗口右键依次创建一个 Cube重命名为 Ground作为地面。一个 Capsule重命名为 Player作为玩家角色。一个 Cube重命名为 Obstacle作为障碍物。几个 Sphere重命名为 Pickup_Coin 之类的收集物。为了让视觉区分更明显可以给不同物体创建材质在 Project 窗口的 Assets 下右键 - Create - Material。将材质命名为 PlayerMaterial。在 Inspector 中修改 Base Map 或 Albedo 颜色将颜色改成亮黄色。把材质拖到 Player 上。障碍物可以做成红色收集物做成金色或绿色地面做成灰白色。这样游戏场景在视觉上更清晰。地面默认 Cube 是 1 米大小如果希望地面大一些可以把 Transform 的 Scale 设置为(20, 1, 20)这样地面就能形成一块较大的平台。注意 Y 轴位置可以设为-0.5让地面顶部正好在 y 0 水平。为了让玩家能被识别需要把 Player 的 Tag 设置为 Player选中 Player。在 Inspector 顶部点击 Tag 下拉框。选择 Player如果列表中没有就在 Add Tag 中添加。如果 Player 后面要触发 Pickup 的碰撞事件Tag 用于判断碰撞对象是不是玩家。接着给 Player 添加 Rigidbody保留 Capsule Collider。Rigidbody 能让物体受重力影响并参与物理模拟。这是角色移动代码的基础。3.2 角色移动与跳跃角色控制是 Demo 里最核心的脚本。新建脚本PlayerController.cs路径放在Assets/Scripts/Player/PlayerController.cs。using UnityEngine; public class PlayerController : MonoBehaviour { [Header(移动参数)] public float moveSpeed 6f; public float jumpForce 8f; [Header(地面检测)] public Transform groundCheckPoint; public float groundCheckRadius 0.2f; public LayerMask groundLayer; private Rigidbody rb; private bool isGrounded; private void Start() { rb GetComponentRigidbody(); if (rb ! null) { rb.interpolation RigidbodyInterpolation.Interpolate; rb.collisionDetectionMode CollisionDetectionMode.Continuous; } } private void Update() { isGrounded Physics.CheckSphere(groundCheckPoint.position, groundCheckRadius, groundLayer); } private void FixedUpdate() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 input new Vector3(horizontal, 0f, vertical); if (input.magnitude 1f) { input.Normalize(); } Vector3 velocity input * moveSpeed; velocity.y rb.velocity.y; rb.velocity velocity; } private void LateUpdate() { if (Input.GetButtonDown(Jump) isGrounded) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } }这段脚本里有几个地方需要详细说明。Physics.CheckSphere会在指定位置创建一个不显示的球形检测区检测范围内是否存在指定 Layer 的碰撞体。我用它来判断角色是否站在地面上。实际使用时需要给 Player 创建一个子物体 GroundCheck放在角色底部中心靠下的位置代码里的groundCheckPoint就指向它。具体操作如下在 Hierarchy 中选中 Player。右键 - Create Empty创建子物体。重命名为 GroundCheck。把 GroundCheck 的 Transform 位置调整到角色底部比如 Capsule 的底部中心略微下方一点可以参考(0, 0, 0)附近再往下偏移一点。将 GroundCheck 拖到 PlayerController 的 GroundCheck Point 字段。Input.GetAxis(Horizontal)和Input.GetAxis(Vertical)读取的是键盘按键输入默认情况下对应 A/D 和 W/S也包含上下左右方向键。移动时会自动从一个值渐变到另一个值手感比较平滑。如果想做更精确的手感可以换成Input.GetAxisRaw它是离散状态按键瞬间到 1 或 -1。FixedUpdate是固定频率执行的物理更新函数适合处理刚体速度。每帧赋值的核心思路是水平方向速度来自输入垂直方向速度保留刚体原来的值。这样角色既能移动又不会覆盖掉重力产生的下落速度。调用rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse)时ForceMode.Impulse表示给物体一个瞬间冲量适合跳跃效果。3.3 为什么用 FixedUpdate而不是 Update新手常见的问题是“移动代码放在 Update 里也能跑为什么要用 FixedUpdate”。原因是Rigidbody 的物理模拟发生在 FixedUpdate 的固定时间步长中。如果在 Update 里修改刚体的 velocity 或施加力会和物理系统的更新频率错开导致运动抖动、穿过碰撞体等问题。Unity 官方推荐把与物理计算相关的逻辑放在 FixedUpdate 中。但是输入检测一般放在 Update 或 LateUpdate 中。上面代码把跳跃按键检测放在 LateUpdate是因为如果在 FixedUpdate 中检测按键可能会连续检测到一次按下的状态多次出现“按一次跳两次”的问题。当然也可以用Input.GetButtonDown配合帧标记处理。对新手来说使用上面代码简单且不容易出错。如果你仔细观察会发现这里的地面检测写在 Update 中移动写在 FixedUpdate 中。这样可以让每帧显示的地面状态更新及时尤其是按下跳跃键时isGrounded已经是当前帧更新后的结果。4. 摄像机跟随LateUpdate 和 SmoothDamp摄像机跟随是几乎每个 3D Demo 都需要实现的功能。新建脚本CameraFollow.cs路径放在Assets/Scripts/Player/CameraFollow.cs。using UnityEngine; public class CameraFollow : MonoBehaviour { [Header(跟随目标)] public Transform target; [Header(偏移与平滑)] public Vector3 offset new Vector3(0f, 6f, -8f); public float smoothTime 0.2f; private Vector3 currentVelocity Vector3.zero; private void LateUpdate() { if (target null) { return; } Vector3 targetPosition target.position offset; transform.position Vector3.SmoothDamp(transform.position, targetPosition, ref currentVelocity, smoothTime); transform.LookAt(target.position Vector3.up * 1.5f); } }将脚本挂到主相机 Main Camera 上然后把 Player 拖到 CameraFollow 的 Target 字段。这里使用LateUpdate而不是Update的原因是如果角色在物理系统中移动摄像机和角色在同一帧的更新顺序没有保证放在LateUpdate中可以确保所有物体的更新逻辑执行完之后再计算相机位置视觉上更稳定。Vector3.SmoothDamp是 Unity 自带的一阶阻尼平滑算法。它比直接Vector3.Lerp的优点是不需要手动调节插值时间只需要提供一个速度引用currentVelocity最终效果是“先快后慢”的平滑跟随有点类似于摄像机追着角色跑的感觉。transform.LookAt让相机始终朝向角色。因为角色胶囊体的中心约在 y 1所以看向的点加了一个Vector3.up * 1.5f保证视线大致对准角色躯干而不是脚底。运行项目后可以用 WASD 移动角色观察相机的跟随效果。如果觉得相机太远或太近修改 Inspector 中的 Offset 即可。5. 收集玩法、UI 与协程渐隐5.1 收集物和触发器给要拾取的 Sphere 添加 Box Collider 或 Sphere Collider并勾选Is Trigger。然后创建标签或按类型区分这里将收集物命名为 Pickup。新建脚本PickupItem.cs路径放在Assets/Scripts/World/PickupItem.cs。using UnityEngine; public class PickupItem : MonoBehaviour { [Header(拾取配置)] public string itemType coin; public int scoreValue 10; public GameObject collectEffectPrefab; private void OnTriggerEnter(Collider other) { if (!other.CompareTag(Player)) { return; } if (GameManager.Instance ! null) { GameManager.Instance.AddCollect(itemType, scoreValue, gameObject.name); } if (collectEffectPrefab ! null) { Instantiate(collectEffectPrefab, transform.position, Quaternion.identity); } Destroy(gameObject); } }OnTriggerEnter 是一个物理事件回调当拥有 Collider 和 Rigidbody 的物体进入另一个 Collider 区域时触发。这里必须确保Pickup 的 Collider 勾选了 Is Trigger。玩家 Player 带有 Collider 和 Rigidbody。玩家身上 Tag 设置为 Player。常见问题是不给 Rigidbody 就拿触发器脚本去测结果 OnTriggerEnter 一直不调用。请记住物理事件要求至少有一方带有 Rigidbody。由于玩家已经在第 3 节加过 Rigidbody这里没问题。拾取特效可以做成一个临时 GameObject例如使用粒子系统 Particle System或者一个会缩放消失的小球。最简单的方式是创建一个空特效 Prefab在里面放一个 Particle System播放完由粒子系统自动销毁。如果没时间做特效也可以先只在 GameManager 中增加得分。5.2 GameManager 单例与数据统计Demo 中需要一个全局对象来管理得分、收集进度和 UI 显示。不要直接把得分变量写死在玩家脚本里那样后续扩展会很麻烦。这里使用单例模式创建 GameManager。新建脚本GameManager.cs路径放在Assets/Scripts/Manager/GameManager.cs。using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } [Header(UI引用)] public Text scoreText; public Text progressText; public CanvasGroup centerTipGroup; public Text centerTipText; [Header(收集配置)] public int totalPickupCount 6; private int totalScore 0; private int collectedCount 0; private Liststring collectRecords new Liststring(); private Dictionarystring, int typeCountMap new Dictionarystring, int(); private void Awake() { if (Instance null) { Instance this; } else { Destroy(gameObject); } } public void AddCollect(string itemType, int score, string itemName) { totalScore score; collectedCount; if (!typeCountMap.ContainsKey(itemType)) { typeCountMap.Add(itemType, 0); } typeCountMap[itemType]; string record $[{itemType}] {itemName} {score}; collectRecords.Add(record); scoreText.text $得分{totalScore}; progressText.text $收集进度{collectedCount} / {totalPickupCount}; if (collectedCount totalPickupCount) { StartCoroutine(ShowCenterTip(恭喜完成全部收集)); } } public IEnumerator ShowCenterTip(string content) { if (centerTipGroup null || centerTipText null) { yield break; } centerTipText.text content; centerTipGroup.alpha 1f; yield return new WaitForSeconds(1.2f); while (centerTipGroup.alpha 0f) { centerTipGroup.alpha - Time.deltaTime / 0.6f; yield return null; } centerTipGroup.alpha 0f; } public string BuildStatistic() { string typeLine 类型统计; foreach (KeyValuePairstring, int kv in typeCountMap) { typeLine ${kv.Key}x{kv.Value} ; } string recordLine 收集记录\n string.Join(\n, collectRecords); return typeLine \n recordLine; } }在场景中新建一个空物体重命名为 GameManager把脚本挂上去。之后还需要制作 UI。在 Hierarchy 中右键 - UI - Canvas创建 Canvas。在 Canvas 下创建两个 Text分别命名为 ScoreText 和 ProgressText并把它们拖到 GameManager 的 UI 引用位置。Canvas 默认自带 Canvas Scaler 组件需要把 UI Scale Mode 设置为 Scale With Screen SizeReference Resolution 可填写1920 x 1080。文本位置可以放在屏幕左上角ProgressText 放在 ScoreText 下方。字体大小建议设为 32 左右颜色改为黑色或白色具体取决于背景亮度。CanvasGroup 组件要挂在 Canvas 或某个 UI 父节点上。这里用一个中心提示区域来演示渐隐在 Canvas 下创建一个空物体 CenterTip再添加一个 Text 子物体中心提示文字可以设为“恭喜完成全部收集”。然后在 CenterTip 上添加 CanvasGroup 组件把初始 Alpha 设置为 0这样运行游戏时不可见。代码中StartCoroutine(ShowCenterTip(...))会启动一个协程。协程允许方法在yield处暂停在下一帧或指定时间后继续执行。这里先显示文字并等待 1.2 秒然后在一帧一帧递减 CanvasGroup 的 Alpha直到文字完全隐藏。这种做法的好处是非常通用以后要做一个开场提示、结算提示或场景淡入淡出都可以复用同一套协程思路。5.3 为什么用 CanvasGroup很多新手想实现 UI 渐隐会直接去修改 Text 组件的 Color 透明度。这样做有一个问题如果 UI 节点下面有很多 Text、Image 子节点需要遍历每个子节点去改颜色非常麻烦。CanvasGroup 是一个统一控制 UI 透明度和交互的组件。修改它的 Alpha 会影响所有子 UI 的显示透明度。所以这里直接把 CanvasGroup 挂到包含所有提示文字的父节点上然后在协程里修改centerTipGroup.alpha一行代码就完成了整个提示区域的渐进隐藏。如果你还需要禁用按钮点击或阻止一段 UI 遮住鼠标事件可以修改 CanvasGroup 的blocksRaycasts属性配合 Alpha 使用。5.4 C# 里 List 与 Dictionary 的配合使用GameManager 中使用了一个 List 和一个 Dictionary。Liststring collectRecords保存收集记录的列表用于保留完整日志。Dictionarystring, int typeCountMap保存统计信息例如 coin 收集了多少次、gem 收集了多少次。在 Unity 开发中这两种容器非常常用。List 适合保存有序数据Dictionary 适合按键快速查找数据。很多场景里需要把 List 转换为 Dictionary比如有一批网络下发数据或配置表格需要按 ID 快速查找。下面给出一个简单的 List 转 Dictionary 的例子后续做背包或关卡系统时会经常用到using System.Collections.Generic; using UnityEngine; public class ListToDictExample : MonoBehaviour { public ListItemConfig itemConfigs; private Dictionarystring, ItemConfig configDic new Dictionarystring, ItemConfig(); private void Start() { configDic.Clear(); foreach (ItemConfig config in itemConfigs) { if (!configDic.ContainsKey(config.itemId)) { configDic.Add(config.itemId, config); } else { configDic[config.itemId] config; } } } } [System.Serializable] public class ItemConfig { public string itemId; public string displayName; public int price; }这个例子的要点是在 foreach 循环里先判断ContainsKey如果已经存在同名的 key 再决定是覆盖还是跳过。如果项目里列表有重复数据直接 Add 会抛出ArgumentException: An item with the same key has already been added这是初学者很容易踩的坑。实际项目里如果静态配置很多还可以用 Unity 的 SerializableDictionary 等扩展实现但原理仍然是 Dictionary。6. 自动生成起伏地形Mathf.PerlinNoise 初体验Demo 如果只用一块平坦 Cube 做地面显得有点单调。除了可以用 Unity 的 Terrain 地形系统用脚本生成网格也是一种很有意思的练习。这里介绍用 Mathf.PerlinNoise 生成起伏地形的方法很多随机地图和地面 Demo 都会用到它。新建脚本TerrainGenerator.cs路径放在Assets/Scripts/World/TerrainGenerator.cs。using UnityEngine; [RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] public class TerrainGenerator : MonoBehaviour { [Header(尺寸)] public int width 30; public int depth 30; [Header(起伏参数)] public float heightScale 2f; public float noiseScale 0.15f; private void Start() { CreateMesh(); } private void CreateMesh() { Mesh mesh new Mesh(); int vertexCountX width 1; int vertexCountZ depth 1; Vector3[] vertices new Vector3[vertexCountX * vertexCountZ]; for (int z 0; z vertexCountZ; z) { for (int x 0; x vertexCountX; x) { float sampleX x * noiseScale; float sampleZ z * noiseScale; float y Mathf.PerlinNoise(sampleX, sampleZ) * heightScale; vertices[z * vertexCountX x] new Vector3(x - width / 2f, y, z - depth / 2f); } } int[] triangles new int[width * depth * 6]; int triIndex 0; for (int z 0; z depth; z) { for (int x 0; x width; x) { int topLeft z * vertexCountX x; int topRight topLeft 1; int bottomLeft (z 1) * vertexCountX x; int bottomRight bottomLeft 1; triangles[triIndex] topLeft; triangles[triIndex] bottomLeft; triangles[triIndex] topRight; triangles[triIndex] topRight; triangles[triIndex] bottomLeft; triangles[triIndex] bottomRight; } } mesh.vertices vertices; mesh.triangles triangles; mesh.RecalculateNormals(); GetComponentMeshFilter().mesh mesh; } }把脚本挂到一个空物体 GameObject 上它会自动创建 MeshFilter 和 MeshRenderer。由于场景里需要用网格生成一个可站立的平台还需要挂 MeshCollider。如果 MeshCollider 性能影响可接受这个 Demo 里直接用也没有问题。代码的核心是Mathf.PerlinNoise(sampleX, sampleZ)。它根据两个坐标值返回一个约 0 到 1 的噪声值。将噪声值乘以heightScale就得到了该点的高度 y。因此不同的位置会有不同高度的起伏整体看起来比较自然而不是完全随机跳变。需要说明的是PerlinNoise 本身不是用来生成“均匀随机”的它生成的是一种自然的连续变化。如果要生成随机地形最适合用它如果要模拟“每局完全不同的地形”可以在采样前加上一个随机偏移量例如float sampleX (x Random.Range(0f, 1000f)) * noiseScale; float sampleZ (z Random.Range(0f, 1000f)) * noiseScale;这样每一局运行时地形都会有变化。不过要注意放在 Start 里生成的地形不会在编辑模式下刷新。如果想在编辑模式下实时预览可以加[ExecuteInEditMode]或使用按钮调用方法生成。本文不展开这个高级用法新手先理解点、三角形网格、噪声三个概念即可。如果你暂时不需要程序化地形可以直接用 Unity 的地形 Terrain 系统刷一座山。但手动刷地形速度慢而且不方便参数化调整。以后再学 3D 地图随机生成时PerlinNoise 是绕不开的工具。7. 运行验证与 Build 打包7.1 编辑器模式运行验证在 Unity 编辑器里点击顶部 Play 按钮前先把需要引用的对象拖拽好将 Player 拖到 CameraFollow 脚本的 Target。在 Player 下创建 GroundCheck并拖到 PlayerController 的 Ground Check Point。将 Ground 或地面 Layer 设置为 Ground 层PlayerController 中 Ground Layer 选择该层。将 GameManager 拖到场景中。将 UI 引用拖到 GameManager 脚本组件上。将收集物 PickupItem 脚本中的 GameManager 在代码中通过单例获取无需手动绑定。点击 Play 后应验证以下几点角色能在地面上移动和跳跃。跳跃后能重新落地不会持续悬空。相机平滑跟随角色。角色接触收集物后收集物消失且得分增加。收集全部 Pickup 后屏幕中央显示文字提示并缓慢消失。如果出现“角色按跳跃键没反应”首先检查 Player 的 GroundCheck 点是否放到了角色脚下再检查 PlayerController 中 Ground Layer 是否选对了层。如果地面没有指定为可识别层则isGrounded会一直是 false所以不能跳跃。7.2 Windows 打包确认 Demo 在编辑器里运行正常后准备打包。点击菜单栏 File - Build Settings打开 Build Settings 窗口。点击 Add Open Scenes将当前场景加入构建列表。Platform 选择 Windows, Mac, Linux 下的 WindowsTarget Platform 为 WindowsArchitecture 选择 x86_64。如果项目需要发布到移动端或 WebGL可以切到对应平台。但第一次打包建议先做 Windows因为构建速度快且不需要额外配置 SDK。设置完成后点击 Build选择一个输出文件夹比如项目根目录下面的Build/Windows。Unity 会开始生成 exe 文件和_Data文件夹。打包完成后双击.exe文件运行。如果只有自己使用使用 Mono 脚本后端就可以了。如果要发布 Android 或 iOS在 Player Settings 里可以将 Scripting Backend 切到 IL2CPP。对于新手 Demo这一步不是必须的。打包时还有一个常见问题如果项目路径中有中文或特殊字符某些 Unity 版本可能出现构建异常。建议整个项目路径都使用英文例如D:/UnityProjects/CollectDemo。8. 一周自学遇到的常见问题与排查这一周里我整理了初学者最容易遇到的几个问题按照“现象、原因、解决”三个维度列出来问题现象常见原因解决思路角色穿过地面掉出场景没有 Collider或 Rigidbody 碰撞检测模式为默认 Discrete 且速度过快给地面添加 MeshCollider 或 BoxCollider检查角色碰撞体跳跃键没反应地面检测点位置不对或 Ground Layer 未设置将 GroundCheck 放在角色底部确认层选择正确OnTriggerEnter 不触发Collider 没有勾选 Is Trigger或双方都没有 Rigidbody至少让移动方带 Rigidbody并勾选目标 Collider 的 Is Trigger移动时角色抖动在 Update 中直接修改刚体速度或没有开启插值将物理移动放入 FixedUpdate设置 Rigidbody Interpolation相机抖动或卡顿跟随逻辑放在 Update 而不是 LateUpdate改用 LateUpdate并用 SmoothDamp 平滑UI 文字不清晰Canvas Scaler 设置不当设置为 Scale With Screen SizeReference Resolution 设为 1920x1080控制台报错 NullReferenceException没有给 UI Text 或 CanvasGroup 赋值在 Inspector 中检查引用是否为空Build 失败项目路径含中文或场景没有加入 Build