Browse Source

Make weapons swapable.

CartBlanche 2 weeks ago
parent
commit
0e0a7bce44

+ 157 - 0
Shooter/Gameplay/Components/WeaponViewModel.cs

@@ -0,0 +1,157 @@
+using Shooter.Core.Components;
+using Shooter.Core.Entities;
+using System.Numerics;
+
+namespace Shooter.Gameplay.Components;
+
+/// <summary>
+/// Attaches a weapon model to the camera for first-person view.
+///
+/// UNITY COMPARISON - FPS WEAPON VIEW MODELS:
+///
+/// Unity Approach:
+/// 1. Weapon is a child GameObject of the Camera
+/// 2. Uses Transform hierarchy: Camera → WeaponHolder → Weapon
+/// 3. Position is relative to camera (localPosition)
+/// 4. Automatically follows camera because it's a child
+///
+/// MonoGame Approach (This Component):
+/// 1. Weapon is a separate entity (no built-in parenting)
+/// 2. Must manually update weapon position every frame
+/// 3. Calculate world position from camera position + offset
+/// 4. Apply camera rotation to weapon
+///
+/// EDUCATIONAL NOTE - WHY NO PARENTING?
+///
+/// Unity's Transform hierarchy is convenient but has overhead:
+/// - Automatic matrix recalculation on every transform change
+/// - Traversing hierarchy to build world matrices
+/// - Memory overhead for parent/child relationships
+///
+/// MonoGame's explicit approach:
+/// - Only update when needed (performance control)
+/// - Explicit about what's happening (learning)
+/// - Can optimize for specific use cases (FPS viewmodel vs world object)
+///
+/// For FPS games, viewmodels have special requirements:
+/// - Different FOV than world (to prevent distortion)
+/// - Rendered on top of everything (depth buffer tricks)
+/// - Smooth independent movement (weapon sway, recoil)
+///
+/// This component handles the basic case: stick weapon to camera.
+/// Phase 3 will add: weapon sway, bobbing, recoil animations.
+/// </summary>
+public class WeaponViewModel : EntityComponent
+{
+    private Entity? _cameraEntity;
+    private Camera? _camera;
+    private Transform3D? _weaponTransform;
+
+    /// <summary>
+    /// Offset from camera position (right, down, forward in camera space).
+    /// Similar to Unity's localPosition when weapon is child of camera.
+    /// </summary>
+    public Vector3 ViewmodelOffset { get; set; } = new Vector3(0.5f, -0.5f, -1.5f);
+
+    /// <summary>
+    /// The camera entity to follow. Must be set before Initialize() is called.
+    /// </summary>
+    public Entity? CameraEntity { get; set; }
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        _weaponTransform = Owner?.GetComponent<Transform3D>();
+        _cameraEntity = CameraEntity;
+
+        if (_cameraEntity != null)
+        {
+            _camera = _cameraEntity.GetComponent<Camera>();
+        }
+
+        if (_camera == null)
+        {
+            Console.WriteLine("[WeaponViewModel] WARNING: No camera found! Weapon will not follow camera.");
+        }
+    }
+
+    public override void Update(Core.Components.GameTime gameTime)
+    {
+        base.Update(gameTime);
+
+        if (_camera == null || _weaponTransform == null || _cameraEntity?.Transform == null)
+            return;
+
+        // Get camera's position and direction
+        var cameraPos = _camera.Position;
+        var forward = Vector3.Normalize(_camera.Target - _camera.Position);
+
+        // Calculate right and up vectors for camera space
+        var worldUp = new Vector3(0, 1, 0);
+        var right = Vector3.Normalize(Vector3.Cross(worldUp, forward));
+        var up = Vector3.Normalize(Vector3.Cross(forward, right));
+
+        // Transform viewmodel offset from camera space to world space
+        // ViewmodelOffset is in camera space: (right, up, forward)
+        // Note: Negative Z in camera space means "in front of camera"
+        var worldOffset =
+            right * ViewmodelOffset.X +      // Right component
+            up * ViewmodelOffset.Y +          // Up component
+            forward * -ViewmodelOffset.Z;     // Forward component (negated!)
+
+        // Position weapon relative to camera
+        _weaponTransform.Position = cameraPos + worldOffset;
+
+        // Apply camera rotation to weapon
+        var cameraRot = QuaternionFromForward(forward);
+        _weaponTransform.Rotation = cameraRot;
+
+        // EDUCATIONAL NOTE - WHY THIS WORKS:
+        //
+        // Camera space offset (0.5, -0.5, -1.5) means:
+        // - 0.5 units to the RIGHT of camera
+        // - 0.5 units DOWN from camera
+        // - 1.5 units IN FRONT of camera (negative Z in camera forward direction)
+        //
+        // Vector3.Transform(offset, rotation) converts camera-relative offset
+        // to world-relative offset, accounting for where camera is looking.
+        //
+        // Example:
+        // - Camera looking NORTH (rotation = 0°):
+        //   Offset (0, 0, -1.5) → World offset (0, 0, -1.5) NORTH
+        //
+        // - Camera looking EAST (rotation = 90°):
+        //   Offset (0, 0, -1.5) → World offset (1.5, 0, 0) EAST
+        //
+        // This is equivalent to Unity's transform.TransformPoint(localPosition)!
+    }
+
+    /// <summary>
+    /// Create a quaternion from a forward direction vector.
+    /// This assumes the up vector is (0, 1, 0).
+    /// </summary>
+    private Quaternion QuaternionFromForward(Vector3 forward)
+    {
+        // Ensure forward is normalized
+        forward = Vector3.Normalize(forward);
+
+        // Calculate right vector (cross product of world up and forward)
+        var worldUp = new Vector3(0, 1, 0);
+        var right = Vector3.Normalize(Vector3.Cross(worldUp, forward));
+
+        // Recalculate up vector (cross product of forward and right)
+        var up = Vector3.Cross(forward, right);
+
+        // Build rotation matrix from basis vectors
+        var rotationMatrix = new System.Numerics.Matrix4x4(
+            right.X, right.Y, right.Z, 0,
+            up.X, up.Y, up.Z, 0,
+            forward.X, forward.Y, forward.Z, 0,
+            0, 0, 0, 1
+        );
+
+        // Convert matrix to quaternion
+        return Quaternion.CreateFromRotationMatrix(rotationMatrix);
+    }
+}

