Unity车机地图Tile流式渲染系统高性能架构方案

Unity车机地图Tile流式渲染系统高性能架构方案


方案架构图

一、方案整体概述

1.1 业务背景

车机地图导航系统采用分Tile块流式推流模式,服务端按地块推送结构化地图数据,包含道路、建筑、河流、绿地、地标等独立图层数据流。客户端需实时解析流式数据、动态生成地图地块、随车辆位置动态加载/卸载Tile,同时严格保证车机端低帧率波动、零内存泄漏、低GC、低CPU占用,适配车机中低端硬件性能。

本方案结合 Unity DOTS/ECS、Job System、Burst 编译、DynamicBuffer 存储、自定义分层内存池、对象复用、静态合批、九宫格Tile视野管理、LRU 缓存、方向预取、LOD 分级、纹理流式管理,构建完整的高性能车机地图渲染系统。

1.2 核心设计目标

  • 高性能流畅度:主线程零卡顿,数据解析、实体构建、视野更新全异步Job化,稳定满帧运行
  • 零内存泄漏:全对象池复用、Native 内存统一生命周期管理、内存定点回收、内存快照监控
  • 低GC开销:杜绝 Update 内 new 对象、字符串拼接、集合扩容,全部使用预分配容器与 Native 容器
  • 高效渲染:图层静态合批、材质合并、ECS 实例化渲染,降低 DrawCall
  • 动态视野管理:基于车辆坐标的九宫格 Tile 加载策略,配合方向预取 + LRU 数据缓存,出视野 Tile 即时回收复用
  • 可配置 LOD 策略:支持 4 级几何 LOD,自动适配车机硬件性能(低/中/高三档),确保各档次流畅体验
  • 大范围坐标精度:原点跟随策略,解决 float 精度在大范围地图下的抖动问题
  • 视觉平滑:Tile 淡入过渡,消除加载 Pop

1.3 核心技术栈

Unity DOTS(ECS 1.0+)、Job System + Burst 编译、DynamicBuffer / BlobAsset 存储、自定义分层内存池、LRU 缓存、九宫格视野管理器、纹理流式管理器


二、整体架构分层设计

系统采用五层分层解耦架构,数据自上而下单向流转,层级职责单一,无交叉依赖。

2.1 五层架构明细

第一层:网络数据流层

负责接收服务端推送的分Tile、分图层二进制流(道路/建筑/河流等独立块数据),包含边界顶点数据 用于跨 Tile 缝合。完成粘包拆包、CRC校验、数据有序队列缓存,不做任何业务逻辑处理,纯数据透传。

核心特性:异步接收、队列缓冲、防抖动、重复包过滤、断连重连数据补偿。

第二层:数据解析工厂层(MapDataFactory)

网络原始二进制数据统一入口,负责将裸流解析为标准化的 Tile 结构化数据,区分图层类型、地块坐标、顶点数据、贴图ID、层级高度、遮挡关系、边界顶点列表

核心设计:无GC解析 ,使用预分配的 NativeArray / 全局复用缓冲区,不生成临时托管对象。解析完成后优先查询 LRU 数据缓存,命中则跳过网络请求。

第三层:内存池与对象复用层

系统核心底层支撑,分为三层独立池体系

  1. 数据内存池:托管 Tile 结构化数据(NativeArray 复用)
  2. ECS 实体池:预创建实体常驻,永不 Destroy
  3. 纹理缓存池:显存预算管理 + LRU 淘汰

所有地图对象全复用、零销毁。

第四层:ECS + Job 业务逻辑层

基于 DOTS 架构,所有地图 Tile、地图要素均以 ECS 实体存在,通过 Job System 多线程处理:

  • 视野计算与预取(TileCalcViewJob + TilePrefetchJob)
  • Tile 数据填充(TileDataFillJob)
  • 跨 Tile 边界缝合(TileStitchJob)
  • 出视野回收检测(TileRecycleCheckJob)
  • LOD 过渡插值(LodGeomorphJob)
  • Tile 淡入效果(TileFadeInJob)

主线程仅做极简调度 + 帧末回收操作,核心计算全部异步 Burst 加速。

第五层:渲染合批展示层

统一管理各图层渲染资源,实现同材质、同层级地图要素静态合批 + ECS 实例化渲染。联动视口剔除(Frustum Culling)与 OverDraw 优化,配合 Tile 回收机制清空废弃渲染数据。

2.2 整体数据流转链路

复制代码
服务端 Tile 推流(含边界顶点数据)
    ↓
网络缓冲队列 → 数据解析工厂 (MapDataFactory)
    ↓
┌─ LRU Cache ───┐  缓存命中 → 直接复用结构化数据
│ (跳过网络请求) │
└──────┬────────┘  缓存未命中 → 正常解析
       ↓
异步 Job 多线程处理:
  1. TileCalcViewJob  (九宫格差值 + LOD 级别分配)
  2. TilePrefetchJob  (方向感知前瞻预取)
  3. TileDataFillJob  (ECS 组件赋值, DynamicBuffer 填充)
  4. TileStitchJob    (跨 Tile 边界顶点焊接)
  5. LodGeomorphJob   (LOD 切换平滑过渡)
  6. TileFadeInJob    (淡入效果)
    ↓
主线程帧末:
  - 从队列消费回收/激活实体 (EntityManager 安全操作)
  - 批量回收出视野 Tile
  - 处理原点跟随偏移
    ↓
渲染合批提交 (GPU Instancing + 静态合批)
    ↓
性能监控器 (PerfMonitor) → 超阈值自动降级
    ↓
车辆位移 → 原点跟随检测 → 下一帧视野刷新 → 循环

三、核心模块详细设计

