Major refactor and organization, optimizations to chunk, world and renderer
This commit is contained in:
373
World/World.cs
Normal file
373
World/World.cs
Normal file
@@ -0,0 +1,373 @@
|
||||
using OpenTK.Mathematics;
|
||||
using Voxel.Graphics;
|
||||
using Voxel.Physics;
|
||||
|
||||
namespace Voxel.Core
|
||||
{
|
||||
public class World
|
||||
{
|
||||
private Dictionary<(int, int), Chunk> _chunks;
|
||||
|
||||
private (int x, int z) _lastCenter = (0, 0);
|
||||
private int _loadDistance = 8;
|
||||
bool chunkLoadingInitialized = false;
|
||||
|
||||
private static readonly Dictionary<Orientation, (int x, int y)> _neighborOffsets = new()
|
||||
{
|
||||
{ Orientation.West, (1, 0) },
|
||||
{ Orientation.East, (-1, 0) },
|
||||
{ Orientation.North, (0, 1) },
|
||||
{ Orientation.South, (0, -1) }
|
||||
};
|
||||
|
||||
public World()
|
||||
{
|
||||
_chunks = new Dictionary<(int, int), Chunk>();
|
||||
}
|
||||
|
||||
public Chunk GetChunk(int chunkX, int chunkZ)
|
||||
{
|
||||
_chunks.TryGetValue((chunkX, chunkZ), out Chunk chunk);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
public void AddChunk(Chunk chunk)
|
||||
{
|
||||
_chunks[(chunk.X, chunk.Y)] = chunk;
|
||||
|
||||
UpdateNeighboringChunks(chunk);
|
||||
chunk.UpdateChunkMesh();
|
||||
Renderer.MarkBuffersDirty();
|
||||
}
|
||||
|
||||
public void RemoveChunk(int chunkX, int chunkZ)
|
||||
{
|
||||
if (_chunks.TryGetValue((chunkX, chunkZ), out Chunk chunk))
|
||||
{
|
||||
var neighbors = GetChunkNeighbors(chunk).ToList();
|
||||
|
||||
_chunks.Remove((chunkX, chunkZ));
|
||||
|
||||
// update neighbor references to this chunk
|
||||
foreach (var (orientation, neighbor) in neighbors)
|
||||
{
|
||||
var opposite = GetOppositeOrientation(orientation);
|
||||
neighbor.SetNeighbor(opposite, null);
|
||||
neighbor.UpdateChunkMesh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public new void UpdateChunkLoading(Vector3 playerPosition)
|
||||
{
|
||||
int centerX = (int)Math.Floor(playerPosition.X / Chunk.Size);
|
||||
int centerZ = (int)Math.Floor(playerPosition.Z / Chunk.Size);
|
||||
|
||||
if ((centerX == _lastCenter.x && centerZ == _lastCenter.z) && chunkLoadingInitialized)
|
||||
return;
|
||||
|
||||
_lastCenter = (centerX, centerZ);
|
||||
|
||||
int minX = centerX - _loadDistance;
|
||||
int maxX = centerX + _loadDistance;
|
||||
int minZ = centerZ - _loadDistance;
|
||||
int maxZ = centerZ + _loadDistance;
|
||||
|
||||
UnloadDistantChunks(minX, maxX, minZ, maxZ);
|
||||
LoadChunksInRange(minX, maxX, minZ, maxZ);
|
||||
|
||||
chunkLoadingInitialized = true;
|
||||
}
|
||||
|
||||
private void UnloadDistantChunks(int minX, int maxX, int minZ, int maxZ)
|
||||
{
|
||||
var chunksToRemove = new List<(int, int)>();
|
||||
|
||||
foreach (var (chunkX, chunkZ) in _chunks.Keys)
|
||||
{
|
||||
if (chunkX < minX || chunkX > maxX || chunkZ < minZ || chunkZ > maxZ)
|
||||
{
|
||||
chunksToRemove.Add((chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var chunkPos in chunksToRemove)
|
||||
{
|
||||
RemoveChunk(chunkPos.Item1, chunkPos.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadChunksInRange(int minX, int maxX, int minZ, int maxZ)
|
||||
{
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
{
|
||||
for (int z = minZ; z <= maxZ; z++)
|
||||
{
|
||||
if (!_chunks.ContainsKey((x, z)))
|
||||
{
|
||||
Chunk chunk = new Chunk(x, z);
|
||||
AddChunk(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateNeighboringChunks(Chunk chunk)
|
||||
{
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
chunk.SetNeighbor((Orientation)i, null);
|
||||
}
|
||||
|
||||
foreach (var (orientation, neighbor) in GetChunkNeighbors(chunk))
|
||||
{
|
||||
Orientation opposite = GetOppositeOrientation(orientation);
|
||||
|
||||
neighbor.SetNeighbor(opposite, chunk);
|
||||
chunk.SetNeighbor(orientation, neighbor);
|
||||
|
||||
neighbor.UpdateChunkMesh();
|
||||
}
|
||||
}
|
||||
|
||||
private Orientation GetOppositeOrientation(Orientation orientation)
|
||||
{
|
||||
return orientation switch
|
||||
{
|
||||
Orientation.West => Orientation.East,
|
||||
Orientation.East => Orientation.West,
|
||||
Orientation.North => Orientation.South,
|
||||
Orientation.South => Orientation.North,
|
||||
_ => orientation
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<Chunk> GetAllChunks()
|
||||
{
|
||||
return _chunks.Values;
|
||||
}
|
||||
|
||||
public Blocks GetBlock(int worldX, int worldY, int worldZ)
|
||||
{
|
||||
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ);
|
||||
|
||||
Chunk chunk = GetChunk(chunkX, chunkZ);
|
||||
if (chunk == null) return 0; // air if chunk not loaded
|
||||
|
||||
return chunk.GetBlock(localX, worldY, localZ);
|
||||
}
|
||||
|
||||
public void SetBlock(int worldX, int worldY, int worldZ, Blocks block, bool updateMesh = true)
|
||||
{
|
||||
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ);
|
||||
|
||||
Chunk chunk = GetChunk(chunkX, chunkZ);
|
||||
if (chunk == null) return;
|
||||
|
||||
chunk.SetBlock(localX, worldY, localZ, block, updateMesh: updateMesh);
|
||||
|
||||
// temporary tnt functionality
|
||||
if (block == Blocks.TNT)
|
||||
{
|
||||
Explode(worldX, worldY, worldZ, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updateMesh) return;
|
||||
|
||||
UpdateNeighborsAtBoundary(chunk, localX, worldY, localZ);
|
||||
RebuildDirtyChunks();
|
||||
}
|
||||
|
||||
private void UpdateNeighborsAtBoundary(Chunk chunk, int lx, int ly, int lz)
|
||||
{
|
||||
if (lx == Chunk.Size - 1) chunk.GetNeighbor(Orientation.West)?.MarkDirty();
|
||||
else if (lx == 0) chunk.GetNeighbor(Orientation.East)?.MarkDirty();
|
||||
if (lz == Chunk.Size - 1) chunk.GetNeighbor(Orientation.North)?.MarkDirty();
|
||||
else if (lz == 0) chunk.GetNeighbor(Orientation.South)?.MarkDirty();
|
||||
}
|
||||
|
||||
public bool IsOnChunkBorder(int worldX, int worldZ)
|
||||
{
|
||||
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ);
|
||||
return (localX == Chunk.Size - 1 || localX == 0) || (localZ == Chunk.Size - 1 || localZ == 0);
|
||||
}
|
||||
|
||||
// temporary tnt functionality
|
||||
public void Explode(int centerX, int centerY, int centerZ, int radius)
|
||||
{
|
||||
int radiusSq = radius * radius;
|
||||
var affectedChunks = new HashSet<Chunk>(); // store chunks that will change
|
||||
|
||||
// bounding box
|
||||
int minX = centerX - radius;
|
||||
int maxX = centerX + radius;
|
||||
int minY = Math.Max(0, centerY - radius);
|
||||
int maxY = Math.Min(Chunk.Height - 1, centerY + radius);
|
||||
int minZ = centerZ - radius;
|
||||
int maxZ = centerZ + radius;
|
||||
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
{
|
||||
for (int z = minZ; z <= maxZ; z++)
|
||||
{
|
||||
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(x, z);
|
||||
Chunk chunk = GetChunk(chunkX, chunkZ);
|
||||
if (chunk == null) continue;
|
||||
|
||||
for (int y = minY; y <= maxY; y++)
|
||||
{
|
||||
int dx = x - centerX;
|
||||
int dy = y - centerY;
|
||||
int dz = z - centerZ;
|
||||
|
||||
if (dx * dx + dy * dy + dz * dz <= radiusSq)
|
||||
{
|
||||
chunk.SetBlock(localX, y, localZ, Blocks.Air, updateMesh: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RebuildDirtyChunks();
|
||||
}
|
||||
|
||||
private void RebuildDirtyChunks()
|
||||
{
|
||||
foreach (var chunk in _chunks.Values)
|
||||
{
|
||||
if (chunk.IsDirty)
|
||||
{
|
||||
chunk.UpdateChunkMesh();
|
||||
Renderer.UpdateChunkBuffer(chunk);
|
||||
chunk.IsDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (int chunkX, int chunkZ, int localX, int localZ) WorldToChunkCoords(int worldX, int worldZ)
|
||||
{
|
||||
int chunkX = worldX >= 0 ? worldX / Chunk.Size : (worldX + 1) / Chunk.Size - 1;
|
||||
int chunkZ = worldZ >= 0 ? worldZ / Chunk.Size : (worldZ + 1) / Chunk.Size - 1;
|
||||
|
||||
int localX = ((worldX % Chunk.Size) + Chunk.Size) % Chunk.Size;
|
||||
int localZ = ((worldZ % Chunk.Size) + Chunk.Size) % Chunk.Size;
|
||||
|
||||
return (chunkX, chunkZ, localX, localZ);
|
||||
}
|
||||
|
||||
|
||||
List<Orientation> GetEdgeOrientations(int localX, int localZ, int size)
|
||||
{
|
||||
var orientations = new List<Orientation>();
|
||||
|
||||
if (localX == size - 1) orientations.Add(Orientation.West);
|
||||
if (localX == 0) orientations.Add(Orientation.East);
|
||||
if (localZ == size - 1) orientations.Add(Orientation.North);
|
||||
if (localZ == 0) orientations.Add(Orientation.South);
|
||||
|
||||
return orientations;
|
||||
}
|
||||
|
||||
public IEnumerable<(Orientation orientation, Chunk neighbor)> GetChunkNeighbors(Chunk chunk)
|
||||
{
|
||||
int chunkX = chunk.X;
|
||||
int chunkY = chunk.Y;
|
||||
|
||||
foreach (var kv in _neighborOffsets)
|
||||
{
|
||||
Chunk neighbor = GetChunk(chunkX + kv.Value.x, chunkY + kv.Value.y);
|
||||
if (neighbor != null)
|
||||
yield return (kv.Key, neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
public (bool success, Blocks block, int x, int y, int z, Vector3i normal) Raycast(Vector3 origin, Vector3 direction, float maxDistance)
|
||||
{
|
||||
int x = (int)MathF.Floor(origin.X);
|
||||
int y = (int)MathF.Floor(origin.Y);
|
||||
int z = (int)MathF.Floor(origin.Z);
|
||||
|
||||
int stepX = direction.X > 0 ? 1 : -1;
|
||||
int stepY = direction.Y > 0 ? 1 : -1;
|
||||
int stepZ = direction.Z > 0 ? 1 : -1;
|
||||
|
||||
float tDeltaX = direction.X != 0 ? MathF.Abs(1 / direction.X) : float.MaxValue;
|
||||
float tDeltaY = direction.Y != 0 ? MathF.Abs(1 / direction.Y) : float.MaxValue;
|
||||
float tDeltaZ = direction.Z != 0 ? MathF.Abs(1 / direction.Z) : float.MaxValue;
|
||||
|
||||
float tMaxX = direction.X > 0 ? (MathF.Floor(origin.X) + 1 - origin.X) * tDeltaX
|
||||
: (origin.X - MathF.Floor(origin.X)) * tDeltaX;
|
||||
float tMaxY = direction.Y > 0 ? (MathF.Floor(origin.Y) + 1 - origin.Y) * tDeltaY
|
||||
: (origin.Y - MathF.Floor(origin.Y)) * tDeltaY;
|
||||
float tMaxZ = direction.Z > 0 ? (MathF.Floor(origin.Z) + 1 - origin.Z) * tDeltaZ
|
||||
: (origin.Z - MathF.Floor(origin.Z)) * tDeltaZ;
|
||||
|
||||
float distance = 0f;
|
||||
Vector3i normal = Vector3i.Zero;
|
||||
|
||||
while (distance <= maxDistance)
|
||||
{
|
||||
Blocks block = GetBlock(x, y, z);
|
||||
|
||||
if (block != Blocks.Air)
|
||||
return (true, block, x, y, z, normal);
|
||||
|
||||
if (tMaxX < tMaxY && tMaxX < tMaxZ)
|
||||
{
|
||||
x += stepX;
|
||||
normal = new Vector3i(-stepX, 0, 0);
|
||||
distance = tMaxX;
|
||||
tMaxX += tDeltaX;
|
||||
}
|
||||
else if (tMaxY < tMaxZ)
|
||||
{
|
||||
y += stepY;
|
||||
normal = new Vector3i(0, -stepY, 0);
|
||||
distance = tMaxY;
|
||||
tMaxY += tDeltaY;
|
||||
}
|
||||
else
|
||||
{
|
||||
z += stepZ;
|
||||
normal = new Vector3i(0, 0, -stepZ);
|
||||
distance = tMaxZ;
|
||||
tMaxZ += tDeltaZ;
|
||||
}
|
||||
}
|
||||
|
||||
return (false, Blocks.Air, 0, 0, 0, Vector3i.Zero);
|
||||
}
|
||||
|
||||
public List<AABB> GetColliders(AABB body)
|
||||
{
|
||||
List<AABB> collisions = new List<AABB>();
|
||||
|
||||
int minX = (int)Math.Floor(body.Min.X);
|
||||
int maxX = (int)Math.Ceiling(body.Max.X) - 1;
|
||||
int minY = (int)Math.Floor(body.Min.Y);
|
||||
int maxY = (int)Math.Ceiling(body.Max.Y) - 1;
|
||||
int minZ = (int)Math.Floor(body.Min.Z);
|
||||
int maxZ = (int)Math.Ceiling(body.Max.Z) - 1;
|
||||
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
{
|
||||
for (int y = minY; y <= maxY; y++)
|
||||
{
|
||||
for (int z = minZ; z <= maxZ; z++)
|
||||
{
|
||||
if (GetBlock(x, y, z) != Blocks.Air)
|
||||
{
|
||||
collisions.Add(new AABB(
|
||||
new Vector3(x, y, z),
|
||||
new Vector3(x + 1, y + 1, z + 1)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collisions;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user