Unity基础:Destroy与DestroyImmediate------正确销毁游戏物体的方式

大家好,欢迎回到Unity教程系列。上一篇文章我们学习了如何在运行时创建物体(Instantiate),有创建就有销毁。今天我们来学习Unity中销毁物体的两个核心方法------Destroy和DestroyImmediate。
很多新手对这两个方法的区别认识模糊,导致出现一些奇怪的Bug:物体"销毁"了但还在发日志、DestroyImmediate在运行时使用导致问题、销毁父物体后子物体还在访问已销毁的引用......这篇文章会把销毁相关的所有知识点一次性讲清楚。
一、Destroy------标准销毁
1.1 Destroy的基本用法
csharp
// 立即标记销毁(在当前帧结束时实际销毁)
Destroy(gameObject);
// 延迟销毁(在指定秒数后销毁)
Destroy(gameObject, 3f); // 3秒后销毁
// 销毁组件(而非整个GameObject)
Destroy(GetComponent<AudioSource>());
// 销毁其他GameObject
Destroy(otherGameObject);
1.2 Destroy的工作机制
💡 理解Destroy的"延迟执行"很重要:调用Destroy后,物体不会立即被销毁。Unity只是将其标记为"待销毁",实际的销毁发生在当前帧结束时(Update全部执行完毕之后)。
csharp
void Update()
{
Destroy(gameObject);
Debug.Log("这条日志还会输出"); // 物体还没被真正销毁
// 这个物体在当前帧的剩余Update中仍然存在
// 但下一帧就不会再出现了
}
这个设计是合理的------如果Destroy立即销毁物体,可能导致当前帧中其他依赖该物体的逻辑出错。
1.3 延迟销毁的实用场景
csharp
// 子弹:飞行3秒后自动销毁
void SpawnBullet()
{
GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
bullet.GetComponent<Rigidbody>().velocity = firePoint.forward * speed;
Destroy(bullet, 3f);
}
// 爆炸特效:播放完毕后自动销毁
void PlayExplosion(Vector3 position)
{
GameObject effect = Instantiate(explosionPrefab, position, Quaternion.identity);
ParticleSystem ps = effect.GetComponent<ParticleSystem>();
float duration = ps != null ? ps.main.duration : 2f;
Destroy(effect, duration);
}
// 临时音效
void PlayOneShotSound(AudioClip clip, Vector3 position)
{
GameObject audioObj = new GameObject("TempAudio");
audioObj.transform.position = position;
AudioSource source = audioObj.AddComponent<AudioSource>();
source.clip = clip;
source.Play();
Destroy(audioObj, clip.length + 0.1f);
}
1.4 Destroy销毁父物体
csharp
// 销毁父物体时,所有子物体也会被销毁
Destroy(parentGameObject);
// parentGameObject及其所有子物体都被销毁
// 如果想保留子物体,先解除父子关系
void DestroyParentKeepChildren()
{
Transform parent = parentGameObject.transform;
// 将子物体移到根级
while (parent.childCount > 0)
{
parent.GetChild(0).SetParent(null);
}
// 安全销毁父物体
Destroy(parent.gameObject);
}
1.5 销毁后检查物体是否有效
Unity对已销毁的UnityEngine.Object做了特殊处理------它们会被"假null"覆盖。你可以用常规的null检查来验证:
csharp
GameObject obj = new GameObject("Test");
Destroy(obj);
// Unity重载了==运算符,已销毁的Object判断为null
if (obj == null)
{
Debug.Log("物体已被销毁");
}
// 但是使用?.或??时要小心------Unity的对象在底层不是真的null
// 使用 obj != null 来判断是最可靠的方式
if (obj != null)
{
obj.SetActive(true); // 如果已销毁,条件不成立,不会执行
}
二、DestroyImmediate------立即销毁
2.1 DestroyImmediate的基本用法
csharp
// 立即销毁(不等待帧结束)
DestroyImmediate(gameObject);
// 允许销毁资源(Assets)
DestroyImmediate(assetObject, true);
2.2 DestroyImmediate vs Destroy
| 特性 | Destroy | DestroyImmediate |
|---|---|---|
| 销毁时机 | 当前帧结束后 | 立即 |
| 适用场景 | 运行时(Play模式) | 编辑器模式 |
| 安全性 | ✅ 安全 | ⚠️ 需要谨慎 |
| 性能 | 较高(批处理销毁) | 较低(逐个立即销毁) |
2.3 DestroyImmediate的危险性
⚠️ DestroyImmediate会立即销毁物体,可能导致正在执行的代码出现异常:
csharp
// ❌ 危险:在Update中使用DestroyImmediate
void Update()
{
DestroyImmediate(gameObject);
Debug.Log("这条日志可能不会输出"); // 物体已经被销毁,脚本可能也跟着没了
// 后续代码可能因为组件/GameObject被销毁而报NullReferenceException
transform.Translate(Vector3.forward); // 可能已经空了!
}
2.4 DestroyImmediate的正确用途
DestroyImmediate主要用于编辑器脚本中(放在Editor文件夹中的脚本),不用于运行时:
csharp
// 编辑器工具脚本:批量清理场景中的临时物体
#if UNITY_EDITOR
using UnityEditor;
public class SceneCleaner : EditorWindow
{
[MenuItem("Tools/Clean Temporary Objects")]
static void CleanTemporaryObjects()
{
// 在编辑器中使用DestroyImmediate是安全的
GameObject[] tempObjects = GameObject.FindGameObjectsWithTag("Temp");
foreach (GameObject obj in tempObjects)
{
DestroyImmediate(obj);
}
Debug.Log($"清理了 {tempObjects.Length} 个临时物体");
}
}
#endif
💡 在编辑器脚本中使用DestroyImmediate是安全且必要的,因为编辑器中没有"帧"的概念,Destroy不会自动执行。
三、销毁的完整生命周期
3.1 销毁时的生命周期流程
当GameObject被销毁时,以下生命周期方法会被依次调用:
调用 Destroy(gameObject)
↓
当前帧继续执行(物体仍然存在)
↓
当前帧结束
↓
OnDisable() ------ 在所有被销毁物体的所有组件上调用
↓
OnDestroy() ------ 在所有被销毁物体的所有组件上调用
↓
物体从Hierarchy中移除
↓
内存释放(由GC管理)
3.2 OnDisable和OnDestroy在销毁过程中的角色
csharp
public class CleanupExample : MonoBehaviour
{
// 用于验证销毁过程
void OnDisable()
{
Debug.Log($"{gameObject.name} OnDisable------物体即将被禁用");
// 在此清理事件订阅、停止协程等
}
void OnDestroy()
{
Debug.Log($"{gameObject.name} OnDestroy------物体即将被销毁");
// 在此清理持久化资源、从管理器注销等
}
}
📝 注意:OnDisable在物体销毁前被调用,但不一定意味着物体被销毁------可能是物体被临时禁用(SetActive(false))。而OnDestroy明确意味着物体即将被回收。
3.3 在OnDestroy中安全访问外部引用
csharp
public class SafeDestruction : MonoBehaviour
{
public GameManager gameManager;
void OnDestroy()
{
// ⚠️ 必须做null检查!
// 因为GameManager可能在当前物体之前被销毁
if (gameManager != null)
{
gameManager.Unregister(this);
}
// 对于单例模式,使用?.操作符
GameManager.Instance?.Unregister(this);
}
}
四、实用的销毁模式
4.1 自动销毁------超出范围销毁
csharp
public class AutoDestroyer : MonoBehaviour
{
public float lifetime = 5f;
public float minY = -50f; // 掉出地图自动销毁
void Start()
{
// 方式1:固定时间后销毁
Destroy(gameObject, lifetime);
}
void Update()
{
// 方式2:超出边界销毁
if (transform.position.y < minY)
{
Destroy(gameObject);
}
}
}
4.2 碰触销毁------拾取物品
csharp
public class CollectibleItem : MonoBehaviour
{
public int scoreValue = 10;
public GameObject collectEffectPrefab;
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
// 被玩家碰到时
Collect(other.gameObject);
}
}
void Collect(GameObject collector)
{
// 加分数
ScoreManager.Instance?.AddScore(scoreValue);
// 播放收集特效
if (collectEffectPrefab != null)
{
Instantiate(collectEffectPrefab, transform.position, Quaternion.identity);
}
// 销毁自身
Destroy(gameObject);
}
}
4.3 血量归零销毁
csharp
public class DestructibleObject : MonoBehaviour
{
[SerializeField] private float maxHealth = 100f;
private float currentHealth;
public GameObject destructionEffectPrefab;
void Start()
{
currentHealth = maxHealth;
}
public void TakeDamage(float damage)
{
currentHealth -= damage;
if (currentHealth <= 0)
{
Die();
}
}
void Die()
{
// 播放销毁特效(特效独立于被销毁的物体)
if (destructionEffectPrefab != null)
{
Instantiate(destructionEffectPrefab, transform.position, Quaternion.identity);
}
// 通知其他系统
GameEvents.OnObjectDestroyed?.Invoke(gameObject);
// 销毁自身
Destroy(gameObject);
}
}
4.4 场景清理
csharp
public class SceneManager : MonoBehaviour
{
// 清空所有敌人
public void ClearAllEnemies()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
foreach (GameObject enemy in enemies)
{
Destroy(enemy);
}
Debug.Log($"清理了{enemies.Length}个敌人");
}
// 清空指定类型的所有物体
public void ClearByTag(string tag)
{
GameObject[] objects = GameObject.FindGameObjectsWithTag(tag);
foreach (GameObject obj in objects)
{
Destroy(obj);
}
}
}
五、常见问题与排查
5.1 "物体没被销毁"
可能原因:
- 代码中的Destroy是否真的被执行(加Debug.Log确认)
- 是否在错误的GameObject上调用了Destroy(检查调用对象)
- Destroy的延迟时间是否太长
- 物体是否被DontDestroyOnLoad保护
5.2 "MissingReferenceException"
访问了已销毁物体的引用。使用null检查来避免:
csharp
if (targetObject != null)
{
targetObject.transform.position = newPosition; // 安全
}
5.3 场景切换后还有残留物体
检查是否有物体被标记为DontDestroyOnLoad,或者在OnDestroy中做了不正确的操作导致物体没被正常销毁。
六、本篇总结
✅ 本文核心知识点回顾:
- Destroy是运行时的标准销毁方法,在当前帧结束时执行
- Destroy支持延迟销毁(第二个参数)
- DestroyImmediate立即销毁,主要用于编辑器脚本
- 销毁父物体时所有子物体也被销毁
- OnDisable在禁用/销毁前调用,OnDestroy在销毁时调用
- 使用null检查来安全地访问可能已被销毁的物体引用
- 销毁后Instantiate出来的特效要独立于被销毁物体(否则会被一起销毁)
📝 下一篇文章将学习Prefab预制体------Unity中最核心的资源复用机制。
动手任务 :
① 写一个子弹系统(Instantiate+Ddestroy)
② 实现可拾取物品(碰触后销毁+播放特效)
③ 对比Destroy和DestroyImmediate在编辑器中的行为
关于作者:拥有多年Unity开发经验的游戏开发者,专注Unity系统化教学与最佳实践分享。