☰
Unity新手第三章完整复盘:从玩家控制到场景切换的实战指南
2026/10/11 1:50:04 网站建设 项目流程

最近在带新手朋友完成 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 编辑器版本,方便后续不同项目切换。安装流程大致是:

  1. 下载并安装 Unity Hub;
  2. 在 Unity Hub 的 "安装" 页面选择 Unity Editor 版本,勾选需要的 Build Support 模块(比如 Android Build Support、Windows Build Support);
  3. 在 "项目" 页面新建 3D 核心(3D Core)项目;
  4. 如果需要接入微信小游戏或移动端平台,再单独安装对应模块。

不同项目的模板不同,但对新手练习项目来说,3D Core(内置渲染管线)和Universal 3D(URP 渲染管线)都可以选。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 Dictionary<string, int> coinInventory = new Dictionary<string, 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:主游戏场景,包含玩家、金币、摄像机、UI;
  • End.unity:结束场景,展示 "收集完成" 文字。

在Main场景中创建一个球体作为玩家:

  1. GameObject > 3D Object > Sphere,重命名为Player;
  2. 在Player上添加Rigidbody组件;
  3. 创建材质,设置成你喜欢颜色,方便区分场景物体。

给地面创建平面:

  1. GameObject > 3D Object > Plane,重命名为Ground;
  2. 调整大小或缩放,确保地面范围足够。

然后创建金币物体:

  1. GameObject > 3D Object > Capsule,重命名为Coin_01,或者直接使用 Sphere 缩小到合适大小;
  2. 添加SphereCollider,勾选Is Trigger;
  3. 挂到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 = GetComponent<Rigidbody>(); 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:

  1. 选中 Player;
  2. 在 Inspector 顶部 Tag 下拉菜单中选择Player,如果没有就点击Add Tag添加。

4.5 UI 渐隐与场景切换

创建 UGUI 文本提示。

  1. 在场景中GameObject > UI > Text - TextMeshPro,选择导入 TMP Essentials;
  2. 将文本内容设置为 "获得金币 +1";
  3. 挂上CanvasGroup组件,初始alpha = 1;
  4. 新建脚本UIFadeOut.cs,内容和 3.3 节一样,并通过GetComponent<CanvasGroup>获取组件。

再创建游戏管理器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 = FindObjectsOfType<Coin>().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 运行与验证

全部脚本挂好后运行项目,验证顺序建议如下:

  1. 检查 Player 是否能通过 WASD 移动,并且移动方向符合预期;
  2. 检查摄像机是否平滑跟随,不穿墙、不抖动;
  3. 触碰金币后,金币隐藏,UI 文字更新并渐隐;
  4. 收集完所有金币后,场景自动切换到 End;
  5. 在 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,调大 smoothTime
UI 渐隐不生效CanvasGroup 未挂、alpha 被覆盖确认调用的组件是 CanvasGroup,而不是文本自身透明度
OnTriggerEnter 不触发物体没有 Collider、未勾选 Is Trigger、没有 Rigidbody拾取物勾选 Is Trigger;角色必须带 Rigidbody
LoadScene 后 GameManager 丢失没有 DontDestroyOnLoad在 Awake 中调用 DontDestroyOnLoad(gameObject)
打包后 UI 显示模糊Canvas 缩放模式设置不合理UI Canvas 的 CanvasScaler 使用 Scale With Screen Size
新场景加载后相机找不到目标CameraFollow 中的 Target 为空场景中重新指定 Target,或用查找代码自动查找

5.1 排查思路补充

“角色不动”是很经典的问题,新手可以参考下面的排查顺序:

  1. 打开 Inspector,确认脚本是否成功挂载,且没有任何脚本报错;
  2. 检查Input.GetAxis是否收到输入,可以在Update中加一行Debug.Log(horizontal + "," + vertical);;
  3. 检查刚体是否被冻结位置或旋转;
  4. 检查移动代码是否写在Update中且没有乘以Time.deltaTime,导致不同设备速度不一致。

如果你遇到NullReferenceException,大多是 Inspector 中引用没有拖拽到位,比如CameraFollow的target没有赋值。

6. 最佳实践与工程建议

6.1 脚本组织规范

Unity 项目中脚本最好按职责分层,不要把所有逻辑堆在 Player 身上。推荐的分层方式:

  • Controller:玩家控制、摄像机控制;
  • System:游戏管理器、场景切换、存档;
  • UI:UI 面板逻辑、文本动画、按钮事件;
  • 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 渐隐反馈、场景切换与单例管理。这四个能力可以组合成大量常见玩法原型,比如收集类闯关、逃离类小游戏、模拟经营中的点击反馈等。

下一步建议按顺序继续学习:

  1. 预制体与对象池:把金币改为动态生成 + 对象池回收,提升运行效率;
  2. 动画系统:给玩家模型添加简单的 Animator 状态切换;
  3. 音效管理:用AudioSource+AudioMixer管理音效分类;
  4. 存档功能:把得分通过 PlayerPrefs 或 JSON 文件保存,让项目有 "持久化" 概念;
  5. 移动端适配:打包到 Android 前处理分辨率、触控输入和隐私弹窗;
  6. 事件驱动:学习UnityEvent、Action、ScriptableObject事件系统,减少脚本之间的耦合。

这一套流程走完,你的 Unity 基础就算扎实了。之后不管是学 Urp 渲染、Shader 还是 DOTS,都会更从容。

如果你在做第三章练习时碰到了问题,欢迎在评论区把报错信息贴出来,也可以聊聊你目前的练习项目做到哪一步了,一起排查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询