4、C#脚本基础:脚本创建、MonoBehaviour、Start与Update、变量与类型、方法定义、控制台调试

好,咱们正式开始写代码了。

说实话,很多新手学Unity,一上来就被各种组件、面板搞晕了。但我觉得,真正让你和引擎「对话」的,还是C#脚本。你想想看,没有脚本的游戏,就像一具没有灵魂的躯壳——模型再漂亮,它也不会动。

这一章,我们就从零开始,把C#脚本的底子打扎实。我会把我这些年踩过的坑、养成的习惯,都揉碎了讲给你听。

4.1 脚本的创建与挂载

在Unity里创建脚本,最简单的方式就是在Project窗口右键 → Create → C# Script。我个人习惯直接给脚本起好名字,比如 PlayerController,而不是先建一个「NewBehaviourScript」再改。为什么?因为脚本的文件名必须和类名一致,你后面再改容易出幺蛾子。

注意: 脚本文件名和类名必须完全一致,包括大小写。我曾经有个同事,把文件名改成「playercontroller」(全小写),结果类名还是「PlayerController」,Unity直接报错,找了半天才找到原因。

创建好脚本后,拖到Hierarchy里的某个游戏对象上,或者选中对象后在Inspector面板点「Add Component」搜索脚本名。挂上去之后,脚本就活了。

嗯,这里要提一句:脚本本身只是一个文本文件,只有挂到游戏对象上,它才会在游戏运行时执行。

4.2 MonoBehaviour:脚本的基类

你打开任何一个新建的脚本,都会看到这么一行:

public class PlayerController : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

这个 MonoBehaviour 是啥?说白了,它就是Unity给咱们准备好的「脚本底座」。只要你的脚本继承自它,就能使用Unity的生命周期函数(比如Start、Update),也能访问组件、协程等核心功能。

我在项目中遇到过不少新手,自己写了一个普通C#类,没继承MonoBehaviour,然后问我为什么挂不到对象上。记住:只有继承MonoBehaviour的类,才能作为组件挂载

核心要点:
  • 所有挂载到游戏对象上的脚本,都必须继承MonoBehaviour
  • MonoBehaviour提供了Start、Update、Awake、OnEnable等生命周期方法
  • 不要手动new MonoBehaviour的子类,Unity会帮你管理实例

4.3 Start与Update:生命周期入门

这两个方法,是咱们最常用的。我简单说一下它们的区别:

方法 调用时机 用途
Start() 对象被激活时,且只调用一次 初始化数据、获取组件引用、设置初始状态
Update() 每帧调用一次 处理输入、移动物体、检测碰撞等持续逻辑

你想想看,如果每帧都去获取一次组件引用,那性能得多差?所以获取组件这种操作,放在Start里做一次就够了。而移动、旋转这种需要持续更新的逻辑,就放在Update里。

我曾经犯过一个错误:在Update里用 GetComponent<Rigidbody>() 获取刚体,结果每帧都在做重复查找,帧率直接掉了一半。后来改成在Start里缓存引用,问题就解决了。

小技巧: 如果你希望脚本在对象被激活时立刻执行某些逻辑(甚至比Start还早),可以用Awake()。Awake在Start之前调用,适合做组件之间的相互引用。

4.4 变量与类型:存储数据的基础

变量就是用来存数据的「盒子」。C#是强类型语言,每个变量都得声明它存什么类型的数据。

咱们在Unity里最常用的几种类型:

  • int:整数,比如生命值、分数
  • float:浮点数,比如速度、位置坐标
  • bool:布尔值,true或false,比如是否死亡、是否开门
  • string:字符串,比如玩家名字、对话文本
  • Vector3:三维向量,表示位置、方向
  • GameObject:游戏对象引用
  • Transform:位置/旋转/缩放组件

举个例子:

