Unity新手入门:从零构建小球吃金币游戏,掌握核心开发流程 1. 项目概述从零到一构建你的第一个Unity小游戏如果你刚接触Unity或者想找个简单有趣的项目练手那“小球吃金币”绝对是个经典且完美的起点。这个项目麻雀虽小五脏俱全它几乎涵盖了游戏开发最核心的几个模块玩家控制、物理交互、碰撞检测、分数系统和场景管理。我当年就是从类似的项目入门的它能让你在短短一两个小时内亲眼看到自己的代码如何驱动一个游戏世界运转起来那种成就感是看再多教程都无法替代的。简单来说我们要做的就是创建一个3D场景用一个由玩家控制的小球在平台上滚动去触碰“吃”散落各处的金币每吃到一个金币屏幕上的分数就增加当吃完全部金币游戏胜利。听起来很简单对吧但在这个过程中你会亲手实践如何导入资源、编写控制脚本、处理物理碰撞、设计UI界面甚至可能接触到简单的游戏状态管理。这不仅是学习Unity引擎操作更是在理解一个游戏最基本的运行逻辑。无论你是编程新手还是有一定基础想熟悉Unity工作流的开发者这个项目都能给你带来扎实的收获。2. 核心模块设计与思路拆解在动手写代码和摆场景之前我们先花几分钟把整个游戏的骨架搭起来。一个好的设计思路能让你在开发过程中少走很多弯路避免写到一半发现架构有问题要推倒重来。2.1 游戏对象与组件规划Unity采用的是基于组件的实体架构我们的游戏世界由一个个“GameObject”游戏对象构成而功能则通过为这些对象添加“Component”组件来实现。对于这个项目我们需要规划几个核心对象玩家小球 (Player Ball)这是游戏的主角。它需要一个球体碰撞器来定义物理边界一个刚体组件来接受物理引擎的驱动比如重力、碰撞反馈以及一个我们自定义的脚本组件来处理玩家的输入控制小球移动。金币 (Coin)这是玩家收集的目标。它通常是一个简单的模型比如一个圆柱体加一个圆片需要一个碰撞器为了被检测到以及一个脚本当被小球碰撞时触发“被收集”的事件——比如播放一个旋转动画、音效然后销毁自身并通知游戏管理器加分。游戏平台/地面 (Ground)这是一个大的平面或立方体作为小球滚动的舞台。它需要一个碰撞器但通常不需要刚体除非你想要一个会动的地面它的作用是让小球和金币不会掉出世界。游戏管理器 (GameManager)这是一个“幕后黑手”式的空对象。它不直接显示在场景里但挂载着管理游戏全局状态的脚本。比如它负责记录当前分数、判断是否所有金币都被收集游戏胜利、控制UI界面的更新显示分数、胜利文本等。使用单例模式来设计这个管理器会非常方便确保在游戏的任何地方都能访问到它。UI界面 (Canvas)包括显示分数的Text组件以及可能有的“游戏胜利”提示面板。这个规划的核心思想是“职责分离”。小球只负责移动和碰撞金币只负责被收集时销毁自己并发出信号管理器负责统筹全局数据和逻辑。这样代码结构清晰后期如果想增加新功能比如敌人、陷阱也更容易扩展。2.2 物理与输入系统选型Unity内置了强大的物理引擎NVIDIA PhysX和输入管理系统我们需要决定如何利用它们。对于物理系统我们肯定要使用刚体物理。这意味着小球和金币的移动、旋转、碰撞都将由物理引擎模拟效果真实且省力。为小球添加Rigidbody组件后它会自动受到重力下落。我们的控制脚本不是直接修改它的位置而是通过给刚体施加力或直接设置速度来驱动它这样碰撞反馈才会正确。对于输入控制Unity的新输入系统Input System Package功能强大且支持跨平台但对于这个超小型项目使用传统的Input.GetAxis方法更加快捷直观。我们可以通过读取键盘的“Horizontal”A/D或左右箭头和“Vertical”W/S或上下箭头输入来构建一个控制小球滚动的力或速度向量。这里有一个关键考量控制方式。是采用力模式还是速度模式力模式 (AddForce)模拟真实的推力小球会有加速和减速的过程手感更真实但控制起来需要更精细惯性可能让新手觉得“滑”。速度模式 (直接设置velocity)直接指定小球每帧的速度控制响应即时手感更“跟手”适合需要精确操控的街机风格。对于入门项目我推荐先从速度模式开始因为它更简单直接能让你快速获得正反馈。等熟悉了再尝试力模式体验不同的手感差异。3. 场景搭建与资源准备思路清晰后我们就可以打开Unity Hub创建一个新的3D项目开始动手搭建我们的游戏世界了。3.1 基础场景构建创建项目后你首先会看到一个默认场景里面有一个主摄像机和一个平行光。我们第一步是清理和规划场景层级。创建地面在层级视图右键 - 3D Object - Plane重命名为“Ground”。调整它的缩放Scale比如设为 (10, 1, 10)让它成为一个足够大的平台。默认的Plane自带了Mesh Collider这就可以了。你可以给它一个材质改变颜色比如深灰色让它看起来像水泥地。创建玩家小球右键 - 3D Object - Sphere重命名为“Player”。将它放置在Ground上方一点的位置比如设置Position的Y坐标为1这样它生成后会落在平台上而不是嵌进去。调整它的缩放比如(0.5, 0.5, 0.5)让它大小合适。设置玩家物理属性选中Player对象在检查器Inspector中点击“Add Component”搜索并添加Rigidbody组件。你会看到质量Mass、阻力Drag等参数。暂时保持默认即可。为了获得更好的滚动效果我们可以稍微增加一些角阻力Angular Drag比如设为0.5这样小球旋转停下来会快一点手感更稳。创建金币金币我们可以用一个组合体来简单模拟。先创建一个圆柱体Cylinder重命名为“Coin”。调整其缩放比如(1, 0.1, 1)让它变成一个扁平的圆盘。为了好看可以再创建一个Cube缩放成(1.5, 0.05, 0.1)作为金币中间的“横杠”将它作为Coin的子对象。然后为Coin对象添加一个Rigidbody组件但勾选Is Kinematic。Kinematic刚体不受物理力影响但可以参与碰撞检测。我们不需要金币自己乱滚只需要它被碰时能检测到所以这样设置最合适。最后别忘了添加一个碰撞器圆柱体自带的Capsule Collider就可以。现在你的场景里应该有一个平台上面站着一个小球旁边可能有一个金币。基础的物理世界已经就位了。3.2 材质与光照简单美化虽然我们是做玩法原型但一点简单的美化能极大提升开发时的愉悦感。Unity的材质系统非常直观。创建材质在项目窗口右键 - Create - Material命名为“BallMat”。选中它在检查器里将Albedo漫反射颜色调成你喜欢的颜色比如亮红色。用同样的方法创建“GroundMat”灰色和“CoinMat”金色。调整CoinMat的Metallic金属度滑块到0.8Smoothness光滑度到0.6它看起来就更像金属了。应用材质直接将创建好的材质球从项目窗口拖拽到场景中对应的游戏对象上对象的外观就会立即改变。调整光照默认的平行光可能太强或角度不好。选中Directional Light可以调整它的旋转Rotation来改变阴影方向调整强度Intensity来改变亮度。一个从斜上方45度打下来的光通常能提供不错的立体感。你还可以在Window - Rendering - Lighting设置中点击“Generate Lighting”让Unity烘焙一下光照这样静态物体的光影效果会更实运行时光照开销也更小。注意在项目初期不要过度追求美术效果。我们的核心是功能和逻辑。这些美化步骤是为了让测试时不至于太“辣眼睛”但重心一定要放在代码实现上。很多新手容易在这里陷入不断调整材质和灯光的循环耽误了主要进度。4. 核心脚本编写与功能实现游戏场景是舞台脚本才是让舞台活起来的灵魂。接下来我们编写三个核心脚本。4.1 玩家控制脚本 (PlayerController.cs)这个脚本挂载在Player小球上负责读取玩家输入并控制小球运动。using UnityEngine; public class PlayerController : MonoBehaviour { // 移动速度可以在Unity编辑器里调整 public float moveSpeed 10f; // 引用小球的刚体组件 private Rigidbody rb; void Start() { // 在游戏开始时获取挂载在同一对象上的Rigidbody组件 rb GetComponentRigidbody(); if (rb null) { Debug.LogError(PlayerController: 未找到Rigidbody组件); } } void FixedUpdate() { // FixedUpdate在固定的物理时间步长调用更适合处理物理相关的操作 HandleMovement(); } void HandleMovement() { // 1. 获取原始输入值-1 到 1之间 float moveHorizontal Input.GetAxis(Horizontal); // A/D, 左右箭头 float moveVertical Input.GetAxis(Vertical); // W/S, 上下箭头 // 2. 基于摄像机的方向构建一个正确的移动向量。 // 假设摄像机是从上往下看的俯视角或者稍微有点倾斜。 // 这里我们使用最简单的方式将输入直接映射到X和Z轴水平地面。 Vector3 movement new Vector3(moveHorizontal, 0.0f, moveVertical); // 3. 将向量标准化并乘以速度得到最终的速度向量。 // 标准化是为了防止斜向移动时速度更快勾股定理。 if (movement.magnitude 1) { movement.Normalize(); } Vector3 targetVelocity movement * moveSpeed; // 4. 应用速度。这里我们直接设置速度实现即时响应的手感。 // 另一种方式是使用 AddForce手感更真实但惯性大。 rb.velocity new Vector3(targetVelocity.x, rb.velocity.y, targetVelocity.z); // 可选让小球朝向移动方向旋转视觉效果更好 if (movement ! Vector3.zero) { transform.forward Vector3.Slerp(transform.forward, movement, Time.deltaTime * 10f); } } }代码解析与实操要点FixedUpdatevsUpdate: 所有涉及物理引擎操作如修改Rigidbody的速度、位置、施加力的代码都应该放在FixedUpdate中。因为物理引擎的更新是固定频率的默认0.02秒一次与帧率无关。放在Update里会导致物理行为不稳定。GetComponent(): 这是一个非常常用的方法用于获取同一游戏对象上挂载的其他组件。我们在Start中获取并缓存Rigidbody的引用比在每次FixedUpdate中都去获取要高效。速度控制我们直接设置了rb.velocity。velocity是一个Vector3代表刚体每秒钟在世界空间中的移动单位数。我们保留了Y轴原有的速度主要是重力下落的速度只修改X和Z轴这样小球才能在斜坡上自然滚动。输入映射Input.GetAxis(“Horizontal”)返回一个平滑的值。你也可以使用Input.GetAxisRaw它返回-101三个整数没有平滑过渡手感更“硬”。将脚本拖到Player对象上运行游戏你现在应该可以用WASD或方向键控制小球在平台上滚动了4.2 金币脚本 (Coin.cs)这个脚本挂载在每个金币预制体上负责处理被收集的逻辑。using UnityEngine; public class Coin : MonoBehaviour { // 旋转速度让金币看起来在自转 public float rotateSpeed 100f; void Update() { // 每帧绕Y轴旋转产生动态效果 transform.Rotate(0, rotateSpeed * Time.deltaTime, 0); } // 当有其他碰撞体进入触发器时调用 void OnTriggerEnter(Collider other) { // 检查碰撞对象是否是玩家通过标签判断 if (other.CompareTag(Player)) { CollectCoin(); } } void CollectCoin() { // 通知游戏管理器增加分数 GameManager.Instance.AddScore(1); // 这里可以播放收集音效需要AudioSource组件 // AudioSource.PlayClipAtPoint(collectSound, transform.position); // 销毁金币对象 Destroy(gameObject); } }代码解析与实操要点旋转动画在Update中让金币持续旋转这是一个简单的视觉反馈让金币看起来更吸引人。Time.deltaTime确保了旋转速度与帧率无关在任何机器上都是一致的。碰撞检测我们使用了OnTriggerEnter而不是OnCollisionEnter。这两者的区别在于碰撞器Collider的设置。OnCollisionEnter需要两个物体都有非触发器的碰撞器且至少一个刚体模拟真实的物理碰撞会弹开。OnTriggerEnter则需要至少一个物体的碰撞器勾选了Is Trigger选项它只检测重叠不产生物理阻挡。对于金币这种被“吃掉”的物品用触发器更合适。操作选中场景中的Coin对象在检查器中找到它的Capsule Collider组件勾选Is Trigger。标签Tag的使用我们通过other.CompareTag(“Player”)来判断碰撞对象是不是玩家。这是一种高效且清晰的方式。你需要提前给Player对象设置Tag。操作在Unity编辑器顶部的菜单栏点击Edit - Project Settings - Tags and Layers。在Tags列表的空白处添加一个名为“Player”的标签。然后选中场景中的Player对象在检查器顶部的Tag下拉框中选择“Player”。与管理器通信金币被收集后需要通知游戏管理器加分。这里我们通过一个单例GameManager.Instance来访问。这是一种常用的跨对象通信模式。4.3 游戏管理器脚本 (GameManager.cs)这是游戏的大脑管理全局状态和UI。using UnityEngine; using UnityEngine.UI; // 引入UI命名空间 public class GameManager : MonoBehaviour { // 单例实例方便其他脚本访问 public static GameManager Instance { get; private set; } // UI文本组件用于显示分数 public Text scoreText; // 胜利提示面板 public GameObject winPanel; // 场景中总共的金币数量用于判断胜利 private int totalCoins; // 当前分数 private int currentScore 0; void Awake() { // 实现单例模式 if (Instance ! null Instance ! this) { Destroy(this.gameObject); } else { Instance this; } } void Start() { // 游戏开始时查找场景中所有Coin对象计算总数 totalCoins GameObject.FindGameObjectsWithTag(Coin).Length; // 初始化UI显示 UpdateScoreText(); // 确保胜利面板开始时是隐藏的 if (winPanel ! null) { winPanel.SetActive(false); } } // 增加分数的方法由Coin脚本调用 public void AddScore(int points) { currentScore points; UpdateScoreText(); CheckWinCondition(); } // 更新UI上的分数显示 void UpdateScoreText() { if (scoreText ! null) { scoreText.text Score: currentScore.ToString(); } } // 检查是否收集了所有金币 void CheckWinCondition() { if (currentScore totalCoins) { WinGame(); } } // 游戏胜利的逻辑 void WinGame() { Debug.Log(恭喜你收集了所有金币); // 显示胜利UI if (winPanel ! null) { winPanel.SetActive(true); } // 这里可以暂停游戏、播放胜利音乐等 // Time.timeScale 0f; } }代码解析与实操要点单例模式 (Singleton)public static GameManager Instance { get; private set; }这行代码创建了一个静态属性使得在游戏的任何脚本中都可以通过GameManager.Instance来访问这个唯一的游戏管理器。Awake方法中的逻辑确保了场景中只会存在一个GameManager实例。查找游戏对象GameObject.FindGameObjectsWithTag(“Coin”)在游戏开始时运行查找所有带有“Coin”标签的对象并统计数量。这意味着你需要在创建每个金币预制体时都为其设置“Coin”标签。这种方式比手动在编辑器里设置totalCoins变量更灵活你增减金币数量时代码会自动适应。UI系统集成我们使用了Unity的UGUI系统。你需要先在场景中创建UI。操作在层级视图右键 - UI - Canvas。这会创建一个画布和EventSystem。然后在Canvas下右键 - UI - Text创建一个Text对象重命名为“ScoreText”。调整它的位置比如左上角、字体大小和颜色。关联将场景中的ScoreText对象拖拽到GameManager脚本的scoreText公共变量槽中。胜利面板同样在Canvas下创建一个Panel重命名为“WinPanel”在里面放一个Text写着“You Win!”。在GameManager脚本中将这个WinPanel对象拖拽到winPanel变量槽中。5. 预制体制作与场景布置当我们的金币脚本写好并挂载到一个金币上后这个金币就已经是一个功能完整的可收集物了。但场景中需要很多个金币我们不可能一个个手动设置旋转、碰撞器和标签。这时就需要用到“预制体”。5.1 创建金币预制体在场景中确保你的那个样板Coin对象已经设置好模型正确、材质金色、Capsule Collider勾选了Is Trigger、挂载了Coin.cs脚本且rotateSpeed设置合理比如100并且它的Tag是“Coin”。在项目窗口找到一个合适的文件夹比如“Prefabs”右键 - Create - Folder命名为“Prefabs”。将场景中的这个Coin对象直接从层级视图拖拽到项目窗口的“Prefabs”文件夹里。你会看到它变成了一个蓝色的立方体图标这就是预制体。此时场景中的这个对象名字会变成蓝色表示它是预制体的一个实例。预制体的好处现在你可以从项目窗口把这个Coin预制体拖到场景中任意位置创建无数个副本。所有副本都自动拥有完全相同的组件、属性和脚本。如果你想修改所有金币的旋转速度只需要在项目窗口双击打开预制体进行编辑或者直接修改预制体资源所有场景中的实例都会同步更新除非你单独覆盖了某个实例的属性。5.2 高效布置场景有了预制体布置关卡就快多了。从项目窗口将Coin预制体多次拖入场景分散放置在Ground平台的各个位置。你可以利用Unity的移动W、旋转E、缩放R工具进行微调让布局看起来自然有趣有些放在平地有些可以放在小坡上需要你创建一些斜坡物体。为了增加挑战性可以创建一些障碍物。比如在层级视图右键 - 3D Object - Cube重命名为“Obstacle”缩放成各种形状摆放在平台上。为它们添加碰撞器Cube自带Box Collider但不要添加刚体除非你想要可移动的障碍。这样小球撞上去就会被挡住。布置完成后再次运行游戏。GameManager的Start方法会自动统计场景中所有Tag为“Coin”的对象数量作为totalCoins。控制小球去吃金币吧每吃一个左上角的分数应该会增加吃完所有金币后“You Win!”面板应该会弹出。6. 功能扩展与优化思路基础功能完成后这个项目就有了无穷的扩展可能。这里分享几个经典的扩展方向你可以选择一两个来实践能极大提升你的Unity技能。6.1 增加音效与粒子特效游戏离不开视听反馈。收集金币时的一个清脆音效和一个小型爆炸粒子能让体验提升好几个档次。音效准备一个“.wav”或“.mp3”格式的音效文件导入Unity。在Coin预制体上添加一个AudioSource组件。取消勾选Play On Awake。在Coin.cs脚本中增加一个公共变量public AudioClip collectSound;。在Unity编辑器里将导入的音效文件拖到Coin预制体上Coin脚本的collectSound变量槽中。修改CollectCoin方法在Destroy之前添加播放音效的代码// 方法一使用自带的AudioSource播放如果音效短且不需要3D效果 // GetComponentAudioSource().PlayOneShot(collectSound); // 方法二在当前位置创建一个临时的音频源播放更常用尤其适合3D音效 AudioSource.PlayClipAtPoint(collectSound, transform.position); Destroy(gameObject, 0.1f); // 延迟0.1秒销毁确保音效能播出来粒子特效在项目窗口右键 - Effects - Particle System创建一个粒子系统重命名为“CoinCollectEffect”。选中它在检查器里调整参数将Duration持续时间调短如0.5秒取消勾选Looping不循环。在Emission模块将Rate over Time调为0在Bursts里添加一个爆发粒子数设为10-20。在Shape模块将形状设为Sphere。调整Start Color为金色到黄色的渐变Start Size调小一些。将这个调好的粒子系统对象也做成一个预制体。在Coin.cs脚本中增加一个公共变量public GameObject collectEffectPrefab;并将粒子预制体拖拽赋值。在CollectCoin方法中在销毁金币前实例化这个粒子效果if (collectEffectPrefab ! null) { Instantiate(collectEffectPrefab, transform.position, Quaternion.identity); }6.2 实现关卡计时与难度递增增加时间限制和动态难度是让游戏变得“可玩”的关键。计时器在GameManager.cs中增加计时逻辑。public float timeLimit 60f; // 关卡时间限制秒 private float currentTime; public Text timeText; // 用于显示时间的UI Text void Start() { currentTime timeLimit; UpdateTimeText(); } void Update() { if (currentTime 0) { currentTime - Time.deltaTime; UpdateTimeText(); if (currentTime 0) { currentTime 0; GameOver(); // 时间到游戏结束 } } } void UpdateTimeText() { if (timeText ! null) { timeText.text Time: Mathf.Ceil(currentTime).ToString(); } } void GameOver() { // 显示游戏结束UI停止玩家控制等 Debug.Log(时间到游戏结束。); }动态难度你可以让金币旋转速度随时间加快或者每隔一段时间在随机位置生成一个新的障碍物。这需要在GameManager里用协程Coroutine或InvokeRepeating来实现。例如在Start方法中启动一个协程每10秒执行一次IncreaseDifficulty函数该函数可以查找所有金币并增加它们的rotateSpeed。6.3 构建简单关卡与场景切换一个完整的游戏通常有多个关卡。我们可以为每个关卡创建一个独立的Unity场景。保存当前场景File - Save As将你的第一个关卡保存为“Level1”。创建新场景File - New Scene然后搭建一个新的平台用不同的布局摆放金币和障碍物保存为“Level2”。场景切换逻辑在GameManager的WinGame方法中当玩家获胜后可以等待几秒然后加载下一关。public string nextLevelName; // 在编辑器里填入下一关的场景名如Level2 void WinGame() { // ... 显示胜利UI ... Invoke(LoadNextLevel, 2f); // 2秒后加载下一关 } void LoadNextLevel() { if (!string.IsNullOrEmpty(nextLevelName)) { UnityEngine.SceneManagement.SceneManager.LoadScene(nextLevelName); } else { Debug.Log(已是最后一关); } }场景管理你需要确保游戏管理器在关卡切换时不被销毁以便保持分数等数据。这可以通过DontDestroyOnLoad实现但更常见的做法是每个关卡都有自己的GameManager或者使用一个独立的、常驻的“持久化”对象来存储需要跨关卡的数据。7. 常见问题与调试技巧实录开发过程中你几乎一定会遇到下面这些问题。别担心这都是学习的一部分。7.1 小球控制手感“太滑”或“不跟手”问题描述小球像在冰面上一样停不下来或者响应输入有延迟。排查与解决检查刚体参数选中Player小球查看Rigidbody组件。增加Drag阻力和Angular Drag角阻力可以有效地让小球更快停下来。Drag影响直线运动Angular Drag影响旋转。尝试将Drag设为1-3Angular Drag设为0.5-1。检查控制脚本确认你是在FixedUpdate中修改速度或施加力。在Update中做这些事会导致物理更新不稳定。尝试不同的控制方式如果你用的是rb.velocity可以试试AddForce。AddForce需要调整力的大小force参数和力的模式ForceMode。ForceMode.Force是持续的力ForceMode.Impulse是瞬间的冲量。通常用ForceMode.Force并配合一个适中的力值如500手感会更模拟真实。限制最大速度有时小球会因为持续受力而越来越快。可以在FixedUpdate中限制刚体的水平速度。Vector3 horizontalVel new Vector3(rb.velocity.x, 0, rb.velocity.z); if (horizontalVel.magnitude maxSpeed) { horizontalVel horizontalVel.normalized * maxSpeed; rb.velocity new Vector3(horizontalVel.x, rb.velocity.y, horizontalVel.z); }7.2 金币碰撞检测失败问题描述小球穿过了金币但没有触发加分和销毁。排查与解决确认触发器确保金币的碰撞器组件上Is Trigger选项被勾选。这是OnTriggerEnter生效的前提。确认标签确保Player对象的Tag是“Player”并且Coin脚本里OnTriggerEnter方法中判断的标签字符串拼写完全一致区分大小写。确认刚体参与触发器检测的两个物体至少有一个需要有刚体。我们的Player有刚体这没问题。金币的刚体勾选了Is Kinematic这也符合要求。使用Debug.Log调试在OnTriggerEnter方法开头添加Debug.Log(“触发了: ” other.name);。运行游戏让小球去碰金币看看控制台有没有输出。如果有输出但没执行CollectCoin说明标签判断没通过如果根本没输出说明触发器没生效回头检查1和3。7.3 游戏管理器单例报空引用NullReferenceException问题描述运行游戏时控制台报错“NullReferenceException: Object reference not set to an instance of an object”指向GameManager.Instance。排查与解决场景中是否有GameManager对象这是最常见的原因。你必须在场景中创建一个空游戏对象右键 - Create Empty重命名为“GameManager”然后将GameManager.cs脚本挂载上去。脚本执行顺序Awake方法在所有Start方法之前调用。确保你的GameManager对象在场景加载时就存在。如果它是动态实例化的需要在实例化后立刻初始化单例。多个GameManager实例如果你的场景中不小心放了多个带有GameManager脚本的对象Awake里的销毁逻辑可能会把“正确”的那个也销毁掉。检查层级视图确保只有一个。7.4 UI文本不更新或找不到问题描述分数文本一直是0或者控制台报错找不到scoreText。排查与解决公共变量赋值在Unity编辑器中选中GameManager对象在检查器里找到GameManager (Script)组件。你会看到Score Text和Win Panel两个变量槽是空的。你必须把场景中对应的UI对象ScoreText和WinPanel分别拖拽到这两个槽里完成赋值。这是新手最容易忘记的一步。Canvas渲染模式确保你的Canvas的渲染模式是“Screen Space - Overlay”或“Screen Space - Camera”并正确指定了摄像机。如果是“World Space”UI可能会出现在奇怪的位置甚至看不到。Text组件引用确认你拖拽给scoreText的对象上面确实有Text组件。有时不小心拖成了它的父对象Panel。这个项目虽然基础但它像一颗种子包含了游戏开发最核心的基因。当你成功实现它之后那些看似复杂的游戏机制——比如角色技能、敌人AI、任务系统、存档读档——你会发现它们都是在这个基础框架上通过增加更多的对象、更复杂的脚本和更精巧的交互搭建起来的。我建议你在完成基础版本后一定要尝试至少一个扩展功能比如加上音效和粒子或者做一个计时挑战模式。这个过程里遇到的每一个错误和解决的每一个问题都会让你对Unity引擎的理解加深一分。