1. 为什么一个做游戏的人,最后都绕不开 C#
很多人第一次打开 Unity 的时候,心里想的都是“我要做个游戏”,结果点开新建脚本,看到一坨英文和花括号,瞬间就懵了。我当年也是这样,拖了一个方块到场景里,想让它动起来,结果发现不写代码它就跟块砖头一样杵在那儿。后来才明白,Unity 里所有“活”的东西——角色移动、按钮响应、分数计算、关卡切换——背后都是 C# 脚本在驱动。
这篇内容就是写给那些完全没碰过代码、但想用 Unity 做点东西的人。我会从“C# 到底是什么”讲起,一路带你写到能跑起来一个文字小游戏。所谓文字小游戏,就是那种没有复杂画面、主要靠文字和按钮交互的小玩意,比如猜数字、文字冒险、答题闯关。别小看它,这类项目麻雀虽小五脏俱全,变量、判断、循环、函数、输入输出全都能练到,是新手从“看不懂代码”到“能自己写逻辑”的最佳跳板。
你不需要有任何编程基础,只要会打字、能看懂简单的英文单词就行。我会把每个概念都拆开揉碎,用生活里的例子去类比,让你知道这行代码到底在干嘛、为什么要这么写。等你跟着走完一遍,再回头看那些“零基础学 C#”的教程,你会发现它们讲的东西你基本都能看懂了。
2. 先搞明白 C# 和 Unity 到底是什么关系
2.1 C# 不是 Unity 的专属,但 Unity 离不开它
C# 读作“C Sharp”,是微软推出的一门编程语言。它本身可以用来写桌面软件、网站后台、手机应用,但在游戏开发圈子里,它最出名的身份就是“Unity 的官方脚本语言”。你可以把 Unity 想象成一个巨大的乐高工厂,里面有现成的积木——摄像机、灯光、物理系统、UI 组件——而 C# 就是你手里的说明书和胶水,告诉这些积木什么时候动、怎么动、动完以后干什么。
Unity 之所以选 C#,是因为它语法相对干净、类型安全、跨平台支持好,而且有庞大的社区和文档。对新手来说,最直观的好处是:你写错一个字母,编辑器会立刻告诉你哪里有问题,不会让你对着黑屏瞎猜。
2.2 脚本在 Unity 里是怎么跑起来的
Unity 的脚本不是自己主动运行的,它靠的是“生命周期函数”。你可以把这些函数理解成游戏世界里的闹钟,到点了 Unity 就会去叫对应的脚本干活。最常用的几个:
Start():游戏对象第一次出现在场景里时调用一次,适合做初始化,比如设置初始分数、获取组件引用。Update():每一帧调用一次,一秒钟大概跑 60 次(取决于帧率),适合处理实时输入、移动、计时。Awake():比 Start 更早,对象一被创建就调用,适合做单例初始化或者引用赋值。
新手最容易犯的错,就是把所有逻辑都塞进Update()里,结果每帧都在重复计算,性能直接崩掉。记住一个原则:只做一次的事放Start(),需要持续检测的事放Update()。
2.3 为什么用文字小游戏来练手
图形化游戏涉及模型、动画、粒子、光照,新手很容易被这些视觉元素带偏,最后代码没写几行,时间全花在调材质上了。文字小游戏把视觉干扰降到最低,你的注意力会集中在“数据怎么存、逻辑怎么走、用户输入怎么处理”这三件事上。这三件事搞定了,后面做任何类型的游戏都只是换皮。
而且文字小游戏特别容易做出成就感。你写十几行代码,就能让程序跟你对话、判断你输入的数字对不对、给你算分,这种即时反馈对新手来说非常重要。
3. 动手之前,先把这几个 C# 核心概念吃透
3.1 变量:给数据起个名字
变量就是用来存东西的盒子。你有一个分数,就建一个叫score的盒子;你有一个玩家名字,就建一个叫playerName的盒子。C# 是强类型语言,建盒子的时候要说明里面装什么类型的东西:
int score = 0; // 整数,用来存分数 string playerName = "小明"; // 字符串,用来存名字 float timeLeft = 60.5f; // 小数,用来存时间 bool isGameOver = false; // 布尔值,只有 true 或 false这里有个新手常踩的坑:float类型的数字后面必须加f,比如60.5f,不加的话 C# 会把它当成double(精度更高的浮点数),然后报错说类型不匹配。我当初就被这个f卡了半小时,一直以为是自己拼写错了。
3.2 条件判断:让程序学会“如果……就……”
游戏里到处都是选择:如果猜对了就加分,如果时间到了就结束,如果血量小于零就死亡。C# 里用if-else来实现:
if (guess == secretNumber) { Debug.Log("猜对了!"); score += 10; } else if (guess > secretNumber) { Debug.Log("猜大了"); } else { Debug.Log("猜小了"); }注意==和=的区别。=是赋值,把右边的值塞进左边的变量;==是判断相等,问“这两个东西一样吗”。写错一个等号,程序不会报错,但逻辑会完全乱掉,这是新手最隐蔽的 bug 之一。
3.3 循环:重复的事情交给机器
如果我要让程序从 1 数到 100,不可能手写 100 行输出。循环就是干这个的:
for (int i = 1; i <= 100; i++) { Debug.Log(i); }for循环的三个部分用分号隔开:初始化(int i = 1)、继续条件(i <= 100)、每次循环后做什么(i++,也就是 i 加 1)。读起来就是“从 1 开始,只要小于等于 100 就一直做,每次做完加 1”。
文字小游戏里,循环常用来遍历题库、生成随机数范围、或者做倒计时。
3.4 函数:把一段逻辑打包起来
函数就是给一段代码起个名字,以后想用的时候直接叫名字就行。比如你写了一个“生成随机数”的逻辑,不想每次都复制粘贴,就把它包成函数:
int GenerateRandomNumber(int min, int max) { return Random.Range(min, max + 1); }int是返回类型,说明这个函数会吐出一个整数;括号里的min和max是参数,调用的时候要传进去。return就是把结果交出去。函数的好处是:改一处,所有调用它的地方都跟着变,不用满世界找重复代码。
3.5 类与对象:Unity 脚本的基本单位
Unity 里每个脚本本质上都是一个类。你新建一个 C# 脚本,Unity 会自动生成一段模板:
public class MyFirstScript : MonoBehaviour { void Start() { } void Update() { } }public class MyFirstScript就是定义了一个叫MyFirstScript的类,冒号后面的MonoBehaviour表示它继承自 Unity 的基础行为类,只有继承了它,这个脚本才能被挂到游戏对象上。类里面可以放变量、函数,这些统称为“成员”。
新手不用一开始就深究面向对象三大特性,先知道“脚本就是类,类里面写变量和函数”就够了。
4. 搭建你的第一个文字小游戏:猜数字
4.1 项目准备与场景搭建
打开 Unity Hub,新建一个 2D 项目,名字随便起,比如TextGameDemo。创建完成后,在 Hierarchy 面板右键,选择UI -> Canvas,这会创建一个画布,所有 UI 元素都要放在它下面。接着在 Canvas 上右键,创建以下 UI 元素:
- 一个
Text(旧版)或TextMeshPro - Text,用来显示提示信息 - 一个
InputField(旧版)或TMP_InputField,用来让玩家输入数字 - 一个
Button,用来提交答案 - 再一个
Text,用来显示分数和剩余次数
如果你用的是 Unity 2021 之后的版本,默认会推荐 TextMeshPro,功能更强,但需要导入 TMP Essentials。旧版 Text 更简单,新手可以先用它,少一步导入操作。
布局上不用太讲究,大概排成上下结构就行:提示文字在上,输入框在中间,按钮在下面,状态文字放角落。Unity 的 UI 系统会自动帮你处理分辨率适配,只要 Canvas 的 Render Mode 保持默认的 Screen Space - Overlay 就行。
4.2 创建脚本并挂载
在 Project 面板右键,Create -> C# Script,命名为GuessNumberGame。双击打开,它会用你默认的代码编辑器(通常是 Visual Studio 或 VS Code)打开。把里面的内容全部删掉,换成下面这段:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class GuessNumberGame : MonoBehaviour { public Text hintText; public InputField inputField; public Text statusText; private int secretNumber; private int remainingTries; private int score; void Start() { StartNewGame(); } void StartNewGame() { secretNumber = Random.Range(1, 101); remainingTries = 5; score = 0; hintText.text = "猜一个 1 到 100 之间的数字"; statusText.text = "剩余次数:" + remainingTries + " 分数:" + score; inputField.text = ""; } public void OnSubmitButtonClick() { if (remainingTries <= 0) { hintText.text = "游戏已结束,请重新开始"; return; } string input = inputField.text; int guess; if (!int.TryParse(input, out guess)) { hintText.text = "请输入一个有效的数字"; return; } if (guess < 1 || guess > 100) { hintText.text = "数字必须在 1 到 100 之间"; return; } remainingTries--; if (guess == secretNumber) { score += remainingTries * 10 + 10; hintText.text = "恭喜你猜对了!答案就是 " + secretNumber; statusText.text = "最终分数:" + score; remainingTries = 0; } else if (guess > secretNumber) { hintText.text = "猜大了,再试试"; } else { hintText.text = "猜小了,再试试"; } if (remainingTries <= 0 && guess != secretNumber) { hintText.text = "次数用完了,答案是 " + secretNumber; } statusText.text = "剩余次数:" + remainingTries + " 分数:" + score; inputField.text = ""; } }保存后回到 Unity,把GuessNumberGame脚本拖到 Canvas 或者任意一个场景对象上。然后在 Inspector 面板里,把之前创建的那些 UI 元素分别拖到脚本对应的槽位里:hintText拖提示文字,inputField拖输入框,statusText拖状态文字。
最后选中 Button,在 Inspector 的On Click()列表里点加号,把挂载了脚本的那个对象拖进去,然后在函数下拉菜单里选GuessNumberGame -> OnSubmitButtonClick。这样按钮点击就会触发我们的逻辑。
4.3 代码逐段拆解与参数计算
Random.Range(1, 101)返回的是 1 到 100 之间的整数,因为Random.Range对整数是左闭右开区间,包含最小值但不包含最大值。如果你写Random.Range(1, 100),那永远猜不到 100。这个细节文档里写得很清楚,但新手很容易忽略。
int.TryParse(input, out guess)是安全地把字符串转成整数的方法。如果玩家输入的是“abc”,TryParse会返回false,guess保持为 0,我们就知道输入不合法。如果你用int.Parse(),遇到非法输入会直接抛异常,游戏就崩了。所以在处理用户输入时,永远优先用TryParse。
分数计算score += remainingTries * 10 + 10的意思是:剩余次数越多,加分越高。比如还剩 4 次就猜中,得 50 分;还剩 1 次才猜中,得 20 分。这样设计是为了鼓励玩家尽快猜对,而不是乱试。
remainingTries--是自减运算符,等价于remainingTries = remainingTries - 1。每次提交答案扣一次机会,不管猜对猜错。猜对之后我把remainingTries设为 0,是为了让后续的按钮点击直接走“游戏已结束”的分支,防止玩家继续刷分。
4.4 测试与调试
点 Unity 上方的播放按钮,游戏开始运行。在输入框里输入数字,点按钮,看提示文字有没有正确变化。如果没反应,先检查这几件事:
- 按钮的
On Click()有没有绑定错对象或函数 - 脚本里的
public变量有没有在 Inspector 里赋值 - 控制台有没有报错,红色错误会阻止脚本运行
我建议你在OnSubmitButtonClick开头加一句Debug.Log("按钮被点击了,输入是:" + inputField.text);,这样能在 Console 面板看到每次点击的记录,快速定位是按钮没触发还是逻辑没走对。
5. 从猜数字扩展到真正的“文字小游戏”
5.1 加入多轮制和难度选择
猜数字只能玩一局,太单薄。我们可以加一个“轮次”概念:每局猜中后,自动进入下一局,难度递增,比如第一局 1 到 50,第二局 1 到 100,第三局 1 到 200。这样就有了成长感。
实现思路是加一个currentRound变量,在StartNewGame()里根据轮次计算最大值:
int maxNumber = 50 * currentRound; secretNumber = Random.Range(1, maxNumber + 1);同时把剩余次数也调整一下,比如remainingTries = 6 - currentRound,让后面的局更紧张。这些数值没有标准答案,你可以根据自己的手感调,关键是让玩家觉得“有点难但能克服”。
5.2 用 PlayerPrefs 保存最高分
PlayerPrefs 是 Unity 提供的一个简易存储系统,适合存最高分、音量设置这类小数据。用法很简单:
int highScore = PlayerPrefs.GetInt("HighScore", 0); if (score > highScore) { PlayerPrefs.SetInt("HighScore", score); PlayerPrefs.Save(); }GetInt的第二个参数是默认值,如果之前没存过就返回 0。Save()是强制写入磁盘,不调用的话游戏退出时可能丢失。注意 PlayerPrefs 不适合存大量数据或敏感信息,但存个最高分绰绰有余。
5.3 把猜数字改成文字冒险的框架
文字冒险的核心是“状态机”:每个场景有描述文字、若干选项、每个选项指向下一个场景。你可以用字典来存这些数据:
[System.Serializable] public class StoryNode { public string description; public string[] choices; public int[] nextNodes; }然后在 Inspector 里手动配置每个节点,或者用 JSON 文件加载。这样你的文字小游戏就从“猜数字”升级成了“有剧情、有分支”的完整作品。虽然代码量会增加,但核心逻辑还是那几样:变量存状态、条件判断走分支、函数处理输入。
6. 新手最容易踩的坑与排查清单
6.1 空引用异常:Unity 新手的头号杀手
NullReferenceException是新手遇到最多的报错,意思是“你试图用一个不存在的东西”。常见原因有:
- Inspector 里的槽位没拖对象,脚本里的
public Text hintText是空的 - 用
GetComponent<Text>()获取组件,但对象上根本没有这个组件 - 场景切换后旧对象的引用失效了
排查方法:在报错的那行代码前面加Debug.Log(hintText == null);,如果输出True,说明确实是空的。然后回到 Inspector 检查赋值。
6.2 按钮点击没反应
先确认按钮的Interactable属性是勾选的,再确认On Click()列表里绑定的对象和函数正确。如果函数有参数,Unity 的按钮事件默认不支持直接传参,需要用代码包装一层,或者用UnityEvent的静态参数功能。
还有一个隐蔽问题:如果 Canvas 上盖了别的 UI 元素,按钮可能被挡住了,点击事件会被上层元素拦截。检查一下有没有全屏的 Image 或 Panel 挡在按钮前面。
6.3 输入框回车不提交
InputField 有个On End Edit事件,可以绑定到提交函数,这样玩家按回车也能提交。但要注意,On End Edit在输入框失去焦点时也会触发,可能会造成重复提交。更稳妥的做法是用Update()检测Input.GetKeyDown(KeyCode.Return),但前提是输入框处于选中状态。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 脚本无法挂载 | 类名和文件名不一致 | 确保两者完全相同,包括大小写 |
| 按钮点击无反应 | 未绑定 OnClick 或对象为空 | 检查 Inspector 绑定,加 Debug.Log 验证 |
| 报 NullReferenceException | 引用变量未赋值 | 在 Inspector 中拖入对应对象 |
| 输入数字无效 | 用了 int.Parse 而非 TryParse | 改用 TryParse 并处理 false 分支 |
| 分数不更新 | 逻辑走了但 UI 没刷新 | 确认 statusText.text 在每次变化后都重新赋值 |
| 游戏结束后还能继续猜 | 没有重置 remainingTries | 猜中后把剩余次数设为 0,并在开头判断 |
7. 从这个小项目里真正带走的东西
写完这个猜数字游戏,你实际上已经摸到了游戏开发最核心的几块砖:用变量存数据、用条件判断做决策、用函数组织逻辑、用 UI 接收输入和展示结果。这些跟做大型游戏用的东西没有本质区别,只是规模不同。
我个人的经验是,新手阶段不要急着去学设计模式、架构、优化,先把“输入-处理-输出”这个循环跑通。你能用代码让一个数字从 1 变到 2,就能让它从 1 变到 100;你能让一个按钮响应点击,就能让一百个按钮各司其职。后面再学数组、列表、协程、事件系统,都是在这些基础之上叠积木。
如果你跟着做完了,建议你试着改几个地方:把猜测范围改成 1 到 1000,把剩余次数改成 10 次,或者加一个“提示”按钮消耗分数来缩小范围。每改一次,你都会对某一行代码的理解更深一层。这比看十篇教程都管用。