Voxel Chunk Streaming Logic
Critical Instructions
- •Memory Budget: Luôn giới hạn số chunk loaded. Với voxel 2cm, mỗi chunk 32³ = 32KB data → budget ~512 chunks = 16MB.
- •Async Loading: KHÔNG block main thread. Dùng Job system cho generation, async I/O cho load/save.
- •Distance-Based: Load/unload dựa trên khoảng cách từ player, xử lý theo ring (priority queue).
- •Project Convention: Đặt file vào
Assets/Scripts/Core/Systems/cho systems,Assets/Scripts/Core/Data/cho data structures.
Core Principles
- •Concentric Rings: Chunks gần player ưu tiên cao nhất. Load từ trong ra ngoài, unload từ ngoài vào trong.
- •Budget Cap: Không bao giờ vượt quá memory budget. Unload chunk xa nhất khi cần slot cho chunk mới.
- •Generation Pipeline: Request → Generate (noise job) → Mesh (greedy mesh job) → Active. Mỗi bước async.
- •Persistence: Chunk đã modified (player edit) phải serialize ra disk trước khi unload. Chunk pristine có thể regenerate.
Data Structures
Chunk States
csharp
/// <summary>
/// Trạng thái lifecycle của một Chunk trong streaming system.
/// </summary>
public enum ChunkState : byte
{
Unloaded = 0, // Không có trong bộ nhớ
Queued = 1, // Đang trong hàng đợi chờ generate/load
Generating = 2, // Job sinh terrain đang chạy
Generated = 3, // Data đã sinh xong, chờ mesh
Meshing = 4, // Job greedy mesh đang chạy
Active = 5, // Hoàn chỉnh, đang render
PendingUnload = 6, // Đánh dấu cần unload (cần save nếu dirty)
Saving = 7 // Đang serialize ra disk
}
Chunk Metadata
csharp
using Unity.Entities;
using Unity.Mathematics;
/// <summary>
/// Metadata cho mỗi chunk trong streaming system.
/// Lưu trong NativeParallelHashMap<int3, ChunkMetadata>.
/// </summary>
public struct ChunkMetadata
{
public ChunkState State;
public bool IsDirty; // Player đã modify → cần save trước unload
public byte LODLevel; // 0 = full detail, 1 = half, 2 = quarter
public float DistanceSq; // Khoảng cách² tới player (cập nhật mỗi frame)
public Entity ChunkEntity; // Entity dùng cho rendering (nếu Active)
public int FrameLastUsed; // Frame cuối cùng chunk ở trong view range
}
Streaming Configuration
csharp
/// <summary>
/// Cấu hình streaming. Singleton component.
/// </summary>
public struct StreamingConfig : IComponentData
{
public int LoadRadius; // Bán kính load (tính bằng chunks), vd: 8
public int UnloadRadius; // Bán kính unload (> LoadRadius), vd: 10
public int MaxChunksPerFrame; // Giới hạn chunk xử lý/frame, vd: 4
public int MaxLoadedChunks; // Budget cap, vd: 512
public int ChunkSize; // Kích thước chunk (voxels), vd: 32
public float VoxelSize; // Kích thước voxel (meters), vd: 0.02
public int LOD0Radius; // Bán kính full detail, vd: 4
public int LOD1Radius; // Bán kính half detail, vd: 8
}
Core Systems
1. Chunk Priority System
Tính priority cho mọi chunk dựa trên player position.
csharp
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
/// <summary>
/// Cập nhật khoảng cách chunk-player, xác định chunks cần load/unload.
/// Chạy mỗi frame nhưng chỉ xử lý khi player di chuyển > 1 chunk.
/// </summary>
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup), OrderFirst = true)]
public partial struct ChunkPrioritySystem : ISystem
{
private int3 _lastPlayerChunk;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<StreamingConfig>();
_lastPlayerChunk = new int3(int.MaxValue); // Force first update
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var config = SystemAPI.GetSingleton<StreamingConfig>();
float3 playerPos = GetPlayerPosition(ref state);
int3 playerChunk = WorldToChunk(playerPos, config.ChunkSize, config.VoxelSize);
// Chỉ update khi player di chuyển sang chunk mới
if (math.all(playerChunk == _lastPlayerChunk)) return;
_lastPlayerChunk = playerChunk;
// 1. Tìm chunks cần LOAD (trong LoadRadius, chưa loaded)
// 2. Tìm chunks cần UNLOAD (ngoài UnloadRadius, đang loaded)
// 3. Sort load queue theo distance (gần → ưu tiên cao)
// 4. Enqueue max MaxChunksPerFrame requests
}
private static int3 WorldToChunk(float3 worldPos, int chunkSize, float voxelSize)
{
float chunkWorldSize = chunkSize * voxelSize;
return (int3)math.floor(worldPos / chunkWorldSize);
}
private float3 GetPlayerPosition(ref SystemState state)
{
// Đọc từ Player entity hoặc singleton
return float3.zero; // placeholder
}
}
2. Chunk Generation System
Schedule noise job cho queued chunks.
csharp
/// <summary>
/// Lấy chunk từ load queue, schedule TerrainDensityJob.
/// Giới hạn MaxChunksPerFrame để tránh spike.
/// </summary>
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(ChunkPrioritySystem))]
public partial struct ChunkGenerationSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var config = SystemAPI.GetSingleton<StreamingConfig>();
int chunksThisFrame = 0;
// Dequeue từ priority queue
// Với mỗi chunk:
// 1. Allocate NativeArray<VoxelData> (ChunkSize³)
// 2. Schedule TerrainDensityJob
// 3. Set state = Generating
// 4. chunksThisFrame++
// 5. if chunksThisFrame >= config.MaxChunksPerFrame → break
}
}
3. Chunk Unload System
Giải phóng chunk xa, save nếu dirty.
csharp
/// <summary>
/// Unload chunks ngoài UnloadRadius. Save dirty chunks trước khi giải phóng.
/// </summary>
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(ChunkGenerationSystem))]
public partial struct ChunkUnloadSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Iterate loaded chunks ngoài UnloadRadius
// Nếu IsDirty → serialize, schedule save job, set state = Saving
// Nếu !IsDirty → Dispose voxel data, destroy chunk entity, set state = Unloaded
// Budget enforcement: nếu loaded > MaxLoadedChunks → force unload farthest
}
}
Streaming Pipeline Flow
code
Player moves to new chunk position
│
▼
┌─────────────────┐
│ ChunkPriority │── Compute distances, build load/unload queues
│ System │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌──────────────────┐
│ ChunkGeneration │────▶│ TerrainDensity │ (Burst Job, async)
│ System │ │ Job │
└────────┬────────┘ └──────────────────┘
│ (job complete callback)
▼
┌─────────────────┐ ┌──────────────────┐
│ MeshRebuild │────▶│ GreedyMeshing │ (Burst Job, async)
│ System │ │ Job │
└────────┬────────┘ └──────────────────┘
│
▼
┌─────────────────┐
│ ChunkEntity │── Create Entity + RenderMesh + LocalToWorld
│ Activation │
└─────────────────┘
║ (parallel)
┌─────────────────┐
│ ChunkUnload │── Dispose far chunks, save dirty ones
│ System │
└─────────────────┘
Serialization Pattern
csharp
/// <summary>
/// Serialize chunk voxel data ra byte array cho disk storage.
/// Dùng simple RLE (Run-Length Encoding) để nén chunks có nhiều vùng uniform.
/// </summary>
[BurstCompile]
public struct ChunkSerializeJob : IJob
{
[ReadOnly] public NativeArray<VoxelData> Voxels;
public NativeList<byte> OutputBytes;
public void Execute()
{
// Header: chunk coord (12 bytes) + version (4 bytes)
// Body: RLE encoded VoxelData
// Mỗi run: [count (ushort)] [VoxelData (4 bytes)]
// Chunk đồng nhất (vd: toàn air) → chỉ ~6 bytes
var current = Voxels[0];
ushort count = 1;
for (int i = 1; i < Voxels.Length; i++)
{
var v = Voxels[i];
if (v.MaterialID == current.MaterialID &&
v.State == current.State &&
count < ushort.MaxValue)
{
count++;
}
else
{
WriteRun(current, count);
current = v;
count = 1;
}
}
WriteRun(current, count);
}
private void WriteRun(VoxelData v, ushort count)
{
// Write count as 2 bytes + VoxelData as 4 bytes
OutputBytes.Add((byte)(count & 0xFF));
OutputBytes.Add((byte)(count >> 8));
OutputBytes.Add(v.MaterialID);
OutputBytes.Add(v.State);
OutputBytes.Add(v.Temperature);
OutputBytes.Add(v.Stress);
}
}
Memory Budget Calculator
code
Chunk size: 32³ = 32,768 voxels VoxelData size: 4 bytes Per chunk: 32,768 × 4 = 128 KB (data) + ~64 KB (mesh) ≈ 192 KB Budget examples: ├── 256 chunks → ~48 MB (load radius ~4 chunks = 2.56m xung quanh) ├── 512 chunks → ~96 MB (load radius ~5 chunks = 3.2m) ├── 1024 chunks → ~192 MB (load radius ~6 chunks = 3.84m) └── 2048 chunks → ~384 MB (load radius ~8 chunks = 5.12m) Lưu ý: Với voxel 2cm, 1 chunk = 32 × 0.02 = 0.64m → Load radius 8 chunks = 5.12m view distance (rất ngắn!) → Cần LOD system để mở rộng view distance
Validation Checklist
- • Không block main thread: Tất cả generation/meshing/serialization chạy async
- • Memory budget: Không vượt MaxLoadedChunks
- • Dirty save: Chunk modified bởi player được save trước unload
- • Priority correct: Chunks gần player load trước
- • No duplicate: Không load chunk đã loaded
- • Smooth transition: Không pop-in đột ngột (dùng fade hoặc LOD transition)
- • Edge handling: Chunk ở biên world xử lý đúng (không crash khi neighbor = null)
- • Frame budget: Không schedule quá MaxChunksPerFrame