Unity新手第三章完整复盘:从玩家控制到场景切换的实战指南
最近在带新手朋友完成 Unity 练习项目时发现一个很常见的现象前两章往往学得挺顺利一旦进入第三章开始涉及玩家控制、摄像机跟随、UI 反馈和场景切换这些 把多个模块组合起来 的内容时网上资料就变得特别零散查了非常多资料勉强跑通一个 Demo却说不清楚每一步到底在做什么。这篇文章围绕 Unity 新手练习项目第三章的内容做一个完整复盘从环境搭建、核心概念、完整代码实现到高频报错和工程建议一次性整理清楚。无论你是刚学完 Unity 基础操作的在校学生还是准备用 Unity 做毕业设计或独立小游戏的开发者都可以直接参考这套闭环流程。1. 第三章练习在 Unity 新手路线中的定位1.1 新手项目第三章通常要掌握什么在 Unity 新手练习路线中第一章一般是软件安装、界面认识、场景基础操作第二章会扩展到预制体、物理碰撞、UI 基础到了第三章通常就会进入一个 综合小玩法 阶段也就是说要把之前学过的分散知识点组合成一个可以交互的完整流程。常见的第三章练习目标包括学习模块具体能力玩家控制使用键盘/鼠标控制角色移动、旋转、跳跃摄像机系统实现第三人称跟随、平滑插值、固定视角切换物理交互碰撞检测、触发器、拾取物品UI 反馈文本显示、按钮交互、渐隐渐现效果场景管理场景切换、单场景加载与叠加加载数据基础使用集合类List、Dictionary管理游戏数据如果你正在做的项目是 角色收集物品 到达终点 这类经典玩法那第三章正好是实现核心玩法的阶段。1.2 本章练习的核心玩法设计为了让教程有落点我们设计一个极简但完整的练习项目玩家通过 WASD 控制小球移动鼠标控制视角旋转摄像机跟随小球。地图上摆放金币碰到金币后触发得分同时 UI 显示 获得 1 金币 并逐渐消失。收集全部金币后自动切换到一个结束场景。这个设计很典型覆盖了移动、旋转、摄像机、UI、场景切换这些第三章高频知识点代码量不大但能帮助你把 Unity 组件逻辑串起来。1.3 完整功能清单完成第三章后你的项目应该具备以下能力角色移动逻辑写在MonoBehaviour脚本的Update或FixedUpdate中使用物理组件Rigidbody或CharacterController驱动角色而不是直接改 Transform摄像机在LateUpdate中做平滑跟随避免抖动通过触发器检测拾取物配合CanvasGroup实现 UI 渐隐使用SceneManager实现场景加载和切换合理使用Dictionary管理金币状态或收集数据。带着这个目标清单去写代码会比 照着别人的项目抄一遍 有效得多。2. 环境准备与版本说明2.1 Unity 安装与 Unity Hub 使用如果你还没有安装 Unity建议先通过 Unity Hub 统一管理多个 Unity 编辑器版本方便后续不同项目切换。安装流程大致是下载并安装 Unity Hub在 Unity Hub 的 安装 页面选择 Unity Editor 版本勾选需要的 Build Support 模块比如 Android Build Support、Windows Build Support在 项目 页面新建 3D 核心3D Core项目如果需要接入微信小游戏或移动端平台再单独安装对应模块。不同项目的模板不同但对新手练习项目来说3D Core内置渲染管线和Universal 3DURP 渲染管线都可以选。URP 的画面效果更现代一些学习曲线会稍微多一点点概念但后续做移动端和性能优化时更有优势。本文代码以 URP 项目为例也能兼容核心逻辑不受影响。版本方面需要提醒一点Unity 从 2020 到 2023 的 LTS 版本在 API 层面总体稳定但如果你后续接入其他 SDK建议先确认 SDK 支持哪个 Unity 版本范围。不需要追求最新版本稳定优先。2.2 创建项目时的推荐设置新建项目后建议先做几项基础设置进入Edit Project Settings Player设置产品名称和公司名称进入Edit Project Settings Quality调整像素质量和阴影质量保证运行流畅度如果使用 URP需要确认Graphics设置中渲染管线资产已指定。对于初学者不需要花太多时间在画质参数上重点是先跑通逻辑。2.3 推荐目录结构一个清晰的目录结构会让后期维护舒服很多推荐按以下方式组织Assets/ Scenes/ // 存放场景如 Main.unity、End.unity Scripts/ // C# 脚本 Player/ Camera/ Pickup/ UI/ Manager/ Prefabs/ // 预制体Player、Coin 等 Materials/ // 材质 ScriptableObjects/ // 可选配置类资产之所以按功能拆分文件夹是因为第三章之后项目会继续变大比如加入音效、动画、数据配置等提前养成分层管理的习惯后续会省很多时间。3. 核心知识拆解控制、跟随、UI 与场景切换3.1 使用 Transform 实现位移与旋转Unity 中最基础的移动方式就是直接操作Transform。using UnityEngine; public class SimpleMove : MonoBehaviour { public float moveSpeed 5f; void Update() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 direction new Vector3(horizontal, 0, vertical); transform.Translate(direction * moveSpeed * Time.deltaTime, Space.World); } }这段代码的作用是Input.GetAxis(Horizontal)读取 A/D 或左右方向键输入Input.GetAxis(Vertical)读取 W/S 或上下方向键输入将输入方向乘以速度再乘Time.deltaTime保证不同帧率下移动速度一致。需要注意直接操作 Transform 适合简单原型但如果角色需要物理碰撞更推荐使用Rigidbody或CharacterController否则容易穿墙或抖动。3.2 摄像机平滑跟随的两种写法摄像机跟随是第三章的必做功能。最简单的写法是把摄像机拖到角色物体下作为子物体但这种跟随是硬跟随旋转和抖动都会直接传过去而且不适合很多需要缓冲效果的场景。更推荐自己在脚本中实现平滑跟随。using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0, 4, -6); public float smoothTime 0.2f; private Vector3 velocity Vector3.zero; void LateUpdate() { if (target null) return; Vector3 targetPosition target.position offset; transform.position Vector3.SmoothDamp( transform.position, targetPosition, ref velocity, smoothTime ); transform.LookAt(target); } }选择LateUpdate而不是Update的原因在于Update中先完成角色移动LateUpdate中再更新摄像机位置能减少因帧顺序问题造成的抖动。如果希望摄像机跟随鼠标旋转视角可以在此基础上叠加欧拉角float mouseX Input.GetAxis(Mouse X) * rotateSpeed; float mouseY Input.GetAxis(Mouse Y) * rotateSpeed; rotationX - mouseY; rotationX Mathf.Clamp(rotationX, -30f, 60f); transform.rotation Quaternion.Euler(rotationX, transform.eulerAngles.y mouseX, 0);3.3 CanvasGroup 实现 UI 渐隐效果第三章不少人会卡在 提示文字慢慢消失 这个需求上。最优雅的方式是使用CanvasGroup。CanvasGroup是挂在 UI 物体上的组件它的alpha属性会影响整个子 UI 集合的透明度用协程来控制 alpha 从 1 渐变到 0非常方便。using System.Collections; using UnityEngine; public class UIFadeOut : MonoBehaviour { public CanvasGroup group; public float fadeDuration 1f; public void StartFadeOut() { StartCoroutine(FadeOutCoroutine()); } private IEnumerator FadeOutCoroutine() { float elapsed 0f; while (elapsed fadeDuration) { elapsed Time.deltaTime; group.alpha Mathf.Lerp(1f, 0f, elapsed / fadeDuration); yield return null; } group.alpha 0f; group.blocksRaycasts false; } }关键点在于group.blocksRaycasts false可以在渐隐结束后让 UI 不再拦截鼠标点击如果需要反过来淡入只需要把Mathf.Lerp的起始值和目标值调换即可。3.4 场景管理Additive 模式第三章后期还会接触到场景切换。Unity 提供SceneManager类来加载场景其中LoadSceneMode.Additive是一种叠加模式可以在不卸载当前场景的情况下加载新场景。using UnityEngine; using UnityEngine.SceneManagement; public class SceneSwitcher : MonoBehaviour { public void LoadEndScene() { SceneManager.LoadScene(End, LoadSceneMode.Additive); } public void UnloadCurrentScene() { SceneManager.UnloadSceneAsync(gameObject.scene); } }使用 Additive 模式的典型场景是在全局关卡场景中叠加 UI、对话场景或者把多个小游戏场景叠加在一个主容器中。对新手来说先掌握最简单的SceneManager.LoadScene(End)就够了但知道 Additive 可以帮你理解多场景管理思路。后续做大型项目时多场景工作流几乎必备。3.5 集合类使用 Dictionary 管理金币数据第三章功能中收集金币往往需要记录 哪些金币已被收集 或 各种币种数量此时Dictionary比List更合适。using System.Collections.Generic; using UnityEngine; public class CoinManager : MonoBehaviour { private Dictionarystring, int coinInventory new Dictionarystring, int(); public void AddCoin(string coinType, int count) { if (coinInventory.ContainsKey(coinType)) { coinInventory[coinType] count; } else { coinInventory.Add(coinType, count); } } public int GetCoinCount(string coinType) { if (coinInventory.TryGetValue(coinType, out int value)) { return value; } return 0; } }使用Dictionary的好处是查找键值的时间复杂度接近 O(1)适合按类型快速累计数量。新手不必过度设计数据结构但学会在合适的场景用集合类是第三章之后的重要进阶点。3.6 宏定义与平台相关代码第三章的练习项目后期可能要打包到 Windows、Android甚至是微信小游戏平台。此时可以使用 Unity 的宏定义来隔离平台相关代码。using UnityEngine; public class PlatformHelper : MonoBehaviour { void Start() { #if UNITY_ANDROID Debug.Log(当前运行在 Android 平台); #elif UNITY_IOS Debug.Log(当前运行在 iOS 平台); #elif UNITY_STANDALONE_WIN Debug.Log(当前运行在 Windows 桌面); #else Debug.Log(当前平台未知); #endif } }实际操作中宏定义还可以配合移动端权限、隐私弹窗、分辨率适配等场景使用。新手可以先记住#if UNITY_ANDROID、#if UNITY_EDITOR、#if UNITY_STANDALONE_WIN这三个最常用的。4. 完整实战案例从一个空场景到可玩的小型练习项目这一部分我们一步步搭建完整项目。为了方便理解我把整个流程拆成下面几个步骤。4.1 场景与角色搭建第一步创建两个场景Main.unity主游戏场景包含玩家、金币、摄像机、UIEnd.unity结束场景展示 收集完成 文字。在Main场景中创建一个球体作为玩家GameObject 3D Object Sphere重命名为Player在Player上添加Rigidbody组件创建材质设置成你喜欢颜色方便区分场景物体。给地面创建平面GameObject 3D Object Plane重命名为Ground调整大小或缩放确保地面范围足够。然后创建金币物体GameObject 3D Object Capsule重命名为Coin_01或者直接使用 Sphere 缩小到合适大小添加SphereCollider勾选Is Trigger挂到Assets/Prefabs/下重命名为Coin。4.2 玩家移动控制脚本在Assets/Scripts/Player/下新建PlayerController.cs使用刚体控制小球移动using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class PlayerController : MonoBehaviour { public float moveSpeed 8f; public float rotateSpeed 120f; private Rigidbody rb; void Start() { rb GetComponentRigidbody(); rb.freezeRotation true; } void FixedUpdate() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 move new Vector3(horizontal, 0f, vertical).normalized; rb.MovePosition(transform.position move * moveSpeed * Time.fixedDeltaTime); if (move ! Vector3.zero) { Quaternion targetRotation Quaternion.LookRotation(move); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.fixedDeltaTime); } } }说明FixedUpdate适合物理操作因为它与物理引擎同步rb.MovePosition不会破坏刚体的物理碰撞freezeRotation true防止角色在碰撞时任意翻转Quaternion.Slerp让角色平滑转向移动方向。4.3 摄像机跟随脚本在Assets/Scripts/Camera/下新建CameraFollow.cs。using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0, 6, -8); public float smoothTime 0.3f; private Vector3 velocity Vector3.zero; void LateUpdate() { if (target null) return; Vector3 targetPos target.position offset; transform.position Vector3.SmoothDamp(transform.position, targetPos, ref velocity, smoothTime); transform.LookAt(target); } }在场景中把 Main Camera 的CameraFollow组件中Target拖入 Player并调整Offset确保能完整看到玩家和周围环境。这里的smoothTime越小跟随越灵敏越大越有缓冲感可以根据手感调整。4.4 拾取物与碰撞判定在Assets/Scripts/Pickup/下新建Coin.cs。using UnityEngine; public class Coin : MonoBehaviour { [SerializeField] private int scoreValue 1; private void OnTriggerEnter(Collider other) { if (other.CompareTag(Player)) { GameManager.Instance.AddScore(scoreValue); gameObject.SetActive(false); } } }这里使用GameManager.Instance来统一管理得分是第三章很好的工程实践避免到处都是静态变量。先在玩家物体上设置 Tag选中 Player在 Inspector 顶部 Tag 下拉菜单中选择Player如果没有就点击Add Tag添加。4.5 UI 渐隐与场景切换创建 UGUI 文本提示。在场景中GameObject UI Text - TextMeshPro选择导入 TMP Essentials将文本内容设置为 获得金币 1挂上CanvasGroup组件初始alpha 1新建脚本UIFadeOut.cs内容和 3.3 节一样并通过GetComponentCanvasGroup获取组件。再创建游戏管理器GameManager.cs放于Assets/Scripts/Manager/。using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } [SerializeField] private TextMeshProUGUI scoreText; [SerializeField] private CanvasGroup fadeGroup; [SerializeField] private float fadeDuration 1f; private int score 0; private int totalCoins; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); } private void Start() { totalCoins FindObjectsOfTypeCoin().Length; UpdateScoreText(); } public void AddScore(int value) { score value; UpdateScoreText(); StartCoroutine(FadeTextRoutine()); } private void UpdateScoreText() { if (scoreText ! null) { scoreText.text $金币{score} / {totalCoins}; } } private System.Collections.IEnumerator FadeTextRoutine() { fadeGroup.alpha 1f; float elapsed 0f; while (elapsed fadeDuration) { elapsed Time.deltaTime; fadeGroup.alpha Mathf.Lerp(1f, 0f, elapsed / fadeDuration); yield return null; } fadeGroup.alpha 0f; } public void CheckAllCoinsCollected() { if (score totalCoins) { SceneManager.LoadScene(End); } } }这里有一个小优化点在AddScore后调用CheckAllCoinsCollected判断是否已经收集所有金币是就切换场景。为了让TextMeshProUGUI可用需要引入TMPro命名空间或在代码中写成TMPro.TextMeshProUGUI。我这里用的是TextMeshProUGUI需要加上using TMPro;否则会报类型找不到。正确引入后开头应该是using TMPro; using UnityEngine; using UnityEngine.SceneManagement;4.6 运行与验证全部脚本挂好后运行项目验证顺序建议如下检查 Player 是否能通过 WASD 移动并且移动方向符合预期检查摄像机是否平滑跟随不穿墙、不抖动触碰金币后金币隐藏UI 文字更新并渐隐收集完所有金币后场景自动切换到 End在 End 场景中添加一个 Button 或按任意键回到主场景完整闭环。End 场景中如果要做得友好一点可以添加一个脚本监听输入来返回主场景using UnityEngine; using UnityEngine.SceneManagement; public class EndPanel : MonoBehaviour { void Update() { if (Input.anyKeyDown) { SceneManager.LoadScene(Main); } } }需要注意如果使用LoadScene切换且没有指定LoadSceneMode.Additive默认会替换当前场景GameManager 如果不保留就会销毁。为了让分数跨场景保留我们在Awake中使用了DontDestroyOnLoad(gameObject)这样场景切换时 GameManager 对象不会消失。5. 常见问题与排查思路问题现象常见原因解决思路角色按 WASD 不动没有挂脚本、方向速度为零、Rigidbody 被冻结检查脚本是否挂到对象输出 Debug.Log 验证输入值摄像机抖动在 Update 中跟随、SmoothDamp 参数过小把跟随逻辑移到 LateUpdate调大 smoothTimeUI 渐隐不生效CanvasGroup 未挂、alpha 被覆盖确认调用的组件是 CanvasGroup而不是文本自身透明度OnTriggerEnter 不触发物体没有 Collider、未勾选 Is Trigger、没有 Rigidbody拾取物勾选 Is Trigger角色必须带 RigidbodyLoadScene 后 GameManager 丢失没有 DontDestroyOnLoad在 Awake 中调用 DontDestroyOnLoad(gameObject)打包后 UI 显示模糊Canvas 缩放模式设置不合理UI Canvas 的 CanvasScaler 使用 Scale With Screen Size新场景加载后相机找不到目标CameraFollow 中的 Target 为空场景中重新指定 Target或用查找代码自动查找5.1 排查思路补充“角色不动”是很经典的问题新手可以参考下面的排查顺序打开 Inspector确认脚本是否成功挂载且没有任何脚本报错检查Input.GetAxis是否收到输入可以在Update中加一行Debug.Log(horizontal , vertical);检查刚体是否被冻结位置或旋转检查移动代码是否写在Update中且没有乘以Time.deltaTime导致不同设备速度不一致。如果你遇到NullReferenceException大多是 Inspector 中引用没有拖拽到位比如CameraFollow的target没有赋值。6. 最佳实践与工程建议6.1 脚本组织规范Unity 项目中脚本最好按职责分层不要把所有逻辑堆在 Player 身上。推荐的分层方式Controller玩家控制、摄像机控制System游戏管理器、场景切换、存档UIUI 面板逻辑、文本动画、按钮事件Data数据结构定义、配置。每一层只负责自己的事模块之间通过GameManager或事件通信避免互相强引用。6.2 输入系统选型建议Unity 现在有两套输入系统旧的Input Manager和新的Input System。初学者建议先用旧Input Manager因为教程多、简单直接如果项目要支持手柄、触屏、多平台映射可以逐步迁移到Input System。个人建议是第三章新手练习不要同时学两套输入方案先跑通一套后续再切换。6.3 渲染管线建议URP 是当前 Unity 官方主推的渲染管线适合移动端优化。如果你的电脑性能允许可以在项目初始阶段就选择 Universal 3D 模板这样可以提前积累 URP 的概念比如 Renderer Feature、后处理 Volume 等。但如果你只是做纯逻辑练习内置渲染管线也能跑不需要频繁切换。脚本逻辑在两种管线中是相通的这一点不用担心。6.4 性能优化注意点虽然新手项目规模小但从一开始就注意性能能省不少后期成本尽量使用对象池而不是频繁Instantiate和Destroy金币场景中不要放太多带实时阴影的光源UI 动画尽量减少每帧修改大尺寸区域的Layout使用Profiler窗口观察 CPU 耗时确认卡顿来源打包移动端时关注 IL2CPP 构建时间同时检查 API Level 和权限声明涉及 Android 隐私合规时要提前处理用户协议弹窗。6.5 日志与调试习惯调试时不要只靠print可以规范化日志格式Debug.Log($[{nameof(GameManager)}] score {score}, this);导出正式包之前建议关闭多余的Debug.Log或者在代码中使用#if UNITY_EDITOR包一层避免影响性能。7. 总结与学习路线这一章你练到的核心能力可以总结为四个方面刚体驱动的玩家控制、LateUpdate 中的摄像机跟随、CanvasGroup 渐隐反馈、场景切换与单例管理。这四个能力可以组合成大量常见玩法原型比如收集类闯关、逃离类小游戏、模拟经营中的点击反馈等。下一步建议按顺序继续学习预制体与对象池把金币改为动态生成 对象池回收提升运行效率动画系统给玩家模型添加简单的 Animator 状态切换音效管理用AudioSourceAudioMixer管理音效分类存档功能把得分通过 PlayerPrefs 或 JSON 文件保存让项目有 持久化 概念移动端适配打包到 Android 前处理分辨率、触控输入和隐私弹窗事件驱动学习UnityEvent、Action、ScriptableObject事件系统减少脚本之间的耦合。这一套流程走完你的 Unity 基础就算扎实了。之后不管是学 Urp 渲染、Shader 还是 DOTS都会更从容。如果你在做第三章练习时碰到了问题欢迎在评论区把报错信息贴出来也可以聊聊你目前的练习项目做到哪一步了一起排查。