Files
voxel/Entity.cs
2025-10-07 10:37:46 +02:00

449 lines
15 KiB
C#

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;
}
}
}
}
}
}