|
@@ -2,6 +2,7 @@ using Microsoft.Xna.Framework;
|
|
|
using Microsoft.Xna.Framework.Input;
|
|
using Microsoft.Xna.Framework.Input;
|
|
|
using Shooter.Core.Components;
|
|
using Shooter.Core.Components;
|
|
|
using Shooter.Core.Services;
|
|
using Shooter.Core.Services;
|
|
|
|
|
+using Shooter.Core.Plugins.Physics;
|
|
|
using System.Numerics;
|
|
using System.Numerics;
|
|
|
|
|
|
|
|
namespace Shooter.Gameplay.Components;
|
|
namespace Shooter.Gameplay.Components;
|
|
@@ -41,32 +42,68 @@ namespace Shooter.Gameplay.Components;
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public class FirstPersonController : Core.Components.EntityComponent
|
|
public class FirstPersonController : Core.Components.EntityComponent
|
|
|
{
|
|
{
|
|
|
|
|
+ // Stance system
|
|
|
|
|
+ public enum PlayerStance { Standing, Crouching }
|
|
|
|
|
+ private PlayerStance _stance = PlayerStance.Standing;
|
|
|
|
|
+ public PlayerStance Stance => _stance;
|
|
|
|
|
+
|
|
|
|
|
+ // Camera bobbing
|
|
|
|
|
+ private float _bobTimer = 0f;
|
|
|
|
|
+ private float _bobFrequency = 7.5f; // Bobbing speed
|
|
|
|
|
+ private float _bobAmplitude = 0.05f; // Bobbing height
|
|
|
|
|
+ private float _bobSmoothing = 8.0f; // Smoothing for transition
|
|
|
|
|
+
|
|
|
|
|
+ // Death zone settings
|
|
|
|
|
+ private float _killHeight = -50.0f; // Y position below which player dies
|
|
|
|
|
+ // Fall damage settings
|
|
|
|
|
+ private float _fallDamageThreshold = -12.0f; // Minimum Y velocity to trigger damage
|
|
|
|
|
+ private float _fallDamageMultiplier = 5.0f; // Damage per unit of velocity over threshold
|
|
|
|
|
+ private float _lastVerticalVelocity = 0f;
|
|
|
|
|
+ private bool _wasGroundedLastFrame = true;
|
|
|
// Movement settings
|
|
// Movement settings
|
|
|
private float _moveSpeed = 5.0f;
|
|
private float _moveSpeed = 5.0f;
|
|
|
private float _sprintMultiplier = 1.8f;
|
|
private float _sprintMultiplier = 1.8f;
|
|
|
private float _jumpForce = 8.0f;
|
|
private float _jumpForce = 8.0f;
|
|
|
-
|
|
|
|
|
|
|
+ private float _crouchSpeedMultiplier = 0.5f; // Crouch movement speed
|
|
|
|
|
+ private float _crouchHeight = 0.9f; // Camera height when crouched
|
|
|
|
|
+ private float _standHeight = 1.6f; // Camera height when standing
|
|
|
|
|
+ private float _crouchTransitionSpeed = 8.0f; // How fast camera/capsule transitions
|
|
|
|
|
+
|
|
|
// Look settings
|
|
// Look settings
|
|
|
private float _mouseSensitivity = 0.15f;
|
|
private float _mouseSensitivity = 0.15f;
|
|
|
private float _maxPitchAngle = 89f; // Prevent looking straight up/down (gimbal lock)
|
|
private float _maxPitchAngle = 89f; // Prevent looking straight up/down (gimbal lock)
|
|
|
private float _cameraHeightOffset = 1.6f; // Camera height above player position (head height)
|
|
private float _cameraHeightOffset = 1.6f; // Camera height above player position (head height)
|
|
|
private bool _invertMouseY = false; // Invert Y-axis for mouse look
|
|
private bool _invertMouseY = false; // Invert Y-axis for mouse look
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// State
|
|
// State
|
|
|
private float _currentYaw = 0f; // Horizontal rotation (left/right)
|
|
private float _currentYaw = 0f; // Horizontal rotation (left/right)
|
|
|
private float _currentPitch = 0f; // Vertical rotation (up/down)
|
|
private float _currentPitch = 0f; // Vertical rotation (up/down)
|
|
|
private bool _isGrounded = false;
|
|
private bool _isGrounded = false;
|
|
|
private bool _mouseCaptured = false;
|
|
private bool _mouseCaptured = false;
|
|
|
private bool _firstUpdate = true; // Skip input checks on first frame
|
|
private bool _firstUpdate = true; // Skip input checks on first frame
|
|
|
-
|
|
|
|
|
|
|
+ private bool _isCrouching = false;
|
|
|
|
|
+ private float _targetCameraHeight = 1.6f;
|
|
|
|
|
+ private float _currentCameraHeight = 1.6f;
|
|
|
|
|
+
|
|
|
|
|
+ // Ground detection
|
|
|
|
|
+ private float _groundCheckDistance = 0.1f; // Distance to check below player for ground
|
|
|
|
|
+ private float _groundCheckRadius = 0.3f; // Radius of sphere for ground check
|
|
|
|
|
+
|
|
|
// Cached components
|
|
// Cached components
|
|
|
private Camera? _camera;
|
|
private Camera? _camera;
|
|
|
private Transform3D? _transform;
|
|
private Transform3D? _transform;
|
|
|
private Core.Components.Rigidbody? _rigidbody;
|
|
private Core.Components.Rigidbody? _rigidbody;
|
|
|
-
|
|
|
|
|
|
|
+ private IPhysicsProvider? _physicsProvider;
|
|
|
|
|
+
|
|
|
// Services
|
|
// Services
|
|
|
private IInputService? _inputService;
|
|
private IInputService? _inputService;
|
|
|
-
|
|
|
|
|
|
|
+ private IAudioService? _audioService;
|
|
|
|
|
+
|
|
|
|
|
+ // Footstep audio
|
|
|
|
|
+ private float _footstepTimer = 0f;
|
|
|
|
|
+ private float _footstepInterval = 0.45f; // Time between footsteps (seconds)
|
|
|
|
|
+ private bool _wasMovingLastFrame = false;
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Movement speed in units per second.
|
|
/// Movement speed in units per second.
|
|
|
/// Unity's CharacterController uses meters/second by default.
|
|
/// Unity's CharacterController uses meters/second by default.
|
|
@@ -76,7 +113,7 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
get => _moveSpeed;
|
|
get => _moveSpeed;
|
|
|
set => _moveSpeed = value;
|
|
set => _moveSpeed = value;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Sprint speed multiplier (applied when holding Shift).
|
|
/// Sprint speed multiplier (applied when holding Shift).
|
|
|
/// Default 1.8x means sprinting at 180% normal speed.
|
|
/// Default 1.8x means sprinting at 180% normal speed.
|
|
@@ -86,7 +123,7 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
get => _sprintMultiplier;
|
|
get => _sprintMultiplier;
|
|
|
set => _sprintMultiplier = value;
|
|
set => _sprintMultiplier = value;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Upward force applied when jumping.
|
|
/// Upward force applied when jumping.
|
|
|
/// Higher = jump higher. Typical range: 5-12.
|
|
/// Higher = jump higher. Typical range: 5-12.
|
|
@@ -96,7 +133,7 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
get => _jumpForce;
|
|
get => _jumpForce;
|
|
|
set => _jumpForce = value;
|
|
set => _jumpForce = value;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Mouse sensitivity for looking around.
|
|
/// Mouse sensitivity for looking around.
|
|
|
/// Lower = slower/smoother, Higher = faster/twitchy.
|
|
/// Lower = slower/smoother, Higher = faster/twitchy.
|
|
@@ -107,7 +144,7 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
get => _mouseSensitivity;
|
|
get => _mouseSensitivity;
|
|
|
set => _mouseSensitivity = value;
|
|
set => _mouseSensitivity = value;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Camera height offset above player position (simulates head height).
|
|
/// Camera height offset above player position (simulates head height).
|
|
|
/// Typical value: 1.6 for human height.
|
|
/// Typical value: 1.6 for human height.
|
|
@@ -117,7 +154,7 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
get => _cameraHeightOffset;
|
|
get => _cameraHeightOffset;
|
|
|
set => _cameraHeightOffset = value;
|
|
set => _cameraHeightOffset = value;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Whether to invert the Y-axis for mouse look (up moves camera down, down moves camera up).
|
|
/// Whether to invert the Y-axis for mouse look (up moves camera down, down moves camera up).
|
|
|
/// Some players prefer inverted controls.
|
|
/// Some players prefer inverted controls.
|
|
@@ -127,91 +164,225 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
get => _invertMouseY;
|
|
get => _invertMouseY;
|
|
|
set => _invertMouseY = value;
|
|
set => _invertMouseY = value;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Whether the mouse is currently captured (locked to center).
|
|
/// Whether the mouse is currently captured (locked to center).
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public bool IsMouseCaptured => _mouseCaptured;
|
|
public bool IsMouseCaptured => _mouseCaptured;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Initialize the controller.
|
|
/// Initialize the controller.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public override void Initialize()
|
|
public override void Initialize()
|
|
|
{
|
|
{
|
|
|
base.Initialize();
|
|
base.Initialize();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Cache components
|
|
// Cache components
|
|
|
_camera = Owner?.GetComponent<Camera>();
|
|
_camera = Owner?.GetComponent<Camera>();
|
|
|
_transform = Owner?.GetComponent<Transform3D>();
|
|
_transform = Owner?.GetComponent<Transform3D>();
|
|
|
_rigidbody = Owner?.GetComponent<Core.Components.Rigidbody>();
|
|
_rigidbody = Owner?.GetComponent<Core.Components.Rigidbody>();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Get services
|
|
// Get services
|
|
|
_inputService = ServiceLocator.Get<IInputService>();
|
|
_inputService = ServiceLocator.Get<IInputService>();
|
|
|
-
|
|
|
|
|
|
|
+ _physicsProvider = ServiceLocator.Get<IPhysicsProvider>();
|
|
|
|
|
+ _audioService = ServiceLocator.Get<IAudioService>();
|
|
|
|
|
+
|
|
|
// Initialize yaw/pitch to look forward along negative Z axis (MonoGame default)
|
|
// Initialize yaw/pitch to look forward along negative Z axis (MonoGame default)
|
|
|
_currentYaw = 180f; // 180° = looking down -Z axis
|
|
_currentYaw = 180f; // 180° = looking down -Z axis
|
|
|
_currentPitch = 0f; // 0° = looking straight ahead (not up/down)
|
|
_currentPitch = 0f; // 0° = looking straight ahead (not up/down)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Capture mouse by default for FPS games
|
|
// Capture mouse by default for FPS games
|
|
|
CaptureMouse();
|
|
CaptureMouse();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (_camera == null)
|
|
if (_camera == null)
|
|
|
{
|
|
{
|
|
|
Console.WriteLine("[FirstPersonController] WARNING: No Camera component found on entity. Mouse look will not work.");
|
|
Console.WriteLine("[FirstPersonController] WARNING: No Camera component found on entity. Mouse look will not work.");
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (_transform == null)
|
|
if (_transform == null)
|
|
|
{
|
|
{
|
|
|
Console.WriteLine("[FirstPersonController] ERROR: No Transform3D component found on entity. Movement will not work.");
|
|
Console.WriteLine("[FirstPersonController] ERROR: No Transform3D component found on entity. Movement will not work.");
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Update controller each frame.
|
|
/// Update controller each frame.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public override void Update(Core.Components.GameTime gameTime)
|
|
public override void Update(Core.Components.GameTime gameTime)
|
|
|
{
|
|
{
|
|
|
base.Update(gameTime);
|
|
base.Update(gameTime);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Death zone check
|
|
|
|
|
+ if (_transform != null && _transform.Position.Y < _killHeight)
|
|
|
|
|
+ {
|
|
|
|
|
+ KillPlayer();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (_inputService == null || _transform == null)
|
|
if (_inputService == null || _transform == null)
|
|
|
{
|
|
{
|
|
|
Console.WriteLine("[FPS] ERROR: InputService or Transform is null!");
|
|
Console.WriteLine("[FPS] ERROR: InputService or Transform is null!");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Skip input processing on first frame (prevents false Escape detection)
|
|
// Skip input processing on first frame (prevents false Escape detection)
|
|
|
if (_firstUpdate)
|
|
if (_firstUpdate)
|
|
|
{
|
|
{
|
|
|
_firstUpdate = false;
|
|
_firstUpdate = false;
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
|
|
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
|
|
|
-
|
|
|
|
|
|
|
+ // ...existing code...
|
|
|
// Handle mouse capture toggle (press Escape to release, click to recapture)
|
|
// Handle mouse capture toggle (press Escape to release, click to recapture)
|
|
|
HandleMouseCapture();
|
|
HandleMouseCapture();
|
|
|
-
|
|
|
|
|
// Only process input if mouse is captured (prevents moving when in menus)
|
|
// Only process input if mouse is captured (prevents moving when in menus)
|
|
|
if (_mouseCaptured)
|
|
if (_mouseCaptured)
|
|
|
{
|
|
{
|
|
|
|
|
+ // Handle crouch/stand input and smooth transition
|
|
|
|
|
+ HandleCrouch(deltaTime);
|
|
|
// Sync camera position with transform FIRST (before rotation calculations)
|
|
// Sync camera position with transform FIRST (before rotation calculations)
|
|
|
if (_camera != null)
|
|
if (_camera != null)
|
|
|
{
|
|
{
|
|
|
- // Offset camera to head height above player position
|
|
|
|
|
- _camera.Position = _transform.Position + new System.Numerics.Vector3(0, _cameraHeightOffset, 0);
|
|
|
|
|
|
|
+ // Smoothly interpolate camera height for crouch/stand
|
|
|
|
|
+ _currentCameraHeight = MathHelper.Lerp(_currentCameraHeight, _targetCameraHeight, deltaTime * _crouchTransitionSpeed);
|
|
|
|
|
+ float bobOffset = 0f;
|
|
|
|
|
+ if (IsPlayerMoving() && _isGrounded)
|
|
|
|
|
+ {
|
|
|
|
|
+ _bobTimer += deltaTime * _bobFrequency;
|
|
|
|
|
+ bobOffset = (float)Math.Sin(_bobTimer) * _bobAmplitude;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ // Smoothly reset bobbing when not moving
|
|
|
|
|
+ _bobTimer = MathHelper.Lerp(_bobTimer, 0f, deltaTime * _bobSmoothing);
|
|
|
|
|
+ bobOffset = 0f;
|
|
|
|
|
+ }
|
|
|
|
|
+ _camera.Position = _transform.Position + new System.Numerics.Vector3(0, _currentCameraHeight + bobOffset, 0);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
// Handle looking (mouse movement)
|
|
// Handle looking (mouse movement)
|
|
|
HandleMouseLook(deltaTime);
|
|
HandleMouseLook(deltaTime);
|
|
|
-
|
|
|
|
|
// Handle movement (WASD)
|
|
// Handle movement (WASD)
|
|
|
|
|
+ bool isMoving = IsPlayerMoving();
|
|
|
HandleMovement(deltaTime);
|
|
HandleMovement(deltaTime);
|
|
|
-
|
|
|
|
|
|
|
+ // Footstep audio logic
|
|
|
|
|
+ if (isMoving && _isGrounded)
|
|
|
|
|
+ {
|
|
|
|
|
+ _footstepTimer += deltaTime;
|
|
|
|
|
+ if (_footstepTimer >= _footstepInterval)
|
|
|
|
|
+ {
|
|
|
|
|
+ PlayFootstepSound();
|
|
|
|
|
+ _footstepTimer = 0f;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ _footstepTimer = 0f;
|
|
|
|
|
+ }
|
|
|
|
|
+ _wasMovingLastFrame = isMoving;
|
|
|
|
|
+ // Update ground detection
|
|
|
|
|
+ UpdateGroundDetection();
|
|
|
// Handle jumping (Space)
|
|
// Handle jumping (Space)
|
|
|
HandleJump();
|
|
HandleJump();
|
|
|
}
|
|
}
|
|
|
|
|
+ // Track vertical velocity for fall damage
|
|
|
|
|
+ if (_rigidbody != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ _lastVerticalVelocity = _rigidbody.Velocity.Y;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Detect landing and apply fall damage
|
|
|
|
|
+ if (!_wasGroundedLastFrame && _isGrounded)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_lastVerticalVelocity < _fallDamageThreshold)
|
|
|
|
|
+ {
|
|
|
|
|
+ float damage = MathF.Abs(_lastVerticalVelocity + _fallDamageThreshold) * _fallDamageMultiplier;
|
|
|
|
|
+ ApplyFallDamage(damage);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ _wasGroundedLastFrame = _isGrounded;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Kill the player if they fall below kill height. Replace with respawn or game over logic.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void KillPlayer()
|
|
|
|
|
+ {
|
|
|
|
|
+ // TODO: Integrate with game state/respawn system. For now, print to console.
|
|
|
|
|
+ Console.WriteLine($"[FPS] Player killed: fell below kill height {_killHeight}");
|
|
|
|
|
+ // Example: Owner?.GetComponent<Health>()?.Kill();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Apply fall damage to the player. Replace with health system integration.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void ApplyFallDamage(float damage)
|
|
|
|
|
+ {
|
|
|
|
|
+ // TODO: Integrate with health system. For now, print to console.
|
|
|
|
|
+ Console.WriteLine($"[FPS] Fall damage applied: {damage:0.0}");
|
|
|
|
|
+ // Example: Owner?.GetComponent<Health>()?.TakeDamage(damage);
|
|
|
|
|
+ }
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Handle crouch/stand toggle and smooth transition.
|
|
|
|
|
+ /// Ctrl to crouch, release to stand. Smoothly transitions camera/capsule.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void HandleCrouch(float deltaTime)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_inputService == null)
|
|
|
|
|
+ return;
|
|
|
|
|
+ // Hold Ctrl to crouch, release to stand
|
|
|
|
|
+ bool crouchKey = _inputService.IsKeyDown(Keys.LeftControl) || _inputService.IsKeyDown(Keys.RightControl);
|
|
|
|
|
+ if (crouchKey && _stance != PlayerStance.Crouching)
|
|
|
|
|
+ {
|
|
|
|
|
+ _stance = PlayerStance.Crouching;
|
|
|
|
|
+ _isCrouching = true;
|
|
|
|
|
+ _targetCameraHeight = _crouchHeight;
|
|
|
|
|
+ // TODO: Resize capsule/collider if needed
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (!crouchKey && _stance != PlayerStance.Standing)
|
|
|
|
|
+ {
|
|
|
|
|
+ _stance = PlayerStance.Standing;
|
|
|
|
|
+ _isCrouching = false;
|
|
|
|
|
+ _targetCameraHeight = _standHeight;
|
|
|
|
|
+ // TODO: Resize capsule/collider if needed
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Update ground detection using physics raycast.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// EDUCATIONAL NOTE - GROUND DETECTION:
|
|
|
|
|
+ /// Unity's CharacterController has built-in isGrounded property.
|
|
|
|
|
+ /// In MonoGame, we implement it ourselves using raycasts:
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// 1. Cast a ray downward from player position
|
|
|
|
|
+ /// 2. If it hits something within a small distance, we're grounded
|
|
|
|
|
+ /// 3. This prevents jumping while in air (bunny-hopping)
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// We use a small sphere cast instead of a single ray for more reliable
|
|
|
|
|
+ /// detection on uneven terrain.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void UpdateGroundDetection()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_physicsProvider == null || _transform == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ _isGrounded = true; // Assume grounded if no physics
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Cast downward from player position
|
|
|
|
|
+ System.Numerics.Vector3 origin = _transform.Position;
|
|
|
|
|
+ System.Numerics.Vector3 direction = new System.Numerics.Vector3(0, -1, 0); // Downward
|
|
|
|
|
+ float maxDistance = _groundCheckDistance;
|
|
|
|
|
+ // Use sphere cast for more reliable ground detection
|
|
|
|
|
+ // This catches edges and slopes better than a single ray
|
|
|
|
|
+ if (_physicsProvider.SphereCast(origin, _groundCheckRadius, direction, maxDistance, out var hit))
|
|
|
|
|
+ {
|
|
|
|
|
+ _isGrounded = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ _isGrounded = false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Handle mouse capture/release.
|
|
/// Handle mouse capture/release.
|
|
|
/// Press Escape to release mouse, click to recapture.
|
|
/// Press Escape to release mouse, click to recapture.
|
|
@@ -220,20 +391,20 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
{
|
|
{
|
|
|
if (_inputService == null)
|
|
if (_inputService == null)
|
|
|
return;
|
|
return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Release mouse on Escape key
|
|
// Release mouse on Escape key
|
|
|
if (_inputService.IsKeyPressed(Keys.Escape))
|
|
if (_inputService.IsKeyPressed(Keys.Escape))
|
|
|
{
|
|
{
|
|
|
ReleaseMouse();
|
|
ReleaseMouse();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Recapture on left click (when not captured)
|
|
// Recapture on left click (when not captured)
|
|
|
if (!_mouseCaptured && _inputService.IsMouseButtonPressed(MouseButton.Left))
|
|
if (!_mouseCaptured && _inputService.IsMouseButtonPressed(MouseButton.Left))
|
|
|
{
|
|
{
|
|
|
CaptureMouse();
|
|
CaptureMouse();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Handle mouse look (camera rotation).
|
|
/// Handle mouse look (camera rotation).
|
|
|
///
|
|
///
|
|
@@ -248,40 +419,40 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
{
|
|
{
|
|
|
if (_inputService == null || _camera == null)
|
|
if (_inputService == null || _camera == null)
|
|
|
return;
|
|
return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Get mouse movement delta
|
|
// Get mouse movement delta
|
|
|
var mouseDelta = _inputService.MouseDelta;
|
|
var mouseDelta = _inputService.MouseDelta;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Convert System.Numerics.Vector2 to XNA Vector2
|
|
// Convert System.Numerics.Vector2 to XNA Vector2
|
|
|
var mouseDeltaXna = new Microsoft.Xna.Framework.Vector2(mouseDelta.X, mouseDelta.Y);
|
|
var mouseDeltaXna = new Microsoft.Xna.Framework.Vector2(mouseDelta.X, mouseDelta.Y);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (mouseDeltaXna == Microsoft.Xna.Framework.Vector2.Zero)
|
|
if (mouseDeltaXna == Microsoft.Xna.Framework.Vector2.Zero)
|
|
|
return;
|
|
return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply sensitivity
|
|
// Apply sensitivity
|
|
|
float yawDelta = -mouseDeltaXna.X * _mouseSensitivity; // Negative X for correct left/right
|
|
float yawDelta = -mouseDeltaXna.X * _mouseSensitivity; // Negative X for correct left/right
|
|
|
float pitchDelta = mouseDeltaXna.Y * _mouseSensitivity; // Positive Y = look down (standard FPS)
|
|
float pitchDelta = mouseDeltaXna.Y * _mouseSensitivity; // Positive Y = look down (standard FPS)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply inversion if enabled
|
|
// Apply inversion if enabled
|
|
|
if (_invertMouseY)
|
|
if (_invertMouseY)
|
|
|
{
|
|
{
|
|
|
pitchDelta = -pitchDelta;
|
|
pitchDelta = -pitchDelta;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Accumulate rotation
|
|
// Accumulate rotation
|
|
|
_currentYaw += yawDelta;
|
|
_currentYaw += yawDelta;
|
|
|
_currentPitch += pitchDelta;
|
|
_currentPitch += pitchDelta;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Clamp pitch to prevent over-rotation (gimbal lock)
|
|
// Clamp pitch to prevent over-rotation (gimbal lock)
|
|
|
// This keeps you from looking more than 89° up or down
|
|
// This keeps you from looking more than 89° up or down
|
|
|
_currentPitch = Math.Clamp(_currentPitch, -_maxPitchAngle, _maxPitchAngle);
|
|
_currentPitch = Math.Clamp(_currentPitch, -_maxPitchAngle, _maxPitchAngle);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply rotation to camera
|
|
// Apply rotation to camera
|
|
|
// Yaw rotates around Y axis (left/right)
|
|
// Yaw rotates around Y axis (left/right)
|
|
|
// Pitch rotates around X axis (up/down)
|
|
// Pitch rotates around X axis (up/down)
|
|
|
_camera.Rotate(_currentYaw, _currentPitch);
|
|
_camera.Rotate(_currentYaw, _currentPitch);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Handle WASD movement.
|
|
/// Handle WASD movement.
|
|
|
///
|
|
///
|
|
@@ -304,10 +475,10 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
{
|
|
{
|
|
|
if (_inputService == null || _transform == null)
|
|
if (_inputService == null || _transform == null)
|
|
|
return;
|
|
return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Calculate movement direction based on input
|
|
// Calculate movement direction based on input
|
|
|
System.Numerics.Vector3 inputDirection = System.Numerics.Vector3.Zero;
|
|
System.Numerics.Vector3 inputDirection = System.Numerics.Vector3.Zero;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (_inputService.IsKeyDown(Keys.W))
|
|
if (_inputService.IsKeyDown(Keys.W))
|
|
|
inputDirection.Z -= 1f; // Forward
|
|
inputDirection.Z -= 1f; // Forward
|
|
|
if (_inputService.IsKeyDown(Keys.S))
|
|
if (_inputService.IsKeyDown(Keys.S))
|
|
@@ -316,47 +487,55 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
inputDirection.X -= 1f; // Left
|
|
inputDirection.X -= 1f; // Left
|
|
|
if (_inputService.IsKeyDown(Keys.D))
|
|
if (_inputService.IsKeyDown(Keys.D))
|
|
|
inputDirection.X += 1f; // Right
|
|
inputDirection.X += 1f; // Right
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// No movement input - early out
|
|
// No movement input - early out
|
|
|
if (inputDirection == System.Numerics.Vector3.Zero)
|
|
if (inputDirection == System.Numerics.Vector3.Zero)
|
|
|
return;
|
|
return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Normalize to prevent faster diagonal movement
|
|
// Normalize to prevent faster diagonal movement
|
|
|
inputDirection = System.Numerics.Vector3.Normalize(inputDirection);
|
|
inputDirection = System.Numerics.Vector3.Normalize(inputDirection);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Calculate forward and right vectors based on camera yaw
|
|
// Calculate forward and right vectors based on camera yaw
|
|
|
// We ignore pitch (vertical tilt) for ground-based movement
|
|
// We ignore pitch (vertical tilt) for ground-based movement
|
|
|
float yawRadians = _currentYaw * (MathF.PI / 180f);
|
|
float yawRadians = _currentYaw * (MathF.PI / 180f);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
System.Numerics.Vector3 forward = new System.Numerics.Vector3(
|
|
System.Numerics.Vector3 forward = new System.Numerics.Vector3(
|
|
|
MathF.Sin(yawRadians),
|
|
MathF.Sin(yawRadians),
|
|
|
0,
|
|
0,
|
|
|
MathF.Cos(yawRadians)
|
|
MathF.Cos(yawRadians)
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
System.Numerics.Vector3 right = new System.Numerics.Vector3(
|
|
System.Numerics.Vector3 right = new System.Numerics.Vector3(
|
|
|
MathF.Cos(yawRadians),
|
|
MathF.Cos(yawRadians),
|
|
|
0,
|
|
0,
|
|
|
-MathF.Sin(yawRadians)
|
|
-MathF.Sin(yawRadians)
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Combine input with camera-relative directions
|
|
// Combine input with camera-relative directions
|
|
|
- System.Numerics.Vector3 moveDirection =
|
|
|
|
|
|
|
+ System.Numerics.Vector3 moveDirection =
|
|
|
(forward * -inputDirection.Z) + // Forward/back (inverted Z)
|
|
(forward * -inputDirection.Z) + // Forward/back (inverted Z)
|
|
|
(right * inputDirection.X); // Left/right
|
|
(right * inputDirection.X); // Left/right
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply speed
|
|
// Apply speed
|
|
|
float speed = _moveSpeed;
|
|
float speed = _moveSpeed;
|
|
|
-
|
|
|
|
|
|
|
+ // Stance modifier
|
|
|
|
|
+ if (_stance == PlayerStance.Crouching)
|
|
|
|
|
+ {
|
|
|
|
|
+ speed *= _crouchSpeedMultiplier;
|
|
|
|
|
+ }
|
|
|
// Sprint modifier (hold Shift)
|
|
// Sprint modifier (hold Shift)
|
|
|
if (_inputService.IsKeyDown(Keys.LeftShift) || _inputService.IsKeyDown(Keys.RightShift))
|
|
if (_inputService.IsKeyDown(Keys.LeftShift) || _inputService.IsKeyDown(Keys.RightShift))
|
|
|
{
|
|
{
|
|
|
speed *= _sprintMultiplier;
|
|
speed *= _sprintMultiplier;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+ // Air control: reduce movement speed if not grounded
|
|
|
|
|
+ if (!_isGrounded)
|
|
|
|
|
+ {
|
|
|
|
|
+ speed *= 0.35f; // Air control multiplier (tweakable)
|
|
|
|
|
+ }
|
|
|
// Calculate velocity
|
|
// Calculate velocity
|
|
|
System.Numerics.Vector3 velocity = moveDirection * speed;
|
|
System.Numerics.Vector3 velocity = moveDirection * speed;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply movement
|
|
// Apply movement
|
|
|
if (_rigidbody != null)
|
|
if (_rigidbody != null)
|
|
|
{
|
|
{
|
|
@@ -375,7 +554,7 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
_transform.Position += velocity * deltaTime;
|
|
_transform.Position += velocity * deltaTime;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Handle jumping (Space bar).
|
|
/// Handle jumping (Space bar).
|
|
|
///
|
|
///
|
|
@@ -389,18 +568,15 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
{
|
|
{
|
|
|
if (_inputService == null)
|
|
if (_inputService == null)
|
|
|
return;
|
|
return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Check for jump input
|
|
// Check for jump input
|
|
|
if (!_inputService.IsKeyPressed(Keys.Space))
|
|
if (!_inputService.IsKeyPressed(Keys.Space))
|
|
|
return;
|
|
return;
|
|
|
-
|
|
|
|
|
- // TODO: Implement proper ground detection
|
|
|
|
|
- // For now, assume always grounded (will fix in Phase 2 polish)
|
|
|
|
|
- _isGrounded = true;
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Check if grounded (now properly detected via raycast)
|
|
|
if (!_isGrounded)
|
|
if (!_isGrounded)
|
|
|
return; // Can't jump in air
|
|
return; // Can't jump in air
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply jump force
|
|
// Apply jump force
|
|
|
if (_rigidbody != null)
|
|
if (_rigidbody != null)
|
|
|
{
|
|
{
|
|
@@ -414,7 +590,7 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
_transform.Position += new System.Numerics.Vector3(0, _jumpForce * 0.1f, 0);
|
|
_transform.Position += new System.Numerics.Vector3(0, _jumpForce * 0.1f, 0);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Capture the mouse (lock to center, hide cursor).
|
|
/// Capture the mouse (lock to center, hide cursor).
|
|
|
/// Standard for FPS games.
|
|
/// Standard for FPS games.
|
|
@@ -422,19 +598,19 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
public void CaptureMouse()
|
|
public void CaptureMouse()
|
|
|
{
|
|
{
|
|
|
_mouseCaptured = true;
|
|
_mouseCaptured = true;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (_inputService != null)
|
|
if (_inputService != null)
|
|
|
{
|
|
{
|
|
|
_inputService.IsMouseLocked = true;
|
|
_inputService.IsMouseLocked = true;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
Microsoft.Xna.Framework.Input.Mouse.SetPosition(
|
|
Microsoft.Xna.Framework.Input.Mouse.SetPosition(
|
|
|
Microsoft.Xna.Framework.Graphics.GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width / 2,
|
|
Microsoft.Xna.Framework.Graphics.GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width / 2,
|
|
|
Microsoft.Xna.Framework.Graphics.GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height / 2
|
|
Microsoft.Xna.Framework.Graphics.GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height / 2
|
|
|
);
|
|
);
|
|
|
// TODO: Hide cursor (MonoGame doesn't have built-in API, needs platform-specific code)
|
|
// TODO: Hide cursor (MonoGame doesn't have built-in API, needs platform-specific code)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Release the mouse (unlock from center, show cursor).
|
|
/// Release the mouse (unlock from center, show cursor).
|
|
|
/// Used for menus and UI interaction.
|
|
/// Used for menus and UI interaction.
|
|
@@ -442,15 +618,15 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
public void ReleaseMouse()
|
|
public void ReleaseMouse()
|
|
|
{
|
|
{
|
|
|
_mouseCaptured = false;
|
|
_mouseCaptured = false;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (_inputService != null)
|
|
if (_inputService != null)
|
|
|
{
|
|
{
|
|
|
_inputService.IsMouseLocked = false;
|
|
_inputService.IsMouseLocked = false;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// TODO: Show cursor
|
|
// TODO: Show cursor
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Set the camera rotation directly.
|
|
/// Set the camera rotation directly.
|
|
|
/// Useful for cutscenes or resetting view.
|
|
/// Useful for cutscenes or resetting view.
|
|
@@ -459,10 +635,31 @@ public class FirstPersonController : Core.Components.EntityComponent
|
|
|
{
|
|
{
|
|
|
_currentYaw = yaw;
|
|
_currentYaw = yaw;
|
|
|
_currentPitch = Math.Clamp(pitch, -_maxPitchAngle, _maxPitchAngle);
|
|
_currentPitch = Math.Clamp(pitch, -_maxPitchAngle, _maxPitchAngle);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (_camera != null)
|
|
if (_camera != null)
|
|
|
{
|
|
{
|
|
|
_camera.Rotate(_currentYaw, _currentPitch);
|
|
_camera.Rotate(_currentYaw, _currentPitch);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Returns true if player is moving (WASD keys held)
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private bool IsPlayerMoving()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_inputService == null)
|
|
|
|
|
+ return false;
|
|
|
|
|
+ return _inputService.IsKeyDown(Keys.W) || _inputService.IsKeyDown(Keys.A) || _inputService.IsKeyDown(Keys.S) || _inputService.IsKeyDown(Keys.D);
|
|
|
|
|
+ }
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Play footstep sound using audio service
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void PlayFootstepSound()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_audioService != null && _transform != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ // TODO: Use surface type for varied sounds
|
|
|
|
|
+ _audioService.PlaySound("footstep", _transform.Position, 1.0f);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|