3.1 网络流式数据模块设计

3.1.1 数据格式规范

服务端以 Tile 块为最小推送单元,单 Tile 内按图层分包推送。每个数据包固定包头:

字段 大小 说明
TileX 4 bytes Tile 列号
TileY 4 bytes Tile 行号
LayerType 1 byte 图层类型(道路/建筑/河流/绿地/地标)
DataLength 4 bytes 本包数据长度
Checksum 4 bytes CRC32 校验
Version 2 bytes 数据版本号
BorderFlag 1 byte 是否包含边界顶点数据
Payload DataLength 实际数据

优势:单块数据独立,支持增量推送、局部更新、单Tile刷新,无需全地图重绘。

3.1.2 网络缓冲策略

  • 双缓冲队列:接收队列、解析队列,分离 IO 线程与解析线程,避免阻塞
  • 丢弃延迟过期包:根据车辆当前坐标时间戳,过滤滞后的旧数据
  • 数据包预排序:按 Tile 距离车辆远近排序,优先加载近处核心地块

3.2 数据工厂模块(MapDataFactory)核心设计

作为数据唯一转换入口,核心目标:网络二进制 → ECS 标准化结构化数据,零GC、零临时分配

3.2.1 核心能力

  • 分图层解析:区分道路、建筑、河流、地标等不同要素,生成对应 ECS 组件数据
  • 数据校验:顶点合法性、坐标范围、数据完整性校验
  • 边界数据提取:从 Payload 中分离四边边界顶点列表供 StitchJob 使用
  • 数据格式化:统一 Tile 坐标、缩放、偏移、层级参数

3.2.2 无GC实现方案

全程使用 NativeArrayNativeList、全局复用缓冲,禁止 ListDictionarystring 等托管类型做实时解析。解析中间缓冲区全局唯一,每次解析覆盖复用,不新增内存。

csharp 复制代码
[BurstCompile]
public static class MapDataFactory
{
    private static NativeArray<byte> _globalParseBuffer;

    public static void Init(int bufferSize)
    {
        _globalParseBuffer = new NativeArray<byte>(bufferSize, Allocator.Persistent);
    }

    public static bool ParseTilePacket(NativeArray<byte> netData, out TileRawData outTileData)
    {
        outTileData = default;
        if (!CheckPacketValid(netData)) return false;

        NativeArray<byte>.Copy(netData, _globalParseBuffer, netData.Length);

        outTileData.TileX = ReadInt(_globalParseBuffer, 4);
        outTileData.TileY = ReadInt(_globalParseBuffer, 8);
        outTileData.LayerType = ReadByte(_globalParseBuffer, 12);
        outTileData.VertexCount = ReadInt(_globalParseBuffer, 16);

        FillVertexData(ref outTileData, _globalParseBuffer);
        FillBorderData(ref outTileData, _globalParseBuffer); // 边界顶点
        return true;
    }

    public static void Dispose()
    {
        if (_globalParseBuffer.IsCreated)
            _globalParseBuffer.Dispose();
    }
}

3.3 分层内存池 + 对象复用系统

摒弃 Unity 原生对象创建销毁逻辑,设计三层分级内存池,分别管理逻辑数据、渲染实体与纹理资源。

3.3.1 内存池分层设计

1)地图数据内存池(逻辑层)

缓存所有 Tile 的结构化逻辑数据:顶点数据、UV 数据、图层属性、坐标信息、边界数据。

  • 池类型:Native 内存池 + LRU 缓存,Burst 兼容,无GC
  • 复用规则:出视野 Tile 逻辑数据标记清除,放回空闲池;新 Tile 直接复写内存
  • 扩容策略:预分配 + 步进扩容,禁止频繁小扩容
2)ECS 实体渲染池(表现层)

预创建足量各图层 ECS 实体(道路/建筑/河流),统一托管复用,永不 Destroy

  • 初始化:启动时根据车机性能预生成最大视野实体数量,常驻内存
  • 复用逻辑:获取空闲实体 → 绑定新 Tile 数据 → 激活渲染 → 出视野解绑数据、隐藏、回池
  • 分层隔离:不同地图要素独立池子,避免类型混乱
3)纹理缓存池(显存层)

独立管理 GPU 纹理资源,基于 LRU 策略控制显存预算。详见 3.7 节。

3.3.2 内存池核心 API

csharp 复制代码
[BurstCompile]
public static class MapTilePool
{
    private static NativeQueue<Entity> _roadIdlePool;
    private static NativeQueue<Entity> _buildIdlePool;
    private static NativeQueue<Entity> _riverIdlePool;

    public static void InitPool(EntityManager entityManager, int preCreateCount)
    {
        _roadIdlePool = new NativeQueue<Entity>(Allocator.Persistent);
        _buildIdlePool = new NativeQueue<Entity>(Allocator.Persistent);
        _riverIdlePool = new NativeQueue<Entity>(Allocator.Persistent);

        for (int i = 0; i < preCreateCount; i++)
        {
            var roadEntity = CreateEmptyTileEntity(entityManager, LayerType.Road);
            entityManager.SetEnabled(roadEntity, false);
            _roadIdlePool.Enqueue(roadEntity);
            // 同理创建 Build / River 实体
        }
    }

    public static Entity GetIdleEntity(LayerType layerType)
    {
        var pool = GetPool(layerType);
        return pool.TryDequeue(out var entity) ? entity : Entity.Null;
    }

    // 注意:此方法仅由主线程调用,不标记 BurstCompile
    public static void ReturnToPool(Entity entity, LayerType layerType)
    {
        GetPool(layerType).Enqueue(entity);
    }

