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下方便管理。这里有一个小技巧在移动和旋转物体时按住CtrlWindows或CmdMac可以启用网格吸附让对齐更精准。现在创建我们的主角——小球。在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 GetComponentRigidbody(); // 安全检查 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(); // 播放粒子效果如果有 // GetComponentParticleSystem()?.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.csusing 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.csusing 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.AddComponentAudioSource(); audioSource.playOnAwake false; } public void PlayCoinSound() { if (coinCollectSound ! null) { audioSource.PlayOneShot(coinCollectSound); } } }将音效文件拖拽到coinCollectSound变量上。然后在Coin.cs的CollectCoin方法中调用AudioManager.Instance.PlayCoinSound()。对于粒子效果你可以为金币预制体添加一个Particle System组件设计一个简单的金色粒子爆发效果。在Coin.cs的CollectCoin方法中通过GetComponentParticleSystem()?.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就会开始编译打包。完成后你会在该文件夹下得到一个.exeWindows或.appMac文件以及相关的数据文件夹。将它们一起发送给朋友他们就可以在没有安装Unity的环境下运行你的游戏了。走到这一步你已经完成了一个完整的、可玩的Unity小游戏。这个项目虽然基础但它像一块坚实的基石上面可以搭建出无数复杂的游戏大厦。理解这里的每一个环节——从物体、组件、脚本的交互到物理、UI、状态的管理——是通往更高级游戏开发的必经之路。下次你可以尝试给金币加上不同的分数设计多关卡甚至加入敌人和更复杂的玩法这个“小球吃金币”的框架将是你最好的起点。