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 数据缓存,命中则跳过网络请求。
第三层:内存池与对象复用层
系统核心底层支撑,分为三层独立池体系:
- 数据内存池:托管 Tile 结构化数据(NativeArray 复用)
- ECS 实体池:预创建实体常驻,永不 Destroy
- 纹理缓存池:显存预算管理 + 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实现方案
全程使用 NativeArray、NativeList、全局复用缓冲,禁止 List、Dictionary、string 等托管类型做实时解析。解析中间缓冲区全局唯一,每次解析覆盖复用,不新增内存。
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
- 场景销毁、重连、切页时,全池统一回收,无残留
- 禁止任何动态
new、new GameObject、Destroy、DestroyImmediate
3.4 ECS 组件系统设计
3.4.1 组件定义
按数据职责拆分轻量化组件,使用 DynamicBuffer 和 BlobAsset 存储可变长度数据。
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 初始化阶段
- 程序启动,根据
DeviceTier确定内存预算、视野范围、LOD 级别 - 初始化各图层内存池、ECS 实体池(预创建常驻实体)
- 初始化 LRU 数据缓存、纹理流式管理器
- 预分配 Native 全局缓冲区、双缓冲容器
- 初始化九宫格视野管理器,以车辆初始坐标为中心 Tile
- 启动 ECS System 调度、Burst 编译预热
- 初始化坐标原点,创建
MapOriginOffsetComponent全局实体
4.2 运行时动态加载流程
- 服务端推送周边 Tile 流式数据(含边界顶点数据)
- 网络层接收后存入有序缓冲队列
- MapDataFactory 异步解析二进制流 → 查询 LRU Cache
- 未命中缓存:生成标准化 Native Tile 数据,写入 LRU
TileCalcViewJob计算当前九宫格视野,筛选新增 Tile + LOD 级别TilePrefetchJob根据车速方向前瞻预取- 从对应图层内存池获取 Idle 空闲 ECS 实体
TileDataFillJob异步填充 DynamicBuffer 顶点数据- 邻居 Tile 已加载 →
TileStitchJob缝合边界顶点 - 实体标记为 Active,加入
TileFadeInJob淡入队列 - 渲染合批提交,画面展示
4.3 视野回收复用流程
- 车辆移动,
TileCalcViewJob检测中心 Tile 变更 - 出视野 Tile 标记为 Recycle,
TileRecycleCheckJob写入回收队列 - 帧末主线程批量消费回收队列:
EntityManager.SetEnabled(entity, false)隐藏渲染- 清空 DynamicBuffer 数据
- 状态重置为 Idle
- 实体回收入池
- 对应 LRU 缓存保留结构化数据(等待折返命中)
- 纹理缓存根据 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 并行数量
七、方案核心优势总结
- 极致内存复用:三层池化(数据 / 实体 / 纹理)全复用,零运行时动态分配
- 全异步高性能:Job + Burst 承载所有计算,主线程仅极简调度 + EntityManager 操作
- 智能视野管理:九宫格差值 + 方向预取 + LRU 缓存,加载回收零冗余
- 大范围高精度:原点跟随策略消除 float 精度抖动,支持全国级地图
- 高效渲染:分层合批 + GPU Instancing,大幅降低 DrawCall
- 视觉平滑:LOD Geomorphing + Tile 淡入消除视觉 Pop
- 高稳定性:闭环状态机、容错机制、内存监控、帧超时自动降级
附录: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 |