    // 注意:此方法仅由主线程调用,不标记 BurstCompile
    public static void RecycleEntity(EntityManager em, Entity entity, LayerType layerType)
    {
        // 主线程安全操作
        em.SetComponentData(entity, new TileStateComponent
        {
            Status = TileStatus.Idle,
            IsNeedRecycle = false,
            LoadProgress = 0
        });
        em.SetEnabled(entity, false);
        ReturnToPool(entity, layerType);
    }

    public static void DisposePool()
    {
        _roadIdlePool.Dispose();
        _buildIdlePool.Dispose();
        _riverIdlePool.Dispose();
    }
}

3.3.3 零内存泄漏保障机制

  • 所有 Native 内存手动托管,帧末巡检未释放内存,强制回收
  • Tile 状态双向绑定:ECS 实体绑定 Tile 坐标,Tile 坐标反向映射实体 ID
  • 场景销毁、重连、切页时,全池统一回收,无残留
  • 禁止任何动态 newnew GameObjectDestroyDestroyImmediate

3.4 ECS 组件系统设计

3.4.1 组件定义

按数据职责拆分轻量化组件,使用 DynamicBufferBlobAsset 存储可变长度数据。

csharp 复制代码
// ===== 定位组件 =====
[BurstCompile]
public struct TilePosComponent : IComponentData
{
    public int TileX;
    public int TileY;
    public int LayerType;
    public float ViewWeight;
    public float3 WorldPosition; // 双精度映射后的 world pos
}

// ===== 状态组件 =====
[BurstCompile]
public struct TileStateComponent : IComponentData
{
    public TileStatus Status;
    public float LoadProgress;
    public bool IsNeedRecycle;
    public int CurrentLodLevel;
    public bool IsStitched; // 边界是否已缝合
}

// ===== 顶点数据(DynamicBuffer 方案)=====
[BurstCompile]
public struct MeshVertexElement : IBufferElementData
{
    public float3 Position;
    public float2 UV;
}

[BurstCompile]
public struct MeshIndexElement : IBufferElementData
{
    public int Value;
}

// ===== 网格元数据 =====
[BurstCompile]
public struct MapMeshComponent : IComponentData
{
    public int VertexCount;
    public int IndexCount;
    public bool IsRenderValid;
}

// ===== 只读网格数据(BlobAsset 方案,LOD 共享)=====
[BurstCompile]
public struct MeshBlobData
{
    public BlobArray<float3> Positions;
    public BlobArray<float2> UVs;
    public BlobArray<int> Triangles;
}

[BurstCompile]
public struct MapMeshBlobComponent : IComponentData
{
    public BlobAssetReference<MeshBlobData> MeshDataRef;
    public bool IsRenderValid;
}

// ===== 边界数据组件 =====
[BurstCompile]
public struct TileBorderBlobData
{
    public BlobArray<float2> TopBorder;
    public BlobArray<float2> BottomBorder;
    public BlobArray<float2> LeftBorder;
    public BlobArray<float2> RightBorder;
    public BlobArray<int> TopBorderInnerIndices;
    public BlobArray<int> BottomBorderInnerIndices;
    public BlobArray<int> LeftBorderInnerIndices;
    public BlobArray<int> RightBorderInnerIndices;
}

[BurstCompile]
public struct TileBorderComponent : IComponentData
{
    public BlobAssetReference<TileBorderBlobData> BorderDataRef;
}

// ===== LOD 过渡组件 =====
[BurstCompile]
public struct LodTransitionComponent : IComponentData
{
    public int FromLodLevel;
    public int ToLodLevel;
    public float TransitionProgress;
    public bool IsTransitioning;
}

// ===== 淡入组件 =====
[BurstCompile]
public struct TileFadeInComponent : IComponentData
{
    public float FadeTimer;
    public float FadeDuration;
}

// ===== 渲染配置 =====
[BurstCompile]
public struct MapLayerComponent : IComponentData
{
    public int MaterialID;
    public int RenderOrder;
    public bool IsStaticBatch;
}

// ===== 全局坐标偏移 =====
[BurstCompile]
public struct MapOriginOffsetComponent : IComponentData
{
    public float3 Offset;
}

// ===== 状态枚举 =====
public enum TileStatus
{
    Idle,
    Loading,
    Active,
    Recycle
}

3.4.2 数据存储方案选择

场景 推荐方案 原因
Tile 顶点数据频繁更新 DynamicBuffer 可写,复用缓冲区
LOD 多级数据只读 BlobAsset 实体间共享,零拷贝
边界顶点数据 BlobAsset 仅 StitchJob 读取
数据量极小(< 64 bytes) IComponentData 直接内联存储

3.5 核心 Job 系统设计

3.5.1 Job 拆分

将所有耗时操作拆分为独立 Burst Job,并行执行。

复制代码
帧 N 的 Job 依赖链:

TileCalcViewJob (视野计算)
    │
    ├── TilePrefetchJob (方向预取, 依赖 viewJob)
    │
    ├── TileDataFillJob (数据填充, 依赖 viewJob)
    │       │
    │       └── TileStitchJob (边界缝合, 依赖 fillJob)
    │
    └── TileRecycleCheckJob (回收检测, 依赖 viewJob)
    
独立执行的 Job:
    - LodGeomorphJob (LOD 插值)
    - TileFadeInJob (淡入)
    - MapCullingJob (视口剔除)

3.5.2 TileCalcViewJob ------ 九宫格视野计算

csharp 复制代码
[BurstCompile]
public struct TileCalcViewJob : IJob
{
    public float3 CarWorldPos;
    public int CurCenterTileX;
    public int CurCenterTileY;
    public int ViewRange;

    public NativeList<TilePos> NewLoadTileList;
    public NativeList<TilePos> NeedRecycleTileList;

