Files
voxel/Entity.cs
2026-03-24 00:14:17 +01:00

448 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 _yMultiplier = 0.98f;
private float _groundMultiplier = 0.6f;
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;
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.X *= _airMultiplier;
Velocity.Z *= _airMultiplier;
Velocity.Y *= _yMultiplier;
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 independently
position = originalPosition;
// Resolve Y collision first
ResolveYCollisionIndependent(ref position, movement.Y);
// Resolve X collision
ResolveXCollisionIndependent(ref position, movement.X);
// Resolve Z collision
ResolveZCollisionIndependent(ref position, movement.Z);
// After moving horizontally, reresolve 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);
AABB testBox = GetBoundingBoxAt(testPos);
if (HasCollision(testBox))
{
if (velocityY > 0) // Hitting ceiling
{
float ceilingY = GetCeilingHeight(testBox);
position.Y = ceilingY - (Height / 2);
Velocity.Y = 0;
}
else // Hitting floor
{
float floorY = GetFloorHeight(testBox);
position.Y = floorY + (Height / 2);
Velocity.Y = 0;
OnGround = true;
}
}
else
{
position.Y += velocityY;
}
}
private void ResolveXCollisionIndependent(ref Vector3 position, float velocityX)
{
if (velocityX == 0) return;
float halfWidth = Width / 2;
float direction = Math.Sign(velocityX);
float targetX = position.X + velocityX;
// 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))
{
// Find the wall we hit
float wallX;
if (direction > 0) // Moving right
{
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;
}
}
private void ResolveZCollisionIndependent(ref Vector3 position, float velocityZ)
{
if (velocityZ == 0) return;
float halfWidth = Width / 2;
float direction = Math.Sign(velocityZ);
float targetZ = position.Z + velocityZ;
// 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))
{
// Find the wall we hit
float wallZ;
if (direction > 0) // Moving forward (+Z)
{
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;
}
}
private float GetFloorHeight(AABB box)
{
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;
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 = 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;
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 = 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;
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 = 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;
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 = 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;
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 = 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;
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 = 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++)
{
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,
position.Z - halfWidth
);
Vector3 max = new Vector3(
position.X + halfWidth,
position.Y + halfHeight,
position.Z + halfWidth
);
return new AABB(min, max);
}
public void UpdateOnGround()
{
AABB box = GetBoundingBox();
float yCheck = box.Min.Y - COLLISION_EPSILON;
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, y, z);
if (block != Blocks.Air)
{
OnGround = true;
return;
}
}
}
}
}
}