+ 1 - 12
Shooter/Graphics/ForwardGraphicsProvider.cs

@@ -231,19 +231,8 @@ public class ForwardGraphicsProvider : IGraphicsProvider
         // Draw each model
         foreach (var modelRenderer in modelRenderers)
         {
-            if (!modelRenderer.Visible)
-            {
-                Console.WriteLine($"[RenderModels] Skipping invisible model");
+            if (!modelRenderer.Visible || modelRenderer.Model == null)
                 continue;
-            }
-
-            if (modelRenderer.Model == null)
-            {
-                Console.WriteLine($"[RenderModels] Skipping null model");
-                continue;
-            }
-
-            Console.WriteLine($"[RenderModels] Drawing model with {modelRenderer.Model.Meshes.Count} meshes, scale={modelRenderer.Scale}");
 
             // ModelMeshRenderer handles its own drawing (it has MonoGame's BasicEffect embedded)
             modelRenderer.Draw(_graphicsDevice, Matrix.Identity, camera.ViewMatrix, camera.ProjectionMatrix);

+ 123 - 29
Shooter/Platforms/Desktop/Game.cs

@@ -365,39 +365,126 @@ public class ShooterGame : Game
     /// - Rotation: Slightly tilted for visual interest
     /// - Scale: Often enlarged because camera is so close
     /// </summary>
+    // Store weapon viewmodel entities for switching
+    private Core.Entities.Entity? _primaryWeaponViewModel;
+    private Core.Entities.Entity? _secondaryWeaponViewModel;
+    private int _lastWeaponIndex = -1;
+
     private void LoadWeaponModel(Core.Entities.Entity playerEntity)
     {
         try
         {
-            // Create a weapon entity as a "child" of the player
-            var weaponEntity = new Core.Entities.Entity("PlayerWeaponViewModel");
-            var weaponTransform = weaponEntity.AddComponent<Core.Components.Transform3D>();
-
-            // Position weapon in front of camera (right, down, forward relative to view)
-            // These values are tuned to match Unity FPS Microgame's weapon position
-            // Note: This is a static position for now - Phase 3 will attach to camera transform
-            // Camera at (0, 2, 10) looks toward -Z, so weapon at Z=9 is in front
-            weaponTransform.Position = playerEntity.Transform.Position + new System.Numerics.Vector3(0.5f, -0.5f, -1.5f);
-
-            // Add ModelMeshRenderer and load the weapon model
-            var weaponRenderer = weaponEntity.AddComponent<Core.Components.ModelMeshRenderer>();
-            weaponRenderer.LoadModel(Content, "Models/Mesh_Weapon_Primary");
-            weaponRenderer.Scale = 0.3f; // Much smaller - Unity models are often too large
-            weaponRenderer.TintColor = new System.Numerics.Vector4(1, 1, 1, 1); // No tint
-
-            // Add to scene
             var scene = _sceneManager?.ActiveScene;
+
+            // The camera is on the player entity, not a separate entity
+            var cameraEntity = playerEntity;
+
+            // Load PRIMARY weapon viewmodel (Pistol - slot 0)
+            _primaryWeaponViewModel = CreateWeaponViewModel(
+                "PrimaryWeaponViewModel",
+                "Models/Mesh_Weapon_Primary",
+                cameraEntity,
+                new System.Numerics.Vector3(0.5f, -0.5f, -1.5f),
+                0.3f);
+
+            // Load SECONDARY weapon viewmodel (Assault Rifle - slot 1)
+            _secondaryWeaponViewModel = CreateWeaponViewModel(
+                "SecondaryWeaponViewModel",
+                "Models/Mesh_Weapon_Secondary",
+                cameraEntity,
+                new System.Numerics.Vector3(0.5f, -0.5f, -1.5f),
+                0.3f);
+
+            // Add both to scene
             if (scene != null)
             {
-                scene.AddEntity(weaponEntity);
-                weaponEntity.Initialize();
-                Console.WriteLine("[LoadContent] Loaded weapon model 'Mesh_Weapon_Primary' for player view");
+                scene.AddEntity(_primaryWeaponViewModel);
+                _primaryWeaponViewModel.Initialize();
+
+                scene.AddEntity(_secondaryWeaponViewModel);
+                _secondaryWeaponViewModel.Initialize();
+
+                // Start with primary weapon visible
+                SetActiveWeaponViewModel(0);
+
+                Console.WriteLine("[LoadContent] Loaded both weapon viewmodels with camera tracking");
             }
         }
         catch (Exception ex)
         {
-            Console.WriteLine($"[LoadContent] Failed to load weapon model: {ex.Message}");
-            Console.WriteLine("  Make sure Content.mgcb has been built and Models/Mesh_Weapon_Primary.fbx is included");
+            Console.WriteLine($"[LoadContent] Failed to load weapon models: {ex.Message}");
+            Console.WriteLine("  Make sure Content.mgcb has been built and weapon FBX files are included");
+        }
+    }
+
+    /// <summary>
+    /// Helper to create a weapon viewmodel entity
+    /// </summary>
+    private Core.Entities.Entity CreateWeaponViewModel(
+        string name,
+        string modelPath,
+        Core.Entities.Entity? cameraEntity,
+        System.Numerics.Vector3 offset,
+        float scale)
+    {
+        var weaponEntity = new Core.Entities.Entity(name);
+        weaponEntity.AddComponent<Core.Components.Transform3D>();
+
+        var viewModel = weaponEntity.AddComponent<Gameplay.Components.WeaponViewModel>();
+        if (viewModel != null)
+        {
+            viewModel.CameraEntity = cameraEntity;
+            viewModel.ViewmodelOffset = offset;
+        }
+
+        var weaponRenderer = weaponEntity.AddComponent<Core.Components.ModelMeshRenderer>();
+        weaponRenderer.LoadModel(Content, modelPath);
+        weaponRenderer.Scale = scale;
+        weaponRenderer.TintColor = new System.Numerics.Vector4(1, 1, 1, 1);
+
+        return weaponEntity;
+    }
+
+    /// <summary>
+    /// Switch which weapon viewmodel is visible based on equipped weapon slot
+    /// </summary>
+    private void SetActiveWeaponViewModel(int weaponIndex)
+    {
+        // Hide all viewmodels first
+        if (_primaryWeaponViewModel != null)
+        {
+            var primaryRenderer = _primaryWeaponViewModel.GetComponent<Core.Components.ModelMeshRenderer>();
+            if (primaryRenderer != null)
+                primaryRenderer.Visible = false;
+        }
+
+        if (_secondaryWeaponViewModel != null)
+        {
+            var secondaryRenderer = _secondaryWeaponViewModel.GetComponent<Core.Components.ModelMeshRenderer>();
+            if (secondaryRenderer != null)
+                secondaryRenderer.Visible = false;
+        }
+
+        // Show the active weapon viewmodel
+        switch (weaponIndex)
+        {
+            case 0: // Primary weapon (Pistol)
+                if (_primaryWeaponViewModel != null)
+                {
+                    var primaryRenderer = _primaryWeaponViewModel.GetComponent<Core.Components.ModelMeshRenderer>();
+                    if (primaryRenderer != null)
+                        primaryRenderer.Visible = true;
+                }
+                break;
+
+            case 1: // Secondary weapon (Assault Rifle)
+                if (_secondaryWeaponViewModel != null)
+                {
+                    var secondaryRenderer = _secondaryWeaponViewModel.GetComponent<Core.Components.ModelMeshRenderer>();
+                    if (secondaryRenderer != null)
+                        secondaryRenderer.Visible = true;
+                }
+                break;
         }
     }
 
@@ -492,7 +579,19 @@ public class ShooterGame : Game
         );
         
         _sceneManager?.Update(coreGameTime);
