You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Unity中C# Dictionary移除元素失败的异常问题求助

排查Unity字典移除失败且突然为空的问题

我来帮你捋捋这个诡异的问题——明明初始字典里有Key,移除失败就算了,之后还直接变空,结合你说换string键测试正常的情况,大概率是Unity生命周期调用时机冲突或者管理类实例不唯一导致的,咱们一步步拆解排查:

核心排查点

1. 确认你操作的是不是同一个字典实例

这是最常见的坑:如果你的WaveControler在场景中有多个实例,或者在敌人销毁前已经被重新创建/销毁,那么你调用EnemyDied时访问的remainingMobs可能已经是一个新的空字典了,而非最初存储敌人的那个。

验证方法
WaveControlerAwakeOnDestroy方法中加日志,同时在EnemyDied开头打印当前字典的哈希码(用来区分实例):

private void Awake()
{
    print($"WaveController instance created, dict hash: {remainingMobs.GetHashCode()}");
}

private void OnDestroy()
{
    print($"WaveController instance destroyed, dict hash: {remainingMobs.GetHashCode()}");
}

public void EnemyDied(int someID) {
    print($"EnemyDied called, current dict hash: {remainingMobs.GetHashCode()}, count: {remainingMobs.Count}");
    // 原有的移除逻辑...
}

如果敌人销毁时打印的字典哈希码和最初添加敌人时的不一致,说明你操作的是不同的字典实例。

2. 检查MobID是否被意外修改

虽然你说MobID是自增int,但如果敌人对象在生命周期中,MobID被其他代码意外修改(比如赋值错误、序列化问题),那么调用EnemyDied时传入的ID就不是当初添加到字典的Key了。

验证方法
MobID改成只读属性,避免被意外修改,同时在添加和移除时打印ID值:

// EnemyControler中
public int MobID { get; private set; }

// 生成敌人时赋值
public void Init(int mobId)
{
    MobID = mobId;
    print($"Enemy initialized with ID: {MobID}");
}

// WaveControler添加时
remainingMobs.Add(newEnemy.MobID, newEnemy);
print($"Added enemy ID: {newEnemy.MobID}, dict count: {remainingMobs.Count}");

3. OnDestroy的调用时机陷阱

Unity中OnDestroy的调用顺序是不确定的:如果WaveControler和敌人对象同时被销毁(比如场景切换、批量销毁),WaveControler可能先被销毁,此时它的remainingMobs虽然是引用类型,但后续敌人调用EnemyDied时,访问的是已经被标记为销毁的WaveControler实例,可能出现不可预期的行为(比如字典被隐式清空)。

解决方案

方案1:确保WaveControler是唯一单例

用单例模式保证全局只有一个WaveControler实例,避免操作错误的字典:

public class WaveControler : MonoBehaviour
{
    public static WaveControler Instance { get; private set; }
    private Dictionary<int, EnemyControler> remainingMobs = new Dictionary<int, EnemyControler>();

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        // 如果跨场景保留,加上下面这句
        DontDestroyOnLoad(gameObject);
    }

    // 其他代码...
}

然后敌人调用时直接用单例:

// EnemyControler的OnDestroy中
WaveControler.Instance.EnemyDied(MobID);

方案2:提前调用移除逻辑,避免依赖OnDestroy

不要等到OnDestroy才调用移除,而是在敌人死亡的逻辑中主动调用,再销毁对象:

// EnemyControler的死亡处理方法
public void OnEnemyKilled()
{
    WaveControler.Instance.EnemyDied(MobID);
    Destroy(gameObject);
}

这样能确保在对象销毁前,字典的移除操作已经完成,不会受生命周期顺序影响。

方案3:加线程安全锁(可选)

如果你的项目中有非主线程操作字典的情况(比如后台加载敌人),需要加锁避免并发修改:

private readonly object _dictLock = new object();

// 添加元素时
lock(_dictLock)
{
    remainingMobs.Add(newEnemy.MobID, newEnemy);
}

// 移除元素时
public void EnemyDied(int someID) {
    lock(_dictLock)
    {
        if(remainingMobs.Remove(someID)) {
            print("removed "+someID);
        } else {
            print("Failed to remove " + someID);
            DisplayRemainingMobs();
        }
    }
}

总结

你遇到的情况最可能是操作了错误的字典实例或者OnDestroy时机晚于WaveControler销毁,先通过日志验证字典实例的一致性,再用单例+提前调用移除的方式解决问题。

内容的提问来源于stack exchange,提问作者Weird

火山引擎 最新活动