AgentSkillsCN

voxel-chunk-streaming-logic

根据玩家位置,管理Voxel分块的加载、生成、卸载以及序列化存储。当用户需要进行世界流式传输、分块加载、LOD管理,或为开放世界进行内存管理时,可使用此功能。

SKILL.md
--- frontmatter
name: voxel-chunk-streaming-logic
description: Quản lý việc nạp (load), sinh (generate), hủy (unload) và lưu trữ (serialize) các Chunk voxel dựa trên vị trí người chơi. Dùng khi user yêu cầu streaming world, chunk loading, LOD management, hoặc memory management cho thế giới mở.

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