
1. 项目概述为什么第五天就要做项目如果你正在学习Unity或者任何一门编程或游戏开发技术可能会觉得“第五天”就搞项目有点太快了。通常的教程路线图前五天可能还在熟悉界面、学习移动方块、理解刚体和碰撞体。但我想说这个节奏恰恰是最高效的。我自己带过不少新人也走过很多弯路发现一个核心问题知识如果不用忘得比学得快。“Unity学习90天 - 第5天 - 阶段小项目”这个标题背后隐藏的是一种“微项目驱动”的学习理念。它不是让你在第五天就做出一个完整的游戏而是通过一个目标明确、范围极小、功能闭环的微型项目把前几天学到的零散知识点比如场景搭建、物体移动、基础脚本、组件添加串联起来形成一个初步的“肌肉记忆”。这就像学游泳与其在岸上背一百遍划水口诀不如直接下水扑腾几下呛两口水感受会更深刻。这个阶段小项目的核心价值在于建立正反馈。学习初期最容易放弃就是因为看不到成果。一个能在半小时到一小时内完成的小项目能让你立刻看到自己的代码和操作变成了屏幕上可以交互的东西这种成就感是持续学习最强的动力。同时它也是一个绝佳的“查漏补缺”机会。你以为你懂了Transform但在让物体按特定路径移动时可能就卡住了你以为你懂了OnCollisionEnter但在检测碰撞时发现根本没触发。项目就是最好的试金石。2. 核心需求解析第五天我们到底要练什么基于“第五天”这个时间点我们假设你已经完成了最基础的入门。你可能已经知道了Unity编辑器的基本布局Scene视图、Game视图、Hierarchy、Project、Inspector。如何创建基本的3D物体Cube, Sphere, Plane。了解Transform组件Position, Rotation, Scale的基本操作。可能写过一个简单的脚本让物体绕Y轴旋转或者用键盘WASD移动。那么第五天的项目就需要围绕这些已掌握但尚不熟练的核心技能来设计。它的需求必须满足以下几点2.1 目标明确且单一项目不能贪多求全。一个明确的目标比如“制作一个玩家可以控制移动并收集散落宝石的小场景”就比“做一个有战斗、有NPC、有背包的RPG demo”要好一万倍。前者目标清晰后者只会让人无从下手充满挫败感。2.2 覆盖核心基础操作这个微型项目必须强制你使用以下基础操作场景搭建摆放地形Plane、障碍物Cube、收集物Sphere。组件管理给玩家物体添加Rigidbody刚体组件以实现物理运动给收集物添加Collider碰撞体并设置为触发器Is Trigger。基础脚本编写编写控制玩家移动的脚本处理Input输入修改Transform或使用Rigidbody.AddForce编写检测碰撞并销毁收集物的脚本。预制体Prefab初步使用如果收集物有多个应该使用预制体来创建这是Unity工作流中极其重要的一环。2.3 具备完整的“输入-处理-输出”循环一个有效的练习项目应该能让你体验完整的游戏逻辑链条输入玩家按下键盘按键如WASD。处理你的脚本接收到输入计算出移动方向Vector3并施加给玩家物体通过Transform.translate或Rigidbody。输出玩家物体在场景中移动。当与收集物碰撞时触发另一个脚本的逻辑如销毁收集物、播放音效、更新UI分数。2.4 可扩展性为后续学习埋下伏笔虽然项目简单但它的结构应该能方便地加入一点点新东西。比如收集完所有宝石后可以简单地弹出一个“胜利”文本。这引入了UI系统的概念为后续学习UGUI或UI Toolkit做了铺垫。基于以上分析我建议第五天的阶段小项目定为“3D滚球收集”。这是一个经典到不能再经典的入门项目但它完美契合了所有需求。3. 项目实战“3D滚球收集”全流程拆解下面我将带你一步步完成这个“3D滚球收集”项目。我会假设你是一个有前几天基础的初学者过程中我会穿插解释“为什么这么做”以及你可能遇到的坑。3.1 场景与基础物体搭建首先打开Unity创建一个新的3D项目模板选Core就行。我们将搭建一个最简单的竞技场。创建地面在Hierarchy窗口右键 - 3D Object - Plane。这将是我们的地面。在Inspector面板你可以重命名为“Ground”。为了更好看我们可以稍微调整一下。在Project窗口右键 - Create - Material创建一个新材质球命名为“GroundMat”。选中它在Inspector里将Albedo基础颜色调成浅灰色或绿色。然后把这个材质球拖到场景中的Ground物体上。创建玩家球Hierarchy右键 - 3D Object - Sphere。重命名为“Player”。用鼠标或移动工具快捷键W把它拉到地面上方一点比如Position Y设为1这样它出生时会落在地面上而不是嵌进去。创建收集物宝石Hierarchy右键 - 3D Object - Cube。重命名为“Pickup”。把它缩小一些Scale设为0.5, 0.5, 0.5并放到地面上某个位置。为了区分我们同样给它一个醒目的材质。新建一个材质球“PickupMat”颜色设为亮黄色或红色并拖给Pickup物体。创建墙壁可选但推荐复制几个CubeCtrlD拉长或压扁摆放在Ground的边缘形成一个围墙防止我们的球滚出地图。这是很好的场景设计练习。注意在这个阶段不要急于写代码。先花几分钟把场景布置得像个样子。良好的场景组织是后续开发的基础。你可以把所有的墙壁物体都拖到Hierarchy中的一个空物体右键Create Empty下并命名为“Walls”这样层次结构会更清晰。这个操作叫“Parenting”建立父子关系。3.2 为玩家添加物理与移动控制现在让我们的球动起来。添加刚体Rigidbody选中Player物体在Inspector面板点击“Add Component”搜索并添加Rigidbody组件。刚体组件让物体受物理引擎控制会有重力可以碰撞。这是实现真实滚动感的关键。创建移动脚本在Project窗口的Assets文件夹下右键 - Create - C# Script命名为PlayerController。双击用你的代码编辑器如VSCode, Rider打开。编写移动逻辑我们将使用力Force的方式来移动这样更符合物理直觉球会有惯性手感更好。using UnityEngine; public class PlayerController : MonoBehaviour { // 声明一个公共变量来控制移动速度这样可以在Unity编辑器里随时调整无需修改代码 public float moveSpeed 10f; // 声明一个私有变量来引用刚体组件 private Rigidbody rb; // Start在游戏开始时调用一次用于初始化 void Start() { // 获取挂载在同一物体上的Rigidbody组件引用 rb GetComponentRigidbody(); } // FixedUpdate在每个固定的物理帧被调用处理物理相关的操作如施加力最好在这里 void FixedUpdate() { // 获取键盘输入得到的是一个-1到1之间的值 float moveHorizontal Input.GetAxis(Horizontal); // A/D 或 左右箭头 float moveVertical Input.GetAxis(Vertical); // W/S 或 上下箭头 // 根据输入创建一个方向向量。注意我们是在XZ平面上移动所以Y轴为0。 Vector3 movement new Vector3(moveHorizontal, 0.0f, moveVertical); // 给刚体施加一个力。ForceMode.Force表示持续施加力会产生加速度效果。 rb.AddForce(movement * moveSpeed); } }代码解析与避坑点public float moveSpeed声明为public它就会出现在Unity的Inspector面板中。你可以运行游戏后实时调整这个值来感受不同的手感这是Unity非常强大的一个特性。GetComponentRigidbody()这是在获取当前物体this.gameObject上的Rigidbody组件。我们把它存到rb变量里避免在每次FixedUpdate中都去获取一次提高效率。Input.GetAxis(“Horizontal”)返回一个平滑的值。如果你按下A键它会从0渐变到-1松开时渐变回0。这比Input.GetKey那种非0即1的检测手感更平滑更适合移动控制。FixedUpdatevsUpdateUpdate每帧调用但帧率不稳定。FixedUpdate以固定的时间间隔调用默认0.02秒专门用于物理计算。对Rigidbody进行操作如AddForce必须在FixedUpdate中否则会导致物理行为不稳定。常见问题如果你发现球移动得非常慢或非常快先检查两个地方一是moveSpeed的值先从10开始试二是球的Rigidbody组件的Mass质量和Drag阻力。质量太大需要更大的力阻力太大会让球很快停下。挂载脚本保存代码回到Unity。将Project中的PlayerController脚本拖到Hierarchy里的Player物体上。你会看到Inspector里Player物体下多了这个脚本组件并且有一个Move Speed参数可以调整。现在运行游戏点击顶部播放按钮你应该可以用WASD键控制球滚动了3.3 实现收集物与碰撞检测球能动了接下来要让收集物能被“收集”。设置收集物为触发器选中我们之前创建的PickupCube。在Inspector中找到Box Collider组件。勾选Is Trigger选项。这意味着这个碰撞体不会产生物理阻挡球会穿过去但会触发碰撞事件正好用于检测“穿过”行为。创建收集物脚本新建一个C#脚本命名为Pickup。using UnityEngine; public class Pickup : MonoBehaviour { // 这是一个可选的效果让收集物自身缓慢旋转看起来更吸引人 public float rotationSpeed 50f; void Update() { // 每帧绕Y轴旋转一定角度 transform.Rotate(0, rotationSpeed * Time.deltaTime, 0); } // 当有其他碰撞体进入本触发器时调用 void OnTriggerEnter(Collider other) { // 判断进入触发器的物体是否是玩家通过Tag标签来识别 if (other.gameObject.CompareTag(Player)) { // 如果是玩家则销毁这个收集物 Destroy(gameObject); // 这里可以添加更多效果比如播放音效、增加分数等。 // Debug.Log(Pickup Collected!); // 可以在控制台输出信息用于调试 } } }设置玩家标签为了让上面的CompareTag(“Player”)生效我们需要给Player物体打上“Player”标签。选中Player物体在Inspector顶部点击Tag下拉框选择“Add Tag…”。在打开的标签管理器中点击“”号新建一个标签命名为“Player”。然后关闭管理器再次选中Player物体在Tag下拉框中就能选择我们新建的“Player”标签了。挂载脚本并测试将Pickup脚本拖到Pickup物体上。运行游戏控制球撞向那个黄色的方块方块应该会消失。3.4 使用预制体批量创建收集物场景里只有一个收集物太无聊了。我们需要很多个。手动复制粘贴效率低且修改一个要改所有。这时就该预制体Prefab出场了。创建预制体在Project窗口的Assets文件夹下创建一个新文件夹叫“Prefabs”。然后将Hierarchy中的Pickup物体直接拖到这个文件夹里。你会看到它变成了一个蓝色的立方体图标这表示它已经是一个预制体了。此时Hierarchy中的那个Pickup实例名字会变成蓝色表示它是预制体的一个实例。批量放置实例现在你可以放心地删除Hierarchy中原来的那个Pickup实例。然后从Project的Prefabs文件夹里将Pickup预制体拖到Scene视图中想放几个就放几个。你可以按住CtrlDWindows或CmdDMac来快速复制选中的实例。预制体的威力现在如果你想修改所有收集物的外观比如颜色或行为比如旋转速度你只需要修改预制体资源本身。在Project中双击打开Prefab编辑模式或者直接在Project里选中预制体在Inspector里修改其材质或Pickup脚本上的rotationSpeed参数。修改后场景中所有由这个预制体生成的实例都会自动更新。这是Unity工作流的基石务必掌握。3.5 添加简单的UI反馈最后我们给这个微型项目一个完成的标志当所有收集物被吃完显示“You Win!”。创建UI文本在Hierarchy窗口右键 - UI - Text - TextMeshPro如果第一次使用会弹出导入TMP Essentials的窗口点击导入。这会自动创建一个Canvas画布和一个TextMeshPro文本子物体。重命名文本为“WinText”。设置文本属性选中WinText在Inspector中Text Input框里输入“You Win!”。调整Font Size比如调到60。在Alignment对齐方式中选择水平居中和垂直居中。你可以拖动文本到屏幕中央Game视图能看到效果。最关键的一步在WinText物体的Inspector左上角取消勾选即禁用这个物体。因为我们希望游戏开始时它不显示收集完所有物品后才显示。修改玩家控制器脚本以计数我们需要让PlayerController知道一共有多少个收集物以及它收集了多少个。using UnityEngine; using TMPro; // 需要引入TextMeshPro的命名空间 public class PlayerController : MonoBehaviour { public float moveSpeed 10f; private Rigidbody rb; // 新增用于计数的变量 private int totalPickups; private int collectedPickups 0; // 新增用于引用UI文本 public TextMeshProUGUI winText; // 在Inspector中拖拽赋值 void Start() { rb GetComponentRigidbody(); // 新增在游戏开始时查找场景中所有带有Pickup标签的物体更健壮的做法是查找Pickup脚本这里用标签简单演示 // 注意这里假设你给所有Pickup预制体实例打上了“Pickup”标签就像给Player打标签一样。 GameObject[] pickupObjects GameObject.FindGameObjectsWithTag(Pickup); totalPickups pickupObjects.Length; // 初始化时隐藏胜利文本双重保险 if (winText ! null) winText.gameObject.SetActive(false); } void FixedUpdate() { // ... 原有的移动代码不变 ... float moveHorizontal Input.GetAxis(Horizontal); float moveVertical Input.GetAxis(Vertical); Vector3 movement new Vector3(moveHorizontal, 0.0f, moveVertical); rb.AddForce(movement * moveSpeed); } // 新增一个公共方法供收集物脚本调用通知玩家“我已被收集” public void PickupCollected() { collectedPickups; Debug.Log($Collected: {collectedPickups}/{totalPickups}); // 检查是否收集完毕 if (collectedPickups totalPickups winText ! null) { winText.gameObject.SetActive(true); // 显示胜利文本 Debug.Log(You Win!); } } }修改收集物脚本以调用计数Pickup脚本的OnTriggerEnter方法需要修改不再直接Destroy而是通知PlayerController。void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag(Player)) { // 获取玩家身上的PlayerController脚本 PlayerController player other.gameObject.GetComponentPlayerController(); if (player ! null) { player.PickupCollected(); // 通知玩家 } Destroy(gameObject); // 然后销毁自己 } }设置引用与标签选中Player物体在PlayerController组件上你会看到新增的Win Text字段。将Hierarchy中Canvas下的WinText物体拖到这个字段上完成引用赋值。给所有的Pickup预制体实例打上“Pickup”标签就像之前给Player打标签一样。或者更推荐的做法是直接修改Pickup预制体资源本身给它加上“Pickup”标签。这样所有实例都会自动拥有这个标签。运行测试运行游戏控制球收集所有方块。当最后一个方块被收集时“You Win!”文本应该会出现在屏幕中央。4. 项目复盘与核心经验总结这个“3D滚球收集”项目虽然小但几乎涵盖了Unity新手入门期所有最核心的概念和操作。做完它你再回头看前四天学的零散知识会发现它们突然被串联成了一个有意义的整体。下面是我总结的几点核心经验很多是官方教程里不会细说的“坑”4.1 物理移动的“手感”调校是一门学问你可能会觉得球太滑、停不下来或者加速太慢。这不仅仅是moveSpeed一个参数的问题。你需要联动调整Rigidbody上的几个参数Mass质量质量越大惯性越大加速减速都更慢。玩家角色一般设置在1-10之间。Drag阻力模拟空气/地面阻力。值越大物体停下得越快。如果你希望球有冰面滑行的感觉就调低如0.1如果希望它很快停下就调高如2。Angular Drag角阻力影响旋转的阻力。对于球体可以适当调高以防止它乱转。最佳实践不要只调脚本的moveSpeed把Rigidbody的这些参数也纳入你的调试范围。创建一个测试场景专门用来调试手感。4.2 碰撞与触发器的区别必须门清这是新手最容易混淆的地方之一。普通碰撞体Is Trigger false两个物体都会产生物理相互作用会弹开、会阻挡。需要双方都有Collider至少一方有Rigidbody。相关事件是OnCollisionEnter/Stay/Exit。触发器Is Trigger true物体间会互相穿透不会产生物理阻挡。用于检测区域、拾取物品、触发机关。同样需要碰撞体和刚体。相关事件是OnTriggerEnter/Stay/Exit。关键点如果你的物体需要物理模拟如掉落、被推开用普通碰撞。如果只是需要知道“有没有碰到”用触发器。在这个项目中收集物用触发器是完美的选择。4.3 预制体工作流是效率的生命线很多新手喜欢在场景里直接编辑物体然后复制。一旦需要修改就得一个个改极易出错和遗漏。从今天起养成习惯任何会在场景中出现多次的物体敌人、子弹、道具、特效第一时间做成预制体。在场景中编辑的是预制体实例大部分修改如位置、旋转只影响该实例。但如果想修改所有同类物体的核心属性如血量、伤害值、材质、脚本参数务必去Project中修改预制体资源。修改预制体资源后场景中所有实例的蓝色文字提示可能会显示“Prefab Modified”。你可以选择“Overrides - Apply All”将实例的特定修改也应用回预制体或者“Revert All”放弃实例的修改同步回预制体的状态。理解这个流程至关重要。4.4 代码与Inspector的配合Public变量的妙用把脚本中的变量声明为public或者使用[SerializeField]私有变量特性可以让它们在Unity编辑器的Inspector中显示和修改。这带来了巨大的灵活性实时调试游戏运行时你可以直接拖拽Inspector中的数值立即看到效果无需停止游戏、修改代码、重新编译。分工协作美术或策划同学可以不需要懂代码直接通过Inspector调整角色的速度、血量、攻击力等参数。资源引用就像我们把WinText拖给脚本一样这是一种松耦合的引用方式。脚本不需要知道UI文本具体在哪谁在什么时候创建它只需要一个对TextMeshProUGUI组件的引用而这个引用由你在编辑器里配置。4.5 关于FindGameObjectsWithTag与性能在这个示例中我们在Start里使用了GameObject.FindGameObjectsWithTag(“Pickup”)来查找所有收集物。这对于小型项目或初始化阶段是可以接受的。但是请务必注意Find系列方法Find,FindWithTag,FindGameObjectsWithTag是比较耗性能的操作尤其不适合在Update这类每帧执行的方法中调用。更好的做法对于动态生成的物体比如敌人、子弹通常采用对象池Object Pooling技术来管理并通过事件或列表来跟踪它们而不是每一帧去查找。当前项目的替代方案可以创建一个空的GameObject命名为“PickupManager”挂载一个脚本负责生成所有收集物并记录它们的数量。PlayerController只需引用这个Manager即可。这引入了“管理器”的概念是构建更复杂游戏架构的第一步。5. 常见问题与排查技巧实录即使跟着步骤做你也可能会遇到一些问题。这里列出一些第五天项目最典型的“坑”和解决方法。5.1 球根本不移动或者移动异常缓慢/飞快检查步骤脚本挂载确认PlayerController脚本是否确实挂在了Player物体上。刚体组件确认Player物体上有Rigidbody组件。输入轴名称确认代码中Input.GetAxis(“Horizontal”)和“Vertical”的拼写完全正确大小写敏感。Unity默认的输入轴就是这两个名字。力的大小检查moveSpeed的值。先从10开始如果太慢调到50太快调到5。同时检查Rigidbody的Mass如果设为1000那10的力就像在推卡车。力的模式AddForce默认是ForceMode.Force这是持续的力。如果你错误地每帧在Update中调用而不是FixedUpdate由于帧率波动力的施加会不稳定。5.2 碰撞/触发器没有反应直接穿过去了检查步骤碰撞体存在双方物体都必须有Collider组件。检查PlayerSphere是否有Sphere ColliderPickupCube是否有Box Collider。刚体存在在触发碰撞/触发事件时至少有一方需要有Rigidbody组件。通常让运动的、主动的一方Player带有Rigidbody即可。触发器开关确认Pickup的Box Collider上Is Trigger是否被勾选。层级碰撞矩阵Unity有一个物理层Layer系统可以设置哪些层之间忽略碰撞。默认情况下所有层都相互碰撞。但如果你不小心修改了可能会导致碰撞失效。检查Edit - Project Settings - Physics查看Layer Collision Matrix。标签判断确认Player物体的Tag确实被设置成了“Player”注意大小写并且Pickup脚本中CompareTag(“Player”)的字符串拼写一致。5.3 胜利文本不显示检查步骤引用为空这是最常见的问题。检查Player物体上PlayerController组件的Win Text字段是否为“None (Text Mesh Pro UGUI)”。确保你已经把Hierarchy里的WinText对象拖拽到了这个字段里。对象未激活虽然我们在代码和Start里设置了SetActive(false)但也要确保一开始在Inspector里WinText物体本身是激活的复选框勾选。代码SetActive(false)是运行时禁用但如果物体在编辑器里本来就是未激活的代码可能无法正确引用它取决于获取引用的方式。最保险的做法是编辑器里保持激活在Start中禁用。计数逻辑错误在PlayerController的Start方法中打印一下totalPickups的值看是否和你场景中放置的Pickup数量一致。如果使用了标签查找确保所有Pickup实例的标签都设置正确。方法调用失败在Pickup脚本的OnTriggerEnter方法里添加Debug.Log(“Trigger with: ” other.name);看看碰撞是否真的被检测到。再检查获取PlayerController组件是否成功。5.4 预制体修改后场景中的实例没更新检查步骤确认修改的是预制体资源在Project窗口中选中那个蓝色的预制体文件进行修改而不是选中场景中的某个蓝色实例。实例存在覆盖如果场景中的某个实例之前被单独修改过比如你调了它的旋转速度那么预制体资源的修改可能不会自动覆盖这个实例的特定修改。此时该实例在Inspector中预制体部分会显示“Overrides”。你可以点击“Overrides”下拉按钮选择“Apply All”将预制体的修改应用到该实例并覆盖其独有修改或者选择“Revert All”放弃实例的修改使其完全同步预制体。断开连接极少数情况下实例可能与预制体资源断开了连接显示为黑色文字。这时需要重新将预制体资源从Project拖到该实例上选择“Replace”。6. 从“小项目”到“大思维”下一步学什么完成这个第五天的小项目绝对值得给自己点个赞。你已经跨过了“看教程”到“做东西”的第一个关键门槛。接下来你的学习路径应该围绕这个项目的扩展和深化展开而不是盲目跳到一个全新的复杂主题。6.1 立即可以做的扩展巩固当前知识增加更多反馈当收集物被吃掉时不要只是消失。尝试播放一个简单的粒子效果Particle System。在Project右键 - Effects - Particle System创建一个做成预制体在Pickup脚本的Destroy之前实例化它。播放一个音效AudioSource。给Pickup预制体添加AudioSource组件取消勾选Play On Awake。在Pickup脚本中声明一个public AudioClip collectSound;将声音文件拖进去在OnTriggerEnter中调用AudioSource.PlayOneShot(collectSound)。增加简单挑战加入几个会移动的障碍物比如来回移动的方块。这需要你写一个新的脚本用Vector3.Lerp或Mathf.PingPong让物体在两个点之间移动。加入一个计时器UI显示收集所有物品所用的时间。这需要你了解Time.deltaTime的累积以及如何更新UI Text。优化代码结构将收集物的计数逻辑从PlayerController中抽离出来创建一个独立的GameManager单例脚本。这引入了“游戏管理器”的概念是架构思维的开始。6.2 后续学习方向建议在接下来的85天里建议以“小项目簇”的方式推进第6-15天深入Rigidbody物理、Collider形状、关节Joints、射线检测Raycast。可以做“物理谜题”小项目比如推箱子、破坏墙体。第16-30天系统学习动画系统Animator Controller、状态机、导航系统NavMeshAgent对应热词中的unity ai navigation。做一个简单的第三人称角色能走能跑能跳敌人能自动寻路过来。第31-45天深入学习UI系统UGUI或UI Toolkit、数据持久化PlayerPrefs、ScriptableObject。为你之前的项目添加完整的主菜单、暂停界面、游戏数据存储。第46-60天接触图形相关概念材质、Shader基础、灯光、后处理。尝试修改物体的外观打造不同的场景氛围。可以研究热词中提到的unity urp shader 体积光等进阶话题。第61-90天学习资源管理Addressables解决热词中unity addressables打包后tmp材质紫了的问题、性能优化Profiler使用、打包发布对应unity发布抖音小游戏。尝试将你的小项目打包成PC或移动端可执行文件。记住学习Unity或者说学习任何游戏开发技能核心在于“做”。每天以完成一个可运行、可交互的微小功能为目标积累下去90天后回头看你会惊讶于自己的进步。这个第五天的“3D滚球收集”就是你坚实的第一步。代码里的每一个BugInspector里的每一次调试都是你从新手走向熟练的必经之路。