-        
+
+        // Check for weapon switching (update viewmodel visibility)
+        var playerEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("Player").FirstOrDefault();
+        if (playerEntity != null)
+        {
+            var weaponController = playerEntity.GetComponent<Gameplay.Components.WeaponController>();
+            if (weaponController != null && weaponController.CurrentWeaponIndex != _lastWeaponIndex)
+            {
+                SetActiveWeaponViewModel(weaponController.CurrentWeaponIndex);
+                _lastWeaponIndex = weaponController.CurrentWeaponIndex;
+            }
+        }
+
         // Step physics simulation with fixed timestep
         // We use the scaled delta time from TimeService to support slow-motion/time effects
         // Guard against zero/negative timestep on first frame
@@ -567,16 +666,11 @@ public class ShooterGame : Game
                     var modelRenderer = entity.GetComponent<Core.Components.ModelMeshRenderer>();
                     if (modelRenderer != null)
                     {
-                        var pos = entity.Transform.Position;
-                        Console.WriteLine($"[Render] Found ModelMeshRenderer on '{entity.Name}' at ({pos.X:F2}, {pos.Y:F2}, {pos.Z:F2})");
                         modelRenderers.Add(modelRenderer);
                     }
                 }
             }
 
-            // Debug: Log what we're about to render
-            Console.WriteLine($"[Render] Found {renderables.Count} procedural meshes, {modelRenderers.Count} models");
-
             // Render the scene (procedural meshes first)
             graphics.RenderScene(camera, renderables);
 
@@ -663,4 +757,4 @@ public class ShooterGame : Game
         
         base.UnloadContent();
     }
-}
+}