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

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

相关推荐
sunxfancy1 小时前
EVEngine 0.6 到 0.6.1:让 AI 参与游戏开发,还需要补上哪些环节?
游戏
迪菲赫尔曼2 小时前
【基础篇 02】文件、文件夹和路径:先把学习资料放对地方
yolo·计算机视觉·论文·教程·ultralyt
sunxfancy2 小时前
AI 改完场景,怎么知道没改坏?EVEngine 0.6.1 的白盒自动试玩
游戏
风无雨4 小时前
三角洲如何快速拾取物品
游戏
lllhhh_远方6 小时前
unity中URP场景阴影锯齿严重解决方案
unity·游戏引擎
老王的笔记v7 小时前
71期 免费开源游戏资源管理工具 Pavise模式切换 游戏库 程序白名单与系统体检
游戏·电脑
淡海水10 小时前
07-06-源码-.NET Core GC的极致优化技术
unity·架构·c#·.netcore·gc·1024程序员节
野区捕龙为宠10 小时前
Unity 中使用 UnityWebRequest 加载 StreamingAssets 文件夹中的图片
unity·游戏引擎
Escalating_xu10 小时前
【C语言项目实战】贪吃蛇(下):移动、碰撞与计分,完成一局可玩的游戏
c语言·开发语言·游戏