    public void Execute()
    {
        int newCenterX = WorldToTileX(CarWorldPos.x);
        int newCenterY = WorldToTileZ(CarWorldPos.z);

        if (newCenterX != CurCenterTileX || newCenterY != CurCenterTileY)
        {
            for (int x = -ViewRange; x <= ViewRange; x++)
            {
                for (int y = -ViewRange; y <= ViewRange; y++)
                {
                    int targetX = newCenterX + x;
                    int targetY = newCenterY + y;
                    if (!IsTileInOldView(targetX, targetY))
                        NewLoadTileList.Add(new TilePos(targetX, targetY));
                }
            }
            CalcOutBoundsTiles(ref NeedRecycleTileList, newCenterX, newCenterY);
        }
    }

    private int WorldToTileX(float worldX) => (int)(worldX / MapDefine.TileSize);
    private int WorldToTileZ(float worldZ) => (int)(worldZ / MapDefine.TileSize);
    private bool IsTileInOldView(int x, int y) => 略;
    private void CalcOutBoundsTiles(ref NativeList<TilePos> list, int newX, int newY) => 略;
}

3.5.3 TilePrefetchJob ------ 方向感知前瞻预取

根据车速和行驶方向,在基础视野前方预取 Tile:

csharp 复制代码
[BurstCompile]
public struct TilePrefetchJob : IJob
{
    public float3 CarPosition;
    public float3 CarForward;
    public float CarSpeed;
    public int CurrentTileX;
    public int CurrentTileY;
    public int ViewRange;

    public NativeList<TilePos> PrefetchTileList;

    public void Execute()
    {
        float speedKmh = CarSpeed * 3.6f;
        int extraTiles = speedKmh switch
        {
            < 30f => 1,
            < 80f => 2,
            _     => 4
        };

        int2 dir = QuantizeDirection(CarForward);

        // 主方向预取
        for (int i = 1; i <= extraTiles; i++)
        {
            PrefetchTileList.Add(new TilePos(
                CurrentTileX + dir.x * (ViewRange + i),
                CurrentTileY + dir.y * (ViewRange + i)
            ));
        }

        // 侧向预取(应对变道)
        int2 sideDir = new int2(-dir.y, dir.x);
        for (int i = 1; i <= extraTiles; i++)
        {
            PrefetchTileList.Add(new TilePos(
                CurrentTileX + dir.x * (ViewRange + i) + sideDir.x,
                CurrentTileY + dir.y * (ViewRange + i) + sideDir.y
            ));
            PrefetchTileList.Add(new TilePos(
                CurrentTileX + dir.x * (ViewRange + i) - sideDir.x,
                CurrentTileY + dir.y * (ViewRange + i) - sideDir.y
            ));
        }
    }

    private int2 QuantizeDirection(float3 forward) { /* 8 方向量化 */ }
}

3.5.4 TileStitchJob ------ 跨 Tile 边界缝合

csharp 复制代码
[BurstCompile]
public struct TileStitchJob : IJob
{
    public TilePos CurrentTile;
    public TilePos NeighborTile;

    [ReadOnly] public BlobAssetReference<TileBorderBlobData> CurrentBorder;
    [ReadOnly] public BlobAssetReference<TileBorderBlobData> NeighborBorder;

    public BufferAccessor<MeshVertexElement> CurrentVertexBuffer;
    public BufferAccessor<MeshVertexElement> NeighborVertexBuffer;

    public void Execute()
    {
        int dx = NeighborTile.X - CurrentTile.X;
        int dy = NeighborTile.Y - CurrentTile.Y;

        if (dx == 1)
            StitchBorder(
                CurrentBorder.Value.RightBorder, CurrentBorder.Value.RightBorderInnerIndices,
                NeighborBorder.Value.LeftBorder, NeighborBorder.Value.LeftBorderInnerIndices,
                CurrentVertexBuffer, NeighborVertexBuffer
            );
        else if (dx == -1)
            StitchBorder(
                CurrentBorder.Value.LeftBorder, CurrentBorder.Value.LeftBorderInnerIndices,
                NeighborBorder.Value.RightBorder, NeighborBorder.Value.RightBorderInnerIndices,
                CurrentVertexBuffer, NeighborVertexBuffer
            );
        // 同理处理 dy == 1 (上) / dy == -1 (下)
    }

    private void StitchBorder(/* 参数 */)
    {
        for (int i = 0; i < borderA.Length && i < borderB.Length; i++)
        {
            float2 avgPos = (borderA[i] + borderB[i]) * 0.5f;
            var verticesA = CurrentVertexBuffer[0];
            int idxA = innerIdxA[i];
            verticesA[idxA] = new MeshVertexElement
            {
                Position = new float3(avgPos.x, verticesA[idxA].Position.y, avgPos.y),
                UV = verticesA[idxA].UV
            };
            // 同理处理 verticesB
        }
    }
}

3.5.5 Job 调度器 ------ 主线程极简调度

csharp 复制代码
public class MapTileScheduler : MonoBehaviour
{
    private JobHandle _viewJobHandle;
    private JobHandle _fillJobHandle;

    // 双缓冲容器 --- 避免读写竞争
    private NativeList<TilePos> _newLoadTiles_Write;
    private NativeList<TilePos> _recycleTiles_Write;

    // 主线程操作队列
    private NativeQueue<Entity> _recycleQueue;
    private NativeQueue<Entity> _activateQueue;

    private void Start()
    {
        _newLoadTiles_Write = new NativeList<TilePos>(256, Allocator.Persistent);
        _recycleTiles_Write = new NativeList<TilePos>(256, Allocator.Persistent);
        _recycleQueue = new NativeQueue<Entity>(Allocator.Persistent);
        _activateQueue = new NativeQueue<Entity>(Allocator.Persistent);
    }

