testing building some core systems
- serialization - chunks - parralelfor test
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
using MoonTools.ECS;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Nerfed.Runtime.Components;
|
||||
|
||||
namespace Nerfed.Runtime.Scene.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Status of a chunk in the streaming system.
|
||||
/// </summary>
|
||||
public enum ChunkState
|
||||
{
|
||||
Unloaded,
|
||||
Loading,
|
||||
Loaded,
|
||||
Unloading
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A system that manages spatial partitioning. It determines which chunks should be loaded based on observers.
|
||||
/// </summary>
|
||||
public class ChunkStreamingSystem : MoonTools.ECS.System
|
||||
{
|
||||
private readonly struct ChunkCoord : IEquatable<ChunkCoord>
|
||||
{
|
||||
public readonly int X;
|
||||
public readonly int Y;
|
||||
public readonly int Z;
|
||||
|
||||
// Pre-calculated on creation
|
||||
public readonly long Id;
|
||||
|
||||
public ChunkCoord(int x, int y, int z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
|
||||
// We allocate 21 bits per axis (allowing ~2 million chunks positive and negative).
|
||||
var hashX = (long)x & 0x1FFFFF;
|
||||
var hashY = (long)y & 0x1FFFFF;
|
||||
var hashZ = (long)z & 0x1FFFFF;
|
||||
|
||||
Id = hashX | (hashY << 21) | (hashZ << 42);
|
||||
}
|
||||
|
||||
public bool Equals(ChunkCoord other) => Id == other.Id;
|
||||
public override bool Equals(object? obj) => obj is ChunkCoord other && Equals(other);
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
}
|
||||
|
||||
// Configurable size of a chunk in world coordinates.
|
||||
public float ChunkSize { get; set; } = 64f;
|
||||
|
||||
private readonly Filter _observerFilter;
|
||||
private readonly Filter _chunkMemberFilter;
|
||||
private readonly Filter _unloadedFilter;
|
||||
|
||||
// Active loaded/loading chunks
|
||||
private readonly Dictionary<ChunkCoord, ChunkState> _activeChunks = new();
|
||||
|
||||
// Queue of chunks waiting to be loaded
|
||||
private readonly Queue<ChunkCoord> _pendingLoads = new();
|
||||
|
||||
// Queue of chunks waiting to be completely tagged for unloading
|
||||
private readonly Queue<ChunkCoord> _pendingUnloads = new();
|
||||
|
||||
public ChunkStreamingSystem(World world) : base(world)
|
||||
{
|
||||
_observerFilter = FilterBuilder
|
||||
.Include<ChunkObserverComponent>()
|
||||
.Include<LocalToWorld>() // Needs a world position
|
||||
.Exclude<ChunkUnloadPendingTag>() // Ignore dying observers
|
||||
.Build();
|
||||
|
||||
_chunkMemberFilter = FilterBuilder
|
||||
.Include<ChunkMemberComponent>()
|
||||
.Exclude<ChunkUnloadPendingTag>() // Ignore entities already marked for death
|
||||
.Build();
|
||||
|
||||
_unloadedFilter = FilterBuilder
|
||||
.Include<ChunkUnloadPendingTag>()
|
||||
.Build();
|
||||
}
|
||||
|
||||
public override void Update(TimeSpan delta)
|
||||
{
|
||||
var requiredChunks = new HashSet<ChunkCoord>();
|
||||
|
||||
// 1. Find all chunks that should be loaded based on observers
|
||||
foreach (var observerEntity in _observerFilter.Entities)
|
||||
{
|
||||
var observer = Get<ChunkObserverComponent>(observerEntity);
|
||||
var transform = Get<LocalToWorld>(observerEntity);
|
||||
|
||||
// Convert world pos to grid coordinates
|
||||
var worldPos = transform.localToWorldMatrix.Translation;
|
||||
var centerChunk = GetChunkCoord(worldPos);
|
||||
|
||||
// Determine chunk radius based on observer radius and chunk size
|
||||
int chunkRadius = (int)MathF.Ceiling(observer.ViewRadius / ChunkSize);
|
||||
|
||||
for (int x = -chunkRadius; x <= chunkRadius; x++)
|
||||
{
|
||||
for (int y = -chunkRadius; y <= chunkRadius; y++)
|
||||
{
|
||||
for (int z = -chunkRadius; z <= chunkRadius; z++)
|
||||
{
|
||||
var coord = new ChunkCoord(centerChunk.X + x, centerChunk.Y + y, centerChunk.Z + z);
|
||||
requiredChunks.Add(coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Unload chunks that are active but no longer required
|
||||
var chunksToUnload = new List<ChunkCoord>();
|
||||
foreach (var activeChunk in _activeChunks.Keys)
|
||||
{
|
||||
if (!requiredChunks.Contains(activeChunk) && _activeChunks[activeChunk] != ChunkState.Unloading)
|
||||
{
|
||||
chunksToUnload.Add(activeChunk);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var coord in chunksToUnload)
|
||||
{
|
||||
_activeChunks[coord] = ChunkState.Unloading;
|
||||
_pendingUnloads.Enqueue(coord);
|
||||
}
|
||||
|
||||
// 3. Queue newly required chunks
|
||||
foreach (var coord in requiredChunks)
|
||||
{
|
||||
if (!_activeChunks.ContainsKey(coord))
|
||||
{
|
||||
// Mark as unloaded so we don't queue it multiple times
|
||||
_activeChunks[coord] = ChunkState.Unloaded;
|
||||
_pendingLoads.Enqueue(coord);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Process only ONE chunk load per frame to prevent stuttering
|
||||
if (_pendingLoads.Count > 0)
|
||||
{
|
||||
var chunkToLoad = _pendingLoads.Dequeue();
|
||||
// Double check it wasn't unloaded before we got around to loading it
|
||||
if (_activeChunks.TryGetValue(chunkToLoad, out var state) && state == ChunkState.Unloaded)
|
||||
{
|
||||
LoadChunk(chunkToLoad);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Process only ONE chunk unload tagging per frame
|
||||
if (_pendingUnloads.Count > 0)
|
||||
{
|
||||
var chunkToUnload = _pendingUnloads.Dequeue();
|
||||
UnloadChunk(chunkToUnload);
|
||||
}
|
||||
}
|
||||
|
||||
private ChunkCoord GetChunkCoord(Vector3 worldPos)
|
||||
{
|
||||
return new ChunkCoord(
|
||||
(int)MathF.Floor(worldPos.X / ChunkSize),
|
||||
(int)MathF.Floor(worldPos.Y / ChunkSize),
|
||||
(int)MathF.Floor(worldPos.Z / ChunkSize)
|
||||
);
|
||||
}
|
||||
|
||||
private void LoadChunk(ChunkCoord coord)
|
||||
{
|
||||
_activeChunks[coord] = ChunkState.Loading;
|
||||
|
||||
// TODO: In a real system, you'd queue async I/O here to read SceneData for this chunk
|
||||
// and spawn the entities. Once they are all spawned, set state to Loaded.
|
||||
|
||||
// For demonstration, immediately set to loaded.
|
||||
_activeChunks[coord] = ChunkState.Loaded;
|
||||
}
|
||||
|
||||
private void UnloadChunk(ChunkCoord coord)
|
||||
{
|
||||
// Instead of destroying everything instantly, we tag the entities as 'Unloaded'
|
||||
// so that they stop participating in rendering/gameplay, and get destroyed slowly
|
||||
// by the ChunkTeardownSystem.
|
||||
long coordId = coord.Id;
|
||||
foreach (var entity in _chunkMemberFilter.Entities)
|
||||
{
|
||||
var chunkMember = Get<ChunkMemberComponent>(entity);
|
||||
if (chunkMember.ChunkId == coordId)
|
||||
{
|
||||
Set(entity, new ChunkUnloadPendingTag());
|
||||
}
|
||||
}
|
||||
|
||||
// Immediately remove it from the required grid so it can be re-loaded
|
||||
// if the player turns around quickly, while older entities are just garbage collected.
|
||||
_activeChunks.Remove(coord);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user