From 24ca4288af59dd3a43927ad718ffd18163172134 Mon Sep 17 00:00:00 2001 From: maxwes08 Date: Tue, 24 Mar 2026 00:14:17 +0100 Subject: [PATCH] collision refactor --- Blocks.cs | 14 ++- Entity.cs | 251 +++++++++++++++++++++++++++--------------------------- Player.cs | 36 ++++++-- World.cs | 81 +++++++++++++++++- 4 files changed, 242 insertions(+), 140 deletions(-) diff --git a/Blocks.cs b/Blocks.cs index 8bc6638..dc2b982 100644 --- a/Blocks.cs +++ b/Blocks.cs @@ -8,7 +8,8 @@ OakPlanks, Grass, Bedrock, - Sand + Sand, + TNT } public enum Orientation : byte @@ -88,6 +89,17 @@ Voxel.Textures.GrassSide, // North Voxel.Textures.GrassSide // South )}, + + + { Voxel.Blocks.TNT, new BlockDefinition( + Voxel.Blocks.TNT, + Voxel.Textures.TntSide, // West + Voxel.Textures.TntSide, // East + Voxel.Textures.TntTop, // Top + Voxel.Textures.TntBottom, // Bottom + Voxel.Textures.TntSide, // North + Voxel.Textures.TntSide // South + )}, }; } } diff --git a/Entity.cs b/Entity.cs index 3f445b4..65dc214 100644 --- a/Entity.cs +++ b/Entity.cs @@ -20,12 +20,17 @@ namespace Voxel private float _gravity = 0.08f; private float _terminalVelocity = -3.92f; private float _airMultiplier = 0.91f; + private float _yMultiplier = 0.98f; private float _groundMultiplier = 0.6f; - private const float COLLISION_EPSILON = 0.01f; + private const float COLLISION_EPSILON = 0.001f; protected World _world; + // Helper methods for consistent block coordinate conversion + private static int BlockCoordMin(float coord) => (int)MathF.Floor(coord - 1e-6f); + private static int BlockCoordMax(float coord) => (int)MathF.Floor(coord + 1e-6f); + public Entity(Vector3 position, float width, float height, World world) { Position = position; @@ -47,8 +52,9 @@ namespace Voxel { Vector3 acceleration = new Vector3(0, -_gravity, 0); Velocity += acceleration; - Velocity *= _airMultiplier; - + Velocity.X *= _airMultiplier; + Velocity.Z *= _airMultiplier; + Velocity.Y *= _yMultiplier; if (Velocity.Y < _terminalVelocity) { @@ -77,36 +83,42 @@ namespace Voxel return; } - // Collision detected, resolve each axis separately but independently + // Collision detected, resolve each axis independently position = originalPosition; - // Resolve Y collision (always first for ground detection) + // Resolve Y collision first ResolveYCollisionIndependent(ref position, movement.Y); - // Resolve X and Z collisions independently of each other - Vector3 tempPosX = new Vector3(position.X + movement.X, position.Y, position.Z); - if (!HasCollision(GetBoundingBoxAt(tempPosX))) - { - position.X = tempPosX.X; - } - else - { - ResolveXCollisionIndependent(ref position, movement.X); - } + // Resolve X collision + ResolveXCollisionIndependent(ref position, movement.X); - Vector3 tempPosZ = new Vector3(position.X, position.Y, position.Z + movement.Z); - if (!HasCollision(GetBoundingBoxAt(tempPosZ))) - { - position.Z = tempPosZ.Z; - } - else - { - ResolveZCollisionIndependent(ref position, movement.Z); - } + // Resolve Z collision + ResolveZCollisionIndependent(ref position, movement.Z); + + // After moving horizontally, re‑resolve Y to handle any new vertical collisions + // (e.g., sliding into a ledge) + ResolveYCollisionIndependent(ref position, 0); } private void ResolveYCollisionIndependent(ref Vector3 position, float velocityY) { + // First, handle if we're already inside a block (shouldn't happen normally) + AABB currentBox = GetBoundingBoxAt(position); + if (HasCollision(currentBox)) + { + // Try to push upward to escape + float step = 0.05f; + for (int i = 0; i < 10; i++) + { + position.Y += step; + if (!HasCollision(GetBoundingBoxAt(position))) + break; + } + Velocity.Y = 0; + OnGround = false; + return; + } + if (velocityY == 0) return; Vector3 testPos = new Vector3(position.X, position.Y + velocityY, position.Z); @@ -117,13 +129,13 @@ namespace Voxel if (velocityY > 0) // Hitting ceiling { float ceilingY = GetCeilingHeight(testBox); - position.Y = ceilingY - (Height / 2) - COLLISION_EPSILON; + position.Y = ceilingY - (Height / 2); Velocity.Y = 0; } else // Hitting floor { float floorY = GetFloorHeight(testBox); - position.Y = floorY + (Height / 2) + COLLISION_EPSILON; + position.Y = floorY + (Height / 2); Velocity.Y = 0; OnGround = true; } @@ -138,89 +150,75 @@ namespace Voxel { if (velocityX == 0) return; - // Check if we're already inside a block at current position - AABB currentBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, position.Z)); - if (HasCollision(currentBox)) - { - // Already inside a block, allow movement to escape - position.X += velocityX; - return; - } - + float halfWidth = Width / 2; float direction = Math.Sign(velocityX); - float checkDistance = Math.Abs(velocityX) + COLLISION_EPSILON; + float targetX = position.X + velocityX; - for (float offset = 0; offset <= checkDistance; offset += 0.01f) + // Sweep the bounding box to the target X position + Vector3 testPos = new Vector3(targetX, position.Y, position.Z); + AABB testBox = GetBoundingBoxAt(testPos); + + if (HasCollision(testBox)) { - float testX = position.X + (offset * direction); - AABB testBox = GetBoundingBoxAt(new Vector3(testX, position.Y, position.Z)); - - if (HasCollision(testBox)) + // Find the wall we hit + float wallX; + if (direction > 0) // Moving right { - if (direction > 0) // Moving right - { - position.X = GetRightWallPosition(testBox) - (Width / 2) - COLLISION_EPSILON; - } - else // Moving left - { - position.X = GetLeftWallPosition(testBox) + (Width / 2) + COLLISION_EPSILON; - } - Velocity.X = 0; - return; + wallX = GetRightWallPosition(testBox) - halfWidth - COLLISION_EPSILON; } + else // Moving left + { + wallX = GetLeftWallPosition(testBox) + halfWidth + COLLISION_EPSILON; + } + position.X = wallX; + Velocity.X = 0; + } + else + { + position.X = targetX; } - - // No collision found, apply full movement - position.X += velocityX; } private void ResolveZCollisionIndependent(ref Vector3 position, float velocityZ) { if (velocityZ == 0) return; - // Check if we're already inside a block at current position - AABB currentBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, position.Z)); - if (HasCollision(currentBox)) - { - // Already inside a block, allow movement to escape - position.Z += velocityZ; - return; - } - + float halfWidth = Width / 2; float direction = Math.Sign(velocityZ); - float checkDistance = Math.Abs(velocityZ) + COLLISION_EPSILON; + float targetZ = position.Z + velocityZ; - for (float offset = 0; offset <= checkDistance; offset += 0.01f) + // Sweep the bounding box to the target Z position + Vector3 testPos = new Vector3(position.X, position.Y, targetZ); + AABB testBox = GetBoundingBoxAt(testPos); + + if (HasCollision(testBox)) { - float testZ = position.Z + (offset * direction); - AABB testBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, testZ)); - - if (HasCollision(testBox)) + // Find the wall we hit + float wallZ; + if (direction > 0) // Moving forward (+Z) { - if (direction > 0) // Moving forward - { - position.Z = GetFrontWallPosition(testBox) - (Width / 2) - COLLISION_EPSILON; - } - else // Moving backward - { - position.Z = GetBackWallPosition(testBox) + (Width / 2) + COLLISION_EPSILON; - } - Velocity.Z = 0; - return; + wallZ = GetFrontWallPosition(testBox) - halfWidth - COLLISION_EPSILON; } + else // Moving backward (-Z) + { + wallZ = GetBackWallPosition(testBox) + halfWidth + COLLISION_EPSILON; + } + position.Z = wallZ; + Velocity.Z = 0; + } + else + { + position.Z = targetZ; } - - // No collision found, apply full movement - position.Z += velocityZ; } private float GetFloorHeight(AABB box) { - int minX = (int)MathF.Floor(box.Min.X); - int maxX = (int)MathF.Floor(box.Max.X); - int minZ = (int)MathF.Floor(box.Min.Z); - int maxZ = (int)MathF.Floor(box.Max.Z); - int checkY = (int)MathF.Floor(box.Min.Y); + int minX = BlockCoordMin(box.Min.X); + int maxX = BlockCoordMax(box.Max.X); + int minZ = BlockCoordMin(box.Min.Z); + int maxZ = BlockCoordMax(box.Max.Z); + int checkY = BlockCoordMin(box.Min.Y); float highestFloor = float.MinValue; @@ -241,11 +239,11 @@ namespace Voxel private float GetCeilingHeight(AABB box) { - int minX = (int)MathF.Floor(box.Min.X); - int maxX = (int)MathF.Floor(box.Max.X); - int minZ = (int)MathF.Floor(box.Min.Z); - int maxZ = (int)MathF.Floor(box.Max.Z); - int checkY = (int)MathF.Floor(box.Max.Y); + int minX = BlockCoordMin(box.Min.X); + int maxX = BlockCoordMax(box.Max.X); + int minZ = BlockCoordMin(box.Min.Z); + int maxZ = BlockCoordMax(box.Max.Z); + int checkY = BlockCoordMax(box.Max.Y); // Fix: use the block above the entity float lowestCeiling = float.MaxValue; @@ -266,11 +264,11 @@ namespace Voxel private float GetLeftWallPosition(AABB box) { - int minY = (int)MathF.Floor(box.Min.Y); - int maxY = (int)MathF.Floor(box.Max.Y); - int minZ = (int)MathF.Floor(box.Min.Z); - int maxZ = (int)MathF.Floor(box.Max.Z); - int checkX = (int)MathF.Floor(box.Min.X); + int minY = BlockCoordMin(box.Min.Y); + int maxY = BlockCoordMax(box.Max.Y); + int minZ = BlockCoordMin(box.Min.Z); + int maxZ = BlockCoordMax(box.Max.Z); + int checkX = BlockCoordMin(box.Min.X); // leftmost block the entity overlaps float rightmostWall = float.MinValue; @@ -291,11 +289,11 @@ namespace Voxel private float GetRightWallPosition(AABB box) { - int minY = (int)MathF.Floor(box.Min.Y); - int maxY = (int)MathF.Floor(box.Max.Y); - int minZ = (int)MathF.Floor(box.Min.Z); - int maxZ = (int)MathF.Floor(box.Max.Z); - int checkX = (int)MathF.Floor(box.Max.X); + int minY = BlockCoordMin(box.Min.Y); + int maxY = BlockCoordMax(box.Max.Y); + int minZ = BlockCoordMin(box.Min.Z); + int maxZ = BlockCoordMax(box.Max.Z); + int checkX = BlockCoordMax(box.Max.X); // rightmost block the entity overlaps float leftmostWall = float.MaxValue; @@ -316,11 +314,11 @@ namespace Voxel private float GetBackWallPosition(AABB box) { - int minX = (int)MathF.Floor(box.Min.X); - int maxX = (int)MathF.Floor(box.Max.X); - int minY = (int)MathF.Floor(box.Min.Y); - int maxY = (int)MathF.Floor(box.Max.Y); - int checkZ = (int)MathF.Floor(box.Min.Z); + int minX = BlockCoordMin(box.Min.X); + int maxX = BlockCoordMax(box.Max.X); + int minY = BlockCoordMin(box.Min.Y); + int maxY = BlockCoordMax(box.Max.Y); + int checkZ = BlockCoordMin(box.Min.Z); // backmost block the entity overlaps float frontmostWall = float.MinValue; @@ -341,11 +339,11 @@ namespace Voxel private float GetFrontWallPosition(AABB box) { - int minX = (int)MathF.Floor(box.Min.X); - int maxX = (int)MathF.Floor(box.Max.X); - int minY = (int)MathF.Floor(box.Min.Y); - int maxY = (int)MathF.Floor(box.Max.Y); - int checkZ = (int)MathF.Floor(box.Max.Z); + int minX = BlockCoordMin(box.Min.X); + int maxX = BlockCoordMax(box.Max.X); + int minY = BlockCoordMin(box.Min.Y); + int maxY = BlockCoordMax(box.Max.Y); + int checkZ = BlockCoordMax(box.Max.Z); // frontmost block the entity overlaps float backmostWall = float.MaxValue; @@ -366,12 +364,12 @@ namespace Voxel public bool HasCollision(AABB box) { - int minX = (int)MathF.Floor(box.Min.X); - int maxX = (int)MathF.Floor(box.Max.X); - int minY = (int)MathF.Floor(box.Min.Y); - int maxY = (int)MathF.Floor(box.Max.Y); - int minZ = (int)MathF.Floor(box.Min.Z); - int maxZ = (int)MathF.Floor(box.Max.Z); + int minX = BlockCoordMin(box.Min.X); + int maxX = BlockCoordMax(box.Max.X); + int minY = BlockCoordMin(box.Min.Y); + int maxY = BlockCoordMax(box.Max.Y); + int minZ = BlockCoordMin(box.Min.Z); + int maxZ = BlockCoordMax(box.Max.Z); for (int x = minX; x <= maxX; x++) { @@ -408,13 +406,13 @@ namespace Voxel Vector3 min = new Vector3( position.X - halfWidth, - position.Y - halfHeight, // Center Y minus half height + position.Y - halfHeight, position.Z - halfWidth ); Vector3 max = new Vector3( position.X + halfWidth, - position.Y + halfHeight, // Center Y plus half height + position.Y + halfHeight, position.Z + halfWidth ); @@ -424,19 +422,20 @@ namespace Voxel public void UpdateOnGround() { AABB box = GetBoundingBox(); - float yCheck = box.Min.Y - 0.05f; + float yCheck = box.Min.Y - COLLISION_EPSILON; - int minX = (int)MathF.Floor(box.Min.X); - int maxX = (int)MathF.Floor(box.Max.X); - int minZ = (int)MathF.Floor(box.Min.Z); - int maxZ = (int)MathF.Floor(box.Max.Z); + int minX = BlockCoordMin(box.Min.X); + int maxX = BlockCoordMax(box.Max.X); + int minZ = BlockCoordMin(box.Min.Z); + int maxZ = BlockCoordMax(box.Max.Z); + int y = BlockCoordMin(yCheck); OnGround = false; for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { - Blocks block = _world.GetBlock(x, (int)MathF.Floor(yCheck), z); + Blocks block = _world.GetBlock(x, y, z); if (block != Blocks.Air) { OnGround = true; diff --git a/Player.cs b/Player.cs index b9e9cde..9762ad5 100644 --- a/Player.cs +++ b/Player.cs @@ -10,7 +10,7 @@ namespace Voxel public readonly float mouseCooldown = 0.2f; private int _blockIndex = 0; - private double _tickTime = 0; + private int _jumpTicks = 0; private Blocks _selectedBlock = Blocks.OakPlanks; private Vector3 previousPosition; @@ -40,8 +40,10 @@ namespace Voxel _world.SetBlock(x, y, z, Blocks.Air); } - public new void Tick() + public void Tick() { + if (_jumpTicks > 0) _jumpTicks--; + _world.UpdateChunkLoading(Position); previousPosition = Position; @@ -58,12 +60,6 @@ namespace Voxel if (Input.GetKey(Keys.D)) sidewards = 1; - if (Input.GetKey(Keys.Space) && OnGround) - { - Velocity = new Vector3(Velocity.X, 0.42f, Velocity.Z); - OnGround = false; - } - if (Input.GetMouseButton(MouseButton.Right) && lastClick == 0) { lastClick = mouseCooldown; @@ -78,8 +74,30 @@ namespace Voxel BreakBlock(); } + if (Position.Y < -8) + { + Position.Y = 64; + Velocity.Y = 0; + } + ApplyWalkSpeed(sidewards, forwards); base.Tick(); + + if (Input.GetKey(Keys.Space)) + { + if (OnGround && _jumpTicks == 0) + { + Jump(); + _jumpTicks = 10; + } + } + else _jumpTicks = 0; + } + + public void Jump() + { + Velocity = new Vector3(Velocity.X, 0.42f, Velocity.Z); + OnGround = false; } public void Update(float deltaTime, float alpha) @@ -125,7 +143,7 @@ namespace Voxel } else { - Vector3 groundAccel = worldDir * 0.1f * M_t; + Vector3 groundAccel = worldDir * 0.13f * M_t; Velocity = new Vector3(Velocity.X * groundMultiplier + groundAccel.X, Velocity.Y, Velocity.Z * groundMultiplier + groundAccel.Z); diff --git a/World.cs b/World.cs index c0f52d3..1c53a7e 100644 --- a/World.cs +++ b/World.cs @@ -47,7 +47,7 @@ namespace Voxel var neighbors = GetChunkNeighbors(chunk).ToList(); _chunks.Remove((chunkX, chunkZ)); - // 3. For each neighbor, remove reference to this chunk + // update neighbor references to this chunk foreach (var (orientation, neighbor) in neighbors) { var opposite = GetOppositeOrientation(orientation); @@ -159,16 +159,26 @@ namespace Voxel return chunk.GetBlock(localX, worldY, localZ); } - public void SetBlock(int worldX, int worldY, int worldZ, Blocks block) + 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); + chunk.SetBlock(localX, worldY, localZ, block, updateMesh: updateMesh); - if (block == Blocks.Air && ((localX == Chunk.Size - 1 || localX == 0) || (localZ == Chunk.Size - 1 || localZ == 0))) + // temporary tnt functionality + + if (block == Blocks.TNT) + { + int radius = 4; + Explode(worldX, worldY, worldZ, 4); + } + + if (updateMesh == false) return; + + if (block == Blocks.Air && IsOnChunkBorder(worldX, worldZ)) { foreach (var orientation in GetEdgeOrientations(localX, localZ, Chunk.Size)) { @@ -180,6 +190,69 @@ namespace Voxel } } + 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(); // store chunks that will change + + // bounding box + int minX = centerX - radius; + int maxX = centerX + radius; + int minY = centerY - radius; + int maxY = centerY + radius; + int minZ = centerZ - radius; + int maxZ = centerZ + radius; + + for (int x = minX; x <= maxX; x++) + { + for (int y = minY; y <= maxY; y++) + { + for (int z = minZ; z <= maxZ; z++) + { + int dx = x - centerX; + int dy = y - centerY; + int dz = z - centerZ; + if (dx * dx + dy * dy + dz * dz <= radiusSq) + { + // no update + SetBlock(x, y, z, Blocks.Air, updateMesh: false); + + // add chunk to update list + var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(x, z); + Chunk chunk = GetChunk(chunkX, chunkZ); + + if (chunk != null) + affectedChunks.Add(chunk); + + if (IsOnChunkBorder(x, z)) + { + foreach (var orientation in GetEdgeOrientations(localX, localZ, Chunk.Size)) + { + if (chunk.Neighbors.TryGetValue(orientation, out var neighbor) && neighbor != null) + { + affectedChunks.Add(neighbor); + } + } + } + } + } + } + } + + // Now rebuild meshes for all affected chunks + foreach (var chunk in affectedChunks) + { + chunk.UpdateChunkMesh(); + } + } + 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;