说实话,当我第一次看到“从零开始做游戏”这个命题时,脑子里全是Unity那些密密麻麻的菜单栏:Hierarchy、Inspector、Project、Console……对于一个完全不懂C#、甚至有点怕代码的人来说,这感觉就像让你直接驾驶一架波音747上太空。但别慌,我当年也是这么过来的,而且后来我发现,游戏开发其实是一场“逻辑与美学”的拼图游戏,只要你理清了步骤,那些看似高深的技术名词,其实都可以拆解成你可以掌控的小模块。
这篇教程不是那种干巴巴的官方手册,而是我把这几年踩过的坑、掉过的头发,甚至是我给小朋友讲清楚“游戏是怎么动起来的”那种大白话逻辑,全部揉碎了喂给你。咱们不玩虚的,直接上手。
第一阶段:别急着下载引擎,先搞懂“游戏到底是什么”
很多人上来就装Unity,然后发现连按钮都点不动,最后骂一句“Unity太难了”卸载掉。这完全是误解。
1.1 游戏的核心循环(Game Loop)
你给小朋友讲游戏的时候,可以这么比喻:游戏就像是一个不断重复的“眨眼”动作。
每一帧(Frame),游戏都在做这三件事:
- 输入(Input):你在干什么?按了空格?碰了对手?还是屏幕滑动?
- 更新(Update):根据输入,调整数据。比如,你按了空格,角色的“跳跃速度”变了;时间过了0.1秒,重力让角色位置往下移了一点。
- 渲染(Render):把现在的画面画在屏幕上。
在Unity里,你其实就是在写这个“眨眼”里的“更新”部分。当你看到 Update() 函数时,别紧张,它就是每帧执行一次的“大脑思考”过程。
1.2 你的第一个Hello World
咱们别一上来就搞什么3D射击游戏,那会把人吓跑。我们来做个最简单的:屏幕中间有个按钮,点击它,弹出一个“Hi!”。
打开Unity Hub,安装最新的2022 LTS或2023 LTS版本(2024年建议用2022.3 LTS或更高,稳定第一)。新建一个2D Core项目。
第一步:创建按钮 在Hierarchy窗口右键 -> UI -> Button。 然后你会看到,Button下面有个Text子对象。选中Text,在Inspector里把字体改大,把颜色改亮,把文字改成“开始游戏”。
第二步:写代码 在Project窗口右键 -> Create -> C# Script,命名为“GameManager”。 双击打开,你会看到一段代码:
using UnityEngine;
using UnityEngine.UI; // 注意:操作UI需要这个命名空间
public class GameManager : MonoBehaviour
{
public Text scoreText; // 声明一个公开变量,用来绑定UI上的Text
// Start is called before the first frame update
void Start()
{
scoreText = GetComponent<Text>(); // 获取当前游戏对象上的Text组件
scoreText.text = "游戏开始!"; // 直接修改文字
Debug.Log("游戏初始化完成"); // 在Console里打印日志,这是你调试的好帮手
}
// Update is called once per frame
void Update()
{
// 这里放每帧都要做的事,比如检查输入
if (Input.GetKeyDown(KeyCode.Space))
{
scoreText.text = "你按了空格键!";
}
}
// 这是一个按钮点击触发的函数
public void OnButtonClicked()
{
scoreText.text = "你点击了按钮!";
// 这里可以写更复杂的逻辑,比如播放音效、切换场景
}
}
第三步:绑定
把GameManager脚本拖到Hierarchy里的“Main Camera”上(或者新建一个空物体“GameManager”)。
然后,把Hierarchy里的Button对象,拖到Inspector里GameManager组件的“On Click()”事件框里,选择GameManager > OnButtonClicked。
看,这就是游戏开发的本质:绑定事件 -> 触发逻辑 -> 改变状态。就这么简单。
第二阶段:核心机制——让角色动起来
现在,我们要做一个真正的游戏角色了。假设我们做一个简单的“躲避障碍”游戏,玩家控制一个小方块,躲避掉下来的苹果。
2.1 刚体与碰撞器:物理世界的基石
Unity里,物体不动是因为它没有力作用在它上面。要让物体动,我们需要Rigidbody(刚体)和Collider(碰撞器)。
- Collider:给物体一个“形状边界”。没有碰撞器,物体之间会穿过彼此,像幽灵一样。
- Rigidbody:给物体“物理属性”,比如重力、质量、速度。
操作指南:
- 创建一个Cube作为玩家,命名为“Player”。
- 给Player添加组件:
Add Component->Physics 2D->Rigidbody 2D和Box Collider 2D。 - 在Inspector里,把
Body Type改成Kinematic(如果是纯靠代码移动,这样性能更好);或者改成Dynamic,让它受重力影响(如果是下落游戏)。
2.2 脚本控制移动
这是新手最容易卡住的地方:不要用 Transform.position 直接移动,那样会穿过障碍物。要用Rigidbody2D的MovePosition或者AddForce。
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 5f; // 公开变量,可以在Inspector里调整
private Rigidbody2D rb;
void Start()
{
rb = GetComponent<Rigidbody2D>();
}
void Update()
{
// 获取水平输入(A/D键 或 左/右箭头)
float moveInput = Input.GetAxis("Horizontal");
// 计算移动目标位置
Vector3 newPosition = transform.position;
newPosition.x += moveInput * moveSpeed * Time.deltaTime;
// 边界限制,防止飞出屏幕
newPosition.x = Mathf.Clamp(newPosition.x, -4f, 4f);
// 使用MovePosition平滑移动,而不是直接设置position
rb.MovePosition(newPosition);
}
}
这里有个关键点:Time.deltaTime。
很多新手会问:“为什么不直接用 newPosition.x += moveInput * moveSpeed?”
因为不同电脑性能不同,有的60帧每秒,有的144帧每秒。Time.deltaTime是上一帧到这一帧的时间间隔(秒)。乘以它,就能保证不管帧率多少,角色移动的速度都是一样快的。这是游戏开发里最重要的“时间无关性”概念。
2.3 障碍物生成器
游戏不能一直只有玩家一个人在动,得有东西来挑战他。我们写一个“Spawner(生成器)”。
using UnityEngine;
public class ObstacleSpawner : MonoBehaviour
{
public GameObject obstaclePrefab; // 拖入你的苹果预制体
public float spawnRate = 1.5f; // 每1.5秒生成一个
private float nextSpawnTime = 0f;
public float moveSpeed = 3f;
void Update()
{
if (Time.time >= nextSpawnTime)
{
SpawnObstacle();
nextSpawnTime = Time.time + spawnRate;
}
}
void SpawnObstacle()
{
// 在屏幕顶部随机X位置生成
float randomX = Random.Range(-4f, 4f);
Vector3 spawnPosition = new Vector3(randomX, 6f, 0f);
Instantiate(obstaclePrefab, spawnPosition, Quaternion.identity);
}
}
然后把这段脚本拖到场景里一个空物体上,把做好的苹果Prefab拖到Obstacle Prefab槽位里。
第三阶段:让游戏“活”起来——UI与场景管理
游戏跑起来了,但还没法结束,也没法显示分数。这时候需要UI和Scene Management。
3.1 UI系统:Canvas是画布
Unity的UI都在Canvas里。你可以想象Canvas是一张透明的玻璃板,贴在摄像机前面。所有的按钮、文字、血条都要放在Canvas的子节点下。
动态更新分数:
我们在PlayerController里增加一个逻辑:当障碍物碰到玩家,游戏结束。同时,我们需要一个GameManager来管理分数。
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
public static GameManager Instance; // 单例模式,让其他脚本能找到这个GameManager
public Text scoreText;
public GameObject gameOverPanel; // 拖入Game Over的UI面板
private int score = 0;
private bool isGameOver = false;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
void Start()
{
gameOverPanel.SetActive(false); // 开始时隐藏Game Over界面
}
public void AddScore()
{
if (isGameOver) return;
score++;
scoreText.text = "Score: " + score;
}
public void EndGame()
{
isGameOver = true;
gameOverPanel.SetActive(true); // 显示Game Over界面
}
public void RestartGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name); // 重新加载当前场景
}
}
注意单例模式(Singleton):
public static GameManager Instance; 这行代码非常有用。当你写其他脚本时,可以直接用GameManager.Instance.AddScore(),而不需要到处找引用。这是中大型项目必备的技巧。
3.2 碰撞检测: OnTriggerEnter2D
回到你的苹果预制体,确保它有Is Trigger勾选(这样它不会物理阻挡玩家,只会触发事件)。然后写一个脚本挂在苹果上:
using UnityEngine;
public class Obstacle : MonoBehaviour
{
private bool scored = false;
void Update()
{
// 检查是否超出屏幕底部
if (transform.position.y < -6f)
{
Destroy(gameObject); // 销毁超过屏幕的苹果
}
}
void OnTriggerEnter2D(Collider2D other)
{
// 如果碰到玩家
if (other.CompareTag("Player"))
{
GameManager.Instance.EndGame(); // 调用游戏结束逻辑
}
// 如果玩家已经通过了(假设玩家有触发器穿过苹果得分)
else if (other.CompareTag("Player") && !scored)
{
// 这里可以设计成穿过苹果得分,而不是碰撞
}
}
}
第四阶段:2024年最常见的崩溃类型与排查技巧
你辛辛苦苦做完的游戏,一跑就崩?别慌,这是每个开发者的必经之路。2024年,Unity常见的崩溃主要分为这几类:
4.1 NullReferenceException:空引用异常
这是90%新手的噩梦。简单说,就是你试图用一个“不存在”的东西。
典型场景:
public class BadExample : MonoBehaviour
{
public GameObject player; // 忘记在Inspector里拖入值
void Start()
{
// 这里会崩!因为player是null
player.transform.position = new Vector3(0, 0, 0);
}
}
排查技巧:
- 看Console里的报错,它会告诉你哪一行代码崩了。
- 在崩溃行前面加
Debug.Log("Player is: " + player);,你会发现输出是“Player is: ”(空的)。 - 解决方法:确保在Inspector里绑定了对象,或者在代码里初始化:
player = GameObject.FindGameObjectWithTag("Player");
4.2 内存溢出(OOM):OutOfMemoryException
常见于加载大量高清贴图或音频。
症状: 游戏运行几分钟后突然闪退,Console里可能有OutOfMemoryException,也可能什么都没说直接退出了。
排查技巧:
- 使用Unity的Memory Profiler包(Window -> Analysis -> Memory Profiler)。
- 观察“Heap Allocations”曲线,如果一直在飙升,说明你有内存泄漏。
- 常见原因:
- 在
Update里频繁new对象(如new Vector3()在某些极端循环中)。 - 加载了未压缩的大贴图。
- 没有及时
Destroy或SetActive(false)不用的对象。
- 在
解决方案: 使用对象池(Object Pooling)。不要Instantiate和Destroy苹果,而是创建一个池子,用完后隐藏,用时再拿出来。
4.3 主线程阻塞
如果你在Update或OnTriggerEnter里写了耗时的操作(比如复杂的网络请求、文件读写),游戏会卡死。
排查技巧: 使用Frame Debugger(Window -> Analysis -> Frame Debugger)。看每一帧的耗时分布。如果某个操作占了超过16ms(60帧的限制),就会掉帧。
4.4 Android特有崩溃:Native Crash
有时候Console里看不到报错,但包打出来在手机上就闪退。这通常是C++层面的崩溃。
排查技巧:
- 连接手机,打开Android Studio的Logcat。
- 过滤关键字“FATAL EXCEPTION”或“AndroidRuntime”。
- 使用
adb logcat命令在终端查看。 - 如果是Unity版本问题,去Unity论坛搜索相关版本的已知Bug。2024年建议升级到最新的LTS版本,很多原生崩溃都已经被修复了。
第五阶段:性能优化——让游戏在低端机上也丝滑
上架前,你必须确保游戏在千元机上也能跑。以下是2024年最实用的优化技巧:
5.1 合批(Batching):减少Draw Call
什么是Draw Call? 每画一个物体,CPU就要告诉GPU“画这个”。这个指令叫Draw Call。如果场景里有1000个物体,就有1000次Draw Call,CPU忙不过来,帧率就低。
优化方法:
- 静态合批(Static Batching):把不移动的物体(如背景、墙壁)勾选上
Static标记。Unity会在烘焙时将它们合并。 - 动态合批(Dynamic Batching):移动的小物体,Unity会自动合并,但有顶点数限制(300顶点以内)。
- GPU Instancing:对于大量相同物体(如草地、敌人),开启GPU Instancing。在Material的Shader里勾选“Instancing”,然后物体上勾选“Enable GPU Instancing”。这能把几千个物体变成1次Draw Call。
5.2 资源压缩
2024年,玩家不再愿意下载几百兆的游戏。
纹理压缩:
- Android推荐使用ASTC格式。Unity的
TextureImporter里,把Compression改为ASTC 6x6或8x8,画质损失不大,但体积能缩小3-4倍。 - 检查每个贴图,把
Read/Write Enabled取消勾选(除非你需要在代码里读取像素),这能节省大量内存。
- Android推荐使用ASTC格式。Unity的
音频压缩:
- 背景音乐用
MP3或Ogg Vorbis,音质一般但体积小。 - 音效用
Vorbis,采样率设为22050或44100足够了。 - 在
AudioClip的Importer里调整Quality。
- 背景音乐用
5.3 对象池(Object Pooling)
这是性能优化的银弹。
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public static ObjectPool Instance;
public GameObject prefab;
public int poolSize = 10;
private Queue<GameObject> pool = new Queue<GameObject>();
void Awake()
{
Instance = this;
// 初始化池子
for (int i = 0; i < poolSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
public GameObject GetObject()
{
GameObject obj;
if (pool.Count > 0)
{
obj = pool.Dequeue();
}
else
{
obj = Instantiate(prefab);
}
obj.SetActive(true);
return obj;
}
public void ReturnObject(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
使用示例:
// 生成苹果时
GameObject apple = ObjectPool.Instance.GetObject();
apple.transform.position = new Vector3(Random.Range(-4, 4), 6, 0);
// 苹果被销毁时
ObjectPool.Instance.ReturnObject(gameObject);
这样,你避免了频繁的Instantiate和Destroy,这两个操作非常消耗CPU。
第六阶段:上架应用市场——从调试到发布
代码写完了,性能也优化了,接下来就是让全世界玩到你的游戏。
6.1 打包设置
- File -> Build Settings,选择
Android,点击Switch Platform。 - Player Settings:
- Company Name 和 Product Name:填好。
- Bundle Identifier:格式必须是
com.你的名字.游戏名,如com.agne.gamesrunner。这个ID一旦上传就不能改,所以要想好。 - Minimum API Level:2024年建议至少支持到Android 8.0(API 26),以覆盖绝大多数用户。
- Target API Level:选最高(如Android 14)。
- Scripting Backend:选
IL2CPP(2024年标准,