    private void Update()
    {
        // === 阶段一:Complete 上一帧 Job ===
        _viewJobHandle.Complete();
        _fillJobHandle.Complete();

        // === 阶段二:主线程处理上一帧结果 ===
        BatchRecycleTiles(_recycleTiles_Write);
        BatchActivateTiles();

        _newLoadTiles_Write.Clear();
        _recycleTiles_Write.Clear();

        // === 阶段三:调度当前帧视野 Job ===
        var viewJob = new TileCalcViewJob
        {
            CarWorldPos = CarTransform.position,
            CurCenterTileX = _currentCenterX,
            CurCenterTileY = _currentCenterY,
            ViewRange = MapGlobalConfig.ViewRange,
            NewLoadTileList = _newLoadTiles_Write,
            NeedRecycleTileList = _recycleTiles_Write
        };
        _viewJobHandle = viewJob.Schedule();

        // === 阶段四:调度数据填充 Job(依赖视野)===
        var fillJob = new TileDataFillJob
        {
            NewLoadTiles = _newLoadTiles_Write,
            ActivateQueue = _activateQueue.AsParallelWriter()
        };
        _fillJobHandle = fillJob.Schedule(_viewJobHandle);

        // === 阶段五:处理回收队列 ===
        ProcessRecycleQueue();
    }

    private void BatchRecycleTiles(NativeList<TilePos> recycleList)
    {
        var em = World.DefaultGameObjectInjectionWorld.EntityManager;
        for (int i = 0; i < recycleList.Length; i++)
        {
            var entity = TileMapping.GetEntity(recycleList[i]);
            if (entity == Entity.Null) continue;
            em.SetComponentData(entity, new TileStateComponent
            {
                Status = TileStatus.Idle,
                IsNeedRecycle = false,
                LoadProgress = 0
            });
            em.SetEnabled(entity, false);
        }
    }

    private void OnDestroy()
    {
        JobHandle.CompleteAll(ref _viewJobHandle, ref _fillJobHandle);
        _newLoadTiles_Write.Dispose();
        _recycleTiles_Write.Dispose();
        _recycleQueue.Dispose();
        _activateQueue.Dispose();
    }
}

3.6 九宫格 Tile 视野管理系统

以车辆当前所在 Tile 为中心Tile,维护 3×3 九宫格视野区域。

3.6.1 核心规则

  • 视野范围:默认 3×3,根据车机配置可扩展为 5×5 或 7×7
  • 车辆移动检测:每帧 Job 异步计算车辆世界坐标对应的 Tile 坐标,对比上一帧
  • 视野变更判定:中心 Tile 切换时触发差值加载/回收

3.6.2 Tile 状态机

所有 Tile 统一四种状态,严格状态流转:

复制代码
Idle(空闲态)→ Loading(加载态)→ Active(激活态)→ Recycle(回收态)→ Idle
状态 说明 关联操作
Idle 在内存池内,未挂载数据 等待被获取
Loading 正在填充 ECS 数据 TileDataFillJob
Active 在视野内,正常渲染 渲染合批 + LOD 管理
Recycle 出视野,等待回收 帧末统一回池

3.6.3 增量加载与回收策略

  • 差值计算:新旧九宫格做差集,仅加载新增 Tile、仅回收退出 Tile
  • 远近优先级:中心 Tile 优先,边缘延后
  • 延迟回收:出视野延迟 1 帧回收,避免小幅抖动造成闪烁
  • 批量回收:帧末批量处理,避免单帧多次内存操作

3.7 LOD 系统

3.7.1 LOD 层级定义

LOD 顶点数比例 贴图分辨率 适用距离 适用设备
LOD0 100% 1024×1024 0~50m(中心Tile) 所有
LOD1 50% 512×512 50~150m(内圈) 中高端
LOD2 20% 256×256 150~300m(外圈) 所有
LOD3 5% 平面 128×128 300m+(扩展范围) 仅低端

3.7.2 LOD 与九宫格映射

csharp 复制代码
public static int GetLodLevel(int tileX, int tileY, int centerX, int centerY, int viewRange)
{
    int maxDist = math.max(math.abs(tileX - centerX), math.abs(tileY - centerY));
    if (maxDist == 0) return 0;
    if (maxDist <= 1) return 1;
    if (maxDist <= viewRange) return 2;
    return 3;
}

3.7.3 LOD Geomorphing 平滑过渡

csharp 复制代码
[BurstCompile]
public struct LodGeomorphJob : IJobChunk
{
    public BufferTypeHandle<MeshVertexElement> VertexBufferHandle;
    [ReadOnly] public ComponentTypeHandle<LodTransitionComponent> TransitionHandle;
    [ReadOnly] public BlobAssetReference<MeshBlobData> FromMesh;
    [ReadOnly] public BlobAssetReference<MeshBlobData> ToMesh;

    public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
    {
        var vertexBuffer = chunk.GetBufferAccessor(ref VertexBufferHandle);
        var transitions = chunk.GetNativeArray(ref TransitionHandle);

        for (int i = 0; i < chunk.Count; i++)
        {
            if (!transitions[i].IsTransitioning) continue;

            float t = transitions[i].TransitionProgress;
            var vertices = vertexBuffer[i];

            for (int v = 0; v < vertices.Length; v++)
            {
                float3 fromPos = FromMesh.Value.Positions[v];
                float3 toPos = ToMesh.Value.Positions[v];
                vertices[v] = new MeshVertexElement
                {
                    Position = math.lerp(fromPos, toPos, t),
                    UV = vertices[v].UV
                };
            }
        }
    }
}

