using OpenTK.Mathematics; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Voxel { public class Entity { public Vector3 Position; public Vector3 Velocity; public bool OnGround; public float Rotation; public float Width; public float Height; private float _gravity = 0.08f; private float _terminalVelocity = -3.92f; private float _airMultiplier = 0.91f; private float _groundMultiplier = 0.6f; private const float COLLISION_EPSILON = 0.01f; protected World _world; public Entity(Vector3 position, float width, float height, World world) { Position = position; Width = width; Height = height; _world = world; } public void Tick() { Vector3 desiredMovement = Velocity; Vector3 newPosition = Position; CheckAndResolveCollisions(ref newPosition, desiredMovement); Position = newPosition; if (!OnGround) { Vector3 acceleration = new Vector3(0, -_gravity, 0); Velocity += acceleration; Velocity *= _airMultiplier; if (Velocity.Y < _terminalVelocity) { Velocity.Y = _terminalVelocity; } } else { Velocity = new Vector3(Velocity.X * _groundMultiplier, 0f, Velocity.Z * _groundMultiplier); } UpdateOnGround(); } public void CheckAndResolveCollisions(ref Vector3 position, Vector3 movement) { Vector3 originalPosition = position; // Try full movement first AABB futureBox = GetBoundingBoxAt(originalPosition + movement); if (!HasCollision(futureBox)) { // No collision, apply full movement position = originalPosition + movement; return; } // Collision detected, resolve each axis separately but independently position = originalPosition; // Resolve Y collision (always first for ground detection) 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); } 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); } } private void ResolveYCollisionIndependent(ref Vector3 position, float velocityY) { if (velocityY == 0) return; Vector3 testPos = new Vector3(position.X, position.Y + velocityY, position.Z); AABB testBox = GetBoundingBoxAt(testPos); if (HasCollision(testBox)) { if (velocityY > 0) // Hitting ceiling { float ceilingY = GetCeilingHeight(testBox); position.Y = ceilingY - (Height / 2) - COLLISION_EPSILON; Velocity.Y = 0; } else // Hitting floor { float floorY = GetFloorHeight(testBox); position.Y = floorY + (Height / 2) + COLLISION_EPSILON; Velocity.Y = 0; OnGround = true; } } else { position.Y += velocityY; } } private void ResolveXCollisionIndependent(ref Vector3 position, float velocityX) { 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 direction = Math.Sign(velocityX); float checkDistance = Math.Abs(velocityX) + COLLISION_EPSILON; for (float offset = 0; offset <= checkDistance; offset += 0.01f) { float testX = position.X + (offset * direction); AABB testBox = GetBoundingBoxAt(new Vector3(testX, position.Y, position.Z)); if (HasCollision(testBox)) { 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; } } // 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 direction = Math.Sign(velocityZ); float checkDistance = Math.Abs(velocityZ) + COLLISION_EPSILON; for (float offset = 0; offset <= checkDistance; offset += 0.01f) { float testZ = position.Z + (offset * direction); AABB testBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, testZ)); if (HasCollision(testBox)) { 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; } } // 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); float highestFloor = float.MinValue; for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { Blocks block = _world.GetBlock(x, checkY, z); if (block != Blocks.Air) { highestFloor = Math.Max(highestFloor, checkY + 1); // Top of the block } } } return highestFloor != float.MinValue ? highestFloor : box.Min.Y; } 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); float lowestCeiling = float.MaxValue; for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { Blocks block = _world.GetBlock(x, checkY, z); if (block != Blocks.Air) { lowestCeiling = Math.Min(lowestCeiling, checkY); // Bottom of the block } } } return lowestCeiling != float.MaxValue ? lowestCeiling : box.Max.Y; } 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); float rightmostWall = float.MinValue; for (int y = minY; y <= maxY; y++) { for (int z = minZ; z <= maxZ; z++) { Blocks block = _world.GetBlock(checkX, y, z); if (block != Blocks.Air) { rightmostWall = Math.Max(rightmostWall, checkX + 1); // Right side of the block } } } return rightmostWall != float.MinValue ? rightmostWall : box.Min.X; } 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); float leftmostWall = float.MaxValue; for (int y = minY; y <= maxY; y++) { for (int z = minZ; z <= maxZ; z++) { Blocks block = _world.GetBlock(checkX, y, z); if (block != Blocks.Air) { leftmostWall = Math.Min(leftmostWall, checkX); // Left side of the block } } } return leftmostWall != float.MaxValue ? leftmostWall : box.Max.X; } 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); float frontmostWall = float.MinValue; for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { Blocks block = _world.GetBlock(x, y, checkZ); if (block != Blocks.Air) { frontmostWall = Math.Max(frontmostWall, checkZ + 1); // Front side of the block } } } return frontmostWall != float.MinValue ? frontmostWall : box.Min.Z; } 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); float backmostWall = float.MaxValue; for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { Blocks block = _world.GetBlock(x, y, checkZ); if (block != Blocks.Air) { backmostWall = Math.Min(backmostWall, checkZ); // Back side of the block } } } return backmostWall != float.MaxValue ? backmostWall : box.Max.Z; } 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); for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { for (int z = minZ; z <= maxZ; z++) { Blocks block = _world.GetBlock(x, y, z); if (block != Blocks.Air) { return true; } } } } return false; } public void ApplyImpulse(Vector3 force) { Velocity += force; } public AABB GetBoundingBox() { return GetBoundingBoxAt(Position); } public AABB GetBoundingBoxAt(Vector3 position) { float halfWidth = Width / 2; float halfHeight = Height / 2; Vector3 min = new Vector3( position.X - halfWidth, position.Y - halfHeight, // Center Y minus half height position.Z - halfWidth ); Vector3 max = new Vector3( position.X + halfWidth, position.Y + halfHeight, // Center Y plus half height position.Z + halfWidth ); return new AABB(min, max); } public void UpdateOnGround() { AABB box = GetBoundingBox(); float yCheck = box.Min.Y - 0.05f; 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); 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); if (block != Blocks.Air) { OnGround = true; return; } } } } } }