3、C#脚本基础:MonoBehaviour生命周期、Start与Update、变量与类型、公共变量的序列化

好,咱们正式开始写脚本了。

很多新手拿到Unity,第一件事就是拖个模型进来,然后到处找“怎么让角色动起来”。其实核心就一句话:一切行为靠脚本驱动。而脚本的根基,就是MonoBehaviour的生命周期。

我个人习惯把MonoBehaviour理解成一个“有生命的对象”。它从出生到死亡,会经历一系列固定的阶段。你只要在对应的阶段里写代码,Unity就会自动帮你调用。

3.1 MonoBehaviour是什么?

说白了,MonoBehaviour是Unity里所有脚本的基类。你新建一个C#脚本,默认继承的就是它。它让你的脚本能挂载到GameObject上,并且能响应Unity引擎的各种事件。

举个例子:

using UnityEngine;

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

嗯,就这么简单。只要继承了MonoBehaviour,你的脚本就有了“生命”。

3.2 生命周期:从Awake到OnDestroy

生命周期是MonoBehaviour最核心的概念。我刚开始学的时候,经常搞混Awake和Start的区别。后来踩了一次坑才彻底明白。

核心生命周期顺序(按调用先后):

  1. Awake():脚本实例被创建时调用。无论脚本是否启用,都会执行。适合做初始化,比如获取组件引用。
  2. OnEnable():脚本或GameObject被激活时调用。每次激活都会触发。
  3. Start():在第一次Update之前调用。只调用一次。适合做游戏逻辑的初始化。
  4. Update():每帧调用一次。频率不固定,取决于帧率。适合处理持续变化的逻辑,比如移动、输入检测。
  5. FixedUpdate():固定时间间隔调用。默认0.02秒一次。适合处理物理相关逻辑,比如刚体移动。
  6. LateUpdate():所有Update执行完后调用。适合做相机跟随等需要最后处理的操作。
  7. OnDisable():脚本或GameObject被禁用时调用。
  8. OnDestroy():脚本或GameObject被销毁时调用。

我曾经在项目里犯过一个低级错误:在Awake里给一个变量赋值,然后在Start里用这个变量去初始化另一个对象。结果因为脚本加载顺序的问题,那个对象还没准备好,直接报空引用。后来我养成了一个习惯:组件引用在Awake里获取,游戏逻辑初始化在Start里做。这样基本不会出问题。

3.3 Start与Update:最常用的两个方法

这两个方法你几乎每个脚本都会用到。咱们重点说说它们的区别和使用场景。

Start()

只执行一次。适合做这些事情:

  • 初始化变量
  • 获取其他组件或对象的引用
  • 设置初始状态

举个例子:

private Rigidbody rb;
private int score;

void Start()
{
    rb = GetComponent<Rigidbody>();
    score = 0;
    Debug.Log("游戏开始!");
}

Update()

每帧执行。适合做这些事情:

  • 检测玩家输入(键盘、鼠标、触屏)
  • 移动非物理对象
  • 播放动画或音效
  • 计时器逻辑

看个简单的移动例子:

public float speed = 5f;

void Update()
{
    float horizontal = Input.GetAxis("Horizontal");
    float vertical = Input.GetAxis("Vertical");
    
    Vector3 movement = new Vector3(horizontal, 0, vertical) * speed * Time.deltaTime;
    transform.Translate(movement);
}

注意这里用了Time.deltaTime。为什么?因为Update每帧调用,帧率不同会导致移动速度不一样。乘以deltaTime后,移动速度就与帧率无关了。这个坑我刚开始也踩过——角色在60帧时跑得飞快,掉到30帧就慢得像蜗牛。加上deltaTime就稳了。

小技巧:如果你需要处理物理碰撞或刚体移动,记得用FixedUpdate。我曾经在Update里直接修改刚体的位置,结果物理模拟变得很不稳定,角色穿墙了。后来改成FixedUpdate就正常了。

3.4 变量与类型:C#的基础

写脚本离不开变量。Unity里常用的数据类型,我列个表给你看:

类型 说明 示例
int 整数 int health = 100;
float 浮点数(带小数) float speed = 5.5f;
bool 布尔值(true/false) bool isDead = false;
string 字符串 string playerName = "Hero";
Vector3 三维向量(位置、方向) Vector3 pos = new Vector3(0, 1, 0);
GameObject 游戏对象引用 GameObject player;
Transform 变换组件(位置/旋转/缩放) Transform t;

你想想看,这些类型其实跟现实世界是对应的。int就是“有几个”,float是“有多长”,bool是“是或否”。写代码时选对类型,逻辑就清晰一半。

3.5 公共变量的序列化:让Inspector变强大

这是Unity一个非常实用的特性。你只要在变量前面加上public,它就会显示在Inspector面板上。你可以在编辑器里直接修改数值,不用改代码。

public class PlayerMovement : MonoBehaviour
{
    public float moveSpeed = 5f;
    public float jumpForce = 10f;
    public GameObject bulletPrefab;
    public Transform firePoint;
}

把这段代码挂到GameObject上,Inspector面板就会出现四个可编辑的字段。你可以拖拽预制体、调整数值,非常方便。

序列化的好处:

  • 设计师和策划可以直接调参数,不用碰代码
  • 不同角色可以共用同一个脚本,只要参数不同就行
  • 调试时随时改数值,不用重新编译

不过要注意一点:公共变量会暴露给所有外部脚本。如果你不想让其他脚本随意修改,可以用[SerializeField]属性配合private。这样既能在Inspector里看到,又不会被外部代码乱改。

[SerializeField]
private float health = 100f;  // 只在Inspector显示,外部无法访问

我曾经踩过的坑:有一次我把一个关键的游戏配置写成了public,结果另一个脚本在Start里把它改成了0,导致整个关卡无法正常开始。排查了半天才发现是变量被意外修改了。从那以后,我养成了习惯:能用private就用private,需要暴露给Inspector就用[SerializeField]。这样既安全又方便。

3.6 实战小练习:写一个简单的角色控制器

咱们把今天学的知识串起来,写一个最简单的角色移动脚本:

using UnityEngine;

public class SimplePlayer : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float rotateSpeed = 100f;
    
    private Rigidbody rb;
    
    void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }
    
    void Start()
    {
        Debug.Log("角色已生成!");
    }
    
    void Update()
    {
        // 检测输入
        float move = Input.GetAxis("Vertical");
        float turn = Input.GetAxis("Horizontal");
        
        // 移动和旋转
        transform.Translate(Vector3.forward * move * moveSpeed * Time.deltaTime);
        transform.Rotate(Vector3.up * turn * rotateSpeed * Time.deltaTime);
    }
    
    void FixedUpdate()
    {
        // 如果有物理需求,可以在这里处理
        // 比如:rb.AddForce(...);
    }
}

这个脚本包含了Awake、Start、Update、FixedUpdate四个生命周期方法,用到了public序列化变量和私有序列化变量。你把它挂到角色上,按WASD就能移动了。

嗯,今天的内容就到这里。生命周期是Unity脚本的骨架,变量和序列化是血肉。把这两块吃透了,后面的动画控制和角色状态机就好理解多了。

课后建议:打开Unity,新建一个脚本,把Awake、Start、Update、FixedUpdate、LateUpdate都写上,每个方法里加一句Debug.Log。运行游戏,看看控制台的输出顺序。亲手验证一遍,比看十遍文章都管用。