3.7.4 设备自适应

csharp 复制代码
public enum DeviceTier { Low, Mid, High }

public static class LodAdaptiveConfig
{
    public static DeviceTier DetectDeviceTier()
    {
        long totalMem = UnityEngine.Profiling.Profiler.GetTotalSystemMemorySize();
        if (totalMem < 3L * 1024 * 1024 * 1024) return DeviceTier.Low;
        if (totalMem < 6L * 1024 * 1024 * 1024) return DeviceTier.Mid;
        return DeviceTier.High;
    }

    public static int GetViewRange(DeviceTier tier) => tier switch
    {
        DeviceTier.Low  => 1,   // 3×3
        DeviceTier.Mid  => 2,   // 5×5
        DeviceTier.High => 3,   // 7×7
    };

    public static int GetMaxLodLevel(DeviceTier tier) => tier switch
    {
        DeviceTier.Low  => 3,
        DeviceTier.Mid  => 2,
        DeviceTier.High => 1,
    };
}

3.8 纹理流式管理系统

3.8.1 纹理分层

层级 内容 管理策略
底图纹理 卫星图/道路底色 按 Tile 流式加载,池化复用
细节纹理 路面标线、路口箭头 全局复用,非 Tile 独占
装饰纹理 建筑立面、地标 LRU 缓存

3.8.2 纹理复用策略

要素类型 纹理策略 复用范围
道路面 过程化生成(UV 渐变) 全地图无纹理
道路标线 小图 Repeat 全局 4~8 张
路口/转弯 路口图集(Atlas) 按类型索引
水面 法线贴图 全局 2~4 张
建筑 立面图集 LRU 缓存

3.8.3 显存预算管理

csharp 复制代码
public class TextureStreamingManager : IDisposable
{
    private readonly long _budgetBytes;
    private long _currentUsage;
    private readonly Dictionary<TilePos, Texture2D> _cache;
    private readonly LinkedList<TilePos> _lruList;

    public TextureStreamingManager(long budgetMB)
    {
        _budgetBytes = budgetMB * 1024 * 1024;
        _cache = new Dictionary<TilePos, Texture2D>(256);
        _lruList = new LinkedList<TilePos>();
    }

    public Texture2D RequestTexture(TilePos pos, byte[] data)
    {
        if (_cache.TryGetValue(pos, out var tex))
        {
            _lruList.Remove(pos);
            _lruList.AddLast(pos);
            return tex;
        }

        tex = new Texture2D(256, 256, TextureFormat.ASTC_6x6, false);
        tex.LoadRawTextureData(data);
        tex.Apply(false, true);

        long size = tex.GetRawTextureData().Length;
        while (_currentUsage + size > _budgetBytes && _lruList.Count > 0)
        {
            var oldest = _lruList.First.Value;
            _lruList.RemoveFirst();
            _currentUsage -= _cache[oldest].GetRawTextureData().Length;
            Object.Destroy(_cache[oldest]);
            _cache.Remove(oldest);
        }

        _cache[pos] = tex;
        _lruList.AddLast(pos);
        _currentUsage += size;
        return tex;
    }

    public void Dispose()
    {
        foreach (var tex in _cache.Values) Object.Destroy(tex);
        _cache.Clear();
        _lruList.Clear();
    }
}

3.9 LRU 数据缓存层

车辆折返/绕圈时,避免已加载过的 Tile 重新下载解析。

csharp 复制代码
public class TileDataLruCache : IDisposable
{
    private readonly int _maxCapacity;
    private readonly Dictionary<TilePos, LinkedListNode<CachedTileData>> _map;
    private readonly LinkedList<CachedTileData> _lruList;

    private int _hitCount;
    private int _missCount;
    public float HitRate => (_hitCount + _missCount) > 0
        ? (float)_hitCount / (_hitCount + _missCount) : 0f;

    public bool TryGet(TilePos pos, out TileRawData data)
    {
        if (_map.TryGetValue(pos, out var node))
        {
            _lruList.Remove(node);
            _lruList.AddLast(node);
            data = node.Value.Data;
            _hitCount++;
            return true;
        }
        _missCount++;
        data = default;
        return false;
    }

    public void Add(TilePos pos, TileRawData data)
    {
        if (_map.ContainsKey(pos)) return;

        while (_map.Count >= _maxCapacity)
        {
            var oldest = _lruList.First;
            _map.Remove(oldest.Value.Position);
            oldest.Value.Data.Dispose();
            _lruList.RemoveFirst();
        }

        var node = _lruList.AddLast(new CachedTileData(pos, data));
        _map[pos] = node;
    }

    public void Dispose()
    {
        foreach (var kv in _map) kv.Value.Value.Data.Dispose();
        _map.Clear();
        _lruList.Clear();
    }
}
设备档次 LRU 容量
Low 16 Tile
Mid 32 Tile
High 64 Tile

3.10 坐标精度与原点跟随

3.10.1 问题

地图覆盖城市 ~ 全国范围(50~1000km),float 在 10km 外精度误差开始可见,100km 外顶点抖动明显。

3.10.2 原点跟随策略

始终以车辆位置为世界坐标系原点,车辆移动超过阈值后整体偏移:

csharp 复制代码
public class MapCoordinateSystem
{
    public double2 WorldOrigin { get; private set; }
    public double2 CarRealPosition { get; private set; }

    public float3 CarRenderPosition => new float3(
        (float)(CarRealPosition.x - WorldOrigin.x),
        0f,
        (float)(CarRealPosition.y - WorldOrigin.y)
    );

