4、C#脚本基础:MonoBehaviour、Start与Update、变量与类型、公共变量序列化、Debug日志

好,咱们开始聊C#脚本。很多新手一上来就被各种术语吓住了,什么MonoBehaviour、生命周期、序列化……其实没那么复杂。你想想看,脚本说白了就是给游戏对象写一份“行为说明书”。

我个人习惯把脚本理解成“大脑”。一个空壳子GameObject,挂上脚本就有了灵魂。今天咱们就把这个“大脑”的基本构造拆开看看。

4.1 MonoBehaviour:脚本的“身份证”

你在Unity里新建一个C#脚本,打开后会发现它默认继承了 MonoBehaviour。这是什么?

说白了,MonoBehaviour 是Unity提供的一个基类。你的脚本只有继承它,才能被挂到GameObject上,才能使用那些神奇的生命周期函数(比如Start、Update)。

核心要点: 所有挂载在GameObject上的脚本,都必须直接或间接继承 MonoBehaviour

我记得刚入行时,有次写了一个纯逻辑类,忘了继承MonoBehaviour,结果拖拽到物体上死活没反应。排查了半天,脸都绿了。嗯,这种低级错误,犯一次就记住了。

// 标准脚本模板
using UnityEngine;

public class MyFirstScript : MonoBehaviour
{
    // 你的代码写在这里
}

小提示: 脚本文件名必须和类名完全一致,包括大小写。否则Unity会报错,挂不上去。

4.2 Start与Update:脚本的“呼吸”与“心跳”

这两个函数是MonoBehaviour最常用的生命周期方法。我习惯这么理解:

  • Start:脚本启动时执行一次,适合做初始化工作。
  • Update:每帧执行一次,适合做持续更新逻辑。

为什么会这样设计?因为游戏是循环运行的。你想想看,玩家按一下W键,角色不能只动一帧吧?它需要每帧都检测输入、更新位置。这就是Update的用武之地。

public class PlayerMovement : MonoBehaviour
{
    void Start()
    {
        // 初始化:设置初始位置、获取组件引用等
        Debug.Log("玩家已生成!");
    }

    void Update()
    {
        // 每帧检测输入并移动
        if (Input.GetKey(KeyCode.W))
        {
            transform.Translate(Vector3.forward * Time.deltaTime);
        }
    }
}

注意: Update里不要做耗时操作,比如加载资源、复杂的数学计算。每帧都跑一遍,性能会崩。我曾经在一个项目里把寻路算法放Update里,结果手机发烫得像暖手宝……

除了Start和Update,还有几个常用的:

函数名 调用时机 典型用途
Awake 脚本实例被创建时(早于Start) 初始化引用、设置初始状态
Start 第一次Update之前 依赖其他对象的初始化
Update 每帧 常规更新逻辑
FixedUpdate 固定时间间隔(物理更新) 物理相关操作(刚体、力)
LateUpdate 所有Update执行完后 摄像机跟随等

我的习惯: Awake里做“自己”的初始化,Start里做“依赖别人”的初始化。这样顺序清晰,不容易出空引用错误。

4.3 变量与类型:数据的“容器”

变量就是用来存数据的。C#是强类型语言,每个变量都得声明它是什么类型。说白了,就是告诉Unity:“我要存一个数字”、“我要存一段文字”。

常用的基础类型:

  • int:整数,比如生命值、分数。
  • float:浮点数(带小数),比如速度、位置。
  • bool:布尔值,true或false,比如是否死亡、是否开门。
  • string:字符串,比如玩家名字、对话文本。
  • Vector3:三维向量,Unity特有,存位置、方向。
public class PlayerStats : MonoBehaviour
{
    int health = 100;          // 整数
    float speed = 5.5f;        // 浮点数,注意加f后缀
    bool isAlive = true;       // 布尔值
    string playerName = "勇者"; // 字符串
    Vector3 startPosition;     // 三维向量
}

避坑指南: 浮点数赋值时一定要加 f 后缀,比如 5.5f。不加的话,编译器会当成double类型,可能报错。我曾经因为这个被卡了十分钟,检查来检查去,最后发现少了个f……

4.4 公共变量序列化:在Inspector里“调参”

这是Unity最爽的功能之一。你只要在变量前面加上 public,它就会出现在Inspector面板上。你可以直接在编辑器里调整数值,不用改代码。

public class EnemyConfig : MonoBehaviour
{
    public float moveSpeed = 3f;      // 在Inspector里可见
    public int maxHealth = 50;
    public string enemyName = "小怪";
    
    private int currentHealth;        // private的不会显示
}

你想想看,这有多方便?策划想调怪物的速度,不用找你改代码,直接在Inspector里拖拽滑块就行。这就是“序列化”的威力——把C#变量暴露给Unity编辑器。

进阶技巧: 如果不想暴露给Inspector,但又想序列化(比如保存数据),可以用 [SerializeField] 特性标记private变量。

[SerializeField]
private int secretValue = 42;  // 在Inspector可见,但其他脚本不能访问

注意: 公共变量不要滥用。只暴露那些需要外部调整的参数。内部逻辑用的变量,尽量用private。这是代码封装的基本素养。

4.5 Debug日志:程序员的“听诊器”

写代码不可能一次写对。怎么排查问题?靠 Debug.Log。它会在Unity的控制台输出信息,帮你了解程序运行状态。

void Start()
{
    Debug.Log("游戏开始了!");
    
    int score = 100;
    Debug.Log("当前分数:" + score);
    
    // 带颜色的警告和错误
    Debug.LogWarning("这只是一个警告");
    Debug.LogError("出错了!快检查!");
}

我个人习惯在关键节点加日志:

  • 初始化完成时
  • 玩家触发事件时
  • 变量值发生变化时
  • 进入/退出某个状态时

我的经验: 发布游戏前,记得把调试日志删掉或注释掉。否则玩家打开控制台看到一堆“Debug.Log”,体验很不好。我曾经有个项目忘了删,结果玩家反馈说“游戏里全是乱码”……

另外,Debug.Log 还可以打印对象引用,方便你检查某个组件是否为空:

public Rigidbody rb;

void Start()
{
    Debug.Log("Rigidbody组件:" + rb, rb);  // 第二个参数传对象,点击日志会高亮该对象
}

小技巧:Debug.DrawLineDebug.DrawRay 可以在Scene视图画线,调试位置和方向非常直观。比看数字强多了。

好了,这一章的内容就这些。总结一下:

  • 脚本继承 MonoBehaviour 才能挂载
  • Start 做初始化,Update 做持续更新
  • 变量要声明类型,public 变量可序列化到Inspector
  • Debug.Log 是调试利器,但发布前要清理

下一章咱们会深入聊“生命周期”的更多细节,以及如何用 AwakeFixedUpdate 写出更健壮的脚本。到时候见。