collision refactor

This commit is contained in:
maxwes08
2026-03-24 00:14:17 +01:00
parent 9ffda6f5af
commit 24ca4288af
4 changed files with 242 additions and 140 deletions

View File

@@ -8,7 +8,8 @@
OakPlanks, OakPlanks,
Grass, Grass,
Bedrock, Bedrock,
Sand Sand,
TNT
} }
public enum Orientation : byte public enum Orientation : byte
@@ -88,6 +89,17 @@
Voxel.Textures.GrassSide, // North Voxel.Textures.GrassSide, // North
Voxel.Textures.GrassSide // South 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
)},
}; };
} }
} }

251
Entity.cs
View File

@@ -20,12 +20,17 @@ namespace Voxel
private float _gravity = 0.08f; private float _gravity = 0.08f;
private float _terminalVelocity = -3.92f; private float _terminalVelocity = -3.92f;
private float _airMultiplier = 0.91f; private float _airMultiplier = 0.91f;
private float _yMultiplier = 0.98f;
private float _groundMultiplier = 0.6f; private float _groundMultiplier = 0.6f;
private const float COLLISION_EPSILON = 0.01f; private const float COLLISION_EPSILON = 0.001f;
protected World _world; 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) public Entity(Vector3 position, float width, float height, World world)
{ {
Position = position; Position = position;
@@ -47,8 +52,9 @@ namespace Voxel
{ {
Vector3 acceleration = new Vector3(0, -_gravity, 0); Vector3 acceleration = new Vector3(0, -_gravity, 0);
Velocity += acceleration; Velocity += acceleration;
Velocity *= _airMultiplier; Velocity.X *= _airMultiplier;
Velocity.Z *= _airMultiplier;
Velocity.Y *= _yMultiplier;
if (Velocity.Y < _terminalVelocity) if (Velocity.Y < _terminalVelocity)
{ {
@@ -77,36 +83,42 @@ namespace Voxel
return; return;
} }
// Collision detected, resolve each axis separately but independently // Collision detected, resolve each axis independently
position = originalPosition; position = originalPosition;
// Resolve Y collision (always first for ground detection) // Resolve Y collision first
ResolveYCollisionIndependent(ref position, movement.Y); ResolveYCollisionIndependent(ref position, movement.Y);
// Resolve X and Z collisions independently of each other // Resolve X collision
Vector3 tempPosX = new Vector3(position.X + movement.X, position.Y, position.Z); ResolveXCollisionIndependent(ref position, movement.X);
if (!HasCollision(GetBoundingBoxAt(tempPosX)))
{
position.X = tempPosX.X;
}
else
{
ResolveXCollisionIndependent(ref position, movement.X);
}
Vector3 tempPosZ = new Vector3(position.X, position.Y, position.Z + movement.Z); // Resolve Z collision
if (!HasCollision(GetBoundingBoxAt(tempPosZ))) ResolveZCollisionIndependent(ref position, movement.Z);
{
position.Z = tempPosZ.Z; // After moving horizontally, reresolve Y to handle any new vertical collisions
} // (e.g., sliding into a ledge)
else ResolveYCollisionIndependent(ref position, 0);
{
ResolveZCollisionIndependent(ref position, movement.Z);
}
} }
private void ResolveYCollisionIndependent(ref Vector3 position, float velocityY) 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; if (velocityY == 0) return;
Vector3 testPos = new Vector3(position.X, position.Y + velocityY, position.Z); Vector3 testPos = new Vector3(position.X, position.Y + velocityY, position.Z);
@@ -117,13 +129,13 @@ namespace Voxel
if (velocityY > 0) // Hitting ceiling if (velocityY > 0) // Hitting ceiling
{ {
float ceilingY = GetCeilingHeight(testBox); float ceilingY = GetCeilingHeight(testBox);
position.Y = ceilingY - (Height / 2) - COLLISION_EPSILON; position.Y = ceilingY - (Height / 2);
Velocity.Y = 0; Velocity.Y = 0;
} }
else // Hitting floor else // Hitting floor
{ {
float floorY = GetFloorHeight(testBox); float floorY = GetFloorHeight(testBox);
position.Y = floorY + (Height / 2) + COLLISION_EPSILON; position.Y = floorY + (Height / 2);
Velocity.Y = 0; Velocity.Y = 0;
OnGround = true; OnGround = true;
} }
@@ -138,89 +150,75 @@ namespace Voxel
{ {
if (velocityX == 0) return; if (velocityX == 0) return;
// Check if we're already inside a block at current position float halfWidth = Width / 2;
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 direction = Math.Sign(velocityX); 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); // Find the wall we hit
AABB testBox = GetBoundingBoxAt(new Vector3(testX, position.Y, position.Z)); float wallX;
if (direction > 0) // Moving right
if (HasCollision(testBox))
{ {
if (direction > 0) // Moving right wallX = GetRightWallPosition(testBox) - halfWidth - COLLISION_EPSILON;
{
position.X = GetRightWallPosition(testBox) - (Width / 2) - COLLISION_EPSILON;
}
else // Moving left
{
position.X = GetLeftWallPosition(testBox) + (Width / 2) + COLLISION_EPSILON;
}
Velocity.X = 0;
return;
} }
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) private void ResolveZCollisionIndependent(ref Vector3 position, float velocityZ)
{ {
if (velocityZ == 0) return; if (velocityZ == 0) return;
// Check if we're already inside a block at current position float halfWidth = Width / 2;
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 direction = Math.Sign(velocityZ); 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); // Find the wall we hit
AABB testBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, testZ)); float wallZ;
if (direction > 0) // Moving forward (+Z)
if (HasCollision(testBox))
{ {
if (direction > 0) // Moving forward wallZ = GetFrontWallPosition(testBox) - halfWidth - COLLISION_EPSILON;
{
position.Z = GetFrontWallPosition(testBox) - (Width / 2) - COLLISION_EPSILON;
}
else // Moving backward
{
position.Z = GetBackWallPosition(testBox) + (Width / 2) + COLLISION_EPSILON;
}
Velocity.Z = 0;
return;
} }
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) private float GetFloorHeight(AABB box)
{ {
int minX = (int)MathF.Floor(box.Min.X); int minX = BlockCoordMin(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X); int maxX = BlockCoordMax(box.Max.X);
int minZ = (int)MathF.Floor(box.Min.Z); int minZ = BlockCoordMin(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z); int maxZ = BlockCoordMax(box.Max.Z);
int checkY = (int)MathF.Floor(box.Min.Y); int checkY = BlockCoordMin(box.Min.Y);
float highestFloor = float.MinValue; float highestFloor = float.MinValue;
@@ -241,11 +239,11 @@ namespace Voxel
private float GetCeilingHeight(AABB box) private float GetCeilingHeight(AABB box)
{ {
int minX = (int)MathF.Floor(box.Min.X); int minX = BlockCoordMin(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X); int maxX = BlockCoordMax(box.Max.X);
int minZ = (int)MathF.Floor(box.Min.Z); int minZ = BlockCoordMin(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z); int maxZ = BlockCoordMax(box.Max.Z);
int checkY = (int)MathF.Floor(box.Max.Y); int checkY = BlockCoordMax(box.Max.Y); // Fix: use the block above the entity
float lowestCeiling = float.MaxValue; float lowestCeiling = float.MaxValue;
@@ -266,11 +264,11 @@ namespace Voxel
private float GetLeftWallPosition(AABB box) private float GetLeftWallPosition(AABB box)
{ {
int minY = (int)MathF.Floor(box.Min.Y); int minY = BlockCoordMin(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y); int maxY = BlockCoordMax(box.Max.Y);
int minZ = (int)MathF.Floor(box.Min.Z); int minZ = BlockCoordMin(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z); int maxZ = BlockCoordMax(box.Max.Z);
int checkX = (int)MathF.Floor(box.Min.X); int checkX = BlockCoordMin(box.Min.X); // leftmost block the entity overlaps
float rightmostWall = float.MinValue; float rightmostWall = float.MinValue;
@@ -291,11 +289,11 @@ namespace Voxel
private float GetRightWallPosition(AABB box) private float GetRightWallPosition(AABB box)
{ {
int minY = (int)MathF.Floor(box.Min.Y); int minY = BlockCoordMin(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y); int maxY = BlockCoordMax(box.Max.Y);
int minZ = (int)MathF.Floor(box.Min.Z); int minZ = BlockCoordMin(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z); int maxZ = BlockCoordMax(box.Max.Z);
int checkX = (int)MathF.Floor(box.Max.X); int checkX = BlockCoordMax(box.Max.X); // rightmost block the entity overlaps
float leftmostWall = float.MaxValue; float leftmostWall = float.MaxValue;
@@ -316,11 +314,11 @@ namespace Voxel
private float GetBackWallPosition(AABB box) private float GetBackWallPosition(AABB box)
{ {
int minX = (int)MathF.Floor(box.Min.X); int minX = BlockCoordMin(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X); int maxX = BlockCoordMax(box.Max.X);
int minY = (int)MathF.Floor(box.Min.Y); int minY = BlockCoordMin(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y); int maxY = BlockCoordMax(box.Max.Y);
int checkZ = (int)MathF.Floor(box.Min.Z); int checkZ = BlockCoordMin(box.Min.Z); // backmost block the entity overlaps
float frontmostWall = float.MinValue; float frontmostWall = float.MinValue;
@@ -341,11 +339,11 @@ namespace Voxel
private float GetFrontWallPosition(AABB box) private float GetFrontWallPosition(AABB box)
{ {
int minX = (int)MathF.Floor(box.Min.X); int minX = BlockCoordMin(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X); int maxX = BlockCoordMax(box.Max.X);
int minY = (int)MathF.Floor(box.Min.Y); int minY = BlockCoordMin(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y); int maxY = BlockCoordMax(box.Max.Y);
int checkZ = (int)MathF.Floor(box.Max.Z); int checkZ = BlockCoordMax(box.Max.Z); // frontmost block the entity overlaps
float backmostWall = float.MaxValue; float backmostWall = float.MaxValue;
@@ -366,12 +364,12 @@ namespace Voxel
public bool HasCollision(AABB box) public bool HasCollision(AABB box)
{ {
int minX = (int)MathF.Floor(box.Min.X); int minX = BlockCoordMin(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X); int maxX = BlockCoordMax(box.Max.X);
int minY = (int)MathF.Floor(box.Min.Y); int minY = BlockCoordMin(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y); int maxY = BlockCoordMax(box.Max.Y);
int minZ = (int)MathF.Floor(box.Min.Z); int minZ = BlockCoordMin(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z); int maxZ = BlockCoordMax(box.Max.Z);
for (int x = minX; x <= maxX; x++) for (int x = minX; x <= maxX; x++)
{ {
@@ -408,13 +406,13 @@ namespace Voxel
Vector3 min = new Vector3( Vector3 min = new Vector3(
position.X - halfWidth, position.X - halfWidth,
position.Y - halfHeight, // Center Y minus half height position.Y - halfHeight,
position.Z - halfWidth position.Z - halfWidth
); );
Vector3 max = new Vector3( Vector3 max = new Vector3(
position.X + halfWidth, position.X + halfWidth,
position.Y + halfHeight, // Center Y plus half height position.Y + halfHeight,
position.Z + halfWidth position.Z + halfWidth
); );
@@ -424,19 +422,20 @@ namespace Voxel
public void UpdateOnGround() public void UpdateOnGround()
{ {
AABB box = GetBoundingBox(); 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 minX = BlockCoordMin(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X); int maxX = BlockCoordMax(box.Max.X);
int minZ = (int)MathF.Floor(box.Min.Z); int minZ = BlockCoordMin(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z); int maxZ = BlockCoordMax(box.Max.Z);
int y = BlockCoordMin(yCheck);
OnGround = false; OnGround = false;
for (int x = minX; x <= maxX; x++) for (int x = minX; x <= maxX; x++)
{ {
for (int z = minZ; z <= maxZ; z++) 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) if (block != Blocks.Air)
{ {
OnGround = true; OnGround = true;

View File

@@ -10,7 +10,7 @@ namespace Voxel
public readonly float mouseCooldown = 0.2f; public readonly float mouseCooldown = 0.2f;
private int _blockIndex = 0; private int _blockIndex = 0;
private double _tickTime = 0; private int _jumpTicks = 0;
private Blocks _selectedBlock = Blocks.OakPlanks; private Blocks _selectedBlock = Blocks.OakPlanks;
private Vector3 previousPosition; private Vector3 previousPosition;
@@ -40,8 +40,10 @@ namespace Voxel
_world.SetBlock(x, y, z, Blocks.Air); _world.SetBlock(x, y, z, Blocks.Air);
} }
public new void Tick() public void Tick()
{ {
if (_jumpTicks > 0) _jumpTicks--;
_world.UpdateChunkLoading(Position); _world.UpdateChunkLoading(Position);
previousPosition = Position; previousPosition = Position;
@@ -58,12 +60,6 @@ namespace Voxel
if (Input.GetKey(Keys.D)) if (Input.GetKey(Keys.D))
sidewards = 1; 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) if (Input.GetMouseButton(MouseButton.Right) && lastClick == 0)
{ {
lastClick = mouseCooldown; lastClick = mouseCooldown;
@@ -78,8 +74,30 @@ namespace Voxel
BreakBlock(); BreakBlock();
} }
if (Position.Y < -8)
{
Position.Y = 64;
Velocity.Y = 0;
}
ApplyWalkSpeed(sidewards, forwards); ApplyWalkSpeed(sidewards, forwards);
base.Tick(); 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) public void Update(float deltaTime, float alpha)
@@ -125,7 +143,7 @@ namespace Voxel
} }
else else
{ {
Vector3 groundAccel = worldDir * 0.1f * M_t; Vector3 groundAccel = worldDir * 0.13f * M_t;
Velocity = new Vector3(Velocity.X * groundMultiplier + groundAccel.X, Velocity = new Vector3(Velocity.X * groundMultiplier + groundAccel.X,
Velocity.Y, Velocity.Y,
Velocity.Z * groundMultiplier + groundAccel.Z); Velocity.Z * groundMultiplier + groundAccel.Z);

View File

@@ -47,7 +47,7 @@ namespace Voxel
var neighbors = GetChunkNeighbors(chunk).ToList(); var neighbors = GetChunkNeighbors(chunk).ToList();
_chunks.Remove((chunkX, chunkZ)); _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) foreach (var (orientation, neighbor) in neighbors)
{ {
var opposite = GetOppositeOrientation(orientation); var opposite = GetOppositeOrientation(orientation);
@@ -159,16 +159,26 @@ namespace Voxel
return chunk.GetBlock(localX, worldY, localZ); 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); var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ);
Chunk chunk = GetChunk(chunkX, chunkZ); Chunk chunk = GetChunk(chunkX, chunkZ);
if (chunk == null) return; 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)) 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<Chunk>(); // 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) 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 chunkX = worldX >= 0 ? worldX / Chunk.Size : (worldX + 1) / Chunk.Size - 1;