    public void Update(double2 carRealPos, float thresholdMeters = 500f)
    {
        CarRealPosition = carRealPos;
        double2 offset = CarRealPosition - WorldOrigin;

        if (math.length(offset) > thresholdMeters)
            RelocateOrigin(CarRealPosition);
    }

    private void RelocateOrigin(double2 newOrigin)
    {
        double2 delta = newOrigin - WorldOrigin;
        WorldOrigin = newOrigin;

        // 更新 ECS 全局偏移组件
        var offsetEntity = World.DefaultGameObjectInjectionWorld
            .EntityManager.CreateEntityQuery(typeof(MapOriginOffsetComponent))
            .GetSingletonEntity();
        World.DefaultGameObjectInjectionWorld.EntityManager
            .SetComponentData(offsetEntity, new MapOriginOffsetComponent
            {
                Offset = new float3((float)delta.x, 0, (float)delta.y)
            });
    }
}

ECS 侧所有 Tile 的位置在渲染时减去全局偏移:

csharp 复制代码
[BurstCompile]
public struct TileWorldPosJob : IJobChunk
{
    [ReadOnly] public ComponentTypeHandle<TilePosComponent> TilePosHandle;
    public ComponentTypeHandle<LocalToWorld> LocalToWorldHandle;
    [ReadOnly] public ComponentDataFromEntity<MapOriginOffsetComponent> OriginOffset;

    public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
    {
        var tilePositions = chunk.GetNativeArray(ref TilePosHandle);
        var localToWorld = chunk.GetNativeArray(ref LocalToWorldHandle);
        float3 offset = OriginOffset[Entity.Null].Offset;

        for (int i = 0; i < chunk.Count; i++)
        {
            float3 renderPos = tilePositions[i].WorldPosition - offset;
            localToWorld[i] = new LocalToWorld { Value = float4x4.Translate(renderPos) };
        }
    }
}

原点跟随 500m 重定位一次,此范围内 float 精度误差 < 0.06mm。

3.11 渲染合批优化方案

3.11.1 图层合批规则

  • 同图层、同材质、同 Shader 的 Tile 要素统一合批渲染
  • 道路、河流等大面积动态图层使用 ECS GPU Instancing
  • 建筑、地标等静态要素使用静态合批预缓存

3.11.2 渲染复用机制

回收的 Tile 实体仅隐藏、清空网格数据,渲染组件、材质、Shader 实例不销毁。新 Tile 直接复用渲染资源。

3.11.3 层级遮挡优化

通过 ECS 组件标记渲染层级,自动处理道路、建筑、河流的遮挡关系,关闭无效透明渲染,降低 OverDraw。

3.12 Tile 加载过渡效果

新增 Tile 使用淡入效果,消除视觉 Pop:

csharp 复制代码
[BurstCompile]
public struct TileFadeInJob : IJobChunk
{
    public ComponentTypeHandle<TileFadeInComponent> FadeHandle;
    public ComponentTypeHandle<TileStateComponent> StateHandle;
    public float DeltaTime;

    public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
    {
        var fades = chunk.GetNativeArray(ref FadeHandle);
        var states = chunk.GetNativeArray(ref StateHandle);

        for (int i = 0; i < chunk.Count; i++)
        {
            if (states[i].Status != TileStatus.Active) continue;
            var fade = fades[i];
            if (fade.FadeTimer >= fade.FadeDuration) continue;

            fade.FadeTimer += DeltaTime;
            fades[i] = fade;

            float t = math.min(fade.FadeTimer / fade.FadeDuration, 1f);
            float alpha = t * t; // ease-in

            // 通过 MaterialPropertyBlock 或 ECS Renderer 设置透明度
            SetTileAlpha(states[i].CurrentLodLevel, alpha);
        }
    }
}
策略 效果 开销 推荐场景
淡入 0.2s 平滑出现 极低 所有设备默认
低 LOD 上采样 先低清后高清 中等 高刷场景
无过渡 生硬 Pop 低端降级

四、完整数据与实体流转流程

4.1 初始化阶段

  1. 程序启动,根据 DeviceTier 确定内存预算、视野范围、LOD 级别
  2. 初始化各图层内存池、ECS 实体池(预创建常驻实体)
  3. 初始化 LRU 数据缓存、纹理流式管理器
  4. 预分配 Native 全局缓冲区、双缓冲容器
  5. 初始化九宫格视野管理器,以车辆初始坐标为中心 Tile
  6. 启动 ECS System 调度、Burst 编译预热
  7. 初始化坐标原点,创建 MapOriginOffsetComponent 全局实体

4.2 运行时动态加载流程

  1. 服务端推送周边 Tile 流式数据(含边界顶点数据)
  2. 网络层接收后存入有序缓冲队列
  3. MapDataFactory 异步解析二进制流 → 查询 LRU Cache
  4. 未命中缓存:生成标准化 Native Tile 数据,写入 LRU
  5. TileCalcViewJob 计算当前九宫格视野,筛选新增 Tile + LOD 级别
  6. TilePrefetchJob 根据车速方向前瞻预取
  7. 从对应图层内存池获取 Idle 空闲 ECS 实体
  8. TileDataFillJob 异步填充 DynamicBuffer 顶点数据
  9. 邻居 Tile 已加载 → TileStitchJob 缝合边界顶点
  10. 实体标记为 Active,加入 TileFadeInJob 淡入队列
  11. 渲染合批提交,画面展示

4.3 视野回收复用流程

  1. 车辆移动,TileCalcViewJob 检测中心 Tile 变更
  2. 出视野 Tile 标记为 Recycle,TileRecycleCheckJob 写入回收队列
  3. 帧末主线程批量消费回收队列:
    • EntityManager.SetEnabled(entity, false) 隐藏渲染
    • 清空 DynamicBuffer 数据
    • 状态重置为 Idle
    • 实体回收入池
  4. 对应 LRU 缓存保留结构化数据(等待折返命中)
  5. 纹理缓存根据 LRU 策略决定是否保留

