From bd2c87ddd17d6e59a1a3f30805f0bb663ada1564 Mon Sep 17 00:00:00 2001 From: Max Westerlund Date: Wed, 1 Oct 2025 00:40:47 +0200 Subject: [PATCH] player and entity physics improvements --- Camera.cs | 2 +- Entity.cs | 418 ++++++++++++++++++++++++++++++++++++++++++++++------ Game.cs | 13 -- Player.cs | 81 ++++++---- Program.cs | 2 +- Window.cs | 21 ++- Worldgen.cs | 12 +- 7 files changed, 444 insertions(+), 105 deletions(-) delete mode 100644 Game.cs diff --git a/Camera.cs b/Camera.cs index c910dd0..d95383c 100644 --- a/Camera.cs +++ b/Camera.cs @@ -7,7 +7,7 @@ namespace Voxel public static Vector3 Position = new Vector3(-8, 16, -8); public static float Pitch = -22.5f; - public static float Yaw = 45f; + public static float Yaw = 0f; public static float FOV = 60f; public static float Speed = 5f; public static float ShiftSpeed = 20f; diff --git a/Entity.cs b/Entity.cs index 3f198a3..c7e1c3c 100644 --- a/Entity.cs +++ b/Entity.cs @@ -17,89 +17,412 @@ namespace Voxel public float Width; public float Height; - private float _airDrag = 2f; - private float _groundDrag = 8f; + private float _gravity = 0.08f; + private float _terminalVelocity = -3.92f; + private float _airMultiplier = 0.98f; + private float _groundMultiplier = 0.91f; - private World _world; + 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(float deltaTime) + public void Tick() { - Console.WriteLine(OnGround); + Vector3 desiredMovement = Velocity; + Vector3 newPosition = Position; + + CheckAndResolveCollisions(ref newPosition, desiredMovement); + + Position = newPosition; if (!OnGround) { - Console.WriteLine("Velocity: " + Velocity.Y.ToString()); - - Velocity -= new Vector3( - Velocity.X * _airDrag * deltaTime, - 1.6f * deltaTime, - Velocity.Z * _airDrag * deltaTime); + Vector3 acceleration = new Vector3(0, -_gravity, 0); + Velocity = (Velocity + acceleration) * _airMultiplier; + if (Velocity.Y < _terminalVelocity) + { + Velocity.Y = _terminalVelocity; + } } else { - Velocity = new Vector3(Velocity.X, 0, Velocity.Z); - Velocity -= new Vector3( - Velocity.X * _groundDrag * deltaTime, - 0, - Velocity.Z * _groundDrag * deltaTime); + Velocity = new Vector3( + Velocity.X * _groundMultiplier, + 0f, + Velocity.Z * _groundMultiplier + ); } UpdateOnGround(); + } - Position += Velocity; - Console.WriteLine("Position: " + Position.Y.ToString()); + 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 - Height, - Position.Z - halfWidth + position.X - halfWidth, + position.Y - halfHeight, // Center Y minus half height + position.Z - halfWidth ); Vector3 max = new Vector3( - Position.X + halfWidth, - Position.Y + Height, - Position.Z + halfWidth + position.X + halfWidth, + position.Y + halfHeight, // Center Y plus half height + position.Z + halfWidth ); return new AABB(min, max); } - private bool IsCollidingSide(Vector3 direction) - { - AABB box = GetBoundingBox(); - - // Determine which axis we’re checking - int checkX = direction.X > 0 ? (int)MathF.Floor(box.Max.X) : (int)MathF.Floor(box.Min.X); - int checkZ = direction.Z > 0 ? (int)MathF.Floor(box.Max.Z) : (int)MathF.Floor(box.Min.Z); - - int minY = (int)MathF.Floor(box.Min.Y); - int maxY = (int)MathF.Floor(box.Max.Y); - - // Sweep along vertical range - for (int y = minY; y <= maxY; y++) - { - if (_world.GetBlock(checkX, y, checkZ) != Blocks.Air) - return true; - } - - return false; - } - public void UpdateOnGround() { AABB box = GetBoundingBox(); @@ -124,6 +447,5 @@ namespace Voxel } } } - } -} +} \ No newline at end of file diff --git a/Game.cs b/Game.cs deleted file mode 100644 index 0ba4fc4..0000000 --- a/Game.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Voxel -{ - public static class Game - { - - } -} diff --git a/Player.cs b/Player.cs index 59bf1a4..8c1a192 100644 --- a/Player.cs +++ b/Player.cs @@ -11,11 +11,15 @@ namespace Voxel { public class Player : Entity { - private World _world; public double lastClick = 0; public readonly float mouseCooldown = 0.2f; + private int _blockIndex = 0; - private Blocks _selectedBlock = Blocks.Dirt; + private double _tickTime = 0; + private bool _isJumping = false; + + private Blocks _selectedBlock = Blocks.OakPlanks; + private Vector3 previousPosition; public Player(Vector3 startPos, World world) : base(startPos, 0.5f, 1.8f, world) { @@ -42,20 +46,26 @@ namespace Voxel _world.SetBlock(x, y, z, Blocks.Air); } - public void Update(float deltaTime) + public void Tick() { - Camera.Position = Position + Vector3.UnitY * 0.5f; - Rotation = Camera.Yaw; + previousPosition = Position; - if (lastClick > 0) - { - lastClick -= deltaTime; - if (lastClick < 0) lastClick = 0; - } + bool sprinting = Input.GetKey(Keys.LeftControl); - if (!Input.GetMouseButton(MouseButton.Right) && !Input.GetMouseButton(MouseButton.Left)) + if (Input.GetKey(Keys.W)) + ApplyWalkSpeed(0, -1, sprinting); + if (Input.GetKey(Keys.S)) + ApplyWalkSpeed(0, 1, sprinting); + if (Input.GetKey(Keys.A)) + ApplyWalkSpeed(-1, 0, false); + if (Input.GetKey(Keys.D)) + ApplyWalkSpeed(1, 0, false); + + if (Input.GetKey(Keys.Space) && OnGround) { - lastClick = 0; + Console.WriteLine("Jump"); + Velocity = new Vector3(Velocity.X, 0.42f, Velocity.Z); + OnGround = false; } if (Input.GetMouseButton(MouseButton.Right) && lastClick == 0) @@ -72,32 +82,47 @@ namespace Voxel BreakBlock(); } - if (Input.GetKey(Keys.W)) - ApplyLocalVelocity(0, -5 * deltaTime); + base.Tick(); - if (Input.GetKey(Keys.Space) && OnGround) + Console.WriteLine(Velocity.X.ToString() + ", " + Velocity.Y.ToString() + ", " + Velocity.Z.ToString()); + } + + public void Update(float deltaTime, float alpha) + { + Camera.Position = Vector3.Lerp(previousPosition, Position, alpha) + Vector3.UnitY * 0.62f; + Rotation = Camera.Yaw; + + if (lastClick > 0) { - Velocity = new Vector3(Velocity.X, 0.5f, Velocity.Z); - OnGround = false; - Console.WriteLine("Jump"); + lastClick -= deltaTime; + if (lastClick < 0) lastClick = 0; + } + + if (!Input.GetMouseButton(MouseButton.Right) && !Input.GetMouseButton(MouseButton.Left)) + { + lastClick = 0; } } - public void ApplyLocalVelocity(float x, float z) + public void ApplyWalkSpeed(float x, float z, bool sprinting) { - Vector3 localVelocity = new Vector3(x, 0, z); + Vector3 inputDir = new Vector3(x, 0, z); + if (inputDir.LengthSquared > 0) + inputDir = Vector3.Normalize(inputDir); - float yaw = MathHelper.DegreesToRadians(Rotation); - float cos = MathF.Cos(yaw); // yaw in radians - float sin = MathF.Sin(yaw); + float yawRad = MathHelper.DegreesToRadians(Rotation); + float cos = MathF.Cos(yawRad); + float sin = MathF.Sin(yawRad); - Vector3 worldVelocity = new Vector3( - localVelocity.X * cos - localVelocity.Z * sin, - Velocity.Y, - localVelocity.X * sin + localVelocity.Z * cos + Vector3 worldDir = new Vector3( + inputDir.X * cos - inputDir.Z * sin, + 0, + inputDir.X * sin + inputDir.Z * cos ); - Velocity = worldVelocity; + float speed = sprinting ? 0.13f : 0.10f; + + Velocity += worldDir * speed; } public void SwitchBlock(bool inverted) diff --git a/Program.cs b/Program.cs index c28eced..7ffa0a7 100644 --- a/Program.cs +++ b/Program.cs @@ -45,7 +45,7 @@ internal class Program Vector3 startPos = new Vector3(15, 64, 15); Player player = new Player(startPos, world); - window.Update += player.Tick; + window.Tick += player.Tick; window.Update += player.Update; window.Run(); diff --git a/Window.cs b/Window.cs index c452f04..fc99a8c 100644 --- a/Window.cs +++ b/Window.cs @@ -12,13 +12,24 @@ namespace Voxel public readonly int Height = height; public uint frames = 0; public double timeElapsed = 0; - public event Action Update; + + public event Action Update; + public event Action Tick; + + private double _tickTime; + public const double TICK_LENGTH = 1.0 / 20.0; // 20 TPS protected override void OnUpdateFrame(FrameEventArgs e) { base.OnUpdateFrame(e); - float deltaTime = (float)e.Time; + _tickTime += e.Time; + + while (_tickTime >= TICK_LENGTH) + { + _tickTime -= TICK_LENGTH; + Tick(); // run exactly once per tick + } if (Input.GetKey(Keys.Escape)) { @@ -32,8 +43,6 @@ namespace Voxel else WindowState = WindowState.Normal; } - - Update.Invoke(deltaTime); } protected override void OnRenderFrame(FrameEventArgs e) @@ -44,6 +53,8 @@ namespace Voxel frames++; timeElapsed += e.Time; + float alpha = (float)(_tickTime / Window.TICK_LENGTH); + float deltaTime = (float)e.Time; if (timeElapsed >= 1) { Console.WriteLine("FPS: " + frames.ToString()); @@ -51,6 +62,8 @@ namespace Voxel frames = 0; } + Update.Invoke(deltaTime, alpha); + Renderer.Render(); SwapBuffers(); diff --git a/Worldgen.cs b/Worldgen.cs index 9715791..7b8b898 100644 --- a/Worldgen.cs +++ b/Worldgen.cs @@ -15,14 +15,11 @@ namespace Voxel private static float amplitude1 = 4; private static float amplitude2 = 2; - private static float mountainMapRes = (float)1/2; - private static float mountainMapAmplitude = 8; - private static float elevationMapRes = (float)1/8; private static float elevationMapAmplitude = 32; - private static FastNoiseLite noise = new FastNoiseLite(); private static Random random = new Random(); + private static FastNoiseLite noise = new FastNoiseLite(random.Next()); public static int GetHeight(int x, int z) { @@ -35,16 +32,11 @@ namespace Voxel z * res2 ) * amplitude2; - float mountainMap = noise.GetNoise( - x * mountainMapRes, - z * mountainMapRes - ) * mountainMapAmplitude; - float elevationMap = (noise.GetNoise( x * elevationMapRes, z * elevationMapRes ) + 0.25f) * elevationMapAmplitude; - return baseHeight + (int)(elevationMap + ((map1 + map2) * 1 + mountainMap)); + return baseHeight + (int)(elevationMap + map1 + map2); } public static Blocks GetBlock(int y, int maxY)