You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中使用KD Tree查询邻近实体性能骤降问题排查

问题分析与优化方案

核心性能瓶颈点

  1. KD Tree重建时机错误且存在数据竞争
    你在TickWorldEntities里先启动KD Tree重建Job,再更新实体位置数组m_points。这会导致重建Job使用的是上一帧的旧位置,逻辑完全错误;同时主线程修改m_points时,后台Job正在读取该数组,触发数据竞争,引发额外线程同步开销甚至内存错误。

  2. 同步阻塞Job执行
    query.ScheduleBatch(...).Complete()强制主线程等待查询Job完成,完全浪费了Unity Jobs系统的异步并行优势。当15个实体每个Tick都调用GetEntityInRange时,主线程会被多次阻塞,直接把帧率拉垮。

  3. Native资源管理混乱

    • GetEntityInRange中频繁销毁重建Persistent类型的Native数组,Persistent分配的内存不会被Unity自动回收,频繁操作会产生大量内存碎片和GC开销。
    • m_rangeResults中的RangeQueryResult内部持有Native数组,但你没有在脚本销毁时正确释放这些资源,会导致内存泄漏。
  4. 高频全量重建KD Tree
    每秒25次全量重建KD Tree,这个操作时间复杂度为O(n log n),当实体数量增加时,开销会急剧上升——甚至可能比直接暴力遍历所有实体做范围查询的开销还大。


针对性优化方案

1. 修正KD Tree重建逻辑与时机

  • 先更新位置,再启动重建Job:确保重建时使用最新的实体位置。
  • 避免不必要的重建:只有当实体位置发生变化时才重建,或者降低重建频率(比如每2-3个Tick重建一次),而非每次Tick都重建。
  • 按需等待重建完成:如果必须在本次Tick内查询,要确保重建Job执行完毕后再启动查询Job,尽量避免同步阻塞。

修正后的TickWorldEntities示例:

public void TickWorldEntities() {
    // 第一步:先更新所有实体位置到m_points
    bool positionsChanged = false;
    for (int i = 0; i < entities.Count; i++) {
        Entity entity = entities[i];
        if (entity == null) continue;
        float3 oldPos = m_points[i];
        m_points[i] = entity._transform.position;
        if (!oldPos.Equals(m_points[i])) {
            positionsChanged = true;
        }
        if (entity.ShouldTick) {
            entity.Tick();
        }
    }

    // 只有位置变化时才重建KD Tree
    if (positionsChanged) {
        rebuildKDTree();
        // 若需在本次Tick内查询,等待重建完成(尽量异步处理)
        rebuildHandle.Complete();
    }
}

2. 异步化查询操作,避免主线程阻塞

  • 不在GetEntityInRange中调用.Complete(),而是将查询Job的JobHandle返回给调用者,或在后续Tick中处理查询结果。
  • 批量处理所有实体的查询请求,收集所有需要查询的位置后一次性启动批量查询Job,减少Job调度开销。

3. 优化Native资源管理

  • 将m_queryPositions、m_results、m_rangeResults的创建移到Init方法中,一次性分配足够容量,避免频繁销毁重建。
  • 在脚本的OnDestroy方法中手动释放所有Native资源,包括m_rangeResults内部的子资源:
private void OnDestroy() {
    if (m_points.IsCreated) m_points.Dispose();
    if (m_queryPositions.IsCreated) m_queryPositions.Dispose();
    if (m_results.IsCreated) m_results.Dispose();
    if (m_container != null) m_container.Dispose();
    if (m_rangeResults.IsCreated) {
        foreach (var result in m_rangeResults) {
            result.Dispose();
        }
        m_rangeResults.Dispose();
    }
    if (!rebuildHandle.IsCompleted) {
        rebuildHandle.Complete();
    }
}

4. 替换KD Tree为更适合动态场景的方案

如果实体是高频移动的动态对象,KD Tree的重建开销会非常高,建议换用:

  • 空间划分网格:将场景划分为固定大小的网格,每个实体只查询所在网格及相邻网格内的实体,实现简单且动态场景下性能更稳定。
  • Burst+Jobs优化暴力查询:如果实体数量在1000以内,用Burst编译的Job暴力遍历所有实体做距离判断,实际性能可能比高频重建KD Tree更好。

5. 优化实体管理

  • 及时从entities列表中移除已销毁的实体,避免每次遍历都做null检查和无效操作。
  • 用NativeArray<Entity>替代List<Entity>,配合Jobs系统直接在后台访问,减少主线程开销。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 10:45:37