五、内存预算与性能基线

5.1 内存预算模型

单 Tile 数据估算

数据项 大小
顶点位置 (float3 × 2000) 24 KB
UV (float2 × 2000) 16 KB
索引 (int × 6000) 24 KB
边界顶点 (200) 4 KB
LOD0 小计 ~68 KB
LOD1 (50%) ~34 KB
LOD2 (20%) ~14 KB
全 LOD ~116 KB

三档配置

配置 活跃 Tile LRU 容量 纹理预算 目标 DrawCall
Low 9 (3×3) 16 128 MB < 200
Mid 25 (5×5) 32 256 MB < 500
High 49 (7×7) 64 512 MB < 1000

5.2 性能基线指标

帧率目标

指标 Low Mid High
目标帧率 30 FPS 30 FPS 60 FPS
帧预算 33ms 33ms 16ms
主线程 < 5ms < 5ms < 3ms
Job < 15ms < 15ms < 8ms
GPU < 13ms < 13ms < 5ms

每帧加载上限

  • Low: 1 Tile/帧 (LOD3)
  • Mid: 2 Tile/帧 (LOD1~2)
  • High: 3 Tile/帧 (LOD0~1)

监控与自动降级

csharp 复制代码
public struct FrameMetrics
{
    public float MainThreadMs;
    public float JobThreadMs;
    public float GpuMs;
    public int DrawCallCount;
    public int ActiveTileCount;
    public int LoadingTileCount;
    public long SystemMemoryMB;
    public long TextureMemoryMB;
    public float LruHitRate;
}

public class PerformanceMonitor : IDisposable
{
    private RingBuffer<FrameMetrics> _history;
    private float _alertThreshold;

    public void RecordFrame(FrameMetrics metrics)
    {
        _history.PushBack(metrics);

        if (metrics.MainThreadMs + metrics.JobThreadMs > _alertThreshold)
        {
            TriggerAdaptiveDegradation();
        }
    }

    private void TriggerAdaptiveDegradation()
    {
        // 自动降低视野范围或 LOD 级别
        // 直到帧时间回到安全范围
    }
}

六、异常容错与兼容策略

  • 网络异常:断连时暂停加载,保留当前视野地图,重连后增量更新
  • 数据异常:脏数据、残缺数据自动过滤,不占用实体池
  • 内存峰值:超出阈值自动收紧池子,优先回收边缘闲置 Tile,降低 LRU 容量
  • 帧超时:PerfMonitor 检测帧超时后触发自适应降级(降低视野 / 提升 LOD 等级)
  • 低端车机兼容:动态适配视野范围、LOD 级别、Job 并行数量

七、方案核心优势总结

  1. 极致内存复用:三层池化(数据 / 实体 / 纹理)全复用,零运行时动态分配
  2. 全异步高性能:Job + Burst 承载所有计算,主线程仅极简调度 + EntityManager 操作
  3. 智能视野管理:九宫格差值 + 方向预取 + LRU 缓存,加载回收零冗余
  4. 大范围高精度:原点跟随策略消除 float 精度抖动,支持全国级地图
  5. 高效渲染:分层合批 + GPU Instancing,大幅降低 DrawCall
  6. 视觉平滑:LOD Geomorphing + Tile 淡入消除视觉 Pop
  7. 高稳定性:闭环状态机、容错机制、内存监控、帧超时自动降级

附录:DOTS 最佳实践速查

需求 正确做法 错误做法
可变长度数据 DynamicBuffer<T> NativeArray in IComponentData
只读共享数据 BlobAssetReference<T> 每实体一份完整拷贝
Job 中安全写入 NativeQueue.ParallelWriter / ECB EntityManager(主线程 only)
Job 依赖管理 JobHandle.Schedule(dependency) 手动 Complete / Wait
跨帧读写 双缓冲容器 单容器读写不隔离
Burst 兼容数据 NativeArray / 纯 struct / float3 List / Dictionary / string
ECS 实体隐藏 SetEnabled(entity, false) DestroyEntity(应永不销毁)
渲染数据更新 DynamicBuffer 覆写 重新创建 Entity
相关推荐
混凝土拌意大利面2 小时前
全球新型均衡经济治理体系:先进架构、发展历程预测、核心阻碍全解析
架构
&Hello Code3 小时前
2.一套可落地的 STM32 六层架构:基础工程框架搭建
stm32·嵌入式硬件·架构
红尘伴蝶舞3 小时前
H5游戏多进程隔离:如何解决Android内存激增与进程冻结问题
架构·app·android studio
BigGayGod3 小时前
IR + VM:让 LLM 生成的控制逻辑安全运行在超低端 MCU 上
架构
张忠琳3 小时前
【NVIDIA】NVIDIA k8s-device-plugin v0.19.3 资源管理器模块深度分析之三
云原生·容器·架构·kubernetes·nvidia
小小工匠4 小时前
Skill - 把无限画布装进 Codex:Cowart 的架构拆解与实践指南
架构·cowart
roman_日积跬步-终至千里4 小时前
【从零开始学架构】DDIA 精要:一套关于“数据系统架构”的分层理论
架构
亲爱的马哥4 小时前
Vue3 + Element Plus 低代码表单设计器架构拆解与私有化落地实践
低代码·架构·敏捷流程
LONGZETECH5 小时前
工业实训仿真设计实践:电机拆装软件的 DAG 流程建模、工具精度分级与数据体系搭建
大数据·算法·unity·架构·汽车