Unity基础:Start与Update方法——Unity脚本生命周期初探

Unity基础:Start与Update方法------Unity脚本生命周期初探

大家好,欢迎回到Unity教程系列。上一篇文章我们学会了创建和挂载C#脚本,还写了几个简单的脚本来让物体旋转和移动。在那些脚本中,我们用到了Start和Update两个方法,但只是简单带过。今天这篇文章,我们要深入理解这两个最核心的Unity生命周期方法。

Start和Update是Unity中使用频率最高的两个方法,几乎每个脚本都会用到它们。理解它们的调用时机、执行频率和正确用法,是写出高质量Unity代码的基础。

一、什么是生命周期

1.1 生命周期的概念

💡 Unity中的每个MonoBehaviour脚本都有一个完整的"生命周期"------从创建到销毁的过程中,Unity引擎会在特定时机自动调用脚本中预定义的方法。这些方法按照固定的顺序被调用,构成了脚本的生命周期。

理解生命周期很重要,因为在不同的生命周期阶段,你应该做不同的事情:

  • 有些代码只需要执行一次(初始化)
  • 有些代码需要每帧都执行(更新逻辑)
  • 有些代码需要在每帧的特定阶段执行(物理更新、渲染后更新)
  • 有些代码需要在销毁前执行(清理工作)

1.2 生命周期方法概览

Unity脚本的完整生命周期包含以下关键方法(按执行顺序):

复制代码
Awake()           ──→ 脚本实例创建时调用一次
OnEnable()        ──→ 脚本启用时调用
Start()           ──→ 首次Update前调用一次
FixedUpdate()     ──→ 固定时间间隔调用(物理更新)
Update()          ──→ 每帧调用
LateUpdate()      ──→ Update之后调用
OnDisable()       ──→ 脚本禁用时调用
OnDestroy()       ──→ 脚本销毁时调用

这篇文章我们重点讲解Start和Update,其他方法在后续文章中会逐一深入。

二、Start方法------初始化入口

2.1 Start的调用时机

💡 Start在脚本实例被启用后,第一次Update之前被调用,且只被调用一次

具体来说,Start的调用条件:

  • GameObjet处于激活状态(activeInHierarchy为true)
  • 脚本组件处于启用状态(enabled为true)
  • Start在这个脚本实例的整个生命周期中只执行一次

2.2 Start的典型用途

Start是初始化的标准位置,适合做以下事情:

获取组件引用

csharp 复制代码
private Rigidbody rb;
private Animator animator;
private AudioSource audioSource;

void Start()
{
    rb = GetComponent<Rigidbody>();
    animator = GetComponent<Animator>();
    audioSource = GetComponent<AudioSource>();
}

设置初始值

csharp 复制代码
void Start()
{
    // 设置初始位置
    transform.position = spawnPoint.position;
    
    // 初始化生命值
    currentHealth = maxHealth;
    
    // 设置初始颜色
    GetComponent<Renderer>().material.color = Color.white;
}

建立初始状态

csharp 复制代码
void Start()
{
    // 暂停游戏,显示菜单
    Time.timeScale = 0;
    
    // 生成初始波次的敌人
    SpawnEnemyWave();
    
    // 播放背景音乐
    bgmAudioSource.Play();
}

2.3 Start vs Awake

Start和Awake都在初始化阶段被调用,且都只调用一次。那么有什么区别呢?

特性 Awake Start
调用时机 脚本实例加载时立即调用 第一次Update之前调用
GameObject激活要求 不要求(即使物体未激活也会调用) 要求物体和脚本都激活
获取自身组件 ✅ 适合 ✅ 适合
获取其他物体的组件 ⚠️ 可能未就绪 ✅ 其他物体已完成Awake
使用Instantiate创建物体 ⚠️ 谨慎 ✅ 更合适

💡 简单来说:Awake负责自身初始化,Start负责关联初始化

csharp 复制代码
// Awake:初始化自身相关
void Awake()
{
    rb = GetComponent<Rigidbody>();
    collider = GetComponent<Collider>();
}

// Start:初始化与其他物体的关联
void Start()
{
    GameObject player = GameObject.FindWithTag("Player");
    if (player != null)
    {
        target = player.transform;
    }
}

2.4 Start是协程

Start方法有一个特殊用法------它可以作为协程的入口:

csharp 复制代码
// 将Start声明为返回IEnumerator的方法,Unity会自动以协程方式执行
IEnumerator Start()
{
    Debug.Log("开始初始化...");
    
    // 等待1秒
    yield return new WaitForSeconds(1f);
    
    Debug.Log("1秒后继续初始化...");
    
    // 等待网络连接
    yield return StartCoroutine(ConnectToServer());
    
    Debug.Log("初始化完成!");
}

⚠️ 注意:如果Start返回IEnumerator,Unity会将其作为协程执行,每次yield return后等待条件满足再继续。但这种方式下Start仍然只执行一次(整个协程执行一次)。

三、Update方法------游戏逻辑的心脏

3.1 Update的调用频率

💡 Update在游戏的每一帧被调用一次。这是Unity中最重要的方法------绝大多数游戏逻辑都在Update中执行。

Update的调用频率取决于游戏的帧率(FPS):

  • 60 FPS时,Update每秒调用60次
  • 30 FPS时,Update每秒调用30次
  • 144 FPS时,Update每秒调用144次

3.2 Update的典型用途

输入检测(高频检查):

csharp 复制代码
void Update()
{
    // 检测按键输入
    if (Input.GetKeyDown(KeyCode.Space))
    {
        Jump();
    }
    
    if (Input.GetMouseButtonDown(0))
    {
        Shoot();
    }
}

持续移动

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

状态检查和更新

csharp 复制代码
void Update()
{
    // 检查距离
    float distance = Vector3.Distance(transform.position, target.position);
    if (distance < attackRange)
    {
        Attack();
    }
    
    // 更新计时器
    cooldownTimer -= Time.deltaTime;
    if (cooldownTimer <= 0)
    {
        canAttack = true;
    }
}

条件触发的逻辑

csharp 复制代码
void Update()
{
    // 玩家血量低于阈值时触发效果
    if (currentHealth < maxHealth * 0.2f)
    {
        ShowLowHealthWarning();
    }
    
    // 掉出地图时重置位置
    if (transform.position.y < -50f)
    {
        Respawn();
    }
}

3.3 Update的性能注意事项

⚠️ Update每帧都执行,因此性能优化至关重要:

不要在Update中做不必要的操作

csharp 复制代码
// ❌ 不好:每帧都调用GetComponent
void Update()
{
    Rigidbody rb = GetComponent<Rigidbody>();
    rb.AddForce(Vector3.up);
}

// ✅ 好:在Start中缓存引用
private Rigidbody rb;
void Start() { rb = GetComponent<Rigidbody>(); }
void Update() { rb.AddForce(Vector3.up); }

避免在Update中做昂贵的查找

csharp 复制代码
// ❌ 不好:每帧都查找GameObject
void Update()
{
    GameObject player = GameObject.Find("Player");
    // ...
}

// ✅ 好:在Start中查找并缓存
private GameObject player;
void Start() { player = GameObject.Find("Player"); }

避免在Update中实例化物体

csharp 复制代码
// ❌ 不好:每帧都创建新对象(造成大量GC)
void Update()
{
    GameObject bullet = Instantiate(bulletPrefab, transform.position, Quaternion.identity);
}

// ✅ 好:按条件创建
void Update()
{
    shootTimer -= Time.deltaTime;
    if (shootTimer <= 0 && Input.GetButton("Fire1"))
    {
        Instantiate(bulletPrefab, transform.position, Quaternion.identity);
        shootTimer = shootInterval;
    }
}

3.4 Time.deltaTime------让运动与帧率无关

由于Update的调用频率随帧率变化,如果直接用固定值做运动计算,高帧率设备上物体会移动得更快。Time.deltaTime就是为了解决这个问题。

csharp 复制代码
// ❌ 帧率相关:60FPS时每秒移动60单位,30FPS时只移动30单位
void Update()
{
    transform.Translate(Vector3.forward * 1f);
}

// ✅ 帧率无关:无论多少FPS,每秒都移动5单位
void Update()
{
    transform.Translate(Vector3.forward * 5f * Time.deltaTime);
}

Time.deltaTime的值为上一帧到当前帧的间隔时间(秒):

  • 60 FPS → deltaTime ≈ 0.0167秒
  • 30 FPS → deltaTime ≈ 0.0333秒
  • 144 FPS → deltaTime ≈ 0.0069秒

四、实战示例------时钟动画

4.1 制作一个模拟时钟