public class PlayerController : MonoBehaviour
{
    // 公开变量,可以在Inspector面板里直接修改
    public float moveSpeed = 5f;
    public int maxHealth = 100;
    public string playerName = "Hero";

    // 私有变量,只在脚本内部使用
    private int currentHealth;
    private Vector3 startPosition;

    void Start()
    {
        currentHealth = maxHealth;
        startPosition = transform.position;
    }
}

这里有个细节:public 变量会在Inspector面板显示出来,方便你调整数值。而 private 变量则隐藏起来,防止不小心改乱。我个人习惯把所有需要外部调整的变量都设为public,内部逻辑用的变量都设为private。

避坑指南: 我曾经把一个 float 类型的变量误写成 int,结果移动速度总是被截断成整数,角色走起来一顿一顿的。后来排查了半天才发现是类型问题。所以定义变量时,一定要想清楚它到底需要小数还是整数。

4.5 方法定义:让脚本动起来

方法就是一段可以重复调用的代码块。咱们把逻辑封装到方法里,代码会清晰很多。

定义一个方法的基本语法:

// 访问修饰符 + 返回类型 + 方法名(参数列表)
public void MoveCharacter(float speed)
{
    // 方法体
    transform.Translate(Vector3.forward * speed * Time.deltaTime);
}

private int CalculateDamage(int baseDamage, float multiplier)
{
    return Mathf.RoundToInt(baseDamage * multiplier);
}

注意看:

  • void 表示这个方法不返回任何值
  • 如果有返回值,要写具体类型,比如 intfloat,并且方法体内要用 return 返回
  • 方法名建议用动词开头,比如 MoveCharacterTakeDamage,一看就知道是干嘛的

我在项目中遇到过一种情况:有人把所有逻辑都塞在Update里,一个方法写了上千行。后来想加个跳跃功能,根本找不到从哪里下手。所以我的建议是:每个方法只做一件事。比如移动写一个方法,攻击写一个方法,受伤写一个方法。这样后期维护起来,你看着也舒服。

4.6 控制台调试:Debug.Log是你的眼睛

写代码哪有不出错的?关键是出错后怎么快速找到问题。Unity的控制台(Console)就是咱们的「照妖镜」。

最常用的调试方法:

void Start()
{
    Debug.Log("游戏开始了!");
    Debug.LogWarning("注意:生命值较低");
    Debug.LogError("错误:找不到目标对象");
}

这三种输出分别对应普通信息、警告(黄色)、错误(红色)。在控制台里一目了然。

我个人的调试习惯是:

  • 在关键逻辑入口处加 Debug.Log,确认代码是否执行到了这里
  • 在变量变化时输出当前值,比如 Debug.Log("当前速度:" + currentSpeed);
  • Debug.LogError 标记那些「绝对不应该发生」的情况
小技巧: 你可以在 Debug.Log 里传入游戏对象,比如 Debug.Log("玩家位置", gameObject);。这样在控制台点击这条日志时,Unity会自动高亮对应的游戏对象,非常方便。

嗯,这里还要说一句:发布游戏时,记得把所有的 Debug.Log 删掉或者注释掉。不然玩家打开控制台,看到满屏的调试信息,体验会很差。我一般会用条件编译 #if UNITY_EDITOR 来包裹调试代码,只在编辑器里输出。

4.7 本章小结

这一章咱们把C#脚本的基础过了一遍。从创建脚本、挂载对象,到理解MonoBehaviour的生命周期,再到变量、方法、调试——这些都是你以后写游戏逻辑的「砖瓦」。

说白了,脚本就是你和Unity沟通的语言。你写得越规范,引擎就越听话。下一章我们会开始写真正的游戏逻辑,比如控制角色移动、跳跃。到时候你会发现,今天学的这些基础,全都会用上。

好,先练练手,打开Unity,创建一个脚本,试着在Start里输出一句话,在Update里让一个方块旋转起来。遇到问题别怕,控制台会告诉你怎么回事。