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系统化教学与最佳实践分享。