这个综合示例演示了Start和Update的分工:

csharp 复制代码
using UnityEngine;

public class ClockAnimation : MonoBehaviour
{
    public Transform hourHand;    // 时针
    public Transform minuteHand;  // 分针
    public Transform secondHand;  // 秒针
    
    [SerializeField] private float timeSpeedMultiplier = 1f;
    
    private float hours, minutes, seconds;
    
    void Start()
    {
        // Start中设置初始时间(使用系统时间)
        System.DateTime now = System.DateTime.Now;
        hours = now.Hour % 12;
        minutes = now.Minute;
        seconds = now.Second;
        
        Debug.Log($"时钟初始化:{hours}时{minutes}分{seconds}秒");
    }
    
    void Update()
    {
        // Update中更新时间和指针旋转
        seconds += Time.deltaTime * timeSpeedMultiplier;
        
        if (seconds >= 60)
        {
            seconds -= 60;
            minutes++;
        }
        if (minutes >= 60)
        {
            minutes -= 60;
            hours++;
        }
        if (hours >= 12)
        {
            hours -= 12;
        }
        
        // 旋转指针
        float secondAngle = (seconds / 60f) * 360f;
        float minuteAngle = (minutes / 60f + seconds / 3600f) * 360f;
        float hourAngle = (hours / 12f + minutes / 720f) * 360f;
        
        secondHand.localRotation = Quaternion.Euler(0, 0, -secondAngle);
        minuteHand.localRotation = Quaternion.Euler(0, 0, -minuteAngle);
        hourHand.localRotation = Quaternion.Euler(0, 0, -hourAngle);
    }
}

五、常见问题

5.1 Start不执行怎么办

检查以下几点:

  • GameObject是否激活(activeInHierarchy是否为true)
  • 脚本组件是否启用(enabled是否为true)
  • 方法名是否拼写正确(C#区分大小写,必须是"Start"不是"start")
  • 返回值类型是否是void(或IEnumerator用于协程)

5.2 Update频繁调用导致性能问题

  • 检查Update中是否有不必要的重复计算
  • 考虑降低检测频率(比如不需要每帧检测的条件改用协程+WaitForSeconds)
  • 使用对象池替代频繁的Instantiate/Destroy

5.3 Start中获取的组件为null

  • 确认目标组件确实挂载在同一个GameObject上
  • 确认GetComponent的泛型类型参数正确
  • 考虑在Awake而非Start中获取自身组件

六、本篇总结

✅ 本文核心知识点回顾:

  • Start在脚本启用后调用一次,适合初始化(获取组件、设置初始值)
  • Update每帧调用一次,是游戏逻辑的核心驱动力
  • Time.deltaTime让运动计算与帧率无关
  • Awake适合自身初始化,Start适合关联初始化
  • Update中避免GetComponent、GameObject.Find等昂贵操作,应该缓存引用
  • Start可以声明为IEnumerator以协程方式执行

📝 下一篇文章将学习Debug.Log与Console窗口------Unity中的调试输出。

动手任务

① 写一个脚本,在Start中输出物体的初始位置,在Update中输出实时位置

② 对比有Time.deltaTime和无Time.deltaTime时物体的移动差异

③ 尝试将Start改为协程(IEnumerator),在其中使用yield return


关于作者:拥有多年Unity开发经验的游戏开发者,专注Unity系统化教学与最佳实践分享。

相关推荐
zephyr051 小时前
链表实现O(n log n)时间复杂度的排序——归并排序详解
java·数据结构·算法
Zane19941 小时前
JRE 去哪了?一次讲清楚 JDK、JRE、JVM 到底是什么关系?
java·后端
PPPPickup1 小时前
基础知识差缺补漏-面试版持续更新
java·开发语言
深入云栈1 小时前
Nacos 2.x 源码深度解析 (二):通信协议迭代 —— HTTP长轮询到gRPC演进
java
andongni2032 小时前
SpringBoot 入门实验报告
java·spring boot·后端
黄鸭部落格ShiYuYuanFang2 小时前
【分享】软考每日一练与解析-计组 8/12
java·开发语言
XWalnut2 小时前
LeetCode刷题 day37
java·数据结构·算法·leetcode
huaweichenai2 小时前
使用maven搭建sprint-boot
java·maven·sprint
zerozrc02 小时前
Java 方法调用的底层原理
java·后端