Unity新手入门:从零构建3D小球吃金币游戏完整教程
1. 从零到一:一个经典小游戏的完整构建思路
如果你刚接触Unity,或者想找一个能串联起Unity核心功能点的练手项目,“小球吃金币”绝对是个绝佳的选择。它麻雀虽小,五脏俱全,涵盖了3D场景搭建、物理系统应用、玩家控制、碰撞检测、UI交互、数据管理以及游戏状态控制等几乎所有基础游戏开发模块。我见过不少开发者,包括我自己带过的实习生,都是从这样一个看似简单的项目里,第一次真正理解了游戏循环、组件化开发和面向数据设计这些核心概念。
这个项目的目标非常明确:在一个3D场景中,控制一个球体移动,去触碰(吃掉)场景中散落的金币,每吃到一个金币,屏幕上的分数就增加,当吃掉所有金币后,游戏胜利。听起来简单吧?但要把这个“简单”的想法变成一个运行流畅、体验完整的游戏,里面每一步都有值得深究的细节。比如,如何让小球滚动起来手感更真实?金币被吃掉时除了消失,能不能加点特效和音效?游戏界面怎么布局才清晰?关卡怎么设计才有挑战性?这些问题,就是我们接下来要一步步拆解和实现的。
我会带你从创建一个空项目开始,到最终打包出一个可以分享给朋友玩的完整游戏。过程中,我不会只告诉你“点这里,拖那里”,更重要的是解释“为什么这么做”,以及“如果不这么做可能会遇到什么坑”。毕竟,复制粘贴代码谁都会,但理解背后的原理,才能让你举一反三,做出属于自己的游戏。
2. 项目初始化与核心场景搭建
万事开头难,但一个好的开始能让后续开发事半功倍。我们首先需要创建一个纯净的3D项目,并进行合理的资源管理和场景规划。
2.1 创建项目与基础设置
打开Unity Hub,点击“新建项目”,选择核心模板为“3D (URP)”。这里我推荐使用URP(通用渲染管线)而不是内置渲染管线,原因有三点:首先,URP是Unity现在主推的现代化、可扩展的渲染方案,性能更好,更适合移动端和未来的项目扩展;其次,URP内置了更好的后期处理效果,我们后面如果想给游戏加个简单的泛光或者调色会非常方便;最后,学习URP也是跟上Unity技术发展的趋势。当然,如果你使用的是较旧的Unity版本(如2019.4 LTS),使用内置渲染管线也完全没问题,核心逻辑是相通的。
项目创建好后,第一件事是整理项目文件夹结构。一个清晰的结构是专业开发的标志,也能极大提升协作和后期维护的效率。在Assets目录下,我通常会创建以下文件夹:
_Scripts: 存放所有C#脚本。前面的下划线让它能排在前面,方便查找。Scenes: 存放游戏场景文件。Prefabs: 存放预制体,比如我们的金币、玩家小球。Materials: 存放材质球。Textures: 存放贴图。Audio: 存放音效和背景音乐。Fonts: 存放字体文件。Settings: 存放一些配置性的资产,如Input System的输入动作资源。
接下来,保存当前场景,命名为MainScene,放入Scenes文件夹。然后,我们开始搭建游戏世界的基础。
2.2 构建基础游戏环境:地面、墙壁与玩家
我们的游戏需要一个封闭的场地。在Hierarchy面板右键 -> 3D Object -> Plane,创建一个平面作为地面。将其重命名为Ground,并适当缩放(例如Scale设为 (10, 1, 10)),让它足够大。接着,创建一些Cube,通过缩放和移动,将它们摆放在平面边缘,作为围墙,防止小球掉出世界。将这些Cube组合成一个空物体Walls下,方便管理。这里有一个小技巧:在移动和旋转物体时,按住Ctrl(Windows)或Cmd(Mac)可以启用网格吸附,让对齐更精准。
现在,创建我们的主角——小球。在Hierarchy面板右键 -> 3D Object -> Sphere,重命名为Player。将其Y轴位置(Position Y)设置为0.5左右,让它刚好“站”在地面上,而不是嵌进去。为了让小球能滚动,我们必须为它添加两个核心组件:Rigidbody(刚体)和Collider(碰撞体)。
选中Player,在Inspector面板点击“Add Component”,搜索并添加Rigidbody。刚体组件赋予了物体物理属性,使其受到重力、可以与其他刚体发生碰撞。这里有几个关键参数需要调整:
- Mass(质量): 设为1。质量会影响惯性,如果质量太大,小球会显得很“笨重”,启动和停止都慢;太小则容易“飘”。1是一个比较中性的值,适合作为起点。
- Drag(阻力): 设为0.5。空气阻力,这个值会让小球在失去动力后慢慢停下来,而不是永远匀速滑动,手感更真实。
- Angular Drag(角阻力): 设为0.05。旋转阻力,控制小球滚动的“顺滑”程度。值太小会转个不停,太大则滚动不自然。
- Constraints(约束): 冻结Rotation的X和Z轴。这是一个非常重要的技巧!如果不冻结,小球在碰撞墙壁时可能会发生不可预测的翻滚,导致控制失灵。我们只希望它围绕自身的Y轴旋转(这由移动方向自然产生,我们后面通过脚本来控制视觉效果),所以冻结X和Z旋转能极大提升控制的稳定性和可预测性。
小球自带的Sphere Collider已经足够,我们暂时不需要修改它。至此,一个具有物理属性的小球就准备好了。
3. 实现玩家控制:让小球“活”起来
一个游戏的核心体验之一就是操控感。我们将通过两种主流方式来实现小球控制:经典的Transform直接操控和更符合物理直觉的AddForce方式。
3.1 创建玩家控制脚本
在_Scripts文件夹下右键 -> Create -> C# Script,命名为PlayerController。双击在IDE(如Visual Studio)中打开,编写以下代码:
using UnityEngine; public class PlayerController : MonoBehaviour { // 移动速度 public float moveSpeed = 10f; // 旋转速度(让小球看起来在滚动) public float rollSpeed = 100f; // 控制方式选择 public enum ControlMode { TransformBased, ForceBased } public ControlMode controlMode = ControlMode.ForceBased; // 引用刚体组件(用于ForceBased模式) private Rigidbody rb; // 用于存储输入 private float horizontalInput; private float verticalInput; void Start() { // 获取刚体组件引用 rb = GetComponent<Rigidbody>(); // 安全检查 if (rb == null) { Debug.LogError("PlayerController: Rigidbody component not found on " + gameObject.name); } } void Update() { // 在Update中获取输入,保证响应及时 horizontalInput = Input.GetAxis("Horizontal"); verticalInput = Input.GetAxis("Vertical"); } void FixedUpdate() { // 在FixedUpdate中执行物理相关的移动,保证与物理引擎步调一致 MovePlayer(); } void MovePlayer() { // 构建移动方向向量(在XZ平面) Vector3 movement = new Vector3(horizontalInput, 0f, verticalInput).normalized; // 根据选择的模式进行移动 switch (controlMode) { case ControlMode.TransformBased: MoveUsingTransform(movement); break; case ControlMode.ForceBased: MoveUsingForce(movement); break; } // 无论哪种移动方式,都让小球视觉上“滚动”起来 VisualRoll(movement); } void MoveUsingTransform(Vector3 direction) { // 直接修改位置,无视物理。手感轻飘,但响应极快。 transform.Translate(direction * moveSpeed * Time.deltaTime, Space.World); } void MoveUsingForce(Vector3 direction) { // 使用物理力来推动小球,手感更真实、有惯性。 if (rb != null) { // 计算目标速度 Vector3 targetVelocity = direction * moveSpeed; // 计算需要施加的力(忽略Y轴速度变化) Vector3 velocityChange = new Vector3(targetVelocity.x - rb.velocity.x, 0, targetVelocity.z - rb.velocity.z); // 施加力,ForceMode.VelocityChange会直接改变速度,忽略质量,控制感更直接。 rb.AddForce(velocityChange, ForceMode.VelocityChange); } } void VisualRoll(Vector3 direction) { // 如果小球有移动输入,则让它沿着移动方向旋转,模拟滚动效果 if (direction.magnitude > 0.1f) { // 计算旋转轴:移动方向向右旋转90度(绕Y轴),得到垂直于移动方向且向上的轴 Vector3 rollAxis = Vector3.Cross(Vector3.up, direction).normalized; // 绕该轴旋转 transform.Rotate(rollAxis, rollSpeed * Time.deltaTime, Space.World); } } }将脚本拖拽到Hierarchy中的Player物体上。运行游戏,你现在应该可以使用键盘的WASD或方向键来控制小球移动了。通过Inspector面板中的Control Mode,你可以切换两种模式感受其区别。我强烈推荐使用ForceBased模式,因为它与物理系统结合得更好,手感更扎实,也更容易做出加速、减速等效果。
3.2 输入系统的优化与新输入系统简介
上面的代码使用了传统的Input.GetAxis,它简单易用。但在更复杂的项目中,你可能需要处理手柄、重新绑定按键等。Unity的新输入系统(Input System Package)是更好的选择。它基于事件驱动,支持多种输入设备,配置更灵活。
要使用它,需要通过Package Manager安装Input System。安装后,可以创建一个Player Input组件,并关联一个.inputactions资源文件,在里面定义“Move”等动作。然后在脚本中,你可以通过PlayerInput组件或直接订阅事件来获取输入,代码会更清晰,与设备无关。对于这个小项目,传统输入已足够,但了解这个方向对你未来项目很有帮助。
4. 金币系统的设计与实现:收集与反馈
金币是这个游戏的核心收集物。我们需要创建金币预制体,并实现“吃”的逻辑和视听反馈。
4.1 创建金币预制体与碰撞检测
首先,创建一个3D物体作为金币,比如一个Cylinder(圆柱体),将其压扁(Scale Y设为0.2)并赋予一个金色的材质(在Materials文件夹创建新材质,Albedo颜色设为金黄色)。重命名为Coin。
接下来,为金币添加一个Collider。圆柱体自带的Capsule Collider或Box Collider都可以,确保Is Trigger复选框被勾选。“触发器(Trigger)”是关键。当勾选Is Trigger后,这个碰撞体不会产生物理阻挡(小球可以穿过去),但会发送碰撞事件,这正是我们需要的“收集”行为。
然后,创建一个C#脚本Coin.cs,并挂载到金币上。
using UnityEngine; public class Coin : MonoBehaviour { // 金币被吃掉时的旋转速度(让它看起来更活泼) public float rotateSpeed = 180f; // 金币被收集时上浮的速度 public float floatSpeed = 2f; // 金币被收集时销毁的延迟时间(留出播放效果的时间) public float destroyDelay = 0.5f; // 是否已被收集 private bool isCollected = false; // 收集时的初始位置 private Vector3 startPosition; void Start() { startPosition = transform.position; } void Update() { // 让金币持续缓慢旋转,吸引玩家注意 transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime); // 如果已被收集,执行上浮消失的动画 if (isCollected) { transform.Translate(Vector3.up * floatSpeed * Time.deltaTime, Space.World); } } // 触发器事件:当其他带有Collider的物体进入时调用 void OnTriggerEnter(Collider other) { // 检查进入触发器的是否是玩家 if (other.CompareTag("Player") && !isCollected) { CollectCoin(); } } void CollectCoin() { isCollected = true; // 通知游戏管理器(或分数管理器)增加分数 // 这里我们先直接调用一个静态方法,后面会优化 GameManager.Instance?.AddScore(1); // 播放音效(如果有) AudioManager.Instance?.PlayCoinSound(); // 播放粒子效果(如果有) // GetComponent<ParticleSystem>()?.Play(); // 延迟销毁物体 Destroy(gameObject, destroyDelay); } }注意,脚本中引用了GameManager.Instance和AudioManager.Instance,这是我们接下来要创建的单例管理器。现在,为了让OnTriggerEnter中的判断生效,我们需要给玩家小球Player打上一个Tag(标签)。在Inspector面板顶部,将Player的Tag设置为“Player”(如果没有,点击Add Tag...先创建)。
最后,将设置好的Coin物体从Hierarchy拖入Prefabs文件夹,生成预制体。然后可以在场景中复制多个,随意摆放。删除Hierarchy中的原始金币实例,后续都使用预制体实例化。
4.2 游戏状态与分数管理:单例模式的应用
我们需要一个全局的管理器来跟踪分数、金币总数和游戏状态(进行中/胜利)。使用单例模式(Singleton)是管理这种全局访问点的常用方法。
创建脚本GameManager.cs:
using UnityEngine; using UnityEngine.UI; // 引入UI命名空间 public class GameManager : MonoBehaviour { // 单例实例 public static GameManager Instance { get; private set; } // 当前分数 private int currentScore = 0; // 场景中总金币数(在游戏开始时计算) private int totalCoins = 0; // 游戏是否结束 private bool isGameOver = false; // UI文本引用(在Inspector中赋值) public Text scoreText; public Text gameOverText; // 用于显示“胜利!” void Awake() { // 单例初始化 if (Instance == null) { Instance = this; // DontDestroyOnLoad(gameObject); // 如果需要跨场景,可以取消注释 } else { Destroy(gameObject); } } void Start() { // 游戏开始时,查找场景中所有金币并计数 totalCoins = GameObject.FindGameObjectsWithTag("Coin").Length; // 记得给金币预制体也加上“Coin”标签 UpdateScoreUI(); // 隐藏胜利文本 if (gameOverText != null) gameOverText.gameObject.SetActive(false); } // 增加分数,由金币脚本调用 public void AddScore(int points) { if (isGameOver) return; // 游戏结束后不再计分 currentScore += points; UpdateScoreUI(); // 检查是否收集了所有金币 if (currentScore >= totalCoins) { GameWin(); } } void UpdateScoreUI() { if (scoreText != null) { scoreText.text = "Score: " + currentScore + " / " + totalCoins; } } void GameWin() { isGameOver = true; Debug.Log("恭喜!你收集了所有金币!"); // 显示胜利UI if (gameOverText != null) { gameOverText.text = "Victory!"; gameOverText.gameObject.SetActive(true); } // 可以在这里停止玩家输入、播放胜利音效等 // Time.timeScale = 0f; // 暂停游戏(谨慎使用,会停止所有Update和物理模拟) } // 一个公共方法,用于重新开始游戏(可以绑定到UI按钮) public void RestartGame() { // 简单重载当前场景 UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex); } }在场景中创建一个空物体,命名为GameManager,挂载此脚本。然后,我们需要创建UI来显示分数。
5. 用户界面(UI)与视听反馈
一个好的游戏离不开清晰的UI和及时的反馈。我们将创建一个简单的UI来显示分数和胜利信息。
5.1 创建Canvas与分数显示
在Hierarchy面板右键 -> UI -> Canvas。Canvas是所有UI元素的画布。为了适配不同屏幕,将其Canvas Scaler组件的UI Scale Mode设置为Scale With Screen Size,参考分辨率设为1920x1080。
在Canvas下右键 -> UI -> Text,创建一个Text物体,重命名为ScoreText。将其锚点(Anchor)设置为左上角(Top-Left),并调整位置和大小。在Inspector中,可以设置字体、字号、颜色(如白色)。将GameManager脚本中公开的scoreText变量,拖拽赋值为此Text物体。
同理,创建另一个Text物体,命名为GameOverText,将其放置在屏幕中央,初始文本设为“Victory!”,字体可以设置得大一些、颜色鲜艳一些(如金色)。同样,将其赋值给GameManager的gameOverText变量,并确保它在Start时是隐藏的(SetActive(false))。
5.2 添加音效与简单粒子效果
视听反馈能极大提升游戏体验。我们为金币收集添加一个“叮”的音效。
在Audio文件夹中放入一个简短的收集音效文件(.wav或.mp3)。创建一个空物体AudioManager,挂载脚本AudioManager.cs:
using UnityEngine; public class AudioManager : MonoBehaviour { public static AudioManager Instance { get; private set; } public AudioClip coinCollectSound; private AudioSource audioSource; void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } audioSource = gameObject.AddComponent<AudioSource>(); audioSource.playOnAwake = false; } public void PlayCoinSound() { if (coinCollectSound != null) { audioSource.PlayOneShot(coinCollectSound); } } }将音效文件拖拽到coinCollectSound变量上。然后在Coin.cs的CollectCoin方法中,调用AudioManager.Instance.PlayCoinSound()。
对于粒子效果,你可以为金币预制体添加一个Particle System组件,设计一个简单的金色粒子爆发效果。在Coin.cs的CollectCoin方法中,通过GetComponent<ParticleSystem>()?.Play()来播放它。记得将粒子系统的Play On Awake关闭,并通过脚本控制播放。
6. 关卡设计、优化与打包发布
基础功能完成后,我们可以考虑如何让游戏变得更有趣,以及如何优化性能并最终分享出去。
6.1 设计更有挑战性的关卡
单纯在地面上放金币可能有些单调。我们可以利用Unity的物理和场景物件设计一些简单挑战:
- 移动平台: 创建一些来回移动的平台(使用
Animation或脚本控制Transform位置),玩家需要看准时机跳上去获取金币。 - 陷阱与障碍: 创建一些带有伤害的物体(如红色区域),玩家碰到后会重置位置或扣分。这需要为陷阱添加一个脚本,在
OnCollisionEnter(非Trigger)中检测玩家并触发惩罚逻辑。 - 跳跃机制: 为小球添加跳跃能力。在
PlayerController脚本的Update方法中检测空格键按下,然后在FixedUpdate中给刚体施加一个向上的力(rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse))。注意要处理空中连跳的问题,通常通过射线检测小球是否着地。 - 计时挑战: 在
GameManager中加入计时器,要求玩家在规定时间内收集所有金币。
6.2 性能优化与小技巧
- 预制体与实例化: 我们已经使用了金币预制体。如果关卡中有大量相同物体,一定要用预制体,这是Unity优化Draw Call(绘制调用)的基础。
- 静态批处理(Static Batching): 对于场景中永远不会移动的物体(如地面、静态墙壁),在Inspector右上角勾选
Static。Unity会在构建时将它们合并,大幅减少绘制调用。 - 相机跟随: 创建一个脚本让相机平滑跟随玩家。常用方法是
Vector3.SmoothDamp。 - 输入处理: 将所有输入逻辑放在
Update中,但物理移动放在FixedUpdate中,这是标准做法。 - 调试: 善用
Debug.Log和Debug.DrawRay来调试逻辑和物理射线。
6.3 游戏打包与发布
最后,让我们把游戏打包成可执行文件。点击菜单栏 File -> Build Settings。将当前场景MainScene拖入Scenes In Build列表。
在Platform中选择目标平台,比如PC, Mac & Linux Standalone。点击Player Settings,可以设置公司名、产品名、图标、分辨率等。
设置完毕后,点击Build,选择一个输出文件夹,Unity就会开始编译打包。完成后,你会在该文件夹下得到一个.exe(Windows)或.app(Mac)文件以及相关的数据文件夹。将它们一起发送给朋友,他们就可以在没有安装Unity的环境下运行你的游戏了。
走到这一步,你已经完成了一个完整的、可玩的Unity小游戏。这个项目虽然基础,但它像一块坚实的基石,上面可以搭建出无数复杂的游戏大厦。理解这里的每一个环节——从物体、组件、脚本的交互,到物理、UI、状态的管理——是通往更高级游戏开发的必经之路。下次,你可以尝试给金币加上不同的分数,设计多关卡,甚至加入敌人和更复杂的玩法,这个“小球吃金币”的框架,将是你最好的起点。
